Validators¶
A check that several contracts share gets a name. A validator type is a
trait whose supertype is Validator, implemented for the type of value it
checks with one function, validate(self) : Bool. A contract names it in a
rule, or declares a field with it.
Declaring one¶
Email below is implemented for String. The field work_email: Email is
a String held to the check as it is read; the rule
validate contact? with Email holds the optional contact to the same
check, where it is present:
trait Email : Validator { }
impl Email for String {
fn validate(self) : Bool = self.contains("@") and not self.starts_with("@")
}
type Signup : DataContract = {
contact?: String
work_email: Email
validate contact? with Email
}
d : Data = {work_email: "ada@lovelace.org"}
d | Signup
Signup(contact = none, work_email = "ada@lovelace.org")
Signup.work_email is a String: a field declared with a validator type
has the type that implements it. The validator adds a check, not a type of
value.
A tree {work_email: "@lovelace.org", contact: "nobody"} breaks both. The
failure names the field's check as work_email: Email and the rule as
written, validate contact? with Email.
A function or a type¶
validate f with V takes either. A function of the field's type to Bool,
as is_country_code was on the previous page, is the
check written once in the program. A validator type is the check with a
name in the type system: a field can be declared with it, a library can
export it, and a diagnostic can name it. A validator named in a rule must be
implemented for the field's type; validate age with Email over an Int
field is a type error naming the missing implementation.
The standard validators¶
import validators brings the standard ones. Email is implemented for
String. NonEmpty is implemented for String, text that is more than
whitespace, and for List<T>, a list with an element:
import validators
type Signup : DataContract = {
email: Email
name: String
tags: List<String>
validate name with NonEmpty
validate tags with NonEmpty as tagged
}
xs : List<Data> = [
{email: "ada@lovelace.org", name: "Ada", tags: ["math"]},
{email: "ada@", name: " ", tags: []}
]
xs | validate(Signup) | map(r => match r {
ok(s) => s.email,
err(e) => e.violations | map(v => v.rule) | join(" "),
})
["ada@lovelace.org", "Signup.email Signup.1 Signup.tagged"]
The second signup breaks three checks, and all three are named: the field
declared with a validator as Signup.email, the unnamed rule by its
position, and the named rule by its name. validate(Signup) is the form
that gives these back as a value instead of stopping, and is the
next page.
A validator implemented for more than one type, as NonEmpty is, is named
in a rule, where the field's type selects the implementation. It cannot be a
field's type, since there would be two types to read the field as.
The module is imported by name and never by the prelude, so a program's own
Email is its own. It is written in HQL, in
lib/std/validators/mod.hql, and a validator of the program's is declared
the same way.