Skip to content

Latest commit

 

History

History
173 lines (136 loc) · 4.6 KB

File metadata and controls

173 lines (136 loc) · 4.6 KB

API Codec: Encodable / Decodable (0-alloc)

The model package defines a typed contract for serialization and deserialization, designed to be 0-alloc, map-free, and reflection-less.

This contract allows models to cross boundaries (JSON, WebAssembly/JavaScript) efficiently by having the models themselves describe how to write and read their fields.

See also: CODEC_AND_FIELDER.md for how the codec relates to Field/Fielder, and WHY_64BIT_ONLY.md for why int64/float64 are the only numeric types.

Core Interfaces

Encodable

A type that knows how to write its own fields to a FieldWriter.

type Encodable interface {
    EncodeFields(w FieldWriter)
    IsNil() bool
}

Decodable

A type that knows how to read its own fields from a FieldReader.

type Decodable interface {
    DecodeFields(r FieldReader)
    IsNil() bool
}

DecodeFields only populates — it never fails. Structural errors (malformed input) are accumulated by the reader and returned by the entrypoint (e.g. json.Decode). Missing or wrong-type fields produce ok=false from the reader and are silently skipped. Semantic validation lives in Validate().

Field Access

FieldWriter

Receives typed field values. Concrete implementations (like JSON or JS-value) reuse their own buffers to achieve zero allocations.

type FieldWriter interface {
    String(name, val string)
    Int(name string, val int64)
    Float(name string, val float64)
    Bool(name string, val bool)
    Bytes(name string, val []byte)
    Null(name string)
    Raw(name, val string)
    Object(name string, val Encodable)
    Array(name string, n int) ArrayWriter
}

ArrayWriter

Used within FieldWriter.Array() to emit array elements.

type ArrayWriter interface {
    String(val string)
    Int(val int64)
    Float(val float64)
    Bool(val bool)
    Bytes(val []byte)
    Object(val Encodable)
    Close()
}

FieldReader

Delivers typed field values by name. Implementations avoid building internal maps for lookups.

type FieldReader interface {
    String(name string) (string, bool)
    Int(name string) (int64, bool)
    Float(name string) (float64, bool)
    Bool(name string) (bool, bool)
    Bytes(name string) ([]byte, bool)
    Object(name string, into Decodable) bool
    Array(name string) (ArrayReader, bool)
    Raw(name string) (string, bool)
}

ArrayReader

Used within FieldReader.Array() to read array elements.

type ArrayReader interface {
    Len() int
    String(i int) string
    Int(i int) int64
    Float(i int) float64
    Bool(i int) bool
    Bytes(i int) []byte
    Object(i int, into Decodable) bool
}

Design Principles

  1. Zero Allocations: The coding path (visitor pattern) does not allocate on the Go heap. No any boxing, no map overhead.
  2. Map-Free: Prohibited in the contract to avoid dragging the hashmap runtime into TinyGo/WASM binaries.
  3. Reflection-Less: Types are handled via explicit, typed method calls.
  4. Agnostic: model only defines the interfaces. Concrete codecs are implemented in:
    • tinywasm/json: Canonical JSON encoder/decoder.
    • tinywasm/jsvalue: JavaScript boundary (WASM).

Nil Checking

Both Encodable and Decodable require an IsNil() method for portable nil checking:

func IsNil(v any) bool {
    if v == nil {
        return true
    }
    if enc, ok := v.(Encodable); ok {
        return enc.IsNil()
    }
    if dec, ok := v.(Decodable); ok {
        return dec.IsNil()
    }
    return false
}

This allows codecs to distinguish between a nil interface value and a pointer receiver that is nil (e.g., (*User)(nil)).

Example Implementation

Usually generated by ormc:

func (u *User) EncodeFields(w model.FieldWriter) {
    w.String("id", u.ID)
    w.String("name", u.Name)
    w.Int("age", int64(u.Age))
    if len(u.Tags) > 0 {
        aw := w.Array("tags", len(u.Tags))
        for _, tag := range u.Tags {
            aw.String(tag)
        }
        aw.Close()
    }
}

func (u *User) DecodeFields(r model.FieldReader) {
    if v, ok := r.String("id"); ok { u.ID = v }
    if v, ok := r.String("name"); ok { u.Name = v }
    if v, ok := r.Int("age"); ok { u.Age = int(v) }
    if ar, ok := r.Array("tags"); ok {
        u.Tags = make([]string, ar.Len())
        for i := 0; i < ar.Len(); i++ {
            u.Tags[i] = ar.String(i)
        }
    }
}

func (u *User) IsNil() bool {
    return u == nil
}

See Also