model

package module
v0.0.7 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 1 Imported by: 0

README

tinywasm/model

Core schema, validation, and codec types for tinywasm ecosystem. This package provides the foundational interfaces and types used across ORM, JSON serialization, form handling, and API layers.

Overview

model provides the foundational type definitions and contracts for the tinywasm ecosystem. It focuses on:

  • Schema Metadata - Field, Fielder, and FieldType for reflection-less struct introspection
  • Validation - Permitted and Validator for data integrity
  • Typed Serialization - Encodable, Decodable, FieldWriter, FieldReader for 0-alloc codec patterns

Key Features

  • 📐 Model Definition - Author schemas manually with Definition and Fields
  • 🏗️ Schema-first design - Describe struct fields without reflection
  • Validation by contract - Character whitelists, length constraints, custom validators
  • 🔄 Typed codecs - Encodable/Decodable for 0-alloc serialization
  • 🧵 Concurrency safe - Thread-safe schema metadata and codecs
  • 📦 Zero dependencies - No reflection, no maps, no any boxing in codec paths
  • WebAssembly optimized - Small binary footprint for WASM targets

Installation

go get github.com/tinywasm/model

Quick Start

Defining a Model Definition

The source of truth for a model is a model.Definition literal. This package uses fail-closed validation: validation used to be opt-in (Widget: nil meant no validation), which contradicted the ecosystem's design doctrine. Now, every field has a Type (Kind) that provides a baseline validation floor.

var UserModel = model.Definition{
    Name: "user",
    Fields: model.Fields{
        {Name: "id",      Type: model.Int(),  DB: &model.FieldDB{PK: true, AutoInc: true}},
        {Name: "name",    Type: model.Text(), NotNull: true, Permitted: model.Permitted{Minimum: 2}},
        {Name: "email",   Type: model.Text(), NotNull: true},
        {Name: "address", Type: model.Struct(&AddressModel)}, // composition
        {Name: "staff_id", Type: model.Int(), Ref: &StaffModel,
            DB: &model.FieldDB{RefColumn: "id"}}, // scalar FK: Go type stays int64
    },
}

Why fail-closed? Validation is no longer optional. A resource left reachable because nobody explicitly configured a widget is a silent failure. Standard kinds like model.Text() provide an input-boundary XSS floor by default. Use model.Raw() or model.Blob() only when you explicitly need an unvalidated escape hatch — these are easily auditable via grep.

The generated Schema

ormc reads the Definition var and generates the row struct plus the Fielder methods. The schema is authored once: the generated Schema() points back at the Definition — it never re-emits the field literals.

// Generated by ormc from UserModel — do not edit.

type User struct {
    ID   int64
    Name string
}

func (u *User) Schema() []model.Field { return UserModel.Fields } // single source of truth

func (u *User) Pointers() []any { return []any{&u.ID, &u.Name} }

func (u *User) Validate(action byte) error {
    return model.ValidateFields(action, u)
}

The struct exists because the ecosystem forbids reflect: something concrete must map row fields to Schema()/Pointers()/codec, and that something is generated, not hand-written. The only case where Schema() does not return XModel.Fields directly is when the Definition has Exclude: true fields: Schema() and Pointers() are parallel arrays, so ormc emits one filtered _schemaX var with the excluded fields dropped from both.

Usage:

err := model.ValidateFields(model.ActionUpdate, user)
Using the Codec

Generated EncodeFields and DecodeFields methods follow the typed codec pattern:

func (u *User) EncodeFields(w model.FieldWriter) {
    w.String("id", u.ID)
    if u.Name != "" {
        w.String("name", u.Name)
    }
    w.Int("age", int64(u.Age))
}

func (u *User) DecodeFields(r model.FieldReader) {
    if id, ok := r.String("id"); ok {
        u.ID = id
    }
    if name, ok := r.String("name"); ok {
        u.Name = name
    }
    if age, ok := r.Int("age"); ok {
        u.Age = int(age)
    }
}

Documentation

Architecture

Separation of Concerns:

Concern Type Used By
DDL & Column Metadata Field.DB orm, sqlt, postgres
Data Validation Field.Type + Permitted orm, form, json
UI Bindings Field.Type (Kind) form, ormc codegen
SQL Scanning Pointers() orm/qb (positional via database/sql)
Serialization Encodable/Decodable + codec json, jsvalue, transports

One schema (Field) serves all layers. Serialization uses the typed codec (Encodable/Decodable) for 0-alloc, map-free performance.


Contributing


License

Documentation

Index

Constants

View Source
const (
	ActionCreate byte = 'c'
	ActionRead   byte = 'r'
	ActionUpdate byte = 'u'
	ActionDelete byte = 'd'
)

CRUD action bytes — the single source of truth for the ecosystem-wide action convention used by Validator, ValidateFields, and downstream libraries (crudp HTTP mapping, mcp Tool.Action / RBAC, form).

