[object Object]

← back to Cli Printing Press

feat(generator): add CRUD aliases to generated CLI commands

5b3d281e58a07552df27881e61f15dabe5a88585 · 2026-03-24 16:21:10 -0700 · Matt Van Horn

Computes list/get/create/update/delete aliases from HTTP method + path
patterns. Generated CLIs now support both operationId names and friendly
aliases (e.g., `pet list` alongside `pet find-by-status`).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 5b3d281e58a07552df27881e61f15dabe5a88585
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Tue Mar 24 16:21:10 2026 -0700

    feat(generator): add CRUD aliases to generated CLI commands
    
    Computes list/get/create/update/delete aliases from HTTP method + path
    patterns. Generated CLIs now support both operationId names and friendly
    aliases (e.g., `pet list` alongside `pet find-by-status`).
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 internal/generator/templates/command.go.tmpl |  3 +
 internal/openapi/parser.go                   | 92 ++++++++++++++++++++++++++++
 internal/spec/spec.go                        |  1 +
 3 files changed, 96 insertions(+)

diff --git a/internal/generator/templates/command.go.tmpl b/internal/generator/templates/command.go.tmpl
index 6120a2ca..9b43f26e 100644
--- a/internal/generator/templates/command.go.tmpl
+++ b/internal/generator/templates/command.go.tmpl
@@ -39,6 +39,9 @@ func new{{camel $.FuncPrefix}}{{camel $eName}}Cmd(flags *rootFlags) *cobra.Comma
 
 	cmd := &cobra.Command{
 		Use:   "{{$eName}}{{positionalArgs $endpoint}}",
+{{- if $endpoint.Alias}}
+		Aliases: []string{"{{$endpoint.Alias}}"},
+{{- end}}
 		Short: "{{oneline $endpoint.Description}}",
 		Example: "{{exampleLine $.CommandPath $eName $endpoint}}",
 		RunE: func(cmd *cobra.Command, args []string) error {
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index 51bf7031..bca323bb 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -393,9 +393,101 @@ func mapResources(doc *openapi3.T, out *spec.APISpec, basePath string) {
 		out.Resources[primaryName] = resource
 	}
 
+	assignEndpointAliases(out.Resources)
 	filterGlobalParams(out.Resources)
 }
 
+func assignEndpointAliases(resources map[string]spec.Resource) {
+	resourceNames := make([]string, 0, len(resources))
+	for name := range resources {
+		resourceNames = append(resourceNames, name)
+	}
+	sort.Strings(resourceNames)
+
+	for _, name := range resourceNames {
+		resource := resources[name]
+		assignAliasesInResource(&resource)
+		resources[name] = resource
+	}
+}
+
+func assignAliasesInResource(resource *spec.Resource) {
+	if resource == nil {
+		return
+	}
+
+	assignAliasesForEndpoints(resource.Endpoints)
+
+	subNames := make([]string, 0, len(resource.SubResources))
+	for name := range resource.SubResources {
+		subNames = append(subNames, name)
+	}
+	sort.Strings(subNames)
+
+	for _, name := range subNames {
+		subResource := resource.SubResources[name]
+		assignAliasesInResource(&subResource)
+		resource.SubResources[name] = subResource
+	}
+}
+
+func assignAliasesForEndpoints(endpoints map[string]spec.Endpoint) {
+	if len(endpoints) == 0 {
+		return
+	}
+
+	endpointNames := make([]string, 0, len(endpoints))
+	nameSet := make(map[string]struct{}, len(endpoints))
+	for name := range endpoints {
+		endpointNames = append(endpointNames, name)
+		nameSet[name] = struct{}{}
+	}
+	sort.Strings(endpointNames)
+
+	usedAliases := map[string]struct{}{}
+	for _, name := range endpointNames {
+		endpoint := endpoints[name]
+		alias := computeAlias(endpoint.Method, endpoint.Path, name)
+		if alias == "" || alias == name {
+			endpoints[name] = endpoint
+			continue
+		}
+		if _, exists := nameSet[alias]; exists {
+			endpoints[name] = endpoint
+			continue
+		}
+		if _, used := usedAliases[alias]; used {
+			endpoints[name] = endpoint
+			continue
+		}
+
+		endpoint.Alias = alias
+		endpoints[name] = endpoint
+		usedAliases[alias] = struct{}{}
+	}
+}
+
+func computeAlias(method, path, endpointName string) string {
+	_ = endpointName
+
+	hasPathParam := strings.Contains(path, "{")
+	switch strings.ToUpper(method) {
+	case "GET":
+		if hasPathParam {
+			return "get"
+		}
+		return "list"
+	case "POST":
+		return "create"
+	case "PUT", "PATCH":
+		return "update"
+	case "DELETE":
+		return "delete"
+	default:
+		return ""
+	}
+}
+
 func filterGlobalParams(resources map[string]spec.Resource) {
 	totalEndpoints := 0
 	paramCounts := map[string]int{}
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index dbc83537..c7d98457 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -51,6 +51,7 @@ type Endpoint struct {
 	Response     ResponseDef `yaml:"response"`
 	Pagination   *Pagination `yaml:"pagination"`
 	ResponsePath string      `yaml:"response_path,omitempty"` // path to extract data array from response (e.g., "data", "results.items")
+	Alias        string      `yaml:"-"`                       // computed, not from YAML
 }
 
 type Param struct {

← 96f0d513 feat(cli): add --force flag to generate command  ·  back to Cli Printing Press  ·  fix(generator): doctor tries health endpoints before reporti 651d11e1 →