From c64c50e0582a5b91a4b9b7842c0252ba934d549e Mon Sep 17 00:00:00 2001 From: selkamand <73202525+selkamand@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:07:41 +1100 Subject: [PATCH 1/2] Rename regex assertions and add helpers --- DESCRIPTION | 3 +- NAMESPACE | 2 + NEWS.md | 2 + R/assert_regex.R | 89 ++++++++++++++++++++++++++++++ man/assert_all_strings_contain.Rd | 46 +++++++++++++++ man/assert_string_contains.Rd | 46 +++++++++++++++ man/contains_pattern.Rd | 29 ++++++++++ tests/testthat/test-assert_regex.R | 35 ++++++++++++ 8 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 R/assert_regex.R create mode 100644 man/assert_all_strings_contain.Rd create mode 100644 man/assert_string_contains.Rd create mode 100644 man/contains_pattern.Rd create mode 100644 tests/testthat/test-assert_regex.R diff --git a/DESCRIPTION b/DESCRIPTION index 48b67ca..1ba4752 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -45,8 +45,9 @@ Collate: 'assert_null.R' 'assert_numerical.R' 'assert_packages.R' + 'has.R' + 'assert_regex.R' 'assert_set.R' 'coverage_testing.R' 'export_testing.R' - 'has.R' BugReports: https://github.com/selkamand/assertions/issues diff --git a/NAMESPACE b/NAMESPACE index 7b9e56d..8f8c406 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,7 @@ export(assert_all_greater_than) export(assert_all_greater_than_or_equal_to) export(assert_all_less_than) export(assert_all_less_than_or_equal_to) +export(assert_all_strings_contain) export(assert_between) export(assert_character) export(assert_character_vector) @@ -62,6 +63,7 @@ export(assert_reactive) export(assert_scalar) export(assert_set_equal) export(assert_string) +export(assert_string_contains) export(assert_subset) export(assert_vector) export(assert_whole_number) diff --git a/NEWS.md b/NEWS.md index 4dc5e3f..da0a03c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # assertions 0.2.0 +* Added `assert_all_strings_contain()` and `assert_string_contains()` for checking character inputs against regular expressions + * Added `assert_packages_installed()` * Added `assert_all_between()` and `assert_between()` assertions diff --git a/R/assert_regex.R b/R/assert_regex.R new file mode 100644 index 0000000..dbc82af --- /dev/null +++ b/R/assert_regex.R @@ -0,0 +1,89 @@ +#' Check if strings contain a regex pattern +#' +#' This helper checks whether all elements of a character vector match a regex pattern. +#' +#' @param x A character vector to check +#' @param pattern A regular expression pattern (string) +#' @param ignore.case,perl,fixed,useBytes Logical flags passed to [grepl()] +#' +#' @return TRUE if all strings in `x` match `pattern`, otherwise FALSE or a string error message. +#' +#' @concept assert_regex +contains_pattern <- function(x, pattern, ignore.case = FALSE, perl = FALSE, fixed = FALSE, useBytes = FALSE) { + if (!is_string(pattern)) + return("'pattern' must be a string (length 1 character vector)") + if (!is_flag(ignore.case)) + return("'ignore.case' must be a logical flag") + if (!is_flag(perl)) + return("'perl' must be a logical flag") + if (!is_flag(fixed)) + return("'fixed' must be a logical flag") + if (!is_flag(useBytes)) + return("'useBytes' must be a logical flag") + + all(grepl(pattern, x, ignore.case = ignore.case, perl = perl, fixed = fixed, useBytes = useBytes)) +} + +#' Assert all strings contain a regex pattern +#' +#' Assert all elements of a character vector match a regex pattern. +#' +#' @include assert_create.R +#' @include assert_type.R +#' @include has.R +#' @include is_functions.R +#' @param x An object to check +#' @param pattern A regular expression pattern (string) +#' @param ignore.case,perl,fixed,useBytes Logical flags passed to [grepl()] +#' @param msg A character string containing the error message to display if `x` does not match `pattern` +#' @inheritParams common_roxygen_params +#' +#' @return invisible(TRUE) if `x` matches `pattern`, otherwise aborts with the error message specified by `msg` +#' +#' @examples +#' try({ +#' assert_all_strings_contain(c("abc", "a1"), "^a") # Passes +#' assert_all_strings_contain(c("abc", "b1"), "^a") # Throws default error +#' assert_all_strings_contain(c("abc", "b1"), "^a", msg = "Custom error message") # Throws custom error +#' }) +#' +#' @concept assert_regex +#' @export +assert_all_strings_contain <- assert_create_chain( + assert_character_vector, + assert_no_missing, + assert_create( + func = contains_pattern, + default_error_msg = "'{.strong {arg_name}}' must all match regex {.strong {pattern}}" + ) +) + +#' Assert string contains a regex pattern +#' +#' Assert a string matches a regex pattern. +#' +#' @include assert_create.R +#' @include assert_type.R +#' @include has.R +#' @include is_functions.R +#' @param x An object to check +#' @param pattern A regular expression pattern (string) +#' @param ignore.case,perl,fixed,useBytes Logical flags passed to [grepl()] +#' @param msg A character string containing the error message to display if `x` does not match `pattern` +#' @inheritParams common_roxygen_params +#' +#' @return invisible(TRUE) if `x` matches `pattern`, otherwise aborts with the error message specified by `msg` +#' +#' @examples +#' try({ +#' assert_string_contains("abc", "^a") # Passes +#' assert_string_contains("abc", "^b") # Throws default error +#' assert_string_contains("abc", "^b", msg = "Custom error message") # Throws custom error +#' }) +#' +#' @concept assert_regex +#' @export +assert_string_contains <- assert_create_chain( + assert_string, + assert_all_strings_contain +) diff --git a/man/assert_all_strings_contain.Rd b/man/assert_all_strings_contain.Rd new file mode 100644 index 0000000..39a2d97 --- /dev/null +++ b/man/assert_all_strings_contain.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/assert_regex.R +\name{assert_all_strings_contain} +\alias{assert_all_strings_contain} +\title{Assert all strings contain a regex pattern} +\usage{ +assert_all_strings_contain( + x, + pattern, + ignore.case = FALSE, + perl = FALSE, + fixed = FALSE, + useBytes = FALSE, + msg = NULL, + call = rlang::caller_env(), + arg_name = NULL +) +} +\arguments{ +\item{x}{An object to check} + +\item{pattern}{A regular expression pattern (string)} + +\item{ignore.case, perl, fixed, useBytes}{Logical flags passed to \code{\link[=grepl]{grepl()}}} + +\item{msg}{A character string containing the error message to display if \code{x} does not match \code{pattern}} + +\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 \code{x} matches \code{pattern}, otherwise aborts with the error message specified by \code{msg} +} +\description{ +Assert all elements of a character vector match a regex pattern. +} +\examples{ +try({ +assert_all_strings_contain(c("abc", "a1"), "^a") # Passes +assert_all_strings_contain(c("abc", "b1"), "^a") # Throws default error +assert_all_strings_contain(c("abc", "b1"), "^a", msg = "Custom error message") # Throws custom error +}) + +} +\concept{assert_regex} diff --git a/man/assert_string_contains.Rd b/man/assert_string_contains.Rd new file mode 100644 index 0000000..60eb952 --- /dev/null +++ b/man/assert_string_contains.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/assert_regex.R +\name{assert_string_contains} +\alias{assert_string_contains} +\title{Assert string contains a regex pattern} +\usage{ +assert_string_contains( + x, + pattern, + ignore.case = FALSE, + perl = FALSE, + fixed = FALSE, + useBytes = FALSE, + msg = NULL, + call = rlang::caller_env(), + arg_name = NULL +) +} +\arguments{ +\item{x}{An object to check} + +\item{pattern}{A regular expression pattern (string)} + +\item{ignore.case, perl, fixed, useBytes}{Logical flags passed to \code{\link[=grepl]{grepl()}}} + +\item{msg}{A character string containing the error message to display if \code{x} does not match \code{pattern}} + +\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 \code{x} matches \code{pattern}, otherwise aborts with the error message specified by \code{msg} +} +\description{ +Assert a string matches a regex pattern. +} +\examples{ +try({ +assert_string_contains("abc", "^a") # Passes +assert_string_contains("abc", "^b") # Throws default error +assert_string_contains("abc", "^b", msg = "Custom error message") # Throws custom error +}) + +} +\concept{assert_regex} diff --git a/man/contains_pattern.Rd b/man/contains_pattern.Rd new file mode 100644 index 0000000..380ccc7 --- /dev/null +++ b/man/contains_pattern.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/assert_regex.R +\name{contains_pattern} +\alias{contains_pattern} +\title{Check if strings contain a regex pattern} +\usage{ +contains_pattern( + x, + pattern, + ignore.case = FALSE, + perl = FALSE, + fixed = FALSE, + useBytes = FALSE +) +} +\arguments{ +\item{x}{A character vector to check} + +\item{pattern}{A regular expression pattern (string)} + +\item{ignore.case, perl, fixed, useBytes}{Logical flags passed to \code{\link[=grepl]{grepl()}}} +} +\value{ +TRUE if all strings in \code{x} match \code{pattern}, otherwise FALSE or a string error message. +} +\description{ +This helper checks whether all elements of a character vector match a regex pattern. +} +\concept{assert_regex} diff --git a/tests/testthat/test-assert_regex.R b/tests/testthat/test-assert_regex.R new file mode 100644 index 0000000..65ad3d2 --- /dev/null +++ b/tests/testthat/test-assert_regex.R @@ -0,0 +1,35 @@ +cli::test_that_cli("assert_all_strings_contain() works", configs = "plain", { + # Works for matching strings + expect_identical(assert_all_strings_contain(c("abc", "a1"), "^a"), TRUE) + + # Aborts for non-matching strings + expect_error(assert_all_strings_contain(c("abc", "b1"), "^a"), "must all match regex", fixed = FALSE) + + # Aborts for non-character inputs + expect_error(assert_all_strings_contain(1, "^a"), "character vector", fixed = FALSE) + + # Aborts for invalid pattern inputs + expect_error(assert_all_strings_contain("abc", 1), "pattern.*string", fixed = FALSE) + + # Error messages use variable name of passed arguments + x <- c("abc", "b1") + expect_error(assert_all_strings_contain(x, "^a"), "'x'.*match regex", fixed = FALSE) + + # Custom error messages work + expect_error(assert_all_strings_contain(c("abc", "b1"), "^a", msg = "Custom error message"), "Custom error message") +}) + +cli::test_that_cli("assert_string_contains() works", configs = "plain", { + # Works for matching string + expect_identical(assert_string_contains("abc", "^a"), TRUE) + expect_identical(assert_string_contains("Abc", "^a", ignore.case = TRUE), TRUE) + + # Aborts for non-matching string + expect_error(assert_string_contains("abc", "^b"), "match regex", fixed = FALSE) + + # Aborts for non-string inputs + expect_error(assert_string_contains(c("abc", "b1"), "^a"), "string", fixed = FALSE) + + # Custom error messages work + expect_error(assert_string_contains("abc", "^b", msg = "Custom error message"), "Custom error message") +}) From b5c6555c2c0b7ee7c53d7ce47eedd0123e3f0738 Mon Sep 17 00:00:00 2001 From: selkamand <73202525+selkamand@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:03:28 +1100 Subject: [PATCH 2/2] Refine regex assertion messages --- R/assert_regex.R | 23 ++++++++++++++++++++--- tests/testthat/test-assert_regex.R | 15 +++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/R/assert_regex.R b/R/assert_regex.R index dbc82af..80af607 100644 --- a/R/assert_regex.R +++ b/R/assert_regex.R @@ -21,7 +21,25 @@ contains_pattern <- function(x, pattern, ignore.case = FALSE, perl = FALSE, fixe if (!is_flag(useBytes)) return("'useBytes' must be a logical flag") - all(grepl(pattern, x, ignore.case = ignore.case, perl = perl, fixed = fixed, useBytes = useBytes)) + matches <- grepl(pattern, x, ignore.case = ignore.case, perl = perl, fixed = fixed, useBytes = useBytes) + + if (all(matches)) + return(TRUE) + + total <- length(x) + failed <- sum(!matches) + + if (total == 1) { + return("'{.strong {arg_name}}' must match regex `{pattern}`") + } + + paste0( + "'{.strong {arg_name}}' must all match regex `{pattern}`. ", + failed, + "/", + total, + " elements did not match pattern" + ) } #' Assert all strings contain a regex pattern @@ -53,8 +71,7 @@ assert_all_strings_contain <- assert_create_chain( assert_character_vector, assert_no_missing, assert_create( - func = contains_pattern, - default_error_msg = "'{.strong {arg_name}}' must all match regex {.strong {pattern}}" + func = contains_pattern ) ) diff --git a/tests/testthat/test-assert_regex.R b/tests/testthat/test-assert_regex.R index 65ad3d2..a6c08c6 100644 --- a/tests/testthat/test-assert_regex.R +++ b/tests/testthat/test-assert_regex.R @@ -3,7 +3,18 @@ cli::test_that_cli("assert_all_strings_contain() works", configs = "plain", { expect_identical(assert_all_strings_contain(c("abc", "a1"), "^a"), TRUE) # Aborts for non-matching strings - expect_error(assert_all_strings_contain(c("abc", "b1"), "^a"), "must all match regex", fixed = FALSE) + expect_error( + assert_all_strings_contain(c("abc", "b1"), "^a"), + "must all match regex `\\^a`.*1/2 elements did not match pattern", + fixed = FALSE + ) + + # Single element does not include count + expect_error( + assert_all_strings_contain("abc", "^b"), + "must match regex `\\^b`$", + fixed = FALSE + ) # Aborts for non-character inputs expect_error(assert_all_strings_contain(1, "^a"), "character vector", fixed = FALSE) @@ -25,7 +36,7 @@ cli::test_that_cli("assert_string_contains() works", configs = "plain", { expect_identical(assert_string_contains("Abc", "^a", ignore.case = TRUE), TRUE) # Aborts for non-matching string - expect_error(assert_string_contains("abc", "^b"), "match regex", fixed = FALSE) + expect_error(assert_string_contains("abc", "^b"), "must match regex `\\^b`$", fixed = FALSE) # Aborts for non-string inputs expect_error(assert_string_contains(c("abc", "b1"), "^a"), "string", fixed = FALSE)