Deliberately plain byte constants (not a named type) so every existing `action byte` signature accepts them unchanged.

Not to be confused with orm.Action (an int enum describing query execution plans) — that is a different, orm-internal concept.

Variables

This section is empty.

Functions

func IsNil added in v0.0.2

func IsNil(v any) bool

IsNil comprueba de forma segura y portable si un valor es nil o si es un puntero nil dentro de una interfaz Encodable o Decodable.

func IsZeroPtr added in v0.0.2

func IsZeroPtr(ptr any, ft FieldType) bool

IsZeroPtr checks if a pointer points to a zero value.

func ReadStringPtr added in v0.0.2

func ReadStringPtr(ptr any) (string, bool)

ReadStringPtr reads a string from a typed pointer. Returns the string value and true if the pointer is *string, or ("", false) otherwise.

func ReadValues added in v0.0.2

func ReadValues(schema []Field, ptrs []any) []any

ReadValues reads field values through Pointers by dereferencing based on Storage type. Used by consumers that need []any (e.g., orm for SQL args). Hot-path consumers (json, form) should read through Pointers directly to avoid boxing.

func ValidateFields added in v0.0.2

func ValidateFields(action byte, f Fielder) error

ValidateFields validates all fields of a Fielder based on the action. For each FieldText field, reads the string value and calls Field.Validate. For non-text fields with NotNull, checks against zero value.

Common actions are ActionCreate, ActionUpdate, and ActionDelete.

This is the single validation entry point — ormc-generated Validate() methods call this function first.

Types

type ArrayReader added in v0.0.2

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
}

ArrayReader recorre un array tipado.

type ArrayWriter added in v0.0.2

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

ArrayWriter empuja elementos tipados de un array (sin []any).

type Decodable added in v0.0.2

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

Decodable: un valor que sabe leer SUS campos (lo genera ormc).

type Definition added in v0.0.3

type Definition struct {
	Name   string // model identity: table name, ModelName(), route key
	Fields Fields // ordered schema; Kinds come from model (e.g. model.Text())
}

Definition is the hand-written, fully-typed source of truth for a model. From a Definition, the ormc generator produces the concrete Go struct and its zero-reflection plumbing (Schema/Pointers/EncodeFields/DecodeFields/List).

This inverts the previous flow (struct + string tags → generated schema): the schema literal is now authored by hand and everything else is derived.

func (Definition) Field added in v0.0.3

func (d Definition) Field(name string) (Field, bool)

Field returns the field with the given name and true, or a zero Field and false.

type Encodable added in v0.0.2

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

Encodable: un valor que sabe escribir SUS campos (lo genera ormc).

type Field added in v0.0.2

type Field struct {
	Name      string
	Type      Kind
	NotNull   bool
	OmitEmpty bool     // omit from JSON when zero value
	DB        *FieldDB // nil for formonly/transport structs
	// Ref is the Definition of the table this column references (scalar foreign key).
	// e.g. a "staff_id int64" field pointing to StaffModel. It drives DDL FK constraint
	// generation (orm.FieldExt/SchemaExt) — see FieldDB.RefColumn/OnDelete.
	// It does NOT change the field's Go type, which stays the plain scalar mapping from Type.
	//
	// Composition (FieldStruct/FieldStructSlice) no longer uses this slot; the nested
	// Definition is now a mandatory parameter of the Kind constructor (model.Struct(ref)).
	// Setting Field.Ref on a composition field is a contradiction and results in a
	// generation error in ormc.
	//
	// A *Definition with a bidirectional Ref to another package-level Definition in the same
	// package cannot be expressed as a single literal (Go rejects it: "initialization cycle") —
	// this is a Go language limitation, not a model design gap. Two-phase assignment via init()
	// is a workaround but is out of scope for ormc to auto-detect; report it if ever needed.
	Ref     *Definition
	Exclude bool // field exists on the generated struct but is NOT part of
	// Pointers()/EncodeFields()/DecodeFields() — no persistence, no wire codec.
	// Use for hand-managed data the struct must carry but ormc must not touch
	// (e.g. a password hash set via a side channel, never scanned/serialized).
	Permitted // embedded: validation rules (characters, min/max)
}

Field describes a single field in a struct's schema. It provides type metadata, constraint flags, and validation rules used by database (orm), transport (json), UI (form), and validation layers.

Validation rules are provided by its Type (Kind) and embedded via Permitted. Field.Validate(value) checks both. A nil Type (Kind) is an error — the ecosystem is fail-closed by design.

Deterministic Field.Type.Storage() → Go type mapping:

| Storage | Go Type | |---|---| | FieldText, FieldRaw | string | | FieldInt | int64 | | FieldFloat | float64 | | FieldBool | bool | | FieldBlob | []byte | | FieldIntSlice | []int | | FieldStruct | type of the kind's ref — Struct(ref) | | FieldStructSlice | [] of the kind's ref — StructSlice(ref) |

