-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] Add command catalog discovery #3
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| package lathe | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/spf13/cobra" | ||
|
|
||
| "github.com/samzong/lathe/pkg/config" | ||
| "github.com/samzong/lathe/pkg/runtime" | ||
| ) | ||
|
|
||
| func commandsCmd(m *config.Manifest) *cobra.Command { | ||
| var jsonOut bool | ||
| var includeHidden bool | ||
| cmd := &cobra.Command{ | ||
| Use: "commands", | ||
| Short: "List generated commands", | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| catalog := runtime.BuildCatalog(cmd.Root(), catalogOptions(m, includeHidden)) | ||
| if jsonOut { | ||
| return writeJSON(cmd, catalog) | ||
| } | ||
| for i, entry := range catalog.Commands { | ||
| if i > 0 { | ||
| fmt.Fprintln(cmd.OutOrStdout()) | ||
| } | ||
| writeCommandSummary(cmd, entry) | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| addJSONFlag(cmd, &jsonOut, "Emit catalog JSON") | ||
| cmd.Flags().BoolVar(&includeHidden, "include-hidden", false, "Include hidden commands") | ||
| cmd.AddCommand(commandsShowCmd(m), commandsSchemaCmd()) | ||
| return cmd | ||
| } | ||
|
|
||
| func commandsShowCmd(m *config.Manifest) *cobra.Command { | ||
| var jsonOut bool | ||
| var includeHidden bool | ||
| cmd := &cobra.Command{ | ||
| Use: "show <path...>", | ||
| Short: "Show one generated command", | ||
| Args: cobra.MinimumNArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| entry, ok := runtime.FindCatalogCommand(cmd.Root(), args, catalogOptions(m, includeHidden)) | ||
| if !ok { | ||
| return fmt.Errorf("generated command not found: %s", strings.Join(args, " ")) | ||
| } | ||
| if jsonOut { | ||
| return writeJSON(cmd, entry) | ||
| } | ||
| writeCommandSummary(cmd, entry) | ||
| if len(entry.Flags) > 0 { | ||
| fmt.Fprintln(cmd.OutOrStdout()) | ||
| fmt.Fprintln(cmd.OutOrStdout(), "Flags:") | ||
| for _, flag := range entry.Flags { | ||
| required := "" | ||
| if flag.Required { | ||
| required = " required" | ||
| } | ||
| fmt.Fprintf(cmd.OutOrStdout(), " --%s %s%s %s\n", flag.Flag, flag.Type, required, flag.Help) | ||
| } | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| addJSONFlag(cmd, &jsonOut, "Emit command JSON") | ||
| cmd.Flags().BoolVar(&includeHidden, "include-hidden", false, "Include hidden commands") | ||
| return cmd | ||
| } | ||
|
|
||
| func commandsSchemaCmd() *cobra.Command { | ||
| var jsonOut bool | ||
| cmd := &cobra.Command{ | ||
| Use: "schema", | ||
| Short: "Print command catalog schema version", | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| data := map[string]int{"catalog_schema_version": runtime.CatalogSchemaVersion} | ||
| if jsonOut { | ||
| return writeJSON(cmd, data) | ||
| } | ||
| fmt.Fprintf(cmd.OutOrStdout(), "catalog schema %d\n", runtime.CatalogSchemaVersion) | ||
| return nil | ||
| }, | ||
| } | ||
| addJSONFlag(cmd, &jsonOut, "Emit schema JSON") | ||
| return cmd | ||
| } | ||
|
|
||
| func searchCmd(m *config.Manifest) *cobra.Command { | ||
| var jsonOut bool | ||
| var limit int | ||
| cmd := &cobra.Command{ | ||
| Use: "search <query>", | ||
| Short: "Search generated commands", | ||
| Args: cobra.MinimumNArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| query := strings.Join(args, " ") | ||
| results := runtime.SearchCatalog(cmd.Root(), query, runtime.SearchOptions{ | ||
| CatalogOptions: catalogOptions(m, false), | ||
| Limit: limit, | ||
| }) | ||
| if jsonOut { | ||
| return writeJSON(cmd, results) | ||
| } | ||
| for i, result := range results { | ||
| if i > 0 { | ||
| fmt.Fprintln(cmd.OutOrStdout()) | ||
| } | ||
| writeCommandSummary(cmd, result.Command) | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| addJSONFlag(cmd, &jsonOut, "Emit search results JSON") | ||
| cmd.Flags().IntVar(&limit, "limit", runtime.DefaultSearchLimit, "Maximum number of results") | ||
| return cmd | ||
| } | ||
|
|
||
| func catalogOptions(m *config.Manifest, includeHidden bool) runtime.CatalogOptions { | ||
| return runtime.CatalogOptions{ | ||
| CLIName: m.CLI.Name, | ||
| CLIVersion: Version, | ||
| IncludeHidden: includeHidden, | ||
| } | ||
| } | ||
|
|
||
| func writeCommandSummary(cmd *cobra.Command, entry runtime.CatalogCommand) { | ||
| fmt.Fprintln(cmd.OutOrStdout(), strings.Join(entry.Path, " ")) | ||
| if entry.Summary != "" { | ||
| fmt.Fprintf(cmd.OutOrStdout(), " %s\n", entry.Summary) | ||
| } | ||
| if entry.HTTP.Method != "" || entry.HTTP.PathTemplate != "" { | ||
| fmt.Fprintf(cmd.OutOrStdout(), " %s %s\n", entry.HTTP.Method, entry.HTTP.PathTemplate) | ||
| } | ||
| } | ||
|
|
||
| func writeJSON(cmd *cobra.Command, v any) error { | ||
| enc := json.NewEncoder(cmd.OutOrStdout()) | ||
| enc.SetIndent("", " ") | ||
| return enc.Encode(v) | ||
| } | ||
|
|
||
| func addJSONFlag(cmd *cobra.Command, target *bool, usage string) { | ||
| cmd.Flags().BoolVar(target, "json", false, usage) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| package lathe | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/spf13/cobra" | ||
|
|
||
| "github.com/samzong/lathe/pkg/config" | ||
| "github.com/samzong/lathe/pkg/runtime" | ||
| ) | ||
|
|
||
| func TestCommandsJSON_EmptyCatalog(t *testing.T) { | ||
| root := NewApp(testManifest()) | ||
| out, err := execute(root, "commands", "--json") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if !strings.Contains(out, `"commands": []`) { | ||
| t.Fatalf("output missing empty commands array:\n%s", out) | ||
| } | ||
| var catalog runtime.Catalog | ||
| if err := json.Unmarshal([]byte(out), &catalog); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if catalog.Commands == nil || len(catalog.Commands) != 0 { | ||
| t.Fatalf("commands = %#v", catalog.Commands) | ||
| } | ||
| } | ||
|
|
||
| func TestCommandsShowAndSearchJSON(t *testing.T) { | ||
| root := NewApp(testManifest()) | ||
| runtime.Build(root, "demo", []runtime.CommandSpec{{ | ||
| Group: "Users", | ||
| Use: "get-user", | ||
| Short: "Get a user", | ||
| OperationID: "getUser", | ||
| Method: "GET", | ||
| PathTpl: "/users/{id}", | ||
| Params: []runtime.ParamSpec{ | ||
| {Name: "id", Flag: "id", In: runtime.InPath, GoType: "string", Required: true, Help: "User id"}, | ||
| }, | ||
| }}) | ||
|
|
||
| out, err := execute(root, "commands", "show", "demo", "users", "get-user", "--json") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| var entry runtime.CatalogCommand | ||
| if err := json.Unmarshal([]byte(out), &entry); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if strings.Join(entry.Path, " ") != "demo users get-user" || entry.Group != "Users" { | ||
| t.Fatalf("entry = %+v", entry) | ||
| } | ||
|
|
||
| out, err = execute(root, "search", "getUser", "--json") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| var results []runtime.SearchResult | ||
| if err := json.Unmarshal([]byte(out), &results); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if len(results) != 1 || results[0].Command.Use != "get-user" { | ||
| t.Fatalf("results = %+v", results) | ||
| } | ||
| } | ||
|
|
||
| func TestCommandsShow_NotFound(t *testing.T) { | ||
| root := NewApp(testManifest()) | ||
| _, err := execute(root, "commands", "show", "demo", "users", "missing") | ||
| if err == nil { | ||
| t.Fatal("expected error") | ||
| } | ||
| } | ||
|
|
||
| func TestCommandsSchemaJSON(t *testing.T) { | ||
| root := NewApp(testManifest()) | ||
| out, err := execute(root, "commands", "schema", "--json") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| var data map[string]int | ||
| if err := json.Unmarshal([]byte(out), &data); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if data["catalog_schema_version"] != runtime.CatalogSchemaVersion { | ||
| t.Fatalf("schema = %d", data["catalog_schema_version"]) | ||
| } | ||
| } | ||
|
|
||
| func TestSearchExcludesHiddenCommands(t *testing.T) { | ||
| root := NewApp(testManifest()) | ||
| runtime.Build(root, "demo", []runtime.CommandSpec{{ | ||
| Group: "Users", | ||
| Use: "delete-user", | ||
| Short: "Delete a user", | ||
| Method: "DELETE", | ||
| PathTpl: "/users/{id}", | ||
| Hidden: true, | ||
| }}) | ||
|
|
||
| out, err := execute(root, "search", "delete", "--json") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| var results []runtime.SearchResult | ||
| if err := json.Unmarshal([]byte(out), &results); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if len(results) != 0 { | ||
| t.Fatalf("results = %+v", results) | ||
| } | ||
| } | ||
|
|
||
| func testManifest() *config.Manifest { | ||
| return &config.Manifest{CLI: config.CLIInfo{Name: "myctl", Short: "test cli", HostEnv: "MYCTL_HOST"}} | ||
| } | ||
|
|
||
| func execute(root *cobra.Command, args ...string) (string, error) { | ||
| var buf bytes.Buffer | ||
| root.SetOut(&buf) | ||
| root.SetErr(&buf) | ||
| root.SetArgs(args) | ||
| err := root.Execute() | ||
| return buf.String(), err | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,8 @@ func NewApp(m *config.Manifest) *cobra.Command { | |
| authCmd := auth.NewCommand(m) | ||
| authCmd.GroupID = authGroupID | ||
| cmd.AddCommand(authCmd) | ||
| cmd.AddCommand(commandsCmd(m)) | ||
| cmd.AddCommand(searchCmd(m)) | ||
|
Comment on lines
+41
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Adding fixed root commands Useful? React with 👍 / 👎. |
||
| cmd.AddCommand(versionCmd()) | ||
| return cmd | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
commandsThe parent
commandscommand defines local--json/--include-hiddenflags, andcommands showdefines flags with the same names again; when users place flags before the subcommand (for example,commands --json show ...orcommands --include-hidden show ...), Cobra consumes the parent flag value butshowreads its own separate variables, so the request is silently ignored. This makes valid-looking invocations return non-JSON output or fail to find hidden commands unexpectedly.Useful? React with 👍 / 👎.