Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

natural language & static context key code gen #282

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 93 additions & 19 deletions cmd/lekko/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package main

import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
Expand All @@ -25,11 +26,15 @@ import (
"text/template"

featurev1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/feature/v1beta1"
rulesv1beta3 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/rules/v1beta3"
"github.com/lainio/err2/try"
"github.com/lekkodev/cli/pkg/repo"
"github.com/lekkodev/cli/pkg/secrets"
"github.com/pkg/errors"
"github.com/spf13/cobra"
strcase "github.com/stoewer/go-strcase"
"golang.org/x/mod/modfile"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)

Expand All @@ -56,6 +61,9 @@ func genGoCmd() *cobra.Command {
if err != nil {
return errors.Wrap(err, "new repo")
}
_, nsMDs := try.To2(r.ParseMetadata(cmd.Context()))

staticCtxType := unpackProtoType(moduleRoot, nsMDs[ns].ContextProto)
ffs, err := r.GetFeatureFiles(cmd.Context(), ns)
if err != nil {
return err
Expand All @@ -66,6 +74,9 @@ func genGoCmd() *cobra.Command {
var protoAsByteStrings []string
var codeStrings []string
protoImportSet := make(map[string]*protoImport)
if staticCtxType != nil {
protoImportSet[staticCtxType.ImportPath] = staticCtxType
}
for _, ff := range ffs {
fff, err := os.ReadFile(wd + "/" + ns + "/" + ff.CompiledProtoBinFileName)
if err != nil {
Expand All @@ -75,13 +86,13 @@ func genGoCmd() *cobra.Command {
if err := proto.Unmarshal(fff, f); err != nil {
return err
}
codeString, err := genGoForFeature(f, ns)
codeString, err := genGoForFeature(f, ns, staticCtxType)
if err != nil {
return err
}
if f.Type == featurev1beta1.FeatureType_FEATURE_TYPE_PROTO {
protoImport := unpackProtoType(moduleRoot, f.Tree.Default.TypeUrl)
protoImportSet[protoImport.ImportPath] = &protoImport
protoImportSet[protoImport.ImportPath] = protoImport
}
protoAsBytes := fmt.Sprintf("\t\t\"%s\": []byte{", f.Key)
for idx, b := range fff {
Expand Down Expand Up @@ -166,7 +177,6 @@ var StaticConfig = map[string]map[string][]byte{
"--include-imports",
wd) // #nosec G204
pCmd.Dir = "."
fmt.Println("executing in wd: " + wd + " command: " + pCmd.String())
if out, err := pCmd.CombinedOutput(); err != nil {
fmt.Println("this is the error probably")
fmt.Println(string(out))
Expand Down Expand Up @@ -210,17 +220,18 @@ var genCmd = &cobra.Command{
Short: "generate library code from configs",
}

func genGoForFeature(f *featurev1beta1.Feature, ns string) (string, error) {
func genGoForFeature(f *featurev1beta1.Feature, ns string, staticCtxType *protoImport) (string, error) {
const defaultTemplateBody = `// {{$.Description}}
func (c *LekkoClient) {{$.FuncName}}(ctx context.Context) ({{$.RetType}}, error) {
return c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}")
}

// {{$.Description}}
func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context) {{$.RetType}} {
{{if $.NaturalLanguage}}func (c *SafeLekkoClient) {{$.FuncName}}(ctx *{{$.StaticType}}) {{$.RetType}} {
{{range $.NaturalLanguage}}{{ . }}
{{end}}{{else}}func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context) {{$.RetType}} {
return c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}")
}
`
{{end}}}`

const protoTemplateBody = `// {{$.Description}}
func (c *LekkoClient) {{$.FuncName}}(ctx context.Context) (*{{$.RetType}}, error) {
Expand Down Expand Up @@ -255,6 +266,8 @@ func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context, result interface{}
var retType string
var getFunction string
templateBody := defaultTemplateBody
var natty []string

switch f.Type {
case 1:
retType = "bool"
Expand All @@ -268,6 +281,7 @@ func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context, result interface{}
case 4:
retType = "string"
getFunction = "GetString"
natty = translateFeature(f)
case 5:
getFunction = "GetJSON"
templateBody = jsonTemplateBody
Expand All @@ -281,19 +295,23 @@ func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context, result interface{}
}

data := struct {
Description string
FuncName string
GetFunction string
RetType string
Namespace string
Key string
Description string
FuncName string
GetFunction string
RetType string
Namespace string
Key string
NaturalLanguage []string
StaticType string
}{
f.Description,
funcName,
getFunction,
retType,
ns,
f.Key,
natty,
fmt.Sprintf("%s.%s", staticCtxType.PackageAlias, staticCtxType.Type),
}
templ, err := template.New("go func").Parse(templateBody)
if err != nil {
Expand All @@ -310,26 +328,82 @@ type protoImport struct {
Type string
}

func unpackProtoType(moduleRoot string, typeURL string) protoImport {
// This function handles both the google.protobuf.Any.TypeURL variable
// which has the format of `types.googleapis.com/fully.qualified.Proto`
// and purely `fully.qualified.Proto`
//
// return nil if typeURL is empty. Panics on any problems like the rest of the file.
func unpackProtoType(moduleRoot string, typeURL string) *protoImport {
if typeURL == "" {
return nil
}
anyURLSplit := strings.Split(typeURL, "/")
if anyURLSplit[0] != "type.googleapis.com" {
panic("invalid any type url: " + typeURL)
fqType := anyURLSplit[0]
if len(anyURLSplit) > 1 {
if anyURLSplit[0] != "type.googleapis.com" {
panic("invalid any type url: " + typeURL)
}
fqType = anyURLSplit[1]
}

// turn default.config.v1beta1.DBConfig into:
// moduleRoot/internal/lekko/proto/default/config/v1beta1
typeParts := strings.Split(anyURLSplit[1], ".")
typeParts := strings.Split(fqType, ".")

importPath := strings.Join(append([]string{moduleRoot + "/internal/lekko/proto"}, typeParts[:len(typeParts)-1]...), "/")

prefix := fmt.Sprintf(`%s%s`, typeParts[len(typeParts)-3], typeParts[len(typeParts)-2])

// TODO do google.protobuf.X
switch anyURLSplit[1] {
switch fqType {
case "google.protobuf.Duration":
importPath = "google.golang.org/protobuf/types/known/durationpb"
prefix = "durationpb"
default:
}

return protoImport{PackageAlias: prefix, ImportPath: importPath, Type: typeParts[len(typeParts)-1]}
return &protoImport{PackageAlias: prefix, ImportPath: importPath, Type: typeParts[len(typeParts)-1]}
}

func translateFeature(f *featurev1beta1.Feature) []string {
var buffer []string
for i, constraint := range f.Tree.Constraints {
ifToken := "} else if"
if i == 0 {
ifToken = "if"
}
rule := translateRule(constraint.GetRuleAstNew())
buffer = append(buffer, fmt.Sprintf("\t%s %s {", ifToken, rule))

// TODO this doesn't work for proto, but let's try

buffer = append(buffer, fmt.Sprintf("\t\treturn %s", try.To1(protojson.Marshal(try.To1(constraint.Value.UnmarshalNew())))))
}
if len(f.Tree.Constraints) > 0 {
buffer = append(buffer, "\t}")
}
buffer = append(buffer, fmt.Sprintf("\treturn %s", try.To1(protojson.Marshal(try.To1(f.Tree.Default.UnmarshalNew())))))
return buffer
}

func translateRule(rule *rulesv1beta3.Rule) string {
if rule == nil {
return ""
}
switch v := rule.GetRule().(type) {
case *rulesv1beta3.Rule_Atom:
switch v.Atom.GetComparisonOperator() {
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_EQUALS:
b, err := json.Marshal(v.Atom.ComparisonValue)
if err != nil {
panic(err)
}
return fmt.Sprintf("ctx.%s == %s", strcase.UpperCamelCase(v.Atom.ContextKey), string(b))
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_CONTAINED_WITHIN:
// TODO, probably logical to have this here but we need slice syntax, use slices as of golang 1.21
}
case *rulesv1beta3.Rule_LogicalExpression:
// TODO do some ands and ors
}
return ""
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ require (
github.com/go-git/go-billy/v5 v5.4.1
github.com/go-git/go-git/v5 v5.8.0
github.com/google/go-github/v52 v52.0.0
github.com/lainio/err2 v0.9.51
github.com/lekkodev/go-sdk v0.2.6-0.20230830172236-f072eb8bf64e
github.com/lekkodev/rules v1.5.3-0.20230724195144-d0ed93c3e218
github.com/migueleliasweb/go-github-mock v0.0.16
github.com/mitchellh/go-homedir v1.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.5.0
github.com/stoewer/go-strcase v1.2.0
github.com/stretchr/testify v1.8.0
github.com/whilp/git-urls v1.0.0
golang.org/x/mod v0.8.0
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/lainio/err2 v0.9.51 h1:R9lzMUxvP0T2U1C5DnpQG0FMMYooqGjUypP6aVmw+eY=
github.com/lainio/err2 v0.9.51/go.mod h1:glTVV2qNFbBy6WzZFDP2G5BqMiZI58cudp588cEgCuM=
github.com/lekkodev/go-sdk v0.2.6-0.20230830172236-f072eb8bf64e h1:UO23VqLwbW0NC8yP5dY7S9jBX/liGodXNSRnGBq02Js=
github.com/lekkodev/go-sdk v0.2.6-0.20230830172236-f072eb8bf64e/go.mod h1:zJ3izZC3/2MvgKrM1O4kV3PcaBHhCmmFziH1ls1APFI=
github.com/lekkodev/rules v1.5.3-0.20230724195144-d0ed93c3e218 h1:ULjfHubYgiEHrGdwcfNpAg+DNQCWMaU/zBNPUQNDNBE=
Expand Down Expand Up @@ -208,6 +210,7 @@ github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
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/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
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=
Expand Down
5 changes: 3 additions & 2 deletions pkg/metadata/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ type RootConfigRepoMetadata struct {
type NamespaceConfigRepoMetadata struct {
// This version refers to the version of the configuration in the repo itself.
// TODO we should move this to a separate version number.
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
ContextProto string `json:"contextProto,omitempty" yaml:"contextProto,omitempty"`
}

const DefaultRootConfigRepoMetadataFileName = "lekko.root.yaml"
Expand Down
Loading