func (Field) IsAutoInc added in v0.0.2

func (f Field) IsAutoInc() bool

IsAutoInc returns true if the field is Auto-Increment.

func (Field) IsPK added in v0.0.2

func (f Field) IsPK() bool

IsPK returns true if the field is a Primary Key.

func (Field) IsUnique added in v0.0.2

func (f Field) IsUnique() bool

IsUnique returns true if the field has a Unique constraint.

func (Field) Validate added in v0.0.2

func (f Field) Validate(value string) error

Validate checks a string value against this field's constraints. Checks NotNull first, then Kind, then Permitted (length and chars).

Security note: standard Kinds (Text, Int, Float, Bool) provide an input-boundary floor (A03 Injection/XSS) by using whitelists that exclude HTML-dangerous characters. ValidateFields provides implicit fail-closed protection for form-submitted data. Data from external sources (DB reads, API responses) bypasses this check and must be encoded at the output layer.

type FieldDB added in v0.0.2

type FieldDB struct {
	PK      bool
	Unique  bool
	AutoInc bool

	// RefColumn/OnDelete apply only when the owning Field is a scalar foreign key
	// (Field.Ref set — composition kinds carry their ref in the constructor and
	// never set Field.Ref; see Field.Ref doc).
	// Both are optional: RefColumn empty = auto-detect the PK of Ref's table;
	// OnDelete empty = generator default (e.g. CASCADE).
	RefColumn string
	OnDelete  string
}

FieldDB contains database-specific metadata. Extracted from Field to keep transport/UI structs lean.

type FieldReader added in v0.0.2

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 devuelve el valor crudo asociado al nombre.
	Raw(name string) (string, bool)
}

FieldReader entrega los campos por nombre, TIPADOS. El bool indica presencia. Lee por nombre directo (NO construye un map).

type FieldType added in v0.0.2

type FieldType int

FieldType represents the abstract storage type of a struct field.

const (
	FieldText        FieldType = iota // Any string
	FieldInt                          // Any integer
	FieldFloat                        // Any float
	FieldBool                         // Boolean
	FieldBlob                         // Binary data ([]byte)
	FieldStruct                       // Nested struct (implements Fielder)
	FieldIntSlice                     // []int
	FieldStructSlice                  // []Fielder
	FieldRaw                          // Pre-serialized JSON — emitted inline, no quoting
)

func (FieldType) String added in v0.0.2

func (ft FieldType) String() string

type FieldWriter added in v0.0.2

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 emite el valor directamente sin escapar.
	Raw(name, val string)
	// Anidado: objeto hijo que también es Encodable.
	Object(name string, val Encodable)
	// Arrays tipados sin []any: retorna ArrayWriter directo para evitar closures.
	Array(name string, n int) ArrayWriter
}

FieldWriter recibe los campos de un valor por llamadas TIPADAS. Implementaciones: jsvalue (escribe js.Value), json (escribe bytes), etc. Reglas: cero `any`, cero `map`, cero asignación en el heap Go (el writer reusa su buffer).

type Fielder added in v0.0.2

type Fielder interface {
	Schema() []Field
	Pointers() []any
}

Fielder describes any type that can expose its schema and writable pointers. It is the shared contract between orm (database), form (UI), and json layers, replacing runtime reflection.

Implementations are generated by code generators — never written by hand.

Contract:

  • Schema() and Pointers() MUST return slices of the same length.
  • The i-th element in each slice corresponds to the same struct field.
  • Pointers() returns pointers to fields for reading (dereference) and writing (scan/sync).

type FielderSlice added in v0.0.2

type FielderSlice interface {
	Fielder
	Len() int
	At(i int) Fielder
	Append() Fielder
}

FielderSlice is implemented by generated code to allow iteration over a slice of structs without reflection.

type Fields added in v0.0.3

type Fields = []Field

Fields is the ordered list of field definitions of a model. Alias (not a named type) so a []Field literal is assignable without conversion.

type Kind added in v0.0.7

type Kind interface {
	Storage() FieldType          // deterministic Go/DDL mapping
	Name() string                // semantic name: "text", "int", "email", ...
	Validate(value string) error // ALWAYS present — fail-closed
}

Kind replaces the Field.Type enum slot + Field.Widget pair. Implementations are stateless templates, safe for concurrent reuse.

func Blob added in v0.0.7

func Blob() Kind

Blob returns the base Blob kind. No content validation (binary).

func Bool added in v0.0.7

func Bool() Kind

Bool returns the base Bool kind. Accepts "true", "false", "1", "0", "".

func Float added in v0.0.7

func Float() Kind

Float returns the base Float kind. Accepts digits, at most one '.', and an optional leading minus sign.

