Skip to content

Latest commit

 

History

History
263 lines (195 loc) · 8.65 KB

File metadata and controls

263 lines (195 loc) · 8.65 KB

Field and Fielder API

The field API provides a standard way to describe struct schemas, access their values, and perform validation without using runtime reflection. This is essential for maintaining a small binary size in WebAssembly environments.

FieldType

FieldType represents the abstract storage type of a struct field.

Constant String Description
FieldText "text" String or text data
FieldInt "int" Integer numbers
FieldFloat "float" Floating point numbers
FieldBool "bool" Boolean values
FieldBlob "blob" Binary data
FieldStruct "struct" Nested struct
FieldIntSlice "intslice" Slice of integers ([]int)
FieldStructSlice "structslice" Slice of nested structs ([]Fielder)
FieldRaw "raw" Pre-serialized JSON — emitted inline, no quoting

String()

Returns the string representation of the FieldType.

fmt.FieldInt.String() // returns "int"

Field

Field describes a single field in a struct's schema with its metadata, constraints, and validation rules.

type Field struct {
    Name      string
    Type      FieldType
    NotNull   bool
    OmitEmpty bool      // omit from JSON when zero value
    Widget    Widget    // semantic input type; nil = no UI binding (set by ormc from `input:` tag)
    DB        *FieldDB  // nil for formonly/transport structs
    Permitted           // embedded: validation rules (characters, min/max)
}

FieldDB

FieldDB contains database-specific metadata, extracted from Field to keep transport/UI structs lean. When DB is nil, the field has no database concerns (e.g., formonly structs).

type FieldDB struct {
    PK      bool
    Unique  bool
    AutoInc bool
}

Helper methods on Field

Convenience methods to avoid nil-check boilerplate:

func (f Field) IsPK() bool      { return f.DB != nil && f.DB.PK }
func (f Field) IsUnique() bool   { return f.DB != nil && f.DB.Unique }
func (f Field) IsAutoInc() bool  { return f.DB != nil && f.DB.AutoInc }

RawJSON Type

RawJSON is a type alias for string that signals pre-serialized JSON content. It tells the encoder to emit the field inline without quoting or re-serializing, following the pattern of encoding/json.RawMessage in the standard library.

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

Benefits:

  • No linter warnings: Using the type alias avoids the unknown JSON option "raw" error from placing raw in the standard json: tag.
  • Self-documenting: The type clearly signals that the field contains pre-serialized JSON.
  • Zero overhead in WASM: Type alias with = creates no runtime boxing — at compile time it's indistinguishable from string.

How it works:

  1. The developer declares a field with type RawJSON
  2. ormc code generation detects the type name and marks the field as FieldRaw in the Schema()
  3. tinywasm/json encoder checks Type == FieldRaw and emits the value inline (no quoting or re-serializing)

Widget Interface

Widget is the contract for a semantic input type. It is implemented by tinywasm/form/input types and by custom project inputs defined in web/inputs/. Set by ormc code generation from the input: struct tag.

type Widget interface {
    Type() string                              // Semantic type name (e.g., "email", "textarea")
    Validate(value string) error               // Semantic validation for this input type
    Clone(parentID, name string) Widget        // Returns a positioned instance; pass ("","") for a bare template
}

Field.Validate() calls Widget.Validate() before Permitted.Validate(). If the widget fails, Permitted is not evaluated. If Widget is nil, only Permitted rules apply.

// Example: field with Widget + additive Permitted rules
Field{
    Name:      "email",
    Widget:    input.NewEmail(),            // validates email format
    Permitted: fmt.Permitted{Minimum: 10}, // additionally enforces min length
}

Why Clone(parentID, name) instead of Clone(): The form layer always needs positioned instances for rendering. A parameterless Clone() would force a separate Build() method — two constructors for the same concern. With Clone(parentID, name), consumers that only need a bare template pass ("", ""), and form consumers get a positioned instance directly.

Why not include RenderHTML(): Interface Segregation — ORM and validation consumers need only Type() and Validate(). Rendering is handled by tinywasm/form, which type-asserts field.Widget.Clone(parentID, name).(input.Input) for HTML output.

Validation (Permitted)

Validation rules are embedded in the Field via the Permitted struct. This includes character-level whitelisting, length constraints, and structural format checks.

Field Type Description
Letters bool Allows a-z, A-Z, ñ, Ñ
Tilde bool Allows accented characters (á, é, etc.)
Numbers bool Allows 0-9
Spaces bool Allows ' '
BreakLine bool Allows \n
Tab bool Allows \t
Extra []rune Additional allowed characters
NotAllowed []string Forbidden substrings
Minimum int Minimum length (runes)
Maximum int Maximum length (runes)
StartWith *Permitted Rules for the first character

Field.Validate()

Checks a string value against the field's constraints in order: NotNullWidget.Validate()Permitted.

err := field.Validate("some value")

Fielder Interface

The Fielder interface is the shared contract between various layers (like ORM and UI forms) to interact with structs.

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

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.

FielderSlice Interface

FielderSlice is the interface used to read and append elements to a typed slice of structs without depending on runtime reflection. It embeds Fielder, allowing slice types to be passed to functions accepting Fielder.

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

This interface is implemented by generated code to allow iterators (like JSON encoding) to pull out nested structures reliably and with zero allocation footprint.

Validator and SafeFields

// Validator can self-validate
type Validator interface {
    Validate(action byte) error
}

// Model describes a resource with a schema and a name.
type Model interface {
    Fielder
    ModelName() string
}

// SafeFields combines Fielder and Validator
type SafeFields interface {
    Fielder
    Validator
}

ValidateFields() Helper

Generic function that iterates through a Fielder's schema and pointers to perform full validation based on the action. It handles FieldText (calling Field.Validate) and FieldStruct (recursive validation).

Action PK + AutoInc PK without AutoInc NotNull Permitted
'c' create skip (DB assigns) required required applies
'u' update required required required applies
'd' delete required required skip skip
other/unknown required required required applies
err := fmt.ValidateFields('u', myFielder)

Conversion and Reading Helpers

ReadValues()

For consumers that need a []any of values (like ORM for SQL arguments):

vals := fmt.ReadValues(myFielder.Schema(), myFielder.Pointers())

ReadStringPtr()

High-performance codecs can read string values without boxing to any:

if val, ok := fmt.ReadStringPtr(ptrs[i]); ok {
    // use val (string) directly
}

isZeroPtr()

Checks if a pointer points to its type's zero value.

if fmt.isZeroPtr(ptr, fieldType) {
    // value is zero
}

Example Implementation

type User struct {
    ID   string
    Name string
}

func (u *User) Schema() []fmt.Field {
    return []fmt.Field{
        {Name: "id", Type: fmt.FieldText, DB: &fmt.FieldDB{PK: true}},
        {Name: "name", Type: fmt.FieldText, NotNull: true, Permitted: fmt.Permitted{Letters: true}},
    }
}

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

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