library(rdantic)
#>
#> Attaching package: 'rdantic'
#> The following object is masked from 'package:graphics':
#>
#> frame
#> The following object is masked from 'package:base':
#>
#> dateMost custom rules fit inside type_from()
(vignette("types-in-depth")): a predicate, an optional
coercion, and rdantic looks after the problem format for you. This
vignette covers new_type(), the lower-level constructor
that everything else in the package – int,
list_of(), struct(), fn() – is
itself built from, for when a type needs full control over its
validation and its JSON Schema fragment.
What a type actually is
A type is a closure that validates a value, carrying a
spec attribute describing it (name,
validate, schema, and whatever else the
constructor stored). Calling a type runs validate();
printing it, or asking schema() for its JSON Schema, reads
the rest of the spec. new_type() is how that closure gets
built:
even <- new_type(
"even",
validate = function(x, path) {
if (is.numeric(x) && all(x %% 2 == 0)) x else rdantic:::.bad(path, "even", type_of(x))
},
schema = function() list(type = "integer", multipleOf = 2)
)
even
#> <type> even
even(c(2, 4))
#> [1] 2 4
even(3)
#> Error:
#> ! 1 validation problem in even
#> <value> expected even, got num[1]validate receives the value and the path at
which it appears (so an error nested inside a struct or a list reports
the right location) and returns either the – possibly coerced – value,
or a problem record built by rdantic’s internal helpers.
type_of() (exported, used above) renders a value the way
rdantic names types in its own error messages, so a custom type’s errors
read the same as a built-in one’s.
schema(even)
#> $type
#> [1] "integer"
#>
#> $multipleOf
#> [1] 2When to reach for this vs. type_from()
| Need | Use |
|---|---|
| A predicate, maybe a coercion, default JSON Schema shape | type_from() |
| Full control over the problem message, or a custom JSON Schema fragment | new_type() |
| A restricted set of exact values | one_of() |
| A record with named fields | struct() |
In practice new_type() is rarely needed directly – it
exists so the rest of rdantic can be implemented in terms of it, and
remains available for the rare case a project’s own domain type needs
the same level of control.