library(rdantic)
#>
#> Attaching package: 'rdantic'
#> The following object is masked from 'package:graphics':
#>
#> frame
#> The following object is masked from 'package:base':
#>
#> dateA type in rdantic is an ordinary R value: a closure you can call,
print, store in a variable, and pass to struct() or
fn(). This vignette covers the vocabulary for building one
out of R’s own atomic vectors, [ ] and |. For
records and functions see vignette("structs") and
vignette("typed-functions"); for lists, maps and data
frames see vignette("containers").
The four primitives
int, num, chr and
lgl cover R’s atomic vector types. Each one, called on a
value, validates it and hands it back – coerced only when that coercion
cannot lose anything.
int; num; chr; lgl
#> <type> int
#> <type> num
#> <type> chr
#> <type> lgl
int(c(1, 2, 3)) # whole doubles -> integer
#> [1] 1 2 3
num(2L) # integer -> double
#> [1] 2
chr(factor("a")) # factor -> character
#> [1] "a"
int(1.5) # lossy: rejected
#> Error:
#> ! 1 validation problem in int
#> <value> expected int, got num[1] 1.5 -- not a whole number
int("3") # parsing strings is never implicit
#> Error:
#> ! 1 validation problem in int
#> <value> expected int, got chr[1] "3" -- strings are never parsed implicitly
lgl(1) # 0/1 is not TRUE/FALSE
#> Error:
#> ! 1 validation problem in lgl
#> <value> expected lgl, got num[1] 1 -- 0/1 is not TRUE/FALSEEvery error names what was expected and what arrived – type, length, and for scalars the value itself. That shape is the same everywhere in rdantic, whether the failure came from a bare type, a struct field or a function argument.
Because types are values, name them and reuse them across a codebase:
id_t <- int[1][. > 0]
email_t <- chr[1][grepl("@", ., fixed = TRUE)]Use them exactly like any other type – call them to validate:
id_t(1)
#> [1] 1
id_t(c(0, 1))
#> Error:
#> ! 1 validation problem in int[1][. > 0]
#> <value> expected int[1][. > 0], got num[2] -- length is 2, not 1
email_t("something@example.com")
#> [1] "something@example.com"
email_t("This is not an email")
#> Error:
#> ! 1 validation problem in chr[1][grepl("@", ., fixed = TRUE)]
#> <value> expected chr[1][grepl("@", ., fixed = TRUE)], got chr[1] "This is not an email"Length
R’s central data structure is the vector, so a type without a length
is a type of vectors. T[n] fixes the length to exactly
n; T[1] is the common scalar case.
int[3](c(1, 2, 3))
#> [1] 1 2 3
int[3](c(1, 2))
#> Error:
#> ! 1 validation problem in int[3]
#> <value> expected int[3], got num[2] -- length is 2, not 3
int[1](c(1, 2))
#> Error:
#> ! 1 validation problem in int[1]
#> <value> expected int[1], got num[2] -- length is 2, not 1[1] matters more than it looks: most bugs from R’s
recycling rules come from a vector of the wrong length arriving where a
single value was meant.
For a range rather than an exact length, use length(.):
inside [ ], . is bound to the whole vector,
not one element at a time, so length(.) is the vector’s
length.
int[length(.) <= 3](c(1, 2))
#> [1] 1 2
int[length(.) <= 3](c(1, 2, 3, 4))
#> Error:
#> ! 1 validation problem in int[length(.) <= 3]
#> <value> expected int[length(.) <= 3], got num[4]
int[length(.) >= 10](1:10)
#> [1] 1 2 3 4 5 6 7 8 9 10
int[length(.) >= 10](1:5)
#> Error:
#> ! 1 validation problem in int[length(.) >= 10]
#> <value> expected int[length(.) >= 10], got int[5]Constraints
Anything inside [ ] that mentions . is a
predicate over the value, applied elementwise. Length and constraints
chain in any order, and can be combined:
num[. > 0](c(1, 2))
#> [1] 1 2
num[. > 0](c(1, -2))
#> Error:
#> ! 1 validation problem in num[. > 0]
#> <value> expected num[. > 0], got num[2] -- fails at element 2
int[1][. > 5](9)
#> [1] 9
int[1][. > 5](3)
#> Error:
#> ! 1 validation problem in int[1][. > 5]
#> <value> expected int[1][. > 5], got num[1] 3
chr[1][nchar(.) <= 3]("abcd")
#> Error:
#> ! 1 validation problem in chr[1][nchar(.) <= 3]
#> <value> expected chr[1][nchar(.) <= 3], got chr[1] "abcd"The constraint’s source text becomes part of the type’s name, so the error message tells the reader the rule that was broken, not just that some rule was.
Unions, optionals and defaults
A | B accepts either type. T | NULL (or the
shorthand opt(T)) is how to say “optional”.
%default% attaches a value used when nothing is supplied –
by a struct field or a function argument, see
vignette("structs") and
vignette("typed-functions").
(int[1] | chr[1])(7)
#> [1] 7
(int[1] | chr[1])("seven")
#> [1] "seven"
(int[1] | chr[1])(TRUE)
#> Error:
#> ! 1 validation problem in int[1] | chr[1]
#> <value> expected int[1] | chr[1], got lgl[1] TRUE
(int[1] | NULL)(NULL)
#> NULL
opt(int[1])(NULL)
#> NULL
one_of("admin", "user") %default% "user"
#> <type> one_of("admin", "user") (default: "user")Enums, missing values and custom rules
one_of(...) restricts a value to an exact set of
options. no_na(T) wraps any type to additionally reject
NA.
one_of("low", "mid", "high")("high")
#> [1] "high"
one_of("low", "mid", "high")("medium")
#> Error:
#> ! 1 validation problem in one_of("low", "mid", "high")
#> <value> expected one_of("low", "mid", "high"), got chr[1] "medium"
no_na(num)(c(1, 2))
#> [1] 1 2
no_na(num)(c(1, NA))
#> Error:
#> ! 1 validation problem in no_na(num)
#> <value> expected no_na(num), got num[2] -- contains NAFor dates, times and restricted factors, see
vignette("temporal-types").
When nothing built in fits, type_from() builds a type
from a plain predicate and an optional coercion, without touching
rdantic’s internal problem format:
hex <- type_from("hex", function(x) is.character(x) && all(grepl("^#[0-9a-f]{6}$", x)))
hex("#00ff99")
#> [1] "#00ff99"
hex("green")
#> Error:
#> ! 1 validation problem in hex
#> <value> expected hex, got chr[1] "green"
port <- type_from(
"port",
function(x) is.integer(x) && length(x) == 1 && x > 0 && x < 65536,
coerce = function(x) as.integer(x),
json = "integer",
scalar = TRUE
)
port(8080)
#> [1] 8080For a type that needs full control over its validation logic and JSON
Schema fragment, see new_type() in
vignette("extending-rdantic").
Validating a value directly
A type is callable, but three other entry points exist for when the
type is not known ahead of time, or when a thrown error is not what you
want – see vignette("errors-and-validation") for the full
picture: