schema

package
v0.4.3 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package schema implements the minimal JSON-Schema subset used by the types/properties layer. It operates natively on *anyenc.Value — no conversion to interface{} or JSON on the validation path — and the Validate method allocates zero on scalar success paths. This is load-bearing: schema checks run on every write op.

Supported in v1:

  • Leaf kinds: `string`, `number`, `boolean`, `null`, `array`, `object`.
  • `items` on arrays — recursive sub-schema for array elements.
  • `properties` on objects — recursive sub-schemas for named fields.

Fields not present on a record are left unconstrained (an object without `properties` accepts any shape; an array without `items` accepts any element). This is progressive disclosure — simple schemas stay simple. Additional keywords (`enum`, `required`, `additionalProperties`) land when a concrete need appears. See docs/types-properties-proposal.md.

The Validator's single entry point is Validate(name, value), called after an op has been applied to the record. The caller identifies which top-level field(s) the op touched and asks "is the current state valid?". This keeps the validator API op-type-agnostic — $set, $inc, $addToSet, $pull, $unset all reduce to "inspect the top-level field's current value".

Index

Constants

View Source
const (
	DefaultIdPattern = `[A-Za-z0-9._:-]+`
	DefaultIdMaxLen  = 128
)

Defaults for IdUser constraints when the declaration leaves them zero.

View Source
const MaxSlugLen = 64

Slug rules for part and dataset keys: lowercase ASCII letters, digits and underscores, starting with a letter, at most MaxSlugLen bytes. A key names a collection segment (`<typeId>_<key>`) and a wire path segment, so nothing that needs quoting is admitted.

Variables

View Source
var (
	ErrCompile = errors.New("schema: compile error")
	ErrUnknown = errors.New("schema: unknown property")
	ErrKind    = errors.New("schema: kind mismatch")
)

Sentinel errors — specific error messages wrap these via fmt.Errorf on the cold failure path. Callers classify with errors.Is. The success path never constructs an error.

View Source
var ErrDecl = errors.New("schema: invalid dataset declaration")

ErrDecl is the sentinel wrapped by dataset-declaration validation failures. Classify with errors.Is.

Functions

func CompileIdPattern

func CompileIdPattern(ds Dataset) (*regexp.Regexp, int, error)

CompileIdPattern compiles the dataset's user-id constraint into a full-match regexp, applying defaults. Call after ValidateDatasetDecl; compile errors are impossible on a validated declaration.

func SearchTextFromAnyenc

func SearchTextFromAnyenc(v *anyenc.Value) []string

SearchTextFromAnyenc parses the wire form of a search `text` leaf: a bare string or an array of field keys. Empty/absent forms (nil, "", []) return nil — "no text mapping", matching the empty-string tolerance of the single-field era. Array elements ride verbatim (non-strings become "") so ValidateSearchText can flag bad entries.

func SearchTextToAnyenc

func SearchTextToAnyenc(arena *anyenc.Arena, keys []string) *anyenc.Value

SearchTextToAnyenc encodes a text mapping in the canonical wire form: nil for no keys, the bare string for a single key, an array otherwise — SearchTextFromAnyenc's inverse. Every anyenc writer of a `search.text` leaf (the head-record encoder, the PatchDataset leaf rewrite) goes through this one canonicalization so locally-authored records and the x-search marshal (which applies the same rule in JSON) can never disagree.

func ValidateDatasetDecl

func ValidateDatasetDecl(ds Dataset) error

ValidateDatasetDecl checks a dataset declaration's well-formedness. Shared by the generic schema handler's constructor, external-type catalog validation, and the runtime dataset-def write preflight, so a declaration rejected here can neither register nor sync.

Rules:

  • field ids are non-empty, unique, and dot-free;
  • Stamp forces ScopeDerived (zero scope is normalized by the handler; an explicit conflicting scope is rejected);
  • at most one field per stamp kind;
  • Required is mutually exclusive with Stamp, and required fields are synced-scope (they must ride the create change);
  • stamped fields cannot also declare MutableBy (handler-written);
  • MutableByAuthor anywhere or DeleteByAuthor requires a StampCreator field — the authorship fact must live on the record so apply-time checks read only ctx.Before;
  • IdPattern must compile (RE2) when set; id constraints only make sense under IdUser;
  • a search text mapping, when present, names at least one field key, with no empty or duplicate keys (mapped keys are NOT required to be declared fields — the annotation stays opaque).

