Derive string syntax

v0.1.4

The derives: option of a field takes a string. This page lists everything that string can contain: its groups, the forms an op can take, the operands, the combinators and the checks made when a module compiles. GuardedStruct 0.1.4 has 65 validate ops and 23 sanitize ops, listed on Validate ops and Sanitize ops.

Groups

A derive string is one or more groups separated by spaces. A group is sanitize(...) or validate(...) with ops separated by commas. A group with another name, such as validte(string), and a group with no closing ), such as validate(string, raise a Spark.Error.DslError that names the group when the module compiles. An empty or blank string checks nothing.

elixir
derives: "sanitize(trim, downcase) validate(string, not_empty, max_len=80)"
  • Groups of one kind merge, and all sanitize ops run before all validate ops. See How derives work.
  • The same string can be written as the @derives attribute above a field, or as the legacy derive: option. See DSL reference.

Op forms

FormExampleRead as
Namenot_emptyAn op without a parameter.
Name and numbermax_len=80, tell=49, equal=3, default_when_empty=-1, default_when_empty=1.5An op with a number: an integer, a negative number or a float.
Name and wordtag=strip_tags, record=user, default_when_empty=unknownThe word becomes a string. For record it becomes an atom.
Name and quoted textdefault_when_empty="anonymous"A string.
Name and literaldefault_when_nil=false, default_when_nil=:noneA boolean or an atom.
Name and listclamp=[0, 100]A list of numbers or words.
Name and typed listenum=String[admin::user]A list of values of one type. See the next table.
Module and functioncustom=[MyApp.Checks, ok?]A function of one argument that returns true or false.
Patternregex=^[a-z0-9-]+$A regular expression.
Nested opseach=[trim, downcase]A combinator. See Combinators.

Numbers can be negative or floats, in a list too: clamp=[-5, 5] and clamp=[0, 1.5]. A parameter the parser cannot read raises a Spark.Error.DslError when the module compiles, so a typo never becomes a rule that does nothing.

Enum operands

enum checks that the value is one of a fixed list. Write the type, then the values in brackets, separated by ::. The values are evaluated once, when the module compiles.

FormExample
String[...]enum=String[admin::user::guest]
Atom[...]enum=Atom[admin::user]
Integer[...]enum=Integer[1::2::3]
Float[...]enum=Float[1.5::2.0]
Map[...]enum=Map[%{status: 1}::%{status: 2}]
Tuple[...]enum=Tuple[{:admin, 1}::{:user, 2}]

The value must match the type: a string never matches Atom[...]. The values must be ones the Elixir parser reads. enum=Atom[a, b] raises a Spark.Error.DslError when the module compiles, and so does enum=Integer[1::x], because x is not a number. Use equal to compare with one fixed value.

Equal operands

equal checks that the value is the same as one fixed value, compared with ===. Write an integer as it is, or write the type, :: and the value.

FormExample
Integerequal=3, equal=Integer::-3
Floatequal=Float::1.5
Stringequal=String::hello
Atomequal=Atom::name
Mapequal=Map::%{a: 1}
Tupleequal=Tuple::{:a, 1}

The typed value can be quoted too, as in equal="Integer::3". A value that does not match its type, such as equal=Integer::abc, raises a Spark.Error.DslError when the module compiles. In a domain: rule the separator is >>, written Equal[Atom>>name].

Regular expressions

Write the pattern after regex=. The parser keeps brackets, parentheses and braces together, and it skips escaped characters, so most patterns work as they are.

elixir
derives: "validate(regex=^[a-z0-9-]+$)"
derives: "validate(regex=^[A-Z]{2,5}$)"
derives: "validate(regex=^https?://[a-z.-]+(:[0-9]+)?(/.*)?$)"
derives: "validate(each=[regex=^[a-z0-9.-]+$])"

Quote the pattern when it holds a comma, a closing parenthesis or a closing bracket outside any group. The sigil ~S saves you from escaping the quotes.

elixir
derives: ~S|validate(regex="^a,b$")|

A regex op matches strings. A value that is not a string fails with the action type, and a string that does not match fails with the action regex.

Combinators

Three ops take a list of other ops. They nest to any depth.

CombinatorPasses whenNotes
each=[...]Every item of a list passes all inner ops.Works in sanitize and validate. Each error carries __index__. Fails with the action each if the value is not a list.
optional=[...]The value is nil, or passes all inner ops.Only in validate.
either=[...]The value passes at least one inner op.Only in validate. Fails with the action either.
elixir
derives: "sanitize(each=[trim, downcase], reject_empty, uniq) validate(list, max_len=20, each=[string, hostname])"
derives: "validate(each=[optional=[either=[each=[string], each=[integer]]]])"

A single inner op can drop the brackets for optional and each: each=string.

Checks made when the module compiles

These parameter checks raise a Spark.Error.DslError that names the field.

OpParameter must be
max_len, min_lenA non-negative integer.
tellAn integer, the country calling code.
regexA string, a charlist or a compiled regex.
enumA Type[...] form whose values the parser reads, or a list.
equalAn integer, or a Type::value string whose value matches its type.
recordAn atom or a string.
custom[Module, function] or a string.
tagAn atom or a string, the name of a sanitize op.
clamp[min, max], both numbers, min not above max.
each, optionalA list of ops of the right kind. Built-in ops and the ops of the module's derive extensions are known. An unknown inner op is an error, unless a validate_derive or sanitize_derive module is set, which accepts any name.
eitherA list. Each inner op is checked for its parameters, not for its name.

A derive string that the Elixir parser refuses, such as validate(enum=Atom[a, b]), a group named something other than sanitize or validate, and a group that is not closed raise the same error. Validate.run/2 makes the same checks and returns an error with the action bad_parameters instead of raising.