fix(jsontypes): handle sample format in Go struct generator and path parser

- parsePath now uses brace-depth matching to correctly parse nested JSON
  in sample type intros like {Root:{"name":"pikachu","id":25}}
- cleanTypeName strips sample data from type intros (Root:{...} → Root)
- isPrimitiveType recognizes sample leaf values (quoted strings, numbers)
- primitiveToGo maps sample values to proper Go types (string, int64, etc.)
- All three pipeline tests pass: raw→Go, coalesced→Go, and sample→Go
This commit is contained in:
AJ ONeal 2026-03-08 00:27:05 -07:00
parent 2129157f8d
commit e7c6d25bed
No known key found for this signature in database
3 changed files with 128 additions and 6 deletions

View File

@ -42,9 +42,9 @@ func parsePath(path string) []segment {
i = i + end + 1 i = i + end + 1
} }
// Type: read {Type} // Type: read {Type} (handling nested braces in sample data)
if i < len(path) && path[i] == '{' { if i < len(path) && path[i] == '{' {
end := strings.IndexByte(path[i:], '}') end := findMatchingBrace(path[i:])
if end < 0 { if end < 0 {
break break
} }
@ -290,6 +290,24 @@ func buildBareWithParent(segs []segment, parentIdx int) string {
return buf.String() return buf.String()
} }
// findMatchingBrace returns the index of the closing '}' that matches the
// opening '{' at s[0], respecting nested braces. Returns -1 if not found.
func findMatchingBrace(s string) int {
depth := 0
for i, c := range s {
switch c {
case '{':
depth++
case '}':
depth--
if depth == 0 {
return i
}
}
}
return -1
}
// buildBare builds a path string without any type annotations, for sorting. // buildBare builds a path string without any type annotations, for sorting.
func buildBare(segs []segment) string { func buildBare(segs []segment) string {
var buf strings.Builder var buf strings.Builder

View File

@ -509,18 +509,37 @@ func primitiveToGo(typ string) string {
return "int64" return "int64"
case "float": case "float":
return "float64" return "float64"
case "bool": case "bool", "true", "false":
return "bool" return "bool"
case "null", "unknown": case "null", "unknown", "empty", "undefined":
return "any" return "any"
default: default:
// Sample format: quoted strings → string, numbers → numeric type.
if len(typ) > 0 && typ[0] == '"' {
return "string"
}
if len(typ) > 0 && (typ[0] >= '0' && typ[0] <= '9' || typ[0] == '-') {
if strings.ContainsAny(typ, ".eE") {
return "float64"
}
return "int64"
}
return typ return typ
} }
} }
func isPrimitiveType(typ string) bool { func isPrimitiveType(typ string) bool {
switch typ { switch typ {
case "string", "int", "float", "bool", "null", "unknown", "any": case "string", "int", "float", "bool", "null", "unknown", "any",
"empty", "undefined":
return true
}
// Sample format: quoted strings like `"pikachu"`, numbers like `25` or `3.14`,
// booleans `true`/`false` are leaf values, not type names.
if len(typ) > 0 && typ[0] == '"' {
return true
}
if len(typ) > 0 && (typ[0] >= '0' && typ[0] <= '9' || typ[0] == '-') {
return true return true
} }
return false return false
@ -534,7 +553,16 @@ func makePointer(typ string) string {
} }
func cleanTypeName(typ string) string { func cleanTypeName(typ string) string {
return strings.TrimSuffix(typ, "?") typ = strings.TrimSuffix(typ, "?")
// Strip sample data from type intros like "Root:{...}" → "Root".
if idx := strings.Index(typ, ":"); idx > 0 {
candidate := typ[:idx]
// A type name starts with uppercase.
if len(candidate) > 0 && candidate[0] >= 'A' && candidate[0] <= 'Z' {
return candidate
}
}
return typ
} }
func sortGoFields(fields []goField) { func sortGoFields(fields []goField) {

View File

@ -0,0 +1,76 @@
package jsontypes
import (
"strings"
"testing"
)
func TestPipelineEndToEnd(t *testing.T) {
// Test the full pipeline: JSON → RawPaths → Coalesce → GenerateGoStructs.
data := loadJSONFixture(t, "testdata/swapi-luke.json")
raw := RawPaths(data)
coalesced := Coalesce(raw)
// The coalesced output should be directly usable by the Go generator.
goCode := GenerateGoStructs(coalesced)
if goCode == "" {
t.Fatal("GenerateGoStructs returned empty output")
}
// Should produce a Root struct with expected fields.
if !strings.Contains(goCode, "type Root struct") {
t.Error("missing Root struct")
}
if !strings.Contains(goCode, "`json:\"name\"`") {
t.Error("missing name field")
}
if !strings.Contains(goCode, "`json:\"height\"`") {
t.Error("missing height field")
}
t.Logf("Go output:\n%s", goCode)
}
func TestPipelinePokemon(t *testing.T) {
data := loadJSONFixture(t, "testdata/pokemon-pikachu.json")
raw := RawPaths(data)
coalesced := Coalesce(raw)
goCode := GenerateGoStructs(coalesced)
if !strings.Contains(goCode, "type Root struct") {
t.Error("missing Root struct")
}
if !strings.Contains(goCode, "type AbilitiesItem struct") {
t.Error("missing AbilitiesItem struct")
}
if !strings.Contains(goCode, "type NameUrl struct") {
t.Error("missing NameUrl struct")
}
// NameUrl should be used as a field type in multiple structs.
nameUrlCount := strings.Count(goCode, "NameUrl")
if nameUrlCount < 3 {
t.Errorf("NameUrl used only %d times (expected many references)", nameUrlCount)
}
t.Logf("Go output (%d bytes, NameUrl used %d times):\n%s", len(goCode), nameUrlCount, goCode)
}
func TestPipelineSampleToGo(t *testing.T) {
// Sample output should also work with the Go generator.
data := loadJSONFixture(t, "testdata/swapi-planets.json")
sample := SampleWithConfig(data, SampleConfig{MaxStringLen: 20})
goCode := GenerateGoStructs(sample)
if !strings.Contains(goCode, "type Root struct") {
t.Error("missing Root struct")
}
if !strings.Contains(goCode, "type ResultsItem struct") {
t.Error("missing ResultsItem struct")
}
t.Logf("Go output:\n%s", goCode)
}