mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
Call the generator Layouts, not Codecs
A codec is a runtime encode/decode step, which is the thing ZAP removes: a field IS its offset, and a read is a pointer into the bytes that arrived. Nothing here runs on the served path — Layouts reflects at BUILD time and emits constant offsets. Naming it for the layer it exists to delete told readers the opposite of what it does. LayoutOf already names one type's shape; this emits those as source. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
|
||||
Co-authored-by: Hanzo Dev <[email protected]>
|
||||
@@ -553,7 +553,7 @@ and points `ZIP_RUNTIME_DIR` at it.
|
||||
|
||||
## Stating a type's wire — `codec.go` (v1.36.26)
|
||||
|
||||
`zip.Codecs(roots...)` renders the `MarshalZAP`/`UnmarshalZAP` pair for each
|
||||
`zip.Layouts(roots...)` renders the `MarshalZAP`/`UnmarshalZAP` pair for each
|
||||
root and for every struct nested below it, grouped by the package that owns the
|
||||
type. `LayoutOf`'s doc named three readers of the one derivation — the plane
|
||||
encodes against it, a `.zap` schema states it, a generator emits it as constants
|
||||
@@ -587,7 +587,7 @@ block-local for the same reason. The assertion is what makes a deleted codec a
|
||||
build failure rather than a silent return to reflection.
|
||||
|
||||
**The wire does not move**, and that is held, not asserted:
|
||||
`internal/codec.TestTheCodecKeepsTheWire` requires the generated bytes to equal
|
||||
`internal/codec.TestTheLayoutKeepsTheWire` requires the generated bytes to equal
|
||||
`zapenc.Marshal`'s byte for byte over a fixture carrying one field of every form.
|
||||
That is what lets a codec roll out one pod at a time. `TestTheCodecIsCheckedIn`
|
||||
regenerates and diffs, so a type that moved without a regeneration is a failing
|
||||
|
||||
@@ -68,7 +68,7 @@ const CallPath = "/.well-known/zip/op/"
|
||||
|
||||
// CallContentType marks a body on the op-call plane. It names ZAP explicitly so
|
||||
// a request that arrives with anything else is a caller that has not been
|
||||
// updated, rather than a body silently read under the wrong codec.
|
||||
// updated, rather than a body silently read under the wrong layout.
|
||||
const CallContentType = "application/zap"
|
||||
|
||||
// callFault is how a refusal crosses the plane: the status the op chose, its
|
||||
|
||||
+1
-1
@@ -191,7 +191,7 @@ func TestCall_WireIsZAPFramesNotHTTP(t *testing.T) {
|
||||
req.URI().SetPath(zip.CallPath + "flags_bool")
|
||||
// The body is ZAP, because this plane carries nothing else — a JSON body
|
||||
// here is a caller that has not been updated, and is refused rather than
|
||||
// read under the wrong codec.
|
||||
// read under the wrong layout.
|
||||
inBody, err := zapenc.Marshal(&boolIn{Flag: "beta"})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal ZAP body: %v", err)
|
||||
|
||||
+4
-5
@@ -27,10 +27,10 @@ func crossApp() *zip.App {
|
||||
zip.Describe("POST /v1/account", zip.Doc{
|
||||
Description: "GetAccount retrieves account details by ID.\n\nReturns the balance and active status.",
|
||||
Fields: map[string]string{
|
||||
"AccountIn.id": "Unique account identifier",
|
||||
"AccountOut.id": "Account identifier",
|
||||
"AccountOut.balance": "Current balance in cents",
|
||||
"AccountOut.active": "Whether account is currently active",
|
||||
"AccountIn.id": "Unique account identifier",
|
||||
"AccountOut.id": "Account identifier",
|
||||
"AccountOut.balance": "Current balance in cents",
|
||||
"AccountOut.active": "Whether account is currently active",
|
||||
},
|
||||
Example: []byte(`{"id": "acc_1"}`),
|
||||
Response: []byte(`{"id": "acc_1", "balance": 1000, "active": true}`),
|
||||
@@ -205,4 +205,3 @@ func TestCppCLI_GeneratesCppCLIBindings(t *testing.T) {
|
||||
t.Errorf("C++ CLI missing run_cli dispatcher")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ type optionalID struct {
|
||||
}
|
||||
|
||||
func TestAnOptionalIDIsReadBack(t *testing.T) {
|
||||
written, err := Codecs(reflect.TypeOf(optionalID{}))
|
||||
written, err := Layouts(reflect.TypeOf(optionalID{}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package codec
|
||||
package layout
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -93,14 +93,14 @@ func trunks() []Trunk {
|
||||
Text: "hello", Raw: []byte{1, 2, 3},
|
||||
Leaf: Leaf{N: 1, S: "leaf"},
|
||||
PtrU: &u, PtrS: &s, PtrB: &b, PtrL: &Leaf{N: 2, S: "ptr"},
|
||||
Nums: []uint32{1, 2, 3},
|
||||
Sigs: []int32{-1, 0, 1},
|
||||
Bigs: []float64{1.5, -2.5},
|
||||
Strs: []string{"a", "bb"},
|
||||
Bufs: [][]byte{{9}, {8, 7}},
|
||||
Bits: []bool{true, false, true},
|
||||
Kids: []Leaf{{N: 3, S: "x"}, {N: 4, S: "y"}},
|
||||
Ptrs: []*Leaf{{N: 5, S: "p"}, {N: 6, S: "q"}},
|
||||
Nums: []uint32{1, 2, 3},
|
||||
Sigs: []int32{-1, 0, 1},
|
||||
Bigs: []float64{1.5, -2.5},
|
||||
Strs: []string{"a", "bb"},
|
||||
Bufs: [][]byte{{9}, {8, 7}},
|
||||
Bits: []bool{true, false, true},
|
||||
Kids: []Leaf{{N: 3, S: "x"}, {N: 4, S: "y"}},
|
||||
Ptrs: []*Leaf{{N: 5, S: "p"}, {N: 6, S: "q"}},
|
||||
Seals: []Sealed{{}, {}},
|
||||
PKids: &[]Leaf{{N: 7, S: "pk"}},
|
||||
PNums: &[]uint32{11, 12},
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package codec holds the types that [zip.Codecs] is proved against.
|
||||
// Package layout holds the types that [zip.Layouts] is proved against.
|
||||
//
|
||||
// The proof it exists for is byte equality: for a type the REFLECTIVE encoder
|
||||
// can write, the generated codec must write the same bytes and read back the
|
||||
@@ -10,7 +10,7 @@
|
||||
// The fixtures cover every slot form the emitter has a branch for, plus the one
|
||||
// the reflective encoder refuses outright: bytes_fixed[N], where there are no
|
||||
// bytes to compare against because there are none at all.
|
||||
package codec
|
||||
package layout
|
||||
|
||||
// Count is a defined scalar, which needs a conversion on the way in and out.
|
||||
type Count uint64
|
||||
@@ -31,33 +31,33 @@ type Sealed struct {
|
||||
|
||||
// Trunk carries one field of every form the emitter writes.
|
||||
type Trunk struct {
|
||||
B bool
|
||||
I8 int8
|
||||
I16 int16
|
||||
I32 int32
|
||||
I64 int64
|
||||
U8 uint8
|
||||
U16 uint16
|
||||
U32 uint32
|
||||
U64 uint64
|
||||
F32 float32
|
||||
F64 float64
|
||||
C Count
|
||||
Text string
|
||||
Raw []byte
|
||||
Leaf Leaf
|
||||
PtrU *uint64
|
||||
PtrS *string
|
||||
PtrB *bool
|
||||
PtrL *Leaf
|
||||
Nums []uint32
|
||||
Sigs []int32
|
||||
Bigs []float64
|
||||
Strs []string
|
||||
Bufs [][]byte
|
||||
Bits []bool
|
||||
Kids []Leaf
|
||||
Ptrs []*Leaf
|
||||
B bool
|
||||
I8 int8
|
||||
I16 int16
|
||||
I32 int32
|
||||
I64 int64
|
||||
U8 uint8
|
||||
U16 uint16
|
||||
U32 uint32
|
||||
U64 uint64
|
||||
F32 float32
|
||||
F64 float64
|
||||
C Count
|
||||
Text string
|
||||
Raw []byte
|
||||
Leaf Leaf
|
||||
PtrU *uint64
|
||||
PtrS *string
|
||||
PtrB *bool
|
||||
PtrL *Leaf
|
||||
Nums []uint32
|
||||
Sigs []int32
|
||||
Bigs []float64
|
||||
Strs []string
|
||||
Bufs [][]byte
|
||||
Bits []bool
|
||||
Kids []Leaf
|
||||
Ptrs []*Leaf
|
||||
Seal Sealed
|
||||
Seals []Sealed
|
||||
PKids *[]Leaf
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by zip; DO NOT EDIT.
|
||||
|
||||
package codec
|
||||
package layout
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
+12
-12
@@ -1,7 +1,7 @@
|
||||
package zip
|
||||
|
||||
// The Go codec — the source that STATES a type's ZAP wire instead of having one
|
||||
// derived from it at every call.
|
||||
// Layouts — the source that STATES a type's ZAP wire instead of deriving one at
|
||||
// every call. Nothing here runs on the served path; this is a generator.
|
||||
//
|
||||
// [LayoutOf] names three readers of the one derivation: the plane encodes
|
||||
// against it, a .zap schema states it as `Name type @Offset`, and a generator
|
||||
@@ -32,7 +32,7 @@ package zip
|
||||
// for field and in the same order, for every field the reflective encoder can
|
||||
// write at all. That is not a promise about intent — it falls out of reading the
|
||||
// offsets from [LayoutOf] rather than deriving them a second time, and
|
||||
// TestTheCodecKeepsTheWire holds it against the reflective encoder directly.
|
||||
// TestTheLayoutKeepsTheWire holds it against the reflective encoder directly.
|
||||
//
|
||||
// The sequence it reproduces: every list is written FIRST, because SetList takes
|
||||
// an offset that must already exist; then the object is started at the declared
|
||||
@@ -58,9 +58,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Codec is one emitted file: the Go source stating the wire of every type in one
|
||||
// Layout is one emitted file: the Go source stating the wire of every type in one
|
||||
// package, and the names of the types it states.
|
||||
type Codec struct {
|
||||
type Layout struct {
|
||||
Path string // the package's import path
|
||||
Package string // its package clause
|
||||
Types []string // the types declared, alphabetically
|
||||
@@ -74,7 +74,7 @@ type Codec struct {
|
||||
Stated bool
|
||||
}
|
||||
|
||||
// Codecs renders a [Wire] implementation for each of roots and for every struct
|
||||
// Layouts renders a [Wire] implementation for each of roots and for every struct
|
||||
// nested below them, grouped by the package that owns the type.
|
||||
//
|
||||
// A nested value is reached BY ITS METHOD, so the set is closed downward: a
|
||||
@@ -87,7 +87,7 @@ type Codec struct {
|
||||
// is no layout to state and a codec that guessed one would speak a wire nobody
|
||||
// reads. Answering that is a change to the TYPE, which is not a generator's to
|
||||
// make.
|
||||
func Codecs(roots ...reflect.Type) ([]Codec, error) {
|
||||
func Layouts(roots ...reflect.Type) ([]Layout, error) {
|
||||
want := map[reflect.Type]bool{}
|
||||
var refused []string
|
||||
for _, r := range roots {
|
||||
@@ -104,7 +104,7 @@ func Codecs(roots ...reflect.Type) ([]Codec, error) {
|
||||
for t := range want {
|
||||
byPath[t.PkgPath()] = append(byPath[t.PkgPath()], t)
|
||||
}
|
||||
out := make([]Codec, 0, len(byPath))
|
||||
out := make([]Layout, 0, len(byPath))
|
||||
for path, ts := range byPath {
|
||||
sort.Slice(ts, func(i, j int) bool { return ts[i].Name() < ts[j].Name() })
|
||||
c, err := one(path, ts, want)
|
||||
@@ -308,7 +308,7 @@ func (p *pkg) spell(t reflect.Type) string {
|
||||
return t.String()
|
||||
}
|
||||
|
||||
func one(path string, ts []reflect.Type, want map[reflect.Type]bool) (Codec, error) {
|
||||
func one(path string, ts []reflect.Type, want map[reflect.Type]bool) (Layout, error) {
|
||||
p := &pkg{
|
||||
self: path,
|
||||
alias: map[string]string{},
|
||||
@@ -319,7 +319,7 @@ func one(path string, ts []reflect.Type, want map[reflect.Type]bool) (Codec, err
|
||||
names := make([]string, 0, len(ts))
|
||||
for _, t := range ts {
|
||||
if err := declare(&body, t, p); err != nil {
|
||||
return Codec{}, err
|
||||
return Layout{}, err
|
||||
}
|
||||
names = append(names, t.Name())
|
||||
}
|
||||
@@ -352,9 +352,9 @@ func one(path string, ts []reflect.Type, want map[reflect.Type]bool) (Codec, err
|
||||
|
||||
src, err := format.Source(out.Bytes())
|
||||
if err != nil {
|
||||
return Codec{}, fmt.Errorf("zip: emitted source for %s does not parse: %w\n%s", path, err, out.String())
|
||||
return Layout{}, fmt.Errorf("zip: emitted source for %s does not parse: %w\n%s", path, err, out.String())
|
||||
}
|
||||
return Codec{Path: path, Package: clause, Types: names, Source: src}, nil
|
||||
return Layout{Path: path, Package: clause, Types: names, Source: src}, nil
|
||||
}
|
||||
|
||||
// stated is the compile-time assertion that a type answers for its own bytes.
|
||||
@@ -8,22 +8,22 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/zap-proto/zip"
|
||||
"github.com/zap-proto/zip/internal/codec"
|
||||
"github.com/zap-proto/zip/internal/layout"
|
||||
)
|
||||
|
||||
// TestTheCodecIsCheckedIn regenerates internal/codec/zap_gen.go and requires the
|
||||
// TestTheLayoutIsCheckedIn regenerates internal/layout/zap_gen.go and requires the
|
||||
// file on disk to match. The generated codec is the wire those types speak, so a
|
||||
// change to the types that nobody regenerated is a wire that moved under its
|
||||
// readers — and this is where that is noticed, not in a rolling deploy.
|
||||
func TestTheCodecIsCheckedIn(t *testing.T) {
|
||||
got, err := zip.Codecs(reflect.TypeOf(codec.Trunk{}), reflect.TypeOf(codec.Ided{}))
|
||||
func TestTheLayoutIsCheckedIn(t *testing.T) {
|
||||
got, err := zip.Layouts(reflect.TypeOf(layout.Trunk{}), reflect.TypeOf(layout.Ided{}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("got %d packages, want 1", len(got))
|
||||
}
|
||||
path := filepath.Join("internal", "codec", "zap_gen.go")
|
||||
path := filepath.Join("internal", "layout", "zap_gen.go")
|
||||
want, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -35,7 +35,7 @@ func TestTheCodecIsCheckedIn(t *testing.T) {
|
||||
}
|
||||
t.Fatal("wrote " + path + "; run again")
|
||||
}
|
||||
t.Errorf("%s is stale; regenerate with WRITE=1 go test -run TestTheCodecIsCheckedIn ./...", path)
|
||||
t.Errorf("%s is stale; regenerate with WRITE=1 go test -run TestTheLayoutIsCheckedIn ./...", path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ func TestTheCodecIsCheckedIn(t *testing.T) {
|
||||
// only proves it PARSES; a codec that names a field that moved, or converts a
|
||||
// value the type will not take, parses and does not build.
|
||||
func TestTheEmittedSourceCompiles(t *testing.T) {
|
||||
out, err := exec.Command("go", "build", "./internal/codec").CombinedOutput()
|
||||
out, err := exec.Command("go", "build", "./internal/layout").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("go build ./internal/codec: %v\n%s", err, out)
|
||||
t.Fatalf("go build ./internal/layout: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
Co-authored-by: Hanzo Dev <[email protected]>
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
Co-authored-by: Hanzo Dev <[email protected]>
|
||||
+1
-1
@@ -455,7 +455,7 @@ func TestSDK_AWholeAppStillGeneratesAroundAGap(t *testing.T) {
|
||||
// wire, which is the failure this projection exists to remove. It is also not
|
||||
// fixable by generating harder: the declared type's MarshalZAP lives in the
|
||||
// service's package, and restating the field as [32]uint8 restates the bytes and
|
||||
// not the codec.
|
||||
// not the layout.
|
||||
func TestSDK_AnIdCrossesOnTheCodecTheSDKWrites(t *testing.T) {
|
||||
type ID [32]byte
|
||||
type Tx struct {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// The fixture these tests share.
|
||||
//
|
||||
// Declared at package scope, because reflect gives a type declared inside a
|
||||
// function no package path — and [zip.Codecs] groups by the package that owns
|
||||
// function no package path — and [zip.Layouts] groups by the package that owns
|
||||
// the type, which is the question here: the SDK restates these types in a
|
||||
// package of its own, and the codec it writes beside them has to be the codec
|
||||
// zip writes here.
|
||||
@@ -63,7 +63,7 @@ type Ided struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TestSDK_TheCodecIsTheOneZipEmits is the wire claim stated as source. A Go
|
||||
// TestSDK_TheLayoutIsTheOneZipEmits is the wire claim stated as source. A Go
|
||||
// client and the Rust and C++ clients speak to the same service, so what the
|
||||
// SDK writes for an id has to be what zip writes for it — not something that
|
||||
// also happens to work.
|
||||
@@ -72,7 +72,7 @@ type Ided struct {
|
||||
// codec" is literal: the section is byte-identical, because it is the same
|
||||
// emitter reading the same [LayoutOf] offsets. Anything less than identical
|
||||
// here is a second wire.
|
||||
func TestSDK_TheCodecIsTheOneZipEmits(t *testing.T) {
|
||||
func TestSDK_TheLayoutIsTheOneZipEmits(t *testing.T) {
|
||||
app := zip.New(zip.Config{AppName: "p", DisableStartupMessage: true})
|
||||
zip.Post(app, "/v1/tx", func(_ context.Context, in *Tx) (*Tx, error) { return in, nil },
|
||||
zip.WithOperationID("get_tx"))
|
||||
@@ -81,9 +81,9 @@ func TestSDK_TheCodecIsTheOneZipEmits(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("SDK: %v", err)
|
||||
}
|
||||
native, err := zip.Codecs(reflect.TypeOf(Tx{}))
|
||||
native, err := zip.Layouts(reflect.TypeOf(Tx{}))
|
||||
if err != nil {
|
||||
t.Fatalf("Codecs: %v", err)
|
||||
t.Fatalf("Layouts: %v", err)
|
||||
}
|
||||
if len(native) != 1 {
|
||||
t.Fatalf("got %d packages, want 1", len(native))
|
||||
@@ -197,9 +197,9 @@ func TestSDK_TheTwoCodecsWriteTheSameBytes(t *testing.T) {
|
||||
if len(sdk.Gaps) != 0 {
|
||||
t.Fatalf("gaps: %+v", sdk.Gaps)
|
||||
}
|
||||
native, err := zip.Codecs(reflect.TypeOf(Tx{}), reflect.TypeOf(Ided{}))
|
||||
native, err := zip.Layouts(reflect.TypeOf(Tx{}), reflect.TypeOf(Ided{}))
|
||||
if err != nil {
|
||||
t.Fatalf("Codecs: %v", err)
|
||||
t.Fatalf("Layouts: %v", err)
|
||||
}
|
||||
// The emitter names the package the TYPES live in, and in the probe they
|
||||
// live in svc. That one line is the only edit; the codec below it is the
|
||||
@@ -481,7 +481,7 @@ func fail(format string, a ...any) {
|
||||
// "reached by its method". A type whose fields are all unexported — a time.Time,
|
||||
// a netip.AddrPort — has no slots, so it crosses as a complete and EMPTY object
|
||||
// that its parent writes inline. There is nothing to state and no method to
|
||||
// reach for, and [Codecs] answers the same way. A codec here that called
|
||||
// reach for, and [Layouts] answers the same way. A codec here that called
|
||||
// MarshalZAP on it would be a method that does not exist.
|
||||
func TestSDK_AValueWithNoSlotsIsStillWrittenInline(t *testing.T) {
|
||||
type stamped struct {
|
||||
@@ -39,7 +39,7 @@ import (
|
||||
// bytes_fixed[32] of a .zap schema — crosses the plane on a codec or not at all.
|
||||
// A type implementing Wire writes it inline (zap.ObjectBuilder.SetBytesFixed) and
|
||||
// reads it back as a slice of the buffer that arrived (zap.Object.BytesFixed).
|
||||
// [Codecs] writes that pair for a service's own types, and [App.SDK] writes it
|
||||
// [Layouts] writes that pair for a service's own types, and [App.SDK] writes it
|
||||
// for the restatement a generated client holds.
|
||||
//
|
||||
// The compatibility rule does not change: the layout is still the type, so
|
||||
@@ -77,7 +77,7 @@ type Shape = zapenc.Shape
|
||||
// relative offset. Packing, or giving a nested value four bytes, moves every
|
||||
// field after it.
|
||||
//
|
||||
// bytes_fixed[N] is laid out here and is NOT carried by the reflective codec. The
|
||||
// bytes_fixed[N] is laid out here and is NOT carried by the reflective layout. The
|
||||
// layout is what a schema and a generator need; refusing to encode it is what
|
||||
// makes an id — an ids.ID is [32]byte — force a type to declare its own wire
|
||||
// rather than quietly acquiring a reflective one.
|
||||
|
||||
Reference in New Issue
Block a user