4.2 KiB
Constraints and Defaults
A constraint describes a range that a value must satisfy.
Int
String
>= 1
<= 65535
/Hello! .*/
Constraints can be composed with &.
Port = Int & >= 1 & <= 65535;
NarrowedPort = Port & > 443;
The result is the range that satisfies both sides. Incompatible constraints produce a conflict.
Int & String # conflict
> 10 & < 5 # conflict
Int & > 10 & < 11 # conflict: integer candidate does not exist
Primitive and comparison constraints
The built-in primitive ranges are:
String
Int
Float
Bool
Composing different primitive types with & produces a conflict.
Numeric comparison constraints combine as bounds and conflict if they form an empty range.
Int & >= 1 & <= 65535 & > 443
Comparison constraints for Int use integer literals.
Comparison constraints for Float may use either integer or float literals.
Unknown
Unknown is the top range containing every Decodal value.
It is not an Any that disables checking and erases concrete information; it means that the value's range has not yet been narrowed.
Int & Unknown # Int
42 as Unknown # 42
Unknown as Int # conflict
Unknown has no concrete value of its own and therefore cannot be materialized.
It must be narrowed by a concrete value or given a default.
Unknown default {}
Regex constraints
A regex literal is a string constraint.
Host = /^api-[0-9]+$/;
When several regex constraints are composed, a concrete string must match all of them.
String & /^a/ & /z$/
The intersection of regex constraints is not determined during composition. The following range therefore does not conflict when composed, but fails when a concrete value is validated.
String & /^a$/ & /^b$/
In the Rust runtime, enable the regex engine with the regex feature.
Without that feature, validating a regex constraint against a concrete value produces an unsupported-feature diagnostic.
Array constraints
Write an array constraint as [...T]; it applies T to every element.
The element constraint is required.
Names = [...String];
PositiveInts = [...(Int & > 0)];
When composed with a concrete array, each element is refined against T using the same rules as as.
An empty array satisfies every array constraint.
If the element constraint is an object range, fields present only on the right remain abstract in every element. A field present only on the left is outside the right-hand field domain and produces a conflict.
Associative-array and object rest constraints
Write an associative-array constraint as {...T}; it applies T to every field value in an object.
Services = {...{
port = Int;
enabled = Bool default true;
}};
Keys are not enumerated, and an empty object is allowed.
Writing ...T at the end of an object with named fields applies T only to fields that were not named explicitly.
{
enabled = Bool default true;
...Unknown
}
A rest constraint does not generate fields. It only validates additional fields that actually exist.
default
default is not a constraint.
It is a fallback used only when no concrete value exists during materialization.
Port = Int & >= 1 & <= 65535;
Service = {
port = Port default 8080;
};
When an explicit value has been composed into the range, that value is used and default is not evaluated.
Config = {
port = 9000;
} as Service;
Without an explicit value, materialization selects 8080 and verifies that it satisfies Port.
Default composition
- If only one side of
&has adefault, thatdefaultis preserved. - If both sides of
&have different defaults, they conflict. - If
&between a constraint and a concrete value succeeds, the result is concrete and does not retain thedefault. - Because
//is right-biased, the right-hand value ordefaultreplaces the left-hand one for the same field. asdoes not inject a right-handdefaultinto the left side. A field that exists only on the right, however, remains abstract in the result together with itsdefault.
A default expression is evaluated when materialization requires it and must satisfy the constraints of the same range.