From c82ca43f9fccb70dd6c1a5b763b770831b4211c0 Mon Sep 17 00:00:00 2001 From: kozmod Date: Mon, 5 Feb 2024 15:02:13 +0300 Subject: [PATCH] [#89] Add groups of actions --- .gitignore | 3 + Readme.md | 2 +- internal/config/config.go | 108 ++++++++++- internal/config/config_test.go | 291 ++++------------------------ internal/config/unmarshaler.go | 36 +--- internal/config/unmarshaler_test.go | 119 ++++++++++++ internal/entity/entity.go | 11 ++ internal/factory/action_filter.go | 75 +++++++ internal/factory/chain_exec.go | 51 ++++- internal/flag/flags.go | 9 +- internal/flag/flags_test.go | 58 ++++++ internal/flag/group.go | 27 +++ main.go | 29 ++- 13 files changed, 500 insertions(+), 319 deletions(-) create mode 100644 internal/config/unmarshaler_test.go create mode 100644 internal/factory/action_filter.go create mode 100644 internal/flag/group.go diff --git a/.gitignore b/.gitignore index d269160..a33d980 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ out/ cover.out progen main + +progen.yaml +progen.yml diff --git a/Readme.md b/Readme.md index af13302..078acf7 100644 --- a/Readme.md +++ b/Readme.md @@ -436,7 +436,7 @@ cmd2: ``` ```console -% progen -v -dr -f progen.yml -skip=^dirs$$ -skip=cmd.+ +% progen -v -dr -f progen.yml -skip=^dirs$ -skip=cmd.+ 2023-02-05 14:18:11 INFO application working directory: /Users/user_1/GoProjects/service 2023-02-05 14:18:11 INFO configuration file: progen.yml 2023-02-05 14:18:11 INFO action tag will be skipped: cmd1 diff --git a/internal/config/config.go b/internal/config/config.go index 88ab7af..2e4f9db 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,13 +1,11 @@ package config import ( - "net/url" - "strings" - + "github.com/kozmod/progen/internal/entity" "golang.org/x/xerrors" "gopkg.in/yaml.v3" - - "github.com/kozmod/progen/internal/entity" + "net/url" + "strings" ) const ( @@ -27,7 +25,8 @@ type Config struct { } type Settings struct { - HTTP *HTTPClient `yaml:"http"` + HTTP *HTTPClient `yaml:"http"` + Groups Groups `yaml:"groups"` } type HTTPClient struct { @@ -36,6 +35,42 @@ type HTTPClient struct { Debug bool `yaml:"debug"` } +type Groups []Group + +func (g Groups) ManualActions() map[string]struct{} { + manual := make(map[string]struct{}) + for _, group := range g { + if !group.Manual { + continue + } + for _, action := range group.Actions { + manual[action] = struct{}{} + } + } + return manual +} + +func (g Groups) GroupByAction() map[string]map[string]struct{} { + manual := make(map[string]map[string]struct{}) + for _, group := range g { + for _, action := range group.Actions { + groups, ok := manual[action] + if !ok { + groups = make(map[string]struct{}) + } + groups[group.Name] = struct{}{} + manual[action] = groups + } + } + return manual +} + +type Group struct { + Name string `yaml:"name"` + Actions []string `yaml:"actions"` + Manual bool `yaml:"manual"` +} + type Section[T any] struct { Line int32 Tag string @@ -114,7 +149,27 @@ func (addr *AddrURL) UnmarshalYAML(unmarshal func(any) error) error { return nil } -func ValidateFile(file File) error { +func (c Config) Validate() error { + for i, files := range c.Files { + for _, file := range files.Val { + err := validateFile(file) + if err != nil { + return xerrors.Errorf("files: %d [%s]: %w", i, file.Path, err) + } + } + } + + if err := validateGroups(c.Settings.Groups); err != nil { + return xerrors.Errorf("groups: %w", err) + } + + if err := validateConfigSections(c); err != nil { + return xerrors.Errorf("sections: %w", err) + } + return nil +} + +func validateFile(file File) error { notNil := entity.NotNilValues(file.Get, file.Data, file.Local) switch { case notNil == 0: @@ -124,5 +179,44 @@ func ValidateFile(file File) error { case strings.TrimSpace(file.Path) == entity.Empty: return xerrors.Errorf("files: save `path` are empty") } + + return nil +} + +func validateGroups(groups Groups) error { + var ( + groupNameSet = make(map[string]int, len(groups)) + groupNames = make([]string, 0, len(groups)) + ) + for _, group := range groups { + var ( + name = group.Name + quantity, ok = groupNameSet[name] + ) + if ok && quantity == 1 { + groupNames = append(groupNames, name) + } + groupNameSet[name] = groupNameSet[name] + 1 + } + + if len(groupNames) > 0 { + return xerrors.Errorf("duplicate names [%s]", strings.Join(groupNames, entity.LogSliceSep)) + } + return nil +} + +func validateConfigSections(conf Config) error { + var ( + files = len(conf.Files) + dirs = len(conf.Dirs) + cmd = len(conf.Cmd) + fs = len(conf.FS) + ) + if files == 0 && dirs == 0 && cmd == 0 && fs == 0 { + return xerrors.Errorf( + "config not contains executable actions [dirs: %d, files: %d, cms: %d, fs: %d]", + dirs, files, cmd, fs, + ) + } return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6b103c8..9694958 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "regexp" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -108,7 +107,7 @@ steps: }) } -func Test_ValidateFile(t *testing.T) { +func Test_validateFile(t *testing.T) { t.Parallel() const ( @@ -120,7 +119,7 @@ func Test_ValidateFile(t *testing.T) { Data: nil, Get: &Get{}, } - err := ValidateFile(in) + err := validateFile(in) assert.NoError(t, err) }) t.Run("not_error_when_data_is_not_nil", func(t *testing.T) { @@ -129,7 +128,7 @@ func Test_ValidateFile(t *testing.T) { Data: func(d Bytes) *Bytes { return &d }(Bytes("some data")), Get: nil, } - err := ValidateFile(in) + err := validateFile(in) assert.NoError(t, err) }) t.Run("error_when_data_and_get_are_nil", func(t *testing.T) { @@ -138,7 +137,7 @@ func Test_ValidateFile(t *testing.T) { Data: nil, Get: nil, } - err := ValidateFile(in) + err := validateFile(in) assert.Error(t, err) }) t.Run("error_when_data_and_get_are_not_nil_both", func(t *testing.T) { @@ -147,7 +146,7 @@ func Test_ValidateFile(t *testing.T) { Data: func(d Bytes) *Bytes { return &d }(Bytes("some data")), Get: &Get{}, } - err := ValidateFile(in) + err := validateFile(in) assert.Error(t, err) }) t.Run("error_when_path_is_empty", func(t *testing.T) { @@ -155,15 +154,47 @@ func Test_ValidateFile(t *testing.T) { Path: "", Get: &Get{}, } - err := ValidateFile(in) + err := validateFile(in) assert.Error(t, err) in.Path = " " - err = ValidateFile(in) + err = validateFile(in) assert.Error(t, err) }) } +func Test_validateGroups(t *testing.T) { + t.Parallel() + + const ( + groupA = "grpA" + groupB = "grpB" + ) + + t.Run("not_error", func(t *testing.T) { + in := Groups{ + {Name: groupA}, + {Name: groupB}, + } + err := validateGroups(in) + assert.NoError(t, err) + }) + t.Run("error_when_duplicate_name", func(t *testing.T) { + in := Groups{ + { + Name: groupB, + }, + { + Name: groupB, + }, + } + err := validateGroups(in) + assert.Error(t, err) + assert.Equal(t, fmt.Sprintf("duplicate names [%s]", groupB), err.Error()) + + }) +} + func Test_Read(t *testing.T) { t.Parallel() @@ -206,196 +237,6 @@ cmd2: }) } -func Test_YamlUnmarshaler_Unmarshal(t *testing.T) { - t.Parallel() - - t.Run("success_unmarshal", func(t *testing.T) { - const ( - in = ` -cmd: - - exec: pwd - dir: .. - - exec: ls - args: [-l] - dir: .. - -dirs: - - x/api/{{.vars.service_name}}/v1 - - s - -dirs2: - - y/api - -cmd1: - - ls -a - -files: - - path: x/DDDDDD - tmpl_skip: true - data: | - ENV GOPROXY "{{.vars.GOPROXY}} ,proxy.golang.org,direct" - -cmd2: - - exec: ls - args: [ -lS ] - dir: / - - exec: whoami -` - ) - - conf, err := NewYamlConfigUnmarshaler(nil, nil).Unmarshal([]byte(in)) - a := assert.New(t) - a.NoError(err) - a.Len(conf.Files, 1) - a.Len(conf.Dirs, 2) - a.Len(conf.Cmd, 3) - - assertSection(a, conf.Cmd, "cmd", 3, - Command{Dir: "..", Exec: "pwd"}, - Command{Dir: "..", Exec: "ls", Args: []string{"-l"}}, - ) - assertSection(a, conf.Cmd, "cmd1", 17, - Command{Dir: entity.Empty, Exec: "ls", Args: []string{"-a"}}, - ) - assertSection(a, conf.Cmd, "cmd2", 26, - Command{Dir: "/", Exec: "ls", Args: []string{"-lS"}}, - Command{Exec: "whoami"}) - - assertSection(a, conf.Dirs, "dirs", 10, "x/api/{{.vars.service_name}}/v1", "s") - assertSection(a, conf.Dirs, "dirs2", 14, "y/api") - - files := conf.Files[0] - a.Equal(int32(20), files.Line) - a.Equal(TagFiles, files.Tag) - a.Len(files.Val, 1) - - file := files.Val[0] - a.Equal("x/DDDDDD", file.Path) - a.NotNil(file.Data) - a.Equal(Bytes("ENV GOPROXY \"{{.vars.GOPROXY}} ,proxy.golang.org,direct\"\n"), *file.Data) - }) - - t.Run("success_unmarshal_skip_all_cmd_and_dirs2_sections", func(t *testing.T) { - const ( - in = ` -cmd: - - exec: [pwd] - dir: / - -dirs: - - x/api/{{.vars.service_name}}/v1 - - s - -dirs2: - - y/api - -cmd1: - - exec: [ls -a] - dir: . - -files: - - path: x/DDDDDD - tmpl_skip: true - data: | - ENV GOPROXY "{{.vars.GOPROXY}} ,proxy.golang.org,direct" - -cmd2: - - exec: [ls -lS, whoami] - dir: -` - ) - - var ( - a = assert.New(t) - filter = entity.NewRegexpChain("cmd.?", "dirs2") - logger = MockLogger{ - infof: func(format string, args ...any) { - a.Equal("action tag will be skipped: %s", format) - for i, arg := range args { - a.Containsf([]any{"cmd", "cmd1", "cmd2", "dirs2"}, arg, "contains_%d", i) - } - }, - } - ) - conf, err := NewYamlConfigUnmarshaler(filter, logger).Unmarshal([]byte(in)) - a.NoError(err) - a.Len(conf.Files, 1) - a.Len(conf.Dirs, 1) - a.Len(conf.Cmd, 0) - - assertSection(a, conf.Dirs, "dirs", 7, "x/api/{{.vars.service_name}}/v1", "s") - - files := conf.Files[0] - a.Equal(int32(18), files.Line) - a.Equal(TagFiles, files.Tag) - a.Len(files.Val, 1) - - file := files.Val[0] - a.Equal("x/DDDDDD", file.Path) - a.NotNil(file.Data) - a.Equal(Bytes("ENV GOPROXY \"{{.vars.GOPROXY}} ,proxy.golang.org,direct\"\n"), *file.Data) - }) - t.Run("error_when-config_not_contains_executable_actions", func(t *testing.T) { - const ( - in = ` -var: - some_1: val_1 -` - ) - _, err := NewYamlConfigUnmarshaler(nil, nil).Unmarshal([]byte(in)) - assert.Error(t, err) - assert.Contains(t, err.Error(), "config not contains executable actions") - }) -} - -func Test_Command_UnmarshalYAML(t *testing.T) { - t.Run("success", func(t *testing.T) { - const ( - in = ` -cmd: - - exec: pwd - dir: / - - ls -a -` - ) - var ( - a = assert.New(t) - ) - conf, err := NewYamlConfigUnmarshaler(nil, nil).Unmarshal([]byte(in)) - a.NoError(err) - assertSection(a, conf.Cmd, "cmd", 3, - Command{Dir: "/", Exec: "pwd"}, - Command{Dir: entity.Empty, Exec: "ls", Args: []string{"-a"}}, - ) - }) - t.Run("success_v2", func(t *testing.T) { - const ( - in = ` -cmd: - - ls -a - - dh -f - - exec: pwd - dir: / -` - ) - - var ( - a = assert.New(t) - TestCmd struct { - Commands []Command `yaml:"cmd,flow"` - } - ) - err := yaml.Unmarshal([]byte(in), &TestCmd) - a.NoError(err) - a.Equal([]Command{ - {Dir: entity.Empty, Exec: "ls", Args: []string{"-a"}}, - {Dir: entity.Empty, Exec: "dh", Args: []string{"-f"}}, - {Dir: "/", Exec: "pwd"}, - }, TestCmd.Commands) - - }) -} - func Test_AddrURL_UnmarshalYAML(t *testing.T) { t.Run("success", func(t *testing.T) { const ( @@ -427,57 +268,3 @@ func Test_AddrURL_UnmarshalYAML(t *testing.T) { a.Error(err) }) } - -func assertSection[T any](a *assert.Assertions, sections []Section[[]T], tag string, line int32, expected ...T) { - var ( - section Section[[]T] - found bool - ) - - for _, s := range sections { - if s.Tag == tag { - section = s - found = true - break - } - } - a.Truef(found, "section not contains tag [%s]", tag) - a.NotNil(section) - a.Equal(line, section.Line) - d := section.Val - a.Equal(expected, d) -} - -func Test_commandFromString(t *testing.T) { - t.Parallel() - - const ( - command = "ls" - arg = "-a" - ) - - t.Run("success", func(t *testing.T) { - var ( - commandStr = strings.Join(append([]string{command}, arg), entity.Space) - ) - - cmd, err := commandFromString(commandStr) - assert.NoError(t, err) - assert.Equal(t, Command{Dir: entity.Empty, Exec: command, Args: []string{arg}}, cmd) - }) - t.Run("error_when_command_is_empty", func(t *testing.T) { - cmd, err := commandFromString("") - assert.Error(t, err) - assert.NotNil(t, cmd) - assert.ErrorIs(t, err, ErrCommandEmpty) - }) -} - -type MockLogger struct { - entity.Logger - infof func(format string, args ...any) -} - -func (m MockLogger) Infof(format string, args ...any) { - m.infof(format, args...) -} diff --git a/internal/config/unmarshaler.go b/internal/config/unmarshaler.go index 735c1d9..7742903 100644 --- a/internal/config/unmarshaler.go +++ b/internal/config/unmarshaler.go @@ -18,11 +18,8 @@ type YamlUnmarshaler struct { logger entity.Logger } -func NewYamlConfigUnmarshaler(tagFilter *entity.RegexpChain, logger entity.Logger) *YamlUnmarshaler { - return &YamlUnmarshaler{ - tagFilter: tagFilter, - logger: logger, - } +func NewYamlConfigUnmarshaler() *YamlUnmarshaler { + return &YamlUnmarshaler{} } func (u *YamlUnmarshaler) Unmarshal(rawConfig []byte) (Config, error) { @@ -43,9 +40,6 @@ func (u *YamlUnmarshaler) Unmarshal(rawConfig []byte) (Config, error) { var settings Settings err = node.Decode(&settings) conf.Settings = settings - case u.tagFilter != nil && u.tagFilter.MatchString(tag): - u.logger.Infof("action tag will be skipped: %s", tag) - continue case strings.Index(tag, TagDirs) == 0: conf.Dirs, err = decode(conf.Dirs, node, tag) case strings.Index(tag, TagFiles) == 0: @@ -61,16 +55,7 @@ func (u *YamlUnmarshaler) Unmarshal(rawConfig []byte) (Config, error) { } } - for i, files := range conf.Files { - for _, file := range files.Val { - err := ValidateFile(file) - if err != nil { - return conf, xerrors.Errorf("validate config: files: %d [%s]: %w", i, file.Path, err) - } - } - } - - return conf, validateConfigSections(conf) + return conf, nil } func decode[T any](target []Section[T], node yaml.Node, tag string) ([]Section[T], error) { @@ -80,21 +65,6 @@ func decode[T any](target []Section[T], node yaml.Node, tag string) ([]Section[T return target, err } -func validateConfigSections(conf Config) error { - var ( - files = len(conf.Files) - dirs = len(conf.Dirs) - cmd = len(conf.Cmd) - fs = len(conf.FS) - ) - if files == 0 && dirs == 0 && cmd == 0 && fs == 0 { - return xerrors.Errorf( - "validate config: config not contains executable actions [dirs: %d, files: %d, cms: %d, fs: %d]", - dirs, files, cmd, fs) - } - return nil -} - func commandFromString(cmd string) (Command, error) { var ( splitCmd = strings.Split(cmd, entity.Space) diff --git a/internal/config/unmarshaler_test.go b/internal/config/unmarshaler_test.go new file mode 100644 index 0000000..060a933 --- /dev/null +++ b/internal/config/unmarshaler_test.go @@ -0,0 +1,119 @@ +package config + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/kozmod/progen/internal/entity" +) + +func Test_commandFromString(t *testing.T) { + t.Parallel() + + const ( + command = "ls" + arg = "-a" + ) + + t.Run("success", func(t *testing.T) { + var ( + commandStr = strings.Join(append([]string{command}, arg), entity.Space) + ) + + cmd, err := commandFromString(commandStr) + assert.NoError(t, err) + assert.Equal(t, Command{Dir: entity.Empty, Exec: command, Args: []string{arg}}, cmd) + }) + t.Run("error_when_command_is_empty", func(t *testing.T) { + cmd, err := commandFromString("") + assert.Error(t, err) + assert.NotNil(t, cmd) + assert.ErrorIs(t, err, ErrCommandEmpty) + }) +} + +func Test_settings_tag(t *testing.T) { + t.Run("parse_http", func(t *testing.T) { + const ( + baseUrl = `https://gitlab.repo_2.com/api/v4/projects/5/repository/files/` + headerKey = `PRIVATE-TOKEN` + headerVal = `glpat-SOME_TOKEN` + queryKey = `Q_PARAM_1` + queryVal = `Q_Val_1` + ) + in := fmt.Sprintf(` +settings: + http: + debug: true + base_url: %s + headers: + %s: %s + query_params: + %s: %s +`, + baseUrl, + headerKey, + headerVal, + queryKey, + queryVal, + ) + + conf, err := NewYamlConfigUnmarshaler().Unmarshal([]byte(in)) + assert.NoError(t, err) + assert.NotNil(t, conf.Settings.HTTP) + + httpConf := conf.Settings.HTTP + assert.True(t, httpConf.Debug) + assert.Equal(t, baseUrl, httpConf.BaseURL.String()) + assert.Len(t, httpConf.Headers, 1) + + header, ok := httpConf.Headers[headerKey] + assert.True(t, ok) + assert.Equal(t, headerVal, header) + + assert.Len(t, httpConf.QueryParams, 1) + + query, ok := httpConf.QueryParams[queryKey] + assert.True(t, ok) + assert.Equal(t, queryVal, query) + }) + + t.Run("groups_http", func(t *testing.T) { + const ( + groupA = "a" + groupB = "b" + actionA = "cmdA" + actionB = "cmdB" + ) + in := fmt.Sprintf(` +settings: + groups: + - name: %s + actions: [%s] + - name: %s + actions: + - %s + - %s + manual: true +`, + groupA, + actionA, + groupB, + actionA, + actionB, + ) + + conf, err := NewYamlConfigUnmarshaler().Unmarshal([]byte(in)) + assert.NoError(t, err) + + assert.NotEmpty(t, conf.Settings.Groups) + assert.Len(t, conf.Settings.Groups, 2) + + assert.Equal(t, conf.Settings.Groups[0], Group{Name: groupA, Actions: []string{actionA}, Manual: false}) + assert.Equal(t, conf.Settings.Groups[1], Group{Name: groupB, Actions: []string{actionA, actionB}, Manual: true}) + + }) +} diff --git a/internal/entity/entity.go b/internal/entity/entity.go index cc1bed0..f152f87 100644 --- a/internal/entity/entity.go +++ b/internal/entity/entity.go @@ -37,10 +37,13 @@ const ( Empty = "" Dash = "-" Dot = "." + Comma = "," EqualsSign = "=" LessThan = "<" Tilda = "~" NewLine = "\n" + + LogSliceSep = Comma + Space ) type ( @@ -164,3 +167,11 @@ func (c *RegexpChain) MatchString(s string) bool { } return false } + +func SliceSet[T comparable](in []T) map[T]struct{} { + set := make(map[T]struct{}, len(in)) + for _, val := range in { + set[val] = struct{}{} + } + return set +} diff --git a/internal/factory/action_filter.go b/internal/factory/action_filter.go new file mode 100644 index 0000000..c3190f4 --- /dev/null +++ b/internal/factory/action_filter.go @@ -0,0 +1,75 @@ +package factory + +import ( + "github.com/kozmod/progen/internal/entity" + "slices" + "strings" +) + +type ( + actionFilter interface { + MatchString(s string) bool + } +) + +type ActionFilter struct { + skipFilter actionFilter + selectedGroups map[string]struct{} + groupsByAction map[string]map[string]struct{} + manualActions map[string]struct{} + + logger entity.Logger +} + +func NewActionFilter( + skipActions []string, + selectedGroups []string, + groupsByAction map[string]map[string]struct{}, + manualActionsSet map[string]struct{}, + logger entity.Logger, + +) *ActionFilter { + selectedGroupsSet := entity.SliceSet(selectedGroups) + skipActions = slices.Compact(skipActions) + + switch { + case len(selectedGroups) > 0: + logger.Infof("groups will be execute: [%s]", strings.Join(selectedGroups, entity.LogSliceSep)) + case len(manualActionsSet) > 0: + manualActions := make([]string, 0, len(manualActionsSet)) + for action := range manualActionsSet { + manualActions = append(manualActions, action) + } + logger.Infof("manual actions will be skipped: [%s]", strings.Join(manualActions, entity.LogSliceSep)) + } + + return &ActionFilter{ + skipFilter: entity.NewRegexpChain(skipActions...), + selectedGroups: selectedGroupsSet, + groupsByAction: groupsByAction, + manualActions: manualActionsSet, + logger: logger, + } +} + +func (f *ActionFilter) MatchString(action string) bool { + if f.skipFilter.MatchString(action) { + f.logger.Infof("actions tag will be skipped: [%s]", action) + return false + } + + switch { + case len(f.selectedGroups) > 0: + if groups, ok := f.groupsByAction[action]; ok { + for group := range groups { + if _, ok = f.selectedGroups[group]; ok { + return true + } + } + } + return false + default: + _, ok := f.manualActions[action] + return !ok + } +} diff --git a/internal/factory/chain_exec.go b/internal/factory/chain_exec.go index afa4eed..ca90f6b 100644 --- a/internal/factory/chain_exec.go +++ b/internal/factory/chain_exec.go @@ -12,6 +12,7 @@ import ( func NewExecutorChain( conf config.Config, + actionFilter actionFilter, templateData map[string]any, templateOptions []string, logger entity.Logger, @@ -21,6 +22,7 @@ func NewExecutorChain( type ( ExecutorBuilder struct { + action string line int32 procFn func() (entity.Executor, error) } @@ -28,10 +30,18 @@ func NewExecutorChain( builders := make([]ExecutorBuilder, 0, len(conf.Dirs)+len(conf.Files)+len(conf.Cmd)+len(conf.FS)) for _, dirs := range conf.Dirs { - d := dirs + var ( + d = dirs + action = d.Tag + ) + + if !actionFilter.MatchString(action) { + continue + } builders = append(builders, ExecutorBuilder{ - line: d.Line, + action: action, + line: d.Line, procFn: func() (entity.Executor, error) { return NewMkdirExecutor(d.Val, logger, dryRun) }, @@ -40,10 +50,17 @@ func NewExecutorChain( var preprocessors []entity.Preprocessor for _, files := range conf.Files { - f := files + var ( + f = files + action = f.Tag + ) + if !actionFilter.MatchString(action) { + continue + } builders = append(builders, ExecutorBuilder{ - line: f.Line, + action: action, + line: f.Line, procFn: func() (entity.Executor, error) { executor, l, err := NewFileExecutor( f.Val, @@ -60,20 +77,34 @@ func NewExecutorChain( } for _, commands := range conf.Cmd { - cmd := commands + var ( + cmd = commands + action = cmd.Tag + ) + if !actionFilter.MatchString(action) { + continue + } builders = append(builders, ExecutorBuilder{ - line: cmd.Line, + action: action, + line: cmd.Line, procFn: func() (entity.Executor, error) { return NewRunCommandExecutor(cmd.Val, logger, dryRun) }, }) } for _, path := range conf.FS { - fs := path + var ( + fs = path + action = fs.Tag + ) + if actionFilter.MatchString(action) { + continue + } builders = append(builders, ExecutorBuilder{ - line: fs.Line, + action: action, + line: fs.Line, procFn: func() (entity.Executor, error) { return NewFSExecutor( fs.Val, @@ -90,10 +121,10 @@ func NewExecutorChain( }) executors := make([]entity.Executor, 0, len(builders)) - for i, builder := range builders { + for _, builder := range builders { e, err := builder.procFn() if err != nil { - return nil, xerrors.Errorf("configure executor [%d]: %w", i, err) + return nil, xerrors.Errorf("configure executor [%s]: %w", builder.action, err) } if e == nil { continue diff --git a/internal/flag/flags.go b/internal/flag/flags.go index 612af81..998f337 100644 --- a/internal/flag/flags.go +++ b/internal/flag/flags.go @@ -27,6 +27,7 @@ const ( flagKeySkip = "skip" flagKeyPreprocessingAllFiles = "pf" flagKeyMissingKey = "missingkey" + flagKeyGroup = "gp" ) var ( @@ -44,6 +45,7 @@ type Flags struct { Skip SkipFlag PreprocessFiles bool MissingKey MissingKeyFlag + Group GroupFlag PrintErrorStackTrace bool PrintProcessedConfig bool } @@ -117,7 +119,7 @@ func parseFlags(fs *flag.FlagSet, args []string) (Flags, error) { fs.Var( &f.Skip, flagKeySkip, - "list of skipping 'yaml' tags") + "list of skipping actions") fs.BoolVar( &f.PreprocessFiles, flagKeyPreprocessingAllFiles, @@ -134,6 +136,11 @@ func parseFlags(fs *flag.FlagSet, args []string) (Flags, error) { entity.MissingKeyError, ), ) + fs.Var( + &f.Group, + flagKeyGroup, + "list of executing groups", + ) err := fs.Parse(args) if err != nil { return f, err diff --git a/internal/flag/flags_test.go b/internal/flag/flags_test.go index bb37294..a4acd74 100644 --- a/internal/flag/flags_test.go +++ b/internal/flag/flags_test.go @@ -211,6 +211,64 @@ func Test_SkipFlag(t *testing.T) { }) } +func Test_group(t *testing.T) { + t.Parallel() + + const ( + usage = "group_flag_test_usage" + setName = "group" + flagName = setName + ) + + var ( + flagKey = fmt.Sprintf("-%s", flagName) + ) + + t.Run("success_when_flag_not_set", func(t *testing.T) { + var ( + fs = flag.NewFlagSet(setName, flag.ContinueOnError) + group GroupFlag + ) + fs.Var(&group, flagName, usage) + + err := fs.Parse([]string{ + flagKey, "", + flagKey, "", + }) + assert.NoError(t, err) + assert.Len(t, group, 0) + }) + t.Run("success_when_flag_not_set_v2", func(t *testing.T) { + var ( + fs = flag.NewFlagSet(setName, flag.ContinueOnError) + group GroupFlag + ) + fs.Var(&group, flagName, usage) + + err := fs.Parse(nil) + assert.NoError(t, err) + assert.Len(t, group, 0) + }) + t.Run("success_when_flag_set", func(t *testing.T) { + var ( + fs = flag.NewFlagSet(setName, flag.ContinueOnError) + group GroupFlag + + flagA = "group1" + flagB = "group2" + ) + fs.Var(&group, flagName, usage) + + err := fs.Parse([]string{ + flagKey, flagA, + flagKey, flagB, + }) + assert.NoError(t, err) + assert.Len(t, group, 2) + assert.ElementsMatch(t, []string{flagA, flagB}, group) + }) +} + func Test_parseFlags(t *testing.T) { t.Parallel() diff --git a/internal/flag/group.go b/internal/flag/group.go new file mode 100644 index 0000000..2b61463 --- /dev/null +++ b/internal/flag/group.go @@ -0,0 +1,27 @@ +package flag + +import ( + "fmt" + "strings" + + "github.com/kozmod/progen/internal/entity" +) + +type GroupFlag []string + +func (s *GroupFlag) String() string { + type alias struct { + skip []string + } + a := alias{skip: *s} + return fmt.Sprintf("%v", a.skip) +} + +func (s *GroupFlag) Set(value string) error { + value = strings.TrimSpace(value) + if value == entity.Empty { + return nil + } + *s = append(*s, value) + return nil +} diff --git a/main.go b/main.go index 3593e54..64de71c 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,6 @@ import ( "os" "time" - "golang.org/x/sync/errgroup" "golang.org/x/xerrors" "github.com/kozmod/progen/internal" @@ -80,26 +79,26 @@ func main() { } var ( - eg errgroup.Group - - conf config.Config - tagFilter = entity.NewRegexpChain(flags.Skip...) + conf config.Config ) + conf, err = config.NewYamlConfigUnmarshaler().Unmarshal(rawConfig) + if err != nil { + logger.Fatalf(logFatalSuffixFn("unmarshal config: "), err) + } - eg.Go(func() error { - conf, err = config.NewYamlConfigUnmarshaler(tagFilter, logger).Unmarshal(rawConfig) - if err != nil { - return xerrors.Errorf("unmarshal config: %w", err) - } - return nil - }) - - if err = eg.Wait(); err != nil { - logger.Fatalf(logFatalSuffixFn("prepare config: "), err) + if err = conf.Validate(); err != nil { + logger.Fatalf(logFatalSuffixFn("validate config: "), err) } procChain, err := factory.NewExecutorChain( conf, + factory.NewActionFilter( + flags.Skip, + flags.Group, + conf.Settings.Groups.GroupByAction(), + conf.Settings.Groups.ManualActions(), + logger, + ), templateData, []string{flags.MissingKey.String()}, logger,