seatgen writes a date-time as time.Time (a string in TypeScript), by value when required, by pointer when optional (B5d; ADR 0634; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 15:01:10 +01:00
parent c46009709d
commit f61760f738
5 changed files with 32 additions and 2 deletions
@@ -219,6 +219,16 @@ func goWhole(m *Module, d seatdef.Definition) ([]byte, []byte, error) {
if err := goTypes(&b, tp, map[string]*typ{}); err != nil {
return nil, nil, err
}
if bytes.Contains(b.Bytes(), []byte("time.Time")) {
// A moment is the standard library's time (the only import a contract may have besides its states' readers).
raw := b.Bytes()
if bytes.Contains(raw, []byte("\nimport (\n")) {
raw = bytes.Replace(raw, []byte("\nimport (\n"), []byte("\nimport (\n\t\"time\"\n"), 1)
} else {
raw = bytes.Replace(raw, []byte("package "+pkg+"\n\n"), []byte("package "+pkg+"\n\nimport \"time\"\n\n"), 1)
}
b = *bytes.NewBuffer(raw)
}
src, err := gofmt(&b)
if err != nil {
return nil, nil, err
@@ -285,6 +295,11 @@ func goType(t *typ, kinds map[string]*typ, required bool) string {
switch t.kind {
case "string":
return "string"
case "time":
if required {
return "time.Time"
}
return "*time.Time"
case "integer":
return "int64"
case "number":
@@ -72,6 +72,15 @@ func TestEachSeatDefinedWholeIsAGoModuleOfItsOwn(t *testing.T) {
if !strings.Contains(src, "type Ref struct") || !strings.Contains(src, "type PublishedEvent") {
t.Errorf("%s does not hold the part's types whole", seat)
}
// A moment (format date-time) is the standard library's time in Go, a required one by value and an optional one
// by pointer (a zero time is not an absent one), and a string in TypeScript.
if !re.MustCompile(`Pushed\s+time\.Time\s+`+"`json:\"pushed\"`").MatchString(src) || !re.MustCompile(`At\s+\*time\.Time\s+`+"`json:\"at,omitempty\"`").MatchString(src) ||
!(strings.Contains(src, "\t\"time\"\n") || strings.Contains(src, "import \"time\"")) {
t.Errorf("%s does not write a date-time as time.Time:\n%s", seat, src)
}
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 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"},"reason":{"type":"string","enum":["tag-gone","retention"]}},"required":["digest"]}}}
"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"]}}}
@@ -204,6 +204,8 @@ func tsType(t *typ) string {
return strings.Join(parts, " | ")
}
return "string"
case "time":
return "string"
case "integer", "number":
return "number"
case "boolean":
@@ -10,7 +10,7 @@ import (
// properties, maps, arrays, strings, integers, numbers, booleans, references to a definition's own types). Anything
// else is `any`, so a schema the generator does not understand still decodes.
type typ struct {
kind string // string, integer, number, boolean, any, map, slice, struct, ref, object (free)
kind string // string, time (a string of format date-time), integer, number, boolean, any, map, slice, struct, ref, object (free)
elem *typ // a map's value or a slice's item
ref string // a referenced type's name
fields []field
@@ -71,6 +71,10 @@ func (tp *typer) read(where string, s map[string]any, top bool) (*typ, error) {
switch t {
case "string":
out := &typ{kind: "string"}
if f, _ := s["format"].(string); f == "date-time" {
// A moment: Go writes time.Time (RFC 3339 on the wire), TypeScript the string (ADR 0634).
return &typ{kind: "time"}, nil
}
if e, ok := s["enum"].([]any); ok {
for _, v := range e {
if str, ok := v.(string); ok {