std.contract
all_of : Array Dyn -> Dyn
Builds a contract that checks if the value satisfies all of the given
contracts. all_of
is just an alias for std.contract.Sequence
, to
maintain consistency with std.contract.any_of
and other JSON
Schema-style combinators.
As opposed to other boolean combinators such as not
or any_of
,
all_of
work as you expect most of the time, including on contracts
with a delayed part. The only contracts for which all_of
is
overstrict are function contracts. In general, you shouldn't use
function contracts with contract combinators.
any_of | Array Dyn -> Dyn
Builds a contract that checks if the value satisfies at least one of
the given contracts. More precisely, any_of
applies each contract in
order and returns the result of the first one that doesn't fail
immediately (that is, the first one which returns the value 'Ok _
).
Important any_of
is only an approximation of what you could
expect from an or
operation on contracts. Because any_of
must
preserve the lazyness of contracts, it means that it has to
pick the right branch by relying only on the immediate part of each
contract, and not on the delayed part.
Fortunately, any_of
is an overstrict approximation, in that it might
reject more values than what you expect, but it'll never let invalid
values slip through.
Typically, std.contract.any_of [{ foo | String }, { foo | Number }]
will behave exactly as { foo | Number }
, because the immediate part
of the built-in record contracts can't discriminate between the two
possibilities. In particular, perhaps surprisingly, the value {foo = 1+1}
is rejected. Please refer to the boolean combinators section of
the contract chapter of the manual for a more detailed
explanation of the limitations of any_of
and other boolean
combinators, as well as how to work around them.
Examples
let Date = std.contract.any_of [
String,
{
day | Number,
month | Number,
year | Number
}
]
in
{ day = 1, month = 1, year = 1970 } | Date
apply
Applies a contract to a label and a value. Either aborts the execution with a broken contract error if the contract fails, or proceeds with the evaluation of the value.
A contract annotation such as value | Contract
is eventually
translated (automatically) to a call to std.contract.apply Contract label value
where label
is provided by the Nickel interpreter.
Note that depending on the situation, you might need the variant
std.contract.check
instead. Please refer to the documentation of
std.contract.check
for more information.
Type
For technical reasons, apply
isn't statically typed nor protected by
a contract. The hypothetical type of apply
is Contract -> Label -> Dyn -> Dyn
(note Contract
and Label
currently don't exist).
Arguments
apply
takes three arguments. The arguments are in order:
- a contract which is currently either a custom contract (produced by
the stdlib constructors
std.contract.custom
,std.contract.from_validator
orstd.contract.from_predicate
), a record, a static type or a naked function (see legacy contracts below). - a label (see
std.contract.label
) - the value to be checked
Return value
apply
returns the original value upon success, with potential
delayed checks inserted inside. Some contracts even return a modified
value, such as record contract which can set default values.
Legacy contracts
For backward-compatibility reasons, the argument can also be a naked
function Label -> Dyn -> Dyn
, but this is discouraged and will be
deprecated in the future. For such a contract, please wrap it using
std.contract.custom
instead (the return type must be adapted).
Examples
This example is a custom re-implementation of the builtin contract
[| 'Foo Number |]
. This makes for a short illustration of apply
,
but you should of course just use the builtin contract in real-world
applications.
let FooOfNumber =
std.contract.custom
(fun label =>
match {
'Foo arg => 'Ok ('Foo (std.contract.apply Number label arg)),
_ => 'Error {},
}
)
in
'Foo 3 | FooOfNumber
Diagnostics stack
Using apply
will stack the current custom error reporting data, and
create a fresh working diagnostic. apply
thus acts as a split point
between a contract and its subcontracts, providing the subcontracts
with a fresh diagnostic to use, while remembering the previous
diagnostics set by parent contracts.
Illustration
let ChildContract = std.contract.from_validator (fun _ =>
'Error {
message = "child's message",
notes = ["child's note"]
}
)
in
let ParentContract = std.contract.from_validator (fun _ =>
'Error {
message = "parent's message",
notes = ["parent's note"]
}
)
in
null | ParentContract
# => error: contract broken by a value
This example will print two diagnostics: the main one, using the message and note of the child contract, and a secondary diagnostic, using the parent contract message and note.
This behavior is also observed when manipulating the label directly
and using std.contract.blame
instead of returning an error value.
blame
Raises blame for a given label.
Type: forall a. Label -> a
(for technical reasons, this function isn't actually statically typed)
Blame is the mechanism to signal contract violation in Nickel. It ends the program execution and prints a detailed report thanks to the information tracked inside the label.
Examples
let IsZero = fun label value =>
if value == 0 then
value
else
std.contract.blame label
in
0 | IsZero
blame_with_message
Raises blame with respect to a given label with a custom error message.
Type: forall a. String -> Label -> a
(for technical reasons, this function isn't actually statically typed)
Same as blame
, but takes an additional error message that will be
displayed as part of the blame error. blame_with_message message label
is equivalent to blame (label.with_message message label)
Examples
let IsZero = fun label value =>
if value == 0 then
value
else
std.contract.blame_with_message "Not zero" label
in
0 | IsZero
check : Dyn
-> Dyn
-> Dyn
-> [|
'Ok Dyn,
'Error { message | String | optional, notes | Array String | optional, }
|]
Variant of std.contract.apply
which returns 'Error {..}
upon
immediate failure of the contract instead of aborting the execution
with a blame error, or wraps the return value as 'Ok value
otherwise. Delayed errors may still cause uncatchable blame errors.
check
is intended to be used when calling another contract from a
custom contract, since the return type of check
is precisely the
same as the return type of a custom contract, and because it preserves
immediate errors.
Another use-case is to check if the immediate part of a contract is failing or not without aborting the execution with an uncatchable error.
check
has the same behavior as std.contract.apply
with respect to
the diagnostic stack. See std.contract.apply
for more details.
Looking at the action of check
on a custom contract might help: if
Contract
is a custom contract defined as std.contract.custom contract_impl
where contract_impl
is a function fun label value => ...
, then std.contract.check Contract label value
is just the
normal function application contract_impl label value
(this isn't
entirely true because of the diagnostic stack and how checked values
are tracked for error reporting, but it's mostly true). Of course,
check
works on other types of contracts, such as static types or
record contracts.
Examples
let Nullable = fun Contract =>
std.contract.custom
(fun label value =>
if value == null then
'Ok value
else
std.contract.check Contract label value
)
in
{ foo = 1 } | Nullable {foo | Number}
custom : (Dyn
-> Dyn
-> [|
'Ok Dyn,
'Error { message | String | optional, notes | Array String | optional, }
|])
-> Dyn
Build a generic custom contract. Whenever possible, more restricted
constructors such as std.contract.from_predicate
or
std.contract.from_validator
should be used. However, the most
general std.contract.custom
might be required to write delayed
contracts or contracts taking other contracts as parameters.
Custom contracts
The argument of std.contract.custom
is a function that takes a label
and the value to be checked, and returns 'Ok value
to return
the original value with potential delayed checks inside, or 'Error {..}
to signal an immediate contract violation.
The label should only be used for delayed checks. The preferred method
to indicate a contract violation is to return 'Error {..}
.
For more details on custom contracts and delayed contracts, please refer to the corresponding section of the user manual.
Typing
Because Nickel doesn't currently have proper types for labels and
contracts, they are replaced with Dyn
in the type signature. You can
think of the actual type of the delayed part as (Label -> Dyn -> [| ... |])
, and the return type of custom
as Contract
.
Examples
let Nullable = fun Contract =>
std.contract.custom (fun label value =>
if value == null then
'Ok value
else
std.contract.check Contract label value
)
in
null | Nullable Number
# => null
Equal
Equal
is a delayed contract enforcing equality to a given constant.
Equal
is lazy over arrays and records. When checking such values,
Equal
doesn't test for equality of elements right away (it just tests
that the size is the same for arrays and that fields are the same for
records), but returns a new value where equality subcontracts have been
pushed inside each element.
Example
1 + 4 | std.contract.Equal 5
# => 5
4 | std.contract.Equal 5
# => error: contract broken by a value
from_predicate | (Dyn -> Bool) -> Dyn
Build a contract from a boolean predicate.
Contract application
Before Nickel 1.8, from_predicate
used to wrap the predicate as a
naked function of a label and value. You could thus theoretically
apply it as a normal function, as in
(std.contract.from_predicate my_pred) label value
. While possible,
this wasn't recommended nor really officially supported: all contracts
should be applied using a contract annotation or std.contract.apply
and its variants, as explained in the documentation of
std.contract.apply
.
From Nickel 1.8 and onwards, from_predicate
generates a bespoke
contract value that can only be used in an annotation, be passed to
std.contract.apply
or other operations on contracts, but can't be
used as normal function anymore.
Typing
Because Nickel doesn't currently have proper types for contracts, the
return type of from_predicate
is simply Dyn
in the type signature.
You should think of from_predicate
as some returning a value of some
hypothetical type Contract
.
Examples
let IsZero = std.contract.from_predicate (fun x => x == 0) in
0 | IsZero
from_validator | (Dyn
-> [|
'Ok,
'Error { message | String | optional, notes | Array String | optional, }
|])
-> Dyn
Build a contract from a validator.
Validator
A validator is a function returning either 'Ok
or 'Error {message, notes}
with an optional error message and notes to display. Both
validators and predicates are immediate contracts: they must decide
right away if the value passes or fails (as opposed to delayed
contracts). But as opposed to predicates, validators have the
additional ability to customize the error reporting.
Typing
Because Nickel doesn't currently have proper types for contracts, the
return type of from_validator
is simply Dyn
in the type signature.
You should think of from_validator
as returning a value of some
hypothetical type Contract
.
Examples
let IsZero = std.contract.from_validator (match {
0 => 'Ok,
n if std.is_number n =>
'Error {
message = "expected 0, got %{std.to_string n}",
notes = ["The value is a number, but it isn't 0"],
},
v =>
let vtype = v |> std.typeof |> std.to_string in
'Error { message = "expected a number, got a %{vtype}" }
})
in
0 | IsZero
label
The label submodule provides functions that manipulate the label associated with a contract application.
A label is a special opaque value automatically passed by the Nickel interpreter to contracts when performing a contract check.
A label stores a stack of custom error diagnostics, that can be manipulated by the functions in this module. Labels thus offer a way to customize the error message that will be shown if a contract is broken.
Note that the label should only be used for delayed checks. Immediate error reporting is better done in custom contracts by returning an error value.
The setters (with_XXX
functions) always operate on the current error
diagnostic, which is the last diagnotic on the stack. If the stack is
empty, a fresh diagnostic is silently created when using a setter.
The previous diagnostics on the stack are considered archived, and can't be modified anymore. All diagnostics will be shown during error reporting, with the most recent being used as the main one.
std.contract.apply
and std.contract.check
are the
operations that push a new fresh diagnostic on the stack, saving the
previously current error diagnostic. apply
and friends are typically
used to apply subcontracts inside a parent contract. Stacking the
current diagnostic potentially customized by the parent contract saves
the information inside, and provides a fresh diagnostic for the child
contract to use.
label.append_note
Appends a note to the error notes of the current diagnostic of a label.
Type: String -> Label -> Label
(for technical reasons, this function isn't actually statically typed)
Examples
This example is illustrative. In practice, returning 'Error {..}
should always be preferred over manipulating the label directly
whenever possible.
let AlwaysFailWithNotes = std.contract.custom (fun label _ =>
label
|> std.contract.label.append_note "This is note 1"
|> std.contract.label.append_note "This is note 2"
|> std.contract.blame
)
in
null | AlwaysFailWithNotes
# => error
label.with_message
Attaches a custom error message to the current error diagnostic of a label.
Type: String -> Label -> Label
(for technical reasons, this function isn't actually statically typed)
If a custom error message was previously set, there are two possibilities:
- the label has gone through a
std.contract.apply
call in-between. In this case, the previous diagnostic has been stacked, and usingwith_message
won't erase anything but rather modify the fresh current diagnostic. - no
std.contract.apply
has taken place since the last message was set. In this case, the current diagnostic's error message is replaced.
Examples
This example implements a contract which ensures that the value is
a record with a field foo
that is an even number.
let FooIsEven = std.contract.custom (fun label =>
match {
record @ { foo, .. } =>
'Ok (
std.record.map (fun key value =>
if key == "foo"
&& !(std.is_number value && value % 2 == 0) then
label
|> std.contract.label.with_message "field foo must be an even number"
|> std.contract.blame
else
value
)
record
),
_ => 'Error {},
}
)
in
{ foo = 4, hello = "world" } | FooIsEven
# => { foo = 4, hello = "world" }
label.with_notes
Attaches custom error notes to the current error diagnostic of a label.
Type: Array String -> Label -> Label
(for technical reasons, this function isn't actually statically typed)
If custom error notes were previously set, there are two possibilities:
- the label has gone through a
std.contract.apply
call in-between. In this case, the previous diagnostic has been stacked, and usingwith_notes
won't erase anything but rather modify the fresh current diagnostic. - no
std.contract.apply
has taken place since the last message was set. In this case, the current diagnostic's error notes will be replaced.
Examples
This example is illustrative. In practice, returning 'Error {..}
should always be preferred over manipulating the label directly,
at least whenever possible.
let AlwaysFailWithNotes = std.contract.custom (fun label _ =>
label
|> std.contract.label.with_notes [
"This is note 1",
"This is note 2",
]
|> std.contract.blame
)
in
null | AlwaysFailWithNotes
# => error
not | Dyn -> Dyn
Builds a contract that checks if the value doesn't satisfy the given
contract. More specifically, not Contract
will accept value
if the
application of Contract
to value
fails immediately, returning
'Error {..}
.
Important: as for other boolean combinators, not
is only an
overstrict approximation of what you could expect. For example,
std.contract.not (Array Number)
will reject ["a"]
, because the
check that elements are numbers is delayed. See the boolean
combinators section of the contract chapter of the manual for a more
detailed explanation of the limitations of not
and other boolean
combinators, as well as how to work around them.
Examples
let NotNumber = std.contract.not Number in
"a" | NotNumber
# => "a"
Sequence | Array Dyn -> Dyn
Apply multiple contracts from left to right.
Type: Array Contract -> Contract
(for technical reasons, this function isn't actually statically typed)
Examples
x | std.contract.Sequence [ Foo, Bar ]
is equivalent to
(x | Foo) | Bar
This is useful in positions where one cannot apply multiple contracts, such as an argument to another contract:
x | Array (std.contract.Sequence [ Foo, Bar ])
Or stored in a variable:
let C = std.contract.Sequence [ Foo, Bar ] in
x | C
unstable
The unstable module gathers contracts that are used right now in the standard library to check additional pre-condition, but aren't yet stabilized.
Unstable contracts are useful as a temporary solution for argument validation, but they shouldn't be used outside of the standard library. They aren't part of the backward compatibility policy, and are subject to change at any point in time.
unstable.ArraySliceFun
Warning: this is an unstable item. It might be renamed, modified or deleted in any subsequent minor Nickel version.
A function contract which checks that the three first arguments
are, in order, two numbers and an array such that the two numbers
are a valid slice of that array, that is they are natural numbers
such that: 0 <= n1 <= n2 <= length array
This contract blames only if the arguments are of the right type but don't satisfy the tested condition. The type of arguments is assumed to be checked by an associated static type annotation. For example, if the first argument is a string, this contract silently passes.
unstable.DependentFun
Warning: this is an unstable item. It might be renamed, modified or deleted in any subsequent minor Nickel version.
A dependent function contract, where the argument is also passed to the contract of the returned value.
That is, some_function | DependentFun Foo Bar
performs the
same checks as
fun arg =>
let arg_checked = (arg | Foo) in
(some_function arg_checked | (Bar arg_checked))
unstable.HasField
Warning: this is an unstable item. It might be renamed, modified or deleted in any subsequent minor Nickel version.
A function contract for 2-ary functions which checks that second argument is a record that contains the first argument as a field. This contract is parametrized by the return type of the function.
This contract blames only if the arguments are of the right type but don't satisfy the tested condition (the first is a string and the second is a record). The type of arguments is assumed to be checked by an associated static type annotation. For example, if the first argument is a number, this contract silently passes.
Example
let f | HasField Dyn = fun field record => record."%{field}" in
(f "foo" { foo = 1, bar = 2 })
+ (f "baz" { foo = 1, bar = 2 })
# => error: missing field
In this example, the first call to f
won't blame, but the second
will, as baz
isn't a field of the record argument.
unstable.IndexedArrayFun | [| 'Index, 'Split |] -> Dyn
Warning: this is an unstable item. It might be renamed, modified or deleted in any subsequent minor Nickel version.
A function contract which checks that the two first argument are,
in order, a number and an array such that the number is a valid
index for that array, that is a natural number such that
0 <= index <= length array
.
This contract blames only if the arguments are of the right type but don't satisfy the tested condition. The type of arguments is assumed to be checked by an associated static type annotation. For example, if the first argument is a string, this contract silently passes.
unstable.RangeFun
Warning: this is an unstable item. It might be renamed, modified or deleted in any subsequent minor Nickel version.
A function contract which checks that the two first argument are a
valid range, that is two numbers start
and end
such that
start <= end
.
This contract blames only if the arguments are of the right type but don't satisfy the tested condition. The type of arguments is assumed to be checked by an associated static type annotation. For example, if the first argument is a string, this contract silently passes.
unstable.RangeStep
Warning: this is an unstable item. It might be renamed, modified or deleted in any subsequent minor Nickel version.
A contract for a range step, which must be a positive number.
This contract blames only if the value is a number but is not positive: the fact that the value is a number is assumed to be checked by an associated static type annotation.