diff --git a/cmd/lekko/gen.go b/cmd/lekko/gen.go index c14ce5c9..d841a0bf 100644 --- a/cmd/lekko/gen.go +++ b/cmd/lekko/gen.go @@ -77,6 +77,7 @@ func genNative(ctx context.Context, project *native.Project, lekkoPath, repoPath return gen.GenNative(ctx, project, lekkoPath, repoPath, opts) } +// TODO: Add option to read encoded repo contents like gen ts func genGoCmd() *cobra.Command { var lekkoPath, repoPath, ns string var initMode bool @@ -99,22 +100,41 @@ func genGoCmd() *cobra.Command { } func genTSCmd() *cobra.Command { - var ns string - var repoPath string - var lekkoPath string + var lekkoPath, repoPath, encodedRepoContents, ns string cmd := &cobra.Command{ Use: "ts", Short: "generate TypeScript library code from lekkos", RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + // For now, output generated code instead of writing to repo if using encoded contents + // TODO: make this optional even for local-based + if len(encodedRepoContents) > 0 { + repoContents, err := repo.DecodeRepositoryContents([]byte(encodedRepoContents)) + if err != nil { + return errors.Wrap(err, "decode") + } + generated, err := gen.GenTS(repoContents, ns) + if err != nil { + return errors.Wrap(err, "gen") + } + formatted, err := gen.FormatTS(generated) + if err != nil { + return errors.Wrap(err, "format") + } + fmt.Println(formatted) + return nil + } + nlProject := try.To1(native.DetectNativeLang("")) if nlProject.Language != native.LangTypeScript { return errors.Errorf("not a TypeScript project, detected %v instead", nlProject.Language) } - return genNative(cmd.Context(), nlProject, lekkoPath, repoPath, ns, false) + return genNative(ctx, nlProject, lekkoPath, repoPath, ns, false) }, } cmd.Flags().StringVarP(&lekkoPath, "lekko-path", "p", "", "path to Lekko native config files, will use autodetect if not set") cmd.Flags().StringVarP(&repoPath, "repo-path", "r", "", "path to config repository, will use autodetect if not set") + cmd.Flags().StringVarP(&encodedRepoContents, "repo-contents", "R", "", "base64-encoded serialized repository contents, will use repo-path if not set") cmd.Flags().StringVarP(&ns, "namespace", "n", "default", "namespace to generate code from") return cmd } diff --git a/cmd/lekko/main.go b/cmd/lekko/main.go index 6bb8467e..574cf1bb 100644 --- a/cmd/lekko/main.go +++ b/cmd/lekko/main.go @@ -21,8 +21,8 @@ import ( "os" "strconv" + "connectrpc.com/connect" "github.com/AlecAivazis/survey/v2" - "github.com/bufbuild/connect-go" "github.com/lainio/err2" "github.com/pkg/errors" "github.com/spf13/cobra" diff --git a/cmd/lekko/sync.go b/cmd/lekko/sync.go index 60a7d107..4f770902 100644 --- a/cmd/lekko/sync.go +++ b/cmd/lekko/sync.go @@ -296,12 +296,11 @@ func isSame(ctx context.Context, existing map[string]map[string]*featurev1beta1. } else { // These might still be equal, because the typescript path combines logical things in ways that the go path does not // Using ts since it has fewer args.. - gen.TypeRegistry = registry - o, err := gen.GenTSForFeature(f, namespace.Name, "") + o, err := gen.GenTSForFeature(f, namespace.Name, "", registry) if err != nil { return false, err } - e, err := gen.GenTSForFeature(existingConfig, namespace.Name, "") + e, err := gen.GenTSForFeature(existingConfig, namespace.Name, "", registry) if err != nil { return false, err } @@ -378,13 +377,12 @@ func isSameTS(ctx context.Context, existing map[string]map[string]*featurev1beta } else { // These might still be equal, because the typescript path combines logical things in ways that the go path does not // Using ts since it has fewer args.. - gen.TypeRegistry = registry //fmt.Printf("%+v\n\n", f) - o, err := gen.GenTSForFeature(f, namespace.Name, "") + o, err := gen.GenTSForFeature(f, namespace.Name, "", registry) if err != nil { return false, err } - e, err := gen.GenTSForFeature(existingConfig, namespace.Name, "") + e, err := gen.GenTSForFeature(existingConfig, namespace.Name, "", registry) if err != nil { return false, err } @@ -583,7 +581,6 @@ func ProtoJSONToTS(nsString []byte, fdString []byte) (string, error) { if err != nil { return "", err } - gen.TypeRegistry = registry.Types var featureStrings []string for _, namespace := range namespaces.Namespaces { for _, c := range namespace.Configs { @@ -598,7 +595,7 @@ func ProtoJSONToTS(nsString []byte, fdString []byte) (string, error) { } var ourParameters string - sigType, err := gen.TypeRegistry.FindMessageByName(protoreflect.FullName(namespace.Name + ".config.v1beta1." + strcase.ToCamel(f.Key) + "Args")) + sigType, err := registry.Types.FindMessageByName(protoreflect.FullName(namespace.Name + ".config.v1beta1." + strcase.ToCamel(f.Key) + "Args")) if err == nil { d := sigType.Descriptor() var varNames []string @@ -613,7 +610,7 @@ func ProtoJSONToTS(nsString []byte, fdString []byte) (string, error) { ourParameters = fmt.Sprintf("{%s}: {%s}", strings.Join(varNames, ", "), strings.Join(fields, " ")) } - fs, err := gen.GenTSForFeature(f, namespace.Name, ourParameters) + fs, err := gen.GenTSForFeature(f, namespace.Name, ourParameters, registry.Types) featureStrings = append(featureStrings, fs) if err != nil { return "", err diff --git a/go.mod b/go.mod index ee0be1ba..6b2fc985 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.22 replace github.com/bazelbuild/buildtools => github.com/lekkodev/buildtools v0.0.0-20240325231538-96eefd799042 require ( - buf.build/gen/go/lekkodev/cli/bufbuild/connect-go v1.10.0-20240528213244-5fdc18b47eea.1 - buf.build/gen/go/lekkodev/cli/protocolbuffers/go v1.34.2-20240923164736-6b09ba83efbf.2 + buf.build/gen/go/lekkodev/cli/connectrpc/go v1.17.0-20240926040046-3e1042256cdf.1 + buf.build/gen/go/lekkodev/cli/protocolbuffers/go v1.34.2-20240926040046-3e1042256cdf.2 + connectrpc.com/connect v1.17.0 github.com/AlecAivazis/survey/v2 v2.3.6 github.com/atotto/clipboard v0.1.4 github.com/bazelbuild/buildtools v0.0.0-20220907133145-b9bfff5d7f91 github.com/briandowns/spinner v1.23.0 - github.com/bufbuild/connect-go v1.10.0 github.com/cli/browser v1.0.0 github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 diff --git a/go.sum b/go.sum index 62275806..a03fdbc1 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,12 @@ -buf.build/gen/go/lekkodev/cli/bufbuild/connect-go v1.10.0-20240528213244-5fdc18b47eea.1 h1:JqArhl+OClAdLQis1N2N6WmLv96CbOaNrQEYWL2ntlI= -buf.build/gen/go/lekkodev/cli/bufbuild/connect-go v1.10.0-20240528213244-5fdc18b47eea.1/go.mod h1:gkMKhhTCMDLJVmimyqao6P3g7jB7wDe3r7u8hV2iShE= -buf.build/gen/go/lekkodev/cli/protocolbuffers/go v1.34.2-20240923164736-6b09ba83efbf.2 h1:JEiSyCH0wTycYIeIpm8xs/HcyPgHtJpdKBrUer8Jq6E= -buf.build/gen/go/lekkodev/cli/protocolbuffers/go v1.34.2-20240923164736-6b09ba83efbf.2/go.mod h1:j/ek65dWz+D5GM7p9QUiHQj5X5gtRUMfGl1+GpSfm6g= +buf.build/gen/go/lekkodev/cli/connectrpc/go v1.17.0-20240926040046-3e1042256cdf.1 h1:agiFYCNijwOenKQtub2FQ3lUdZvXQTii7eVV7ByY6h4= +buf.build/gen/go/lekkodev/cli/connectrpc/go v1.17.0-20240926040046-3e1042256cdf.1/go.mod h1:1ia6DcIYiqkQNmSTA00+Kys2yObN8ByxgcLbaTY1hNo= +buf.build/gen/go/lekkodev/cli/protocolbuffers/go v1.34.2-20240926040046-3e1042256cdf.2 h1:sS7KHlg3fCpYqz52WrRAOEr7uxezWXoGM7gopqB2gYQ= +buf.build/gen/go/lekkodev/cli/protocolbuffers/go v1.34.2-20240926040046-3e1042256cdf.2/go.mod h1:j/ek65dWz+D5GM7p9QUiHQj5X5gtRUMfGl1+GpSfm6g= buf.build/gen/go/lekkodev/sdk/protocolbuffers/go v1.34.2-20230810202034-1c821065b9a0.2 h1:ZEir2Lbw+XH5Dlnqiv0FUc8hC7QdUMpGSCIiREMriJ0= buf.build/gen/go/lekkodev/sdk/protocolbuffers/go v1.34.2-20230810202034-1c821065b9a0.2/go.mod h1:YAvVDcY/tXuUXkpfm3LHCD6vz9SPv73CktuPGgqzJkI= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= +connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= @@ -31,8 +33,6 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= -github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg= -github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= diff --git a/make/go/dep_protoc_gen_connect_go.mk b/make/go/dep_protoc_gen_connect_go.mk index 233ca5c3..ef32e3bc 100644 --- a/make/go/dep_protoc_gen_connect_go.mk +++ b/make/go/dep_protoc_gen_connect_go.mk @@ -7,13 +7,16 @@ $(call _assert_var,CACHE_VERSIONS) $(call _assert_var,CACHE_BIN) # Settable -# https://github.com/bufbuild/connect-go 20220531 checked 20220601 -CONNECT_VERSION ?= v0.1.0 +# https://github.com/connectrpc/connect-go 20240920 checked 20240920 +CONNECT_VERSION ?= v1.17.0 + +GO_GET_PKGS := $(GO_GET_PKGS) \ + connectrpc.com/connect@$(CONNECT_VERSION) PROTOC_GEN_CONNECT_GO := $(CACHE_VERSIONS)/connect-go/$(CONNECT_VERSION) $(PROTOC_GEN_CONNECT_GO): - @rm -f $(CACHE_BIN)/connect-go - GOBIN=$(CACHE_BIN) go install github.com/bufbuild/connect-go/cmd/protoc-gen-connect-go@$(CONNECT_VERSION) + @rm -f $(CACHE_BIN)/protoc-gen-connect-go + GOBIN=$(CACHE_BIN) go install connectrpc.com/connect/cmd/protoc-gen-connect-go@$(CONNECT_VERSION) @rm -rf $(dir $(PROTOC_GEN_CONNECT_GO)) @mkdir -p $(dir $(PROTOC_GEN_CONNECT_GO)) @touch $(PROTOC_GEN_CONNECT_GO) diff --git a/pkg/apikey/apikey.go b/pkg/apikey/apikey.go index 403746d3..c080fc52 100644 --- a/pkg/apikey/apikey.go +++ b/pkg/apikey/apikey.go @@ -17,10 +17,10 @@ package apikey import ( "context" - bffv1beta1connect "buf.build/gen/go/lekkodev/cli/bufbuild/connect-go/lekko/bff/v1beta1/bffv1beta1connect" + bffv1beta1connect "buf.build/gen/go/lekkodev/cli/connectrpc/go/lekko/bff/v1beta1/bffv1beta1connect" bffv1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/bff/v1beta1" + "connectrpc.com/connect" - connect_go "github.com/bufbuild/connect-go" "github.com/pkg/errors" ) @@ -34,7 +34,7 @@ func NewAPIKey(bff bffv1beta1connect.BFFServiceClient) *APIKeyManager { } func (a *APIKeyManager) Create(ctx context.Context, teamname, nickname string) (*bffv1beta1.GenerateAPIKeyResponse, error) { - resp, err := a.bff.GenerateAPIKey(ctx, connect_go.NewRequest(&bffv1beta1.GenerateAPIKeyRequest{ + resp, err := a.bff.GenerateAPIKey(ctx, connect.NewRequest(&bffv1beta1.GenerateAPIKeyRequest{ Nickname: nickname, TeamName: teamname, })) @@ -45,7 +45,7 @@ func (a *APIKeyManager) Create(ctx context.Context, teamname, nickname string) ( } func (a *APIKeyManager) List(ctx context.Context, teamname string) ([]*bffv1beta1.APIKey, error) { - resp, err := a.bff.ListAPIKeys(ctx, connect_go.NewRequest(&bffv1beta1.ListAPIKeysRequest{TeamName: teamname})) + resp, err := a.bff.ListAPIKeys(ctx, connect.NewRequest(&bffv1beta1.ListAPIKeysRequest{TeamName: teamname})) if err != nil { return nil, errors.Wrap(err, "list api keys") } @@ -53,7 +53,7 @@ func (a *APIKeyManager) List(ctx context.Context, teamname string) ([]*bffv1beta } func (a *APIKeyManager) Check(ctx context.Context, apikey string) (*bffv1beta1.APIKey, error) { - resp, err := a.bff.CheckAPIKey(ctx, connect_go.NewRequest(&bffv1beta1.CheckAPIKeyRequest{ + resp, err := a.bff.CheckAPIKey(ctx, connect.NewRequest(&bffv1beta1.CheckAPIKeyRequest{ ApiKey: apikey, })) if err != nil { @@ -63,7 +63,7 @@ func (a *APIKeyManager) Check(ctx context.Context, apikey string) (*bffv1beta1.A } func (a *APIKeyManager) Delete(ctx context.Context, nickname string) error { - _, err := a.bff.DeleteAPIKey(ctx, connect_go.NewRequest(&bffv1beta1.DeleteAPIKeyRequest{ + _, err := a.bff.DeleteAPIKey(ctx, connect.NewRequest(&bffv1beta1.DeleteAPIKeyRequest{ Nickname: nickname, })) return err diff --git a/pkg/gen/golang.go b/pkg/gen/golang.go index 72d28be1..870644e5 100644 --- a/pkg/gen/golang.go +++ b/pkg/gen/golang.go @@ -76,7 +76,7 @@ func NewGoGenerator(moduleRoot, outputPath, lekkoPath string, repoContents *feat func NewGoGeneratorFromLocal(ctx context.Context, moduleRoot, outputPath, lekkoPath string, repoPath string) (*goGenerator, error) { repoContents, err := ReadRepoContents(ctx, repoPath) if err != nil { - return nil, errors.Wrapf(err, "read contents from %s", repoContents) + return nil, errors.Wrapf(err, "read contents from %s", repoPath) } typeRegistry, err := protoutils.FileDescriptorSetToTypeRegistry(repoContents.FileDescriptorSet) if err != nil { @@ -326,6 +326,7 @@ func renderGoTemplate(templateBody string, fileName string, data any) (string, e // Generates public and private function files for the namespace as well as the overall client file. // Writes outputs to the output paths specified in the construction args. // TODO: since generator takes in whole repo contents now, could generate for all/filtered namespaces +// TODO: split away write out functionality and/or return contents func (g *goGenerator) Gen(ctx context.Context, namespaceName string) (err error) { defer err2.Handle(&err) // Validate namespace diff --git a/pkg/gen/ts.go b/pkg/gen/ts.go index c2457e3e..145ffbbc 100644 --- a/pkg/gen/ts.go +++ b/pkg/gen/ts.go @@ -18,11 +18,8 @@ import ( "bytes" "context" "fmt" - "io" - "log" "os" "os/exec" - "path/filepath" "regexp" "slices" "sort" @@ -34,6 +31,7 @@ import ( rulesv1beta3 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/rules/v1beta3" "github.com/iancoleman/strcase" "github.com/lainio/err2/try" + protoutils "github.com/lekkodev/cli/pkg/proto" "github.com/lekkodev/cli/pkg/repo" "github.com/pkg/errors" "golang.org/x/exp/maps" @@ -47,8 +45,6 @@ import ( "google.golang.org/protobuf/types/known/wrapperspb" ) -var TypeRegistry *protoregistry.Types - func FieldDescriptorToTS(f protoreflect.FieldDescriptor) string { var t string switch f.Kind() { @@ -115,7 +111,7 @@ func GetTSInterface(d protoreflect.MessageDescriptor) (string, error) { string(d.Name()), fields, } - templ, err := template.New("go func").Parse(templateBody) + templ, err := template.New("ts interface").Parse(templateBody) if err != nil { return "", err } @@ -127,7 +123,7 @@ func GetTSInterface(d protoreflect.MessageDescriptor) (string, error) { return ret.String(), nil } -func getTSParameters(d protoreflect.MessageDescriptor) string { +func GetTSParameters(d protoreflect.MessageDescriptor) string { var fields []string for i := 0; i < d.Fields().Len(); i++ { f := d.Fields().Get(i) @@ -137,95 +133,63 @@ func getTSParameters(d protoreflect.MessageDescriptor) string { return fmt.Sprintf("{%s}: %s", strings.Join(fields, ", "), d.Name()) } -// Pipe `GenTS` to prettier -func GenFormattedTS(ctx context.Context, repoPath, ns, outFilename string) error { +// Requires `prettier` NPM package to be installed +func FormatTS(unformatted string) (string, error) { prettierCmd := exec.Command("npx", "prettier", "--parser", "typescript") stdinPipe, err := prettierCmd.StdinPipe() if err != nil { - return err + return "", errors.Wrap(err, "open prettier pipe") } - err = GenTS(ctx, repoPath, ns, func() (io.Writer, error) { - return stdinPipe, nil - }) - if err != nil { - return err + if _, err := stdinPipe.Write([]byte(unformatted)); err != nil { + return "", errors.Wrap(err, "pipe to prettier") } stdinPipe.Close() out, err := prettierCmd.Output() if err != nil { - return err + return "", errors.Wrap(err, "exec prettier") } - return os.WriteFile(outFilename, out, 0600) + return string(out), nil } -func GenTS(ctx context.Context, repoPath, ns string, getWriter func() (io.Writer, error)) error { - // TODO to avoid weird error message we should compile first. - var err error - if len(repoPath) == 0 { - repoPath, err = repo.PrepareGithubRepo() - if err != nil { - return err - } - } - // TODO: generate from contents, maybe split repo/repoless - r, err := repo.NewLocal(repoPath, nil) +// Generate TS code, format it, and write to destination +func GenFormattedTS(ctx context.Context, repoPath, ns, outFilename string) error { + unformatted, err := GenTSFromLocal(ctx, repoPath, ns) if err != nil { - return errors.Wrap(err, "new repo") + return errors.Wrap(err, "gen ts") } - rootMD, nsMDs := try.To2(r.ParseMetadata(ctx)) - TypeRegistry = try.To1(r.BuildDynamicTypeRegistry(ctx, rootMD.ProtoDirectory)) - var parameters string - interfaces := make(map[string]string) - if _, ok := nsMDs[ns]; !ok { - log.Fatal("unknown namespace: ", ns) + formatted, err := FormatTS(unformatted) + if err != nil { + return errors.Wrap(err, "format ts") } - if len(nsMDs[ns].ContextProto) > 0 { - ptype, err := TypeRegistry.FindMessageByName(protoreflect.FullName(nsMDs[ns].ContextProto)) - if err != nil { - log.Fatal("error finding the message in the registry", err) - } - parameters = getTSParameters(ptype.Descriptor()) - face, err := GetTSInterface(ptype.Descriptor()) - if err != nil { - return err + return os.WriteFile(outFilename, []byte(formatted), 0600) +} + +// Generates TypeScript code for a namespace. +// Outputs the contents of a single file. +func GenTS(repoContents *featurev1beta1.RepositoryContents, namespaceName string) (string, error) { + var namespace *featurev1beta1.Namespace + for _, n := range repoContents.Namespaces { + if n.Name == namespaceName { + namespace = n } - interfaces[nsMDs[ns].ContextProto] = face } - var codeStrings []string - /* - typeRegistry.RangeMessages(func(mt protoreflect.MessageType) bool { - splitName := strings.Split(string(mt.Descriptor().FullName()), ".") - if splitName[0] == "google" { - return true - } - face, err := getTSInterface(mt.Descriptor()) - if err != nil { - panic(err) - } - codeStrings = append(codeStrings, face) - return true - }) - */ - - ffs, err := r.GetFeatureFiles(ctx, ns) + if namespace == nil { + return "", errors.Errorf("namespace %s not found", namespaceName) + } + if repoContents.FileDescriptorSet == nil { + return "", errors.New("missing fds") + } + typeRegistry, err := protoutils.FileDescriptorSetToTypeRegistry(repoContents.FileDescriptorSet) if err != nil { - return err + return "", errors.Wrap(err, "parse type registry") } - sort.SliceStable(ffs, func(i, j int) bool { - return ffs[i].CompiledProtoBinFileName < ffs[j].CompiledProtoBinFileName - }) protoImports := make(map[string]struct{}) - for _, ff := range ffs { - ourParameters := parameters - fff, err := os.ReadFile(filepath.Join(repoPath, ns, ff.CompiledProtoBinFileName)) - if err != nil { - return err - } - f := &featurev1beta1.Feature{} - if err := proto.Unmarshal(fff, f); err != nil { - return err - } + interfaces := make(map[string]string) + var codeStrings []string + + for _, f := range namespace.Features { + ourParameters := "" if f.Type == featurev1beta1.FeatureType_FEATURE_TYPE_PROTO { // TODO: refactor this shared function? pImport := UnpackProtoType("", "internal/lekko", f.Tree.Default.TypeUrl) @@ -234,20 +198,20 @@ func GenTS(ctx context.Context, repoPath, ns string, getWriter func() (io.Writer } else { name := strings.Split(f.Tree.Default.TypeUrl, "/")[1] if _, ok := interfaces[name]; !ok { - ptype, err := TypeRegistry.FindMessageByName(protoreflect.FullName(name)) + ptype, err := typeRegistry.FindMessageByName(protoreflect.FullName(name)) if err != nil { - return errors.Wrapf(err, "could not find message: %s", protoreflect.FullName(name)) + return "", errors.Wrapf(err, "could not find message: %s", protoreflect.FullName(name)) } face, err := GetTSInterface(ptype.Descriptor()) if err != nil { - return err + return "", err } interfaces[name] = face } } } // Check if there is a per-config signature proto - sigType, err := TypeRegistry.FindMessageByName(protoreflect.FullName(ns + ".config.v1beta1." + strcase.ToCamel(f.Key) + "Args")) + sigType, err := typeRegistry.FindMessageByName(protoreflect.FullName(namespaceName + ".config.v1beta1." + strcase.ToCamel(f.Key) + "Args")) if err == nil { d := sigType.Descriptor() var varNames []string @@ -262,12 +226,13 @@ func GenTS(ctx context.Context, repoPath, ns string, getWriter func() (io.Writer ourParameters = fmt.Sprintf("{%s}: {%s}", strings.Join(varNames, ", "), strings.Join(fields, " ")) } - codeString, err := GenTSForFeature(f, ns, ourParameters) + codeString, err := GenTSForFeature(f, namespaceName, ourParameters, typeRegistry) if err != nil { - return err + return "", err } codeStrings = append(codeStrings, codeString) } + const templateBody = `{{range $.CodeStrings}} {{ . }} {{end}}` @@ -283,18 +248,37 @@ func GenTS(ctx context.Context, repoPath, ns string, getWriter func() (io.Writer Namespace string CodeStrings []string }{ - ns, + namespaceName, append(maps.Keys(protoImports), append(interfaceStrings, codeStrings...)...), } - wr, err := getWriter() + var contents bytes.Buffer + templ := template.Must(template.New("").Parse(templateBody)) + if err := templ.Execute(&contents, data); err != nil { + return "", errors.Wrap(err, "contents template") + } + return contents.String(), nil +} + +// Generates TypeScript code for a namespace from a local config repository. +// Outputs the contents of a single file. +func GenTSFromLocal(ctx context.Context, repoPath, namespaceName string) (string, error) { + var err error + // TODO to avoid weird error message we should compile first. + if len(repoPath) == 0 { + repoPath, err = repo.PrepareGithubRepo() + if err != nil { + return "", err + } + } + repoContents, err := ReadRepoContents(ctx, repoPath) if err != nil { - return err + return "", errors.Wrapf(err, "read contents from %s", repoPath) } - templ := template.Must(template.New("").Parse(templateBody)) - return templ.Execute(wr, data) + + return GenTS(repoContents, namespaceName) } -func GenTSForFeature(f *featurev1beta1.Feature, ns string, parameters string) (string, error) { +func GenTSForFeature(f *featurev1beta1.Feature, ns string, parameters string, typeRegistry *protoregistry.Types) (string, error) { // TODO: support multiline descriptions const templateBody = `{{if $.Description}}/** {{$.Description}} */{{end}} export function {{$.FuncName}}({{$.Parameters}}): {{$.RetType}} { @@ -331,7 +315,7 @@ export function {{$.FuncName}}({{$.Parameters}}): {{$.RetType}} { } usedVariables := make(map[string]string) - code := translateFeatureTS(f, usedVariables) + code := translateFeatureTS(f, usedVariables, typeRegistry) if len(parameters) == 0 && len(usedVariables) > 0 { var keys []string var keyAndTypes []string @@ -378,7 +362,7 @@ export function {{$.FuncName}}({{$.Parameters}}): {{$.RetType}} { return ret.String(), nil } -func translateFeatureTS(f *featurev1beta1.Feature, usedVariables map[string]string) []string { +func translateFeatureTS(f *featurev1beta1.Feature, usedVariables map[string]string, typeRegistry *protoregistry.Types) []string { var buffer []string for i, constraint := range f.Tree.Constraints { ifToken := "} else if" @@ -389,12 +373,12 @@ func translateFeatureTS(f *featurev1beta1.Feature, usedVariables map[string]stri 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;", translateRetValueTS(constraint.Value, f.Type))) + buffer = append(buffer, fmt.Sprintf("\t\treturn %s;", translateRetValueTS(constraint.Value, f.Type, typeRegistry))) } if len(f.Tree.Constraints) > 0 { buffer = append(buffer, "\t}") } - buffer = append(buffer, fmt.Sprintf("\treturn %s;", translateRetValueTS(f.GetTree().GetDefault(), f.Type))) + buffer = append(buffer, fmt.Sprintf("\treturn %s;", translateRetValueTS(f.GetTree().GetDefault(), f.Type, typeRegistry))) return buffer } @@ -487,14 +471,14 @@ func translateRuleTS(rule *rulesv1beta3.Rule, usedVariables map[string]string) s } // returns only the formatted value -func FieldValueToTS(f protoreflect.FieldDescriptor, val protoreflect.Value) string { +func FieldValueToTS(f protoreflect.FieldDescriptor, val protoreflect.Value, typeRegistry *protoregistry.Types) string { if msg, ok := val.Interface().(protoreflect.Message); ok { - if _, err := TypeRegistry.FindMessageByName((msg.Descriptor().FullName())); err != nil { + if _, err := typeRegistry.FindMessageByName((msg.Descriptor().FullName())); err != nil { // THIS SUCKS but is probably a bug we should file with anypb if someone / konrad is bored. - try.To(TypeRegistry.RegisterMessage(msg.Type())) + try.To(typeRegistry.RegisterMessage(msg.Type())) } kind := featurev1beta1.FeatureType_FEATURE_TYPE_PROTO - return translateRetValueTS(try.To1(anypb.New(msg.Interface())), kind) + return translateRetValueTS(try.To1(anypb.New(msg.Interface())), kind, typeRegistry) } else { switch f.Kind() { case protoreflect.EnumKind: @@ -551,7 +535,7 @@ func FieldValueToTS(f protoreflect.FieldDescriptor, val protoreflect.Value) stri val.Map().Range(func(mk protoreflect.MapKey, mv protoreflect.Value) bool { lines = append(lines, fmt.Sprintf("\"%s\": %s", mk.String(), - FieldValueToTS(f.MapValue(), mv))) + FieldValueToTS(f.MapValue(), mv, typeRegistry))) return true }) sort.Strings(lines) @@ -563,7 +547,7 @@ func FieldValueToTS(f protoreflect.FieldDescriptor, val protoreflect.Value) stri list := val.List() for i := 0; i < list.Len(); i++ { item := list.Get(i) - results = append(results, FieldValueToTS(f, item)) + results = append(results, FieldValueToTS(f, item, typeRegistry)) } return "[" + strings.Join(results, ", ") + "]" } @@ -574,7 +558,7 @@ func FieldValueToTS(f protoreflect.FieldDescriptor, val protoreflect.Value) stri panic("Unreachable code was reached") } -func translateRetValueTS(val *anypb.Any, t featurev1beta1.FeatureType) string { +func translateRetValueTS(val *anypb.Any, t featurev1beta1.FeatureType, typeRegistry *protoregistry.Types) string { // TODO - move to lekkoAny if val.TypeUrl == "type.googleapis.com/lekko.rules.v1beta3.ConfigCall" { call := &rulesv1beta3.ConfigCall{} @@ -602,7 +586,7 @@ func translateRetValueTS(val *anypb.Any, t featurev1beta1.FeatureType) string { // TODO double if t != featurev1beta1.FeatureType_FEATURE_TYPE_PROTO { // we are guessing this is a primitive, (unless we have i64 so let's do that later) - return marshalOptions.Format(try.To1(anypb.UnmarshalNew(val, proto.UnmarshalOptions{Resolver: TypeRegistry}))) + return marshalOptions.Format(try.To1(anypb.UnmarshalNew(val, proto.UnmarshalOptions{Resolver: typeRegistry}))) } switch strings.Split(val.TypeUrl, "/")[1] { @@ -612,16 +596,16 @@ func translateRetValueTS(val *anypb.Any, t featurev1beta1.FeatureType) string { try.To(val.UnmarshalTo(&v)) return fmt.Sprintf("protobuf.Duration.fromJsonString(%s)", marshalOptions.Format(&v)) default: - dynMsg, err := anypb.UnmarshalNew(val, proto.UnmarshalOptions{Resolver: TypeRegistry}) + dynMsg, err := anypb.UnmarshalNew(val, proto.UnmarshalOptions{Resolver: typeRegistry}) if err != nil { - TypeRegistry.RangeMessages(func(m protoreflect.MessageType) bool { + typeRegistry.RangeMessages(func(m protoreflect.MessageType) bool { return true }) panic(fmt.Sprintf("idk what is going on: %e %+v", err, err)) } var lines []string dynMsg.ProtoReflect().Range(func(f protoreflect.FieldDescriptor, val protoreflect.Value) bool { - lines = append(lines, fmt.Sprintf("\t\"%s\": %s", strcase.ToLowerCamel(f.TextName()), FieldValueToTS(f, val))) + lines = append(lines, fmt.Sprintf("\t\"%s\": %s", strcase.ToLowerCamel(f.TextName()), FieldValueToTS(f, val, typeRegistry))) return true }) diff --git a/pkg/lekko/client.go b/pkg/lekko/client.go index 840cca1f..e873d77a 100644 --- a/pkg/lekko/client.go +++ b/pkg/lekko/client.go @@ -19,8 +19,8 @@ import ( "fmt" "net/http" - bffv1beta1connect "buf.build/gen/go/lekkodev/cli/bufbuild/connect-go/lekko/bff/v1beta1/bffv1beta1connect" - connect_go "github.com/bufbuild/connect-go" + bffv1beta1connect "buf.build/gen/go/lekkodev/cli/connectrpc/go/lekko/bff/v1beta1/bffv1beta1connect" + "connectrpc.com/connect" ) const ( @@ -45,15 +45,15 @@ type AuthCredentials interface { func NewBFFClient(creds AuthCredentials) bffv1beta1connect.BFFServiceClient { interceptor := NewUserAuthInterceptor(creds) - return bffv1beta1connect.NewBFFServiceClient(http.DefaultClient, URL, connect_go.WithInterceptors(interceptor)) + return bffv1beta1connect.NewBFFServiceClient(http.DefaultClient, URL, connect.WithInterceptors(interceptor)) } -func NewUserAuthInterceptor(a AuthCredentials) connect_go.UnaryInterceptorFunc { - interceptor := func(next connect_go.UnaryFunc) connect_go.UnaryFunc { - return connect_go.UnaryFunc(func( +func NewUserAuthInterceptor(a AuthCredentials) connect.UnaryInterceptorFunc { + interceptor := func(next connect.UnaryFunc) connect.UnaryFunc { + return connect.UnaryFunc(func( ctx context.Context, - req connect_go.AnyRequest, - ) (connect_go.AnyResponse, error) { + req connect.AnyRequest, + ) (connect.AnyResponse, error) { if a.HasLekkoToken() { req.Header().Set(AuthorizationHeaderKey, fmt.Sprintf("Bearer %s", a.GetLekkoToken())) if lekkoTeam := a.GetLekkoTeam(); len(lekkoTeam) > 0 { @@ -69,5 +69,5 @@ func NewUserAuthInterceptor(a AuthCredentials) connect_go.UnaryInterceptorFunc { return next(ctx, req) }) } - return connect_go.UnaryInterceptorFunc(interceptor) + return connect.UnaryInterceptorFunc(interceptor) } diff --git a/pkg/oauth/lekko.go b/pkg/oauth/lekko.go index 47d1a882..6e9f21bb 100644 --- a/pkg/oauth/lekko.go +++ b/pkg/oauth/lekko.go @@ -23,9 +23,9 @@ import ( "os" "time" - bffv1beta1connect "buf.build/gen/go/lekkodev/cli/bufbuild/connect-go/lekko/bff/v1beta1/bffv1beta1connect" + bffv1beta1connect "buf.build/gen/go/lekkodev/cli/connectrpc/go/lekko/bff/v1beta1/bffv1beta1connect" bffv1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/bff/v1beta1" - connect_go "github.com/bufbuild/connect-go" + "connectrpc.com/connect" "github.com/cenkalti/backoff/v4" "github.com/cli/browser" "github.com/pkg/errors" @@ -56,7 +56,7 @@ type AuthCredentials struct { } func (f *DeviceFlow) Authorize(ctx context.Context) (*AuthCredentials, error) { - resp, err := f.lekkoAuthClient.GetDeviceCode(ctx, connect_go.NewRequest(&bffv1beta1.GetDeviceCodeRequest{ + resp, err := f.lekkoAuthClient.GetDeviceCode(ctx, connect.NewRequest(&bffv1beta1.GetDeviceCodeRequest{ ClientId: LekkoClientID, })) if err != nil { @@ -76,7 +76,7 @@ func (f *DeviceFlow) pollToken(ctx context.Context, deviceCode string, interval var ret *AuthCredentials operation := func() error { - resp, err := f.lekkoAuthClient.GetAccessToken(ctx, connect_go.NewRequest(&bffv1beta1.GetAccessTokenRequest{ + resp, err := f.lekkoAuthClient.GetAccessToken(ctx, connect.NewRequest(&bffv1beta1.GetAccessTokenRequest{ DeviceCode: deviceCode, ClientId: LekkoClientID, })) diff --git a/pkg/oauth/oauth.go b/pkg/oauth/oauth.go index b2e3d746..6cf928cd 100644 --- a/pkg/oauth/oauth.go +++ b/pkg/oauth/oauth.go @@ -23,10 +23,10 @@ import ( "sync" "time" - bffv1beta1connect "buf.build/gen/go/lekkodev/cli/bufbuild/connect-go/lekko/bff/v1beta1/bffv1beta1connect" + bffv1beta1connect "buf.build/gen/go/lekkodev/cli/connectrpc/go/lekko/bff/v1beta1/bffv1beta1connect" bffv1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/bff/v1beta1" + "connectrpc.com/connect" "github.com/briandowns/spinner" - connect_go "github.com/bufbuild/connect-go" ghauth "github.com/cli/oauth" "github.com/lekkodev/cli/pkg/gh" "github.com/lekkodev/cli/pkg/lekko" @@ -115,7 +115,7 @@ func (a *OAuth) PreRegister(ctx context.Context, username string, ws secrets.Wri s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) s.Suffix = " Generating OAuth code..." s.Start() - dcResp, err := a.lekkoAuthClient.GetDeviceCode(ctx, connect_go.NewRequest(&bffv1beta1.GetDeviceCodeRequest{ + dcResp, err := a.lekkoAuthClient.GetDeviceCode(ctx, connect.NewRequest(&bffv1beta1.GetDeviceCodeRequest{ ClientId: LekkoClientID, })) s.Stop() @@ -124,7 +124,7 @@ func (a *OAuth) PreRegister(ctx context.Context, username string, ws secrets.Wri } s.Suffix = " Connecting to Lekko..." s.Start() - _, err = a.lekkoAuthClient.PreRegisterUser(ctx, connect_go.NewRequest(&bffv1beta1.PreRegisterUserRequest{ + _, err = a.lekkoAuthClient.PreRegisterUser(ctx, connect.NewRequest(&bffv1beta1.PreRegisterUserRequest{ Username: username, // We're passing in user code just because that's what backend handles better at the moment DeviceCode: dcResp.Msg.UserCode, @@ -155,7 +155,7 @@ func (a *OAuth) PreRegister(ctx context.Context, username string, ws secrets.Wri } func (a *OAuth) Register(ctx context.Context, username, password, confirmPassword string) error { - registerResp, err := a.lekkoAuthClient.RegisterUser(ctx, connect_go.NewRequest(&bffv1beta1.RegisterUserRequest{ + registerResp, err := a.lekkoAuthClient.RegisterUser(ctx, connect.NewRequest(&bffv1beta1.RegisterUserRequest{ Username: username, Password: password, ConfirmPassword: confirmPassword, @@ -170,7 +170,7 @@ func (a *OAuth) Register(ctx context.Context, username, password, confirmPasswor } func (a *OAuth) ConfirmUser(ctx context.Context, username, code string) error { - _, err := a.lekkoAuthClient.ConfirmUser(ctx, connect_go.NewRequest(&bffv1beta1.ConfirmUserRequest{ + _, err := a.lekkoAuthClient.ConfirmUser(ctx, connect.NewRequest(&bffv1beta1.ConfirmUserRequest{ Username: username, Code: code, })) @@ -188,7 +188,7 @@ func (a *OAuth) Tokens(ctx context.Context, rs secrets.ReadSecrets) []string { } func (a *OAuth) ForgotPassword(ctx context.Context, email string) error { - _, err := a.lekkoAuthClient.ForgotPassword(ctx, connect_go.NewRequest( + _, err := a.lekkoAuthClient.ForgotPassword(ctx, connect.NewRequest( &bffv1beta1.ForgotPasswordRequest{Username: email}), ) return err @@ -197,7 +197,7 @@ func (a *OAuth) ForgotPassword(ctx context.Context, email string) error { func (a *OAuth) ConfirmForgotPassword( ctx context.Context, email string, newPassword string, confirmNewPassword string, verificationCode string, ) error { - _, err := a.lekkoAuthClient.ConfirmForgotPassword(ctx, connect_go.NewRequest( + _, err := a.lekkoAuthClient.ConfirmForgotPassword(ctx, connect.NewRequest( &bffv1beta1.ConfirmForgotPasswordRequest{ Username: email, NewPassword: newPassword, @@ -209,7 +209,7 @@ func (a *OAuth) ConfirmForgotPassword( } func (a *OAuth) ResendVerification(ctx context.Context, email string) error { - _, err := a.lekkoAuthClient.ResendVerificationCode(ctx, connect_go.NewRequest( + _, err := a.lekkoAuthClient.ResendVerificationCode(ctx, connect.NewRequest( &bffv1beta1.ResendVerificationCodeRequest{Username: email}), ) return err @@ -338,7 +338,7 @@ func (a *OAuth) loginGithub(ctx context.Context, ws secrets.WriteSecrets) error } func (a *OAuth) checkLekkoAuth(ctx context.Context) (username string, err error) { - req := connect_go.NewRequest(&bffv1beta1.GetUserLoggedInInfoRequest{}) + req := connect.NewRequest(&bffv1beta1.GetUserLoggedInInfoRequest{}) resp, err := a.lekkoBFFClient.GetUserLoggedInInfo(ctx, req) if err != nil { return "", errors.Wrap(err, "check lekko auth") diff --git a/pkg/repo/cmd.go b/pkg/repo/cmd.go index e8f7f24f..7962bca0 100644 --- a/pkg/repo/cmd.go +++ b/pkg/repo/cmd.go @@ -21,10 +21,10 @@ import ( "os" "path/filepath" - bffv1beta1connect "buf.build/gen/go/lekkodev/cli/bufbuild/connect-go/lekko/bff/v1beta1/bffv1beta1connect" + bffv1beta1connect "buf.build/gen/go/lekkodev/cli/connectrpc/go/lekko/bff/v1beta1/bffv1beta1connect" bffv1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/bff/v1beta1" + "connectrpc.com/connect" "github.com/AlecAivazis/survey/v2" - connect_go "github.com/bufbuild/connect-go" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" @@ -61,7 +61,7 @@ var ErrRemoteHasChanges = errors.New("Remote repository has new changes, " + fmt.Sprintf("please run %s to merge them locally and try again.", logging.Bold("lekko pull"))) func (r *RepoCmd) List(ctx context.Context) ([]*Repository, error) { - resp, err := r.lekkoBFFClient.ListRepositories(ctx, connect_go.NewRequest(&bffv1beta1.ListRepositoriesRequest{})) + resp, err := r.lekkoBFFClient.ListRepositories(ctx, connect.NewRequest(&bffv1beta1.ListRepositoriesRequest{})) if err != nil { return nil, errors.Wrap(err, "list repos") } @@ -85,7 +85,7 @@ func repoFromProto(repo *bffv1beta1.Repository) *Repository { } func (r *RepoCmd) Create(ctx context.Context, owner, repo, description string) (string, error) { - resp, err := r.lekkoBFFClient.CreateRepository(ctx, connect_go.NewRequest(&bffv1beta1.CreateRepositoryRequest{ + resp, err := r.lekkoBFFClient.CreateRepository(ctx, connect.NewRequest(&bffv1beta1.CreateRepositoryRequest{ RepoKey: &bffv1beta1.RepositoryKey{ OwnerName: owner, RepoName: repo, @@ -99,7 +99,7 @@ func (r *RepoCmd) Create(ctx context.Context, owner, repo, description string) ( } func (r *RepoCmd) Delete(ctx context.Context, owner, repo string, deleteOnRemote bool) error { - _, err := r.lekkoBFFClient.DeleteRepository(ctx, connect_go.NewRequest(&bffv1beta1.DeleteRepositoryRequest{ + _, err := r.lekkoBFFClient.DeleteRepository(ctx, connect.NewRequest(&bffv1beta1.DeleteRepositoryRequest{ RepoKey: &bffv1beta1.RepositoryKey{ OwnerName: owner, RepoName: repo, @@ -342,7 +342,7 @@ func (r *RepoCmd) Import(ctx context.Context, repoPath, owner, repoName, descrip } // Import new repo into Lekko - _, err = r.lekkoBFFClient.ImportRepository(ctx, connect_go.NewRequest(&bffv1beta1.ImportRepositoryRequest{ + _, err = r.lekkoBFFClient.ImportRepository(ctx, connect.NewRequest(&bffv1beta1.ImportRepositoryRequest{ RepoKey: &bffv1beta1.RepositoryKey{ OwnerName: owner, RepoName: repoName, diff --git a/pkg/repo/contents.go b/pkg/repo/contents.go new file mode 100644 index 00000000..220c6272 --- /dev/null +++ b/pkg/repo/contents.go @@ -0,0 +1,52 @@ +// Copyright 2022 Lekko Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import ( + "encoding/base64" + + featurev1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/feature/v1beta1" + protoutils "github.com/lekkodev/cli/pkg/proto" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" +) + +// Expects base64 encoded serialized RepositoryContents message +func DecodeRepositoryContents(encoded []byte) (*featurev1beta1.RepositoryContents, error) { + // Because Protobuf is not self-describing, we have to jump through some hoops here for deserialization. + // The RepositoryContents message contains the FDS which we want to use as the resolver for unmarshalling + // the rest of the contents. + decoded, err := base64.StdEncoding.DecodeString(string(encoded)) + if err != nil { + return nil, errors.Wrap(err, "base64 decode") + } + // First pass unmarshal to get the FDS while ignoring any unresolvable Anys + tempRepoContents := &featurev1beta1.RepositoryContents{} + err = proto.UnmarshalOptions{DiscardUnknown: true, Resolver: &protoutils.IgnoreAnyResolver{}}.Unmarshal(decoded, tempRepoContents) + if err != nil { + return nil, errors.Wrap(err, "shallow unmarshal") + } + typeRegistry, err := protoutils.FileDescriptorSetToTypeRegistry(tempRepoContents.FileDescriptorSet) + if err != nil { + return nil, errors.Wrap(err, "get type registry") + } + // Re-unmarshal using type registry this time - we can resolve Anys correctly + repoContents := &featurev1beta1.RepositoryContents{} + err = proto.UnmarshalOptions{Resolver: typeRegistry}.Unmarshal(decoded, repoContents) + if err != nil { + return nil, errors.Wrap(err, "full unmarshal") + } + return repoContents, nil +} diff --git a/pkg/sync/ts.go b/pkg/sync/ts.go index aed090ce..05a67d14 100644 --- a/pkg/sync/ts.go +++ b/pkg/sync/ts.go @@ -16,16 +16,14 @@ package sync import ( "context" - "encoding/base64" "os/exec" "path/filepath" "strings" "github.com/lekkodev/cli/pkg/gen" "github.com/lekkodev/cli/pkg/native" - protoutils "github.com/lekkodev/cli/pkg/proto" + "github.com/lekkodev/cli/pkg/repo" "github.com/pkg/errors" - "google.golang.org/protobuf/proto" featurev1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/feature/v1beta1" ) @@ -56,7 +54,15 @@ func SyncTS(lekkoPath string) (*featurev1beta1.RepositoryContents, error) { if err != nil { return nil, errors.Wrapf(err, "sync ts: %s", output) } +<<<<<<< HEAD return SyncTSOutputToRepositoryContents(output) +======= + repoContents, err := repo.DecodeRepositoryContents(output) + if err != nil { + return nil, errors.Wrap(err, "decode sync ts output") + } + return repoContents, nil +>>>>>>> origin/main } func SyncTSFiles(lekkoFiles ...string) (*featurev1beta1.RepositoryContents, error) { @@ -66,6 +72,7 @@ func SyncTSFiles(lekkoFiles ...string) (*featurev1beta1.RepositoryContents, erro if err != nil { return nil, errors.Wrapf(err, "sync ts: %s", output) } +<<<<<<< HEAD return SyncTSOutputToRepositoryContents(output) } @@ -75,24 +82,11 @@ func SyncTSOutputToRepositoryContents(output []byte) (*featurev1beta1.Repository // The RepositoryContents message contains the FDS which we want to use as the resolver for unmarshalling // the rest of the contents. decoded, err := base64.StdEncoding.DecodeString(string(output)) +======= + repoContents, err := repo.DecodeRepositoryContents(output) +>>>>>>> origin/main if err != nil { return nil, errors.Wrap(err, "decode sync ts output") } - // First pass unmarshal to get the FDS while ignoring any unresolvable Anys - tempRepoContents := &featurev1beta1.RepositoryContents{} - err = proto.UnmarshalOptions{DiscardUnknown: true, Resolver: &protoutils.IgnoreAnyResolver{}}.Unmarshal(decoded, tempRepoContents) - if err != nil { - return nil, errors.Wrap(err, "initial unmarshal sync ts output") - } - typeRegistry, err := protoutils.FileDescriptorSetToTypeRegistry(tempRepoContents.FileDescriptorSet) - if err != nil { - return nil, errors.Wrap(err, "get type registry") - } - // Re-unmarshal using type registry this time - we can resolve Anys correctly - repoContents := &featurev1beta1.RepositoryContents{} - err = proto.UnmarshalOptions{Resolver: typeRegistry}.Unmarshal(decoded, repoContents) - if err != nil { - return nil, errors.Wrap(err, "final unmarshal sync ts output") - } return repoContents, nil } diff --git a/pkg/team/team.go b/pkg/team/team.go index 7937d37b..7b0b00c4 100644 --- a/pkg/team/team.go +++ b/pkg/team/team.go @@ -18,9 +18,9 @@ import ( "context" "strings" - bffv1beta1connect "buf.build/gen/go/lekkodev/cli/bufbuild/connect-go/lekko/bff/v1beta1/bffv1beta1connect" + bffv1beta1connect "buf.build/gen/go/lekkodev/cli/connectrpc/go/lekko/bff/v1beta1/bffv1beta1connect" bffv1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/bff/v1beta1" - connect_go "github.com/bufbuild/connect-go" + "connectrpc.com/connect" "github.com/pkg/errors" ) @@ -49,7 +49,7 @@ func (t *Team) Show(ts TeamStore) string { } func (t *Team) Use(ctx context.Context, team string, wts WriteTeamStore) error { - if _, err := t.lekkoBFFClient.UseTeam(ctx, connect_go.NewRequest(&bffv1beta1.UseTeamRequest{ + if _, err := t.lekkoBFFClient.UseTeam(ctx, connect.NewRequest(&bffv1beta1.UseTeamRequest{ Name: team, })); err != nil { return errors.Wrap(err, "use team") @@ -91,7 +91,7 @@ type TeamMembership struct { } func (t *Team) List(ctx context.Context) ([]*TeamMembership, error) { - resp, err := t.lekkoBFFClient.ListUserMemberships(ctx, connect_go.NewRequest(&bffv1beta1.ListUserMembershipsRequest{})) + resp, err := t.lekkoBFFClient.ListUserMemberships(ctx, connect.NewRequest(&bffv1beta1.ListUserMembershipsRequest{})) if err != nil { return nil, errors.Wrap(err, "list team memberships") } @@ -131,7 +131,7 @@ func roleToProto(role MemberRole) bffv1beta1.MembershipRole { } func (t *Team) Create(ctx context.Context, name, domainName string, wts WriteTeamStore) error { - if _, err := t.lekkoBFFClient.CreateTeam(ctx, connect_go.NewRequest(&bffv1beta1.CreateTeamRequest{ + if _, err := t.lekkoBFFClient.CreateTeam(ctx, connect.NewRequest(&bffv1beta1.CreateTeamRequest{ Name: name, DomainName: domainName, })); err != nil { @@ -142,7 +142,7 @@ func (t *Team) Create(ctx context.Context, name, domainName string, wts WriteTea } func (t *Team) AddMember(ctx context.Context, email string, role MemberRole) error { - if _, err := t.lekkoBFFClient.UpsertMembership(ctx, connect_go.NewRequest(&bffv1beta1.UpsertMembershipRequest{ + if _, err := t.lekkoBFFClient.UpsertMembership(ctx, connect.NewRequest(&bffv1beta1.UpsertMembershipRequest{ Username: email, Role: roleToProto(role), })); err != nil { @@ -152,7 +152,7 @@ func (t *Team) AddMember(ctx context.Context, email string, role MemberRole) err } func (t *Team) ListMemberships(ctx context.Context) ([]*TeamMembership, error) { - resp, err := t.lekkoBFFClient.ListTeamMemberships(ctx, connect_go.NewRequest(&bffv1beta1.ListTeamMembershipsRequest{})) + resp, err := t.lekkoBFFClient.ListTeamMemberships(ctx, connect.NewRequest(&bffv1beta1.ListTeamMembershipsRequest{})) if err != nil { return nil, errors.Wrap(err, "list team memberships") } @@ -164,7 +164,7 @@ func (t *Team) ListMemberships(ctx context.Context) ([]*TeamMembership, error) { } func (t *Team) RemoveMember(ctx context.Context, email string) error { - if _, err := t.lekkoBFFClient.RemoveMembership(ctx, connect_go.NewRequest(&bffv1beta1.RemoveMembershipRequest{ + if _, err := t.lekkoBFFClient.RemoveMembership(ctx, connect.NewRequest(&bffv1beta1.RemoveMembershipRequest{ Username: email, })); err != nil { return errors.Wrap(err, "remove membership") @@ -173,7 +173,7 @@ func (t *Team) RemoveMember(ctx context.Context, email string) error { } func (t *Team) Delete(ctx context.Context, name string) error { - if _, err := t.lekkoBFFClient.DeleteTeam(ctx, connect_go.NewRequest(&bffv1beta1.DeleteTeamRequest{ + if _, err := t.lekkoBFFClient.DeleteTeam(ctx, connect.NewRequest(&bffv1beta1.DeleteTeamRequest{ TeamName: name, })); err != nil { return errors.Wrap(err, "delete team") diff --git a/proto/lekko/bff/v1beta1/bff.proto b/proto/lekko/bff/v1beta1/bff.proto index b2971c99..d8cf25c2 100644 --- a/proto/lekko/bff/v1beta1/bff.proto +++ b/proto/lekko/bff/v1beta1/bff.proto @@ -74,6 +74,8 @@ service BFFService { rpc GetFeature(GetFeatureRequest) returns (GetFeatureResponse) {} rpc GetFeatureHistory(GetFeatureHistoryRequest) returns (GetFeatureHistoryResponse) {} rpc GetRepositoryContents(GetRepositoryContentsRequest) returns (GetRepositoryContentsResponse) {} + // Gets native lang code for a specific namespace. + rpc GetNativeLang(GetNativeLangRequest) returns (GetNativeLangResponse) {} rpc AddNamespace(AddNamespaceRequest) returns (AddNamespaceResponse) {} rpc RemoveNamespace(RemoveNamespaceRequest) returns (RemoveNamespaceResponse) {} rpc AddFeature(AddFeatureRequest) returns (AddFeatureResponse) {} @@ -84,6 +86,8 @@ service BFFService { rpc Save(SaveRequest) returns (SaveResponse) {} // Saves the raw starlark of a feature to the local repo, and runs compilation. rpc SaveStarlark(SaveStarlarkRequest) returns (SaveStarlarkResponse) {} + // Saves changes to a namespace using native lang code and runs compilation. + rpc SaveNativeLang(SaveNativeLangRequest) returns (SaveNativeLangResponse) {} // Helper for Rules AST -> String. rpc ConvertRuleToString(ConvertRuleToStringRequest) returns (ConvertRuleToStringResponse) {} @@ -450,6 +454,17 @@ message GetRepositoryContentsResponse { google.protobuf.FileDescriptorSet file_descriptor_set = 4; } +message GetNativeLangRequest { + BranchKey key = 1; + string namespace_name = 2; + string language = 3; +} + +message GetNativeLangResponse { + Branch branch = 1; + string code = 2; +} + message GetFeatureRequest { // includes the branch name to fetch from. If 'main', // we will fetch from mysql. If not, we will fetch @@ -795,6 +810,21 @@ message SaveStarlarkResponse { bool success = 5; } +message SaveNativeLangRequest { + BranchKey key = 1; + string namespace_name = 2; + string language = 3; + string code = 4; +} + +message SaveNativeLangResponse { + Branch branch = 1; + string formatted = 2; + // TODO: We probably want some kind of linting/checking endpoint that surfaces diagnostic errors + // if parsing native lang code fails, so that the editor can show in-code errors or suggest fixes. + // This could be part of SaveNativeLang with a dry_run-esque option or a separate endpoint. +} + message ConvertRuleToStringRequest { lekko.rules.v1beta3.Rule rule = 1; }