← back to Cli Printing Press
fix(openapi): smart operationId cleaning for clean command names
84823431b7b711e71c96e6e648fa837bdf1d8755 · 2026-03-23 17:21:35 -0700 · Matt Van Horn
Strip api_ prefix, resource name, version segments from operationIds.
Handle CamelCase IDs (listPets -> list). Convert underscores to hyphens.
Tested with Stytch, Petstore, and Discord operationId patterns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Files touched
M internal/openapi/parser.goM internal/openapi/parser_test.go
Diff
commit 84823431b7b711e71c96e6e648fa837bdf1d8755
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Mon Mar 23 17:21:35 2026 -0700
fix(openapi): smart operationId cleaning for clean command names
Strip api_ prefix, resource name, version segments from operationIds.
Handle CamelCase IDs (listPets -> list). Convert underscores to hyphens.
Tested with Stytch, Petstore, and Discord operationId patterns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
internal/openapi/parser.go | 149 ++++++++++++++++++++++++++++++++++++++--
internal/openapi/parser_test.go | 35 ++++++++++
2 files changed, 180 insertions(+), 4 deletions(-)
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index e7bcee9a..8f5a1bb9 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -298,7 +298,8 @@ func resourceDescription(op *openapi3.Operation, tagDescriptions map[string]stri
}
func resolveEndpointName(method, path string, op *openapi3.Operation, existing map[string]spec.Endpoint, resourceName, basePath string) string {
- name := operationIDToName(operationID(op))
+ name := operationIDToName(operationID(op), resourceName)
+ name = strings.ReplaceAll(name, "-", "_")
if name == "" {
name = defaultEndpointName(method, path)
}
@@ -938,11 +939,151 @@ func baseURLPath(baseURL string) string {
return parsed.Path
}
-func operationIDToName(operationID string) string {
- if operationID == "" {
+func operationIDToName(operationID, resourceName string) string {
+ if strings.TrimSpace(operationID) == "" {
return ""
}
- return toSnakeCase(operationID)
+ original := toSnakeCase(operationID)
+ if original == "" {
+ return ""
+ }
+
+ name := strings.TrimPrefix(original, "api_")
+ resourceVariants := operationIDResourceVariants(resourceName)
+
+ name = stripOperationIDVersionPrefix(name)
+ name = stripOperationIDResourcePrefix(name, resourceVariants)
+ name = stripOperationIDVersionPrefix(name)
+ name = stripOperationIDResourceSegments(name, resourceVariants)
+ name = strings.Trim(name, "_")
+
+ if name == "" {
+ return original
+ }
+
+ return strings.ReplaceAll(name, "_", "-")
+}
+
+func operationIDResourceVariants(resourceName string) []string {
+ resource := toSnakeCase(strings.TrimSpace(resourceName))
+ if resource == "" {
+ return nil
+ }
+
+ seen := map[string]struct{}{}
+ variants := make([]string, 0, 3)
+ addVariant := func(candidate string) {
+ if candidate == "" {
+ return
+ }
+ if _, ok := seen[candidate]; ok {
+ return
+ }
+ seen[candidate] = struct{}{}
+ variants = append(variants, candidate)
+ }
+
+ addVariant(resource)
+ if strings.HasSuffix(resource, "s") && len(resource) > 1 {
+ addVariant(strings.TrimSuffix(resource, "s"))
+ } else {
+ addVariant(resource + "s")
+ }
+
+ return variants
+}
+
+func stripOperationIDVersionPrefix(name string) string {
+ for {
+ switch {
+ case strings.HasPrefix(name, "v1_"):
+ name = strings.TrimPrefix(name, "v1_")
+ case strings.HasPrefix(name, "v2_"):
+ name = strings.TrimPrefix(name, "v2_")
+ case strings.HasPrefix(name, "v3_"):
+ name = strings.TrimPrefix(name, "v3_")
+ default:
+ return name
+ }
+ }
+}
+
+func stripOperationIDResourcePrefix(name string, variants []string) string {
+ if name == "" || len(variants) == 0 {
+ return name
+ }
+
+ for {
+ stripped := false
+ for _, variant := range variants {
+ prefix := variant + "_"
+ if strings.HasPrefix(name, prefix) {
+ name = strings.TrimPrefix(name, prefix)
+ stripped = true
+ break
+ }
+ }
+ if !stripped {
+ return name
+ }
+ }
+}
+
+func stripOperationIDResourceSegments(name string, variants []string) string {
+ if name == "" || len(variants) == 0 {
+ return name
+ }
+
+ tokens := strings.Split(name, "_")
+ if len(tokens) == 0 {
+ return name
+ }
+
+ sequences := make([][]string, 0, len(variants))
+ for _, variant := range variants {
+ parts := strings.Split(variant, "_")
+ if len(parts) == 0 {
+ continue
+ }
+ sequences = append(sequences, parts)
+ }
+
+ if len(sequences) == 0 {
+ return name
+ }
+
+ filtered := make([]string, 0, len(tokens))
+ for i := 0; i < len(tokens); {
+ matched := false
+ for _, sequence := range sequences {
+ if len(sequence) == 0 || i+len(sequence) > len(tokens) {
+ continue
+ }
+
+ sequenceMatches := true
+ for j, part := range sequence {
+ if tokens[i+j] != part {
+ sequenceMatches = false
+ break
+ }
+ }
+ if !sequenceMatches {
+ continue
+ }
+
+ i += len(sequence)
+ matched = true
+ break
+ }
+ if matched {
+ continue
+ }
+
+ filtered = append(filtered, tokens[i])
+ i++
+ }
+
+ return strings.Join(filtered, "_")
}
func toSnakeCase(input string) string {
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index 18b10300..eff3512f 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -210,3 +210,38 @@ func TestSanitizeResourceName(t *testing.T) {
})
}
}
+
+func TestOperationIDToName(t *testing.T) {
+ tests := []struct {
+ operationID string
+ resourceName string
+ want string
+ }{
+ {operationID: "api_user_v1_create", resourceName: "users", want: "create"},
+ {operationID: "api_user_v1_delete_biometric_registration", resourceName: "users", want: "delete-biometric-registration"},
+ {operationID: "api_user_v1_connected_apps", resourceName: "users", want: "connected-apps"},
+ {operationID: "api_user_v1_get", resourceName: "users", want: "get"},
+ {operationID: "api_user_v1_search", resourceName: "users", want: "search"},
+ {operationID: "listPets", resourceName: "pet", want: "list"},
+ {operationID: "createPet", resourceName: "pet", want: "create"},
+ {operationID: "getPetById", resourceName: "pet", want: "get-by-id"},
+ {operationID: "addPet", resourceName: "pet", want: "add"},
+ {operationID: "deletePet", resourceName: "pet", want: "delete"},
+ {operationID: "findPetsByStatus", resourceName: "pet", want: "find-by-status"},
+ {operationID: "findPetsByTags", resourceName: "pet", want: "find-by-tags"},
+ {operationID: "getInventory", resourceName: "store", want: "get-inventory"},
+ {operationID: "placeOrder", resourceName: "store", want: "place-order"},
+ {operationID: "createUser", resourceName: "user", want: "create"},
+ {operationID: "loginUser", resourceName: "user", want: "login"},
+ {operationID: "GetApplicationCommandPermissions", resourceName: "applications", want: "get-command-permissions"},
+ {operationID: "", resourceName: "users", want: ""},
+ {operationID: "list", resourceName: "users", want: "list"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.operationID+"_"+tt.resourceName, func(t *testing.T) {
+ got := operationIDToName(tt.operationID, tt.resourceName)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
← 588c694e fix(generator): add PATCH support + skip unexported fields i
·
back to Cli Printing Press
·
docs: add Steinberger parity plan c8942bc3 →