[object Object]

← 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

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 →