Validators
Run in Rust; TypeScript not yet
comline build verifies validators: an unknown validator name, a validator
put on a value of a type it doesn't apply to, a keyword argument that isn't
a property or whose literal type is wrong, a missing argument for a property
without a default, or an undeclared params.* reference all fail the build. Generated Rust code then
runs them. Generated TypeScript doesn't yet: it
generates the types without the checks, and comline generate warns. See
TypeScript's language reference.
A validator is a named, parameterised check you attach to a
struct or error field, or to a
type alias.
Target types — for
Every validator names the type(s) it checks, after for. A validator without
for is a build error.
- A field can take a validator only if its type is one of the targets, matched
exactly: a
u8field doesn't take afor u32validator. - A
typealias counts as the type it stands for, so a field of typetype Name = stringtakes afor stringvalidator. - An
optionalfield is checked as its inner type. - On an array field, a validator checks the array, not each element: a
for stringvalidator doesn't go on astring[]field. - Targets are primitives, arrays of them, or aliases. There are no generic targets; list each type instead.
Properties
name: Type [= default], one per line — the same shape as a
struct field, minus optional and annotations. They are
the validator's configuration, supplied where the validator is used.
validate
A validate block holds one or more assert(condition, message) calls.
condition— member paths overvalue.*(the field under check) andparams.*(this validator's properties), compared with== != >= <= > <and joined byand/or.assert(c, m)passes whencis true. Numbers compare by value, so au8value against au32property is fine.valueaccessors:valueitself;value.length, only when every target isstringor an array. A string's length counts characters (Unicode scalar values), not bytes, in every language; an array's counts elements.value.name, where the value sits, for messages:handle,friends[1],author.handle.
Anything else is a build error at the validator.
- message — a string, or an f-string whose
{value.…} and {params.…} placeholders are substituted. A plain "..."
is printed as written.
Three mistakes build, but warn:
| Warning | When |
|---|---|
validation::validator-checks-nothing |
the validator has no validate block, or an empty one; shown at the block when there is one |
validation::validator-property-unused |
a property no assert or message reads |
validation::message-not-interpolated |
a plain "..." message with a {...} in it, which would print as written; the editor's quick fix makes it an f-string |
The condition language is deliberately small and non-computational — no precedence, no loops, no bindings.
Using one — @validators
Attach validators to a field with the @validators annotation: a list of calls
that bind the validator's properties by name.
Unbound properties fall back to their declared default; leaving out one without
a default is a build error. A validator with nothing to bind can be written
bare: @validators = [NonEmpty] is the same as [NonEmpty()].
Errors' fields take @validators too.
A validator from another schema needs a use, like a type does (such as
use std::validators::StringBounds). Without one it's unknown, and the editor
offers to add the use. An imported validator is checked the same way as a
local one: its for targets and its arguments, against its declaration.
@validators goes on a field or a type alias. On a struct, error, protocol or
function there's no single value for it to check, so it's a build error.
On a type alias
Put @validators on a type alias to check every value of that alias,
wherever it's used:
- The alias stays the type it stands for:
Handleis still aStringin generated Rust. Adding a validator never changes a type's API or its wire format. - The validator's
fortarget is checked once, against what the alias resolves to. - A field of the alias runs the alias's checks first, then its own.
- An alias of an alias collects the checks along the chain.
- An array of the alias (
Handle[]) checks each element. - An alias is the only way to check a function argument: arguments take no
annotations of their own, but
to: HandlerunsHandle's checks.
@deprecated works on an alias too, and warns wherever the alias is used.
In generated code
In Rust, every struct, error and function-params struct with something to
check implements comline_runtime::validation::Validate. That includes checks
on its own fields, through an alias, or further down in a nested struct or an
array element.
use comline_runtime::validation::Validate;
account.validate()?; // Err(ValidationErrors): every failed check
account.validate_first()?; // Err(ValidationError): the first one
Each ValidationError carries field, the path to the failing value
(friends[1]), and message, the validator's message with its placeholders
filled in (friends[1] must be at least 2 characters). An absent optional
field skips its checks.
Nothing calls validate() for you unless you ask:
validate_on_decode
in comline.toml makes decoding run the checks, and makes a provider refuse a
call whose arguments fail them.
Properties are filled in when the code is generated: the argument you wrote, else the property's default, with constants resolved. Nothing about the validator exists at run time beyond the checks themselves.