
<!-- README.md is generated from README.Rmd. Please edit that file -->

# favr <img id="logo" src="man/figures/logo.png" align="right" width="17%" height="17%" />

<!-- badges: start -->

[![R-CMD-check](https://github.com/LJ-Jenkins/favr/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/LJ-Jenkins/favr/actions/workflows/R-CMD-check.yaml)
<!-- badges: end -->

Function Argument Validation for R (favr) provides tools for the
succinct validation and safe type coercion/recycling of function
arguments. A focus is placed on clear error messaging.

## Overview

- `abort_if_not()` for general validation.
- `cast_if_not()` and `recycle_if_not()` for safe type casting and
  recycling of variables.
- `enforce()` for validation and safe type casting and recycling of
  variables.
- `schema()` for the validation and safe type casting and recycling of
  named elements of data.frames/lists.
- `enforce_schema()` to re-evaluate a prior schema call that was
  attached to the data.frame/list.
- `add_to_schema()` add arguments to an existing attached schema and
  re-evaluate.

favr also provides simple wrappers for many
[rlang](https://rlang.r-lib.org/index.html) predicates that enable them
to accept multiple arguments. In nearly all cases, these are
differentiated by replacing the `is_*` prefix with `are_*`.

Any predicate function/expression that returns a `logical`, or raises an
error, will work with favr validations. Named logicals will show which
named element/s gave `FALSE` or `NA`:

``` r
library(favr, warn.conflicts = FALSE)

x <- c(1L, 2L)
y <- data.frame(x = "hi")
z <- list(1)

abort_if_not(
  "{.var x} is not scalar integerish, given: {x}." = rlang::is_scalar_integerish(x)
)
#> Error:
#> Caused by error in `abort_if_not()`.
#> ℹ In argument: `rlang::is_scalar_integerish(x)`.
#> ! `x` is not scalar integerish, given: 1 and 2.

schema(y, x + 1 > 2)
#> Error:
#> Caused by error in `schema()`.
#> ℹ In argument: `x + 1 > 2`.
#> ! Non-numeric argument to binary operator.

abort_if_not(are_list(z, x))
#> Error:
#> Caused by error in `abort_if_not()`.
#> ℹ In argument: `are_list(z, x)`.
#> ! Returned `FALSE`.
#> ✖ `x` is `FALSE`.
```

## Installation

``` r
# Install the latest version of favr from CRAN.

install.packages("favr")

# Or install the development version of favr from GitHub.

# install.packages("pak")
pak::pak("LJ-Jenkins/favr")
```

## Usage

`abort_if_not` can be used for all validations:

``` r
f <- \(x, y) {
  abort_if_not(
    is.character(x),
    "`{x}` is too short!" = nchar(x) > 5,
    y$x == 1
  )
}

f(1L, list(x = 1))
#> Error in `f()`:
#> Caused by error in `abort_if_not()`.
#> ℹ In argument: `is.character(x)`.
#> ! Returned `FALSE`.

f("hi", list(x = 1))
#> Error in `f()`:
#> Caused by error in `abort_if_not()`.
#> ℹ In argument: `nchar(x) > 5`.
#> ! `hi` is too short!
```

`cast_if_not` and `recycle_if_not` provide safe casting and recycling
from [vctrs](https://vctrs.r-lib.org/). Variables are given on the left
hand side (name of the argument) and the expected type/size is given on
the right (input). Assignment is automatically done back into the
environment specified (default is the
[caller_env()](https://rlang.r-lib.org/reference/stack.html)):

``` r
x <- 5L
y <- 1

cast_if_not(x = double())
recycle_if_not(y = x)

class(x)
#> [1] "numeric"
length(y)
#> [1] 5

x <- 1.5

cast_if_not(x = lossy(integer()))

class(x)
#> [1] "integer"

x <- "hi"

cast_if_not(x = integer())
#> Error:
#> Caused by error in `cast_if_not()`.
#> ℹ In argument: `x = integer()`.
#> ! Can't convert `x` <character> to <integer>.
```

`enforce` allows both validations, casting and recycling using the
keyword functions of `cast`, `recycle` and `coerce`. [rlang
formulas](https://rlang.r-lib.org/reference/is_formula.html) need to be
used for casting/recycling, and `c()` can be used in formulas to pass
multiple objects to validations/calls. Multiple validations/calls can be
given on the rhs of a formula when wrapped in `list()`. Assignment
occurs back into the environment specified (default is the
[caller_env()](https://rlang.r-lib.org/reference/stack.html)).

``` r
li <- list(x = 1.5)
y <- 1.5

enforce(
  "{.var li} problem" = li ~ list(
    \(.x) names(.x) == "x",
    coerce(type = list(x = integer()), size = 3, lossy = TRUE),
    "list element not 1?" = ~ length(.x$x) == 1,
    "list itself now length 3" = ~ length(.x) == 3
  ),
  "{.var y} below zero" = y > 0,
  y ~ recycle(10)
)

class(li$x)
#> [1] "numeric"
length(li)
#> [1] 3
length(y)
#> [1] 10

#-- vctrs type/size rules are for all `cast`, `recycle` and `coerce` calls within favr functions

df <- data.frame(x = 1L, y = "hi")

enforce(df ~ cast(data.frame(x = integer(), y = double())))
#> Error:
#> Caused by error in `enforce()`.
#> ℹ In argument: `df ~ cast(data.frame(x = integer(), y = double()))`.
#> ! Can't convert `df$y` <character> to match type of `y` <double>.

x <- 1
y <- 1:5

enforce(c(x, y) ~ list(~ .x > 0, recycle(10)))
#> Error:
#> Caused by error in `enforce()`.
#> ℹ In argument: `c(x, y) ~ list(... recycle(10) ...)`.
#> ! Can't recycle `y` (size 5) to size 10.
```

`schema` provide the same functionality for data-masked arguments from
data.frames/lists. The size of the data.frame/list and whether certain
names are present can also be checked using the `.names` and `.size`
arguments. The altered data-mask object is returned with an attached
class `with_schema` which is used by `add_to_schema()` and
`enforce_schema()` to edit and/or re-evaluate the original schema call.
[Tidyselect
syntax](https://tidyselect.r-lib.org/reference/language.html) can be
used on the lhs of formulas.

``` r
data.frame(x = 2) |>
  schema(x == 1)
#> Error:
#> Caused by error in `schema()`.
#> ℹ In argument: `x == 1`.
#> ! Returned `FALSE`.

data.frame(x = 1L) |>
  schema(x ~ cast(double())) |>
  (\(.) class(.$x))()
#> [1] "numeric"

# recycling is only implemented for lists.
list(x = 1, y = 1, z = 1) |>
  schema(
    x ~ recycle(3),
    y ~ recycle(5),
    z ~ recycle(vctrs::vec_size(x))
  ) |>
  lengths()
#> x y z 
#> 3 5 3

# enforce_schema reapplies the original call.
li <- list(x = 1, y = "hi")
li_with_schema <- schema(li, x == 1, is.character(y))
li_with_schema$y <- 1

enforce_schema(li_with_schema)
#> Error:
#> Caused by error in `enforce_schema()`.
#> ℹ In argument: `is.character(y)`.
#> ! Returned `FALSE`.

df <- data.frame(x = 1:2, xx = 3:4)
df_with_schema <- schema(df, starts_with("x") ~ cast(integer(), lossy = TRUE))
df_with_schema$x <- c(1.5, 2.5)

enforce_schema(df_with_schema)$x
#> [1] 1 2

li_with_schema <- schema(li, c(x, y) ~ recycle(3))
li_with_schema$y <- "hi"

enforce_schema(li_with_schema)$y
#> [1] "hi" "hi" "hi"

# add_to_schema adds to an existing schema and then re-evaluates.
li_with_schema <- li_with_schema |>
  add_to_schema(.names = c("x", "y"), .size = 2)

li_with_schema <- li_with_schema |>
  add_to_schema(y ~ \(.x) nchar(.x) > 2)
#> Error:
#> Caused by error in `add_to_schema()`.
#> ℹ For named element: `y`.
#> ℹ In argument: `y ~ function(.x) nchar(.x) > 2`.
#> ! Returned `FALSE`.
```

Many wrappers of [rlang](https://rlang.r-lib.org) predicates are given
so that multiple inputs can be passed. Optional argument inputs can be
flexibly applied to all or some inputs by using unnamed or named
vectors/lists.

``` r
x <- list()
y <- list(1, 2)

are_list(x, y, list())
#>      x      y list() 
#>   TRUE   TRUE   TRUE

are_list(x, y, list(), .all = TRUE)
#> [1] TRUE

# `.n` is passed to each input.
are_list(x, y, list(), .n = 2)
#>      x      y list() 
#>  FALSE   TRUE  FALSE

# `.n` is passed sequentially.
are_list(x, y, list(), .n = c(0, 2, 0))
#>      x      y list() 
#>   TRUE   TRUE   TRUE

# `.n` is only passed to `y`, other inputs are passed
# the default (NULL).
are_list(x, y, list(), .n = c(y = 5))
#>      x      y list() 
#>   TRUE  FALSE   TRUE
```

### Notes

favr functions that assign into environments (`cast_if_not`,
`recycle_if_not`, and `enforce`) all do clean-up when errors occur:

``` r
x <- 1L
y <- 1L
cast_if_not(x = double(), y = character()) |> try()
#> Error in (function (...)  : Caused by error in `cast_if_not()`.
#> ℹ In argument: `y = character()`.
#> ! Can't convert `y` <integer> to <character>.
cat("Code has errored but `x` has reverted back to:", class(x))
#> Code has errored but `x` has reverted back to: integer
```

favr was inspired by MATLAB’s arguments block and
[schematic](https://whipson.github.io/schematic/). favr relies heavily
on the imported packages [rlang](https://rlang.r-lib.org),
[vctrs](https://vctrs.r-lib.org/), [cli](https://cli.r-lib.org/) and
[tidyselect](https://tidyselect.r-lib.org/). All predicate functions in
favr are simple wrappers around [rlang](https://rlang.r-lib.org)
predicates, for which all credit goes to those authors. For function
argument validation that focuses on performance, see
[checkmate](https://mllg.github.io/checkmate/). An earlier, unreleased
version of this package was called
[restrictr](https://github.com/LJ-Jenkins/restrictr).

## Code of Conduct

Please note that the favr project is released with a [Contributor Code
of Conduct](https://lj-jenkins.github.io/favr/CODE_OF_CONDUCT.html). By
contributing to this project, you agree to abide by its terms.
