tur/schema
runtime schema validation for untyped boundary data.
Schemas are first-class runtime values that describe the expected shape of data arriving at a dynamic boundary (an HTTP body, a config file, a channel message). Unlike tur/contract (which panics), schema-decode returns a Result so callers handle bad data gracefully, and it accumulates *all* violations with dot-separated field paths rather than failing fast.
Decoding is anchored on the tagged JSON node representation from tur/json (json/decode, #json(...)): those nodes carry a runtime type tag (0=null 1=bool 2=int 3=float 4=string 5=array 6=object), which is what makes genuine "expected :cstr, got :int" validation possible. Raw Turmeric int64 values carry no type tag, so the schema decoder reads the shape it needs from JSON nodes.
A schema value is a heap-allocated tagged struct:
{ int64_t kind; int64_t a; int64_t b; int64_t c; }
The kind is one of the SCHEMA_* discriminants; a/b/c are kind-specific. Object schemas are built incrementally (schema/object-new + schema/field) because a field list mixes :cstr keys with schema pointers, which a homogeneous Vec cannot hold.
Decoded result values:
scalar -> the underlying value (cstr ptr / int / float bits / bool) literal -> the matched value object -> the validated JSON object node (use json/get to read fields) array -> a freshly allocated Vec of decoded element values optional, union, transform, rec -> the inner decoded value
Errors are a Vec of SchemaError; schema-decode returns Result<ptr<void>, Vec<SchemaError>>.
Since: Phase SC0
SCHEMA_STR
(def SCHEMA_STR 0)
discriminant for the :cstr scalar schema.
SCHEMA_INT
(def SCHEMA_INT 1)
discriminant for the :int scalar schema.
SCHEMA_FLOAT
(def SCHEMA_FLOAT 2)
discriminant for the :float scalar schema.
SCHEMA_BOOL
(def SCHEMA_BOOL 3)
discriminant for the :bool scalar schema.
SCHEMA_NIL
(def SCHEMA_NIL 4)
discriminant for the null schema.
SCHEMA_LITERAL
(def SCHEMA_LITERAL 5)
discriminant for an exact-value schema.
SCHEMA_OBJECT
(def SCHEMA_OBJECT 6)
discriminant for an object (field-list) schema.
SCHEMA_ARRAY
(def SCHEMA_ARRAY 7)
discriminant for a homogeneous array schema.
SCHEMA_OPTIONAL
(def SCHEMA_OPTIONAL 8)
discriminant for an optional (nullable) schema.
SCHEMA_UNION
(def SCHEMA_UNION 9)
discriminant for a first-match union schema.
SCHEMA_TRANSFORM
(def SCHEMA_TRANSFORM 10)
discriminant for a decode-then-map schema.
SCHEMA_REC
(def SCHEMA_REC 11)
discriminant for a recursive (self-referential) schema.
SCHEMA_ALWAYS
(def SCHEMA_ALWAYS 12)
discriminant for a schema that always succeeds (backs pure).
SCHEMA_NEVER
(def SCHEMA_NEVER 13)
discriminant for a schema that always fails (backs empty).
SCHEMA_AP
(def SCHEMA_AP 14)
discriminant for the Validation applicative (apply + accumulate).
SCHEMA_FIELD
(def SCHEMA_FIELD 15)
discriminant for an extract-one-field schema.
SCHEMA_AP_FAT
(def SCHEMA_AP_FAT 16)
discriminant for a closure-aware applicative apply.
schema/str
(schema/str :int)
a schema that matches JSON strings (:cstr).
A scalar string schema; decodes to the underlying cstr.
(schema-decode (schema/str) (json/string "hi")) ; => ok("hi")
Since: Phase SC0
schema/int
(schema/int :int)
a schema that matches JSON integers (:int).
A scalar int schema; decodes to the underlying int.
(schema-decode (schema/int) (json/int 42)) ; => ok(42)
Since: Phase SC0
schema/float
(schema/float :int)
a schema that matches JSON floats (:float).
A scalar float schema; decodes to the underlying float bits.
Since: Phase SC0
schema/bool
(schema/bool :int)
a schema that matches JSON booleans (:bool).
A scalar bool schema; decodes to 0 or 1.
Since: Phase SC0
schema/nil
(schema/nil :int)
a schema that matches JSON null.
A null schema; decodes to 0.
Since: Phase SC0
schema/literal-int
(schema/literal-int [v :int] :int)
a schema matching one exact integer value.
| v | the required int value |
A literal schema; decodes to v only when the node is exactly v.
(schema-decode (schema/literal-int 7) (json/int 7)) ; => ok(7)
Since: Phase SC0
schema/literal-str
(schema/literal-str [v :cstr] :int)
a schema matching one exact string value.
| v | the required cstr value |
A literal schema; decodes to v only when the node string equals v.
(schema-decode (schema/literal-str "active") (json/string "active"))
Since: Phase SC0
schema/object-new
(schema/object-new :int)
start an empty object schema.
An object schema with no fields yet; add fields with schema/field.
(schema/field (schema/field (schema/object-new)
"name" (schema/str))
"age" (schema/int))
Since: Phase SC1
schema/field
(schema/field [obj :int key :cstr inner :int] :int)
add a declared field to an object schema.
| obj | an object schema (from schema/object-new) | |
| key | the field name | |
| inner | the schema the field value must satisfy. Wrap it in | |
| schema/optional to allow the field to be absent or null. |
The same object schema, with the field appended (mutated in place).
(schema/field (schema/object-new) "name" (schema/str))
Since: Phase SC1
schema/array
(schema/array [elem :int] :int)
a schema for a homogeneous JSON array.
| elem | the schema each element must satisfy |
An array schema; decodes to a Vec of decoded element values.
(schema/array (schema/int)) ; matches [1, 2, 3]
Since: Phase SC2
schema/optional
(schema/optional [inner :int] :int)
a schema where null / absent is acceptable.
| inner | the schema a present, non-null value must satisfy |
An optional schema; decodes to the inner value, or 0 when null. Inside schema/object, an optional field may also be absent entirely.
(schema/optional (schema/str))
Since: Phase SC2
schema/union
(schema/union [arms :int] :int)
a schema where the first matching arm wins.
| arms | a Vec of schemas; tried left to right |
A union schema; decodes to the value from the first arm that matches. If no arm matches, all arms' errors are reported.
(schema/union (vec-of (schema/int) (schema/str)))
Since: Phase SC2
schema/transform
(schema/transform [inner :int ^fat f] :int)
decode with a schema, then map the result.
| inner | the schema to decode with | |
| f | a function or *capturing closure* applied to the decoded value | |
| (only on success); carried as a fat closure (^fat) |
A transform schema; decodes to (f decoded-inner).
(schema/transform (schema/int) (fn [x] (* x 2))) (let [k 100] (schema/transform (schema/int) (fn [x] (+ x k)))) ; captures k
Since: Phase SC2
schema/rec
(schema/rec [f] :int)
a self-referential schema for recursive data.
| f | a function taking the recursive schema itself (self) and | |
| returning the schema body, e.g. | ||
| (fn [self] (schema/field (schema/object-new) | ||
| "children" (schema/array self))) |
A recursive schema. The body is forced (by calling f with self) on first decode and memoized thereafter.
(schema/rec (fn [self]
(schema/field
(schema/field (schema/object-new) "value" (schema/int))
"children" (schema/array self))))
Since: Phase SC4
schema/kind
(schema/kind [s :int] :int)
return the SCHEMA_* discriminant of a schema value.
| s | a schema value |
The integer discriminant (SCHEMA_STR .. SCHEMA_REC).
Since: Phase SC0
schema/always
(schema/always [value :int] :int)
a pure schema that ignores its input, decoding to a fixed value.
| value | the value (an int-carrier; e.g. an int, a cstr ptr, or a | |
| top-level function pointer) the schema always decodes to |
An always-succeeds schema; decoding it never inspects the node.
(schema-decode! (schema/always 42) (json/null)) ; => 42
Since: Phase SC7
schema/never
(schema/never [message :cstr] :int)
the empty schema: always fails with a fixed message (identity of schema/union).
| message | the failure message reported at the current path |
An always-fails schema.
(schema-decode (schema/never "no match") (json/int 1)) ; => err
Since: Phase SC7
schema/ap
(schema/ap [sf :int sa :int] :int)
the Validation applicative: apply sf's decoded function to sa's decoded value, accumulating both arms' errors.
Decodes the function arm sf and the argument arm sa against the *same* input, concatenates their errors left-to-right, and applies the decoded function to the decoded argument only when both arms succeed.
| sf | a schema decoding to a unary function pointer (a/the result of | |
| schema/always over a top-level, non-capturing function) | ||
| sa | a schema decoding to that function's argument |
An applicative schema decoding to (f a). NOTE: the decoded function is applied via a direct C call, so it must be a top-level (non-capturing) function -- multi-argument currying needs closures and is deferred.
;; double-it is a top-level (defn double-it [x] (* x 2))
(schema/ap (schema/always double-it)
(schema/field-of "n" (schema/int)))
Since: Phase SC7
schema/field-of
(schema/field-of [key :cstr inner :int] :int)
decode a JSON object and extract one key's decoded value via an inner schema.
Yields the key's decoded value (not the whole object), so it composes with schema/ap to assemble values field by field.
| key | the object key to extract | |
| inner | the schema the key's value must satisfy. Wrap in schema/optional | |
| to allow the key to be absent. |
A field-extract schema decoding to the inner decoded value.
(schema/field-of "age" (schema/int)) ; on {"age":30} => 30
Since: Phase SC7
schema/fmap
(schema/fmap [inner :int ^fat f] :int)
the Functor map for schemas: decode with inner, then map the result with f (alias for schema/transform).
| inner | the schema to decode with | |
| f | a function or capturing closure applied to the decoded value on | |
| success; carried as a fat closure (^fat) |
A schema decoding to (f decoded-inner).
(schema/fmap (schema/int) double-it)
Since: Phase SC7
schema/alt
(schema/alt [a :int b :int] :int)
the Alternative <|>: a two-arm first-match union (try a, then b).
| a | the first schema to try | |
| b | the schema to try if a does not match |
A union schema over the two arms.
(schema/alt (schema/int) (schema/str))
Since: Phase SC7
schema/ap-fat
(schema/ap-fat [sf :int sa :int] :int)
closure-aware Validation applicative (kind 16).
Like schema/ap, but applies the decoded function arm to the decoded argument through the fat-closure protocol (TUR_APPLY1) rather than a raw C call, so the function arm may be a *capturing* closure -- e.g. the partially-applied curried constructor produced by fmap ctor field when assembling a struct field by field. Same accumulating error semantics as schema/ap. This is the combinator the Applicative [Schema] instance's ap method is built on.
| sf | a schema decoding to a unary fat-closure handle | |
| sa | a schema decoding to that closure's argument |
An applicative schema decoding to (f a).
Since: Phase SC7
Schema
(defstruct Schema [A] (raw :int))
phantom wrapper over an int-carrier schema, for HKT dispatch.
| A | phantom: the type a successful decode produces | |
| raw | the underlying SC0 schema pointer (int-carrier) |
Since: Phase SC7
Functor[Schema]
(definstance Functor [Schema])
map a function over a schema's decoded value.
Applicative[Schema]
(definstance Applicative [Schema])
Validation applicative: pure + accumulating ap.
pure lifts a value (schema/always); ap applies a decoded function arm to a decoded argument arm via the closure-aware schema/ap-fat, accumulating both arms' errors (Validation, not fail-fast). Monad is deliberately omitted: the lawful fail-fast >>= contradicts this accumulating <*> (see schema/ap).
Alternative[Schema]
(definstance Alternative [Schema])
first-match choice (alt-or) + the empty schema.
schema-error-path
(schema-error-path [e :int] :cstr)
the dot-separated field path of a SchemaError.
| e | a SchemaError value |
The path cstr, e.g. "user.address.zip" (empty string at the root).
Since: Phase SC3
schema-error-text
(schema-error-text [e :int] :cstr)
the human-readable message of a SchemaError.
| e | a SchemaError value |
The message cstr, e.g. "expected :cstr, got :int".
Since: Phase SC3
schema-error-count
(schema-error-count [errs :int] :int)
number of errors in an error Vec.
| errs | a Vec of SchemaError (the err payload of schema-decode) |
The error count (0 if errs is null).
Since: Phase SC3
schema-error-at
(schema-error-at [errs :int i :int] :int)
the i-th SchemaError in an error Vec.
| errs | a Vec of SchemaError | |
| i | zero-based index |
The SchemaError at index i (0 if out of bounds).
Since: Phase SC3
schema-error-message
(schema-error-message [errs :int] :cstr)
format an error Vec into one human-readable cstr.
| errs | a Vec of SchemaError |
A newline-separated string, one "path: message" line per error (a root-level error omits the leading "path: "). Caller owns the cstr.
(schema-error-message errs) ; => "user.age: expected :int, got :cstr"
Since: Phase SC3
sch-vpush-
(sch-vpush- [v :ptr x :int] :void)
validate a JSON node against a schema.
| schema | a schema value (from the constructors above) | |
| node | a JSON node (from json/decode or #json(...)) |
Result<ptr<void>, Vec<SchemaError>>. On success, ok holds the decoded value (see the module docstring for the per-kind result shape). On failure, err holds a Vec of every accumulated SchemaError -- decoding does not stop at the first violation.
(let [s (schema/field (schema/object-new) "age" (schema/int))
r (schema-decode s (json/decode "{\"age\":1}"))]
(schema-decode-ok? r)) ; => true
Since: Phase SC1
sch-push-err-
(sch-push-err- [errs :ptr path :cstr msg :cstr val :int] :void)
sch-mkpath-
(sch-mkpath- [base :cstr key :cstr] :ptr)
sch-mkidx-
(sch-mkidx- [base :cstr idx :int] :ptr)
sch-tyname-
(sch-tyname- [jtype :int] :cstr)
sch-want-name-
(sch-want-name- [kind :int] :cstr)
sch-jtype-
(sch-jtype- [n :int] :int)
sch-jpayload-
(sch-jpayload- [n :int] :int)
sch-decode-rec-
(sch-decode-rec- [schema :int node :int path :cstr errs :ptr] :int)
schema-decode
(schema-decode [schema :int node :int] :int)
schema-decode-ok?
(schema-decode-ok? [r :int] :bool)
whether a schema-decode Result succeeded.
| r | the Result from schema-decode |
true if decoding succeeded, false otherwise.
Since: Phase SC1
schema-decode-value
(schema-decode-value [r :int] :int)
the decoded value of a successful Result.
| r | the Result from schema-decode (must be ok) |
The decoded value (see the module docstring for per-kind shape).
Since: Phase SC1
schema-decode-errors
(schema-decode-errors [r :int] :int)
the error Vec of a failed Result.
| r | the Result from schema-decode (must be err) |
The Vec of SchemaError (0 if r is ok).
Since: Phase SC1
schema-decode-report
(schema-decode-report [errs :int] :int)
internal: print an error Vec to stderr.
schema-decode-abort
(schema-decode-abort [errs :int] :int)
internal: report an error Vec and raise.
A panic, not a bare abort(): with no catch boundary in scope the two are indistinguishable (both print and die), but a panic is what makes (catch-unwind (fn [] : T (decode! ...))) -- and with it the Result-returning #json-str?<T> reader -- able to recover from a schema violation instead of taking the process down. The errors are printed first, so the diagnostic detail survives either way.
schema-decode!
(schema-decode! [schema :int node :int] :int)
decode against a schema, panicking on any violation.
| schema | a schema value | |
| node | a JSON node |
The decoded value. Aborts with the formatted error message if the value does not satisfy the schema -- use only for trusted data / tests.
(schema-decode! (schema/int) (json/int 5)) ; => 5
Since: Phase SC1
HasSchema
(defclass HasSchema [a])
a type that knows how to validate-and-build itself from a tagged JSON node.
The single method, decode!, puts its dispatch type variable a only in the return position, so the concrete instance is selected from the *expected type at the use site* (an ascription (:: (decode! node) T) or a typed binding (let [x : T (decode! node)] ...)) rather than from any argument. This is return-type-directed dispatch (Phase RT): the elaborator unifies the method's a against the ascribed type and emits a direct call to the matching instance.
An instance pairs a schema (the wire shape) with a builder that turns the validated node into the typed value, e.g.
(definstance HasSchema [User]
(decode! [node]
(let [v (schema-decode! (user-schema) node)]
(make-struct User (json/get-string (json/get! v "name"))
(json/get-int (json/get! v "age"))))))
and is then driven by an ascription:
(:: (decode! (json/decode body)) User) ; => a real User struct
decode! panics (via schema-decode!) on any schema violation; it is for trusted data and tests. For graceful handling, validate first with schema-decode against the type's schema and branch on schema-decode-ok?.
(decode! [node :int] :a)
Since: Phase SC5