From e7c6d25bed9e85bc07ec7ed3657d0aa78d076e17 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 8 Mar 2026 00:27:05 -0700 Subject: [PATCH] fix(jsontypes): handle sample format in Go struct generator and path parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- tools/jsontypes/format.go | 22 ++++++++- tools/jsontypes/gostruct.go | 36 +++++++++++++-- tools/jsontypes/pipeline_test.go | 76 ++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 tools/jsontypes/pipeline_test.go diff --git a/tools/jsontypes/format.go b/tools/jsontypes/format.go index b268a30..593b03e 100644 --- a/tools/jsontypes/format.go +++ b/tools/jsontypes/format.go @@ -42,9 +42,9 @@ func parsePath(path string) []segment { i = i + end + 1 } - // Type: read {Type} + // Type: read {Type} (handling nested braces in sample data) if i < len(path) && path[i] == '{' { - end := strings.IndexByte(path[i:], '}') + end := findMatchingBrace(path[i:]) if end < 0 { break } @@ -290,6 +290,24 @@ func buildBareWithParent(segs []segment, parentIdx int) 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. func buildBare(segs []segment) string { var buf strings.Builder diff --git a/tools/jsontypes/gostruct.go b/tools/jsontypes/gostruct.go index eda98f1..a8c5aa2 100644 --- a/tools/jsontypes/gostruct.go +++ b/tools/jsontypes/gostruct.go @@ -509,18 +509,37 @@ func primitiveToGo(typ string) string { return "int64" case "float": return "float64" - case "bool": + case "bool", "true", "false": return "bool" - case "null", "unknown": + case "null", "unknown", "empty", "undefined": return "any" 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 } } func isPrimitiveType(typ string) bool { 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 false @@ -534,7 +553,16 @@ func makePointer(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) { diff --git a/tools/jsontypes/pipeline_test.go b/tools/jsontypes/pipeline_test.go new file mode 100644 index 0000000..70672ac --- /dev/null +++ b/tools/jsontypes/pipeline_test.go @@ -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) +}