Skip to content

Commit e8f3d3e

Browse files
feat: initial CLI for codegen with support for golang strongly typed accessors (#13)
<!-- Please use this template for your pull request. --> <!-- Please use the sections that you need and delete other sections --> ## This PR adds initial CLI for codegen with support for golang strongly typed accessors ### Related Issues https://github.com/orgs/open-feature/projects/17?pane=issue&itemId=74282748 ### Follow-up Tasks split to support additional languages add unit tests --------- Signed-off-by: Florin-Mihai Anghel <[email protected]>
1 parent ed844b4 commit e8f3d3e

File tree

10 files changed

+469
-0
lines changed

10 files changed

+469
-0
lines changed

.gitignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Output of the go coverage tool, specifically when used with LiteIDE
15+
*.out
16+
17+
# Dependency directories (remove the comment below to include it)
18+
# vendor/
19+
20+
# Go workspace file
21+
go.work
22+
go.work.sum
23+
24+
# env file
25+
.env

src/example_go/experimentflags.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package experimentflags
2+
3+
import (
4+
"codegen/providers"
5+
"context"
6+
"github.com/open-feature/go-sdk/openfeature"
7+
)
8+
9+
var client *openfeature.Client = nil
10+
// This is a flag.
11+
var MyOpenFeatureFlag = struct {
12+
Value providers.BooleanProvider
13+
}{
14+
Value: func(ctx context.Context) (bool, error) {
15+
return client.BooleanValue(ctx, "myOpenFeatureFlag", false, openfeature.EvaluationContext{})
16+
},
17+
}
18+
19+
func init() {
20+
client = openfeature.NewClient("experimentflags")
21+
}

src/generators/generator.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package generator
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"text/template"
8+
)
9+
10+
// FlagType are the primitive types of flags.
11+
type FlagType int
12+
13+
// Collection of the different kinds of flag types
14+
const (
15+
UnknownFlagType FlagType = iota
16+
IntType
17+
FloatType
18+
BoolType
19+
StringType
20+
ObjectType
21+
)
22+
23+
// FlagTmplData is the per-flag specific data.
24+
// It represents a common interface between Mendel source and codegen file output.
25+
type FlagTmplData struct {
26+
Name string
27+
Type FlagType
28+
DefaultValue string
29+
Docs string
30+
}
31+
32+
// BaseTmplData is the base for all OpenFeature code generation.
33+
type BaseTmplData struct {
34+
OutputDir string
35+
Flags []*FlagTmplData
36+
}
37+
38+
type TmplDataInterface interface {
39+
// BaseTmplDataInfo returns a pointer to a BaseTmplData struct containing
40+
// all the relevant information needed for metadata construction.
41+
BaseTmplDataInfo() *BaseTmplData
42+
}
43+
44+
type Input struct {
45+
BaseData *BaseTmplData
46+
}
47+
48+
// Generator provides interface to generate language specific, strongly-typed flag accessors.
49+
type Generator interface {
50+
Generate(input Input) error
51+
SupportedFlagTypes() map[FlagType]bool
52+
}
53+
54+
// GenerateFile receives data for the Go template engine and outputs the contents to the file.
55+
func GenerateFile(funcs template.FuncMap, outputPath string, contents string, data TmplDataInterface) error {
56+
contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents)
57+
if err != nil {
58+
return fmt.Errorf("error initializing template: %v", err)
59+
}
60+
61+
var buf bytes.Buffer
62+
if err := contentsTmpl.Execute(&buf, data); err != nil {
63+
return fmt.Errorf("error executing template: %v", err)
64+
}
65+
66+
f, err := os.Create(outputPath)
67+
if err != nil {
68+
return fmt.Errorf("error creating file %q: %v", outputPath, err)
69+
}
70+
defer f.Close()
71+
writtenBytes, err := f.Write(buf.Bytes())
72+
if err != nil {
73+
return fmt.Errorf("error writing contents to file %q: %v", outputPath, err)
74+
}
75+
if writtenBytes != buf.Len() {
76+
return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", outputPath)
77+
}
78+
79+
return nil
80+
}

