From f61760f738cfc4283c37da1cb8bfed80247ff307 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:58:27 +0100 Subject: [PATCH] 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 --- modules/mesh-build-go/buildstep/seatgen/gogen.go | 15 +++++++++++++++ .../buildstep/seatgen/layout_test.go | 9 +++++++++ .../testdata/contracts/parts/registry.json | 2 +- modules/mesh-build-go/buildstep/seatgen/tsgen.go | 2 ++ modules/mesh-build-go/buildstep/seatgen/types.go | 6 +++++- 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/modules/mesh-build-go/buildstep/seatgen/gogen.go b/modules/mesh-build-go/buildstep/seatgen/gogen.go index 0ef1a0cf5..8308f77d5 100644 --- a/modules/mesh-build-go/buildstep/seatgen/gogen.go +++ b/modules/mesh-build-go/buildstep/seatgen/gogen.go @@ -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": diff --git a/modules/mesh-build-go/buildstep/seatgen/layout_test.go b/modules/mesh-build-go/buildstep/seatgen/layout_test.go index d95b976df..26faf491d 100644 --- a/modules/mesh-build-go/buildstep/seatgen/layout_test.go +++ b/modules/mesh-build-go/buildstep/seatgen/layout_test.go @@ -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) 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 01a76b786..fd146231e 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"},"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"]}}} diff --git a/modules/mesh-build-go/buildstep/seatgen/tsgen.go b/modules/mesh-build-go/buildstep/seatgen/tsgen.go index bd4855301..e3c80b24b 100644 --- a/modules/mesh-build-go/buildstep/seatgen/tsgen.go +++ b/modules/mesh-build-go/buildstep/seatgen/tsgen.go @@ -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": diff --git a/modules/mesh-build-go/buildstep/seatgen/types.go b/modules/mesh-build-go/buildstep/seatgen/types.go index 7d0afa94b..966477b7c 100644 --- a/modules/mesh-build-go/buildstep/seatgen/types.go +++ b/modules/mesh-build-go/buildstep/seatgen/types.go @@ -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 {