What roxyassert does
R is dynamically typed, so a function can be handed the wrong type or
hand back the wrong shape with no complaint. roxyassert
reads the structured type annotations in your roxygen2 docs and, at
document() time, generates per-function argument and return
assertion helpers — calls to the assert
package — so a function’s documented contract and its runtime validation
come from one source.
See the README for the complete annotation grammar; this vignette is a short tour plus the pattern we use most.
Setup
Register the roclet in DESCRIPTION:
Roxygen: list(markdown = TRUE, roclets = c("namespace", "rd", "roxyassert::contract_roclet"))
Annotate a function
A type annotation is a parenthesised token at the start of a
@param or @return description. A bare type is
a vector; a scalar is declared explicitly.
#' Submit an order.
#'
#' @param symbol (scalar<character>) normalised `BASE/QUOTE` pair.
#' @param side (scalar<character in c("BUY", "SELL")>) order side.
#' @param quantity (scalar<numeric in ]0, Inf[>) order size (positive).
#' @param price_limit (scalar<numeric>?) limit price; `NULL` for market orders.
#' @return (data.table) the accepted order:
#' - order_id (character) exchange order id.
#' - quantity (numeric) accepted size.
#' - datetime (POSIXct) acceptance time.
#' @export
submit_order <- function(symbol, side, quantity, price_limit = NULL) {
assert_args_submit_order(symbol, side, quantity, price_limit) # generated
result <- ...
return(assert_return_submit_order(result)) # generated
}devtools::document() (re)writes
R/contracts-generated.R with
assert_args_submit_order() and
assert_return_submit_order(), committed alongside
NAMESPACE.
The pattern: an abstract class that enforces its own contract
A common use is an abstract base class whose public methods define a
uniform interface. Document each method’s inputs and outputs once, and
let roxyassert generate the checks — every concrete
subclass then inherits a contract that is validated at runtime, on both
the way in and the way out.
#' @title AbstractStore
#' @description A key-value store contract. Subclasses implement `.impl_*`;
#' the public methods validate inputs and returns from the documented types.
AbstractStore <- R6::R6Class(
"AbstractStore",
public = list(
#' @description Fetch records by key.
#' @param keys (character) one or more keys to fetch.
#' @param limit (scalar<integer>?) optional max rows.
#' @return (data.table) the matched records:
#' - key (character) the record key.
#' - value (numeric) the stored value.
#' - updated_at (POSIXct) last-write time.
get = function(keys, limit = NULL) {
assert_args_AbstractStore__get(keys, limit) # generated
result <- private$.impl_get(keys, limit)
return(assert_return_AbstractStore__get(result)) # generated
},
#' @description Write one record.
#' @param key (scalar<character>) the record key.
#' @param value (scalar<numeric>) the value to store.
#' @return (class<AbstractStore>) self, invisibly (for chaining).
put = function(key, value) {
assert_args_AbstractStore__put(key, value) # generated
private$.impl_put(key, value)
return(invisible(assert_return_AbstractStore__put(self)))
}
),
private = list(
.impl_get = function(keys, limit) stop("not implemented"),
.impl_put = function(key, value) stop("not implemented")
)
)Because the method names are scoped by class
(assert_args_AbstractStore__get), two classes can share a
method name without colliding. A subclass that overrides
get() re-uses the same documented contract simply by
calling the same generated helpers.
