Skip to content

Commit

Permalink
Support filtering by tags in client generation
Browse files Browse the repository at this point in the history
  • Loading branch information
nitsanshai authored and eandre committed Oct 3, 2024
1 parent 91a040d commit d1afc74
Show file tree
Hide file tree
Showing 13 changed files with 658 additions and 691 deletions.
29 changes: 18 additions & 11 deletions cli/cmd/encore/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ func init() {
rootCmd.AddCommand(genCmd)

var (
output string
lang string
envName string
genServiceNames []string
excludedServices []string
output string
lang string
envName string
genServiceNames []string
excludedServices []string
endpointTags []string
excludedEndpointTags []string
)

genClientCmd := &cobra.Command{
Use: "client [<app-id>] [--env=<name>] [--services=foo,bar] [--excluded-services=baz,qux]",
Use: "client [<app-id>] [--env=<name>] [--services=foo,bar] [--excluded-services=baz,qux] [--tags=cache,mobile] [--excluded-tags=internal]",
Short: "Generates an API client for your app",
Long: `Generates an API client for your app.
Expand Down Expand Up @@ -84,11 +86,13 @@ To further narrow down the services to generate, use the '--services' flag.
genServiceNames = []string{"*"}
}
resp, err := daemon.GenClient(ctx, &daemonpb.GenClientRequest{
AppId: appID,
EnvName: envName,
Lang: lang,
Services: genServiceNames,
ExcludedServices: excludedServices,
AppId: appID,
EnvName: envName,
Lang: lang,
Services: genServiceNames,
ExcludedServices: excludedServices,
EndpointTags: endpointTags,
ExcludedEndpointTags: excludedEndpointTags,
})
if err != nil {
fatal(err)
Expand Down Expand Up @@ -151,4 +155,7 @@ which may require the user-facing wrapper code to be manually generated.`,

genClientCmd.Flags().StringSliceVarP(&genServiceNames, "services", "s", nil, "The names of the services to include in the output")
genClientCmd.Flags().StringSliceVarP(&excludedServices, "excluded-services", "x", nil, "The names of the services to exclude in the output")
genClientCmd.Flags().StringSliceVarP(&endpointTags, "tags", "t", nil, "The names of endpoint tags to include in the output")
genClientCmd.Flags().
StringSliceVar(&excludedEndpointTags, "excluded-tags", nil, "The names of endpoint tags to exclude in the output")
}
3 changes: 2 additions & 1 deletion cli/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ func (s *Server) GenClient(ctx context.Context, params *daemonpb.GenClientReques
lang := clientgen.Lang(params.Lang)

servicesToGenerate := clientgentypes.NewServiceSet(md, params.Services, params.ExcludedServices)
code, err := clientgen.Client(lang, params.AppId, md, servicesToGenerate)
tagsToInclude := clientgentypes.NewTagSet(params.EndpointTags, params.ExcludedEndpointTags)
code, err := clientgen.Client(lang, params.AppId, md, servicesToGenerate, tagsToInclude)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/echo_app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func doTestEndToEndWithApp(t *testing.T, env []string) {
clientgen.LangJavascript: "js/client.js",
} {
services := clientgentypes.AllServices(app.Meta)
client, err := clientgen.Client(lang, "slug", app.Meta, services)
client, err := clientgen.Client(lang, "slug", app.Meta, services, clientgentypes.TagSet{})
if err != nil {
fmt.Println(err.Error())
c.FailNow()
Expand Down
9 changes: 8 additions & 1 deletion internal/clientgen/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ func Detect(path string) (lang Lang, ok bool) {
// Client generates an API client based on the given app metadata.
// ServiceNames are the services to include in the output.
// If it's nil, all services are included.
func Client(lang Lang, appSlug string, md *meta.Data, services clientgentypes.ServiceSet) (code []byte, err error) {
func Client(
lang Lang,
appSlug string,
md *meta.Data,
services clientgentypes.ServiceSet,
tags clientgentypes.TagSet,
) (code []byte, err error) {
defer func() {
if e := recover(); e != nil {
err = srcerrors.UnhandledPanic(e)
Expand All @@ -79,6 +85,7 @@ func Client(lang Lang, appSlug string, md *meta.Data, services clientgentypes.Se
AppSlug: appSlug,
Meta: md,
Services: services,
Tags: tags,
}

if err := gen.Generate(params); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/clientgen/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestClientCodeGenerationFromGoApp(t *testing.T) {
c.Assert(ok, qt.IsTrue, qt.Commentf("Unable to detect language type for %s", file.Name()))

services := clientgentypes.AllServices(res.Meta)
generatedClient, err := Client(language, "app", res.Meta, services)
generatedClient, err := Client(language, "app", res.Meta, services, clientgentypes.TagSet{})
c.Assert(err, qt.IsNil)

golden.TestAgainst(c, "goapp/"+file.Name(), string(generatedClient))
Expand Down Expand Up @@ -133,7 +133,7 @@ func TestClientCodeGenerationFromTSApp(t *testing.T) {
c.Assert(ok, qt.IsTrue, qt.Commentf("Unable to detect language type for %s", file.Name()))

services := clientgentypes.AllServices(res.Meta)
generatedClient, err := Client(language, "app", res.Meta, services)
generatedClient, err := Client(language, "app", res.Meta, services, clientgentypes.TagSet{})
c.Assert(err, qt.IsNil)

golden.TestAgainst(c, "tsapp/"+file.Name(), string(generatedClient))
Expand Down
51 changes: 51 additions & 0 deletions internal/clientgen/clientgentypes/clientgentypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type GenerateParams struct {
AppSlug string
Meta *meta.Data
Services ServiceSet
Tags TagSet
}

type ServiceSet struct {
Expand Down Expand Up @@ -63,3 +64,53 @@ func NewServiceSet(md *meta.Data, include, exclude []string) ServiceSet {
func AllServices(md *meta.Data) ServiceSet {
return NewServiceSet(md, []string{"*"}, nil)
}

type TagSet struct {
includedTags []string
excludedTags []string
}

func NewTagSet(tags, excludedTags []string) TagSet {
filteredTags := make([]string, 0, len(tags))
for _, t := range tags {
if !slices.Contains(excludedTags, t) {
filteredTags = append(filteredTags, t)
}
}

return TagSet{
includedTags: filteredTags,
excludedTags: excludedTags,
}
}

func (t TagSet) IsRPCIncluded(rpc *meta.RPC) bool {
// First check if the RPC has any of the excluded tags.
for _, selector := range rpc.Tags {
if selector.Type != meta.Selector_TAG {
continue
}

if slices.Contains(t.excludedTags, selector.Value) {
return false
}
}

// If `tags` is empty, all tags are included.
if len(t.includedTags) == 0 {
return true
}

// Check if the RPC has any of the included tags.
for _, selector := range rpc.Tags {
if selector.Type != meta.Selector_TAG {
continue
}

if slices.Contains(t.includedTags, selector.Value) {
return true
}
}

return false
}
6 changes: 3 additions & 3 deletions internal/clientgen/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (g *golang) Generate(p clientgentypes.GenerateParams) (err error) {
seenNs[nsName] = true

if hasPublicRPC(service) && p.Services.Has(service.Name) {
if err := g.generateServiceClient(file, service); err != nil {
if err := g.generateServiceClient(file, service, p.Tags); err != nil {
return errors.Wrapf(err, "unable to generate service client for service: %s", service)
}
}
Expand Down Expand Up @@ -322,7 +322,7 @@ func (g *golang) generateOptionFunc(file *File, optionName string, doc string, p
)
}

func (g *golang) generateServiceClient(file *File, service *meta.Service) error {
func (g *golang) generateServiceClient(file *File, service *meta.Service, tags clientgentypes.TagSet) error {
name := g.cleanServiceName(service)
interfaceName := fmt.Sprintf("%sClient", name)
structName := fmt.Sprintf("%sClient", strings.ToLower(name))
Expand All @@ -332,7 +332,7 @@ func (g *golang) generateServiceClient(file *File, service *meta.Service) error
file.Comment("It is setup as an interface allowing you to use GoMock to create mock implementations during tests.")
var interfaceMethods []Code
for _, rpc := range service.Rpcs {
if rpc.AccessType == meta.RPC_PRIVATE {
if rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) {
continue
}

Expand Down
6 changes: 3 additions & 3 deletions internal/clientgen/javascript.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (js *javascript) Generate(p clientgentypes.GenerateParams) (err error) {
seenNs := make(map[string]bool)
js.writeClient(p.Services)
for _, svc := range p.Meta.Svcs {
if err := js.writeService(svc, p.Services); err != nil {
if err := js.writeService(svc, p.Services, p.Tags); err != nil {
return err
}
seenNs[svc.Name] = true
Expand All @@ -106,7 +106,7 @@ func (js *javascript) Generate(p clientgentypes.GenerateParams) (err error) {
return nil
}

func (js *javascript) writeService(svc *meta.Service, set clientgentypes.ServiceSet) error {
func (js *javascript) writeService(svc *meta.Service, set clientgentypes.ServiceSet, tags clientgentypes.TagSet) error {
// Determine if we have anything worth exposing.
// Either a public RPC or a named type.
isIncluded := hasPublicRPC(svc) && set.Has(svc.Name)
Expand Down Expand Up @@ -135,7 +135,7 @@ func (js *javascript) writeService(svc *meta.Service, set clientgentypes.Service

// RPCs
for _, rpc := range svc.Rpcs {
if rpc.AccessType == meta.RPC_PRIVATE {
if rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) {
continue
}

Expand Down
9 changes: 7 additions & 2 deletions internal/clientgen/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (g *Generator) Generate(p clientgentypes.GenerateParams) (err error) {

for _, svc := range p.Meta.Svcs {
if p.Services.Has(svc.Name) {
if err := g.addService(svc); err != nil {
if err := g.addService(svc, p.Tags); err != nil {
return err
}
}
Expand All @@ -76,13 +76,18 @@ func (g *Generator) Generate(p clientgentypes.GenerateParams) (err error) {
return json.Indent(p.Buf, out, "", " ")
}

func (g *Generator) addService(svc *meta.Service) error {
func (g *Generator) addService(svc *meta.Service, tags clientgentypes.TagSet) error {
for _, rpc := range svc.Rpcs {
// streaming endpoints not supported yet
if rpc.StreamingRequest || rpc.StreamingResponse {
continue
}

// Skip RPCs that don't match the tags
if !tags.IsRPCIncluded(rpc) {
continue
}

if err := g.addRPC(rpc); err != nil {
return err
}
Expand Down
6 changes: 3 additions & 3 deletions internal/clientgen/typescript.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (ts *typescript) Generate(p clientgentypes.GenerateParams) (err error) {
seenNs := make(map[string]bool)
ts.writeClient(p.Services)
for _, svc := range p.Meta.Svcs {
if err := ts.writeService(svc, p.Services); err != nil {
if err := ts.writeService(svc, p.Services, p.Tags); err != nil {
return err
}
seenNs[svc.Name] = true
Expand All @@ -113,7 +113,7 @@ func (ts *typescript) Generate(p clientgentypes.GenerateParams) (err error) {
return nil
}

func (ts *typescript) writeService(svc *meta.Service, p clientgentypes.ServiceSet) error {
func (ts *typescript) writeService(svc *meta.Service, p clientgentypes.ServiceSet, tags clientgentypes.TagSet) error {
// Determine if we have anything worth exposing.
// Either a public RPC or a named type.
isIncluded := hasPublicRPC(svc) && p.Has(svc.Name)
Expand Down Expand Up @@ -165,7 +165,7 @@ func (ts *typescript) writeService(svc *meta.Service, p clientgentypes.ServiceSe

// RPCs
for _, rpc := range svc.Rpcs {
if rpc.AccessType == meta.RPC_PRIVATE {
if rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) {
continue
}

Expand Down
Loading

0 comments on commit d1afc74

Please sign in to comment.