golib/tools/jsontypes/typedef.go
AJ ONeal cdadf91459
ref(jsontypes): replace Prompter with Resolver callback pattern
Separate library from CLI concerns:
- Add Resolver callback type with Decision/Response structs for all
  interactive decisions (map/struct, type name, tuple/list, shape
  unification, shape naming, name collision)
- Move terminal I/O (Prompter) from library to cmd/jsonpaths
- Add public API: New(), ParseFormat(), Generate(), AutoGenerate()
- Add Format type with aliases (ts, py, json-paths, etc.)
- Fix godoc comments to match exported function names
- Update tests to use scriptedResolver instead of Prompter internals
- Update doc.go and README with current API
2026-03-07 21:38:43 -07:00

118 lines
2.8 KiB
Go

package jsontypes
import (
"encoding/json"
"strings"
)
// GenerateTypedef converts formatted flat paths into a JSON Typedef (RFC 8927) document.
func GenerateTypedef(paths []string) string {
types, _ := buildGoTypes(paths)
typeMap := make(map[string]goType)
for _, t := range types {
typeMap[t.name] = t
}
// The first type is the root
if len(types) == 0 {
return "{}\n"
}
root := types[0]
defs := make(map[string]any)
result := structToJTD(root, typeMap, defs)
if len(defs) > 0 {
result["definitions"] = defs
}
data, _ := json.MarshalIndent(result, "", " ")
return string(data) + "\n"
}
// structToJTD converts a goType to a JTD schema object.
func structToJTD(t goType, typeMap map[string]goType, defs map[string]any) map[string]any {
props := make(map[string]any)
optProps := make(map[string]any)
for _, f := range t.fields {
schema := goTypeToJTD(f.goType, f.optional, typeMap, defs)
if f.optional {
optProps[f.jsonName] = schema
} else {
props[f.jsonName] = schema
}
}
result := make(map[string]any)
if len(props) > 0 {
result["properties"] = props
} else if len(optProps) > 0 {
// JTD requires "properties" if "optionalProperties" is present
result["properties"] = map[string]any{}
}
if len(optProps) > 0 {
result["optionalProperties"] = optProps
}
return result
}
// goTypeToJTD converts a Go type string to a JTD schema.
func goTypeToJTD(goTyp string, nullable bool, typeMap map[string]goType, defs map[string]any) map[string]any {
result := goTypeToJTDInner(goTyp, typeMap, defs)
if nullable {
result["nullable"] = true
}
return result
}
func goTypeToJTDInner(goTyp string, typeMap map[string]goType, defs map[string]any) map[string]any {
// Strip pointer
goTyp = strings.TrimPrefix(goTyp, "*")
// Slice
if strings.HasPrefix(goTyp, "[]") {
elemType := goTyp[2:]
return map[string]any{
"elements": goTypeToJTDInner(elemType, typeMap, defs),
}
}
// Map
if strings.HasPrefix(goTyp, "map[string]") {
valType := goTyp[11:]
return map[string]any{
"values": goTypeToJTDInner(valType, typeMap, defs),
}
}
// Primitives
switch goTyp {
case "string":
return map[string]any{"type": "string"}
case "int64":
// JTD (RFC 8927) has no int64 type; int32 is the largest integer type available.
return map[string]any{"type": "int32"}
case "float64":
return map[string]any{"type": "float64"}
case "bool":
return map[string]any{"type": "boolean"}
case "any":
return map[string]any{}
}
// Named struct — emit as ref, add to definitions if not already there
if t, ok := typeMap[goTyp]; ok {
if _, exists := defs[goTyp]; !exists {
// Add placeholder to prevent infinite recursion
defs[goTyp] = nil
defs[goTyp] = structToJTD(t, typeMap, defs)
}
return map[string]any{"ref": goTyp}
}
// Unknown type
return map[string]any{}
}