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:
@@ -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
-1
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user