← back to Cli Printing Press
fix(parser): sanitize schema names, cap title length, handle missing servers, resolve URL templates
3f096bce67197ef073d06199100c397eefad5d2f · 2026-03-24 09:43:04 -0700 · Matt Van Horn
Files touched
M internal/openapi/parser.goM internal/spec/spec.go
Diff
commit 3f096bce67197ef073d06199100c397eefad5d2f
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Tue Mar 24 09:43:04 2026 -0700
fix(parser): sanitize schema names, cap title length, handle missing servers, resolve URL templates
---
internal/openapi/parser.go | 77 ++++++++++++++++++++++++++++++++++++++++++++--
internal/spec/spec.go | 1 +
2 files changed, 75 insertions(+), 3 deletions(-)
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index 033e0ee8..02ccbb46 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -32,9 +32,18 @@ func Parse(data []byte) (*spec.APISpec, error) {
description := ""
version := ""
if doc.Info != nil {
- if v := cleanSpecName(doc.Info.Title); v != "" {
+ if v := cleanSpecName(doc.Info.Title); v != "" && v != "api" {
name = v
}
+ if name == "api" && doc.Info.Extensions != nil {
+ if raw, ok := doc.Info.Extensions["x-api-name"]; ok {
+ if s, ok := raw.(string); ok {
+ if v := cleanSpecName(s); v != "" && v != "api" {
+ name = v
+ }
+ }
+ }
+ }
description = strings.TrimSpace(doc.Info.Description)
version = strings.TrimSpace(doc.Info.Version)
}
@@ -43,6 +52,28 @@ func Parse(data []byte) (*spec.APISpec, error) {
basePath := ""
if len(doc.Servers) > 0 && doc.Servers[0] != nil {
serverURL := strings.TrimRight(strings.TrimSpace(doc.Servers[0].URL), "/")
+ // Resolve server URL template variables using defaults.
+ if strings.Contains(serverURL, "{") && doc.Servers[0].Variables != nil {
+ for varName, variable := range doc.Servers[0].Variables {
+ if variable != nil && variable.Default != "" {
+ serverURL = strings.ReplaceAll(serverURL, "{"+varName+"}", variable.Default)
+ }
+ }
+ }
+ // Strip any remaining unresolved template variables.
+ for strings.Contains(serverURL, "{") {
+ start := strings.Index(serverURL, "{")
+ end := strings.Index(serverURL, "}")
+ if start == -1 || end == -1 || end < start {
+ break
+ }
+ serverURL = serverURL[:start] + serverURL[end+1:]
+ }
+ serverURL = strings.ReplaceAll(serverURL, "//", "/")
+ // Restore protocol double-slash if normalization collapsed it.
+ serverURL = strings.Replace(serverURL, "http:/", "http://", 1)
+ serverURL = strings.Replace(serverURL, "https:/", "https://", 1)
+ serverURL = strings.TrimRight(serverURL, "/")
if serverURL != "" {
lowerURL := strings.ToLower(serverURL)
if strings.HasPrefix(lowerURL, "http://") || strings.HasPrefix(lowerURL, "https://") {
@@ -54,6 +85,10 @@ func Parse(data []byte) (*spec.APISpec, error) {
}
}
}
+ if baseURL == "" && basePath == "" {
+ warnf("no servers defined in spec; generated CLI will require base_url in config")
+ baseURL = "https://api.example.com"
+ }
result := &spec.APISpec{
Name: name,
@@ -872,6 +907,11 @@ func mapTypes(doc *openapi3.T, out *spec.APISpec) {
sort.Strings(names)
for _, name := range names {
+ goName := sanitizeTypeName(name)
+ if goName == "" {
+ continue
+ }
+
schemaRef := schemaMap[name]
schema := schemaRefValue(schemaRef)
if schema == nil {
@@ -902,7 +942,7 @@ func mapTypes(doc *openapi3.T, out *spec.APISpec) {
})
}
- out.Types[name] = spec.TypeDef{Fields: fields}
+ out.Types[goName] = spec.TypeDef{Fields: fields}
}
}
@@ -924,7 +964,7 @@ func collectTypeProperties(schemaRef *openapi3.SchemaRef, properties map[string]
if strings.HasPrefix(name, "_") {
continue
}
- properties[name] = prop
+ properties[sanitizeTypeName(name)] = prop
}
for _, sub := range schema.AllOf {
collectTypeProperties(sub, properties, visited)
@@ -1503,6 +1543,22 @@ func sanitizeResourceName(name string) string {
return name
}
+func sanitizeTypeName(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 toKebabCase(input string) string {
var b strings.Builder
lastHyphen := true
@@ -1559,6 +1615,18 @@ func cleanSpecName(title string) string {
"specification": {},
"preview": {},
"http": {},
+ "with": {},
+ "and": {},
+ "from": {},
+ "for": {},
+ "the": {},
+ "by": {},
+ "of": {},
+ "in": {},
+ "on": {},
+ "to": {},
+ "fixes": {},
+ "improvements": {},
}
filtered := make([]string, 0, len(tokens))
@@ -1571,6 +1639,9 @@ func cleanSpecName(title string) string {
}
filtered = append(filtered, token)
}
+ if len(filtered) > 3 {
+ filtered = filtered[:3]
+ }
name := toKebabCase(strings.Join(filtered, " "))
if name == "" {
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index 2d44b49b..dbc83537 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -111,6 +111,7 @@ func (s *APISpec) Validate() error {
if s.Name == "" {
return fmt.Errorf("name is required")
}
+ // Parser fallback may supply a placeholder base_url when the source spec omits servers.
if s.BaseURL == "" && s.BasePath == "" {
return fmt.Errorf("base_url is required")
}
← 0f1beba2 feat(catalog): add telegram, launchdarkly, sentry from dogfo
·
back to Cli Printing Press
·
docs(plans): update gauntlet findings after parser fixes - 4 2501e565 →