diff --git a/.vscode/settings.json b/.vscode/settings.json index dad81f5..104150f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,6 +40,7 @@ "magick", "memfs", "nakedret", + "nicksnyder", "nolint", "nolintlint", "paramset", diff --git a/go.mod b/go.mod index 8164980..def5b90 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/nicksnyder/go-i18n/v2 v2.2.1 // indirect + github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/samber/lo v1.38.1 github.com/snivilised/extendio v0.3.0 github.com/spf13/viper v1.17.0 diff --git a/src/assistant/i18n/i18n-defs.go b/src/assistant/i18n/i18n-defs.go index 448296d..12306dd 100644 --- a/src/assistant/i18n/i18n-defs.go +++ b/src/assistant/i18n/i18n-defs.go @@ -1,5 +1,9 @@ package i18n +import ( + "github.com/nicksnyder/go-i18n/v2/i18n" +) + const CobrassSourceID = "github.com/snivilised/cobrass" // These definitions are in support of extendio's Localisable @@ -10,3 +14,5 @@ type CobrassTemplData struct{} func (td CobrassTemplData) SourceID() string { return CobrassSourceID } + +type Message = i18n.Message diff --git a/src/assistant/i18n/messages-command.go b/src/assistant/i18n/messages-command.go new file mode 100644 index 0000000..2e213e9 --- /dev/null +++ b/src/assistant/i18n/messages-command.go @@ -0,0 +1,113 @@ +package i18n + +// FilesGlobParamUsageTemplData +// ๐ŸงŠ +type FilesGlobParamUsageTemplData struct { + CobrassTemplData +} + +func (td FilesGlobParamUsageTemplData) Message() *Message { + return &Message{ + ID: "files-glob-filter.param-usage", + Description: "files glob filter (negate-able with leading !)", + Other: "files-gb files glob filter (negate-able with leading !)", + } +} + +// FilesRegExParamUsageTemplData +// ๐ŸงŠ +type FilesRegExParamUsageTemplData struct { + CobrassTemplData +} + +func (td FilesRegExParamUsageTemplData) Message() *Message { + return &Message{ + ID: "files-regex-filter.param-usage", + Description: "files regex filter (negate-able with leading !)", + Other: "files-rx folder regular expression filter (negate-able with leading !)", + } +} + +// FolderGlobParamUsageTemplData +// ๐ŸงŠ +type FolderGlobParamUsageTemplData struct { + CobrassTemplData +} + +func (td FolderGlobParamUsageTemplData) Message() *Message { + return &Message{ + ID: "folders-glob-filter.param-usage", + Description: "folders glob (negate-able with leading !)", + Other: "folders-gb folder glob filter (negate-able with leading !)", + } +} + +// FolderRexExParamUsageTemplData +// ๐ŸงŠ +type FolderRexExParamUsageTemplData struct { + CobrassTemplData +} + +func (td FolderRexExParamUsageTemplData) Message() *Message { + return &Message{ + ID: "folders-regex-filter.param-usage", + Description: "folders regex filter (negate-able with leading !)", + Other: "folders-rx folder regular expression filter (negate-able with leading !)", + } +} + +// WorkerPoolCPUParamUsageTemplData +// ๐ŸงŠ +type WorkerPoolCPUParamUsageTemplData struct { + CobrassTemplData +} + +func (td WorkerPoolCPUParamUsageTemplData) Message() *Message { + return &Message{ + ID: "worker-pool-cpu.param-usage", + Description: "run with the number of workers in pool set to number of CPUs available", + Other: "cpu denotes parallel execution with all available processors", + } +} + +// WorkerPoolCPUParamUsageTemplData +// ๐ŸงŠ +type WorkerPoolNoWParamUsageTemplData struct { + CobrassTemplData +} + +func (td WorkerPoolNoWParamUsageTemplData) Message() *Message { + return &Message{ + ID: "worker-pool-cpu.param-usage", + Description: "run with the number of workers in pool set to this number", + Other: "now denotes parallel execution with this number of workers in pool", + } +} + +// WorkerPoolCPUParamUsageTemplData +// ๐ŸงŠ +type ProfileParamUsageTemplData struct { + CobrassTemplData +} + +func (td ProfileParamUsageTemplData) Message() *Message { + return &Message{ + ID: "profile.param-usage", + Description: "pre-defined flag/option list in config file", + Other: "profile specifies which set of flags/options to load from config", + } +} + +// WorkerPoolCPUParamUsageTemplData +// ๐ŸงŠ +type DryRunParamUsageTemplData struct { + CobrassTemplData +} + +func (td DryRunParamUsageTemplData) Message() *Message { + return &Message{ + ID: "dry-run.param-usage", + Description: "allows the user to preview the effects of a command without running it", + Other: "dry-run allows the user to see the effects of a command without running it", + } +} diff --git a/src/assistant/i18n/utils.go b/src/assistant/i18n/utils.go new file mode 100644 index 0000000..55a49d7 --- /dev/null +++ b/src/assistant/i18n/utils.go @@ -0,0 +1,13 @@ +package i18n + +import ( + "strings" +) + +func LeadsWith(name, text string) string { + if strings.HasPrefix(text, name) { + return text + } + + return name + " " + text +} diff --git a/src/assistant/param-set.go b/src/assistant/param-set.go index 6c8d7e1..5fdf5ae 100644 --- a/src/assistant/param-set.go +++ b/src/assistant/param-set.go @@ -124,6 +124,10 @@ type ParamSet[N any] struct { // FlagSet is the default Cobra FlagSet // FlagSet *pflag.FlagSet + + // Command is the cobra command that the parameter set is bound to. + // + Command *cobra.Command } // NewParamSet is the factory function, which creates a 'parameter set' for @@ -140,6 +144,7 @@ func NewParamSet[N any](command *cobra.Command) (ps *ParamSet[N]) { ps = new(ParamSet[N]) ps.FlagSet = command.Flags() ps.Native = new(N) + ps.Command = command if reflect.TypeOf(*ps.Native).Kind() != reflect.Struct { typeName := reflect.TypeOf(*ps.Native).Name() diff --git a/src/store/families_test.go b/src/store/families_test.go new file mode 100644 index 0000000..62e9119 --- /dev/null +++ b/src/store/families_test.go @@ -0,0 +1,178 @@ +package store_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/snivilised/cobrass/src/assistant" + "github.com/snivilised/cobrass/src/assistant/i18n" + "github.com/snivilised/cobrass/src/internal/helpers" + "github.com/snivilised/cobrass/src/store" + xi18n "github.com/snivilised/extendio/i18n" +) + +func reason(binder string, err error) string { + return fmt.Sprintf("๐Ÿ”ฅ expected '%v' error to be nil, but was '%v'\n", + binder, err, + ) +} + +const ( + shouldMessage = "๐Ÿงช should: bind all parameters without error" +) + +var _ = Describe("Families", Ordered, func() { + var ( + repo string + l10nPath string + + from xi18n.LoadFrom + rootCommand *cobra.Command + execute func(args []string) + ) + + BeforeAll(func() { + repo = helpers.Repo("../..") + l10nPath = helpers.Path(repo, "Test/data/l10n") + + from = xi18n.LoadFrom{ + Path: l10nPath, + Sources: xi18n.TranslationFiles{ + i18n.CobrassSourceID: xi18n.TranslationSource{Name: "test"}, + }, + } + + if err := xi18n.Use(func(o *xi18n.UseOptions) { + o.From = from + }); err != nil { + Fail(err.Error()) + } + + execute = func(args []string) { + _, err := helpers.ExecuteCommand( + rootCommand, args..., + ) + Expect(err).Error().To(BeNil(), reason("BindAll", err)) + } + }) + + BeforeEach(func() { + rootCommand = &cobra.Command{ + Use: "scorch", + Short: "scotch", + Long: "scorch is a fake test command which contains filtering capabilities", + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + } + }) + + DescribeTable("filter family", + func(commandLine []string) { + ps := assistant.NewParamSet[store.FilterParameterSet](rootCommand) + ps.Native.BindAll(ps) + + execute(commandLine) + }, + func(args []string) string { + return shouldMessage + }, + Entry( + nil, []string{"--files-rx", "^foo", "--folders-gb", "bar*"}, + ), + Entry( + nil, []string{"-X", "^foo", "-z", "bar*"}, + ), + Entry( + nil, []string{"--files-gb", "foo*", "--folders-rx", "^bar"}, + ), + Entry( + nil, []string{"-G", "foo*", "-y", "^bar"}, + ), + ) + + DescribeTable("worker pool family", + func(commandLine []string) { + ps := assistant.NewParamSet[store.WorkerPoolParameterSet](rootCommand) + ps.Native.BindAll(ps) + + execute(commandLine) + }, + func(args []string) string { + return shouldMessage + }, + Entry( + nil, []string{"--cpu"}, + ), + Entry( + nil, []string{"-C"}, + ), + Entry( + nil, []string{"--now", "4"}, + ), + Entry( + nil, []string{"-N", "4"}, + ), + ) + + DescribeTable("profile family", + func(commandLine []string) { + ps := assistant.NewParamSet[store.ProfileParameterSet](rootCommand) + ps.Native.BindAll(ps) + + execute(commandLine) + }, + func(args []string) string { + return shouldMessage + }, + Entry( + nil, []string{"--profile", "foo"}, + ), + Entry( + nil, []string{"-P", "foo"}, + ), + ) + + DescribeTable("profile family", + func(commandLine []string) { + ps := assistant.NewParamSet[store.PreviewParameterSet](rootCommand) + ps.Native.BindAll(ps) + + execute(commandLine) + }, + func(args []string) string { + return shouldMessage + }, + Entry( + nil, []string{"--dry-run"}, + ), + Entry( + nil, []string{"-D"}, + ), + ) + + When("usage requested", func() { + It("should: ๐Ÿงช show help text", func() { + filtersPS := assistant.NewParamSet[store.FilterParameterSet](rootCommand) + filtersPS.Native.BindAll(filtersPS) + // + poolPS := assistant.NewParamSet[store.WorkerPoolParameterSet](rootCommand) + poolPS.Native.BindAll(poolPS) + // + profilePS := assistant.NewParamSet[store.ProfileParameterSet](rootCommand) + profilePS.Native.BindAll(profilePS) + // + previewPS := assistant.NewParamSet[store.PreviewParameterSet](rootCommand) + previewPS.Native.BindAll(previewPS) + // + commandLine := []string{"scorch", "--help"} + _, err := helpers.ExecuteCommand( + rootCommand, commandLine..., + ) + Expect(err).Error().To(BeNil(), reason("BindAll", err)) + }) + }) +}) diff --git a/src/store/family-filter.go b/src/store/family-filter.go new file mode 100644 index 0000000..528cc7f --- /dev/null +++ b/src/store/family-filter.go @@ -0,0 +1,73 @@ +package store + +import ( + "regexp" + + "github.com/snivilised/cobrass/src/assistant" + "github.com/snivilised/cobrass/src/assistant/i18n" + xi18n "github.com/snivilised/extendio/i18n" + "github.com/spf13/pflag" +) + +const ( + defaultFilterValue = "" +) + +type FilterParameterSet struct { + FilesGlob string + FilesRexEx string + FoldersGlob string + FoldersRexEx string +} + +func (f *FilterParameterSet) BindAll(self *assistant.ParamSet[FilterParameterSet]) { + // --files-gb(G) + // + self.BindString( + newFlagInfo( + xi18n.Text(i18n.FilesGlobParamUsageTemplData{}), + defaultFilterValue, + ), + &self.Native.FilesGlob, + ) + + // --files-rx(X) + // + self.BindValidatedString( + newFlagInfo( + xi18n.Text(i18n.FilesRegExParamUsageTemplData{}), + defaultFilterValue), + &self.Native.FilesRexEx, + func(value string, _ *pflag.Flag) error { + _, err := regexp.Compile(value) + return err + }, + ) + + // --folders-gb(z) + // + self.BindString( + newFlagInfo( + xi18n.Text(i18n.FolderGlobParamUsageTemplData{}), + defaultFilterValue, + ), + &self.Native.FoldersGlob, + ) + + // --folders-rx(y) + // + self.BindValidatedString( + newFlagInfo( + xi18n.Text(i18n.FolderRexExParamUsageTemplData{}), + defaultFilterValue, + ), + &self.Native.FoldersRexEx, + func(value string, _ *pflag.Flag) error { + _, err := regexp.Compile(value) + return err + }, + ) + + self.Command.MarkFlagsMutuallyExclusive("files-gb", "files-rx") + self.Command.MarkFlagsMutuallyExclusive("folders-gb", "folders-rx") +} diff --git a/src/store/family-preview.go b/src/store/family-preview.go new file mode 100644 index 0000000..9d719c9 --- /dev/null +++ b/src/store/family-preview.go @@ -0,0 +1,27 @@ +package store + +import ( + "github.com/snivilised/cobrass/src/assistant" + "github.com/snivilised/cobrass/src/assistant/i18n" + xi18n "github.com/snivilised/extendio/i18n" +) + +type PreviewParameterSet struct { + DryRun bool +} + +func (f *PreviewParameterSet) BindAll(self *assistant.ParamSet[PreviewParameterSet]) { + // --dry-run(D) + // + const ( + defaultDryRun = false + ) + + self.BindBool( + newFlagInfo( + xi18n.Text(i18n.DryRunParamUsageTemplData{}), + defaultDryRun, + ), + &self.Native.DryRun, + ) +} diff --git a/src/store/family-profile.go b/src/store/family-profile.go new file mode 100644 index 0000000..93fb73b --- /dev/null +++ b/src/store/family-profile.go @@ -0,0 +1,26 @@ +package store + +import ( + "github.com/snivilised/cobrass/src/assistant" + "github.com/snivilised/cobrass/src/assistant/i18n" + xi18n "github.com/snivilised/extendio/i18n" +) + +type ProfileParameterSet struct { + Profile string +} + +func (f *ProfileParameterSet) BindAll(self *assistant.ParamSet[ProfileParameterSet]) { + const ( + defaultProfile = "" + ) + + self.BindValidatedStringIsMatch( + newFlagInfo( + xi18n.Text(i18n.ProfileParamUsageTemplData{}), + defaultProfile, + ), + &self.Native.Profile, + `^[\w-]+$`, + ) +} diff --git a/src/store/family-worker-pool.go b/src/store/family-worker-pool.go new file mode 100644 index 0000000..fc5193a --- /dev/null +++ b/src/store/family-worker-pool.go @@ -0,0 +1,48 @@ +package store + +import ( + "github.com/snivilised/cobrass/src/assistant" + "github.com/snivilised/cobrass/src/assistant/i18n" + xi18n "github.com/snivilised/extendio/i18n" +) + +type WorkerPoolParameterSet struct { + CPU bool + NoWorkers int +} + +func (f *WorkerPoolParameterSet) BindAll(self *assistant.ParamSet[WorkerPoolParameterSet]) { + // --cpu(C) + // + const ( + defaultCPU = false + ) + + self.BindBool( + newFlagInfo( + xi18n.Text(i18n.WorkerPoolCPUParamUsageTemplData{}), + defaultCPU, + ), + &self.Native.CPU, + ) + + // --now(N) + // + const ( + defaultNoW = -1 + minNow = -1 + maxNow = 100 + ) + + self.BindValidatedIntWithin( + newFlagInfo( + xi18n.Text(i18n.WorkerPoolNoWParamUsageTemplData{}), + defaultNoW, + ), + &self.Native.NoWorkers, + minNow, + maxNow, + ) + + self.Command.MarkFlagsMutuallyExclusive("cpu", "now") +} diff --git a/src/store/family.go b/src/store/family.go new file mode 100644 index 0000000..3b2f798 --- /dev/null +++ b/src/store/family.go @@ -0,0 +1,41 @@ +package store + +import ( + "strings" + + "github.com/snivilised/cobrass/src/assistant" +) + +type longFlagName = string +type shortFlagName = string + +type flagDefinitions map[longFlagName]shortFlagName + +var shortFlags = flagDefinitions{ + // worker pool family + // + "cpu": "C", + "now": "N", + + // preview family + // + "dry-run": "D", + + // filter family + // + "files-gb": "G", + "files-rx": "X", + "folders-gb": "z", + "folders-rx": "y", + + // parameter profile + // + "profile": "P", +} + +func newFlagInfo[T any](usage string, defaultValue T) *assistant.FlagInfo { + name := strings.Split(usage, " ")[0] + short := shortFlags[name] + + return assistant.NewFlagInfo(usage, short, defaultValue) +} diff --git a/src/store/gallery-suite_test.go b/src/store/gallery-suite_test.go new file mode 100644 index 0000000..e79bb60 --- /dev/null +++ b/src/store/gallery-suite_test.go @@ -0,0 +1,13 @@ +package store_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestStore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Store Suite") +}