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.
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
@derivesattribute above a field, or as the legacyderive:option. See DSL reference.
Op forms
| Form | Example | Read as |
|---|---|---|
| Name | not_empty | An op without a parameter. |
| Name and number | max_len=80, tell=49, equal=3, default_when_empty=-1, default_when_empty=1.5 | An op with a number: an integer, a negative number or a float. |
| Name and word | tag=strip_tags, record=user, default_when_empty=unknown | The word becomes a string. For record it becomes an atom. |
| Name and quoted text | default_when_empty="anonymous" | A string. |
| Name and literal | default_when_nil=false, default_when_nil=:none | A boolean or an atom. |
| Name and list | clamp=[0, 100] | A list of numbers or words. |
| Name and typed list | enum=String[admin::user] | A list of values of one type. See the next table. |
| Module and function | custom=[MyApp.Checks, ok?] | A function of one argument that returns true or false. |
| Pattern | regex=^[a-z0-9-]+$ | A regular expression. |
| Nested ops | each=[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.
| Form | Example |
|---|---|
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.
| Form | Example |
|---|---|
| Integer | equal=3, equal=Integer::-3 |
| Float | equal=Float::1.5 |
| String | equal=String::hello |
| Atom | equal=Atom::name |
| Map | equal=Map::%{a: 1} |
| Tuple | equal=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.
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.
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.
| Combinator | Passes when | Notes |
|---|---|---|
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. |
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.
| Op | Parameter must be |
|---|---|
max_len, min_len | A non-negative integer. |
tell | An integer, the country calling code. |
regex | A string, a charlist or a compiled regex. |
enum | A Type[...] form whose values the parser reads, or a list. |
equal | An integer, or a Type::value string whose value matches its type. |
record | An atom or a string. |
custom | [Module, function] or a string. |
tag | An atom or a string, the name of a sanitize op. |
clamp | [min, max], both numbers, min not above max. |
each, optional | A 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. |
either | A 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.