func Int added in v0.0.7

func Int() Kind

Int returns the base Int kind. Accepts digits and an optional leading minus sign.

func IntSlice added in v0.0.7

func IntSlice() Kind

IntSlice returns the base IntSlice kind. No string validation.

func Raw added in v0.0.7

func Raw() Kind

Raw returns the base Raw kind. No content validation (pre-serialized JSON).

func Struct added in v0.0.7

func Struct(ref *Definition) Kind

Struct returns the composition kind nesting the given Definition. The ref is REQUIRED: ormc derives the generated Go field type from it. No string validation (the nested Fielder validates itself, fail-closed).

func StructSlice added in v0.0.7

func StructSlice(ref *Definition) Kind

StructSlice returns the composition kind nesting a slice of the given Definition. Same contract as Struct.

func Text added in v0.0.7

func Text() Kind

Text returns the base Text kind. Its whitelist is the input-boundary XSS floor: excludes HTML-dangerous chars (<>"'&`).

type Model

type Model interface {
	Fielder
	ModuleNaming
}

Model describes a resource with a schema and a name. Standard interface used by DB, Form, and API layers.

type ModuleNaming added in v0.0.2

type ModuleNaming interface {
	ModelName() string
}

ModuleNaming is the minimal identity contract shared across the ecosystem: a stable model name used as route key, nav hash and DOM id. Consumers that need only identity (routing, layout shells) embed this; data models embed the richer Model.

type Permitted added in v0.0.2

type Permitted struct {
	Letters    bool       // a-z, A-Z, ñ, Ñ
	Tilde      bool       // á, é, í, ó, ú (and uppercase) — uses aL/aU from mapping.go
	Numbers    bool       // 0-9
	Spaces     bool       // ' '
	BreakLine  bool       // '\n'
	Tab        bool       // '\t'
	Extra      []rune     // additional allowed characters (e.g., '@', '.', '-')
	NotAllowed []string   // forbidden substrings
	Minimum    int        // min length (0 = no limit)
	Maximum    int        // max length (0 = no limit)
	StartWith  *Permitted // rules for first character (nil = same as main rules)
}

Permitted validates strings character-by-character against a configurable whitelist.

Zero value = nothing permitted (strictest). Enable flags to allow character classes. Moved from form/input to fmt for cross-layer reuse.

Rule: if only Minimum/Maximum are configured (no character flags), Validate only checks length — it never rejects characters. To restrict characters, enable at least one flag (Letters, Numbers, etc.) or add entries to Extra/NotAllowed.

Security contract: the whitelist is positive — only explicitly enabled characters pass. HTML-dangerous characters (<, >, &, ", ') are not included in any standard widget's whitelist. Data validated through standard widgets is therefore safe for HTML output without additional escaping.

If a custom widget adds dangerous chars to Extra, it must document the XSS risk and the caller is responsible for output encoding (e.g., fmt.Convert(v).EscapeHTML()).

func (Permitted) NoHTML added in v0.0.2

func (p Permitted) NoHTML() Permitted

NoHTML adds HTML-dangerous characters to NotAllowed as an explicit safety layer. Returns a modified copy. Use when Extra contains characters that could appear in HTML injection attempts and the widget cannot be restricted further.

Example:

t.Permitted = t.Permitted.NoHTML()

func (Permitted) Validate added in v0.0.2

func (p Permitted) Validate(field, text string) error

Validate checks that text conforms to the permitted rules. Order: length → forbidden substrings → start-with → characters.

type RawJSON added in v0.0.2

type RawJSON = string

RawJSON is a type alias for string that signals pre-serialized JSON content. Like encoding/json.RawMessage, it tells the encoder to emit the value inline without quoting or re-serializing, avoiding linter warnings about the `json:",raw"` tag.

Usage:

type Result struct {
    Content RawJSON            // no tag needed; type itself conveys intent
    Error   RawJSON `json:",omitempty"`
}

The encoder detects RawJSON at code-generation time (ormc), marking the field as FieldRaw in the Schema(), and tinywasm/json handles it without re-serializing.

type RefKind added in v0.0.7

type RefKind interface {
	Kind
	Ref() *Definition
}

RefKind is implemented by composition kinds (Struct, StructSlice). Consumers (ormc, orm relations) read the nested Definition from here.

type SafeFields added in v0.0.2

type SafeFields interface {
	Fielder
	Validator
}

SafeFields combines schema access with validation. Handlers that receive user input should accept SafeFields to enforce compile-time validation guarantees.

type Validator added in v0.0.2

type Validator interface {
	Validate(action byte) error
}

Validator is implemented by types that can self-validate. Generated by ormc: func (m *X) Validate(action byte) error { return model.ValidateFields(action, m) } Used by form, json.Decode, and orm pre-insert to enforce data integrity.

Jump to

Keyboard shortcuts

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