func ValidateSearchText

func ValidateSearchText(keys []string) error

ValidateSearchText checks a search text mapping's field keys: at least one, none empty, no duplicates. Shared by the declaration validator and the wire-leaf checks (the dataset-def handler and the PatchDataset preflight), so every entry path rejects the same forms.

func ValidateSlug

func ValidateSlug(what, s string) error

ValidateSlug checks a part or dataset key against the slug rules; `what` names the key's owner in the error.

func ValidateValue

func ValidateValue(s *Schema, value *anyenc.Value) error

ValidateValue checks `value` against the shape `s`. A nil schema is unconstrained; a nil value (absent field) is always accepted. Zero allocations on scalar success — safe for per-op apply paths.

Types

type Dataset

type Dataset struct {
	Fields  []Field
	Dynamic bool

	// DeleteBy: record-delete gate. Zero = anyone.
	DeleteBy DeletePolicy
	// IdRule: how record ids are produced. Zero = auto (derived).
	IdRule IdRule
	// IdPattern is an RE2 pattern user-supplied ids must match in full
	// (IdUser only). Empty = DefaultIdPattern.
	IdPattern string
	// IdMaxLen caps user-supplied id length (IdUser only). 0 =
	// DefaultIdMaxLen.
	IdMaxLen int
	// Search is the optional search-extraction annotation.
	Search *SearchFields
}

Dataset is a dataset's required, JSON-Schema-compatible declaration. Dynamic datasets (shortIds, the per-type `objects` namespace) carry a free-form key space: undeclared fields are allowed and default to ScopeSynced; declared fields (e.g. derived auto-fields) are still enforced.

func (Dataset) MarshalJSON

func (d Dataset) MarshalJSON() ([]byte, error)

MarshalJSON emits a standard JSON Schema object document:

{"type":"object","properties":{<id>:{<value schema>,"title":..,"x-scope":..}},
 "required":[..],"additionalProperties":<Dynamic>}

