diff --git a/modules/mesh-build-go/buildstep/seatgen/gogen.go b/modules/mesh-build-go/buildstep/seatgen/gogen.go index 8308f77d5..0c231490d 100644 --- a/modules/mesh-build-go/buildstep/seatgen/gogen.go +++ b/modules/mesh-build-go/buildstep/seatgen/gogen.go @@ -292,6 +292,11 @@ func goEnum(b *bytes.Buffer, what, prefix string, t *typ) { } func goType(t *typ, kinds map[string]*typ, required bool) string { + if t.null { + n := *t + n.null = false + return "*" + goType(&n, kinds, true) + } switch t.kind { case "string": return "string" diff --git a/modules/mesh-build-go/buildstep/seatgen/layout_test.go b/modules/mesh-build-go/buildstep/seatgen/layout_test.go index 26faf491d..f038c020f 100644 --- a/modules/mesh-build-go/buildstep/seatgen/layout_test.go +++ b/modules/mesh-build-go/buildstep/seatgen/layout_test.go @@ -81,6 +81,15 @@ func TestEachSeatDefinedWholeIsAGoModuleOfItsOwn(t *testing.T) { if ts := string(files["ts/seats/"+seat+"/src/parts/registry.ts"]); !strings.Contains(ts, `"pushed": string;`) || !strings.Contains(ts, `"at"?: string;`) { t.Errorf("%s's TypeScript does not write a date-time as a string:\n%s", seat, ts) } + // A scalar typed ["T","null"] is a pointer in Go and `T | null` in TypeScript, so a declared zero survives with its type. + for _, w := range []string{`Floor\s+\*float64\s+` + "`json:\"floor,omitempty\"`", `Limit\s+\*int64\s+` + "`json:\"limit,omitempty\"`", `Label\s+\*string\s+` + "`json:\"label,omitempty\"`", `On\s+\*bool\s+` + "`json:\"on,omitempty\"`"} { + if !re.MustCompile(w).MatchString(src) { + t.Errorf("%s does not write a nullable scalar as a pointer (%s):\n%s", seat, w, src) + } + } + if ts := string(files["ts/seats/"+seat+"/src/parts/registry.ts"]); !strings.Contains(ts, `"floor"?: number | null;`) || !strings.Contains(ts, `"on"?: boolean | null;`) || !strings.Contains(ts, `"label"?: string | null;`) { + t.Errorf("%s's TypeScript does not write a nullable scalar as T | null:\n%s", seat, ts) + } // A string's enum is a constant per value, so a caller names the definition's value (A3t: the removal reason). if !re.MustCompile(`RefReasonRetention\s+= "retention"`).MatchString(src) || !re.MustCompile(`RefReasonTagGone\s+= "tag-gone"`).MatchString(src) { t.Errorf("%s does not name the part's enum values:\n%s", seat, src) diff --git a/modules/mesh-build-go/buildstep/seatgen/testdata/contracts/parts/registry.json b/modules/mesh-build-go/buildstep/seatgen/testdata/contracts/parts/registry.json index fd146231e..d5a44e1ba 100644 --- a/modules/mesh-build-go/buildstep/seatgen/testdata/contracts/parts/registry.json +++ b/modules/mesh-build-go/buildstep/seatgen/testdata/contracts/parts/registry.json @@ -1,3 +1,3 @@ {"format":1,"part":"registry","verbs":[{"name":"publish","input":{"$ref":"#/defs/Ref"},"answer":{"type":"object"}}], "events":[{"name":"published","body":{"$ref":"#/defs/Ref"}}], - "defs":{"Ref":{"type":"object","properties":{"digest":{"type":"string"},"at":{"type":"string","format":"date-time"},"pushed":{"type":"string","format":"date-time"},"reason":{"type":"string","enum":["tag-gone","retention"]}},"required":["digest","pushed"]}}} + "defs":{"Ref":{"type":"object","properties":{"digest":{"type":"string"},"at":{"type":"string","format":"date-time"},"pushed":{"type":"string","format":"date-time"},"floor":{"type":["number","null"]},"limit":{"type":["integer","null"]},"label":{"type":["string","null"]},"on":{"type":["boolean","null"]},"reason":{"type":"string","enum":["tag-gone","retention"]}},"required":["digest","pushed"]}}} diff --git a/modules/mesh-build-go/buildstep/seatgen/tsgen.go b/modules/mesh-build-go/buildstep/seatgen/tsgen.go index e3c80b24b..17e58563a 100644 --- a/modules/mesh-build-go/buildstep/seatgen/tsgen.go +++ b/modules/mesh-build-go/buildstep/seatgen/tsgen.go @@ -194,6 +194,11 @@ func tsTypes(b *bytes.Buffer, tp *typer) { } func tsType(t *typ) string { + if t.null { + n := *t + n.null = false + return tsType(&n) + " | null" + } switch t.kind { case "string": if len(t.enum) > 0 { diff --git a/modules/mesh-build-go/buildstep/seatgen/types.go b/modules/mesh-build-go/buildstep/seatgen/types.go index 966477b7c..4b96bae42 100644 --- a/modules/mesh-build-go/buildstep/seatgen/types.go +++ b/modules/mesh-build-go/buildstep/seatgen/types.go @@ -15,6 +15,7 @@ type typ struct { ref string // a referenced type's name fields []field enum []string + null bool // a scalar typed ["T","null"]: Go writes *T, TypeScript T | null, so a declared zero survives } type field struct { @@ -68,6 +69,41 @@ func (tp *typer) read(where string, s map[string]any, top bool) (*typ, error) { return &typ{kind: "ref", ref: n}, nil } t, _ := s["type"].(string) + if list, ok := s["type"].([]any); ok { + // ["T","null"] with T a scalar is a nullable T; any other list of types stays `any`. + var base string + null := false + for _, v := range list { + switch str, _ := v.(string); str { + case "null": + null = true + case "number", "integer", "string", "boolean": + if base != "" { + base = "?" + } else { + base = str + } + default: + base = "?" + } + } + if null && base != "" && base != "?" { + inner := make(map[string]any, len(s)) + for k, v := range s { + inner[k] = v + } + inner["type"] = base + out, err := tp.read(where, inner, top) + if err != nil { + return nil, err + } + if out.kind == "time" { + return out, nil + } + out.null = true + return out, nil + } + } switch t { case "string": out := &typ{kind: "string"}