clientcodec

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package clientcodec generates the browser runtime's protobuf codec, its predicate manifest, and the cross-runtime golden vectors, from the same FileDescriptorSet that drives the Go refinement generator.

Why generated

The bugs this code attracts are a wrong field number, a wrong wire type, or a field somebody forgot — and none of them is visible in review. Reading the descriptors makes the client and the server incapable of disagreeing about the wire, and makes "add a field" a regeneration rather than a transcription (docs/protocol.md §10.2).

What it emits

client/codec.gen.js             the codec: schema table, encoder, decoder
client/predicates.manifest.txt  every predicate, and who enforces it
client/test/golden.json         Go-encoded vectors for the JS round-trip

All three are committed. Consumers of this module never run this package, and the browser never sees a code generator.

A schema table, not straight-line code

The codec is a compact schema string plus one generic encoder and one generic decoder that interpret it. Straight-line per-field code would be bigger and would grow linearly with the schema; a table grows by one line per field and compresses, which is what a 12,288-byte gzip ceiling (PRD NFR-2) actually rewards. It is still generated code: every field number, wire kind and length bound in the table comes from the descriptors, which is the property docs/protocol.md §10.2 asks for.

Predicate enforcement is directional

docs/protocol.md §10.3 fixes the line: the client enforces length bounds on decode, because the decoder has already read a length prefix and the check costs two comparisons; it enforces nothing else, because an RE2 engine and a predicate evaluator are not in the byte budget. That asymmetry is emitted as a manifest a reviewer can read rather than left as an unwritten assumption.

Determinism

Regenerating twice must be byte-identical: descriptors are walked in declaration order, nothing is ranged over a map, and no clock, hostname or path enters the output. A spec asserts it.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EmitCodec

func EmitCodec(s *Schema) []byte

EmitCodec renders client/codec.gen.js.

The file is three parts: a generated schema table, a generated enum block, and a fixed encoder/decoder pair that interprets the table. Only the first two vary with the schema, which is the point — the wire knowledge is data read out of the descriptors, and the code that consumes it never has to be re-reviewed when a field is added.

func EmitGolden

func EmitGolden() ([]byte, error)

EmitGolden renders client/test/golden.json.

The frames below are built with the Go runtime, marshalled, and then read back through protoreflect to produce the expected JavaScript-shaped value. Reading it back rather than writing it by hand is deliberate: the expected object then omits exactly the fields proto3 omits on the wire, so a disagreement about default-value handling shows up as a test failure instead of being encoded into the fixture by whoever wrote it.

func EmitManifest

func EmitManifest(s *Schema) []byte

EmitManifest renders client/predicates.manifest.txt.

docs/protocol.md §10.3 requires this file and requires CI to fail when it drifts from the descriptors. Its job is to make the enforcement asymmetry a generated artifact a reviewer can read, rather than a paragraph somebody has to remember.

func Write

func Write(dir string, artifacts []Artifact) error

Write writes artifacts under dir, creating directories as needed.

Types

type Artifact

type Artifact struct {
	// Path is relative to the output directory and is part of the generated
	// output: gen.sh compares it, so a renamed artifact is a diff rather than
	// a silently orphaned file.
	Path string

	// Data is the file's exact bytes. Generating into memory is what makes the
	// determinism spec cheap: it generates twice and compares these.
	Data []byte
}

Artifact is one generated file: a path relative to the output directory and its exact bytes.

func Generate

func Generate(descriptorSetPath string) ([]Artifact, error)

Generate produces every artifact from a descriptor set, without writing anything. Returning the bytes rather than writing them is what makes the determinism spec cheap — it can generate twice and compare, with no temporary directories and no filesystem in the assertion.

type Conjunct

type Conjunct struct {
	// Text is the term's source, verbatim.
	Text string
	// Class is length, numeric or matches — the three rows of
	// docs/protocol.md §10.3's table.
	Class string
	// Enforced records whether the generated client decoder checks it. Only
	// length conjuncts on string and bytes fields are.
	Enforced bool
}

Conjunct is one `&&`-separated term of a predicate.

type Enum

type Enum struct {
	// Name is the enum's proto name, unqualified.
	Name string

	// Values are its non-zero members in declaration order.
	Values []EnumValue
}

