From c67f767d1c96dc69b013c91234276c8d05c80a61 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Tue, 10 Sep 2024 10:05:17 +0200 Subject: [PATCH 01/17] feat!: initial commit for codegen CLI with golang support Signed-off-by: Florin-Mihai Anghel --- .gitignore | 25 +++++ src/example_go/experimentflags.go | 21 +++++ src/generators/generator.go | 78 ++++++++++++++++ src/generators/golang/golang.go | 148 ++++++++++++++++++++++++++++++ src/generators/golang/golang.tmpl | 25 +++++ src/go.mod | 14 +++ src/go.sum | 17 ++++ src/input.json | 9 ++ src/main.go | 110 ++++++++++++++++++++++ src/providers/providers.go | 10 ++ 10 files changed, 457 insertions(+) create mode 100644 .gitignore create mode 100644 src/example_go/experimentflags.go create mode 100644 src/generators/generator.go create mode 100644 src/generators/golang/golang.go create mode 100644 src/generators/golang/golang.tmpl create mode 100644 src/go.mod create mode 100644 src/go.sum create mode 100644 src/input.json create mode 100644 src/main.go create mode 100644 src/providers/providers.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64ae7f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env \ No newline at end of file diff --git a/src/example_go/experimentflags.go b/src/example_go/experimentflags.go new file mode 100644 index 0000000..0ec50c8 --- /dev/null +++ b/src/example_go/experimentflags.go @@ -0,0 +1,21 @@ +package experimentflags + +import ( + "codegen/providers" + "context" + "github.com/open-feature/go-sdk/openfeature" +) + +var client *openfeature.Client = nil +// This is a flag. +var MyOpenFeatureFlag = struct { + Value providers.BooleanProvider +}{ + Value: func(ctx context.Context) (bool, error) { + return client.BooleanValue(ctx, "myOpenFeatureFlag", false, openfeature.EvaluationContext{}) + }, +} + +func init() { + client = openfeature.NewClient("experimentflags") +} diff --git a/src/generators/generator.go b/src/generators/generator.go new file mode 100644 index 0000000..18f6430 --- /dev/null +++ b/src/generators/generator.go @@ -0,0 +1,78 @@ +package generator + +import ( + "bytes" + "fmt" + "os" + "text/template" +) + +// FlagType are the primitive types of flags. +type FlagType int + +// Collection of the different kinds of flag types +const ( + UnknownFlagType FlagType = iota + IntType + FloatType + BoolType + StringType + ObjectType +) + +// FlagTmplData is the per-flag specific data. +// It represents a common interface between Mendel source and codegen file output. +type FlagTmplData struct { + Name string + Type FlagType + DefaultValue string + Docs string +} + +// BaseTmplData is the base for all OpenFeature code generation. +type BaseTmplData struct { + OutputDir string + Flags []*FlagTmplData +} + +type TmplDataInterface interface { + // BaseTmplDataInfo returns a pointer to a BaseTmplData struct containing + // all the relevant information needed for metadata construction. + BaseTmplDataInfo() *BaseTmplData +} + +type Input struct { + BaseData *BaseTmplData +} + +type Generator interface { + Generate(input Input) error + SupportedFlagTypes() map[FlagType]bool +} + +func GenerateFile(funcs template.FuncMap, outputPath string, contents string, data TmplDataInterface) error { + contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents) + if err != nil { + return err + } + + var buf bytes.Buffer + if err := contentsTmpl.Execute(&buf, data); err != nil { + return err + } + + f, err := os.Create(outputPath) + if err != nil { + return err + } + defer f.Close() + writtenBytes, err := f.Write(buf.Bytes()) + if err != nil { + return err + } + if writtenBytes != buf.Len() { + return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", outputPath) + } + + return nil +} diff --git a/src/generators/golang/golang.go b/src/generators/golang/golang.go new file mode 100644 index 0000000..78db8da --- /dev/null +++ b/src/generators/golang/golang.go @@ -0,0 +1,148 @@ +package golang + +import ( + generator "codegen/generators" + _ "embed" + "sort" + "strconv" + "text/template" + + "github.com/iancoleman/strcase" +) + +type TmplData struct { + *generator.BaseTmplData + GoPackage string +} + +type genImpl struct { + file string + goPackage string +} + +func (td *TmplData) BaseTmplDataInfo() *generator.BaseTmplData { + return td.BaseTmplData +} + +// supportedFlagTypes is the flag types supported by the Go template. +var supportedFlagTypes = map[generator.FlagType]bool{ + generator.FloatType: true, + generator.StringType: true, + generator.IntType: true, + generator.BoolType: true, + generator.ObjectType: false, +} + +func (*genImpl) SupportedFlagTypes() map[generator.FlagType]bool { + return supportedFlagTypes +} + +//go:embed golang.tmpl +var golangTmpl string + +// Go Funcs BEGIN + +func flagVarName(flagName string) string { + return strcase.ToCamel(flagName) +} + +func flagInitParam(flagName string) string { + return strconv.Quote(flagName) +} + +// flagVarType returns the Go type for a flag's proto definition. +func providerType(t generator.FlagType) string { + switch t { + case generator.IntType: + return "IntProvider" + case generator.FloatType: + return "FloatProvider" + case generator.BoolType: + return "BooleanProvider" + case generator.StringType: + return "StringProvider" + } + return "" +} + +func flagAccessFunc(t generator.FlagType) string { + switch t { + case generator.IntType: + return "IntValue" + case generator.FloatType: + return "FloatValue" + case generator.BoolType: + return "BooleanValue" + case generator.StringType: + return "StringValue" + } + return "" +} + +func supportImports(flags []*generator.FlagTmplData) []string { + var res []string + if len(flags) > 0 { + res = append(res, "\"context\"") + res = append(res, "\"github.com/open-feature/go-sdk/openfeature\"") + res = append(res, "\"codegen/providers\"") + } + sort.Strings(res) + return res +} + +func defaultValueLiteral(flag *generator.FlagTmplData) (string, error) { + switch flag.Type { + case generator.StringType: + return strconv.Quote(flag.DefaultValue), nil + default: + return flag.DefaultValue, nil + } +} + +func typeString(flagType generator.FlagType) string { + switch flagType { + case generator.StringType: + return "string" + case generator.IntType: + return "int" + case generator.BoolType: + return "bool" + case generator.FloatType: + return "float64" + default: + return "" + } +} + +// Go Funcs END + +func (g *genImpl) Generate(input generator.Input) error { + funcs := template.FuncMap{ + "FlagVarName": flagVarName, + "FlagInitParam": flagInitParam, + "ProviderType": providerType, + "FlagAccessFunc": flagAccessFunc, + "SupportImports": supportImports, + "DefaultValueLiteral": defaultValueLiteral, + "TypeString": typeString, + } + td := TmplData{ + BaseTmplData: input.BaseData, + GoPackage: g.goPackage, + } + return generator.GenerateFile(funcs, g.file, golangTmpl, &td) +} + +// Params are parameters for creating a Generator +type Params struct { + File string + GoPackage string +} + +// NewGenerator creates a generator for Go. +func NewGenerator(params Params) generator.Generator { + return &genImpl{ + file: params.File, + goPackage: params.GoPackage, + } +} diff --git a/src/generators/golang/golang.tmpl b/src/generators/golang/golang.tmpl new file mode 100644 index 0000000..d14bf4f --- /dev/null +++ b/src/generators/golang/golang.tmpl @@ -0,0 +1,25 @@ +package {{.GoPackage}} + +import ( +{{- range $_, $p := SupportImports .Flags}} + {{$p}} +{{- end}} +) + +var client *openfeature.Client = nil + + +{{- range .Flags}} +// {{.Docs}} +var {{FlagVarName .Name}} = struct { + Value providers.{{ProviderType .Type}} +}{ + Value: func(ctx context.Context) ({{TypeString .Type}}, error) { + return client.{{FlagAccessFunc .Type}}(ctx, {{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, openfeature.EvaluationContext{}) + }, +} +{{- end}} + +func init() { + client = openfeature.NewClient("{{.GoPackage}}") +} diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..c90e72f --- /dev/null +++ b/src/go.mod @@ -0,0 +1,14 @@ +module codegen + +go 1.22.5 + +require ( + github.com/go-logr/logr v1.4.2 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect +) + +require ( + github.com/iancoleman/strcase v0.3.0 + github.com/open-feature/go-sdk v1.12.0 + github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 +) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..1a5a2eb --- /dev/null +++ b/src/go.sum @@ -0,0 +1,17 @@ +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/open-feature/go-sdk v1.12.0 h1:V0MAG3lC9o7Pmq0gxlqtKpoasDTm3to9vuvZKyUhhPk= +github.com/open-feature/go-sdk v1.12.0/go.mod h1:UDNuwVrwY5FRHIluVRYzvxuS3nBkhjE6o4tlwFuHxiI= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y= +rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/src/input.json b/src/input.json new file mode 100644 index 0000000..791dfec --- /dev/null +++ b/src/input.json @@ -0,0 +1,9 @@ +{ + "flags": { + "myOpenFeatureFlag":{ + "flag_type": "boolean", + "default_value": false, + "description": "This is a flag." + } + } +} \ No newline at end of file diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..5f8007a --- /dev/null +++ b/src/main.go @@ -0,0 +1,110 @@ +package main + +import ( + generator "codegen/generators" + "codegen/generators/golang" + _ "embed" + "encoding/json" + "flag" + "fmt" + "os" + "path" + "strconv" + + jsonschema "github.com/santhosh-tekuri/jsonschema/v5" +) + +const flagManifestSchema = "../docs/schema/v0/flag_manifest.json" + +var flagManifestPath = flag.String("flag_manifest_path", "", "Path to the flag manifest.") +var moduleName = flag.String("module_name", "", "Name of the module to be generated.") +var outputPath = flag.String("output_path", "", "Output path for the codegen") + +var stringToFlagType = map[string]generator.FlagType{ + "string": generator.StringType, + "boolean": generator.BoolType, + "float": generator.FloatType, + "integer": generator.IntType, +} + +func getDefaultValue(defaultValue interface{}, flagType generator.FlagType) string { + switch flagType { + case generator.BoolType: + return strconv.FormatBool(defaultValue.(bool)) + case generator.IntType: + return strconv.FormatInt(defaultValue.(int64), 10) + case generator.FloatType: + return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64) + case generator.StringType: + return defaultValue.(string) + } + return "" +} + +func unmarshalFlagManifest(data []byte) (*generator.BaseTmplData, error) { + dynamic := make(map[string]interface{}) + err := json.Unmarshal(data, &dynamic) + if err != nil { + return nil, err + } + + sch, err := jsonschema.Compile(flagManifestSchema) + if err != nil { + return nil, err + } + if err = sch.Validate(dynamic); err != nil { + return nil, err + } + iFlags := dynamic["flags"] + flags := iFlags.(map[string]interface{}) + btData := generator.BaseTmplData{ + OutputDir: path.Dir(*outputPath), + } + for flagKey, iFlagData := range flags { + flagData := iFlagData.(map[string]interface{}) + flagType := stringToFlagType[flagData["flag_type"].(string)] + docs := flagData["description"].(string) + defaultValue := getDefaultValue(flagData["default_value"], flagType) + btData.Flags = append(btData.Flags, &generator.FlagTmplData{ + Name: flagKey, + Type: flagType, + DefaultValue: defaultValue, + Docs: docs, + }) + } + return &btData, nil +} + +func loadData(manifestPath string) (*generator.BaseTmplData, error) { + data, err := os.ReadFile(manifestPath) + if err != nil { + return nil, fmt.Errorf("error reading contents from file %q", manifestPath) + } + return unmarshalFlagManifest(data) +} + +func generate(gen generator.Generator) error { + btData, err := loadData(*flagManifestPath) + if err != nil { + return err + } + input := generator.Input{ + BaseData: btData, + } + return gen.Generate(input) +} + +func main() { + flag.Parse() + _, filename := path.Split(*outputPath) + params := golang.Params{ + File: filename, + // Probably some conversion applied here, toLower and remove special characters. + GoPackage: *moduleName, + } + gen := golang.NewGenerator(params) + err := generate(gen) + if err != nil { + fmt.Printf("error generating flag accesssors: %v\n", err) + } +} diff --git a/src/providers/providers.go b/src/providers/providers.go new file mode 100644 index 0000000..aa91ad6 --- /dev/null +++ b/src/providers/providers.go @@ -0,0 +1,10 @@ +package providers + +import ( + "context" +) + +type BooleanProvider func(ctx context.Context) (bool, error) +type FloatProvider func(ctx context.Context) (float64, error) +type IntProvider func(ctx context.Context) (int, error) +type StringProvider func(ctx context.Context) (string, error) From ec7898564e6c9adc5a03c18426d920f0e905d27a Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Tue, 10 Sep 2024 10:23:16 +0200 Subject: [PATCH 02/17] docs!: add comments to few missing exported functions Signed-off-by: Florin-Mihai Anghel --- src/generators/generator.go | 2 ++ src/generators/golang/golang.go | 3 +++ src/main.go | 1 + 3 files changed, 6 insertions(+) diff --git a/src/generators/generator.go b/src/generators/generator.go index 18f6430..97cb393 100644 --- a/src/generators/generator.go +++ b/src/generators/generator.go @@ -45,11 +45,13 @@ type Input struct { BaseData *BaseTmplData } +// Generator provides interface to generate language specific, strongly-typed flag accessors. type Generator interface { Generate(input Input) error SupportedFlagTypes() map[FlagType]bool } +// GenerateFile receives data for the Go template engine and outputs the contents to the file. func GenerateFile(funcs template.FuncMap, outputPath string, contents string, data TmplDataInterface) error { contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents) if err != nil { diff --git a/src/generators/golang/golang.go b/src/generators/golang/golang.go index 78db8da..cc882c4 100644 --- a/src/generators/golang/golang.go +++ b/src/generators/golang/golang.go @@ -10,6 +10,7 @@ import ( "github.com/iancoleman/strcase" ) +// TmplData contains the Golang-specific data and the base data for the codegen. type TmplData struct { *generator.BaseTmplData GoPackage string @@ -20,6 +21,7 @@ type genImpl struct { goPackage string } +// BaseTmplDataInfo provides the base template data for the codegen. func (td *TmplData) BaseTmplDataInfo() *generator.BaseTmplData { return td.BaseTmplData } @@ -116,6 +118,7 @@ func typeString(flagType generator.FlagType) string { // Go Funcs END +// Generate generates the Go flag accessors for OpenFeature. func (g *genImpl) Generate(input generator.Input) error { funcs := template.FuncMap{ "FlagVarName": flagVarName, diff --git a/src/main.go b/src/main.go index 5f8007a..8a4f352 100644 --- a/src/main.go +++ b/src/main.go @@ -55,6 +55,7 @@ func unmarshalFlagManifest(data []byte) (*generator.BaseTmplData, error) { if err = sch.Validate(dynamic); err != nil { return nil, err } + // All casts can be done directly since the JSON is already validated by the schema. iFlags := dynamic["flags"] flags := iFlags.(map[string]interface{}) btData := generator.BaseTmplData{ From 32f8069f0829df32bd2dc1dd9410352ed0fc0bfb Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Tue, 10 Sep 2024 11:01:50 +0200 Subject: [PATCH 03/17] refactor!: add better error messages for the CLI Signed-off-by: Florin-Mihai Anghel --- src/generators/generator.go | 8 ++++---- src/generators/golang/golang.go | 6 +++--- src/main.go | 22 ++++++++++++++-------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/generators/generator.go b/src/generators/generator.go index 97cb393..6cee99c 100644 --- a/src/generators/generator.go +++ b/src/generators/generator.go @@ -55,22 +55,22 @@ type Generator interface { func GenerateFile(funcs template.FuncMap, outputPath string, contents string, data TmplDataInterface) error { contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents) if err != nil { - return err + return fmt.Errorf("error initializing template: %v", err) } var buf bytes.Buffer if err := contentsTmpl.Execute(&buf, data); err != nil { - return err + return fmt.Errorf("error executing template: %v", err) } f, err := os.Create(outputPath) if err != nil { - return err + return fmt.Errorf("error creating file %q: %v", outputPath, err) } defer f.Close() writtenBytes, err := f.Write(buf.Bytes()) if err != nil { - return err + return fmt.Errorf("error writing contents to file %q: %v", outputPath, err) } if writtenBytes != buf.Len() { return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", outputPath) diff --git a/src/generators/golang/golang.go b/src/generators/golang/golang.go index cc882c4..b286dbb 100644 --- a/src/generators/golang/golang.go +++ b/src/generators/golang/golang.go @@ -92,12 +92,12 @@ func supportImports(flags []*generator.FlagTmplData) []string { return res } -func defaultValueLiteral(flag *generator.FlagTmplData) (string, error) { +func defaultValueLiteral(flag *generator.FlagTmplData) string { switch flag.Type { case generator.StringType: - return strconv.Quote(flag.DefaultValue), nil + return strconv.Quote(flag.DefaultValue) default: - return flag.DefaultValue, nil + return flag.DefaultValue } } diff --git a/src/main.go b/src/main.go index 8a4f352..a438322 100644 --- a/src/main.go +++ b/src/main.go @@ -7,6 +7,7 @@ import ( "encoding/json" "flag" "fmt" + "log" "os" "path" "strconv" @@ -41,19 +42,19 @@ func getDefaultValue(defaultValue interface{}, flagType generator.FlagType) stri return "" } -func unmarshalFlagManifest(data []byte) (*generator.BaseTmplData, error) { +func unmarshalFlagManifest(data []byte, supportedFlagTypes map[generator.FlagType]bool) (*generator.BaseTmplData, error) { dynamic := make(map[string]interface{}) err := json.Unmarshal(data, &dynamic) if err != nil { - return nil, err + return nil, fmt.Errorf("error unmarshalling JSON: %v", err) } sch, err := jsonschema.Compile(flagManifestSchema) if err != nil { - return nil, err + return nil, fmt.Errorf("error compiling JSON schema: %v", err) } if err = sch.Validate(dynamic); err != nil { - return nil, err + return nil, fmt.Errorf("error validating JSON schema: %v", err) } // All casts can be done directly since the JSON is already validated by the schema. iFlags := dynamic["flags"] @@ -63,7 +64,12 @@ func unmarshalFlagManifest(data []byte) (*generator.BaseTmplData, error) { } for flagKey, iFlagData := range flags { flagData := iFlagData.(map[string]interface{}) - flagType := stringToFlagType[flagData["flag_type"].(string)] + flagTypeString := flagData["flag_type"].(string) + flagType := stringToFlagType[flagTypeString] + if !supportedFlagTypes[flagType] { + log.Printf("Skipping generation of flag %q as type %v is not supported for this language", flagKey, flagTypeString) + continue + } docs := flagData["description"].(string) defaultValue := getDefaultValue(flagData["default_value"], flagType) btData.Flags = append(btData.Flags, &generator.FlagTmplData{ @@ -76,16 +82,16 @@ func unmarshalFlagManifest(data []byte) (*generator.BaseTmplData, error) { return &btData, nil } -func loadData(manifestPath string) (*generator.BaseTmplData, error) { +func loadData(manifestPath string, supportedFlagTypes map[generator.FlagType]bool) (*generator.BaseTmplData, error) { data, err := os.ReadFile(manifestPath) if err != nil { return nil, fmt.Errorf("error reading contents from file %q", manifestPath) } - return unmarshalFlagManifest(data) + return unmarshalFlagManifest(data, supportedFlagTypes) } func generate(gen generator.Generator) error { - btData, err := loadData(*flagManifestPath) + btData, err := loadData(*flagManifestPath, gen.SupportedFlagTypes()) if err != nil { return err } From e4b02d0185b75d0137a12d5f6b6f3d5ea57b6815 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Tue, 24 Sep 2024 10:25:21 +0200 Subject: [PATCH 04/17] refactor: move package to the top of the repository Signed-off-by: Florin-Mihai Anghel --- src/go.mod => go.mod | 10 +++++----- go.sum | 14 ++++++++++++++ src/example_go/experimentflags.go | 12 +++++++----- src/generators/golang/golang.go | 4 ++-- src/go.sum | 17 ----------------- src/main.go | 4 ++-- 6 files changed, 30 insertions(+), 31 deletions(-) rename src/go.mod => go.mod (86%) create mode 100644 go.sum delete mode 100644 src/go.sum diff --git a/src/go.mod b/go.mod similarity index 86% rename from src/go.mod rename to go.mod index c90e72f..d3ee28a 100644 --- a/src/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module codegen go 1.22.5 require ( - github.com/go-logr/logr v1.4.2 // indirect - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + github.com/iancoleman/strcase v0.3.0 + github.com/open-feature/go-sdk v1.13.0 + github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 ) require ( - github.com/iancoleman/strcase v0.3.0 - github.com/open-feature/go-sdk v1.12.0 - github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 + github.com/go-logr/logr v1.4.2 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect ) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f0bf6ca --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/open-feature/go-sdk v1.13.0 h1:D5NXPhhCL0SNR/DRvrTOm/xY7uE9m0zQQEttgKHlwtI= +github.com/open-feature/go-sdk v1.13.0/go.mod h1:poPa+RFCJumHcb59wgp+tnSyNvMU2C07ykFJ0gczyaM= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= diff --git a/src/example_go/experimentflags.go b/src/example_go/experimentflags.go index 0ec50c8..9141c90 100644 --- a/src/example_go/experimentflags.go +++ b/src/example_go/experimentflags.go @@ -1,19 +1,21 @@ package experimentflags import ( - "codegen/providers" + "codegen/src/providers" "context" + "github.com/open-feature/go-sdk/openfeature" ) var client *openfeature.Client = nil + // This is a flag. var MyOpenFeatureFlag = struct { - Value providers.BooleanProvider + Value providers.BooleanProvider }{ - Value: func(ctx context.Context) (bool, error) { - return client.BooleanValue(ctx, "myOpenFeatureFlag", false, openfeature.EvaluationContext{}) - }, + Value: func(ctx context.Context) (bool, error) { + return client.BooleanValue(ctx, "myOpenFeatureFlag", false, openfeature.EvaluationContext{}) + }, } func init() { diff --git a/src/generators/golang/golang.go b/src/generators/golang/golang.go index b286dbb..32cdfe0 100644 --- a/src/generators/golang/golang.go +++ b/src/generators/golang/golang.go @@ -1,7 +1,7 @@ package golang import ( - generator "codegen/generators" + generator "codegen/src/generators" _ "embed" "sort" "strconv" @@ -86,7 +86,7 @@ func supportImports(flags []*generator.FlagTmplData) []string { if len(flags) > 0 { res = append(res, "\"context\"") res = append(res, "\"github.com/open-feature/go-sdk/openfeature\"") - res = append(res, "\"codegen/providers\"") + res = append(res, "\"codegen/src/providers\"") } sort.Strings(res) return res diff --git a/src/go.sum b/src/go.sum deleted file mode 100644 index 1a5a2eb..0000000 --- a/src/go.sum +++ /dev/null @@ -1,17 +0,0 @@ -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= -github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/open-feature/go-sdk v1.12.0 h1:V0MAG3lC9o7Pmq0gxlqtKpoasDTm3to9vuvZKyUhhPk= -github.com/open-feature/go-sdk v1.12.0/go.mod h1:UDNuwVrwY5FRHIluVRYzvxuS3nBkhjE6o4tlwFuHxiI= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y= -rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0= -rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/src/main.go b/src/main.go index a438322..7774282 100644 --- a/src/main.go +++ b/src/main.go @@ -1,8 +1,8 @@ package main import ( - generator "codegen/generators" - "codegen/generators/golang" + generator "codegen/src/generators" + "codegen/src/generators/golang" _ "embed" "encoding/json" "flag" From 47889fcbb185cf3134ff879df4a2335aeb06b7a3 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Tue, 24 Sep 2024 10:49:11 +0200 Subject: [PATCH 05/17] refactor: move main file to top directory Signed-off-by: Florin-Mihai Anghel --- src/main.go => main.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main.go => main.go (100%) diff --git a/src/main.go b/main.go similarity index 100% rename from src/main.go rename to main.go From e5620ed7e6dcce9d0370f8cdffe6c550d91bd7e4 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Thu, 26 Sep 2024 10:34:02 +0200 Subject: [PATCH 06/17] refactor: embed flag manifest schema into the script Signed-off-by: Florin-Mihai Anghel --- main.go | 7 +++++-- src/generators/generator.go | 11 ++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 7774282..4e29c12 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,10 @@ import ( jsonschema "github.com/santhosh-tekuri/jsonschema/v5" ) -const flagManifestSchema = "../docs/schema/v0/flag_manifest.json" +const flagManifestSchemaPath = "codegen/docs/schema/v0/flag_manifest.json" + +//go:embed docs/schema/v0/flag_manifest.json +var flagManifestSchema string var flagManifestPath = flag.String("flag_manifest_path", "", "Path to the flag manifest.") var moduleName = flag.String("module_name", "", "Name of the module to be generated.") @@ -49,7 +52,7 @@ func unmarshalFlagManifest(data []byte, supportedFlagTypes map[generator.FlagTyp return nil, fmt.Errorf("error unmarshalling JSON: %v", err) } - sch, err := jsonschema.Compile(flagManifestSchema) + sch, err := jsonschema.CompileString(flagManifestSchemaPath, flagManifestSchema) if err != nil { return nil, fmt.Errorf("error compiling JSON schema: %v", err) } diff --git a/src/generators/generator.go b/src/generators/generator.go index 6cee99c..b6c2e8b 100644 --- a/src/generators/generator.go +++ b/src/generators/generator.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "os" + "path" "text/template" ) @@ -52,7 +53,7 @@ type Generator interface { } // GenerateFile receives data for the Go template engine and outputs the contents to the file. -func GenerateFile(funcs template.FuncMap, outputPath string, contents string, data TmplDataInterface) error { +func GenerateFile(funcs template.FuncMap, filename string, contents string, data TmplDataInterface) error { contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents) if err != nil { return fmt.Errorf("error initializing template: %v", err) @@ -63,17 +64,17 @@ func GenerateFile(funcs template.FuncMap, outputPath string, contents string, da return fmt.Errorf("error executing template: %v", err) } - f, err := os.Create(outputPath) + f, err := os.Create(path.Join(data.BaseTmplDataInfo().OutputDir, filename)) if err != nil { - return fmt.Errorf("error creating file %q: %v", outputPath, err) + return fmt.Errorf("error creating file %q: %v", filename, err) } defer f.Close() writtenBytes, err := f.Write(buf.Bytes()) if err != nil { - return fmt.Errorf("error writing contents to file %q: %v", outputPath, err) + return fmt.Errorf("error writing contents to file %q: %v", filename, err) } if writtenBytes != buf.Len() { - return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", outputPath) + return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", filename) } return nil From 9b8ddd8c0328fd1217b9acb7ef862a9df0b498cd Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Thu, 26 Sep 2024 10:50:25 +0200 Subject: [PATCH 07/17] fix: remove files after being moved Signed-off-by: Florin-Mihai Anghel --- src/go.mod | 14 -------------- src/go.sum | 17 ----------------- 2 files changed, 31 deletions(-) delete mode 100644 src/go.mod delete mode 100644 src/go.sum diff --git a/src/go.mod b/src/go.mod deleted file mode 100644 index c90e72f..0000000 --- a/src/go.mod +++ /dev/null @@ -1,14 +0,0 @@ -module codegen - -go 1.22.5 - -require ( - github.com/go-logr/logr v1.4.2 // indirect - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect -) - -require ( - github.com/iancoleman/strcase v0.3.0 - github.com/open-feature/go-sdk v1.12.0 - github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 -) diff --git a/src/go.sum b/src/go.sum deleted file mode 100644 index 1a5a2eb..0000000 --- a/src/go.sum +++ /dev/null @@ -1,17 +0,0 @@ -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= -github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/open-feature/go-sdk v1.12.0 h1:V0MAG3lC9o7Pmq0gxlqtKpoasDTm3to9vuvZKyUhhPk= -github.com/open-feature/go-sdk v1.12.0/go.mod h1:UDNuwVrwY5FRHIluVRYzvxuS3nBkhjE6o4tlwFuHxiI= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y= -rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0= -rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From 5bdfc10e824c005f4b5dde008a4c24ab05257ce0 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Mon, 30 Sep 2024 11:44:09 +0200 Subject: [PATCH 08/17] refactor: move main.go back into SRC and attach module to flagmanifest schema Signed-off-by: Florin-Mihai Anghel --- docs/schema/v0/flagmanifest.go | 12 +++ main.go | 120 ------------------------------ src/example_go/experimentflags.go | 9 +-- src/main.go | 9 +-- 4 files changed, 20 insertions(+), 130 deletions(-) create mode 100644 docs/schema/v0/flagmanifest.go delete mode 100644 main.go diff --git a/docs/schema/v0/flagmanifest.go b/docs/schema/v0/flagmanifest.go new file mode 100644 index 0000000..f43e02f --- /dev/null +++ b/docs/schema/v0/flagmanifest.go @@ -0,0 +1,12 @@ +// Package flagmanifest embeds the flag manifest into a code module. +package flagmanifest + +import _ "embed" + +// Schema contains the embedded flag manifest schema. +// +//go:embed flag_manifest.json +var Schema string + +// SchemaPath proviees the current path and version of flag manifest. +const SchemaPath = "codegen/docs/schema/v0/flag_manifest.json" diff --git a/main.go b/main.go deleted file mode 100644 index 4e29c12..0000000 --- a/main.go +++ /dev/null @@ -1,120 +0,0 @@ -package main - -import ( - generator "codegen/src/generators" - "codegen/src/generators/golang" - _ "embed" - "encoding/json" - "flag" - "fmt" - "log" - "os" - "path" - "strconv" - - jsonschema "github.com/santhosh-tekuri/jsonschema/v5" -) - -const flagManifestSchemaPath = "codegen/docs/schema/v0/flag_manifest.json" - -//go:embed docs/schema/v0/flag_manifest.json -var flagManifestSchema string - -var flagManifestPath = flag.String("flag_manifest_path", "", "Path to the flag manifest.") -var moduleName = flag.String("module_name", "", "Name of the module to be generated.") -var outputPath = flag.String("output_path", "", "Output path for the codegen") - -var stringToFlagType = map[string]generator.FlagType{ - "string": generator.StringType, - "boolean": generator.BoolType, - "float": generator.FloatType, - "integer": generator.IntType, -} - -func getDefaultValue(defaultValue interface{}, flagType generator.FlagType) string { - switch flagType { - case generator.BoolType: - return strconv.FormatBool(defaultValue.(bool)) - case generator.IntType: - return strconv.FormatInt(defaultValue.(int64), 10) - case generator.FloatType: - return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64) - case generator.StringType: - return defaultValue.(string) - } - return "" -} - -func unmarshalFlagManifest(data []byte, supportedFlagTypes map[generator.FlagType]bool) (*generator.BaseTmplData, error) { - dynamic := make(map[string]interface{}) - err := json.Unmarshal(data, &dynamic) - if err != nil { - return nil, fmt.Errorf("error unmarshalling JSON: %v", err) - } - - sch, err := jsonschema.CompileString(flagManifestSchemaPath, flagManifestSchema) - if err != nil { - return nil, fmt.Errorf("error compiling JSON schema: %v", err) - } - if err = sch.Validate(dynamic); err != nil { - return nil, fmt.Errorf("error validating JSON schema: %v", err) - } - // All casts can be done directly since the JSON is already validated by the schema. - iFlags := dynamic["flags"] - flags := iFlags.(map[string]interface{}) - btData := generator.BaseTmplData{ - OutputDir: path.Dir(*outputPath), - } - for flagKey, iFlagData := range flags { - flagData := iFlagData.(map[string]interface{}) - flagTypeString := flagData["flag_type"].(string) - flagType := stringToFlagType[flagTypeString] - if !supportedFlagTypes[flagType] { - log.Printf("Skipping generation of flag %q as type %v is not supported for this language", flagKey, flagTypeString) - continue - } - docs := flagData["description"].(string) - defaultValue := getDefaultValue(flagData["default_value"], flagType) - btData.Flags = append(btData.Flags, &generator.FlagTmplData{ - Name: flagKey, - Type: flagType, - DefaultValue: defaultValue, - Docs: docs, - }) - } - return &btData, nil -} - -func loadData(manifestPath string, supportedFlagTypes map[generator.FlagType]bool) (*generator.BaseTmplData, error) { - data, err := os.ReadFile(manifestPath) - if err != nil { - return nil, fmt.Errorf("error reading contents from file %q", manifestPath) - } - return unmarshalFlagManifest(data, supportedFlagTypes) -} - -func generate(gen generator.Generator) error { - btData, err := loadData(*flagManifestPath, gen.SupportedFlagTypes()) - if err != nil { - return err - } - input := generator.Input{ - BaseData: btData, - } - return gen.Generate(input) -} - -func main() { - flag.Parse() - _, filename := path.Split(*outputPath) - params := golang.Params{ - File: filename, - // Probably some conversion applied here, toLower and remove special characters. - GoPackage: *moduleName, - } - gen := golang.NewGenerator(params) - err := generate(gen) - if err != nil { - fmt.Printf("error generating flag accesssors: %v\n", err) - } -} diff --git a/src/example_go/experimentflags.go b/src/example_go/experimentflags.go index 249a20d..5868666 100644 --- a/src/example_go/experimentflags.go +++ b/src/example_go/experimentflags.go @@ -7,14 +7,13 @@ import ( ) var client *openfeature.Client = nil - // This is a flag. var MyOpenFeatureFlag = struct { - Value providers.BooleanProvider + Value providers.BooleanProvider }{ - Value: func(ctx context.Context) (bool, error) { - return client.BooleanValue(ctx, "myOpenFeatureFlag", false, openfeature.EvaluationContext{}) - }, + Value: func(ctx context.Context) (bool, error) { + return client.BooleanValue(ctx, "myOpenFeatureFlag", false, openfeature.EvaluationContext{}) + }, } func init() { diff --git a/src/main.go b/src/main.go index a438322..adc3075 100644 --- a/src/main.go +++ b/src/main.go @@ -1,8 +1,9 @@ package main import ( - generator "codegen/generators" - "codegen/generators/golang" + flagmanifest "codegen/docs/schema/v0" + generator "codegen/src/generators" + "codegen/src/generators/golang" _ "embed" "encoding/json" "flag" @@ -15,8 +16,6 @@ import ( jsonschema "github.com/santhosh-tekuri/jsonschema/v5" ) -const flagManifestSchema = "../docs/schema/v0/flag_manifest.json" - var flagManifestPath = flag.String("flag_manifest_path", "", "Path to the flag manifest.") var moduleName = flag.String("module_name", "", "Name of the module to be generated.") var outputPath = flag.String("output_path", "", "Output path for the codegen") @@ -49,7 +48,7 @@ func unmarshalFlagManifest(data []byte, supportedFlagTypes map[generator.FlagTyp return nil, fmt.Errorf("error unmarshalling JSON: %v", err) } - sch, err := jsonschema.Compile(flagManifestSchema) + sch, err := jsonschema.CompileString(flagmanifest.SchemaPath, flagmanifest.Schema) if err != nil { return nil, fmt.Errorf("error compiling JSON schema: %v", err) } From a5059d074e74002eefe61f3b13a792a01ae98689 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Tue, 1 Oct 2024 09:50:35 +0200 Subject: [PATCH 09/17] refactor: change property names in manifest to camel case format Signed-off-by: Florin-Mihai Anghel --- src/input.json | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/input.json diff --git a/src/input.json b/src/input.json deleted file mode 100644 index 791dfec..0000000 --- a/src/input.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "flags": { - "myOpenFeatureFlag":{ - "flag_type": "boolean", - "default_value": false, - "description": "This is a flag." - } - } -} \ No newline at end of file From 01240cc511c5250f574581450a0b3c401403678d Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Tue, 1 Oct 2024 09:51:06 +0200 Subject: [PATCH 10/17] refactor: change property names to camel case format #2 Signed-off-by: Florin-Mihai Anghel --- docs/schema/v0/flag_manifest.json | 22 +++++++++++----------- src/main.go | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/schema/v0/flag_manifest.json b/docs/schema/v0/flag_manifest.json index 0adceb6..da02e97 100644 --- a/docs/schema/v0/flag_manifest.json +++ b/docs/schema/v0/flag_manifest.json @@ -36,16 +36,16 @@ "$ref": "#/$defs/objectType" } ], - "required": ["flag_type", "default_value"] + "required": ["flagType", "defaultValue"] }, "booleanType": { "type": "object", "properties": { - "flag_type": { + "flagType": { "type": "string", "enum": ["boolean"] }, - "default_value": { + "defaultValue": { "type": "boolean" }, "description": { @@ -57,11 +57,11 @@ "stringType": { "type": "object", "properties": { - "flag_type": { + "flagType": { "type": "string", "enum": ["string"] }, - "default_value": { + "defaultValue": { "type": "string" }, "description": { @@ -73,11 +73,11 @@ "integerType": { "type": "object", "properties": { - "flag_type": { + "flagType": { "type": "string", "enum": ["integer"] }, - "default_value": { + "defaultValue": { "type": "integer" }, "description": { @@ -89,11 +89,11 @@ "floatType": { "type": "object", "properties": { - "flag_type": { + "flagType": { "type": "string", "enum": ["float"] }, - "default_value": { + "defaultValue": { "type": "number" }, "description": { @@ -105,11 +105,11 @@ "objectType": { "type": "object", "properties": { - "flag_type": { + "flagType": { "type": "string", "enum": ["object"] }, - "default_value": { + "defaultValue": { "type": "object" }, "description": { diff --git a/src/main.go b/src/main.go index adc3075..bfc8051 100644 --- a/src/main.go +++ b/src/main.go @@ -63,14 +63,14 @@ func unmarshalFlagManifest(data []byte, supportedFlagTypes map[generator.FlagTyp } for flagKey, iFlagData := range flags { flagData := iFlagData.(map[string]interface{}) - flagTypeString := flagData["flag_type"].(string) + flagTypeString := flagData["flagType"].(string) flagType := stringToFlagType[flagTypeString] if !supportedFlagTypes[flagType] { log.Printf("Skipping generation of flag %q as type %v is not supported for this language", flagKey, flagTypeString) continue } docs := flagData["description"].(string) - defaultValue := getDefaultValue(flagData["default_value"], flagType) + defaultValue := getDefaultValue(flagData["defaultValue"], flagType) btData.Flags = append(btData.Flags, &generator.FlagTmplData{ Name: flagKey, Type: flagType, From c766e83c58481ad0f5482b2c6662fdd97194d3da Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Wed, 9 Oct 2024 14:32:03 +0200 Subject: [PATCH 11/17] refactor: change folder, package structure; integrate with cobra Signed-off-by: Florin-Mihai Anghel --- cmd/generate/generate.go | 29 ++++ cmd/generate/golang/golang.go | 33 +++++ cmd/root.go | 30 +++++ go.mod | 24 ++++ go.sum | 56 ++++++++ internal/flagkeys/flagkeys.go | 14 ++ internal/generate/generate.go | 61 +++++++++ .../generate/manifestutils/manifestutils.go | 105 +++++++++++++++ .../generate/plugins}/golang/golang.go | 67 +++++----- .../generate/plugins}/golang/golang.tmpl | 7 +- internal/generate/types/types.go | 46 +++++++ main.go | 7 + src/example_go/experimentflags.go | 21 --- src/generators/generator.go | 81 ------------ src/main.go | 124 ------------------ src/providers/providers.go | 10 -- 16 files changed, 443 insertions(+), 272 deletions(-) create mode 100644 cmd/generate/generate.go create mode 100644 cmd/generate/golang/golang.go create mode 100644 cmd/root.go create mode 100644 internal/flagkeys/flagkeys.go create mode 100644 internal/generate/generate.go create mode 100644 internal/generate/manifestutils/manifestutils.go rename {src/generators => internal/generate/plugins}/golang/golang.go (61%) rename {src/generators => internal/generate/plugins}/golang/golang.tmpl (64%) create mode 100644 internal/generate/types/types.go create mode 100644 main.go delete mode 100644 src/example_go/experimentflags.go delete mode 100644 src/generators/generator.go delete mode 100644 src/main.go delete mode 100644 src/providers/providers.go diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go new file mode 100644 index 0000000..5e8fdaf --- /dev/null +++ b/cmd/generate/generate.go @@ -0,0 +1,29 @@ +package generate + +import ( + "codegen/cmd/generate/golang" + "codegen/internal/flagkeys" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Root for `generate“ sub-commands, handling code generation for flag accessors. +var Root = &cobra.Command{ + Use: "generate", + Short: "Code generation for flag accessors for OpenFeature.", + Long: `Code generation for flag accessors for OpenFeature.`, +} + +func init() { + // Add subcommands. + Root.AddCommand(golang.Cmd) + + // Add flags. + Root.PersistentFlags().String(flagkeys.FlagManifestPath, "", "Path to the flag manifest.") + Root.MarkPersistentFlagRequired(flagkeys.FlagManifestPath) + viper.BindPFlag(flagkeys.FlagManifestPath, Root.PersistentFlags().Lookup(flagkeys.FlagManifestPath)) + Root.PersistentFlags().String(flagkeys.OutputPath, "", "Output path for the codegen") + viper.BindPFlag(flagkeys.OutputPath, Root.PersistentFlags().Lookup(flagkeys.OutputPath)) + Root.MarkPersistentFlagRequired(flagkeys.OutputPath) +} diff --git a/cmd/generate/golang/golang.go b/cmd/generate/golang/golang.go new file mode 100644 index 0000000..f2e3688 --- /dev/null +++ b/cmd/generate/golang/golang.go @@ -0,0 +1,33 @@ +package golang + +import ( + "codegen/internal/flagkeys" + "codegen/internal/generate" + "codegen/internal/generate/plugins/golang" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Cmd for `generate“ command, handling code generation for flag accessors +var Cmd = &cobra.Command{ + Use: "go", + Short: "Generate Golang flag accessors for OpenFeature.", + Long: `Generate Golang flag accessors for OpenFeature.`, + RunE: func(cmd *cobra.Command, args []string) error { + params := golang.Params{ + // Probably some conversion applied here, toLower and remove special characters. + GoPackage: viper.GetString(flagkeys.GoPackageName), + } + gen := golang.NewGenerator(params) + err := generate.CreateFlagAccessors(gen) + return err + }, +} + +func init() { + Cmd.Flags().String(flagkeys.GoPackageName, "", "Name of the Go package to be generated.") + Cmd.MarkFlagRequired(flagkeys.GoPackageName) + viper.BindPFlag(flagkeys.GoPackageName, Cmd.Flags().Lookup(flagkeys.GoPackageName)) + +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..2f841d4 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "fmt" + "os" + + "codegen/cmd/generate" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "openfeature", + Short: "CLI for OpenFeature.", + Long: `CLI for OpenFeature related functionalities.`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + rootCmd.AddCommand(generate.Root) +} diff --git a/go.mod b/go.mod index d3ee28a..de23b4e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,31 @@ require ( github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 ) +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.18.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + require ( github.com/go-logr/logr v1.4.2 // indirect + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect ) diff --git a/go.sum b/go.sum index f0bf6ca..39f15cf 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,70 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/open-feature/go-sdk v1.13.0 h1:D5NXPhhCL0SNR/DRvrTOm/xY7uE9m0zQQEttgKHlwtI= github.com/open-feature/go-sdk v1.13.0/go.mod h1:poPa+RFCJumHcb59wgp+tnSyNvMU2C07ykFJ0gczyaM= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/flagkeys/flagkeys.go b/internal/flagkeys/flagkeys.go new file mode 100644 index 0000000..61d8518 --- /dev/null +++ b/internal/flagkeys/flagkeys.go @@ -0,0 +1,14 @@ +// Package commonflags contains keys for all command-line flags related to openfeature CLI. +package flagkeys + +const ( + // `generate` flags: + // FlagManifestPath is the key for the flag that stores the flag manifest path. + FlagManifestPath = "flag_manifest_path" + // OutputPath is the key for the flag that stores the output path. + OutputPath = "output_path" + + // `generate go` flags: + // GoPackageName is the key for the flag that stores the Golang package name. + GoPackageName = "package_name" +) diff --git a/internal/generate/generate.go b/internal/generate/generate.go new file mode 100644 index 0000000..858f5d1 --- /dev/null +++ b/internal/generate/generate.go @@ -0,0 +1,61 @@ +// Package generate contains the top level functions used for generating flag accessors. +package generate + +import ( + "bytes" + "codegen/internal/flagkeys" + "codegen/internal/generate/manifestutils" + "codegen/internal/generate/types" + "fmt" + "os" + "path" + "path/filepath" + "text/template" + + "github.com/spf13/viper" +) + +// GenerateFile receives data for the Go template engine and outputs the contents to the file. +// Intended to be invoked by each language generator with appropiate data. +func GenerateFile(funcs template.FuncMap, contents string, data types.TmplDataInterface) error { + contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents) + if err != nil { + return fmt.Errorf("error initializing template: %v", err) + } + + var buf bytes.Buffer + if err := contentsTmpl.Execute(&buf, data); err != nil { + return fmt.Errorf("error executing template: %v", err) + } + outputPath := data.BaseTmplDataInfo().OutputPath + if err := os.MkdirAll(filepath.Dir(outputPath), 0770); err != nil { + return err + } + f, err := os.Create(path.Join(outputPath)) + if err != nil { + return fmt.Errorf("error creating file %q: %v", outputPath, err) + } + defer f.Close() + writtenBytes, err := f.Write(buf.Bytes()) + if err != nil { + return fmt.Errorf("error writing contents to file %q: %v", outputPath, err) + } + if writtenBytes != buf.Len() { + return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", outputPath) + } + + return nil +} + +// Takes as input a generator and outputs file with the appropiate flag accessors. +// The flag data is taken from the provided flag manifest. +func CreateFlagAccessors(gen types.Generator) error { + bt, err := manifestutils.LoadData(viper.GetString(flagkeys.FlagManifestPath), gen.SupportedFlagTypes()) + if err != nil { + return fmt.Errorf("error loading flag manifest: %v", err) + } + input := types.Input{ + BaseData: bt, + } + return gen.Generate(input) +} diff --git a/internal/generate/manifestutils/manifestutils.go b/internal/generate/manifestutils/manifestutils.go new file mode 100644 index 0000000..ac19db0 --- /dev/null +++ b/internal/generate/manifestutils/manifestutils.go @@ -0,0 +1,105 @@ +// Package manifestutils contains useful functions for loading the flag manifest. +package manifestutils + +import ( + flagmanifest "codegen/docs/schema/v0" + "codegen/internal/flagkeys" + "codegen/internal/generate/types" + "encoding/json" + "fmt" + "os" + "strconv" + + "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/spf13/viper" +) + +// LoadData loads the data from the flag manifest. +func LoadData(manifestPath string, supportedFlagTypes map[types.FlagType]bool) (*types.BaseTmplData, error) { + data, err := os.ReadFile(manifestPath) + if err != nil { + return nil, fmt.Errorf("error reading contents from file %q", manifestPath) + } + unfilteredData, err := unmarshalFlagManifest(data) + if err != nil { + return nil, err + } + + filteredData := filterUnsupportedFlags(unfilteredData, supportedFlagTypes) + + return filteredData, nil +} + +func filterUnsupportedFlags(unfilteredData *types.BaseTmplData, supportedFlagTypes map[types.FlagType]bool) *types.BaseTmplData { + filteredData := &types.BaseTmplData{ + OutputPath: unfilteredData.OutputPath, + } + for _, flagData := range unfilteredData.Flags { + if supportedFlagTypes[flagData.Type] { + filteredData.Flags = append(filteredData.Flags, flagData) + } + } + return filteredData +} + +var stringToFlagType = map[string]types.FlagType{ + "string": types.StringType, + "boolean": types.BoolType, + "float": types.FloatType, + "integer": types.IntType, + "object": types.ObjectType, +} + +func getDefaultValue(defaultValue interface{}, flagType types.FlagType) string { + switch flagType { + case types.BoolType: + return strconv.FormatBool(defaultValue.(bool)) + case types.IntType: + //the conversion to float64 instead of integer typically occurs + //due to how JSON is parsed in Go. In Go's encoding/json package, + //all JSON numbers are unmarshaled into float64 by default when decoding into an interface{}. + return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64) + case types.FloatType: + return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64) + case types.StringType: + return defaultValue.(string) + default: + return "" + } +} + +func unmarshalFlagManifest(data []byte) (*types.BaseTmplData, error) { + dynamic := make(map[string]interface{}) + err := json.Unmarshal(data, &dynamic) + if err != nil { + return nil, fmt.Errorf("error unmarshalling JSON: %v", err) + } + + sch, err := jsonschema.CompileString(flagmanifest.SchemaPath, flagmanifest.Schema) + if err != nil { + return nil, fmt.Errorf("error compiling JSON schema: %v", err) + } + if err = sch.Validate(dynamic); err != nil { + return nil, fmt.Errorf("error validating JSON schema: %v", err) + } + // All casts can be done directly since the JSON is already validated by the schema. + iFlags := dynamic["flags"] + flags := iFlags.(map[string]interface{}) + btData := types.BaseTmplData{ + OutputPath: viper.GetString(flagkeys.OutputPath), + } + for flagKey, iFlagData := range flags { + flagData := iFlagData.(map[string]interface{}) + flagTypeString := flagData["flagType"].(string) + flagType := stringToFlagType[flagTypeString] + docs := flagData["description"].(string) + defaultValue := getDefaultValue(flagData["defaultValue"], flagType) + btData.Flags = append(btData.Flags, &types.FlagTmplData{ + Name: flagKey, + Type: flagType, + DefaultValue: defaultValue, + Docs: docs, + }) + } + return &btData, nil +} diff --git a/src/generators/golang/golang.go b/internal/generate/plugins/golang/golang.go similarity index 61% rename from src/generators/golang/golang.go rename to internal/generate/plugins/golang/golang.go index 71fa9c8..c373ef5 100644 --- a/src/generators/golang/golang.go +++ b/internal/generate/plugins/golang/golang.go @@ -6,37 +6,37 @@ import ( "strconv" "text/template" - generator "codegen/src/generators" + "codegen/internal/generate" + "codegen/internal/generate/types" "github.com/iancoleman/strcase" ) // TmplData contains the Golang-specific data and the base data for the codegen. type TmplData struct { - *generator.BaseTmplData + *types.BaseTmplData GoPackage string } type genImpl struct { - file string goPackage string } // BaseTmplDataInfo provides the base template data for the codegen. -func (td *TmplData) BaseTmplDataInfo() *generator.BaseTmplData { +func (td *TmplData) BaseTmplDataInfo() *types.BaseTmplData { return td.BaseTmplData } // supportedFlagTypes is the flag types supported by the Go template. -var supportedFlagTypes = map[generator.FlagType]bool{ - generator.FloatType: true, - generator.StringType: true, - generator.IntType: true, - generator.BoolType: true, - generator.ObjectType: false, +var supportedFlagTypes = map[types.FlagType]bool{ + types.FloatType: true, + types.StringType: true, + types.IntType: true, + types.BoolType: true, + types.ObjectType: false, } -func (*genImpl) SupportedFlagTypes() map[generator.FlagType]bool { +func (*genImpl) SupportedFlagTypes() map[types.FlagType]bool { return supportedFlagTypes } @@ -54,65 +54,64 @@ func flagInitParam(flagName string) string { } // flagVarType returns the Go type for a flag's proto definition. -func providerType(t generator.FlagType) string { +func providerType(t types.FlagType) string { switch t { - case generator.IntType: + case types.IntType: return "IntProvider" - case generator.FloatType: + case types.FloatType: return "FloatProvider" - case generator.BoolType: + case types.BoolType: return "BooleanProvider" - case generator.StringType: + case types.StringType: return "StringProvider" default: return "" } } -func flagAccessFunc(t generator.FlagType) string { +func flagAccessFunc(t types.FlagType) string { switch t { - case generator.IntType: + case types.IntType: return "IntValue" - case generator.FloatType: + case types.FloatType: return "FloatValue" - case generator.BoolType: + case types.BoolType: return "BooleanValue" - case generator.StringType: + case types.StringType: return "StringValue" default: return "" } } -func supportImports(flags []*generator.FlagTmplData) []string { +func supportImports(flags []*types.FlagTmplData) []string { var res []string if len(flags) > 0 { res = append(res, "\"context\"") res = append(res, "\"github.com/open-feature/go-sdk/openfeature\"") - res = append(res, "\"codegen/src/providers\"") } sort.Strings(res) return res } -func defaultValueLiteral(flag *generator.FlagTmplData) string { +func defaultValueLiteral(flag *types.FlagTmplData) string { switch flag.Type { - case generator.StringType: + case types.StringType: return strconv.Quote(flag.DefaultValue) default: return flag.DefaultValue } } -func typeString(flagType generator.FlagType) string { +func typeString(flagType types.FlagType) string { switch flagType { - case generator.StringType: + case types.StringType: return "string" - case generator.IntType: + case types.IntType: return "int64" - case generator.BoolType: + case types.BoolType: return "bool" - case generator.FloatType: + case types.FloatType: return "float64" default: return "" @@ -122,7 +121,7 @@ func typeString(flagType generator.FlagType) string { // Go Funcs END // Generate generates the Go flag accessors for OpenFeature. -func (g *genImpl) Generate(input generator.Input) error { +func (g *genImpl) Generate(input types.Input) error { funcs := template.FuncMap{ "FlagVarName": flagVarName, "FlagInitParam": flagInitParam, @@ -136,19 +135,17 @@ func (g *genImpl) Generate(input generator.Input) error { BaseTmplData: input.BaseData, GoPackage: g.goPackage, } - return generator.GenerateFile(funcs, g.file, golangTmpl, &td) + return generate.GenerateFile(funcs, golangTmpl, &td) } // Params are parameters for creating a Generator type Params struct { - File string GoPackage string } // NewGenerator creates a generator for Go. -func NewGenerator(params Params) generator.Generator { +func NewGenerator(params Params) types.Generator { return &genImpl{ - file: params.File, goPackage: params.GoPackage, } } diff --git a/src/generators/golang/golang.tmpl b/internal/generate/plugins/golang/golang.tmpl similarity index 64% rename from src/generators/golang/golang.tmpl rename to internal/generate/plugins/golang/golang.tmpl index d14bf4f..014d080 100644 --- a/src/generators/golang/golang.tmpl +++ b/internal/generate/plugins/golang/golang.tmpl @@ -6,13 +6,18 @@ import ( {{- end}} ) +type BooleanProvider func(ctx context.Context) (bool, error) +type FloatProvider func(ctx context.Context) (float64, error) +type IntProvider func(ctx context.Context) (int64, error) +type StringProvider func(ctx context.Context) (string, error) + var client *openfeature.Client = nil {{- range .Flags}} // {{.Docs}} var {{FlagVarName .Name}} = struct { - Value providers.{{ProviderType .Type}} + Value {{ProviderType .Type}} }{ Value: func(ctx context.Context) ({{TypeString .Type}}, error) { return client.{{FlagAccessFunc .Type}}(ctx, {{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, openfeature.EvaluationContext{}) diff --git a/internal/generate/types/types.go b/internal/generate/types/types.go new file mode 100644 index 0000000..861d2e7 --- /dev/null +++ b/internal/generate/types/types.go @@ -0,0 +1,46 @@ +// Package types contains all the common types and interfaces for generating flag accessors. +package types + +// FlagType are the primitive types of flags. +type FlagType int + +// Collection of the different kinds of flag types +const ( + UnknownFlagType FlagType = iota + IntType + FloatType + BoolType + StringType + ObjectType +) + +// FlagTmplData is the per-flag specific data. +// It represents a common interface between Mendel source and codegen file output. +type FlagTmplData struct { + Name string + Type FlagType + DefaultValue string + Docs string +} + +// BaseTmplData is the base for all OpenFeature code generation. +type BaseTmplData struct { + OutputPath string + Flags []*FlagTmplData +} + +type TmplDataInterface interface { + // BaseTmplDataInfo returns a pointer to a BaseTmplData struct containing + // all the relevant information needed for metadata construction. + BaseTmplDataInfo() *BaseTmplData +} + +type Input struct { + BaseData *BaseTmplData +} + +// Generator provides interface to generate language specific, strongly-typed flag accessors. +type Generator interface { + Generate(input Input) error + SupportedFlagTypes() map[FlagType]bool +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..8ff55f1 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "codegen/cmd" + +func main() { + cmd.Execute() +} diff --git a/src/example_go/experimentflags.go b/src/example_go/experimentflags.go deleted file mode 100644 index 5868666..0000000 --- a/src/example_go/experimentflags.go +++ /dev/null @@ -1,21 +0,0 @@ -package experimentflags - -import ( - "codegen/src/providers" - "context" - "github.com/open-feature/go-sdk/openfeature" -) - -var client *openfeature.Client = nil -// This is a flag. -var MyOpenFeatureFlag = struct { - Value providers.BooleanProvider -}{ - Value: func(ctx context.Context) (bool, error) { - return client.BooleanValue(ctx, "myOpenFeatureFlag", false, openfeature.EvaluationContext{}) - }, -} - -func init() { - client = openfeature.NewClient("experimentflags") -} diff --git a/src/generators/generator.go b/src/generators/generator.go deleted file mode 100644 index b6c2e8b..0000000 --- a/src/generators/generator.go +++ /dev/null @@ -1,81 +0,0 @@ -package generator - -import ( - "bytes" - "fmt" - "os" - "path" - "text/template" -) - -// FlagType are the primitive types of flags. -type FlagType int - -// Collection of the different kinds of flag types -const ( - UnknownFlagType FlagType = iota - IntType - FloatType - BoolType - StringType - ObjectType -) - -// FlagTmplData is the per-flag specific data. -// It represents a common interface between Mendel source and codegen file output. -type FlagTmplData struct { - Name string - Type FlagType - DefaultValue string - Docs string -} - -// BaseTmplData is the base for all OpenFeature code generation. -type BaseTmplData struct { - OutputDir string - Flags []*FlagTmplData -} - -type TmplDataInterface interface { - // BaseTmplDataInfo returns a pointer to a BaseTmplData struct containing - // all the relevant information needed for metadata construction. - BaseTmplDataInfo() *BaseTmplData -} - -type Input struct { - BaseData *BaseTmplData -} - -// Generator provides interface to generate language specific, strongly-typed flag accessors. -type Generator interface { - Generate(input Input) error - SupportedFlagTypes() map[FlagType]bool -} - -// GenerateFile receives data for the Go template engine and outputs the contents to the file. -func GenerateFile(funcs template.FuncMap, filename string, contents string, data TmplDataInterface) error { - contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents) - if err != nil { - return fmt.Errorf("error initializing template: %v", err) - } - - var buf bytes.Buffer - if err := contentsTmpl.Execute(&buf, data); err != nil { - return fmt.Errorf("error executing template: %v", err) - } - - f, err := os.Create(path.Join(data.BaseTmplDataInfo().OutputDir, filename)) - if err != nil { - return fmt.Errorf("error creating file %q: %v", filename, err) - } - defer f.Close() - writtenBytes, err := f.Write(buf.Bytes()) - if err != nil { - return fmt.Errorf("error writing contents to file %q: %v", filename, err) - } - if writtenBytes != buf.Len() { - return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", filename) - } - - return nil -} diff --git a/src/main.go b/src/main.go deleted file mode 100644 index e186a5d..0000000 --- a/src/main.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - _ "embed" - "encoding/json" - "flag" - "fmt" - "log" - "os" - "path" - "strconv" - - flagmanifest "codegen/docs/schema/v0" - generator "codegen/src/generators" - "codegen/src/generators/golang" - - jsonschema "github.com/santhosh-tekuri/jsonschema/v5" -) - -var flagManifestPath = flag.String("flag_manifest_path", "", "Path to the flag manifest.") -var moduleName = flag.String("module_name", "", "Name of the module to be generated.") -var outputPath = flag.String("output_path", "", "Output path for the codegen") - -var stringToFlagType = map[string]generator.FlagType{ - "string": generator.StringType, - "boolean": generator.BoolType, - "float": generator.FloatType, - "integer": generator.IntType, - "object": generator.ObjectType, -} - -func getDefaultValue(defaultValue interface{}, flagType generator.FlagType) string { - switch flagType { - case generator.BoolType: - return strconv.FormatBool(defaultValue.(bool)) - case generator.IntType: - //the conversion to float64 instead of integer typically occurs - //due to how JSON is parsed in Go. In Go's encoding/json package, - //all JSON numbers are unmarshaled into float64 by default when decoding into an interface{}. - return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64) - case generator.FloatType: - return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64) - case generator.StringType: - return defaultValue.(string) - default: - return "" - } -} - -func unmarshalFlagManifest(data []byte, supportedFlagTypes map[generator.FlagType]bool) (*generator.BaseTmplData, error) { - dynamic := make(map[string]interface{}) - err := json.Unmarshal(data, &dynamic) - if err != nil { - return nil, fmt.Errorf("error unmarshalling JSON: %v", err) - } - - sch, err := jsonschema.CompileString(flagmanifest.SchemaPath, flagmanifest.Schema) - if err != nil { - return nil, fmt.Errorf("error compiling JSON schema: %v", err) - } - if err = sch.Validate(dynamic); err != nil { - return nil, fmt.Errorf("error validating JSON schema: %v", err) - } - // All casts can be done directly since the JSON is already validated by the schema. - iFlags := dynamic["flags"] - flags := iFlags.(map[string]interface{}) - btData := generator.BaseTmplData{ - OutputDir: path.Dir(*outputPath), - } - for flagKey, iFlagData := range flags { - flagData := iFlagData.(map[string]interface{}) - flagTypeString := flagData["flagType"].(string) - flagType := stringToFlagType[flagTypeString] - if !supportedFlagTypes[flagType] { - log.Printf("Skipping generation of flag %q as type %v is not supported for this language", flagKey, flagTypeString) - continue - } - docs := flagData["description"].(string) - defaultValue := getDefaultValue(flagData["defaultValue"], flagType) - btData.Flags = append(btData.Flags, &generator.FlagTmplData{ - Name: flagKey, - Type: flagType, - DefaultValue: defaultValue, - Docs: docs, - }) - } - return &btData, nil -} - -func loadData(manifestPath string, supportedFlagTypes map[generator.FlagType]bool) (*generator.BaseTmplData, error) { - data, err := os.ReadFile(manifestPath) - if err != nil { - return nil, fmt.Errorf("error reading contents from file %q", manifestPath) - } - return unmarshalFlagManifest(data, supportedFlagTypes) -} - -func generate(gen generator.Generator) error { - btData, err := loadData(*flagManifestPath, gen.SupportedFlagTypes()) - if err != nil { - return err - } - input := generator.Input{ - BaseData: btData, - } - return gen.Generate(input) -} - -// command line params working example -// -flag_manifest_path "sample/sample_manifest.json" -output_path "sample/golang/golang_sample.go" -func main() { - flag.Parse() - _, filename := path.Split(*outputPath) - params := golang.Params{ - File: filename, - // Probably some conversion applied here, toLower and remove special characters. - GoPackage: *moduleName, - } - gen := golang.NewGenerator(params) - err := generate(gen) - if err != nil { - fmt.Printf("error generating flag accesssors: %v\n", err) - } -} diff --git a/src/providers/providers.go b/src/providers/providers.go deleted file mode 100644 index fc7ac3f..0000000 --- a/src/providers/providers.go +++ /dev/null @@ -1,10 +0,0 @@ -package providers - -import ( - "context" -) - -type BooleanProvider func(ctx context.Context) (bool, error) -type FloatProvider func(ctx context.Context) (float64, error) -type IntProvider func(ctx context.Context) (int64, error) -type StringProvider func(ctx context.Context) (string, error) From 087b76b6a87dd2b7611d3839ad1cb6cb56be5aa1 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Thu, 10 Oct 2024 09:52:41 +0200 Subject: [PATCH 12/17] chore: delete comment; this has been replaced with Git project issue Signed-off-by: Florin-Mihai Anghel --- cmd/generate/golang/golang.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/generate/golang/golang.go b/cmd/generate/golang/golang.go index f2e3688..2fdd9b4 100644 --- a/cmd/generate/golang/golang.go +++ b/cmd/generate/golang/golang.go @@ -16,7 +16,6 @@ var Cmd = &cobra.Command{ Long: `Generate Golang flag accessors for OpenFeature.`, RunE: func(cmd *cobra.Command, args []string) error { params := golang.Params{ - // Probably some conversion applied here, toLower and remove special characters. GoPackage: viper.GetString(flagkeys.GoPackageName), } gen := golang.NewGenerator(params) From 36124c880cb6a932f1eaef890bba0e57c53560b6 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel <44744433+anghelflorinm@users.noreply.github.com> Date: Thu, 10 Oct 2024 09:55:17 +0200 Subject: [PATCH 13/17] chore: update internal/generate/plugins/golang/golang.tmpl Co-authored-by: Michael Beemer Signed-off-by: Florin-Mihai Anghel <44744433+anghelflorinm@users.noreply.github.com> --- internal/generate/plugins/golang/golang.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/generate/plugins/golang/golang.tmpl b/internal/generate/plugins/golang/golang.tmpl index 014d080..d513f2b 100644 --- a/internal/generate/plugins/golang/golang.tmpl +++ b/internal/generate/plugins/golang/golang.tmpl @@ -17,7 +17,7 @@ var client *openfeature.Client = nil {{- range .Flags}} // {{.Docs}} var {{FlagVarName .Name}} = struct { - Value {{ProviderType .Type}} + Value {{ProviderType .Type}} }{ Value: func(ctx context.Context) ({{TypeString .Type}}, error) { return client.{{FlagAccessFunc .Type}}(ctx, {{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, openfeature.EvaluationContext{}) From 97cdaff7bf4ff295c717b14761f0e40ace944ab8 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Tue, 29 Oct 2024 09:48:41 +0100 Subject: [PATCH 14/17] test: add small tests for generate command for go and react with in memory files Signed-off-by: Florin-Mihai Anghel --- cmd/generate/generate_test.go | 102 ++++++++++++++++++ cmd/generate/testdata/success_go.golden | 49 +++++++++ cmd/generate/testdata/success_manifest.golden | 32 ++++++ cmd/generate/testdata/success_react.golden | 55 ++++++++++ go.mod | 1 + go.sum | 2 + internal/filesystem/filesystem.go | 17 +++ internal/flagkeys/flagkeys.go | 10 ++ internal/generate/generate.go | 7 +- .../generate/manifestutils/manifestutils.go | 11 +- internal/testutils/testutils.go | 1 + 11 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 cmd/generate/generate_test.go create mode 100644 cmd/generate/testdata/success_go.golden create mode 100644 cmd/generate/testdata/success_manifest.golden create mode 100644 cmd/generate/testdata/success_react.golden create mode 100644 internal/filesystem/filesystem.go create mode 100644 internal/testutils/testutils.go diff --git a/cmd/generate/generate_test.go b/cmd/generate/generate_test.go new file mode 100644 index 0000000..e8b186a --- /dev/null +++ b/cmd/generate/generate_test.go @@ -0,0 +1,102 @@ +package generate + +import ( + "codegen/internal/flagkeys" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +func TestGenerateGoSucces(t *testing.T) { + // Constant paths. + const memoryManifestPath = "manifest/path.json" + const memoryOutputPath = "output/path.go" + const packageName = "testpackage" + const testFileManifest = "testdata/success_manifest.golden" + const testFileGo = "testdata/success_go.golden" + + // Prepare in-memory files. + fs := afero.NewMemMapFs() + viper.Set(flagkeys.FileSystem, fs) + readOsFileAndWriteToMemMap(t, testFileManifest, memoryManifestPath, fs) + + // Prepare command. + Root.SetArgs([]string{"go", + "--flag_manifest_path", memoryManifestPath, + "--output_path", memoryOutputPath, + "--package_name", packageName, + }) + + // Run command. + Root.Execute() + + // Compare result. + compareOutput(t, testFileGo, memoryOutputPath, fs) +} + +func TestGenerateReactSucces(t *testing.T) { + // Constant paths. + const memoryManifestPath = "manifest/path.json" + const memoryOutputPath = "output/path.ts" + const testFileManifest = "testdata/success_manifest.golden" + const testFileReact = "testdata/success_react.golden" + + // Prepare in-memory files. + fs := afero.NewMemMapFs() + viper.Set(flagkeys.FileSystem, fs) + readOsFileAndWriteToMemMap(t, testFileManifest, memoryManifestPath, fs) + + // Prepare command. + Root.SetArgs([]string{"react", + "--flag_manifest_path", memoryManifestPath, + "--output_path", memoryOutputPath, + }) + + // Run command. + Root.Execute() + + // Compare result. + compareOutput(t, testFileReact, memoryOutputPath, fs) +} + +func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) { + data, err := os.ReadFile(inputPath) + if err != nil { + t.Fatalf("error reading file %q: %v", inputPath, err) + } + if err := memFs.MkdirAll(filepath.Dir(memPath), 0770); err != nil { + t.Fatalf("error creating directory %q: %v", filepath.Dir(memPath), err) + } + f, err := memFs.Create(memPath) + if err != nil { + t.Fatalf("error creating file %q: %v", memPath, err) + } + defer f.Close() + writtenBytes, err := f.Write(data) + if err != nil { + t.Fatalf("error writing contents to file %q: %v", memPath, err) + } + if writtenBytes != len(data) { + t.Fatalf("error writing entire file %v: writtenBytes != expectedWrittenBytes", memPath) + } +} + +func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs) { + want, err := os.ReadFile(testFile) + if err != nil { + t.Fatalf("error reading file %q: %v", testFile, err) + + } + got, err := afero.ReadFile(fs, memoryOutputPath) + if err != nil { + t.Fatalf("error reading file %q: %v", memoryOutputPath, err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("output mismatch (-want +got):\n%s", diff) + } +} diff --git a/cmd/generate/testdata/success_go.golden b/cmd/generate/testdata/success_go.golden new file mode 100644 index 0000000..ccf281e --- /dev/null +++ b/cmd/generate/testdata/success_go.golden @@ -0,0 +1,49 @@ +package testpackage + +import ( + "context" + "github.com/open-feature/go-sdk/openfeature" +) + +type BooleanProvider func(ctx context.Context) (bool, error) +type FloatProvider func(ctx context.Context) (float64, error) +type IntProvider func(ctx context.Context) (int64, error) +type StringProvider func(ctx context.Context) (string, error) + +var client *openfeature.Client = nil +// Discount percentage applied to purchases. +var DiscountPercentage = struct { + Value FloatProvider +}{ + Value: func(ctx context.Context) (float64, error) { + return client.FloatValue(ctx, "discountPercentage", 0.15, openfeature.EvaluationContext{}) + }, +} +// Controls whether Feature A is enabled. +var EnableFeatureA = struct { + Value BooleanProvider +}{ + Value: func(ctx context.Context) (bool, error) { + return client.BooleanValue(ctx, "enableFeatureA", false, openfeature.EvaluationContext{}) + }, +} +// The message to use for greeting users. +var GreetingMessage = struct { + Value StringProvider +}{ + Value: func(ctx context.Context) (string, error) { + return client.StringValue(ctx, "greetingMessage", "Hello there!", openfeature.EvaluationContext{}) + }, +} +// Maximum allowed length for usernames. +var UsernameMaxLength = struct { + Value IntProvider +}{ + Value: func(ctx context.Context) (int64, error) { + return client.IntValue(ctx, "usernameMaxLength", 50, openfeature.EvaluationContext{}) + }, +} + +func init() { + client = openfeature.NewClient("testpackage") +} diff --git a/cmd/generate/testdata/success_manifest.golden b/cmd/generate/testdata/success_manifest.golden new file mode 100644 index 0000000..2df98b6 --- /dev/null +++ b/cmd/generate/testdata/success_manifest.golden @@ -0,0 +1,32 @@ +{ + "flags": { + "enableFeatureA": { + "flagType": "boolean", + "defaultValue": false, + "description": "Controls whether Feature A is enabled." + }, + "usernameMaxLength": { + "flagType": "integer", + "defaultValue": 50, + "description": "Maximum allowed length for usernames." + }, + "greetingMessage": { + "flagType": "string", + "defaultValue": "Hello there!", + "description": "The message to use for greeting users." + }, + "discountPercentage": { + "flagType": "float", + "defaultValue": 0.15, + "description": "Discount percentage applied to purchases." + }, + "themeCustomization": { + "flagType": "object", + "defaultValue": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d" + }, + "description": "Allows customization of theme colors." + } + } + } \ No newline at end of file diff --git a/cmd/generate/testdata/success_react.golden b/cmd/generate/testdata/success_react.golden new file mode 100644 index 0000000..9a68ec5 --- /dev/null +++ b/cmd/generate/testdata/success_react.golden @@ -0,0 +1,55 @@ +'use client'; + +import { + useBooleanFlagDetails, + useNumberFlagDetails, + useStringFlagDetails, +} from "@openfeature/react-sdk"; + +/** +* Discount percentage applied to purchases. +* +* **Details:** +* - flag key: `discountPercentage` +* - default value: `0.15` +* - type: `number` +*/ +export const useDiscountPercentage = (options: Parameters[2]) => { + return useNumberFlagDetails("discountPercentage", 0.15, options); +}; + +/** +* Controls whether Feature A is enabled. +* +* **Details:** +* - flag key: `enableFeatureA` +* - default value: `false` +* - type: `boolean` +*/ +export const useEnableFeatureA = (options: Parameters[2]) => { + return useBooleanFlagDetails("enableFeatureA", false, options); +}; + +/** +* The message to use for greeting users. +* +* **Details:** +* - flag key: `greetingMessage` +* - default value: `Hello there!` +* - type: `string` +*/ +export const useGreetingMessage = (options: Parameters[2]) => { + return useStringFlagDetails("greetingMessage", "Hello there!", options); +}; + +/** +* Maximum allowed length for usernames. +* +* **Details:** +* - flag key: `usernameMaxLength` +* - default value: `50` +* - type: `number` +*/ +export const useUsernameMaxLength = (options: Parameters[2]) => { + return useNumberFlagDetails("usernameMaxLength", 50, options); +}; diff --git a/go.mod b/go.mod index de23b4e..97d9d17 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( require ( github.com/go-logr/logr v1.4.2 // indirect + github.com/google/go-cmp v0.6.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect diff --git a/go.sum b/go.sum index 39f15cf..acfce07 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= diff --git a/internal/filesystem/filesystem.go b/internal/filesystem/filesystem.go new file mode 100644 index 0000000..b2e8e6a --- /dev/null +++ b/internal/filesystem/filesystem.go @@ -0,0 +1,17 @@ +// Package filesystem contains the filesystem interface. +package filesystem + +import ( + "codegen/internal/flagkeys" + + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +func FileSystem() afero.Fs { + return viper.Get(flagkeys.FileSystem).(afero.Fs) +} + +func init() { + viper.SetDefault(flagkeys.FileSystem, afero.NewOsFs()) +} diff --git a/internal/flagkeys/flagkeys.go b/internal/flagkeys/flagkeys.go index 61d8518..90f1976 100644 --- a/internal/flagkeys/flagkeys.go +++ b/internal/flagkeys/flagkeys.go @@ -1,6 +1,8 @@ // Package commonflags contains keys for all command-line flags related to openfeature CLI. package flagkeys +import "github.com/spf13/viper" + const ( // `generate` flags: // FlagManifestPath is the key for the flag that stores the flag manifest path. @@ -11,4 +13,12 @@ const ( // `generate go` flags: // GoPackageName is the key for the flag that stores the Golang package name. GoPackageName = "package_name" + + //internal keys: + // FileSystem is the key for the flag that stores the filesystem interface. + FileSystem = "filesystem" ) + +func init() { + viper.SetDefault(FileSystem, "local") +} diff --git a/internal/generate/generate.go b/internal/generate/generate.go index c7b3d97..c3eacc2 100644 --- a/internal/generate/generate.go +++ b/internal/generate/generate.go @@ -3,11 +3,11 @@ package generate import ( "bytes" + "codegen/internal/filesystem" "codegen/internal/flagkeys" "codegen/internal/generate/manifestutils" "codegen/internal/generate/types" "fmt" - "os" "path" "path/filepath" "text/template" @@ -28,10 +28,11 @@ func GenerateFile(funcs template.FuncMap, contents string, data types.TmplDataIn return fmt.Errorf("error executing template: %v", err) } outputPath := data.BaseTmplDataInfo().OutputPath - if err := os.MkdirAll(filepath.Dir(outputPath), 0770); err != nil { + fs := filesystem.FileSystem() + if err := fs.MkdirAll(filepath.Dir(outputPath), 0770); err != nil { return err } - f, err := os.Create(path.Join(outputPath)) + f, err := fs.Create(path.Join(outputPath)) if err != nil { return fmt.Errorf("error creating file %q: %v", outputPath, err) } diff --git a/internal/generate/manifestutils/manifestutils.go b/internal/generate/manifestutils/manifestutils.go index ac19db0..3ef464d 100644 --- a/internal/generate/manifestutils/manifestutils.go +++ b/internal/generate/manifestutils/manifestutils.go @@ -3,20 +3,23 @@ package manifestutils import ( flagmanifest "codegen/docs/schema/v0" + "codegen/internal/filesystem" "codegen/internal/flagkeys" "codegen/internal/generate/types" "encoding/json" "fmt" - "os" + "sort" "strconv" "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/spf13/afero" "github.com/spf13/viper" ) // LoadData loads the data from the flag manifest. func LoadData(manifestPath string, supportedFlagTypes map[types.FlagType]bool) (*types.BaseTmplData, error) { - data, err := os.ReadFile(manifestPath) + fs := filesystem.FileSystem() + data, err := afero.ReadFile(fs, manifestPath) if err != nil { return nil, fmt.Errorf("error reading contents from file %q", manifestPath) } @@ -101,5 +104,9 @@ func unmarshalFlagManifest(data []byte) (*types.BaseTmplData, error) { Docs: docs, }) } + // Ensure consistency of order of flag generation. + sort.Slice(btData.Flags, func(i, j int) bool { + return btData.Flags[i].Name < btData.Flags[j].Name + }) return &btData, nil } diff --git a/internal/testutils/testutils.go b/internal/testutils/testutils.go new file mode 100644 index 0000000..95c4dc9 --- /dev/null +++ b/internal/testutils/testutils.go @@ -0,0 +1 @@ +package testutils From 09f0ac5a7aff5dfe5ada8f22fe9293febe00cb80 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel <44744433+anghelflorinm@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:10:59 +0100 Subject: [PATCH 15/17] chore: update cmd/generate/generate_test.go Co-authored-by: Michael Beemer Signed-off-by: Florin-Mihai Anghel <44744433+anghelflorinm@users.noreply.github.com> --- cmd/generate/generate_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/generate/generate_test.go b/cmd/generate/generate_test.go index e8b186a..f41fbf0 100644 --- a/cmd/generate/generate_test.go +++ b/cmd/generate/generate_test.go @@ -12,7 +12,7 @@ import ( "github.com/spf13/viper" ) -func TestGenerateGoSucces(t *testing.T) { +func TestGenerateGoSuccess(t *testing.T) { // Constant paths. const memoryManifestPath = "manifest/path.json" const memoryOutputPath = "output/path.go" From 6509bdab68348bc23718adda3ad3ba829872fcea Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel <44744433+anghelflorinm@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:11:13 +0100 Subject: [PATCH 16/17] chore: update cmd/generate/generate_test.go Co-authored-by: Michael Beemer Signed-off-by: Florin-Mihai Anghel <44744433+anghelflorinm@users.noreply.github.com> --- cmd/generate/generate_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/generate/generate_test.go b/cmd/generate/generate_test.go index f41fbf0..8a7e76e 100644 --- a/cmd/generate/generate_test.go +++ b/cmd/generate/generate_test.go @@ -39,7 +39,7 @@ func TestGenerateGoSuccess(t *testing.T) { compareOutput(t, testFileGo, memoryOutputPath, fs) } -func TestGenerateReactSucces(t *testing.T) { +func TestGenerateReactSuccess(t *testing.T) { // Constant paths. const memoryManifestPath = "manifest/path.json" const memoryOutputPath = "output/path.ts" From efdcc4bcc798dd7568038f96726a24e219982356 Mon Sep 17 00:00:00 2001 From: Florin-Mihai Anghel Date: Thu, 31 Oct 2024 16:08:36 +0100 Subject: [PATCH 17/17] chore: update the permissions when making directory Signed-off-by: Florin-Mihai Anghel --- cmd/generate/generate_test.go | 2 +- internal/generate/generate.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/generate/generate_test.go b/cmd/generate/generate_test.go index 8a7e76e..6aec68b 100644 --- a/cmd/generate/generate_test.go +++ b/cmd/generate/generate_test.go @@ -69,7 +69,7 @@ func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, if err != nil { t.Fatalf("error reading file %q: %v", inputPath, err) } - if err := memFs.MkdirAll(filepath.Dir(memPath), 0770); err != nil { + if err := memFs.MkdirAll(filepath.Dir(memPath), 0660); err != nil { t.Fatalf("error creating directory %q: %v", filepath.Dir(memPath), err) } f, err := memFs.Create(memPath) diff --git a/internal/generate/generate.go b/internal/generate/generate.go index c3eacc2..e92d2c5 100644 --- a/internal/generate/generate.go +++ b/internal/generate/generate.go @@ -29,7 +29,7 @@ func GenerateFile(funcs template.FuncMap, contents string, data types.TmplDataIn } outputPath := data.BaseTmplDataInfo().OutputPath fs := filesystem.FileSystem() - if err := fs.MkdirAll(filepath.Dir(outputPath), 0770); err != nil { + if err := fs.MkdirAll(filepath.Dir(outputPath), 0660); err != nil { return err } f, err := fs.Create(path.Join(outputPath))