mirror of
https://github.com/therootcompany/golib.git
synced 2026-11-04 06:55:00 +00:00
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:
parent
2129157f8d
commit
e7c6d25bed
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
76
tools/jsontypes/pipeline_test.go
Normal file
76
tools/jsontypes/pipeline_test.go
Normal 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)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user