Enum is an enumeration the client reads or writes. The zero member is dropped: docs/protocol.md §3.5 says it is never valid on the wire, so emitting it would only invite somebody to send it.

type EnumValue

type EnumValue struct {
	// Name is the member's proto name, emitted verbatim.
	Name string

	// Number is the value on the wire.
	Number int32
}

EnumValue is one non-zero member.

type Field

type Field struct {
	// Number is the proto field number, which is what actually appears on the
	// wire and therefore what the decoder dispatches on.
	Number int
	// Kind is the single-letter wire kind the emitted table uses:
	//
	//	v  varint scalar or enum      b  bool
	//	s  string                     y  bytes
	//	m  singular message           r  repeated message
	//	p  repeated packed varint
	Kind string

	// Name is the field's proto name. The emitted JavaScript uses it verbatim,
	// so the client's object keys are the schema's names rather than a
	// camel-cased translation of them.
	Name string
	// Arg is "min:max" for s and y when a length predicate bounds them, the
	// message name for m and r, and empty otherwise.
	Arg string
}

Field is one field, reduced to what a codec needs: where it sits, how it is framed, what to call it, and — for length-delimited scalars — the bound the decoder enforces.

type Message

type Message struct {
	// Name is the message's proto name, unqualified, and the identifier the
	// emitted codec's table is keyed by.
	Name string

	// Fields are its fields in declaration order, not field-number order: the
	// emitted table's order is part of the generated bytes.
	Fields []Field
}

Message is one wire message and its fields.

type Predicate

type Predicate struct {
	// Message is the unqualified name of the message the field belongs to.
	Message string

	// Field is the field's proto name. The pair identifies the row in the
	// manifest.
	Field string

	// Expr is the predicate source text from the .proto option, verbatim. It
	// is reproduced rather than rewritten so the manifest quotes what a
	// reviewer will find in the schema.
	Expr string

	// Conjuncts is Expr split on `&&`, because enforcement is decided term by
	// term: a field can carry a length bound the client checks and a regular
	// expression it does not.
	Conjuncts []Conjunct
}

Predicate is one field's refinement expression, split into the conjuncts the manifest reports individually. Splitting matters: a field can carry a length bound the client enforces and a regular expression it does not, and a manifest that reported one verdict per field could not say so.

type Schema

type Schema struct {
	// Messages are the wire messages, in declaration order.
	Messages []Message

	// Enums are the enumerations the client reads or writes, in declaration
	// order and without their zero members.
	Enums []Enum

	// Predicates are the refinement expressions, one per refined field, in the
	// order the fields were declared. They drive the manifest rather than the
	// codec: most of them are not enforced client-side, and the manifest is
	// where that is said out loud.
	Predicates []Predicate
}

Schema is the whole input to code generation, in the order it was declared. Order is the determinism guarantee: nothing below is sorted, keyed by map, or otherwise given a chance to vary between runs.

func Load

func Load(descriptorSetPath string) (*Schema, error)

Load reads a FileDescriptorSet — the same one that drives protoc-gen-liquidproto — and reduces it to a Schema.

type Vector

type Vector struct {
	// Name identifies the case in the JS test's failure output.
	Name string `json:"name"`

	// Note says what the case is for, so a failure names the property rather
	// than only the vector.
	Note string `json:"note"`

	// Hex is the canonical Go encoding of Frame, hex-encoded because the
	// fixture is JSON.
	Hex string `json:"hex"`

	// Frame is the expected decoded value in JavaScript shape, read back
	// through protoreflect rather than written by hand so that it omits
	// exactly the fields proto3 omits on the wire.
	Frame map[string]any `json:"frame"`

	// Reencode asks the JS test for the harder half: encode Frame and require
	// the bytes to equal Hex. A vector without it checks only that the client
	// can read what Go writes.
	Reencode bool `json:"reencode"`
}

Vector is one cross-runtime round-trip case.

Hex is what the Go protobuf runtime produced for Frame. The JS test decodes it and asserts the result equals Frame, which checks Go→JS; when Reencode is set it also encodes Frame and asserts the bytes come back byte-identical to Hex, which checks JS→Go — byte equality with the canonical Go encoding is a stronger statement than "Go can parse it", because it also rules out a client that is merely tolerantly parseable.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL