Skip to content
Merged
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
3 changes: 2 additions & 1 deletion examples/overlay/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# -sources specs/sources.yaml \
# -overlay internal/overlay
#
# The overlay layer rewrites help text and aliases for commands synthesized
# The overlay layer rewrites command names, help text, and aliases for commands synthesized
# from the upstream spec. The command's Use string matches CommandSpec.Use
# emitted by codegen (typically the lowercased operationId / rpc name).
# Overrides are baked into the generated CommandSpec at codegen time; the
Expand All @@ -27,6 +27,7 @@ commands: {}
# pageSize: "20"
# commands:
# create-user:
# use: create
# aliases: [adduser]
# short: "Create a user in the IAM service"
# long: |
Expand Down
41 changes: 41 additions & 0 deletions internal/codegen/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ func RenderModule(name, cliName string, specs []runtime.CommandSpec, overrides m
}

func ResolveFlatCommandPath(policy string, moduleCount int, specs []runtime.CommandSpec) (bool, error) {
if err := validateCommandPaths(specs); err != nil {
return false, err
}
if policy == "" {
policy = config.CommandPathAuto
}
Expand Down Expand Up @@ -111,6 +114,41 @@ func flatPathConflict(specs []runtime.CommandSpec) (string, bool) {
return "", false
}

func validateCommandPaths(specs []runtime.CommandSpec) error {
seen := map[string]runtime.CommandSpec{}
for _, spec := range specs {
group := rootCommandName(spec.Group)
use := commandUseName(spec.Use)
if group == "" || use == "" {
return fmt.Errorf("command %q has empty generated path", commandIdentity(spec))
}
cmdPath := group + " " + use
if prev, ok := seen[cmdPath]; ok {
return fmt.Errorf("command path %q conflicts between %q and %q", cmdPath, commandIdentity(prev), commandIdentity(spec))
}
seen[cmdPath] = spec
}
return nil
}

func commandUseName(use string) string {
fields := strings.Fields(use)
if len(fields) == 0 {
return ""
}
return fields[0]
}

func commandIdentity(spec runtime.CommandSpec) string {
if spec.OperationID != "" {
return spec.OperationID
}
if spec.Group != "" || spec.Use != "" {
return strings.TrimSpace(spec.Group + " " + spec.Use)
}
return spec.PathTpl
}

func rootCommandName(use string) string {
fields := strings.Fields(strings.ToLower(use))
if len(fields) == 0 {
Expand Down Expand Up @@ -192,6 +230,9 @@ func matchesAny(patterns []string, value string) bool {
}

func applyCommandOverride(spec *runtime.CommandSpec, override overlay.Override) {
if override.Use != "" {
spec.Use = override.Use
}
if override.Short != "" {
spec.Short = override.Short
}
Expand Down
29 changes: 29 additions & 0 deletions internal/codegen/render/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,24 @@ func TestMergeOverlay_ParamRequiredOverride(t *testing.T) {
}
}

func TestMergeOverlay_UseRename(t *testing.T) {
specs := []runtime.CommandSpec{{
Group: "Repos",
Use: "create-repo",
Aliases: []string{"new-repo"},
}}
merged := MergeOverlay(specs, map[string]overlay.Override{
"create-repo": {Use: "create", Aliases: []string{"new"}},
})

if got := merged[0].Use; got != "create" {
t.Fatalf("Use = %q, want create", got)
}
if !reflect.DeepEqual(merged[0].Aliases, []string{"new-repo", "new"}) {
t.Fatalf("aliases = %#v", merged[0].Aliases)
}
}

func TestMergeOverlayModule_BulkPaginationDefaults(t *testing.T) {
specs := []runtime.CommandSpec{
{
Expand Down Expand Up @@ -488,6 +506,17 @@ func TestResolveFlatCommandPath(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "conflicts") {
t.Fatalf("expected duplicate group flat conflict error, got %v", err)
}

renamed := MergeOverlay([]runtime.CommandSpec{
{Group: "Repos", Use: "create-repo", OperationID: "Repos_CreateRepo"},
{Group: "Repos", Use: "create", OperationID: "Repos_Create"},
}, map[string]overlay.Override{
"create-repo": {Use: "create"},
})
_, err = ResolveFlatCommandPath("namespaced", 1, renamed)
if err == nil || !strings.Contains(err.Error(), `command path "repos create" conflicts`) {
t.Fatalf("expected renamed command conflict error, got %v", err)
}
}

func TestRewriteCommandExamples_NormalizesMultiWordGroupPaths(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions internal/overlay/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
)

type Override struct {
Use string `yaml:"use"`
Aliases []string `yaml:"aliases"`
Short string `yaml:"short"`
Long string `yaml:"long"`
Expand Down
4 changes: 4 additions & 0 deletions internal/overlay/overlay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func TestLoadDir_ParsesMultipleModules(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "iam.yaml"), `commands:
create-user:
use: create
aliases: [adduser, new-user]
short: "Create a user"
long: "Long description for create-user."
Expand All @@ -49,6 +50,9 @@ func TestLoadDir_ParsesMultipleModules(t *testing.T) {
t.Fatalf("want 2 modules, got %d: %v", len(got), got)
}
u := got["iam"].Commands["create-user"]
if u.Use != "create" {
t.Errorf("iam create-user use: %q", u.Use)
}
if u.Short != "Create a user" || u.Long == "" || u.Example == "" {
t.Errorf("iam create-user override incomplete: %+v", u)
}
Expand Down