diff --git a/.golangci.yml b/.golangci.yml index f714f75..8ed9b50 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -58,4 +58,8 @@ linters: - wsl run: + skip-dirs: + - /generators/gola/templates + skip-files: + - .*\.tmpl$ issues-exit-code: 1 diff --git a/.vscode/settings.json b/.vscode/settings.json index 8ca4d1a..9875334 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "beezledub", "bindnative", "bodyclose", "colors", @@ -11,6 +12,7 @@ "errcheck", "exportloopref", "extendio", + "faydeaudeau", "fieldalignment", "foobar", "goconst", @@ -32,6 +34,7 @@ "ipnet", "linters", "nakedret", + "nolint", "nolintlint", "paramset", "prealloc", @@ -45,11 +48,15 @@ "stylecheck", "templatise", "thelper", + "tmpl", "tparallel", "unconvert", "unparam", "vactive", "Validatable", "varcheck" - ] + ], + "files.associations": { + "**/*.tmpl": "go" + } } \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml index e030179..030fada 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -76,12 +76,17 @@ tasks: ov-gen-t: cmds: - mkdir -p {{.GEN_TEST_OUTPUT_DIR}} - - cobrass-gen -test -cwd ./ + - cobrass-gen -test -cwd ./ -templates generators/gola -write + - go fmt ./generators/gola/out/assistant/*.go ov-gen: cmds: - go generate ./... + clear-gen-t: + cmds: + - rm -f ./generators/gola/out/assistant/*auto*.go + # === build/deploy code generator =========================== b-gen-linux: @@ -97,7 +102,7 @@ tasks: - GOOS={{.TARGET_OS}} GOARCH={{.TARGET_ARCH}} go build -o {{.DIST_DIR}}/{{.TARGET_OS}}/{{.GEN_BINARY_NAME}} -v {{.APPLICATION_ENTRY}} sources: - - ./generators/gola/*.go + - ./generators/gola/**/*.go generates: - "{{.DIST_DIR}}/{{.TARGET_OS}}/{{.GEN_BINARY_NAME}}" diff --git a/generate.go b/generate.go index 605a14f..64d0a50 100644 --- a/generate.go +++ b/generate.go @@ -1,3 +1,3 @@ package cobrass -//go:generate cobrass-gen -cwd ./ +//go:generate cobrass-gen -cwd ./ -templates gola/templates/ diff --git a/generators/gola/gen/main.go b/generators/gola/gen/main.go index fb9a8f4..6dfb00e 100644 --- a/generators/gola/gen/main.go +++ b/generators/gola/gen/main.go @@ -17,11 +17,12 @@ const ( ) var ( - testFlag = flag.Bool("test", false, "generate code in test location?") - cwdFlag = flag.String("cwd", "", "current working directory") - testPath = filepath.Join("generators", "gola", "out", "assistant") - sourcePath = filepath.Join("src", "assistant") - outputPathNotFound = "Output path '%v', not found" + testFlag = flag.Bool("test", false, "generate code in test location?") + cwdFlag = flag.String("cwd", "", "current working directory") + templatesSubPath = flag.String("templates", "", "templates sub path") + write = flag.Bool("write", false, "write generated code?") + testPath = filepath.Join("generators", "gola", "out", "assistant") + sourcePath = filepath.Join("src", "assistant") ) func Usage() { @@ -42,9 +43,6 @@ func fail(reason string, callback ...func()) { os.Exit(outputPathNotFoundExitCode) } -// ??? -// https://askgolang.com/how-to-get-current-directory-in-golang/ - func main() { flag.Usage = Usage flag.Parse() @@ -58,18 +56,18 @@ func main() { absolutePath, _ := filepath.Abs(*cwdFlag) absolutePath = filepath.Join(absolutePath, outputPath) - if !utils.FileExists(absolutePath) { + if !utils.FolderExists(absolutePath) { callback := func() { fmt.Printf("๐Ÿ’ฅ ---> CWD: '%v' \n", *cwdFlag) fmt.Printf("๐Ÿ’ฅ ---> OUTPUT: '%v' \n", outputPath) fmt.Printf("๐Ÿ’ฅ ---> RESOLVED: '%v' \n", absolutePath) } - fail(fmt.Sprintf(outputPathNotFound, absolutePath), callback) + fail(fmt.Sprintf("Output path '%v', not found", absolutePath), callback) return } - sourceCode := gola.NewSourceCodeContainer() + sourceCode := gola.NewSourceCodeContainer(absolutePath, *templatesSubPath) mode := lo.Ternary(*testFlag, "๐Ÿงช Test", "๐ŸŽ Source") fmt.Printf("โ˜‘๏ธ ---> CWD: '%v' \n", *cwdFlag) @@ -78,12 +76,12 @@ func main() { fmt.Printf("---> ๐Ÿฒ cobrass generator (%v, to: %v)\n", mode, absolutePath) if !*testFlag { - if sourceCode.AnyMissing(absolutePath) { - sourceCode.Verify(absolutePath, func(entry *gola.SourceCodeData) { - exists := entry.Exists(absolutePath) + if sourceCode.AnyMissing() { + sourceCode.Verify(func(entry *gola.SourceCodeData) { + exists := entry.Exists() indicator := lo.Ternary(exists, "โœ”๏ธ", "โŒ") status := lo.Ternary(exists, "exists", "missing") - path := entry.FullPath(absolutePath) + path := entry.FullPath() message := fmt.Sprintf("%v source file: '%v' %v", indicator, path, status) fmt.Printf("%v\n", message) @@ -93,17 +91,7 @@ func main() { } } - sourceCode.Generator().Run() - - logicalEnum := gola.LogicalType{ - TypeName: "Enum", - GoType: "string", - DisplayType: "enum", - UnderlyingTypeName: "String", - FlagName: "Format", - Short: "f", - Def: "xml", + if err := sourceCode.Generator(*write).Run(); err != nil { + fail(err.Error()) } - - fmt.Printf("---> ๐Ÿฒ cobrass generator (enum: %+v)\n", logicalEnum) } diff --git a/generators/gola/gola-defs.go b/generators/gola/gola-defs.go new file mode 100644 index 0000000..d7cf85c --- /dev/null +++ b/generators/gola/gola-defs.go @@ -0,0 +1,6 @@ +package gola + +type executionInfo struct { + Spec *TypeSpec + Operators Operators +} diff --git a/generators/gola/gomega-matchers_test.go b/generators/gola/gomega-matchers_test.go index 389216d..6d92fad 100644 --- a/generators/gola/gomega-matchers_test.go +++ b/generators/gola/gomega-matchers_test.go @@ -28,7 +28,7 @@ func (m *AreAllSourceCodeFilesPresentMatcher) Match(actual interface{}) (bool, e return false, fmt.Errorf("matcher expected a SourceCodeContainer value (actual: '%v')", actual) } - return !sourceCode.AnyMissing(m.directory), nil + return !sourceCode.AnyMissing(), nil } func (m *AreAllSourceCodeFilesPresentMatcher) report( @@ -41,11 +41,11 @@ func (m *AreAllSourceCodeFilesPresentMatcher) report( fmt.Sprintf("๐Ÿ”ฅ Expected all source code files %vto be present\n", not), ) - sourceCode.ReportAll(func(entry *gola.SourceCodeData) { - exists := entry.Exists(m.directory) + sourceCode.ForEach(func(entry *gola.SourceCodeData) { + exists := entry.Exists() indicator := lo.Ternary(exists, "โœ”๏ธ", "โŒ") status := lo.Ternary(exists, "exists", "missing") - path := entry.FullPath(m.directory) + path := entry.FullPath() message := fmt.Sprintf("%v source file: '%v' %v\n", indicator, path, status) builder.WriteString(message) }) diff --git a/generators/gola/hyper-gen_test.go b/generators/gola/hyper-gen_test.go new file mode 100644 index 0000000..0346f76 --- /dev/null +++ b/generators/gola/hyper-gen_test.go @@ -0,0 +1,272 @@ +package gola_test + +import ( + "fmt" + "path/filepath" + "strings" + + . "github.com/onsi/ginkgo/v2" + // . "github.com/onsi/gomega" +) + +type typeSpec struct { + SubTypes []string + // + TypeName string + GoType string + DisplayType string + UnderlyingTypeName string + FlagName string + Short string + Def string + // Assign string + // Setup string + // BindTo string + // Assert string + // QuoteExpect string // bool + Equate string + // Validatable string // bool + // ForeignValidatorFn string // bool + // GenerateSlice string // bool + SliceFlagName string + SliceShort string + DefSliceVal string + ExpectSlice string + SliceValue string + OptionValue string + TcEntry string // object +} + +func generate(spec *typeSpec, subType string) { + builder := strings.Builder{} + builder.WriteString(fmt.Sprintf(` "%v%v": &TypeSpec{`+"\n", spec.TypeName, subType)) + builder.WriteString(fmt.Sprintf(` TypeName: "%v%v",`+"\n", spec.TypeName, subType)) + builder.WriteString(fmt.Sprintf(` GoType: "%v%v",`+"\n", spec.GoType, subType)) + + if spec.DisplayType != "" { + builder.WriteString(fmt.Sprintf(` DisplayType: "%v",`+"\n", spec.DisplayType)) + } + + builder.WriteString(fmt.Sprintf(` UnderlyingTypeName: "%v",`+"\n", spec.UnderlyingTypeName)) + builder.WriteString(fmt.Sprintf(` FlagName: "%v%v",`+"\n", spec.FlagName, subType)) + builder.WriteString(fmt.Sprintf(` Short: "%v",`+"\n", spec.Short)) + + if subType == "" { + builder.WriteString(fmt.Sprintf(` Def: "%v",`+"\n", spec.Def)) + } else { + builder.WriteString(fmt.Sprintf(` Def: "%v%v(-1)",`+"\n", spec.GoType, subType)) + } + + builder.WriteString(` Assign: "...",` + "\n") + builder.WriteString(` Setup: "...",` + "\n") + builder.WriteString(` BindTo: "...",` + "\n") + builder.WriteString(` Assert: "...",` + "\n") + builder.WriteString(` QuoteExpect: true,` + "\n") + builder.WriteString(` Equate: "Equal",` + "\n") + builder.WriteString(` Validatable: true,` + "\n") + builder.WriteString(` ForeignValidatorFn: true,` + "\n") + builder.WriteString(` GenerateSlice: false,` + "\n") + //---- + + builder.WriteString(fmt.Sprintf(` SliceFlagName: "%v",`+"\n", spec.SliceFlagName)) + builder.WriteString(fmt.Sprintf(` SliceShort: "%v",`+"\n", spec.SliceShort)) + builder.WriteString(fmt.Sprintf(` DefSliceVal: "%v",`+"\n", spec.DefSliceVal)) + builder.WriteString(fmt.Sprintf(` ExpectSlice: "%v",`+"\n", spec.ExpectSlice)) + builder.WriteString(` SliceValue: "...",` + "\n") + builder.WriteString(` OptionValue: "...",`) + builder.WriteString(` + TcEntry: &PsCaseEntry{ + AssertFn: "func() { Expect(outputFormatEnum.Source).To(Equal(\"json\")) }", + },`) + builder.WriteString("\n Containable: true," + "\n") + builder.WriteString(" }," + "\n") + + GinkgoWriter.Println(builder.String()) +} + +var _ = Describe("HyperGen", Ordered, func() { + var ( + repo, testPath, sourcePath string + orderedTypes []string + types map[string]*typeSpec + ) + + BeforeAll(func() { + repo = Repo("../..") + testPath = filepath.Join("generators", "gola", "out", "assistant") + sourcePath = filepath.Join("src", "assistant") + _ = testPath + _ = sourcePath + _ = repo + + orderedTypes = []string{ + "Enum", + "String", + "Int", + // "Int8", + // "Int16", + // "Int32", + // "Int64", + "UInt", + // "UInt8", + // "UInt16", + // "UInt32", + // "UInt64", + "Float32", + "Float64", + "Bool", + "Duration", + "IPNet", + "IPMask", + } + + types = map[string]*typeSpec{ + "Enum": { + TypeName: "Enum", + GoType: "string", + DisplayType: "enum", + UnderlyingTypeName: "String", + FlagName: "Format", + Short: "f", + Def: "xml", + Equate: "Equal", + SliceFlagName: "Formats", + SliceShort: "F", + DefSliceVal: "[]string{}", + ExpectSlice: `[]string{\"xml\", \"json\", \"text\"}`, + }, + + "String": { + TypeName: "String", + GoType: "string", + UnderlyingTypeName: "String", + FlagName: "Format", + Short: "f", + Def: "xml", + Equate: "Equal", + SliceFlagName: "Formats", + SliceShort: "F", + DefSliceVal: "[]string{}", + ExpectSlice: `[]string{\"xml\", \"json\", \"text\"}`, + }, + + "Int": { + SubTypes: []string{"8", "16", "32", "64"}, + TypeName: "Int", + GoType: "int", + FlagName: "Format", + Short: "f", + Def: "xml", + Equate: "Equal", + SliceFlagName: "Formats", + SliceShort: "F", + DefSliceVal: "[]string{}", + ExpectSlice: `[]string{\"xml\", \"json\", \"text\"}`, + }, + + "Uint": { + SubTypes: []string{"8", "16", "32", "64"}, + TypeName: "Uint", + GoType: "uint", + FlagName: "Count", + Short: "f", + Def: "xml", + Equate: "Equal", + SliceFlagName: "Formats", + SliceShort: "F", + DefSliceVal: "[]string{}", + ExpectSlice: `[]string{\"xml\", \"json\", \"text\"}`, + }, + + "Float32": { + TypeName: "Float32", + GoType: "float32", + FlagName: "Gradientf32", + Short: "t", + Def: "float32(0)", + Equate: "Equal", + SliceFlagName: "Gradientsf32", + SliceShort: "G", + DefSliceVal: "[]float32{}", + ExpectSlice: "[]float32{3.0, 5.0, 7.0, 9.0}", + SliceValue: "3.0, 5.0, 7.0, 9.0", + }, + + "Float64": { + TypeName: "Float64", + GoType: "float64", + FlagName: "Gradientf64", + Short: "t", + Def: "float64(0)", + Equate: "Equal", + SliceFlagName: "Gradientsf64", + SliceShort: "G", + DefSliceVal: "[]float64{}", + ExpectSlice: "[]float64{4.0, 6.0, 8.0, 10.0}", + SliceValue: "4.0, 6.0, 8.0, 10.0", + }, + + "Bool": { + TypeName: "Bool", + GoType: "bool", + FlagName: "Concise", + Short: "c", + Def: "false", + Equate: "Equal", + SliceFlagName: "Switches", + SliceShort: "S", + DefSliceVal: "[]bool{}", + ExpectSlice: "[]bool{true, false, true, false}", + SliceValue: "true, false, true, false", + }, + + "Duration": { + TypeName: "Duration", + GoType: "time.Duration", + FlagName: "Latency", + Short: "l", + Def: `duration(\"0ms\")`, + Equate: "BeEquivalentTo", + SliceFlagName: "Latencies", + SliceShort: "L", + DefSliceVal: "[]time.Duration{}", + ExpectSlice: `[]time.Duration{duration(\"1s\"), duration(\"2s\"), duration(\"3s\")}`, + SliceValue: "1s, 2s, 3s", + }, + + "IPNet": { + TypeName: "IPNet", + GoType: "net.IPNet", + FlagName: "IPAddress", + Short: "i", + Def: `ipnet(\"default\")`, + Equate: "BeEquivalentTo", + }, + + "IPMask": { + TypeName: "IPMask", + GoType: "net.IPMask", + FlagName: "IPMask", + Short: "m", + Def: `ipmask(\"default\")`, + Equate: "BeEquivalentTo", + }, + } + }) + + Context("generate", func() { + XIt("types ...", func() { + for _, ot := range orderedTypes { + if spec, ok := types[ot]; ok { + generate(spec, "") + + if len(spec.SubTypes) > 0 { + for _, st := range spec.SubTypes { + generate(spec, st) + } + } + } + } + }) + }) +}) diff --git a/generators/gola/internal/utils/fs.go b/generators/gola/internal/utils/fs.go index 28c91bd..abb8858 100644 --- a/generators/gola/internal/utils/fs.go +++ b/generators/gola/internal/utils/fs.go @@ -12,3 +12,14 @@ func FileExists(path string) bool { return result } + +// FileExists provides a simple way to determine whether the item identified by a +// path actually exists as a folder +func FolderExists(path string) bool { + result := false + if info, err := os.Lstat(path); err == nil { + result = info.IsDir() + } + + return result +} diff --git a/generators/gola/logical-type.go b/generators/gola/logical-type.go deleted file mode 100644 index e9ce928..0000000 --- a/generators/gola/logical-type.go +++ /dev/null @@ -1,43 +0,0 @@ -package gola - -type PsCaseEntry struct { // this needs a better name, not Ps - AssertFn string -} - -type TestCaseEntry struct { -} - -type BhTest struct { // binder helper -} - -type BhTestCollection map[string]*BhTest - -type LogicalType struct { - TypeName string - GoType string - DisplayType string - UnderlyingTypeName string - FlagName string - Short string - Def any - Assign string - Setup string - BindTo string - Assert string - QuoteExpect string - Equate string - Validatable bool - ForeignValidatorFn bool - GenerateSlice bool - SliceFlagName string - SliceShort string - DefSliceVal string - ExpectSlice string - SliceValue string - OptionValue string - TcEntry PsCaseEntry - BindDoc string - BindValidatedDoc string - Containable bool - BhTests BhTestCollection -} diff --git a/generators/gola/operator.go b/generators/gola/operator.go new file mode 100644 index 0000000..e1de7f7 --- /dev/null +++ b/generators/gola/operator.go @@ -0,0 +1,9 @@ +package gola + +type OperatorName string + +type Operator struct { + Name OperatorName + Documentation string +} +type Operators = []*Operator diff --git a/generators/gola/source-code-container.go b/generators/gola/source-code-container.go new file mode 100644 index 0000000..3d3e1c0 --- /dev/null +++ b/generators/gola/source-code-container.go @@ -0,0 +1,175 @@ +package gola + +import ( + "fmt" + "path/filepath" + "sort" + "text/template" + + "github.com/samber/lo" +) + +type sourceCodeDataCollection map[CodeFileName]*SourceCodeData + +type SourceCodeContainer struct { + absolutePath string + templatesSubPath string + collection sourceCodeDataCollection +} + +func (d *SourceCodeContainer) init() { + d.collection = sourceCodeDataCollection{ + "option-validator-auto": &SourceCodeData{ + name: "option-validator-auto", + active: true, + directory: d.absolutePath, + // We only define simple functions as template functions + // Anything more should be defined on the typeSpec + // + funcs: map[string]any{ + "getValidatorFn": func(typeName string) string { + return typeName + "ValidatorFn" + }, + "getValidatorStruct": func(typeName string) string { + return typeName + "OptionValidator" + }, + "getDisplayType": func(spec TypeSpec) string { + return lo.Ternary(spec.DisplayType != "", + spec.DisplayType, + spec.GoType, + ) + }, + "getSliceTypeName": func(spec TypeSpec) string { + return spec.TypeName + "Slice" + }, + "getSliceType": func(spec TypeSpec) string { + return "[]" + spec.GoType + }, + "getSliceValidationFn": func(spec TypeSpec) string { + return spec.TypeName + "SliceValidatorFn" + }, + }, + }, + "option-validator-auto_test": &SourceCodeData{ + name: "option-validator-auto_test", + directory: d.absolutePath, + funcs: map[string]any{}, + }, + "param-set-auto": &SourceCodeData{ + name: "param-set-auto", + directory: d.absolutePath, + funcs: map[string]any{}, + }, + "param-set-auto_test": &SourceCodeData{ + name: "param-set-auto_test", + directory: d.absolutePath, + funcs: map[string]any{}, + }, + "param-set-binder-helpers-auto": &SourceCodeData{ + name: "param-set-binder-helpers-auto", + directory: d.absolutePath, + funcs: map[string]any{}, + }, + "param-set-binder-helpers-auto_test": &SourceCodeData{ + name: "param-set-binder-helpers-auto_test", + directory: d.absolutePath, + funcs: map[string]any{}, + }, + } + + for _, page := range d.collection { + path := page.templates() + + if d.templatesSubPath != "" { + path = filepath.Join(d.templatesSubPath, path) + } + + if templ, err := template.New(string(page.name)).Funcs(page.funcs).ParseGlob(path); err == nil { + page.templ = templ + } else { + panic( + fmt.Errorf("๐Ÿ’ฅ error creating templates for '%v' (%v)", + page.name, err, + ), + ) + } + } +} + +func (d *SourceCodeContainer) sourceNames() []string { + keys := lo.Keys(d.collection) + sorted := lo.Map(keys, func(item CodeFileName, index int) string { + return string(item) + }) + sort.Strings(sorted) + + return sorted +} + +func (d *SourceCodeContainer) AnyMissing() bool { + // TODO: this needs to verified after all has been built + // + return d.ForEachUntil(func(data *SourceCodeData) bool { + return !data.Exists() + }) +} + +func (d *SourceCodeContainer) ForEach(fn func(entry *SourceCodeData)) { + names := d.sourceNames() + + for _, name := range names { + sourceCodeName := CodeFileName(name) + data := (d.collection)[sourceCodeName] + + fn(data) + } +} + +// ForEachUntil returns true if exit's early, false otherwise +func (d *SourceCodeContainer) ForEachUntil(fn func(entry *SourceCodeData) bool) bool { + names := d.sourceNames() + + for _, name := range names { + sourceCodeName := CodeFileName(name) + data := (d.collection)[sourceCodeName] + + if fn(data) { + return true + } + } + + return false +} + +func (d *SourceCodeContainer) Verify(fn func(entry *SourceCodeData)) { + if !d.AnyMissing() { + return + } + + d.ForEach(fn) +} + +func (d *SourceCodeContainer) Generator( + doWrite bool, +) *SourceCodeGenerator { + generator := &SourceCodeGenerator{ + doWrite: doWrite, + sourceCodeCollection: &d.collection, + } + generator.init() + + return generator +} + +func NewSourceCodeContainer( + absolutePath string, + templatesSubPath string, +) *SourceCodeContainer { + container := &SourceCodeContainer{ + absolutePath: absolutePath, + templatesSubPath: templatesSubPath, + } + container.init() + + return container +} diff --git a/generators/gola/source-code-data.go b/generators/gola/source-code-data.go index 0171591..d0ea42d 100644 --- a/generators/gola/source-code-data.go +++ b/generators/gola/source-code-data.go @@ -1,13 +1,11 @@ package gola import ( - _ "embed" + "fmt" "path/filepath" - "sort" "strings" "text/template" - "github.com/samber/lo" "github.com/snivilised/cobrass/generators/gola/internal/utils" ) @@ -16,33 +14,16 @@ import ( type CodeFileName string -var ( - //go:embed templates/top/option-validator-auto.templ.top.txt - optionValidatorAutoTop string - - //go:embed templates/top/option-validator-auto_test.templ.top.txt - optionValidatorAutoTestTop string - - //go:embed templates/top/param-set-auto.templ.top.txt - paramSetAutoTop string - - //go:embed templates/top/param-set-auto_test.templ.top.txt - paramSetAutoTestTop string - - //go:embed templates/top/param-set-binder-helpers-auto.templ.top.txt - paramSetBinderHelpersAutoTop string - - //go:embed templates/top/param-set-binder-helpers-auto_test.templ.top.txt - paramSetBinderHelpersAutoTestTop string -) - type SourceCodeData struct { - name CodeFileName - top string - templ *template.Template + name CodeFileName + active bool // THIS IS JUST TEMPORARY + directory string + rootContent string + templ *template.Template + funcs map[string]any } -func (d *SourceCodeData) FileName() string { +func (d *SourceCodeData) GeneratedFileName() string { return string(d.name) + ".go" } @@ -50,123 +31,22 @@ func (d *SourceCodeData) IsTest() bool { return strings.HasSuffix(string(d.name), "_test") } -func (d *SourceCodeData) Exists(absolutePath string) bool { - return utils.FileExists(d.FullPath(absolutePath)) -} - -func (d *SourceCodeData) FullPath(absolutePath string) string { - filename := d.FileName() - return filepath.Join(absolutePath, filename) +func (d *SourceCodeData) Exists() bool { + return utils.FileExists(d.FullPath()) } -type sourceCodeDataCollection map[CodeFileName]*SourceCodeData - -type SourceCodeContainer struct { - collection sourceCodeDataCollection +func (d *SourceCodeData) FullPath() string { + return filepath.Join(d.directory, d.GeneratedFileName()) } -func (d *SourceCodeContainer) init() { - d.collection = sourceCodeDataCollection{ - "option-validator-auto": &SourceCodeData{ - name: "option-validator-auto", - top: optionValidatorAutoTop, - }, - "option-validator-auto_test": &SourceCodeData{ - name: "option-validator-auto_test", - top: optionValidatorAutoTestTop, - }, - "param-set-auto": &SourceCodeData{ - name: "param-set-auto", - top: paramSetAutoTop, - }, - "param-set-auto_test": &SourceCodeData{ - name: "param-set-auto_test", - top: paramSetAutoTestTop, - }, - "param-set-binder-helpers-auto": &SourceCodeData{ - name: "param-set-binder-helpers-auto", - top: paramSetBinderHelpersAutoTop, - }, - "param-set-binder-helpers-auto_test": &SourceCodeData{ - name: "param-set-binder-helpers-auto_test", - top: paramSetBinderHelpersAutoTestTop, - }, - } - - for _, data := range d.collection { - if templ, err := template.New(string(data.name)).Parse(data.top); err == nil { - data.templ = templ - } - } +func (d *SourceCodeData) templates() string { + return fmt.Sprintf("templates/%v/*.go.tmpl", d.name) } -func (d *SourceCodeContainer) sourceNames() []string { - keys := lo.Keys(d.collection) - sorted := lo.Map(keys, func(item CodeFileName, index int) string { - return string(item) - }) - sort.Strings(sorted) - - return sorted +func (d *SourceCodeData) child(_ string) string { + panic("child template name not yet defined (-XXX.go.tmpl?)") } -func (d *SourceCodeContainer) AnyMissing(absolutePath string) bool { - names := d.sourceNames() - - for _, name := range names { - sourceCodeName := CodeFileName(name) - data := (d.collection)[sourceCodeName] - exists := data.Exists(absolutePath) - - if !exists { - return true - } - } - - return false -} - -func (d *SourceCodeContainer) ReportAll(fn ...func(entry *SourceCodeData)) { - names := d.sourceNames() - - for _, name := range names { - sourceCodeName := CodeFileName(name) - data := (d.collection)[sourceCodeName] - - if len(fn) > 0 { - fn[0](data) - } - } -} - -func (d *SourceCodeContainer) Verify(absolutePath string, fn ...func(entry *SourceCodeData)) bool { - result := d.AnyMissing(absolutePath) - - if !result { - return result - } - - names := d.sourceNames() - - for _, name := range names { - sourceCodeName := CodeFileName(name) - data := (d.collection)[sourceCodeName] - - if len(fn) > 0 { - fn[0](data) - } - } - - return result -} - -func (d *SourceCodeContainer) Generator() *SourceCodeGenerator { - return &SourceCodeGenerator{} -} - -func NewSourceCodeContainer() *SourceCodeContainer { - data := &SourceCodeContainer{} - data.init() - - return data +func (d *SourceCodeData) section(s string) string { + return fmt.Sprintf("%v-%v.go.tmpl", d.name, s) } diff --git a/generators/gola/source-code-data_test.go b/generators/gola/source-code-data_test.go index a09a942..f170071 100644 --- a/generators/gola/source-code-data_test.go +++ b/generators/gola/source-code-data_test.go @@ -11,8 +11,6 @@ import ( "github.com/snivilised/cobrass/generators/gola" ) -// . "github.com/onsi/gomega/types" - func Path(parent, relative string) string { segments := strings.Split(relative, "/") return filepath.Join(append([]string{parent}, segments...)...) @@ -24,7 +22,6 @@ func Repo(relative string) string { } var _ = Describe("SourceCodeData", Ordered, func() { - var ( repo, testPath, sourcePath string ) @@ -39,8 +36,9 @@ var _ = Describe("SourceCodeData", Ordered, func() { Context("AnyMissing", func() { When("source mode", func() { It("should: find all source code files are present", func() { - codeData := gola.NewSourceCodeContainer() outputPath := filepath.Join(repo, sourcePath) + templatesSubPath := "" + codeData := gola.NewSourceCodeContainer(outputPath, templatesSubPath) Expect(codeData).To(ContainAllSourceCodeFilesAt(outputPath)) }) diff --git a/generators/gola/source-code-generator.go b/generators/gola/source-code-generator.go index 70a31cd..e6b7fd5 100644 --- a/generators/gola/source-code-generator.go +++ b/generators/gola/source-code-generator.go @@ -1,11 +1,179 @@ package gola -import "bytes" +import ( + "bytes" + "fmt" + "os" + "path" + "sort" + + "github.com/samber/lo" +) + +var ( + noData = struct{}{} +) + +type typeCollection map[TypeNameID]*TypeSpec +type operatorCollection []*Operator type SourceCodeGenerator struct { - buffer bytes.Buffer + sourceCodeCollection *sourceCodeDataCollection + types *typeCollection + operators operatorCollection + doWrite bool +} + +func (g *SourceCodeGenerator) init() { + g.buildOperators() + g.types = buildTypes() } -func (g *SourceCodeGenerator) Run() { +func (g *SourceCodeGenerator) sortedTypeKeys() []TypeNameID { + keys := lo.Keys(*g.types) + sort.Strings(keys) + + return keys +} + +func (g *SourceCodeGenerator) preDefinedOrderedTypeKeys() []TypeNameID { + // TODO: We might want to re-define this order, for now, leave it + // to match the same order as the PowerShell generator, at least + // until we have verified the signature hashing matches (a difference + // in the order will probably impact the has value) + // + return []TypeNameID{ + "Duration", "Enum", + "Float32", "Float64", + "Int", "Int16", "Int32", "Int64", "Int8", + "IPMask", "IPNet", + "String", + "Uint16", "Uint32", "Uint64", "Uint8", "Uint", + } +} + +func (g *SourceCodeGenerator) Run() error { + for _, page := range *g.sourceCodeCollection { + var buffer bytes.Buffer + + overwrite := lo.Ternary(page.Exists(), "โ™ป๏ธ overwrite", "โœจ new") + if page.active { + fmt.Printf("===> ๐Ÿš€ (%v) generating code to '%v' (%v)\n", + page.name, + page.GeneratedFileName(), + overwrite, + ) + + if err := g.renderStatic("header", page, &buffer); err != nil { + return err + } + + for _, specTypeKey := range g.preDefinedOrderedTypeKeys() { + fmt.Printf("---> ๐Ÿฌ๐Ÿฌ๐Ÿฌ generating code for type '%v'\n", specTypeKey) + // TODO: the executionInfo needs to match the structure of the dot + // and child templates. executionInfo is the dot object and everything else + // needs to be relative to this dot. + // + dot := executionInfo{ + Spec: (*g.types)[specTypeKey], + Operators: g.operators, + } + + if err := g.renderSection("body", page, &dot, &buffer); err != nil { + return err + } + } + + if err := g.renderStatic("footer", page, &buffer); err != nil { + return err + } + + if g.doWrite { + if err := g.flush(page.FullPath(), &buffer); err != nil { + fmt.Printf("---> ๐Ÿ”ฅ Write Error occurred for: '%v' (%v), aborting\n", + page.name, err, + ) + + return err + } + } else { + fmt.Printf("===> ๐ŸงŠ๐ŸงŠ๐ŸงŠ Generated %v content: ...\n", + page.name, + ) + fmt.Printf("%v\n", buffer.String()) + } + } else { + fmt.Printf("===> ๐Ÿ“› (%v) SKIPPING generation of code to '%v' (%v)\n", + page.name, + page.GeneratedFileName(), + overwrite, + ) + } + } + + return nil +} + +func (g *SourceCodeGenerator) renderStatic( + section string, page *SourceCodeData, buffer *bytes.Buffer, +) error { + static := page.section(section) + if err := page.templ.ExecuteTemplate( + buffer, + static, + noData, + ); err != nil { + fmt.Printf("---> ๐Ÿ”ฅ Error executing static section template for: '%v' (%v), aborting\n", + page.name, err, + ) + + return err + } + + return nil +} + +func (g *SourceCodeGenerator) renderSection( + section string, page *SourceCodeData, dot *executionInfo, buffer *bytes.Buffer, +) error { + body := page.section(section) + if err := page.templ.ExecuteTemplate( + buffer, + body, + dot, + ); err != nil { + fmt.Printf("---> ๐Ÿ”ฅ Error executing section template for: '%v' (%v), aborting\n", + page.name, err, + ) + + return err + } + + return nil +} + +func (g *SourceCodeGenerator) buildOperators() { + g.operators = operatorCollection{ + &Operator{ + Name: "Within", + Documentation: "fails validation if the option value does not lie within 'low' and 'high' (inclusive)", + }, + } +} + +func (g *SourceCodeGenerator) flush(filepath string, content *bytes.Buffer) error { + faydeaudeau := 0o777 + directory := path.Dir(filepath) + + if err := os.MkdirAll(directory, os.FileMode(faydeaudeau)); err != nil { + return fmt.Errorf("failed to ensure parent directory '%v' exists (%v)", directory, err) + } + + beezledub := 0o666 + + if err := os.WriteFile(filepath, content.Bytes(), os.FileMode(beezledub)); err != nil { + return fmt.Errorf("failed to write generated code to '%v' (%v)", filepath, err) + } + return nil } diff --git a/generators/gola/source-code-generator_test.go b/generators/gola/source-code-generator_test.go new file mode 100644 index 0000000..11d1363 --- /dev/null +++ b/generators/gola/source-code-generator_test.go @@ -0,0 +1,39 @@ +package gola_test + +import ( + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/snivilised/cobrass/generators/gola" +) + +var _ = Describe("SourceCodeGenerator", Ordered, func() { + var ( + repo, testPath, sourcePath string + ) + + BeforeAll(func() { + repo = Repo("../..") + testPath = filepath.Join("generators", "gola", "out", "assistant") + sourcePath = filepath.Join("src", "assistant") + _ = testPath + _ = sourcePath + _ = repo + }) + + Context("AnyMissing", func() { + When("test mode", func() { + It("should: find all source code files are present", func() { + outputPath := filepath.Join(repo, testPath) + templatesSubPath := "" + sourceCode := gola.NewSourceCodeContainer(outputPath, templatesSubPath) + + omitWrite := false + err := sourceCode.Generator(omitWrite).Run() + Expect(err).Error().To(BeNil()) + }) + }) + }) +}) diff --git a/generators/gola/templates/option-validator-auto/generate-slice.go.tmpl b/generators/gola/templates/option-validator-auto/generate-slice.go.tmpl new file mode 100644 index 0000000..e218d55 --- /dev/null +++ b/generators/gola/templates/option-validator-auto/generate-slice.go.tmpl @@ -0,0 +1,24 @@ +{{- define "generate-slice" -}} +{{- /* parameter: executionInfo */ -}} +{{- $sliceTypeName := getSliceTypeName .Spec -}} +{{- $typeName := getValidatorStruct $sliceTypeName -}} +{{- $sliceType := getSliceType .Spec -}} +{{- $sliceValidationStruct := getValidatorStruct $sliceTypeName -}} +{{- $sliceValidatorFn := getSliceValidationFn .Spec -}} + +// {{ $typeName }} defines the validator function for {{ $sliceTypeName }} type. +type {{ $sliceValidatorFn }} func({{ $sliceType }}, *pflag.Flag) error + +// {{ $sliceValidationStruct }} wraps the client defined validator function for type {{ $sliceType }}. +type {{ $sliceValidationStruct }} GenericOptionValidatorWrapper[{{ $sliceType }}] + +// Validate invokes the client defined validator function for {{ $sliceType }} type. +func (validator {{ $sliceValidationStruct }}) Validate() error { + return validator.Fn(*validator.Value, validator.Flag) +} + +// GetFlag returns the flag for {{ $sliceType }} type. +func (validator {{ $sliceValidationStruct }}) GetFlag() *pflag.Flag { + return validator.Flag +} +{{ end -}} \ No newline at end of file diff --git a/generators/gola/templates/option-validator-auto/option-validator-auto-body.go.tmpl b/generators/gola/templates/option-validator-auto/option-validator-auto-body.go.tmpl new file mode 100644 index 0000000..1fa1624 --- /dev/null +++ b/generators/gola/templates/option-validator-auto/option-validator-auto-body.go.tmpl @@ -0,0 +1,26 @@ +{{- $validatorType := .Spec.TypeName -}} +{{- $validatorFn := getValidatorFn .Spec.TypeName -}} +{{- $validatorStruct := getValidatorStruct .Spec.TypeName -}} + +// {{ $validatorFn }} defines the validator function for {{ getDisplayType .Spec }} type. +type {{ $validatorFn }} func({{ .Spec.GoType }}, *pflag.Flag) error + +// {{ $validatorStruct }} defines the struct that wraps the client defined validator function +// {{ $validatorFn }} for {{ getDisplayType .Spec }} type. This is the instance that is returned by +// validated binder function BindValidated{{ .Spec.TypeName }}. +type {{ $validatorStruct }} GenericOptionValidatorWrapper[{{ .Spec.GoType }}] + +{{ if not .Spec.ForeignValidatorFn -}} +// Validate invokes the client defined validator function for {{ getDisplayType .Spec }} type. +func (validator {{ $validatorStruct }}) Validate() error { + return validator.Fn(*validator.Value, validator.Flag) +} + +// GetFlag returns the flag for {{ getDisplayType .Spec }} type. +func (validator {{ $validatorStruct }}) GetFlag() *pflag.Flag { + return validator.Flag +} +{{ end -}} +{{- if .Spec.GenerateSlice }} +{{ template "generate-slice" . }} +{{ end -}} \ No newline at end of file diff --git a/generators/gola/templates/option-validator-auto/option-validator-auto-footer.go.tmpl b/generators/gola/templates/option-validator-auto/option-validator-auto-footer.go.tmpl new file mode 100644 index 0000000..545b7bb --- /dev/null +++ b/generators/gola/templates/option-validator-auto/option-validator-auto-footer.go.tmpl @@ -0,0 +1,3 @@ +{{- define "option-validator-auto-footer.go.tmpl" }} +// <---- end of auto generated +{{ end }} \ No newline at end of file diff --git a/generators/gola/templates/option-validator-auto/option-validator-auto-header.go.tmpl b/generators/gola/templates/option-validator-auto/option-validator-auto-header.go.tmpl new file mode 100644 index 0000000..c9cb382 --- /dev/null +++ b/generators/gola/templates/option-validator-auto/option-validator-auto-header.go.tmpl @@ -0,0 +1,13 @@ +{{- define "option-validator-auto-header.go.tmpl" -}} +package assistant + +import ( + "net" + "time" + + "github.com/spf13/pflag" +) + +// ----> auto generated(option-validator-auto/gen-ov) + +{{ end -}} \ No newline at end of file diff --git a/generators/gola/templates/option-validator-auto_test/option-validator-auto_test-body.go.tmpl b/generators/gola/templates/option-validator-auto_test/option-validator-auto_test-body.go.tmpl new file mode 100644 index 0000000..d504a68 --- /dev/null +++ b/generators/gola/templates/option-validator-auto_test/option-validator-auto_test-body.go.tmpl @@ -0,0 +1,3 @@ +{{ define "option-validator-auto_test_FAKE" }} +// option-validator-auto_test_FAKE ๐Ÿค +{{ end }} diff --git a/generators/gola/templates/option-validator-auto_test/option-validator-auto_test-footer.go.tmpl b/generators/gola/templates/option-validator-auto_test/option-validator-auto_test-footer.go.tmpl new file mode 100644 index 0000000..c5ac911 --- /dev/null +++ b/generators/gola/templates/option-validator-auto_test/option-validator-auto_test-footer.go.tmpl @@ -0,0 +1,4 @@ +{{ define "_footer "}} + +// <---- end of auto generated +{{ end }} diff --git a/generators/gola/templates/option-validator-auto_test/option-validator-auto_test-header.go.tmpl b/generators/gola/templates/option-validator-auto_test/option-validator-auto_test-header.go.tmpl new file mode 100644 index 0000000..549f959 --- /dev/null +++ b/generators/gola/templates/option-validator-auto_test/option-validator-auto_test-header.go.tmpl @@ -0,0 +1,18 @@ +{{ define "_header "}} +package assistant_test + +import ( + "fmt" + "net" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/snivilised/cobrass/src/assistant" +) + +// ----> auto generated(option-validator-auto_test/gen-ov-t) +{{ end }} diff --git a/generators/gola/templates/param-set-auto/param-set-auto-body.go.tmpl b/generators/gola/templates/param-set-auto/param-set-auto-body.go.tmpl new file mode 100644 index 0000000..e6ad965 --- /dev/null +++ b/generators/gola/templates/param-set-auto/param-set-auto-body.go.tmpl @@ -0,0 +1,3 @@ +{{ define "param-set-auto_FAKE" }} +// param-set-auto_FAKE ๐Ÿค +{{ end }} diff --git a/generators/gola/templates/param-set-auto/param-set-auto-footer.go.tmpl b/generators/gola/templates/param-set-auto/param-set-auto-footer.go.tmpl new file mode 100644 index 0000000..c5ac911 --- /dev/null +++ b/generators/gola/templates/param-set-auto/param-set-auto-footer.go.tmpl @@ -0,0 +1,4 @@ +{{ define "_footer "}} + +// <---- end of auto generated +{{ end }} diff --git a/generators/gola/templates/param-set-auto/param-set-auto-header.go.tmpl b/generators/gola/templates/param-set-auto/param-set-auto-header.go.tmpl new file mode 100644 index 0000000..8e0ce02 --- /dev/null +++ b/generators/gola/templates/param-set-auto/param-set-auto-header.go.tmpl @@ -0,0 +1,10 @@ +{{ define "_header "}} +package assistant + +import ( + "net" + "time" +) + +// ----> auto generated(param-set-auto/gen-ps) +{{ end }} diff --git a/generators/gola/templates/param-set-auto_test/param-set-auto_test-body.go.tmpl b/generators/gola/templates/param-set-auto_test/param-set-auto_test-body.go.tmpl new file mode 100644 index 0000000..d177279 --- /dev/null +++ b/generators/gola/templates/param-set-auto_test/param-set-auto_test-body.go.tmpl @@ -0,0 +1,3 @@ +{{ define "param-set-auto_test_FAKE" }} +// param-set-auto_test_FAKE ๐Ÿค +{{ end }} diff --git a/generators/gola/templates/param-set-auto_test/param-set-auto_test-footer.go.tmpl b/generators/gola/templates/param-set-auto_test/param-set-auto_test-footer.go.tmpl new file mode 100644 index 0000000..c5ac911 --- /dev/null +++ b/generators/gola/templates/param-set-auto_test/param-set-auto_test-footer.go.tmpl @@ -0,0 +1,4 @@ +{{ define "_footer "}} + +// <---- end of auto generated +{{ end }} diff --git a/generators/gola/templates/param-set-auto_test/param-set-auto_test-header.go.tmpl b/generators/gola/templates/param-set-auto_test/param-set-auto_test-header.go.tmpl new file mode 100644 index 0000000..ade7e8b --- /dev/null +++ b/generators/gola/templates/param-set-auto_test/param-set-auto_test-header.go.tmpl @@ -0,0 +1,17 @@ +{{ define "_header "}} +package assistant_test + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/snivilised/cobrass/src/assistant" + "github.com/snivilised/cobrass/src/internal/helpers" +) + +// ----> auto generated(param-set-auto_test/gen-ps-t) +{{ end }} diff --git a/generators/gola/templates/param-set-binder-helpers-auto/param-set-binder-helpers-auto-body.go.tmpl b/generators/gola/templates/param-set-binder-helpers-auto/param-set-binder-helpers-auto-body.go.tmpl new file mode 100644 index 0000000..710d1a3 --- /dev/null +++ b/generators/gola/templates/param-set-binder-helpers-auto/param-set-binder-helpers-auto-body.go.tmpl @@ -0,0 +1,3 @@ +{{ define "param-set-binder-helpers-auto_FAKE" }} +// param-set-binder-helpers-auto_FAKE ๐Ÿค +{{ end }} diff --git a/generators/gola/templates/param-set-binder-helpers-auto/param-set-binder-helpers-auto-footer.go.tmpl b/generators/gola/templates/param-set-binder-helpers-auto/param-set-binder-helpers-auto-footer.go.tmpl new file mode 100644 index 0000000..c5ac911 --- /dev/null +++ b/generators/gola/templates/param-set-binder-helpers-auto/param-set-binder-helpers-auto-footer.go.tmpl @@ -0,0 +1,4 @@ +{{ define "_footer "}} + +// <---- end of auto generated +{{ end }} diff --git a/generators/gola/templates/param-set-binder-helpers-auto/param-set-binder-helpers-auto-header.go.tmpl b/generators/gola/templates/param-set-binder-helpers-auto/param-set-binder-helpers-auto-header.go.tmpl new file mode 100644 index 0000000..52a8448 --- /dev/null +++ b/generators/gola/templates/param-set-binder-helpers-auto/param-set-binder-helpers-auto-header.go.tmpl @@ -0,0 +1,16 @@ +{{ define "_header "}} +//nolint:gocritic // regexp.MustCompile: solution unknown +package assistant + +import ( + "regexp" + "time" + + "github.com/samber/lo" + "github.com/snivilised/cobrass/src/assistant/i18n" + + "github.com/spf13/pflag" +) + +// ----> auto generated(param-set-binder-helpers-auto/gen-help) +{{ end }} diff --git a/generators/gola/templates/param-set-binder-helpers-auto_test/param-set-binder-helpers-auto_test-body.go.tmpl b/generators/gola/templates/param-set-binder-helpers-auto_test/param-set-binder-helpers-auto_test-body.go.tmpl new file mode 100644 index 0000000..29707e8 --- /dev/null +++ b/generators/gola/templates/param-set-binder-helpers-auto_test/param-set-binder-helpers-auto_test-body.go.tmpl @@ -0,0 +1,3 @@ +{{ define "param-set-binder-helpers-auto_test_FAKE" }} +// param-set-binder-helpers-auto_test_FAKE ๐Ÿค +{{ end }} diff --git a/generators/gola/templates/param-set-binder-helpers-auto_test/param-set-binder-helpers-auto_test-footer.go.tmpl b/generators/gola/templates/param-set-binder-helpers-auto_test/param-set-binder-helpers-auto_test-footer.go.tmpl new file mode 100644 index 0000000..c5ac911 --- /dev/null +++ b/generators/gola/templates/param-set-binder-helpers-auto_test/param-set-binder-helpers-auto_test-footer.go.tmpl @@ -0,0 +1,4 @@ +{{ define "_footer "}} + +// <---- end of auto generated +{{ end }} diff --git a/generators/gola/templates/param-set-binder-helpers-auto_test/param-set-binder-helpers-auto_test-header.go.tmpl b/generators/gola/templates/param-set-binder-helpers-auto_test/param-set-binder-helpers-auto_test-header.go.tmpl new file mode 100644 index 0000000..66fc511 --- /dev/null +++ b/generators/gola/templates/param-set-binder-helpers-auto_test/param-set-binder-helpers-auto_test-header.go.tmpl @@ -0,0 +1,16 @@ +{{ define "_header "}} +package assistant_test + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/snivilised/cobrass/src/assistant" +) + +// ----> auto generated(param-set-binder-helpers-auto_test/gen-help-t) +{{ end }} diff --git a/generators/gola/templates/top/option-validator-auto.templ.top.txt b/generators/gola/templates/top/option-validator-auto.templ.top.txt deleted file mode 100644 index 025dc2c..0000000 --- a/generators/gola/templates/top/option-validator-auto.templ.top.txt +++ /dev/null @@ -1,14 +0,0 @@ -package assistant - -import ( - "net" - "time" - - "github.com/spf13/pflag" -) - -// ----> auto generated(Build-Validators/gen-ov) - -{{ PLACEHOLDER }} - -// <---- end of auto generated diff --git a/generators/gola/templates/top/option-validator-auto_test.templ.top.txt b/generators/gola/templates/top/option-validator-auto_test.templ.top.txt deleted file mode 100644 index be71a5d..0000000 --- a/generators/gola/templates/top/option-validator-auto_test.templ.top.txt +++ /dev/null @@ -1,73 +0,0 @@ -package assistant_test - -import ( - "fmt" - "net" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - - "github.com/snivilised/cobrass/src/assistant" -) - -type OvEntry struct { - Message string - Validator func() assistant.OptionValidator - Setup func() -} - -var _ = Describe("OptionValidator", func() { - - var rootCommand *cobra.Command - var widgetCommand *cobra.Command - var paramSet *assistant.ParamSet[WidgetParameterSet] - var outputFormatEnumInfo *assistant.EnumInfo[OutputFormatEnum] - - BeforeEach(func() { - outputFormatEnumInfo = assistant.NewEnumInfo(AcceptableOutputFormats) - - rootCommand = &cobra.Command{ - Use: "poke", - Short: "A brief description of your application", - Long: "A long description of the root poke command", - } - - widgetCommand = &cobra.Command{ - Version: "1.0.1", - Use: "widget", - Short: "Create widget", - Long: "Index file system at root: '/'", - Args: cobra.ExactArgs(1), - - RunE: func(command *cobra.Command, args []string) error { - GinkgoWriter.Printf("===> ๐Ÿ“ EXECUTE (Directory: '%v')\n", args[0]) - - paramSet.Native.Directory = args[0] - return nil - }, - } - rootCommand.AddCommand(widgetCommand) - - paramSet = assistant.NewParamSet[WidgetParameterSet](widgetCommand) - }) - - DescribeTable("ParamSet with validation", - func(entry OvEntry) { - validator := entry.Validator() - entry.Setup() - _ = validator.Validate() - }, - func(entry OvEntry) string { - return fmt.Sprintf("๐Ÿงช --> ๐Ÿ’ given: flag type is '%v'", entry.Message) - }, - - // ----> auto generated(Build-TestEntry/gen-ov-t) - - {{ PLACEHOLDER }} - - // <---- end of auto generated - ) -}) diff --git a/generators/gola/templates/top/param-set-auto.templ.top.txt b/generators/gola/templates/top/param-set-auto.templ.top.txt deleted file mode 100644 index cd5f585..0000000 --- a/generators/gola/templates/top/param-set-auto.templ.top.txt +++ /dev/null @@ -1,12 +0,0 @@ -package assistant - -import ( - "net" - "time" -) - -// ----> auto generated(Build-ParamSet/gen-ps) - -{{ PLACEHOLDER }} - -// <---- end of auto generated diff --git a/generators/gola/templates/top/param-set-auto_test.templ.top.txt b/generators/gola/templates/top/param-set-auto_test.templ.top.txt deleted file mode 100644 index 380eaea..0000000 --- a/generators/gola/templates/top/param-set-auto_test.templ.top.txt +++ /dev/null @@ -1,82 +0,0 @@ -package assistant_test - -import ( - "fmt" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/spf13/cobra" - - "github.com/snivilised/cobrass/src/assistant" - "github.com/snivilised/cobrass/src/internal/helpers" -) - -// the auto version of param-set_test.go - -var _ = Describe("ParamSet (auto)", func() { - - When("Binding a flag (auto)", func() { - var rootCommand *cobra.Command - var widgetCommand *cobra.Command - var paramSet *assistant.ParamSet[WidgetParameterSet] - var outputFormatEnumInfo *assistant.EnumInfo[OutputFormatEnum] - var outputFormatEnum assistant.EnumValue[OutputFormatEnum] - - BeforeEach(func() { - rootCommand = &cobra.Command{ - Use: "poke", - Short: "A brief description of your application", - Long: "A long description of the root poke command", - } - - widgetCommand = &cobra.Command{ - Version: "1.0.1", - Use: "widget", - Short: "Create widget", - Long: "Index file system at root: '/'", - Args: cobra.ExactArgs(1), - - PreRun: func(command *cobra.Command, args []string) { - GinkgoWriter.Printf("**** ๐Ÿ‰ PRE-RUN\n") - }, - RunE: func(command *cobra.Command, args []string) error { - GinkgoWriter.Printf("===> ๐Ÿ“ EXECUTE (Directory: '%v')\n", args[0]) - - paramSet.Native.Directory = args[0] - return nil - }, - PostRun: func(command *cobra.Command, args []string) { - GinkgoWriter.Printf("**** ๐Ÿฅฅ POST-RUN\n") - }, - } - rootCommand.AddCommand(widgetCommand) - - paramSet = assistant.NewParamSet[WidgetParameterSet](widgetCommand) - - outputFormatEnumInfo = assistant.NewEnumInfo(AcceptableOutputFormats) - outputFormatEnum = outputFormatEnumInfo.NewValue() - }) - - DescribeTable("binder", - func(entry TcEntry) { - entry.Binder() - - _, _ = helpers.ExecuteCommand( - rootCommand, "widget", "/usr/fuse/home/music", entry.CommandLine, - ) - entry.Assert() - }, - - func(entry TcEntry) string { - return fmt.Sprintf("๐Ÿงช --> ๐Ÿ’ given: flag is '%v'", entry.Message) - }, - - // ----> auto generated(Build-PsTestEntry/gen-ps-t) - - {{ PLACEHOLDER }} - - // <---- auto generated(Build-PsTestEntry/gen-ps-t) - ) - }) -}) diff --git a/generators/gola/templates/top/param-set-binder-helpers-auto.templ.top.txt b/generators/gola/templates/top/param-set-binder-helpers-auto.templ.top.txt deleted file mode 100644 index f478cb4..0000000 --- a/generators/gola/templates/top/param-set-binder-helpers-auto.templ.top.txt +++ /dev/null @@ -1,18 +0,0 @@ -//nolint:gocritic // regexp.MustCompile: solution unknown -package assistant - -import ( - "regexp" - "time" - - "github.com/samber/lo" - "github.com/snivilised/cobrass/src/assistant/i18n" - - "github.com/spf13/pflag" -) - -// ----> auto generated(Build-Predefined/gen-help) - -{{ PLACEHOLDER }} - -// <---- end of auto generated diff --git a/generators/gola/templates/top/param-set-binder-helpers-auto_test.templ.top.txt b/generators/gola/templates/top/param-set-binder-helpers-auto_test.templ.top.txt deleted file mode 100644 index 01400ef..0000000 --- a/generators/gola/templates/top/param-set-binder-helpers-auto_test.templ.top.txt +++ /dev/null @@ -1,64 +0,0 @@ -package assistant_test - -import ( - "fmt" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/spf13/cobra" - - "github.com/snivilised/cobrass/src/assistant" -) - -type validatorDecorator struct { - Decorated assistant.OptionValidator -} - -func (v *validatorDecorator) Validate() error { - flag := v.Decorated.GetFlag() - flag.Changed = true - - return v.Decorated.Validate() -} - -var _ = Describe("ParamSetBinderHelpers", func() { - var rootCommand *cobra.Command - var widgetCommand *cobra.Command - var paramSet *assistant.ParamSet[WidgetParameterSet] - var outputFormatEnumInfo *assistant.EnumInfo[OutputFormatEnum] - var outputFormatEnum assistant.EnumValue[OutputFormatEnum] - - Context("Comparables", func() { - BeforeEach(func() { - outputFormatEnumInfo = assistant.NewEnumInfo(AcceptableOutputFormats) - outputFormatEnum = outputFormatEnumInfo.NewValue() - - rootCommand = &cobra.Command{ - Use: "flick", - Short: "A brief description of your application", - Long: "A long description of the root flick command", - } - - widgetCommand = &cobra.Command{ - Version: "1.0.1", - Use: "widget", - Short: "Create widget", - Long: "Index file system at root: '/'", - Args: cobra.ExactArgs(1), - RunE: func(command *cobra.Command, args []string) error { - paramSet.Native.Directory = args[0] - return nil - }, - } - rootCommand.AddCommand(widgetCommand) - paramSet = assistant.NewParamSet[WidgetParameterSet](widgetCommand) - }) - - // ----> auto generated(Build-BinderHelperTests/gen-help-t) - - {{ PLACEHOLDER }} - - // <---- auto generated - }) -}) diff --git a/generators/gola/type-spec.go b/generators/gola/type-spec.go new file mode 100644 index 0000000..6eeb451 --- /dev/null +++ b/generators/gola/type-spec.go @@ -0,0 +1,554 @@ +package gola + +type PsCaseEntry struct { // this needs a better name, not Ps + AssertFn string +} + +type TestCaseEntry struct { + DoesContain []string + DoesNotContain []string + Below []string + EqualLo []string + Inside []string + EqualHi []string + Above []string +} + +type BhTest struct { // binder helper + First string + Second string + Assign string + Entry TestCaseEntry +} + +type BhTestCollection map[string]*BhTest + +type TypeNameID = string + +type TypeSpec struct { + TypeName string + GoType string + DisplayType string + UnderlyingTypeName string + FlagName string + Short string + Def any + Assign string + Setup string + BindTo string + Assert string + QuoteExpect bool + Equate string + Validatable bool + ForeignValidatorFn bool + GenerateSlice bool + SliceFlagName string + SliceShort string + DefSliceVal string + ExpectSlice string + SliceValue string + IsOptionLess bool + OptionValue string + QuoteOptionValue bool + CommandLineValue string + TcEntry *PsCaseEntry + Comparable bool + BindDoc string + BindValidatedDoc string + Containable bool + BhParent string + CastLiteralsAs string + BhTests BhTestCollection +} + +func buildTypes() *typeCollection { + return &typeCollection{ + "Enum": &TypeSpec{ + TypeName: "Enum", + GoType: "string", + DisplayType: "enum", + UnderlyingTypeName: "String", + FlagName: "Format", + Short: "f", + Def: "xml", + Assign: "outputFormatEnum := outputFormatEnumInfo.NewValue()", + Setup: "paramSet.Native.Format = XMLFormatEn", + BindTo: "&outputFormatEnum.Source", + Assert: `Expect(value).To(Equal("xml"))`, + QuoteExpect: true, + Equate: "Equal", + Validatable: true, + ForeignValidatorFn: true, + GenerateSlice: false, + SliceFlagName: "Formats", + SliceShort: "F", + DefSliceVal: "[]string{}", + ExpectSlice: `[]string{"xml", "json", "text"}`, + SliceValue: "xml,json,text", + OptionValue: "json", + TcEntry: &PsCaseEntry{ + AssertFn: `func() { Expect(outputFormatEnum.Source).To(Equal("json")) }`, + }, + BindDoc: ` + +// Note that normally the client would bind to a member of the native parameter +// set. However, since there is a discrepancy between the type of the native int +// based pseudo enum member and the equivalent acceptable string value typed by +// the user on the command line (idiomatically stored on the enum info), the +// client needs to extract the enum value from the enum info, something like this: +// +// paramSet.Native.Format = OutputFormatEnumInfo.Value() +// +// The best place to put this would be inside the PreRun/PreRunE function, assuming the +// param set and the enum info are both in scope. Actually, every int based enum +// flag, would need to have this assignment performed. + `, + BindValidatedDoc: ` + +// Custom enum types created via the generic 'EnumInfo'/'EnumValue' come with a 'IsValid' method. +// The client can utilise this method inside a custom function passed into 'BindValidatedEnum'. +// The implementation would simply call this method, either on the EnumInfo or the EnumValue. +// Please see the readme for more details. + `, + Containable: true, + BhTests: BhTestCollection{ + "Contains": &BhTest{ + First: `[]string{"json", "text", "xml"}`, + Second: "\"null\"", + Assign: "outputFormatEnum.Source = value", + Entry: TestCaseEntry{ + DoesContain: []string{`"xml"`, "true"}, + DoesNotContain: []string{`"scr"`, "false"}, + }, + }, + }, + }, + + "String": &TypeSpec{ + TypeName: "String", + GoType: "string", + FlagName: "Pattern", + Short: "P", + Def: "default-pattern", + Setup: `paramSet.Native.Pattern = \"{{OPTION-VALUE}}\"`, + Assert: `Assert = "Expect(value).To(Equal(\"{{OPTION-VALUE}}\"))"`, + QuoteExpect: true, + Equate: "Equal", + Validatable: true, + GenerateSlice: true, + SliceFlagName: "Directories", + SliceShort: "C", + DefSliceVal: "[]string{}", + ExpectSlice: `[]string{"alpha", "beta", "delta"}`, + SliceValue: "alpha, beta, delta", + OptionValue: "music.infex", + // + TcEntry: &PsCaseEntry{ + // AssertFn function is optional, but is the first item checked for + // Next we assume Expect(value).To(something), + // where something can be + // - Equal[type](value) => type is optional template variable + // - BeTrue() + // or any other matcher + }, + Comparable: true, + Containable: true, + + BhTests: BhTestCollection{ + "Within": &BhTest{ + First: `"c"`, + Second: `"e"`, + Entry: TestCaseEntry{ + Below: []string{`"b"`, "false"}, + EqualLo: []string{`"c"`, "true"}, + Inside: []string{`"d"`, "true"}, + EqualHi: []string{`"e"`, "true"}, + Above: []string{`"f"`, "false"}, + }, + }, + + // TODO: more operators to come ... + }, + }, + + "Int": &TypeSpec{ + TypeName: "Int", + GoType: "int", + FlagName: "Offset", + Short: "o", + Def: -1, + Setup: "paramSet.Native.Offset = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + GenerateSlice: true, + SliceFlagName: "Offsets", + SliceShort: "D", + DefSliceVal: "[]int{}", + ExpectSlice: `[]int{2, 4, 6, 8}`, + SliceValue: "2,4,6,8", + OptionValue: "-9", + TcEntry: &PsCaseEntry{}, + // + Comparable: true, + Containable: true, + // + }, + + "Int8": &TypeSpec{ + TypeName: "Int8", + GoType: "int8", + FlagName: "Offset8", + Short: "o", + Def: "int8(-1)", + Setup: "paramSet.Native.Offset8 = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "-99", + // + TcEntry: &PsCaseEntry{}, + // + Comparable: true, + Containable: true, + // + BhParent: "Int", + CastLiteralsAs: "int8", + }, + + "Int16": &TypeSpec{ + TypeName: "Int16", + GoType: "int16", + FlagName: "Offset16", + Short: "o", + Def: "int16(-1)", + Setup: "paramSet.Native.Offset16 = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "-999", + // + TcEntry: &PsCaseEntry{}, // why no generate slice? + // + Comparable: true, + Containable: true, + // + BhParent: "Int", + CastLiteralsAs: "int16", + }, + + "Int32": &TypeSpec{ + TypeName: "Int32", + GoType: "int32", + FlagName: "Offset32", + Short: "o", + Def: "int32(-1)", + Setup: "paramSet.Native.Offset32 = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "-9999", + // + TcEntry: &PsCaseEntry{}, + // + GenerateSlice: true, + SliceFlagName: "Offsets32", + SliceShort: "O", + DefSliceVal: "[]int32{}", + ExpectSlice: "[]int32{2, 4, 6, 8}", + SliceValue: "2, 4, 6, 8", + // + Comparable: true, + Containable: true, + // + BhParent: "Int", + CastLiteralsAs: "int32", + }, + + "Int64": &TypeSpec{ + TypeName: "Int64", + GoType: "int64", + FlagName: "Offset64", + Short: "o", + Def: "int64(-1)", + Setup: "paramSet.Native.Offset64 = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "-99999", + // + TcEntry: &PsCaseEntry{}, + // + GenerateSlice: true, + SliceFlagName: "Offsets64", + SliceShort: "O", + DefSliceVal: "[]int64{}", + ExpectSlice: "[]int64{2, 4, 6, 8}", + SliceValue: "2, 4, 6, 8", + // + Comparable: true, + Containable: true, + // + BhParent: "Int", + CastLiteralsAs: "int64", + }, + + "Uint": &TypeSpec{ + // โ˜ข๏ธโ˜ข๏ธโ˜ข๏ธ In the PowerShell version, misspelt as Unit, this means that + // whats published, is probably broken see issue: + // - https://github.com/snivilised/cobrass/issues/192 + // + TypeName: "Uint", + GoType: "uint", + FlagName: "Count", + Short: "c", + Def: "uint(0)", + Setup: "paramSet.Native.Count = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "99999", + // + TcEntry: &PsCaseEntry{}, + // + GenerateSlice: true, + SliceFlagName: "Counts", + SliceShort: "P", + DefSliceVal: "[]uint{}", + ExpectSlice: "[]uint{2, 4, 6, 8}", + SliceValue: "2, 4, 6, 8", + // + Comparable: true, + Containable: true, + // + BhParent: "Int", + CastLiteralsAs: "uint", + }, + + "Uint8": &TypeSpec{ + TypeName: "Uint8", + GoType: "uint8", + FlagName: "Count8", + Short: "c", + Def: "uint8(0)", + Setup: "paramSet.Native.Count8 = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "33", + // + TcEntry: &PsCaseEntry{}, + // + Comparable: true, + Containable: true, + // + BhParent: "Int", + CastLiteralsAs: "uint8", + }, + + "Uint16": &TypeSpec{ + TypeName: "Uint16", + GoType: "uint16", + FlagName: "Count16", + Short: "c", + Def: "uint16(0)", + Setup: "paramSet.Native.Count16 = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "333", + // + TcEntry: &PsCaseEntry{}, + // + Comparable: true, + Containable: true, + // + BhParent: "Int", + CastLiteralsAs: "uint16", + }, + + "Uint32": &TypeSpec{ + TypeName: "Uint32", + GoType: "uint32", + FlagName: "Count32", + Short: "c", + Def: "uint32(0)", + Setup: "paramSet.Native.Count32 = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "3333", + // + TcEntry: &PsCaseEntry{}, + // + Comparable: true, + Containable: true, + // + BhParent: "Int", + CastLiteralsAs: "uint32", + }, + + "Uint64": &TypeSpec{ + TypeName: "Uint64", + GoType: "uint64", + FlagName: "Count64", + Short: "c", + Def: "uint64(0)", + Setup: "paramSet.Native.Count64 = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "33333", + // + TcEntry: &PsCaseEntry{}, + // + Comparable: true, + Containable: true, + // + BhParent: "Int", + CastLiteralsAs: "uint32", + }, + + "Float32": &TypeSpec{ + TypeName: "Float32", + GoType: "float32", + FlagName: "Gradientf32", + Short: "t", + Def: "float32(0)", + Setup: "paramSet.Native.Gradientf32 = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "32.0", + // + TcEntry: &PsCaseEntry{}, + // + GenerateSlice: true, + SliceFlagName: "Gradientsf32", + SliceShort: "G", + DefSliceVal: "[]float32{}", + ExpectSlice: "[]float32{3.0, 5.0, 7.0, 9.0}", + SliceValue: "3.0, 5.0, 7.0, 9.0", + // + Comparable: true, + Containable: true, + BhParent: "Int", + CastLiteralsAs: "float32", + }, + + "Float64": &TypeSpec{ + TypeName: "Float64", + GoType: "float64", + FlagName: "Gradientf64", + Short: "t", + Def: "float64(0)", + Setup: "paramSet.Native.Gradientf64 = {{OPTION-VALUE}}", + Assert: "Expect(value).To(Equal({{OPTION-VALUE}}))", + Equate: "Equal", + Validatable: true, + OptionValue: "64.1234", + // + TcEntry: &PsCaseEntry{}, + // + GenerateSlice: true, + SliceFlagName: "Gradientsf64", + SliceShort: "G", + DefSliceVal: "[]float64{}", + ExpectSlice: "[]float64{4.0, 6.0, 8.0, 10.0}", + SliceValue: "4.0, 6.0, 8.0, 10.0", + // + Comparable: true, + Containable: true, + BhParent: "Int", + CastLiteralsAs: "float64", + }, + + "Bool": &TypeSpec{ + TypeName: "Bool", + GoType: "bool", + FlagName: "Concise", + Short: "c", + Def: "false", + Setup: "paramSet.Native.Concise = {{OPTION-VALUE}}", + Assert: "Expect(value).To(BeTrue())", + QuoteExpect: true, + Equate: "Equal", + // bool is not Validatable, because there's not much to validate, + // can only be true or false + Validatable: true, + GenerateSlice: true, + SliceFlagName: "Switches", + SliceShort: "S", + DefSliceVal: "[]bool{}", + ExpectSlice: "[]bool{true, false, true, false}", + SliceValue: "true, false, true, false", + IsOptionLess: true, + OptionValue: "true", + TcEntry: &PsCaseEntry{}, + }, + + "Duration": &TypeSpec{ + TypeName: "Duration", + GoType: "time.Duration", + FlagName: "Latency", + Short: "l", + Def: `duration("0ms")`, + Setup: "paramSet.Native.Latency = {{OPTION-VALUE}}", + Assert: ` + expect := {{OPTION-VALUE}} + Expect(value).To(BeEquivalentTo(expect)) + `, + Equate: "BeEquivalentTo", + Validatable: true, + OptionValue: "300ms", + QuoteOptionValue: true, + TcEntry: &PsCaseEntry{}, + // + GenerateSlice: true, + SliceFlagName: "Latencies", + SliceShort: "L", + DefSliceVal: "[]time.Duration{}", + ExpectSlice: `[]time.Duration{duration("1s"), duration("2s"), duration("3s")}`, + SliceValue: "1s, 2s, 3s", + Comparable: true, + // + // 'duration' is a function defined in the test suite, that is syntactically the + // same as a type cast. + // + CastLiteralsAs: "duration", + BhTests: BhTestCollection{ + // tbd ... + }, + }, + + "IPNet": &TypeSpec{ + TypeName: "IPNet", + GoType: "net.IPNet", + FlagName: "IPAddress", + Short: "i", + Def: `ipnet("default")`, + Setup: "paramSet.Native.IPAddress = {{OPTION-VALUE}}", + Assert: "Expect(value).To(BeEquivalentTo({{OPTION-VALUE}}))", + Equate: "BeEquivalentTo", + Validatable: true, + OptionValue: `ipnet("orion.net")`, + CommandLineValue: "172.16.0.0", + TcEntry: &PsCaseEntry{}, + }, + + "IPMask": &TypeSpec{ + TypeName: "IPMask", + GoType: "net.IPMask", + FlagName: "IPMask", + Short: "m", + Def: `ipmask("default")`, + Setup: "paramSet.Native.IPMask = {{OPTION-VALUE}}", + Assert: "Expect(value).To(BeEquivalentTo({{OPTION-VALUE}}))", + Equate: "BeEquivalentTo", + Validatable: true, + OptionValue: `ipmask("orion.net")`, + CommandLineValue: "255.255.255.0", + TcEntry: &PsCaseEntry{}, + }, + } +} diff --git a/generators/ps/generate-option-validators.ps1 b/generators/ps/generate-option-validators.ps1 index 88b74e5..1314f65 100644 --- a/generators/ps/generate-option-validators.ps1 +++ b/generators/ps/generate-option-validators.ps1 @@ -890,7 +890,7 @@ function Build-Validators { # - func (validator XXXXOptionValidator) Validate() # - func (validator XXXXOptionValidator) GetFlag() *pflag.Flag # - if ($spec.Validatable) { + if ($spec.Validatable) { # [+] option-validator-type-defs.templ.snip.txt @" // $($validatorFn) defines the validator function for $($displayType) type. type $($validatorFn) func($($spec.GoType), *pflag.Flag) error @@ -901,7 +901,7 @@ type $($validatorFn) func($($spec.GoType), *pflag.Flag) error type $($validatorStruct) GenericOptionValidatorWrapper[$($spec.GoType)] "@ - if (-not($spec.ForeignValidatorFn)) { + if (-not($spec.ForeignValidatorFn)) { # [+] option-validator-func-defs.templ.snip.txt @" // Validate invokes the client defined validator function for $($displayType) type. func (validator $($validatorStruct)) Validate() error { @@ -916,7 +916,7 @@ func (validator $($validatorStruct)) GetFlag() *pflag.Flag { "@ } - if ($spec.GenerateSlice) { + if ($spec.GenerateSlice) { # [+] option-validator-slice-defs.templ.snip.txt # generate # - type XXXXSliceValidatorFn # - type XXXXSliceOptionValidator