diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 48050cb..9f2349a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.21.6 + go-version: 1.22.9 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 238fc5a..25b3833 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.21.6 + go-version: 1.22.9 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e336f2..000a449 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.21.6 + go-version: 1.22.9 - name: Test run: go test -v ./... -cover -coverprofile cover.out && go tool cover -func cover.out diff --git a/.golangci.yml b/.golangci.yml index 3cdd5a0..9f5a016 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,21 +4,15 @@ run: timeout: 5m issues-exit-code: 1 tests: true - skip-dirs: - - bin - - vendor - - var - - tmp - - .github output: - format: colored-line-number + formats: colored-line-number print-issued-lines: true print-linter-name: true linters-settings: govet: - check-shadowing: true + shadow: true dupl: threshold: 100 goconst: @@ -59,12 +53,20 @@ linters: - gosec - govet - ineffassign - - megacheck + - gosimple + - staticcheck + - unused # - revive # comment check - typecheck - unused # will be used insted of varcheck + deadcode + structcheck. More info https://github.com/golangci/golangci-lint/issues/1841 issues: + exclude-dirs: + - bin + - vendor + - var + - tmp + - .github exclude-use-default: false exclude: # _ instead of err checks diff --git a/go.mod b/go.mod index 4f4d723..06293b1 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/kozmod/progen -go 1.21.1 +go 1.22 require ( - github.com/go-resty/resty/v2 v2.11.0 - github.com/stretchr/testify v1.8.4 - go.uber.org/zap v1.26.0 - golang.org/x/sync v0.6.0 - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 + github.com/go-resty/resty/v2 v2.16.2 + github.com/stretchr/testify v1.10.0 + go.uber.org/zap v1.27.0 + golang.org/x/sync v0.10.0 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da gopkg.in/yaml.v3 v3.0.1 ) @@ -15,5 +15,5 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.20.0 // indirect + golang.org/x/net v0.32.0 // indirect ) diff --git a/go.sum b/go.sum index cc0efad..ae23ab7 100644 --- a/go.sum +++ b/go.sum @@ -1,64 +1,25 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= -github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= +github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/config/config.go b/internal/config/config.go index 8fbea72..a57ecef 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,6 +28,72 @@ type Config struct { FS []Section[[]string] `yaml:"fs,flow"` } +func (c Config) CommandActions() []entity.Action[[]entity.Command] { + return toActionsSlice(c.Cmd, func(cmd Command) entity.Command { + return entity.Command{ + Cmd: cmd.Exec, + Args: cmd.Args, + Dir: cmd.Dir, + } + }) +} + +func (c Config) FilesActions() []entity.Action[[]entity.UndefinedFile] { + return toActionsSlice(c.Files, func(file File) entity.UndefinedFile { + uFile := entity.UndefinedFile{ + Path: file.Path, + } + if file.Data != nil { + *uFile.Data = *file.Data + } + if get := file.Get; get != nil { + uFile.Get = &entity.HTTPClientParams{ + URL: get.URL, + Headers: get.Headers, + QueryParams: get.QueryParams, + } + } + if file.Local != nil { + uFile.Local = file.Local + } + return uFile + }) +} + +func (c Config) DirActions() []entity.Action[[]string] { + return toActionsSlice(c.Dirs, func(dir string) string { + return dir + }) +} + +func (c Config) RmActions() []entity.Action[[]string] { + return toActionsSlice(c.Rm, func(rms string) string { + return rms + }) +} + +func (c Config) FsActions() []entity.Action[[]string] { + return toActionsSlice(c.FS, func(fss string) string { + return fss + }) +} + +func toActionsSlice[S any, T any](sections []Section[[]S], mapFn func(s S) T) []entity.Action[[]T] { + actions := make([]entity.Action[[]T], len(sections)) + for i, section := range sections { + action := entity.Action[[]T]{ + Priority: section.Line, + Name: section.Tag, + Val: make([]T, len(section.Val)), + } + for j, val := range section.Val { + action.Val[j] = mapFn(val) + } + actions[i] = action + } + return actions +} + type Settings struct { HTTP *HTTPClient `yaml:"http"` Groups Groups `yaml:"groups"` @@ -76,7 +142,7 @@ type Group struct { } type Section[T any] struct { - Line int32 + Line int Tag string Val T } diff --git a/internal/config/unmarshaler.go b/internal/config/unmarshaler.go index d996f1f..326738d 100644 --- a/internal/config/unmarshaler.go +++ b/internal/config/unmarshaler.go @@ -58,7 +58,7 @@ func (u *YamlUnmarshaler) Unmarshal(rawConfig []byte) (Config, error) { } func decode[T any](target []Section[T], node yaml.Node, tag string) ([]Section[T], error) { - section := Section[T]{Line: int32(node.Line), Tag: tag} + section := Section[T]{Line: node.Line, Tag: tag} err := node.Decode(§ion.Val) target = append(target, section) return target, err diff --git a/internal/entity/entity.go b/internal/entity/entity.go index e3dc746..2a1835b 100644 --- a/internal/entity/entity.go +++ b/internal/entity/entity.go @@ -1,6 +1,7 @@ package entity import ( + "io/fs" "path/filepath" "regexp" @@ -47,6 +48,7 @@ const ( LogSliceSep = Comma + Space ) +//goland:noinspection SpellCheckingInspection type ( FileProducer interface { Get() (DataFile, error) @@ -89,8 +91,47 @@ type ( Debugf(format string, any ...any) Fatalf(format string, any ...any) } + + ActionFilter interface { + MatchString(s string) bool + } ) +type ExecutorBuilder struct { + Action string + Priority int + ProcFn func() (Executor, error) +} + +type Group struct { + Name string + Actions []string + Manual bool +} + +type Action[T any] struct { + Priority int + Name string + Val T +} + +func (a Action[T]) WithPriority(priority int) Action[T] { + a.Priority = priority + return a +} + +type TargetFs struct { + TargetDir string + Fs fs.FS +} + +type UndefinedFile struct { + Path string + Data *[]byte + Get *HTTPClientParams + Local *string +} + type DataFile struct { FileInfo Data []byte diff --git a/internal/entity/util.go b/internal/entity/util.go index d4d1446..b5c34d9 100644 --- a/internal/entity/util.go +++ b/internal/entity/util.go @@ -2,20 +2,6 @@ package entity import "reflect" -func Unique[T comparable](in []T) []T { - set := make(map[T]struct{}, len(in)) - out := make([]T, 0, len(in)) - for _, val := range in { - _, ok := set[val] - if ok { - continue - } - out = append(out, val) - set[val] = struct{}{} - } - return out -} - func MergeKeys(dst, src map[string]any) map[string]any { if dst == nil { return src diff --git a/internal/entity/util_test.go b/internal/entity/util_test.go index d66f08b..dec664e 100644 --- a/internal/entity/util_test.go +++ b/internal/entity/util_test.go @@ -8,67 +8,6 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_Unique(t *testing.T) { - t.Parallel() - - type useCase[T comparable] struct { - in []T - exp []T - } - - stringCases := []useCase[string]{ - { - in: []string{"a", "a", "a"}, - exp: []string{"a"}, - }, - { - in: []string{"a", "b", "c"}, - exp: []string{"a", "b", "c"}, - }, - { - in: []string{}, - exp: []string{}, - }, - { - in: nil, - exp: []string{}, - }, - } - - for i, tc := range stringCases { - t.Run(fmt.Sprintf("str_case_%d", i), func(t *testing.T) { - res := Unique(tc.in) - assert.Equal(t, tc.exp, res) - }) - } - - intCases := []useCase[int32]{ - { - in: []int32{1, 1, 1}, - exp: []int32{1}, - }, - { - in: []int32{1, 2, 3}, - exp: []int32{1, 2, 3}, - }, - { - in: []int32{}, - exp: []int32{}, - }, - { - in: nil, - exp: []int32{}, - }, - } - - for i, tc := range intCases { - t.Run(fmt.Sprintf("int_case_%d", i), func(t *testing.T) { - res := Unique(tc.in) - assert.Equal(t, tc.exp, res) - }) - } -} - func Test_MergeKeys(t *testing.T) { t.Parallel() diff --git a/internal/entity/v.go b/internal/entity/v.go new file mode 100644 index 0000000..1198a99 --- /dev/null +++ b/internal/entity/v.go @@ -0,0 +1,13 @@ +package entity + +type AppendVPlusOrV func(s string) string + +func NewAppendVPlusOrV(vPlus bool) AppendVPlusOrV { + return func(s string) string { + const vp, v = "%+v", "%v" + if vPlus { + return s + vp + } + return s + v + } +} diff --git a/internal/exec/chain.go b/internal/exec/chain.go index 503b0cb..dc9f090 100644 --- a/internal/exec/chain.go +++ b/internal/exec/chain.go @@ -1,36 +1,79 @@ package exec import ( + "sync" + "golang.org/x/xerrors" "github.com/kozmod/progen/internal/entity" ) +type Chain struct { + executors []entity.Executor +} + +func NewChain(executors []entity.Executor) *Chain { + return &Chain{executors: executors} +} + +func (c *Chain) Exec() error { + for i, executor := range c.executors { + err := executor.Exec() + if err != nil { + return xerrors.Errorf("execute proc [%d]: %w", i, err) + } + } + return nil +} + type PreprocessingChain struct { - preprocessors []entity.Preprocessor - executors []entity.Executor + preprocessors *Preprocessors + chain *Chain } -func NewPreprocessingChain(preprocessors []entity.Preprocessor, executors []entity.Executor) *PreprocessingChain { +func NewPreprocessingChain(preprocessors *Preprocessors, executors []entity.Executor) *PreprocessingChain { return &PreprocessingChain{ preprocessors: preprocessors, - executors: executors, + chain: NewChain(executors), } } func (c *PreprocessingChain) Exec() error { - for i, preprocessor := range c.preprocessors { + for i, preprocessor := range c.preprocessors.Get() { err := preprocessor.Process() if err != nil { - return xerrors.Errorf("preload [%d]: %w", i, err) + return xerrors.Errorf("preprocess [%d]: %w", i, err) } } + return c.chain.Exec() +} - for i, executor := range c.executors { - err := executor.Exec() - if err != nil { - return xerrors.Errorf("execute proc [%d]: %w", i, err) - } +type Preprocessors struct { + mx sync.RWMutex + val []entity.Preprocessor +} + +func (p *Preprocessors) Add(in ...entity.Preprocessor) { + if p == nil { + return } - return nil + + p.mx.Lock() + defer p.mx.Unlock() + p.val = append(p.val, in...) +} + +func (p *Preprocessors) Get() []entity.Preprocessor { + if p == nil { + return nil + } + + p.mx.RLock() + defer p.mx.RUnlock() + if len(p.val) == 0 { + return nil + } + res := make([]entity.Preprocessor, len(p.val)) + copy(res, p.val) + return res } diff --git a/internal/exec/file.go b/internal/exec/file.go index 78ea2d3..21acc6b 100644 --- a/internal/exec/file.go +++ b/internal/exec/file.go @@ -33,8 +33,8 @@ func (e *FilesExecutor) Exec() error { return xerrors.Errorf("execute file: get file: %w", err) } - for _, processor := range e.strategies { - file, err = processor.Apply(file) + for _, strategy := range e.strategies { + file, err = strategy.Apply(file) if err != nil { return xerrors.Errorf("execute file: process file: %w", err) } diff --git a/internal/exec/fs.go b/internal/exec/fs.go index 8e23311..876b3ce 100644 --- a/internal/exec/fs.go +++ b/internal/exec/fs.go @@ -10,7 +10,7 @@ import ( "github.com/kozmod/progen/internal/entity" ) -type FileSystemStrategy struct { +type FileSystemModifyStrategy struct { logger entity.Logger strategiesFn func(paths map[string]string) []entity.FileStrategy templateProcFn func() entity.TemplateProc @@ -19,12 +19,12 @@ type FileSystemStrategy struct { removeAllFn func(path string) error } -func NewFileSystemStrategy( +func NewFileSystemModifyStrategy( templateData, templateFns map[string]any, templateOptions []string, - logger entity.Logger) *FileSystemStrategy { - return &FileSystemStrategy{ + logger entity.Logger) *FileSystemModifyStrategy { + return &FileSystemModifyStrategy{ logger: logger, strategiesFn: func(paths map[string]string) []entity.FileStrategy { return []entity.FileStrategy{ @@ -46,7 +46,7 @@ func NewFileSystemStrategy( } } -func (e *FileSystemStrategy) Apply(dir string) (string, error) { +func (e *FileSystemModifyStrategy) Apply(dir string) (string, error) { type ( Entity struct { Path string @@ -67,7 +67,7 @@ func (e *FileSystemStrategy) Apply(dir string) (string, error) { } entPath, err := e.templateProcFn().Process(path, path) if err != nil { - return xerrors.Errorf("fs: process template to path [%s]: %w", path, err) + return xerrors.Errorf("fs modify: process template to path [%s]: %w", path, err) } filePaths[path] = entPath entitySet[path] = Entity{ @@ -77,7 +77,7 @@ func (e *FileSystemStrategy) Apply(dir string) (string, error) { return err }) if err != nil { - return entity.Empty, xerrors.Errorf("fs: walk dir [%s]: %w", dir, err) + return entity.Empty, xerrors.Errorf("fs modify: walk dir [%s]: %w", dir, err) } var ( @@ -102,13 +102,13 @@ func (e *FileSystemStrategy) Apply(dir string) (string, error) { dirExec := e.dirExecutorFn(dirs) err = dirExec.Exec() if err != nil { - return entity.Empty, xerrors.Errorf("fs: dirs execute: %w", err) + return entity.Empty, xerrors.Errorf("fs modify: dirs execute: %w", err) } fileExec := e.fileExecutorFn(fileProducers, e.strategiesFn(filePaths)) err = fileExec.Exec() if err != nil { - return entity.Empty, xerrors.Errorf("fs: files execute: %w", err) + return entity.Empty, xerrors.Errorf("fs modify: files execute: %w", err) } for old, ent := range entitySet { @@ -117,25 +117,25 @@ func (e *FileSystemStrategy) Apply(dir string) (string, error) { } err = e.removeAllFn(old) if err != nil { - return entity.Empty, xerrors.Errorf("fs: remove old: %w", err) + return entity.Empty, xerrors.Errorf("fs modify: remove old: %w", err) } - e.logger.Infof("fs: remove: %s", old) + e.logger.Infof("fs modify: remove: %s", old) } return dir, nil } -type DryRunFileSystemStrategy struct { +type DryRunFileSystemModifyStrategy struct { logger entity.Logger } -func NewDryRunFileSystemStrategy(logger entity.Logger) *DryRunFileSystemStrategy { - return &DryRunFileSystemStrategy{ +func NewDryRunFileSystemModifyStrategy(logger entity.Logger) *DryRunFileSystemModifyStrategy { + return &DryRunFileSystemModifyStrategy{ logger: logger, } } -func (e *DryRunFileSystemStrategy) Apply(dir string) (string, error) { - e.logger.Infof("fs: dir execute: %s", dir) +func (e *DryRunFileSystemModifyStrategy) Apply(dir string) (string, error) { + e.logger.Infof("fs modify: dir execute: %s", dir) return dir, nil } diff --git a/internal/exec/fs_test.go b/internal/exec/fs_test.go index 26f6e08..06616c4 100644 --- a/internal/exec/fs_test.go +++ b/internal/exec/fs_test.go @@ -10,7 +10,7 @@ import ( "github.com/kozmod/progen/internal/entity" ) -func Test_FileSystemStrategy(t *testing.T) { +func Test_FileSystemModifyStrategy(t *testing.T) { SkipSLowTest(t) const ( @@ -56,7 +56,7 @@ func Test_FileSystemStrategy(t *testing.T) { CreateFile(t, pathTempB, dataB) CreateFile(t, pathTempC, dataC) - str := FileSystemStrategy{ + str := FileSystemModifyStrategy{ templateProcFn: func() entity.TemplateProc { return entity.NewTemplateProc(templateData, nil, nil) }, @@ -106,7 +106,7 @@ func Test_FileSystemStrategy(t *testing.T) { CreateFile(t, pathTempB, dataB) CreateFile(t, pathTempC, dataC) - str := NewFileSystemStrategy(templateData, nil, nil, mockLogger) + str := NewFileSystemModifyStrategy(templateData, nil, nil, mockLogger) dir, err := str.Apply(tmpDir) a.NoError(err) @@ -122,7 +122,7 @@ func Test_FileSystemStrategy(t *testing.T) { }) } -func Test_DryRunFileSystemStrategy(t *testing.T) { +func Test_DryRunFileSystemModifyStrategy(t *testing.T) { const ( dir = "some_dir" ) @@ -133,7 +133,7 @@ func Test_DryRunFileSystemStrategy(t *testing.T) { assert.Equal(t, []any{dir}, args) }, } - res, err := NewDryRunFileSystemStrategy(mockLogger).Apply(dir) + res, err := NewDryRunFileSystemModifyStrategy(mockLogger).Apply(dir) assert.NoError(t, err) assert.Equal(t, dir, res) } diff --git a/internal/exec/fss.go b/internal/exec/fss.go new file mode 100644 index 0000000..d4ec660 --- /dev/null +++ b/internal/exec/fss.go @@ -0,0 +1,136 @@ +package exec + +import ( + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + + "golang.org/x/xerrors" + + "github.com/kozmod/progen/internal/entity" +) + +type FileSystemSaveStrategy struct { + fs fs.FS + logger entity.Logger + strategiesFn func() []entity.FileStrategy + templateProcFn func() entity.TemplateProc + dirExecutorFn func(dirs []string) entity.Executor + fileExecutorFn func(producers []entity.FileProducer, strategies []entity.FileStrategy) entity.Executor + removeAllFn func(path string) error +} + +func NewFileSystemSaveStrategy( + fs fs.FS, + templateData, + templateFns map[string]any, + templateOptions []string, + logger entity.Logger) *FileSystemSaveStrategy { + return &FileSystemSaveStrategy{ + fs: fs, + logger: logger, + strategiesFn: func() []entity.FileStrategy { + return []entity.FileStrategy{ + NewTemplateFileStrategy(templateData, templateFns, templateOptions), + NewSaveFileStrategy(logger), + } + }, + templateProcFn: func() entity.TemplateProc { + return entity.NewTemplateProc(templateData, templateFns, templateOptions) + }, + dirExecutorFn: func(dirs []string) entity.Executor { + return NewDirExecutor(dirs, []entity.DirStrategy{NewMkdirAllStrategy(logger)}) + }, + fileExecutorFn: func(producers []entity.FileProducer, strategies []entity.FileStrategy) entity.Executor { + return NewFilesExecutor(producers, strategies) + }, + removeAllFn: os.RemoveAll, + } +} + +func (e *FileSystemSaveStrategy) Apply(targetDir string) (string, error) { + var ( + dirs []string + fileProducers []entity.FileProducer + root = entity.Dot + ) + + err := fs.WalkDir(e.fs, root, func(path string, info fs.DirEntry, err error) error { + if info == nil { + return err + } + + entPath, err := e.templateProcFn().Process(path, path) + if err != nil { + return xerrors.Errorf("fs save: process template to path [%s]: %w", path, err) + } + + entPath = filepath.Join(targetDir, entPath) + + var ( + srcFile fs.File + data []byte + ) + switch { + case info.IsDir(): + dirs = append(dirs, entPath) + default: + srcFile, err = e.fs.Open(path) + if err != nil { + return fmt.Errorf("fs save: open fs file [%s]: %v", path, err) + } + defer func() { + _ = srcFile.Close() + }() + + data, err = io.ReadAll(srcFile) + if err != nil { + return fmt.Errorf("fs save: read fs file [%s]: %v", path, err) + } + + fileProducers = append(fileProducers, + NewDummyProducer( + entity.DataFile{ + FileInfo: entity.NewFileInfo(entPath), + Data: data, + }, + ), + ) + } + return err + }) + if err != nil { + return entity.Empty, xerrors.Errorf("fs save: walk dir: %w", err) + } + + dirExec := e.dirExecutorFn(dirs) + err = dirExec.Exec() + if err != nil { + return entity.Empty, xerrors.Errorf("fs save: dirs execute: %w", err) + } + + fileExec := e.fileExecutorFn(fileProducers, e.strategiesFn()) + err = fileExec.Exec() + if err != nil { + return entity.Empty, xerrors.Errorf("fs save: files execute: %w", err) + } + + return targetDir, nil +} + +type DryRunFileSystemSaveStrategy struct { + logger entity.Logger +} + +func NewDryRunFileSystemSaveStrategy(logger entity.Logger) *DryRunFileSystemSaveStrategy { + return &DryRunFileSystemSaveStrategy{ + logger: logger, + } +} + +func (e *DryRunFileSystemSaveStrategy) Apply(dir string) (string, error) { + e.logger.Infof("fs save: dir execute: %s", dir) + return dir, nil +} diff --git a/internal/exec/fss_test.go b/internal/exec/fss_test.go new file mode 100644 index 0000000..b2365d2 --- /dev/null +++ b/internal/exec/fss_test.go @@ -0,0 +1,135 @@ +package exec + +import ( + "fmt" + "os" + "path/filepath" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" +) + +func Test_FileSystemSaveStrategy(t *testing.T) { + SkipSLowTest(t) + + const ( + pathA = "some_file.txt" + pathB = "{{ .var }}/some_file.txt" + pathC = "c/{{ .var }}.txt" + + varTemplateVariableValue = "DATA" + ) + + var ( + dataA = []byte("{{ .var }}") + dataB = []byte("B") + dataC = []byte("C") + + templateData = map[string]any{ + "var": varTemplateVariableValue, + } + + expectedSubPathB = fmt.Sprintf("%s/some_file.txt", varTemplateVariableValue) + expectedSubPathC = fmt.Sprintf("c/%s.txt", varTemplateVariableValue) + ) + t.Run("real_save_fs", func(t *testing.T) { + WithTempDir(t, func(tmpDir string) { + var ( + tmpDirTarget = filepath.Join(tmpDir, "result") + + expectedPathA = filepath.Join(tmpDirTarget, pathA) + expectedPathB = filepath.Join(tmpDirTarget, expectedSubPathB) + expectedPathC = filepath.Join(tmpDirTarget, expectedSubPathC) + + a = assert.New(t) + mockLogger = MockLogger{ + infof: func(format string, args ...any) { + a.NotEmpty(format) + }, + } + ) + + fs := fstest.MapFS{ + pathA: { + Data: dataA, + }, + pathB: { + Data: dataB, + }, + pathC: { + Data: dataC, + }, + } + + str := NewFileSystemSaveStrategy(fs, templateData, nil, nil, mockLogger) + + dir, err := str.Apply(tmpDirTarget) + a.NoError(err) + a.Equal(tmpDirTarget, dir) + a.FileExists(expectedPathA) + a.FileExists(expectedPathB) + a.FileExists(expectedPathC) + + AssertFileDataEqual(t, expectedPathA, []byte(varTemplateVariableValue)) + AssertFileDataEqual(t, expectedPathB, dataB) + AssertFileDataEqual(t, expectedPathC, dataC) + }) + }) + t.Run("save_from_real_fs", func(t *testing.T) { + WithTempDir(t, func(tmpDir string) { + var ( + pathTempA = filepath.Join(tmpDir, pathA) + pathTempB = filepath.Join(tmpDir, pathB) + pathTempC = filepath.Join(tmpDir, pathC) + + a = assert.New(t) + mockLogger = MockLogger{ + infof: func(format string, args ...any) { + a.NotEmpty(format) + }, + } + tmpDirTarget = filepath.Join(tmpDir, "result") + + expectedPathA = filepath.Join(tmpDirTarget, pathA) + expectedPathB = filepath.Join(tmpDirTarget, expectedSubPathB) + expectedPathC = filepath.Join(tmpDirTarget, expectedSubPathC) + ) + + CreateFile(t, pathTempA, dataA) + CreateFile(t, pathTempB, dataB) + CreateFile(t, pathTempC, dataC) + + fs := os.DirFS(tmpDir) + + str := NewFileSystemSaveStrategy(fs, templateData, nil, nil, mockLogger) + + dir, err := str.Apply(tmpDirTarget) + a.NoError(err) + a.Equal(tmpDirTarget, dir) + a.FileExists(expectedPathA) + a.FileExists(expectedPathB) + a.FileExists(expectedPathC) + + AssertFileDataEqual(t, expectedPathA, []byte(varTemplateVariableValue)) + AssertFileDataEqual(t, expectedPathB, dataB) + AssertFileDataEqual(t, expectedPathC, dataC) + }) + }) +} + +func Test_DryRunFileSystemSaveStrategy(t *testing.T) { + const ( + dir = "some_dir" + ) + mockLogger := MockLogger{ + infof: func(format string, args ...any) { + assert.NotEmpty(t, format) + assert.NotEmpty(t, args) + assert.Equal(t, []any{dir}, args) + }, + } + res, err := NewDryRunFileSystemSaveStrategy(mockLogger).Apply(dir) + assert.NoError(t, err) + assert.Equal(t, dir, res) +} diff --git a/internal/exec/helper_test.go b/internal/exec/helper_test.go index b6afbad..cf2f663 100644 --- a/internal/exec/helper_test.go +++ b/internal/exec/helper_test.go @@ -43,6 +43,7 @@ func CreateFile(t *testing.T, path string, data []byte) { assert.NoError(t, err) } + //nolint:gosec err := os.WriteFile(path, data, os.ModePerm) assert.NoError(t, err) } diff --git a/internal/factory/action_filter.go b/internal/factory/action_filter.go index a1b6489..438de57 100644 --- a/internal/factory/action_filter.go +++ b/internal/factory/action_filter.go @@ -7,14 +7,15 @@ import ( "github.com/kozmod/progen/internal/entity" ) -type ( - actionFilter interface { - MatchString(s string) bool - } -) +type DummyActionFilter struct { +} + +func (f DummyActionFilter) MatchString(_ string) bool { + return true +} -type ActionFilter struct { - skipFilter actionFilter +type FacadeActionFilter struct { + skipFilter entity.ActionFilter selectedGroups map[string]struct{} groupsByAction map[string]map[string]struct{} manualActions map[string]struct{} @@ -29,7 +30,7 @@ func NewActionFilter( manualActionsSet map[string]struct{}, logger entity.Logger, -) *ActionFilter { +) *FacadeActionFilter { selectedGroupsSet := entity.SliceSet(selectedGroups) skipActions = slices.Compact(skipActions) @@ -44,7 +45,7 @@ func NewActionFilter( logger.Infof("manual actions will be skipped: [%s]", strings.Join(manualActions, entity.LogSliceSep)) } - return &ActionFilter{ + return &FacadeActionFilter{ skipFilter: entity.NewRegexpChain(skipActions...), selectedGroups: selectedGroupsSet, groupsByAction: groupsByAction, @@ -53,7 +54,7 @@ func NewActionFilter( } } -func (f *ActionFilter) MatchString(action string) bool { +func (f *FacadeActionFilter) MatchString(action string) bool { if f.skipFilter.MatchString(action) { f.logger.Infof("action will be skipped: [%s]", action) return false diff --git a/internal/factory/chain_exec.go b/internal/factory/chain_exec.go index 52f4fb2..ea15243 100644 --- a/internal/factory/chain_exec.go +++ b/internal/factory/chain_exec.go @@ -5,150 +5,109 @@ import ( "golang.org/x/xerrors" - "github.com/kozmod/progen/internal/config" "github.com/kozmod/progen/internal/entity" - "github.com/kozmod/progen/internal/exec" ) -func NewExecutorChain( - conf config.Config, - actionFilter actionFilter, - templateData map[string]any, - templateOptions []string, +type ( + executorBuilderFactory interface { + Create(logger entity.Logger, dryRun bool) []entity.ExecutorBuilder + } +) + +type ExecutorChainFactory struct { + logger entity.Logger + dryRun bool + + executorBuilderFactories []executorBuilderFactory + createFn func([]entity.Executor) entity.Executor +} + +func NewExecutorChainFactory( logger entity.Logger, - preprocess, dryRun bool, -) (entity.Executor, error) { + createFn func([]entity.Executor) entity.Executor, + executorBuilderFactories ...executorBuilderFactory, - type ( - ExecutorBuilder struct { - action string - line int32 - procFn func() (entity.Executor, error) - } +) *ExecutorChainFactory { + return &ExecutorChainFactory{ + createFn: createFn, + logger: logger, + dryRun: dryRun, + executorBuilderFactories: executorBuilderFactories, + } +} + +func (f ExecutorChainFactory) Create() (entity.Executor, error) { + var ( + allBuilders []entity.ExecutorBuilder ) + for _, factory := range f.executorBuilderFactories { + builder := factory.Create(f.logger, f.dryRun) + allBuilders = append(allBuilders, builder...) + } - builders := make([]ExecutorBuilder, 0, len(conf.Dirs)+len(conf.Files)+len(conf.Cmd)+len(conf.FS)) - for _, dirs := range conf.Dirs { - var ( - d = dirs - action = d.Tag - ) + sort.Slice(allBuilders, func(i, j int) bool { + return allBuilders[i].Priority < allBuilders[j].Priority + }) - if !actionFilter.MatchString(action) { - continue + executors := make([]entity.Executor, 0, len(allBuilders)) + for _, builder := range allBuilders { + e, err := builder.ProcFn() + if err != nil { + return nil, xerrors.Errorf("configure executor [%s]: %w", builder.Action, err) } - builders = append(builders, - ExecutorBuilder{ - action: action, - line: d.Line, - procFn: func() (entity.Executor, error) { - return NewMkdirExecutor(d.Val, logger, dryRun) - }, - }) - } - for _, rm := range conf.Rm { - var ( - r = rm - action = r.Tag - ) - - if !actionFilter.MatchString(action) { + if e == nil { continue } - builders = append(builders, - ExecutorBuilder{ - action: action, - line: r.Line, - procFn: func() (entity.Executor, error) { - return NewRmExecutor(r.Val, logger, dryRun) - }, - }) + executors = append(executors, e) } - var preprocessors []entity.Preprocessor - for _, files := range conf.Files { - var ( - f = files - action = f.Tag - ) - if !actionFilter.MatchString(action) { - continue - } - builders = append(builders, - ExecutorBuilder{ - action: action, - line: f.Line, - procFn: func() (entity.Executor, error) { - executor, l, err := NewFileExecutor( - f.Val, - conf.Settings.HTTP, - templateData, - templateOptions, - logger, - preprocess, - dryRun) - preprocessors = append(preprocessors, l...) - return executor, err - }, - }) - } + return f.createFn(executors), nil +} - for _, commands := range conf.Cmd { - var ( - cmd = commands - action = cmd.Tag - ) - if !actionFilter.MatchString(action) { - continue - } - builders = append(builders, - ExecutorBuilder{ - action: action, - line: cmd.Line, - procFn: func() (entity.Executor, error) { - return NewRunCommandExecutor(cmd.Val, logger, dryRun) - }, - }) - } - for _, path := range conf.FS { +type ( + actionValConsumer[T any] func(vals []T, logger entity.Logger, dryRun bool) (entity.Executor, error) +) + +type ExecutorBuilderFactory[T any] struct { + actionsSupplier []entity.Action[[]T] + actionValConsumer actionValConsumer[T] + actionFilter entity.ActionFilter +} + +func NewExecutorBuilderFactory[T any]( + actionSupplier []entity.Action[[]T], + actionValConsumer actionValConsumer[T], + actionFilter entity.ActionFilter, +) *ExecutorBuilderFactory[T] { + return &ExecutorBuilderFactory[T]{ + actionsSupplier: actionSupplier, + actionValConsumer: actionValConsumer, + actionFilter: actionFilter} +} + +func (y ExecutorBuilderFactory[T]) Create(logger entity.Logger, dryRun bool) []entity.ExecutorBuilder { + var ( + actions = y.actionsSupplier + builders = make([]entity.ExecutorBuilder, 0, len(actions)) + ) + for _, action := range actions { var ( - fs = path - action = fs.Tag + a = action + name = a.Name ) - if actionFilter.MatchString(action) { + if !y.actionFilter.MatchString(name) { continue } builders = append(builders, - ExecutorBuilder{ - action: action, - line: fs.Line, - procFn: func() (entity.Executor, error) { - return NewFSExecutor( - fs.Val, - templateData, - templateOptions, - logger, - dryRun) + entity.ExecutorBuilder{ + Action: name, + Priority: a.Priority, + ProcFn: func() (entity.Executor, error) { + executor, err := y.actionValConsumer(a.Val, logger, dryRun) + return executor, err }, }) } - - sort.Slice(builders, func(i, j int) bool { - return builders[i].line < builders[j].line - }) - - executors := make([]entity.Executor, 0, len(builders)) - for _, builder := range builders { - e, err := builder.procFn() - if err != nil { - return nil, xerrors.Errorf("configure executor [%s]: %w", builder.action, err) - } - if e == nil { - continue - } - executors = append(executors, e) - } - - return exec.NewPreprocessingChain(preprocessors, executors), nil + return builders } diff --git a/internal/factory/cmd_exec.go b/internal/factory/cmd_exec.go index e99173d..291e40d 100644 --- a/internal/factory/cmd_exec.go +++ b/internal/factory/cmd_exec.go @@ -3,13 +3,12 @@ package factory import ( "strings" - "github.com/kozmod/progen/internal/config" "github.com/kozmod/progen/internal/entity" "github.com/kozmod/progen/internal/exec" ) //goland:noinspection SpellCheckingInspection -func NewRunCommandExecutor(cmds []config.Command, logger entity.Logger, dryRun bool) (entity.Executor, error) { +func NewRunCommandExecutor(cmds []entity.Command, logger entity.Logger, dryRun bool) (entity.Executor, error) { if len(cmds) == 0 { logger.Infof("`cmd` section is empty") return nil, nil @@ -26,7 +25,7 @@ func NewRunCommandExecutor(cmds []config.Command, logger entity.Logger, dryRun b args = append(args, cmd.Args...) commands = append(commands, entity.Command{ - Cmd: cmd.Exec, + Cmd: cmd.Cmd, Args: args, Dir: dir, }) diff --git a/internal/factory/dir_exec.go b/internal/factory/dir_exec.go index 9a8640d..e85639c 100644 --- a/internal/factory/dir_exec.go +++ b/internal/factory/dir_exec.go @@ -1,6 +1,8 @@ package factory import ( + "slices" + "github.com/kozmod/progen/internal/entity" "github.com/kozmod/progen/internal/exec" ) @@ -11,7 +13,7 @@ func NewMkdirExecutor(dirs []string, logger entity.Logger, dryRun bool) (entity. return nil, nil } - dirSet := entity.Unique(dirs) + dirSet := slices.Compact(dirs) if dryRun { return exec.NewDirExecutor(dirSet, []entity.DirStrategy{exec.NewDryRunMkdirAllStrategy(logger)}), nil diff --git a/internal/factory/file_exec.go b/internal/factory/file_exec.go index 8399fbd..182da2b 100644 --- a/internal/factory/file_exec.go +++ b/internal/factory/file_exec.go @@ -1,26 +1,87 @@ package factory import ( - resty "github.com/go-resty/resty/v2" "golang.org/x/xerrors" - "github.com/kozmod/progen/internal/config" + resty "github.com/go-resty/resty/v2" + "github.com/kozmod/progen/internal/entity" "github.com/kozmod/progen/internal/exec" ) -func NewFileExecutor( - files []config.File, - http *config.HTTPClient, +type FileExecutorFactory struct { + templateData map[string]any + templateOptions []string +} + +func NewFileExecutorFactory( + templateData map[string]any, + templateOptions []string, +) *FileExecutorFactory { + return &FileExecutorFactory{ + templateData: templateData, + templateOptions: templateOptions, + } +} + +func (ff *FileExecutorFactory) Create(files []entity.UndefinedFile, logger entity.Logger, dryRun bool) (entity.Executor, error) { + if len(files) == 0 { + logger.Infof("`files` section is empty") + return nil, nil + } + + producers := make([]entity.FileProducer, 0, len(files)) + for _, f := range files { + file := entity.DataFile{ + FileInfo: entity.NewFileInfo(f.Path), + Data: *f.Data, + } + producer := exec.NewDummyProducer(file) + producers = append(producers, producer) + } + + strategies := []entity.FileStrategy{exec.NewTemplateFileStrategy(ff.templateData, entity.TemplateFnsMap, ff.templateOptions)} + + switch { + case dryRun: + strategies = append(strategies, exec.NewDryRunFileStrategy(logger)) + default: + strategies = append(strategies, exec.NewSaveFileStrategy(logger)) + } + executor := exec.NewFilesExecutor(producers, strategies) + + return executor, nil +} + +type PreprocessorsFileExecutorFactory struct { + templateData map[string]any + templateOptions []string + + preprocess bool + preprocessors *exec.Preprocessors + httpClientSupplier func(logger entity.Logger) *resty.Client +} + +func NewPreprocessorsFileExecutorFactory( templateData map[string]any, templateOptions []string, - logger entity.Logger, - preprocess, - dryRun bool, -) (entity.Executor, []entity.Preprocessor, error) { + preprocess bool, + preprocessors *exec.Preprocessors, + httpClientSupplier func(logger entity.Logger) *resty.Client, +) *PreprocessorsFileExecutorFactory { + return &PreprocessorsFileExecutorFactory{ + templateData: templateData, + templateOptions: templateOptions, + preprocess: preprocess, + preprocessors: preprocessors, + httpClientSupplier: httpClientSupplier, + } +} + +func (ff *PreprocessorsFileExecutorFactory) Create(files []entity.UndefinedFile, logger entity.Logger, dryRun bool) (entity.Executor, error) { if len(files) == 0 { logger.Infof("`files` section is empty") - return nil, nil, nil + return nil, nil } producers := make([]entity.FileProducer, 0, len(files)) @@ -50,7 +111,7 @@ func NewFileExecutor( } if client == nil { - client = NewHTTPClient(http, logger) + client = ff.httpClientSupplier(logger) } producer = exec.NewRemoteProducer(file, client) @@ -62,24 +123,26 @@ func NewFileExecutor( producer = exec.NewLocalProducer(file) default: - return nil, nil, xerrors.Errorf("build file executor: one of `data`, `get`, `local` must not be empty") + return nil, xerrors.Errorf("build file executor from config: one of `data`, `get`, `local` must not be empty") } producers = append(producers, producer) } - var preprocessors []entity.Preprocessor - if preprocess { + if preprocess := ff.preprocess; preprocess { + if ff.preprocessors == nil { + return nil, xerrors.Errorf("creating file executor - preprocesso is nil [preprocess:%v]", preprocess) + } preloadProducers := make([]entity.FileProducer, 0, len(producers)) preloader := exec.NewPreloadProducer(producers, logger) for i := 0; i < len(producers); i++ { preloadProducers = append(preloadProducers, preloader) } producers = preloadProducers - preprocessors = append(preprocessors, preloader) + ff.preprocessors.Add(preloader) } - strategies := []entity.FileStrategy{exec.NewTemplateFileStrategy(templateData, entity.TemplateFnsMap, templateOptions)} + strategies := []entity.FileStrategy{exec.NewTemplateFileStrategy(ff.templateData, entity.TemplateFnsMap, ff.templateOptions)} switch { case dryRun: @@ -89,5 +152,5 @@ func NewFileExecutor( } executor := exec.NewFilesExecutor(producers, strategies) - return executor, preprocessors, nil + return executor, nil } diff --git a/internal/factory/fs_exec.go b/internal/factory/fs_exec.go index ed71d8c..0fdda99 100644 --- a/internal/factory/fs_exec.go +++ b/internal/factory/fs_exec.go @@ -1,32 +1,48 @@ package factory import ( + "slices" + "github.com/kozmod/progen/internal/entity" "github.com/kozmod/progen/internal/exec" ) -func NewFSExecutor( - dirs []string, +type FsModifyExecFactory struct { + templateData map[string]any + templateOptions []string +} + +func NewFsModifyExecFactory( templateData map[string]any, templateOptions []string, +) *FsModifyExecFactory { + return &FsModifyExecFactory{ + templateData: templateData, + templateOptions: templateOptions, + } +} + +func (f FsModifyExecFactory) Create( + dirs []string, logger entity.Logger, - dryRun bool) (entity.Executor, error) { + dryRun bool, +) (entity.Executor, error) { if len(dirs) == 0 { logger.Infof("fs executor: `dir` section is empty") return nil, nil } - dirSet := entity.Unique(dirs) + dirSet := slices.Compact(dirs) if dryRun { - return exec.NewDirExecutor(dirSet, []entity.DirStrategy{exec.NewDryRunFileSystemStrategy(logger)}), nil + return exec.NewDirExecutor(dirSet, []entity.DirStrategy{exec.NewDryRunFileSystemModifyStrategy(logger)}), nil } return exec.NewDirExecutor(dirSet, []entity.DirStrategy{ - exec.NewFileSystemStrategy( - templateData, + exec.NewFileSystemModifyStrategy( + f.templateData, entity.TemplateFnsMap, - templateOptions, + f.templateOptions, logger), }), nil } diff --git a/internal/factory/fss_exec.go b/internal/factory/fss_exec.go new file mode 100644 index 0000000..66f0c29 --- /dev/null +++ b/internal/factory/fss_exec.go @@ -0,0 +1,65 @@ +package factory + +import ( + "github.com/kozmod/progen/internal/entity" + "github.com/kozmod/progen/internal/exec" +) + +type FsSaveExecFactory struct { + templateData map[string]any + templateOptions []string +} + +func NewFsSaveExecFactory( + templateData map[string]any, + templateOptions []string, +) *FsSaveExecFactory { + return &FsSaveExecFactory{ + templateData: templateData, + templateOptions: templateOptions, + } +} + +func (f FsSaveExecFactory) Create( + fsList []entity.TargetFs, + logger entity.Logger, + dryRun bool, +) (entity.Executor, error) { + if len(fsList) == 0 { + logger.Infof("fs executor: `fs save` section is empty") + return nil, nil + } + + fsStrategyBydDir := make(map[string][]entity.DirStrategy, len(fsList)) + for _, targetFs := range fsList { + fsStrategyBydDir[targetFs.TargetDir] = append( + fsStrategyBydDir[targetFs.TargetDir], + exec.NewFileSystemSaveStrategy( + targetFs.Fs, + f.templateData, + entity.TemplateFnsMap, + f.templateOptions, + logger), + ) + } + + executors := make([]entity.Executor, 0, len(fsStrategyBydDir)) + for dir, strategy := range fsStrategyBydDir { + dirs := []string{dir} + if dryRun { + executors = append(executors, + exec.NewDirExecutor(dirs, []entity.DirStrategy{exec.NewDryRunFileSystemSaveStrategy(logger)}), + ) + continue + } + executors = append(executors, + exec.NewDirExecutor( + dirs, + strategy, + ), + ) + + } + + return exec.NewChain(executors), nil +} diff --git a/internal/factory/rm_exec.go b/internal/factory/rm_exec.go index 0d02a21..aac8fa2 100644 --- a/internal/factory/rm_exec.go +++ b/internal/factory/rm_exec.go @@ -1,6 +1,8 @@ package factory import ( + "slices" + "github.com/kozmod/progen/internal/entity" "github.com/kozmod/progen/internal/exec" ) @@ -11,7 +13,7 @@ func NewRmExecutor(paths []string, logger entity.Logger, dryRun bool) (entity.Ex return nil, nil } - pathsSet := entity.Unique(paths) + pathsSet := slices.Compact(paths) if dryRun { return exec.NewRmAllExecutor(pathsSet, []entity.RmStrategy{exec.NewDryRmAllStrategy(logger)}), nil diff --git a/internal/flag/flags.go b/internal/flag/flags.go index 998f337..0adbb48 100644 --- a/internal/flag/flags.go +++ b/internal/flag/flags.go @@ -34,19 +34,24 @@ var ( ErrDashFlagNotLast = fmt.Errorf(`'-' must be the last argument`) ) -type Flags struct { - ConfigPath string +type DefaultFlags struct { Verbose bool DryRun bool + TemplateVars TemplateVarsFlag + MissingKey MissingKeyFlag + PrintErrorStackTrace bool +} + +type Flags struct { + *DefaultFlags + + ConfigPath string Version bool ReadStdin bool - TemplateVars TemplateVarsFlag AWD string // AWD application working directory Skip SkipFlag PreprocessFiles bool - MissingKey MissingKeyFlag Group GroupFlag - PrintErrorStackTrace bool PrintProcessedConfig bool } @@ -65,52 +70,79 @@ func (f *Flags) FileLocationMessage() string { func Parse() Flags { args := os.Args - flags, err := parseFlags(flag.NewFlagSet(args[0], flag.ExitOnError), args[1:]) + flagSet := flag.NewFlagSet(args[0], flag.ExitOnError) + + var ( + flags = Flags{ + DefaultFlags: NewDefaultFlags(flagSet), + } + ) + + err := flags.Parse(flagSet, args[1:]) if err != nil { - log.Fatalf("parse flags: %v", err) + log.Fatalf("parse additioanl flags: %v", err) } return flags } -func parseFlags(fs *flag.FlagSet, args []string) (Flags, error) { - var ( - f Flags - ) - fs.StringVar( - &f.ConfigPath, - flagKeyConfigFile, - defaultConfigFilePath, - "configuration file path") +func NewDefaultFlags(fs *flag.FlagSet) *DefaultFlags { + var f DefaultFlags fs.BoolVar( &f.Verbose, flagKeyVarbose, false, "verbose output") - //goland:noinspection SpellCheckingInspection fs.BoolVar( &f.PrintErrorStackTrace, flagKeyErrorStackTrace, false, "output errors stacktrace") - fs.BoolVar( - &f.PrintProcessedConfig, - flagKeyPrintConfig, - false, - "output processed config") fs.BoolVar( &f.DryRun, flagKeyDryRun, false, "dry run mode (can be combine with `-v`)") + fs.Var( + &f.MissingKey, + flagKeyMissingKey, + fmt.Sprintf( + "`missingkey` template option: %v, %v, %v, %v", + entity.MissingKeyDefault, + entity.MissingKeyInvalid, + entity.MissingKeyZero, + entity.MissingKeyError, + ), + ) + return &f +} + +func (f *DefaultFlags) Parse(fs *flag.FlagSet, args []string) error { + err := fs.Parse(args) + if err != nil { + return xerrors.Errorf("parse args: %w", err) + } + return nil +} + +func NewFlags(fs *flag.FlagSet) *Flags { + var ( + f = Flags{DefaultFlags: NewDefaultFlags(fs)} + ) + fs.StringVar( + &f.ConfigPath, + flagKeyConfigFile, + defaultConfigFilePath, + "configuration file path") + fs.BoolVar( + &f.PrintProcessedConfig, + flagKeyPrintConfig, + false, + "output processed config") fs.BoolVar( &f.Version, flagKeyVersion, false, "output version") - fs.Var( - &f.TemplateVars, - flagKeyTemplateVariables, - "template variables (override config variables tree)") fs.StringVar( &f.AWD, flagKeyApplicationWorkingDirectory, @@ -125,25 +157,19 @@ func parseFlags(fs *flag.FlagSet, args []string) (Flags, error) { flagKeyPreprocessingAllFiles, true, "preprocessing all files before saving") - fs.Var( - &f.MissingKey, - flagKeyMissingKey, - fmt.Sprintf( - "`missingkey` template option: %v, %v, %v, %v", - entity.MissingKeyDefault, - entity.MissingKeyInvalid, - entity.MissingKeyZero, - entity.MissingKeyError, - ), - ) fs.Var( &f.Group, flagKeyGroup, "list of executing groups", ) + + return &f +} + +func (f *Flags) Parse(fs *flag.FlagSet, args []string) error { err := fs.Parse(args) if err != nil { - return f, err + return xerrors.Errorf("parse args: %w", err) } for i, arg := range args { @@ -152,9 +178,8 @@ func parseFlags(fs *flag.FlagSet, args []string) (Flags, error) { f.ReadStdin = true break } - return f, xerrors.Errorf("%w", ErrDashFlagNotLast) + return xerrors.Errorf("%w", ErrDashFlagNotLast) } } - - return f, nil + return nil } diff --git a/internal/flag/flags_test.go b/internal/flag/flags_test.go index 0870bd8..c5565af 100644 --- a/internal/flag/flags_test.go +++ b/internal/flag/flags_test.go @@ -301,59 +301,113 @@ func Test_parseFlags(t *testing.T) { ) t.Run("success", func(t *testing.T) { - testFs := flag.NewFlagSet(fsName, flag.ContinueOnError) - flags, err := parseFlags(testFs, []string{v, printconfig, errstack, dr, f, configPath}) + var ( + testFs = flag.NewFlagSet(fsName, flag.ContinueOnError) + + flags = NewFlags(testFs) + ) + err := flags.Parse(testFs, []string{v, printconfig, errstack, dr, f, configPath}) assert.NoError(t, err) assert.Equal(t, Flags{ - Verbose: true, - PrintErrorStackTrace: true, + DefaultFlags: &DefaultFlags{ + DryRun: true, + Verbose: true, + PrintErrorStackTrace: true, + }, PrintProcessedConfig: true, PreprocessFiles: true, - DryRun: true, ConfigPath: configPath, AWD: dot}, - flags) + *flags) }) t.Run("success_when_dash_last", func(t *testing.T) { - testFs := flag.NewFlagSet(fsName, flag.ContinueOnError) - flags, err := parseFlags(testFs, []string{v, dr, dash}) + var ( + testFs = flag.NewFlagSet(fsName, flag.ContinueOnError) + + flags = NewFlags(testFs) + ) + err := flags.Parse(testFs, []string{v, dr, dash}) assert.NoError(t, err) assert.Equal(t, - Flags{Verbose: true, PreprocessFiles: true, DryRun: true, ConfigPath: configPath, AWD: dot, ReadStdin: true}, - flags) + Flags{ + DefaultFlags: &DefaultFlags{ + Verbose: true, + DryRun: true, + }, + PreprocessFiles: true, + ConfigPath: configPath, + AWD: dot, + ReadStdin: true, + }, + *flags) }) t.Run("success_when_dash_last_and_before_less_than", func(t *testing.T) { - testFs := flag.NewFlagSet(fsName, flag.ContinueOnError) - flags, err := parseFlags(testFs, []string{v, dr, dash, lessThan, configPath}) + var ( + testFs = flag.NewFlagSet(fsName, flag.ContinueOnError) + + flags = NewFlags(testFs) + ) + err := flags.Parse(testFs, []string{v, dr, dash, lessThan, configPath}) assert.NoError(t, err) assert.Equal(t, - Flags{Verbose: true, PreprocessFiles: true, DryRun: true, ConfigPath: configPath, AWD: dot, ReadStdin: true}, - flags) + Flags{ + DefaultFlags: &DefaultFlags{ + Verbose: true, + DryRun: true, + }, + PreprocessFiles: true, + ConfigPath: configPath, + AWD: dot, + ReadStdin: true, + }, + *flags) }) t.Run("error_when_flag_not_specified", func(t *testing.T) { - testFs := flag.NewFlagSet(fsName, flag.ContinueOnError) + var ( + testFs = flag.NewFlagSet(fsName, flag.ContinueOnError) + + flags = NewFlags(testFs) + ) + testFs.SetOutput(MockWriter{ assertWriteFn: func(p []byte) { assert.NotEmpty(t, p) }, }) - _, err := parseFlags(testFs, []string{v, dr, f}) + err := flags.Parse(testFs, []string{v, dr, f}) assert.Error(t, err) }) t.Run("error_when_dash_flag_not_last", func(t *testing.T) { - testFs := flag.NewFlagSet(fsName, flag.ContinueOnError) - _, err := parseFlags(testFs, []string{v, dash, dr, f, configPath}) + var ( + testFs = flag.NewFlagSet(fsName, flag.ContinueOnError) + + flags = NewFlags(testFs) + ) + err := flags.Parse(testFs, []string{v, dash, dr, f, configPath}) assert.Error(t, err) assert.True(t, errors.Is(err, ErrDashFlagNotLast)) }) t.Run("success_with_preprocess_flag_false", func(t *testing.T) { - testFs := flag.NewFlagSet(fsName, flag.ContinueOnError) - flags, err := parseFlags(testFs, []string{fmt.Sprintf("%s=%v", pf, false)}) + var ( + testFs = flag.NewFlagSet(fsName, flag.ContinueOnError) + + flags = NewFlags(testFs) + ) + err := flags.Parse(testFs, []string{fmt.Sprintf("%s=%v", pf, false)}) assert.NoError(t, err) assert.Equal(t, - Flags{Verbose: false, PreprocessFiles: false, DryRun: false, ConfigPath: configPath, AWD: dot, ReadStdin: false}, - flags) + Flags{ + DefaultFlags: &DefaultFlags{ + Verbose: false, + DryRun: false, + }, + PreprocessFiles: false, + ConfigPath: configPath, + AWD: dot, + ReadStdin: false, + }, + *flags) }) t.Run("missingkey", func(t *testing.T) { t.Run("success_with_set_default", func(t *testing.T) { @@ -361,36 +415,45 @@ func Test_parseFlags(t *testing.T) { missingKeyValueDefault = entity.MissingKeyDefault missingKeyValue = fmt.Sprintf("%s=%v", missingkey, missingKeyValueDefault) missingKeyValueExp = missingKeyValue[1:] + + testFs = flag.NewFlagSet(fsName, flag.ContinueOnError) + + flags = NewFlags(testFs) ) - testFs := flag.NewFlagSet(fsName, flag.ContinueOnError) - flags, err := parseFlags(testFs, []string{missingKeyValue}) + err := flags.Parse(testFs, []string{missingKeyValue}) assert.NoError(t, err) assert.Equal(t, Flags{ - Verbose: false, + DefaultFlags: &DefaultFlags{ + Verbose: false, + DryRun: false, + MissingKey: MissingKeyFlag(missingKeyValueDefault), + }, PreprocessFiles: true, - DryRun: false, ConfigPath: configPath, AWD: dot, ReadStdin: false, - MissingKey: MissingKeyFlag(missingKeyValueDefault)}, - flags) + }, + *flags) assert.Equal(t, missingKeyValueExp, flags.MissingKey.String()) }) t.Run("error_with_set_unexpected", func(t *testing.T) { var ( missingKeyValueDefault = "xxx_unexpected_xxx" missingKeyValue = fmt.Sprintf("%s=%v", missingkey, missingKeyValueDefault) + + testFs = flag.NewFlagSet(fsName, flag.ContinueOnError) + + flags = NewFlags(testFs) ) - testFs := flag.NewFlagSet(fsName, flag.ContinueOnError) testFs.SetOutput(MockWriter{ assertWriteFn: func(p []byte) { assert.NotEmpty(t, p) }, }) - _, err := parseFlags(testFs, []string{missingKeyValue}) + err := flags.Parse(testFs, []string{missingKeyValue}) assert.Error(t, err) }) }) diff --git a/main.go b/main.go index 64de71c..4d2eb03 100644 --- a/main.go +++ b/main.go @@ -6,12 +6,14 @@ import ( "os" "time" + "github.com/go-resty/resty/v2" + "github.com/kozmod/progen/internal/exec" + "github.com/kozmod/progen/internal/factory" "golang.org/x/xerrors" "github.com/kozmod/progen/internal" "github.com/kozmod/progen/internal/config" "github.com/kozmod/progen/internal/entity" - "github.com/kozmod/progen/internal/factory" "github.com/kozmod/progen/internal/flag" ) @@ -24,13 +26,7 @@ func main() { return } - logFatalSuffixFn := func(s string) string { - const pv, v = "%+v", "%v" - if flags.PrintErrorStackTrace { - return s + pv - } - return s + v - } + logFatalSuffixFn := entity.NewAppendVPlusOrV(flags.PrintErrorStackTrace) logger, err := factory.NewLogger(flags.Verbose) if err != nil { @@ -42,13 +38,15 @@ func main() { { if err = os.Chdir(flags.AWD); err != nil { - logger.Fatalf(logFatalSuffixFn("changes the application working directory: "), xerrors.Errorf("%w", err)) + logger.Errorf(logFatalSuffixFn("changes the application working directory: "), xerrors.Errorf("%w", err)) + return } var awd string awd, err = os.Getwd() if err != nil { - logger.Fatalf(logFatalSuffixFn("get the application working directory: "), xerrors.Errorf("%w", err)) + logger.Errorf(logFatalSuffixFn("get the application working directory: "), xerrors.Errorf("%w", err)) + return } logger.Infof("application working directory: %s", awd) } @@ -61,7 +59,8 @@ func main() { data, err := config.NewConfigReader(flags).Read() if err != nil { - logger.Fatalf(logFatalSuffixFn("read config: "), err) + logger.Errorf(logFatalSuffixFn("read config: "), err) + return } rawConfig, templateData, err := config.NewRawPreprocessor( @@ -71,7 +70,8 @@ func main() { []string{flags.MissingKey.String()}, ).Process(data) if err != nil { - logger.Fatalf(logFatalSuffixFn("preprocess raw config: "), err) + logger.Errorf(logFatalSuffixFn("preprocess raw config: "), err) + return } if flags.PrintProcessedConfig { @@ -83,33 +83,78 @@ func main() { ) conf, err = config.NewYamlConfigUnmarshaler().Unmarshal(rawConfig) if err != nil { - logger.Fatalf(logFatalSuffixFn("unmarshal config: "), err) + logger.Errorf(logFatalSuffixFn("unmarshal config: "), err) + return } if err = conf.Validate(); err != nil { - logger.Fatalf(logFatalSuffixFn("validate config: "), err) + logger.Errorf(logFatalSuffixFn("validate config: "), err) + return } - procChain, err := factory.NewExecutorChain( - conf, - factory.NewActionFilter( + var ( + actionFilter = factory.NewActionFilter( flags.Skip, flags.Group, conf.Settings.Groups.GroupByAction(), conf.Settings.Groups.ManualActions(), logger, - ), - templateData, - []string{flags.MissingKey.String()}, + ) + templateOptions = []string{flags.MissingKey.String()} + preprocessors = &exec.Preprocessors{} + ) + + procChain, err := factory.NewExecutorChainFactory( logger, - flags.PreprocessFiles, - flags.DryRun) + flags.DryRun, + func(executors []entity.Executor) entity.Executor { + return exec.NewPreprocessingChain(preprocessors, executors) + }, + factory.NewExecutorBuilderFactory( + conf.DirActions(), + factory.NewMkdirExecutor, + actionFilter, + ), + factory.NewExecutorBuilderFactory( + conf.RmActions(), + factory.NewRmExecutor, + actionFilter, + ), + factory.NewExecutorBuilderFactory( + conf.CommandActions(), + factory.NewRunCommandExecutor, + actionFilter, + ), + factory.NewExecutorBuilderFactory( + conf.FilesActions(), + factory.NewPreprocessorsFileExecutorFactory( + templateData, + templateOptions, + flags.PreprocessFiles, + preprocessors, + func(logger entity.Logger) *resty.Client { + return factory.NewHTTPClient(conf.Settings.HTTP, logger) + }, + ).Create, + actionFilter, + ), + factory.NewExecutorBuilderFactory( + conf.FsActions(), + factory.NewFsModifyExecFactory( + templateData, + templateOptions, + ).Create, + actionFilter, + ), + ).Create() if err != nil { - logger.Fatalf(logFatalSuffixFn("create processors chain: "), err) + logger.Errorf(logFatalSuffixFn("create processors chain: "), err) + return } err = procChain.Exec() if err != nil { - logger.Fatalf(logFatalSuffixFn("execute chain: "), err) + logger.Errorf(logFatalSuffixFn("execute chain: "), err) + return } } diff --git a/pkg/core/engin.go b/pkg/core/engin.go new file mode 100644 index 0000000..f45deb6 --- /dev/null +++ b/pkg/core/engin.go @@ -0,0 +1,129 @@ +package core + +import ( + "flag" + "log" + "os" + "sync" + + "golang.org/x/xerrors" + + "github.com/kozmod/progen/internal/entity" + "github.com/kozmod/progen/internal/exec" + "github.com/kozmod/progen/internal/factory" + engineFlags "github.com/kozmod/progen/internal/flag" +) + +type Engin struct { + mx sync.RWMutex + files []entity.Action[[]entity.UndefinedFile] + cmd []entity.Action[[]entity.Command] + dirs []entity.Action[[]string] + fsModify []entity.Action[[]string] + fsSave []entity.Action[[]entity.TargetFs] + rm []entity.Action[[]string] + + logger entity.Logger + + flags engineFlags.DefaultFlags +} + +// AddActions adds action to the [Engin]. +func (e *Engin) AddActions(actions ...action) *Engin { + e.mx.Lock() + defer e.mx.Unlock() + for _, action := range actions { + action.add(e) + } + return e +} + +// Run all actions. +func (e *Engin) Run() error { + var ( + args = os.Args + flagSet = flag.NewFlagSet(args[0], flag.ExitOnError) + ) + err := e.flags.Parse(flagSet, args) + if err != nil { + log.Printf("parse flags failed: %v", err) + return err + } + + var ( + logFatalSuffixFn = entity.NewAppendVPlusOrV(e.flags.PrintErrorStackTrace) + actionFilter factory.DummyActionFilter + ) + + e.mx.RLock() + defer e.mx.RUnlock() + + var logger entity.Logger + if e.logger == nil { + logger, err = factory.NewLogger(e.flags.Verbose) + if err != nil { + return xerrors.Errorf("failed to initialize logger: %w", err) + } + } + + procChain, err := factory.NewExecutorChainFactory( + logger, + e.flags.DryRun, + func(executors []entity.Executor) entity.Executor { + return exec.NewChain(executors) + }, + factory.NewExecutorBuilderFactory( + e.dirs, + factory.NewMkdirExecutor, + actionFilter, + ), + factory.NewExecutorBuilderFactory( + e.fsModify, + factory.NewFsModifyExecFactory( + e.flags.TemplateVars.Vars, + []string{e.flags.MissingKey.String()}, + ).Create, + actionFilter, + ), + factory.NewExecutorBuilderFactory( + e.fsSave, + factory.NewFsSaveExecFactory( + e.flags.TemplateVars.Vars, + []string{e.flags.MissingKey.String()}, + ).Create, + actionFilter, + ), + factory.NewExecutorBuilderFactory( + e.rm, + factory.NewRmExecutor, + actionFilter, + ), + factory.NewExecutorBuilderFactory( + e.files, + factory.NewFileExecutorFactory( + e.flags.TemplateVars.Vars, + []string{e.flags.MissingKey.String()}, + ).Create, + actionFilter, + ), + factory.NewExecutorBuilderFactory( + e.cmd, + factory.NewRunCommandExecutor, + actionFilter, + ), + ).Create() + + if err != nil { + //goland:noinspection ALL + logger.Errorf(logFatalSuffixFn("create processors chain: "), err) + return err + } + + err = procChain.Exec() + if err != nil { + //goland:noinspection ALL + logger.Errorf(logFatalSuffixFn("execute chain: "), err) + return err + } + return nil +} diff --git a/pkg/core/entity.go b/pkg/core/entity.go new file mode 100644 index 0000000..25f75c5 --- /dev/null +++ b/pkg/core/entity.go @@ -0,0 +1,186 @@ +package core + +import ( + "github.com/kozmod/progen/internal/entity" +) + +type ( + // action is the common interface to adding actions to the engine. + action interface { + add(engin *Engin) + } + + File struct { + Path string + Data []byte + } + + Cmd entity.Command + TargetFs entity.TargetFs +) + +type Files entity.Action[[]File] + +func (c Files) add(e *Engin) { + if e != nil { + files := convert(c.Val, func(s File) entity.UndefinedFile { + return entity.UndefinedFile{ + Path: s.Path, + Data: &s.Data, + } + }) + e.files = append(e.files, entity.Action[[]entity.UndefinedFile]{ + Name: c.Name, + Val: files, + Priority: c.Priority, + }) + } +} + +func FilesAction(name string, files ...File) Files { + return Files{ + Name: name, + Val: files, + } +} + +type Command entity.Action[[]Cmd] + +func (c Command) add(e *Engin) { + if e != nil { + commands := convert(c.Val, func(s Cmd) entity.Command { + return entity.Command{ + Args: s.Args, + Dir: s.Dir, + Cmd: s.Cmd, + } + }) + e.cmd = append(e.cmd, entity.Action[[]entity.Command]{ + Name: c.Name, + Val: commands, + Priority: c.Priority, + }) + } +} + +func (c Command) WithPriority(priority int) Command { + c.Priority = priority + return c +} + +func CmdAction(name string, commands ...Cmd) Command { + return Command{ + Name: name, + Val: commands, + } +} + +type Dirs entity.Action[[]string] + +func (d Dirs) add(e *Engin) { + if e != nil { + e.dirs = append(e.dirs, entity.Action[[]string]{ + Name: d.Name, + Val: d.Val, + Priority: d.Priority, + }) + } +} + +func (d Dirs) WithPriority(priority int) Dirs { + d.Priority = priority + return d +} + +func DirsAction(name string, dirs ...string) Dirs { + return Dirs{ + Name: name, + Val: dirs, + } +} + +type Rm entity.Action[[]string] + +func (r Rm) add(e *Engin) { + if e != nil { + e.rm = append(e.rm, entity.Action[[]string]{ + Name: r.Name, + Val: r.Val, + Priority: r.Priority, + }) + } +} + +func (r Rm) WithPriority(priority int) Rm { + r.Priority = priority + return r +} + +func RmAction(name string, rm ...string) Rm { + return Rm{ + Name: name, + Val: rm, + } +} + +type FsModify entity.Action[[]string] + +func (f FsModify) add(e *Engin) { + if e != nil { + e.fsModify = append(e.fsModify, entity.Action[[]string]{ + Name: f.Name, + Val: f.Val, + Priority: f.Priority, + }) + } +} + +func (f FsModify) WithPriority(priority int) FsModify { + f.Priority = priority + return f +} + +func FsModifyAction(name string, fs ...string) FsModify { + return FsModify{ + Name: name, + Val: fs, + } +} + +type FsSave entity.Action[[]TargetFs] + +func (f FsSave) add(e *Engin) { + if e != nil { + fs := convert(f.Val, func(s TargetFs) entity.TargetFs { + return entity.TargetFs{ + TargetDir: s.TargetDir, + Fs: s.Fs, + } + }) + e.fsSave = append(e.fsSave, entity.Action[[]entity.TargetFs]{ + Name: f.Name, + Val: fs, + Priority: f.Priority, + }) + } +} + +func (f FsSave) WithPriority(priority int) FsSave { + f.Priority = priority + return f +} + +func FsSaveAction(name string, fs ...TargetFs) FsSave { + return FsSave{ + Name: name, + Val: fs, + } +} + +func convert[S any, T any](s []S, fn func(s S) T) []T { + res := make([]T, len(s)) + for i, val := range s { + res[i] = fn(val) + } + return res +}