[object Object]

← back to Cli Printing Press

fix(generator): harden toCamel, flagName, types template for special chars in schema names

d8a93e0acba4f4827049ef922749783dfb09e2a4 · 2026-03-24 10:06:30 -0700 · Matt Van Horn

Files touched

Diff

commit d8a93e0acba4f4827049ef922749783dfb09e2a4
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Tue Mar 24 10:06:30 2026 -0700

    fix(generator): harden toCamel, flagName, types template for special chars in schema names
---
 internal/generator/generator.go            | 32 +++++++++++++++++++++++++++---
 internal/generator/templates/types.go.tmpl |  4 ++--
 internal/openapi/parser.go                 |  7 +++++++
 3 files changed, 38 insertions(+), 5 deletions(-)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 23ec9115..cb523262 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -40,6 +40,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		"add":               func(a, b int) int { return a + b },
 		"oneline":           oneline,
 		"flagName":          flagName,
+		"safeTypeName":      safeTypeName,
 		"exampleLine":       g.exampleLine,
 	}
 	return g
@@ -163,15 +164,22 @@ func (g *Generator) renderTemplate(tmplName, outPath string, data any) error {
 // Template helper functions
 
 func toCamel(s string) string {
+	// Strip characters that are invalid in Go identifiers
+	s = strings.TrimLeft(s, "$")
 	parts := strings.FieldsFunc(s, func(r rune) bool {
-		return r == '_' || r == '-' || r == ' '
+		return r == '_' || r == '-' || r == ' ' || r == '.' || r == '/' || r == '\\' || r == '$' || r == '#' || r == '@'
 	})
 	for i, p := range parts {
 		if len(p) > 0 {
 			parts[i] = strings.ToUpper(p[:1]) + p[1:]
 		}
 	}
-	return strings.Join(parts, "")
+	result := strings.Join(parts, "")
+	// Ensure starts with letter
+	if len(result) > 0 && !unicode.IsLetter(rune(result[0])) {
+		result = "V" + result
+	}
+	return result
 }
 
 func toSnake(s string) string {
@@ -334,7 +342,25 @@ func (g *Generator) exampleLine(commandPath, endpointName string, endpoint spec.
 }
 
 func flagName(name string) string {
-	return strings.ReplaceAll(name, "_", "-")
+	name = strings.TrimLeft(name, "$")
+	name = strings.NewReplacer("_", "-", "/", "-", ".", "-", "\\", "-").Replace(name)
+	return strings.Trim(name, "-")
+}
+
+func safeTypeName(name string) string {
+	name = strings.TrimLeft(name, "$")
+	name = strings.NewReplacer(".", "_", "/", "_", "\\", "_", "-", "_", " ", "_").Replace(name)
+	var b strings.Builder
+	for _, r := range name {
+		if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
+			b.WriteRune(r)
+		}
+	}
+	result := b.String()
+	if len(result) > 0 && !unicode.IsLetter(rune(result[0])) {
+		result = "T" + result
+	}
+	return result
 }
 
 func envVarPlaceholder(envVar string) string {
diff --git a/internal/generator/templates/types.go.tmpl b/internal/generator/templates/types.go.tmpl
index 545f38c2..5c6b7456 100644
--- a/internal/generator/templates/types.go.tmpl
+++ b/internal/generator/templates/types.go.tmpl
@@ -1,8 +1,8 @@
 package types
 {{range $name, $typeDef := .Types}}
-type {{$name}} struct {
+type {{safeTypeName $name}} struct {
 {{- range $typeDef.Fields}}
-	{{title .Name}} {{goType .Type}} `json:"{{.Name}}"`
+	{{camel .Name}} {{goType .Type}} `json:"{{.Name}}"`
 {{- end}}
 }
 {{end}}
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index 02ccbb46..51bf7031 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -906,11 +906,18 @@ func mapTypes(doc *openapi3.T, out *spec.APISpec) {
 	}
 	sort.Strings(names)
 
+	usedTypeNames := map[string]int{}
 	for _, name := range names {
 		goName := sanitizeTypeName(name)
 		if goName == "" {
 			continue
 		}
+		if count, exists := usedTypeNames[goName]; exists {
+			usedTypeNames[goName] = count + 1
+			goName = fmt.Sprintf("%s%d", goName, count+1)
+		} else {
+			usedTypeNames[goName] = 1
+		}
 
 		schemaRef := schemaMap[name]
 		schema := schemaRefValue(schemaRef)

← 2501e565 docs(plans): update gauntlet findings after parser fixes - 4  ·  back to Cli Printing Press  ·  docs(plans): template sanitization round 2 + pipeline E2E pl 15f3d250 →