diff --git a/NAMESPACE b/NAMESPACE index 59645e1..45710ec 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -33,6 +33,7 @@ export(assert_flag) export(assert_function) export(assert_function_expects) export(assert_function_expects_n_arguments) +export(assert_function_variadic) export(assert_greater_than) export(assert_greater_than_or_equal_to) export(assert_identical) diff --git a/NEWS.md b/NEWS.md index 3761704..2b87da3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,8 @@ * Added `assert_function_expects()` for checking required argument names in functions +* Added `assert_function_variadic()` for checking whether functions declare `...` + * Added `assert_all_strings_contain()` and `assert_string_contains()` for checking character inputs against regular expressions * Added `assert_packages_installed()` diff --git a/R/assert_functions.R b/R/assert_functions.R index 118b8c1..f63158f 100644 --- a/R/assert_functions.R +++ b/R/assert_functions.R @@ -24,6 +24,16 @@ function_expects_n_arguments_advanced <- function(x, n, dots = c("throw_error"," return(TRUE) } +function_variadic_advanced <- function(x){ + if(!is.function(x)) + return("{.strong '{arg_name}'} must be a function, not a {class(arg_value)}") + + if(!func_supports_variable_arguments(x)) + return("{.strong '{arg_name}'} must accept variable arguments via {.strong ...}") + + return(TRUE) +} + # Expect function has arguments # @param @@ -70,6 +80,32 @@ function_expects_advanced <- function(x, required){ #' @export assert_function_expects_n_arguments <- assert_create(func = function_expects_n_arguments_advanced) +#' Assert function is variadic +#' +#' Assert that a function signature declares the `...` argument (variadic +#' arguments). +#' +#' @param x a function to check includes `...` in its signature +#' @inheritParams common_roxygen_params +#' +#' @return invisible(TRUE) if function `x` declares `...`, otherwise aborts with +#' the error message specified by `msg` +#' +#' @examples +#' my_fun <- function(x, ...) x +#' assert_function_variadic(my_fun) +#' +#' try({ +#' my_fun_no_dots <- function(x) x +#' assert_function_variadic(my_fun_no_dots) +#' }) +#' +#' @export +assert_function_variadic <- assert_create_chain( + assert_function, + assert_create(func = function_variadic_advanced) +) + #' Assert function expects specific parameter names #' #' Assert that a function signature includes required set of parameter names in its diff --git a/man/assert_function_expects.Rd b/man/assert_function_expects.Rd index bdb71bd..7012487 100644 --- a/man/assert_function_expects.Rd +++ b/man/assert_function_expects.Rd @@ -40,4 +40,5 @@ assert_function_expects(my_fun, c("x", "y")) try({ assert_function_expects(my_fun, c("x", "z")) }) + } diff --git a/man/assert_function_variadic.Rd b/man/assert_function_variadic.Rd new file mode 100644 index 0000000..0d9fa0b --- /dev/null +++ b/man/assert_function_variadic.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/assert_functions.R +\name{assert_function_variadic} +\alias{assert_function_variadic} +\title{Assert function is variadic} +\usage{ +assert_function_variadic( + x, + msg = NULL, + call = rlang::caller_env(), + arg_name = NULL +) +} +\arguments{ +\item{x}{a function to check includes \code{...} in its signature} + +\item{msg}{The error message thrown if the assertion fails (string)} + +\item{call}{Only relevant when pooling assertions into multi-assertion helper functions. See \link[cli]{cli_abort} for details.} + +\item{arg_name}{Advanced use only. Name of the argument passed (default: NULL, will automatically extract arg_name).} +} +\value{ +invisible(TRUE) if function \code{x} declares \code{...}, otherwise aborts with +the error message specified by \code{msg} +} +\description{ +Assert that a function signature declares the \code{...} argument (variadic +arguments). +} +\examples{ +my_fun <- function(x, ...) x +assert_function_variadic(my_fun) + +try({ + my_fun_no_dots <- function(x) x + assert_function_variadic(my_fun_no_dots) +}) + +} diff --git a/tests/testthat/test-assert_functions.R b/tests/testthat/test-assert_functions.R index ab845c6..69fc472 100644 --- a/tests/testthat/test-assert_functions.R +++ b/tests/testthat/test-assert_functions.R @@ -9,6 +9,7 @@ fn_2_args_with_dots <- function(a, b, ...) {} fn_with_required_args <- function(x, y, z = 1, ...) {} fn_with_defaults <- function(x = 1, y = 2) {} fn_with_no_required <- function(...) {} +fn_no_dots <- function(x, y) x + y # Unit tests for `function_expects_n_arguments_advanced` test_that("function_expects_n_arguments_advanced behaves correctly for exact argument count", { @@ -25,7 +26,7 @@ test_that("function_expects_n_arguments_advanced behaves correctly for exact arg test_that("function_expects_n_arguments_advanced handles dots behavior correctly", { # Test with `...` and dots="throw_error" expect_match(function_expects_n_arguments_advanced(fn_1_arg_with_dots, 1, dots = "throw_error"), - "must not contain ... arguments", fixed = TRUE) + "must not contain ... arguments", fixed = TRUE) # dots throw error by default # Test with `...` and dots="count_as_0" expect_true(function_expects_n_arguments_advanced(fn_1_arg_with_dots, 1, dots = "count_as_0")) @@ -44,6 +45,8 @@ test_that("function_expects_n_arguments_advanced returns correct error for non-f # Input is not a function expect_match(function_expects_n_arguments_advanced(42, 1), "must be a function, not a", fixed = TRUE) expect_match(function_expects_n_arguments_advanced("not_a_function", 1), "must be a function, not a", fixed = TRUE) + expect_match(function_expects_n_arguments_advanced(list(), 1), "must be a function, not a", fixed = TRUE) + expect_match(function_expects_n_arguments_advanced(NULL, 1), "must be a function, not a", fixed = TRUE) }) test_that("function_expects_n_arguments_advanced correctly counts arguments for functions with no arguments", { @@ -87,6 +90,18 @@ test_that("function_expects_advanced validates required argument names", { "must be a function, not a", fixed = TRUE) }) +test_that("function_variadic_advanced validates variable arguments", { + expect_true(function_variadic_advanced(fn_1_arg_with_dots)) + expect_true(function_variadic_advanced(fn_2_args_with_dots)) + + expect_match(function_variadic_advanced(fn_no_dots), + "must accept variable arguments", fixed = TRUE) + expect_match(function_variadic_advanced(1), + "must be a function, not a", fixed = TRUE) + expect_match(function_variadic_advanced(list()), + "must be a function, not a", fixed = TRUE) +}) + @@ -171,5 +186,42 @@ cli::test_that_cli("assert_function_expects() works", config = "plain", { "Custom error message" ) + # Works with a single required argument + expect_true(assert_function_expects(fn_with_required_args, "x")) + + # Reports missing parameters when multiple are absent + expect_error( + assert_function_expects(fn_with_defaults, c("x", "z")), + "missing the following parameter", + fixed = TRUE + ) }) +cli::test_that_cli("assert_function_variadic() works", config = "plain", { + my_fun <- function(a, ...) { a } + my_fun_no_dots <- function(a) { a } + my_fun_with_defaults <- function(a = 1, ...) { a } + my_fun_only_dots <- function(...) { } + my_fun_many <- function(a, b = 1, ...) { a + b } + + # Accepts a basic variadic signature + expect_true(assert_function_variadic(my_fun)) + + # Accepts variadic signatures with defaults + expect_true(assert_function_variadic(my_fun_with_defaults)) + + # Accepts dots-only signatures + expect_true(assert_function_variadic(my_fun_only_dots)) + + # Accepts multiple arguments with dots + expect_true(assert_function_variadic(my_fun_many)) + + # Rejects non-variadic signatures + expect_error(assert_function_variadic(my_fun_no_dots), "must accept variable arguments", fixed = TRUE) + + # Rejects non-function numeric input + expect_error(assert_function_variadic(123), "'123' must be a function, not a numeric") + + # Rejects non-function list input + expect_error(assert_function_variadic(list()), "must be a .*function", fixed = FALSE) +})