Skip to content

Commit

Permalink
feat: introduce module API
Browse files Browse the repository at this point in the history
Main file is no longer generated automatically - instead, we generate a
top-level module package which can be imported into a "unified" CLI
tool, if so desired.
  • Loading branch information
odsod committed Jul 28, 2022
1 parent 10339da commit 4d84dfe
Show file tree
Hide file tree
Showing 20 changed files with 629 additions and 526 deletions.
7 changes: 7 additions & 0 deletions .sage/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"os"

"go.einride.tech/sage/sg"
"go.einride.tech/sage/sgtool"
Expand All @@ -12,6 +13,7 @@ type Proto sg.Namespace

func (Proto) All(ctx context.Context) error {
sg.Deps(ctx, Proto.BufFormat, Proto.BufLint)
sg.Deps(ctx, Proto.CleanGeneratedProto)
sg.Deps(ctx, Proto.BufGenerateExample)
return nil
}
Expand Down Expand Up @@ -55,6 +57,11 @@ func (Proto) ProtocGenGoAIPCLI(ctx context.Context) error {
).Run()
}

func (Proto) CleanGeneratedProto(ctx context.Context) error {
sg.Logger(ctx).Println("cleaning generated proto files...")
return os.RemoveAll(sg.FromGitRoot("cmd", "examplectl", "gen"))
}

func (Proto) BufGenerateExample(ctx context.Context) error {
sg.Deps(ctx, Proto.ProtocGenGo, Proto.ProtocGenGoAIPCLI)
sg.Logger(ctx).Println("generating example proto stubs...")
Expand Down
63 changes: 63 additions & 0 deletions aipcli/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,61 @@ import (
"google.golang.org/protobuf/reflect/protoregistry"
)

const (
moduleNameAnnotation = "aip_cli_annotation_module_name"
serviceNameAnnotation = "aip_cli_annotation_service_name"
methodNameAnnotation = "aip_cli_annotation_method_name"
)

// NewMultiModuleCommand initializes a new *cobra.Command for multiple CLI modules.
func NewMultiModuleCommand(
name string,
moduleCmds ...*cobra.Command,
) *cobra.Command {
cmd := &cobra.Command{
Use: name,
}
// TODO: Set custom help function.
for _, moduleCmd := range moduleCmds {
cmd.AddCommand(moduleCmd)
}
return cmd
}

// NewModuleCommand initializes a new *cobra.Command for a CLI module.
// A module is a collection of services with a common CLI config.
func NewModuleCommand(
name string,
config *Config,
serviceCmds ...*cobra.Command,
) *cobra.Command {
cmd := &cobra.Command{
Use: name,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
cmd.SetContext(WithConfig(cmd.Context(), config))
},
Annotations: map[string]string{
moduleNameAnnotation: name,
},
}
// Deduplicate service commands.
serviceNames := make(map[protoreflect.Name][]protoreflect.FullName, len(serviceCmds))
for _, serviceCmd := range serviceCmds {
if serviceName, ok := serviceCmd.Annotations[serviceNameAnnotation]; ok {
fullName := protoreflect.FullName(serviceName)
serviceNames[fullName.Name()] = append(serviceNames[fullName.Name()], fullName)
}
}
// TODO: Set custom help function.
for _, serviceCmd := range serviceCmds {
if serviceName, ok := serviceCmd.Annotations[serviceNameAnnotation]; ok {
serviceCmd.Use = getServiceCommandUse(serviceNames, protoreflect.FullName(serviceName))
cmd.AddCommand(serviceCmd)
}
}
return cmd
}

// NewServiceCommand initializes a new *cobra.Command for the provided gRPC service.
func NewServiceCommand(
service protoreflect.ServiceDescriptor,
Expand All @@ -30,7 +85,11 @@ func NewServiceCommand(
Use: serviceUse(service),
Short: initialUpperCase(trimComment(comments[service.FullName()])),
Long: comments[service.FullName()],
Annotations: map[string]string{
serviceNameAnnotation: string(service.FullName()),
},
}
// TODO: Set custom help function.
return cmd
}

Expand All @@ -45,7 +104,11 @@ func NewMethodCommand(
Use: methodUse(method),
Short: initialUpperCase(trimComment(comments[method.FullName()])),
Long: comments[method.FullName()],
Annotations: map[string]string{
methodNameAnnotation: string(method.FullName()),
},
}
// TODO: Set custom help function.
fromFile := cmd.Flags().String("from-file", "", "path to a JSON file containing the request payload")
_ = cmd.MarkFlagFilename("from-file", "json")
setFlags(comments, cmd, nil, in.ProtoReflect().Descriptor(), in.ProtoReflect)
Expand Down
57 changes: 57 additions & 0 deletions aipcli/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package aipcli

import (
"strings"

"github.com/stoewer/go-strcase"
"google.golang.org/protobuf/reflect/protoreflect"
)

func getServiceCommandUse(
servicesByName map[protoreflect.Name][]protoreflect.FullName,
service protoreflect.FullName,
) string {
var serviceName string
if services := servicesByName[service.Name()]; len(services) > 1 {
serviceName = strings.TrimSuffix(string(service), "Service")
serviceName = strings.TrimPrefix(serviceName, string(longestCommonParent(services)))
serviceName = strings.TrimPrefix(serviceName, ".")
serviceName = strings.Join(reverse(strings.Split(serviceName, ".")), "-")
} else {
serviceName = strings.TrimSuffix(string(service.Name()), "Service")
}
return strcase.KebabCase(serviceName)
}

// longestCommonParent returns the longest common parent of multiple services.
func longestCommonParent(fullNames []protoreflect.FullName) protoreflect.FullName {
if len(fullNames) == 0 {
return ""
}
var i int
var result protoreflect.FullName
ResultLoop:
for {
parts := strings.Split(string(fullNames[0]), ".")
if i > len(parts) {
break
}
candidate := strings.Join(parts[:i], ".")
for _, fullName := range fullNames {
if !strings.HasPrefix(string(fullName), candidate) {
break ResultLoop
}
}
result = protoreflect.FullName(candidate)
i++
}
return result
}

func reverse(ss []string) []string {
last := len(ss) - 1
for i := 0; i < len(ss)/2; i++ {
ss[i], ss[last-i] = ss[last-i], ss[i]
}
return ss
}
Loading

0 comments on commit 4d84dfe

Please sign in to comment.