The per-field class rides as the `x-scope` extension keyword (precedent: the docs' x-refType); behavioral declarations ride as `x-mutable-by`, `x-stamp`, and the dataset-level `x-delete-by` / `x-id` / `x-id-pattern` / `x-id-max-length` / `x-search` keywords. Defaults are omitted. Cold path — discovery only; allocates freely.

func (Dataset) Normalized

func (d Dataset) Normalized() Dataset

Normalized returns a copy with zero-value scopes resolved: stamped fields are ScopeDerived (they are handler-written), everything else defaults to ScopeSynced. Registration and discovery paths call this once so enforcement never sees a zero scope; returns the receiver unchanged when nothing needs resolving.

func (Dataset) ScopeOf

func (d Dataset) ScopeOf(id string) (Scope, bool)

ScopeOf returns the declared class of field id and whether it's declared. Undeclared fields on a Dynamic dataset are treated as ScopeSynced by the apply path; this reports only what's declared.

type DeletePolicy

type DeletePolicy uint8

DeletePolicy is the dataset-level record-delete gate.

const (
	// DeleteByAnyone (zero): any writer may delete a record.
	DeleteByAnyone DeletePolicy = iota
	// DeleteByAuthor: only the record's creator (StampCreator field)
	// may delete it.
	DeleteByAuthor
)

func ParseDeletePolicy

func ParseDeletePolicy(label string) (DeletePolicy, bool)

ParseDeletePolicy parses a delete-policy label ("anyone"/"author").

func (DeletePolicy) String

func (p DeletePolicy) String() string

type Field

type Field struct {
	Id     string
	Name   string
	Schema *Schema // value shape; nil means unconstrained
	Scope  Scope

	// Required: the field must be present in the create payload.
	// Enforced by the generic schema handler; mutually exclusive with
	// Stamp.
	Required bool
	// MutableBy: post-create write rule. Zero = write-once.
	MutableBy Mutability
	// Stamp: apply-time derived value. Non-zero forces ScopeDerived.
	Stamp Stamp

	// Description and XFormat are the field's descriptive slice: a
	// display description and the opaque descriptor bag (semantic slug,
	// icon, options, …). Neither is enforced by any handler and neither
	// enters the schema revision — editing them never re-registers a
	// dataset. Rendered by discovery as `description` / `x-format`.
	Description string
	XFormat     map[string]any
}

Field is one declared dataset field: a JSON-Schema value shape plus its class. Modeled like a type property (Id/Name + recursive Schema) so the two share one representation.

type IdRule

type IdRule uint8

IdRule declares how the dataset's record ids are produced.

const (
	// IdAuto (zero): records are created with an empty id and the id is
	// derived from the change (the existing empty-id upsert sugar);
	// explicit caller ids are rejected at create.
	IdAuto IdRule = iota
	// IdUser: the caller supplies the id, constrained by
	// IdPattern/IdMaxLen. The id doubles as the upsert idempotency
	// key. Contract: one writer per id — concurrent creates of the
	// same id by different members take arrival-order-dependent
	// creation verdicts (required checks, creator stamp) and are
	// outside the convergence guarantee.
	IdUser
)

func ParseIdRule

func ParseIdRule(label string) (IdRule, bool)

ParseIdRule parses an id-rule label ("auto"/"user").

func (IdRule) String

func (r IdRule) String() string

type Kind

type Kind uint8

Kind is the declared type of a value.

const (
	KindUnknown Kind = iota
	KindString
	KindNumber
	KindBoolean
	KindNull
	KindArray
	KindObject
	// KindDatetime is an instant, stored as any-store's native
	// TypeDateTime (unix millis, memcmp-orderable, index-keyable,
	// `{"$date": …}` in JSON). Appended last: the numeric values are
	// mirrored by the public handler.PropertyKind enum, so the existing
	// ones must not move.
	KindDatetime
)

func KindOf

func KindOf(v *anyenc.Value) Kind

KindOf maps an anyenc value's type to its schema Kind. Returns KindUnknown for nil or unsupported types (e.g. binary). Used by per-op handler validators to match a payload against a property's declared kind without compiling a full Validator.

func ParseKind

func ParseKind(s string) (Kind, bool)

ParseKind decodes the on-wire kind label ("string", "number", …) into a Kind. Returns false on an unknown label.

func (Kind) String

func (k Kind) String() string

type Mutability

type Mutability uint8

Mutability is a declared field's post-create write rule, enforced by the generic schema handler. The zero value is write-once: the field is writable ONLY in the record's creating change — there is no late fill (a presence-based rule would diverge under concurrent fills). Declare MutableBy for fields that must stay settable later.

const (
	// MutableNever: writable only in the record's creating change.
	MutableNever Mutability = iota
	// MutableByAuthor: only the record's creator (the StampCreator
	// field) may rewrite; accepted writes bump the modifyTime stamp.
	MutableByAuthor
	// MutableByAnyone: any writer may rewrite; accepted writes bump the
	// modifyTime stamp.
	MutableByAnyone
)

func ParseMutability

func ParseMutability(label string) (Mutability, bool)

ParseMutability parses a mutability label ("never"/"author"/"any").

func (Mutability) String

func (m Mutability) String() string

type Schema

type Schema struct {
	Kind       Kind
	Items      *Schema
	Properties map[string]*Schema
}

Schema is a recursive schema node. Items is populated only for KindArray (nil means "any element"); Properties only for KindObject (nil means "any shape").

func CompileShape

func CompileShape(v *anyenc.Value) (*Schema, error)

CompileShape compiles one record's `kind` / `items` / `properties` fields into a value Schema — the same encoding property-definition records use, reused by dataset-field definitions. Cold path.

func Leaf

func Leaf(k Kind) *Schema

Leaf builds a scalar/leaf value Schema for a Kind (no items/properties). Convenience for declaring simple dataset fields.

func (*Schema) MarshalJSON

func (s *Schema) MarshalJSON() ([]byte, error)

MarshalJSON emits a JSON-Schema node for a value shape: {"type":..}, with `items` for arrays and `properties` for objects (recursive).

type Scope

type Scope uint8

Scope is the single write/sync taxonomy shared by dataset fields AND property definitions: how a value is written, which version domain stamps its `_ver` entries, and how far it syncs. One vocabulary everywhere — dataset schema fields, property defs, and the x-scope discovery keyword all use these labels.

Each scope is a disjoint write route. A given field/property lives in exactly ONE scope for its whole life (scope is pinned at declaration, like a property's kind) — there is no per-value override stack. That disjointness is what lets versions from different domains coexist in one `_ver` tree: no two routes ever gate on the same path.

const (
	// ScopeSynced: user/DAG-written through the object's own tree,
	// change-versioned, normal LWW, synced to everyone with access.
	// (Docs historically called this "base".)
	ScopeSynced Scope = iota + 1
	// ScopeDerived: handler-computed from the change, change-versioned,
	// converges across peers; never writable by an input op. (Was
	// ScopeAuto.) e.g. author(creator)/createdAt/_ver.id.
	ScopeDerived
	// ScopeLocal: materialised on-device via Object.LocalSet,
	// lexid.Next-versioned, never synced. e.g. localStatus.
	// (Docs historically called this "device".)
	ScopeLocal
	// ScopeAccount: synced across the SAME account's devices only, via a
	// carrier record in the private tech space; a per-device watcher
	// mirrors converged values into the target record, stamping the
	// tech tree's versionIds. Invisible to other space members.
	ScopeAccount
)

func ParseScope

func ParseScope(label string) (Scope, bool)

ParseScope parses a scope label. Returns (0, false) on an unknown label.

func (Scope) String

func (s Scope) String() string

type SearchFields

type SearchFields struct {
	Title string
	Text  []string
	Scope string
}

SearchFields is the dataset's search-extraction annotation: which field feeds the document title and which fields the body text, plus the index scope the entries land under. Opaque to the SDK — surfaced through discovery (`x-search`) for external indexers; Scope is a free-form slug the indexer interprets (empty = the indexer's default).

Text holds one or more field keys; the indexer joins the mapped values into one body. On the wire (`x-search` and the head record's `search.text` leaf) a single key rides as a bare string and multiple keys as an array — MarshalJSON canonicalizes, so single-field declarations look exactly as they did before.

type Stamp

type Stamp uint8

Stamp marks a field whose value the generic schema handler derives from the change at apply time. A stamped field is ScopeDerived, so client writes to it are rejected by the controller's scope enforcement; the handler is its only writer.

const (
	StampNone Stamp = iota
	// StampCreator: the creating change's signer identity, set once at
	// create. The authorship fact author-gated rules check against.
	StampCreator
	// StampCreateTime: the creating change's timestamp, set once.
	StampCreateTime
	// StampModifyTime: the change timestamp, set at create and bumped
	// on every accepted mutable write.
	StampModifyTime
)

func ParseStamp

func ParseStamp(label string) (Stamp, bool)

ParseStamp parses a stamp label ("creator"/"createTime"/"modifyTime").

func (Stamp) String

func (s Stamp) String() string

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

Validator is a compiled schema. Built once via Compile; used read-only on many change-apply paths. Safe for concurrent use after Compile returns.

func Compile

func Compile(records []*anyenc.Value) (*Validator, error)

Compile builds a Validator from top-level property records. Each record is an anyenc object with string `key` and a `kind` plus optional `items` / `properties` recursive sub-schemas.

Duplicate keys are last-wins — the CRDT layer is responsible for surfacing/resolving client-side conflicts.

Allocates freely — this is the cold path that runs once when a schema version is compiled.

func (*Validator) Kind

func (v *Validator) Kind(name string) (Kind, bool)

Kind returns the declared kind of a top-level property and whether the property is declared at all.

func (*Validator) Validate

func (v *Validator) Validate(name string, value *anyenc.Value) error

Validate checks that the top-level field `name` with its current `value` is consistent with the schema.

Call after the op has been applied to the record — pass each top-level field the op touched and the field's current value (or nil if the op removed it). Absent fields (nil value) are always accepted in v1 (no `required` keyword yet).

Zero allocations on success when both the schema and value at `name` are primitive (kind is string/number/boolean/null and value is a scalar).

Jump to

Keyboard shortcuts

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