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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ Command `ignore: true` removes a generated command. Command `hidden: true`
keeps it generated but hidden from normal help and catalog output unless
`--include-hidden` is used. Parameter `hidden: true` is a legacy alias for
`deprecated: true`; prefer `deprecated: true` for parameter overlays.
Use `match.method` and `match.path` to scope an override when duplicate upstream
operation IDs generate the same command name.
Bulk pagination defaults fill matching command params that have no spec default.
Explicit `commands.<use>.params.<name>.default` still wins when a command needs a
different value. Parameter `required: true` marks an existing generated flag as
Expand Down
32 changes: 31 additions & 1 deletion internal/codegen/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func validateCommandPaths(specs []runtime.CommandSpec) error {
}
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))
return fmt.Errorf("command path %q conflicts between %s and %s", cmdPath, commandDebugIdentity(prev), commandDebugIdentity(spec))
}
seen[cmdPath] = spec
}
Expand All @@ -158,6 +158,23 @@ func commandIdentity(spec runtime.CommandSpec) string {
return spec.PathTpl
}

func commandDebugIdentity(spec runtime.CommandSpec) string {
var parts []string
if spec.OperationID != "" {
parts = append(parts, fmt.Sprintf("operationId=%q", spec.OperationID))
}
if spec.Method != "" || spec.PathTpl != "" {
parts = append(parts, fmt.Sprintf("http=%q", strings.TrimSpace(spec.Method+" "+spec.PathTpl)))
}
if spec.Group != "" || spec.Use != "" {
parts = append(parts, fmt.Sprintf("command=%q", strings.TrimSpace(spec.Group+" "+spec.Use)))
}
if len(parts) == 0 {
return fmt.Sprintf("%q", commandIdentity(spec))
}
return strings.Join(parts, ", ")
}

func rootCommandName(use string) string {
fields := strings.Fields(strings.ToLower(use))
if len(fields) == 0 {
Expand All @@ -183,6 +200,9 @@ func MergeOverlayModule(specs []runtime.CommandSpec, mod overlay.Module) []runti
var merged []runtime.CommandSpec
for _, s := range specs {
o, ok := mod.Commands[s.Use]
if ok && !overrideMatches(s, o) {
ok = false
}
if ok && o.Ignore {
continue
}
Expand All @@ -198,6 +218,16 @@ func MergeOverlayModule(specs []runtime.CommandSpec, mod overlay.Module) []runti
return merged
}

func overrideMatches(spec runtime.CommandSpec, override overlay.Override) bool {
if override.Match.Method != "" && !strings.EqualFold(override.Match.Method, spec.Method) {
return false
}
if override.Match.Path != "" && override.Match.Path != spec.PathTpl {
return false
}
return true
}

func cloneCommandSpec(spec runtime.CommandSpec) runtime.CommandSpec {
cloned := spec
cloned.Aliases = append([]string(nil), spec.Aliases...)
Expand Down
21 changes: 21 additions & 0 deletions internal/codegen/render/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,27 @@ func TestMergeOverlay_UseRename(t *testing.T) {
}
}

func TestMergeOverlay_PathScopedRename(t *testing.T) {
specs := []runtime.CommandSpec{
{Group: "GatewayService", Use: "create-service", OperationID: "GatewayService_CreateService", Method: "POST", PathTpl: "/apis/v1alpha1/services"},
{Group: "GatewayService", Use: "create-service", OperationID: "GatewayService_CreateService", Method: "POST", PathTpl: "/apis/v1alpha2/services"},
}
_, err := ResolveFlatCommandPath("namespaced", 1, specs)
if err == nil || !strings.Contains(err.Error(), "/apis/v1alpha1/services") || !strings.Contains(err.Error(), "/apis/v1alpha2/services") {
t.Fatalf("conflict error = %v", err)
}

merged := MergeOverlay(specs, map[string]overlay.Override{
"create-service": {Match: overlay.OperationMatch{Method: "POST", Path: "/apis/v1alpha2/services"}, Use: "create-service-v1alpha2"},
})
if merged[0].Use != "create-service" || merged[1].Use != "create-service-v1alpha2" {
t.Fatalf("uses = %q, %q", merged[0].Use, merged[1].Use)
}
if _, err := ResolveFlatCommandPath("namespaced", 1, merged); err != nil {
t.Fatalf("renamed command should not conflict: %v", err)
}
}

func TestMergeOverlayModule_BulkPaginationDefaults(t *testing.T) {
specs := []runtime.CommandSpec{
{
Expand Down
6 changes: 6 additions & 0 deletions internal/overlay/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

type Override struct {
Use string `yaml:"use"`
Match OperationMatch `yaml:"match"`
Aliases []string `yaml:"aliases"`
Shortcuts []Shortcut `yaml:"shortcuts"`
Short string `yaml:"short"`
Expand All @@ -26,6 +27,11 @@ type Override struct {
KnownErrors []KnownError `yaml:"known_errors"`
}

type OperationMatch struct {
Method string `yaml:"method"`
Path string `yaml:"path"`
}

type Example struct {
Summary string `yaml:"summary"`
Command string `yaml:"command"`
Expand Down
6 changes: 6 additions & 0 deletions internal/overlay/overlay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ func TestLoadDir_ParsesExtendedFields(t *testing.T) {
pageSize: "20"
commands:
create-user:
match:
method: POST
path: /users
group: "Identity"
hidden: true
notes:
Expand Down Expand Up @@ -138,6 +141,9 @@ commands:
if cu.Group != "Identity" {
t.Errorf("group = %q, want Identity", cu.Group)
}
if cu.Match.Method != "POST" || cu.Match.Path != "/users" {
t.Errorf("match = %#v", cu.Match)
}
if cu.Hidden == nil || !*cu.Hidden {
t.Errorf("hidden = %v, want true", cu.Hidden)
}
Expand Down