src/generators/golang/golang.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package golang
2+
3+
import (
4+
generator "codegen/generators"
5+
_ "embed"
6+
"sort"
7+
"strconv"
8+
"text/template"
9+
10+
"github.com/iancoleman/strcase"
11+
)
12+
13+
// TmplData contains the Golang-specific data and the base data for the codegen.
14+
type TmplData struct {
15+
*generator.BaseTmplData
16+
GoPackage string
17+
}
18+
19+
type genImpl struct {
20+
file string
21+
goPackage string
22+
}
23+
24+
// BaseTmplDataInfo provides the base template data for the codegen.
25+
func (td *TmplData) BaseTmplDataInfo() *generator.BaseTmplData {
26+
return td.BaseTmplData
27+
}
28+
29+
// supportedFlagTypes is the flag types supported by the Go template.
30+
var supportedFlagTypes = map[generator.FlagType]bool{
31+
generator.FloatType: true,
32+
generator.StringType: true,
33+
generator.IntType: true,
34+
generator.BoolType: true,
35+
generator.ObjectType: false,
36+
}
37+
38+
func (*genImpl) SupportedFlagTypes() map[generator.FlagType]bool {
39+
return supportedFlagTypes
40+
}
41+
42+
//go:embed golang.tmpl
43+
var golangTmpl string
44+
45+
// Go Funcs BEGIN
46+
47+
func flagVarName(flagName string) string {
48+
return strcase.ToCamel(flagName)
49+
}
50+
51+
func flagInitParam(flagName string) string {
52+
return strconv.Quote(flagName)
53+
}
54+
55+
// flagVarType returns the Go type for a flag's proto definition.
56+
func providerType(t generator.FlagType) string {
57+
switch t {
58+
case generator.IntType:
59+
return "IntProvider"
60+
case generator.FloatType:
61+
return "FloatProvider"
62+
case generator.BoolType:
63+
return "BooleanProvider"
64+
case generator.StringType:
65+
return "StringProvider"
66+
}
67+
return ""
68+
}
69+
70+
func flagAccessFunc(t generator.FlagType) string {
71+
switch t {
72+
case generator.IntType:
73+
return "IntValue"
74+
case generator.FloatType:
75+
return "FloatValue"
76+
case generator.BoolType:
77+
return "BooleanValue"
78+
case generator.StringType:
79+
return "StringValue"
80+
}
81+
return ""
82+
}
83+
84+
func supportImports(flags []*generator.FlagTmplData) []string {
85+
var res []string
86+
if len(flags) > 0 {
87+
res = append(res, "\"context\"")
88+
res = append(res, "\"github.com/open-feature/go-sdk/openfeature\"")
89+
res = append(res, "\"codegen/providers\"")
90+
}
91+
sort.Strings(res)
92+
return res
93+
}
94+
95+
func defaultValueLiteral(flag *generator.FlagTmplData) string {
96+
switch flag.Type {
97+
case generator.StringType:
98+
return strconv.Quote(flag.DefaultValue)
99+
default:
100+
return flag.DefaultValue
101+
}
102+
}
103+
104+
func typeString(flagType generator.FlagType) string {
105+
switch flagType {
106+
case generator.StringType:
107+
return "string"
108+
case generator.IntType:
109+
return "int"
110+
case generator.BoolType:
111+
return "bool"
112+
case generator.FloatType:
113+
return "float64"
114+
default:
115+
return ""
116+
}
117+
}
118+
119+
// Go Funcs END
120+
121+
// Generate generates the Go flag accessors for OpenFeature.
122+
func (g *genImpl) Generate(input generator.Input) error {
123+
funcs := template.FuncMap{
124+
"FlagVarName": flagVarName,
125+
"FlagInitParam": flagInitParam,
126+
"ProviderType": providerType,
127+
"FlagAccessFunc": flagAccessFunc,
128+
"SupportImports": supportImports,
129+
"DefaultValueLiteral": defaultValueLiteral,
130+
"TypeString": typeString,
131+
}
132+
td := TmplData{
133+
BaseTmplData: input.BaseData,
134+
GoPackage: g.goPackage,
135+
}
136+
return generator.GenerateFile(funcs, g.file, golangTmpl, &td)
137+
}
138+
139+
// Params are parameters for creating a Generator
140+
type Params struct {
141+
File string
142+
GoPackage string
143+
}
144+
145+
// NewGenerator creates a generator for Go.
146+
func NewGenerator(params Params) generator.Generator {
147+
return &genImpl{
148+
file: params.File,
149+
goPackage: params.GoPackage,
150+
}
151+
}

src/generators/golang/golang.tmpl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package {{.GoPackage}}
2+
3+
import (
4+
{{- range $_, $p := SupportImports .Flags}}
5+
{{$p}}
6+
{{- end}}
7+
)
8+
9+
var client *openfeature.Client = nil
10+
11+
12+
{{- range .Flags}}
13+
// {{.Docs}}
14+
var {{FlagVarName .Name}} = struct {
15+
Value providers.{{ProviderType .Type}}
16+
}{
17+
Value: func(ctx context.Context) ({{TypeString .Type}}, error) {
18+
return client.{{FlagAccessFunc .Type}}(ctx, {{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, openfeature.EvaluationContext{})
19+
},
20+
}
21+
{{- end}}
22+
23+
func init() {
24+
client = openfeature.NewClient("{{.GoPackage}}")
25+
}

src/go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module codegen
2+
3+
go 1.22.5
4+
5+
require (
6+
github.com/go-logr/logr v1.4.2 // indirect
7+
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
8+
)
9+
10+
require (
11+
github.com/iancoleman/strcase v0.3.0
12+
github.com/open-feature/go-sdk v1.12.0
13+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
14+
)

src/go.sum

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
2+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
3+
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
4+
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
5+
github.com/open-feature/go-sdk v1.12.0 h1:V0MAG3lC9o7Pmq0gxlqtKpoasDTm3to9vuvZKyUhhPk=
6+
github.com/open-feature/go-sdk v1.12.0/go.mod h1:UDNuwVrwY5FRHIluVRYzvxuS3nBkhjE6o4tlwFuHxiI=
7+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
8+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
9+
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
10+
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
11+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
12+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
13+
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
14+
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
15+
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
16+
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
17+
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

src/input.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"flags": {
3+
"myOpenFeatureFlag":{
4+
"flag_type": "boolean",
5+
"default_value": false,
6+
"description": "This is a flag."
7+
}
8+
}
9+
}

0 commit comments

Comments
 (0)