seatgen writes a scalar typed [T, null] as *T in Go and T | null in TypeScript, so a declared zero survives with its type (ADR 0634; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 16:43:19 +01:00
parent 36da33c818
commit 75959dfa25
5 changed files with 56 additions and 1 deletions
@@ -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"
@@ -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)
@@ -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"]}}}
@@ -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 {
@@ -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"}