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
- Variables
- func CompileIdPattern(ds Dataset) (*regexp.Regexp, int, error)
- func SearchTextFromAnyenc(v *anyenc.Value) []string
- func SearchTextToAnyenc(arena *anyenc.Arena, keys []string) *anyenc.Value
- func ValidateDatasetDecl(ds Dataset) error
- func ValidateSearchText(keys []string) error
- func ValidateSlug(what, s string) error
- func ValidateValue(s *Schema, value *anyenc.Value) error
- type Dataset
- type DeletePolicy
- type Field
- type IdRule
- type Kind
- type Mutability
- type Schema
- type Scope
- type SearchFields
- type Stamp
- type Validator
Constants ¶
const ( DefaultIdPattern = `[A-Za-z0-9._:-]+` DefaultIdMaxLen = 128 )
Defaults for IdUser constraints when the declaration leaves them zero.
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
ValidateSlug checks a part or dataset key against the slug rules; `what` names the key's owner in the error.
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 ¶
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 ¶
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.
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 ¶
ParseIdRule parses an id-rule label ("auto"/"user").
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 ¶
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.
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 ¶
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 ¶
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 ¶
Leaf builds a scalar/leaf value Schema for a Kind (no items/properties). Convenience for declaring simple dataset fields.
func (*Schema) MarshalJSON ¶
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 ¶
ParseScope parses a scope label. Returns (0, false) on an unknown label.
type SearchFields ¶
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 ¶
ParseStamp parses a stamp label ("creator"/"createTime"/"modifyTime").
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 ¶
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 ¶
Kind returns the declared kind of a top-level property and whether the property is declared at all.
func (*Validator) Validate ¶
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).