Skip to content

Commit e6c9563

Browse files
proto codegen (#281)
* part 1 * make codegen work for internal w/ protos * fix * code review
1 parent 41ab91a commit e6c9563

File tree

2 files changed

+333
-260
lines changed

2 files changed

+333
-260
lines changed

cmd/lekko/gen.go

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
// Copyright 2022 Lekko Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"bytes"
19+
"fmt"
20+
"os"
21+
"os/exec"
22+
"regexp"
23+
"sort"
24+
"strings"
25+
"text/template"
26+
27+
featurev1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/feature/v1beta1"
28+
"github.com/lekkodev/cli/pkg/repo"
29+
"github.com/lekkodev/cli/pkg/secrets"
30+
"github.com/pkg/errors"
31+
"github.com/spf13/cobra"
32+
"golang.org/x/mod/modfile"
33+
"google.golang.org/protobuf/proto"
34+
)
35+
36+
func genGoCmd() *cobra.Command {
37+
var ns string
38+
var wd string
39+
var of string
40+
cmd := &cobra.Command{
41+
Use: "go",
42+
Short: "generate Go library code from configs",
43+
RunE: func(cmd *cobra.Command, args []string) error {
44+
b, err := os.ReadFile("go.mod")
45+
if err != nil {
46+
return err
47+
}
48+
mf, err := modfile.ParseLax("go.mod", b, nil)
49+
if err != nil {
50+
return err
51+
}
52+
moduleRoot := mf.Module.Mod.Path
53+
54+
rs := secrets.NewSecretsOrFail()
55+
r, err := repo.NewLocal(wd, rs)
56+
if err != nil {
57+
return errors.Wrap(err, "new repo")
58+
}
59+
ffs, err := r.GetFeatureFiles(cmd.Context(), ns)
60+
if err != nil {
61+
return err
62+
}
63+
sort.SliceStable(ffs, func(i, j int) bool {
64+
return ffs[i].CompiledProtoBinFileName < ffs[j].CompiledProtoBinFileName
65+
})
66+
var protoAsByteStrings []string
67+
var codeStrings []string
68+
protoImportSet := make(map[string]*protoImport)
69+
for _, ff := range ffs {
70+
fff, err := os.ReadFile(wd + "/" + ns + "/" + ff.CompiledProtoBinFileName)
71+
if err != nil {
72+
return err
73+
}
74+
f := &featurev1beta1.Feature{}
75+
if err := proto.Unmarshal(fff, f); err != nil {
76+
return err
77+
}
78+
codeString, err := genGoForFeature(f, ns)
79+
if err != nil {
80+
return err
81+
}
82+
if f.Type == featurev1beta1.FeatureType_FEATURE_TYPE_PROTO {
83+
protoImport := unpackProtoType(moduleRoot, f.Tree.Default.TypeUrl)
84+
protoImportSet[protoImport.ImportPath] = &protoImport
85+
}
86+
protoAsBytes := fmt.Sprintf("\t\t\"%s\": []byte{", f.Key)
87+
for idx, b := range fff {
88+
if idx%16 == 0 {
89+
protoAsBytes += "\n\t\t\t"
90+
} else {
91+
protoAsBytes += " "
92+
}
93+
protoAsBytes += fmt.Sprintf("0x%02x,", b)
94+
}
95+
protoAsBytes += "\n\t\t},\n"
96+
protoAsByteStrings = append(protoAsByteStrings, protoAsBytes)
97+
codeStrings = append(codeStrings, codeString)
98+
}
99+
const templateBody = `package lekko{{$.Namespace}}
100+
101+
import (
102+
{{range $.ProtoImports}}
103+
{{ . }}{{end}}
104+
"context"
105+
client "github.com/lekkodev/go-sdk/client"
106+
)
107+
108+
type LekkoClient struct {
109+
client.Client
110+
Close client.CloseFunc
111+
}
112+
113+
type SafeLekkoClient struct {
114+
client.Client
115+
Close client.CloseFunc
116+
}
117+
118+
func (c *SafeLekkoClient) GetBool(ctx context.Context, namespace string, key string) bool {
119+
res, err := c.Client.GetBool(ctx, namespace, key)
120+
if err != nil {
121+
panic(err)
122+
}
123+
return res
124+
}
125+
func (c *SafeLekkoClient) GetString(ctx context.Context, namespace string, key string) string {
126+
res, err := c.Client.GetString(ctx, namespace, key)
127+
if err != nil {
128+
panic(err)
129+
}
130+
return res
131+
}
132+
133+
func (c *SafeLekkoClient) GetFloat(ctx context.Context, namespace string, key string) float64 {
134+
res, err := c.Client.GetFloat(ctx, namespace, key)
135+
if err != nil {
136+
panic(err)
137+
}
138+
return res
139+
}
140+
141+
func (c *SafeLekkoClient) GetInt(ctx context.Context, namespace string, key string) int64 {
142+
res, err := c.Client.GetInt(ctx, namespace, key)
143+
if err != nil {
144+
panic(err)
145+
}
146+
return res
147+
}
148+
149+
var StaticConfig = map[string]map[string][]byte{
150+
"{{$.Namespace}}": {
151+
{{range $.ProtoAsByteStrings}}{{ . }}{{end}} },
152+
}
153+
{{range $.CodeStrings}}
154+
{{ . }}{{end}}`
155+
156+
// buf generate --template '{"version":"v1","plugins":[{"plugin":"go","out":"gen/go"}]}'
157+
//
158+
// This generates the code for the config repo, assuming it has a buf.gen.yml in that repo.
159+
// In OUR repos, and maybe some of our customers, they may already have a buf.gen.yml, so if
160+
// that is the case, we should identify that, not run code gen (maybe?) and instead need to
161+
// take the prefix by parsing the buf.gen.yml to understand where the go code goes.
162+
pCmd := exec.Command(
163+
"buf",
164+
"generate",
165+
fmt.Sprintf(`--template={"managed": {"enabled": true, "go_package_prefix": {"default": "%s/internal/lekko/proto"}}, "version":"v1","plugins":[{"plugin":"go","out":"internal/lekko/proto", "opt": "paths=source_relative"}]}`, moduleRoot),
166+
"--include-imports",
167+
wd) // #nosec G204
168+
pCmd.Dir = "."
169+
fmt.Println("executing in wd: " + wd + " command: " + pCmd.String())
170+
if out, err := pCmd.CombinedOutput(); err != nil {
171+
fmt.Printf("Error when generating code with buf: %s\n %e\n", out, err)
172+
return err
173+
}
174+
if err := os.MkdirAll("./internal/lekko/"+ns, 0770); err != nil {
175+
return err
176+
}
177+
f, err := os.Create("./internal/lekko/" + ns + "/" + of)
178+
if err != nil {
179+
return err
180+
}
181+
var protoImports []string
182+
for _, imp := range protoImportSet {
183+
protoImports = append(protoImports, fmt.Sprintf(`%s "%s"`, imp.PackageAlias, imp.ImportPath))
184+
}
185+
data := struct {
186+
ProtoImports []string
187+
Namespace string
188+
ProtoAsByteStrings []string
189+
CodeStrings []string
190+
}{
191+
protoImports,
192+
ns,
193+
protoAsByteStrings,
194+
codeStrings,
195+
}
196+
templ := template.Must(template.New("").Parse(templateBody))
197+
return templ.Execute(f, data)
198+
},
199+
}
200+
cmd.Flags().StringVarP(&ns, "namespace", "n", "default", "namespace to generate code from")
201+
cmd.Flags().StringVarP(&wd, "config-path", "c", ".", "path to configuration repository")
202+
cmd.Flags().StringVarP(&of, "output", "o", "lekko.go", "output file")
203+
return cmd
204+
}
205+
206+
var genCmd = &cobra.Command{
207+
Use: "gen",
208+
Short: "generate library code from configs",
209+
}
210+
211+
func genGoForFeature(f *featurev1beta1.Feature, ns string) (string, error) {
212+
const defaultTemplateBody = `// {{$.Description}}
213+
func (c *LekkoClient) {{$.FuncName}}(ctx context.Context) ({{$.RetType}}, error) {
214+
return c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}")
215+
}
216+
217+
// {{$.Description}}
218+
func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context) {{$.RetType}} {
219+
return c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}")
220+
}
221+
`
222+
223+
const protoTemplateBody = `// {{$.Description}}
224+
func (c *LekkoClient) {{$.FuncName}}(ctx context.Context) (*{{$.RetType}}, error) {
225+
result := &{{$.RetType}}{}
226+
err := c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}", result)
227+
return result, err
228+
}
229+
230+
// {{$.Description}}
231+
func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context) *{{$.RetType}} {
232+
result := &{{$.RetType}}{}
233+
c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}", result)
234+
return result
235+
}
236+
`
237+
const jsonTemplateBody = `// {{$.Description}}
238+
func (c *LekkoClient) {{$.FuncName}}(ctx context.Context, result interface{}) error {
239+
return c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}", result)
240+
}
241+
242+
// {{$.Description}}
243+
func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context, result interface{}) {
244+
c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}", result)
245+
}
246+
`
247+
var funcNameBuilder strings.Builder
248+
funcNameBuilder.WriteString("Get")
249+
for _, word := range regexp.MustCompile("[_-]+").Split(f.Key, -1) {
250+
funcNameBuilder.WriteString(strings.ToUpper(word[:1]) + word[1:])
251+
}
252+
funcName := funcNameBuilder.String()
253+
var retType string
254+
var getFunction string
255+
templateBody := defaultTemplateBody
256+
switch f.Type {
257+
case 1:
258+
retType = "bool"
259+
getFunction = "GetBool"
260+
case 2:
261+
retType = "int64"
262+
getFunction = "GetInt"
263+
case 3:
264+
retType = "float64"
265+
getFunction = "GetFloat"
266+
case 4:
267+
retType = "string"
268+
getFunction = "GetString"
269+
case 5:
270+
getFunction = "GetJSON"
271+
templateBody = jsonTemplateBody
272+
case 6:
273+
getFunction = "GetProto"
274+
templateBody = protoTemplateBody
275+
// we don't need the import path so sending in empty string
276+
protoType := unpackProtoType("", f.Tree.Default.TypeUrl)
277+
// creates configv1beta1.DBConfig
278+
retType = fmt.Sprintf("%s.%s", protoType.PackageAlias, protoType.Type)
279+
}
280+
281+
data := struct {
282+
Description string
283+
FuncName string
284+
GetFunction string
285+
RetType string
286+
Namespace string
287+
Key string
288+
}{
289+
f.Description,
290+
funcName,
291+
getFunction,
292+
retType,
293+
ns,
294+
f.Key,
295+
}
296+
templ, err := template.New("go func").Parse(templateBody)
297+
if err != nil {
298+
return "", err
299+
}
300+
var ret bytes.Buffer
301+
err = templ.Execute(&ret, data)
302+
return ret.String(), err
303+
}
304+
305+
type protoImport struct {
306+
PackageAlias string
307+
ImportPath string
308+
Type string
309+
}
310+
311+
func unpackProtoType(moduleRoot string, typeURL string) protoImport {
312+
anyURLSplit := strings.Split(typeURL, "/")
313+
if anyURLSplit[0] != "type.googleapis.com" {
314+
panic("invalid any type url: " + typeURL)
315+
}
316+
// turn default.config.v1beta1.DBConfig into:
317+
// moduleRoot/internal/lekko/proto/default/config/v1beta1
318+
typeParts := strings.Split(anyURLSplit[1], ".")
319+
320+
importPath := strings.Join(append([]string{moduleRoot + "/internal/lekko/proto"}, typeParts[:len(typeParts)-1]...), "/")
321+
322+
prefix := fmt.Sprintf(`%s%s`, typeParts[len(typeParts)-3], typeParts[len(typeParts)-2])
323+
324+
// TODO do google.protobuf.X
325+
switch anyURLSplit[1] {
326+
case "google.protobuf.Duration":
327+
importPath = "google.golang.org/protobuf/types/known/durationpb"
328+
prefix = "durationpb"
329+
default:
330+
}
331+
332+
return protoImport{PackageAlias: prefix, ImportPath: importPath, Type: typeParts[len(typeParts)-1]}
333+
}

0 commit comments

Comments
 (0)