diff --git a/NAMESPACE b/NAMESPACE index 7ae7a68..6d44595 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,6 +2,7 @@ export(check_container_class) export(check_envvar) +export(check_that) export(get_auth_token) export(get_container) export(list_container_names) diff --git a/R/azkit_helpers.R b/R/azkit_helpers.R index 8003217..550bca7 100644 --- a/R/azkit_helpers.R +++ b/R/azkit_helpers.R @@ -1,3 +1,29 @@ +#' An alternative to stopifnot/assert_that etc +#' +#' If the predicate function is true of `x` then `x` is returned. Otherwise, +#' an error is thrown with a custom `message`. +#' +#' @param x The object to be checked +#' @param predicate The predicate function used to check `x` +#' @param message A custom error message, as a string. Will be shown to the +#' user if the predicate check does not succeed. Can include `glue`d variables +#' and `{cli}` semantic markup. +#' @param pf Set as [parent.frame()] so variables in the caller environment can +#' be used in the custom error message. +#' @seealso [check_vec] +#' @export +check_that <- function(x, predicate, message, pf = parent.frame()) { + if (predicate(x)) { + x + } else { + cli::cli_abort(message, call = rlang::caller_call(), .envir = pf) + } +} + + +#' @keywords internal +ct_error_msg <- \(text) paste0("{.fn check_that}: ", text) + #' An alternative to stopifnot/assert_that etc #' #' This function makes it easy to use the `{purrr}` functions `every()`, @@ -6,10 +32,9 @@ #' Not suitable for checking if `length(x) == 1` as it will check vectors #' element-wise, so will potentially return `TRUE` even if `length(x) > 1` #' -#' @param x The object to be checked #' @param predicate The predicate function used to check elements of `x` #' @param message A custom error message, as a string. Will be shown to the -#' user if the predicate check does not succeed. Can include `glue` variables +#' user if the predicate check does not succeed. Can include `glue`d variables #' and `{cli}` semantic markup. Variable values will be searched for in the #' environment of the caller function (not in the environment of `check_vec()` #' ). This makes it easier to include informative values in the message. @@ -19,8 +44,7 @@ #' predicate. "none" can be used to generate an inverse predicate, or the #' situation where success means that none of the elements of x satisfies the #' predicate. "some" is unlikely to be useful often, but it is available. -#' @param pf Set as [parent.frame] so variables in the caller environment can -#' be used in the custom error message. +#' @inheritParams check_that #' @seealso [check_scalar_type()] #' @keywords internal check_vec <- function( @@ -51,8 +75,8 @@ cv_error_msg <- \(text) paste0("{.fn check_vec}: ", text) #' Possible values for the `type` parameter are: "character", "logical", "list", #' "integer", "double", "string", "bool", "bytes", "raw", "vector", "complex". # Supplying "string" or "bool" will additionally check that `x` is not missing. -#' @seealso [check_vec()] -#' @inheritParams check_vec +#' @seealso [check_that] +#' @inheritParams check_that #' @param type A string defining the R object type that `x` is checked to be #' @keywords internal check_scalar_type <- function( @@ -88,6 +112,32 @@ check_scalar_type <- function( cst_error_msg <- \(text) paste0("{.fn check_scalar_type}: ", text) +#' Check if a supplied non-NULL value is a string with >0 characters +#' +#' Will error if x is equal to `""`, or if it is otherwise missing or invalid. +#' With the exception that if x is NULL, then NULL will be passed through. +#' @inheritParams check_that +#' @param message A custom error message, as a string. Will be shown to the +#' user if the check does not pass. Can include `glue` variables and `{cli}` +#' semantic markup. Variable values will be searched for in the environment of +#' the caller function (not in the environment of `check_nzchar()`). This +#' makes it easier to include informative values in the message. +#' @keywords internal +check_nzchar <- function(x, message, pf = parent.frame()) { + if (is.null(x)) { + NULL + } + cnz <- "check_nzchar" # nolint + check_scalar_type(x, "string", "{.fn {cnz}}: {.var x} is not a string") + if (nzchar(x)) { + x + } else { + message <- paste0("{.fn {cnz}}: ", message) + cli::cli_abort(message, call = rlang::caller_call(), .envir = pf) + } +} + + #' grepl a glued regex #' #' Use \{glue\} expressions in grepl (and put the arguments the right way round) diff --git a/R/get_container.R b/R/get_container.R index f090401..edc7331 100644 --- a/R/get_container.R +++ b/R/get_container.R @@ -1,23 +1,27 @@ #' Get Azure storage container #' -#' Use [list_container_names] to see a list of available containers +#' The environment variable "AZ_STORAGE_EP" must be set. This provides the URL +#' for the default Azure storage endpoint. +#' Use [list_container_names] to get a list of available container names. #' #' @param container_name Name of the container as a string. `NULL` by default, #' which means the function will look instead for a container name stored in #' the environment variable "AZ_CONTAINER" -#' @param ... arguments to be passed through to [get_auth_token()] +#' @param ... arguments to be passed through to [get_auth_token] #' @returns An Azure blob container (list object of class "blob_container") #' @export get_container <- function(container_name = NULL, ...) { - cst_msg <- cst_error_msg("{.var container_name} must be a string") - container_name <- (container_name %||% check_envvar("AZ_CONTAINER")) |> - check_scalar_type("character", cst_msg) + msg <- glue::glue( + "{.var container_name} is empty. ", + "Did you forget to set an environment variable?" + ) + cont_nm <- check_nzchar(container_name, msg) %||% check_envvar("AZ_CONTAINER") token <- get_auth_token(...) endpoint <- get_default_endpoint(token) container_names <- list_container_names(token) - not_found_msg <- cv_error_msg("Container {.val {container_name}} not found") - container_name |> - check_vec(\(x) x %in% container_names, not_found_msg) |> + not_found_msg <- ct_error_msg("Container {.val {cont_nm}} not found") + cont_nm |> + check_that(\(x) x %in% container_names, not_found_msg) |> AzureStor::blob_container(endpoint = endpoint) } @@ -59,6 +63,6 @@ get_default_endpoint <- function(token) { #' @returns the value of the environment variable named in `x` #' @export check_envvar <- function(x) { - cst_msg <- cst_error_msg("{.envvar {x}} is not set") + cst_msg <- cst_error_msg("The environment variable {.envvar {x}} is not set") check_scalar_type(Sys.getenv(x, NA_character_), "string", cst_msg) } diff --git a/R/list_files.R b/R/list_files.R index a33ed39..cfff3f3 100644 --- a/R/list_files.R +++ b/R/list_files.R @@ -31,8 +31,8 @@ list_files <- function(container, path = "", ext = "", recursive = TRUE) { stopifnot(rlang::is_character(c(path, ext), 2)) stopifnot(rlang::is_bool(recursive)) - pnf_msg <- cv_error_msg("Path {.val {path}} not found") - check_vec(path, \(x) AzureStor::blob_dir_exists(container, x), pnf_msg) + pnf_msg <- ct_error_msg("Path {.val {path}} not found") + check_that(path, \(x) AzureStor::blob_dir_exists(container, x), pnf_msg) tbl <- AzureStor::list_blobs(container, path, recursive = recursive) if (nrow(tbl) > 0) { diff --git a/R/read_azure_files.R b/R/read_azure_files.R index a5806e6..201ecda 100644 --- a/R/read_azure_files.R +++ b/R/read_azure_files.R @@ -14,7 +14,7 @@ #' being read. Useful for checking the function is doing what is expected, but #' can be turned off with `FALSE`. Can be set persistently with the option #' "azkit.info". If `NULL` then it will default to the value of -#' [rlang::is_interactive] (ie `TRUE` for interactive sessions). +#' [rlang::is_interactive] (that is, `TRUE` for interactive sessions). #' @param ... optional arguments to be passed through to [arrow::read_parquet] #' @returns A tibble #' @examples \dontrun{ @@ -139,12 +139,10 @@ check_blob_exists <- function(container, file, ext, info, path) { dplyr::filter(dplyr::if_any("name", \(x) x == {{ file_path }})) |> dplyr::pull("name") - if (length(filepath_out) == 0) { - cli::cli_abort("no matching {ext} file found") - } - if (length(filepath_out) > 1) { - cli::cli_abort("multiple matching {ext} files found") - } + msg1 <- ct_error_msg("no matching {ext} file found") + msg2 <- cst_error_msg("multiple matching {ext} files found") + check_that(filepath_out, \(x) length(x) > 0, msg1) # check length > 0 + check_scalar_type(filepath_out, "character", msg2) # check length == 1 info_option <- getOption("azkit.info") stopifnot(rlang::is_scalar_logical(info) || is.null(info)) diff --git a/man/check_blob_exists.Rd b/man/check_blob_exists.Rd index d513fe9..472231a 100644 --- a/man/check_blob_exists.Rd +++ b/man/check_blob_exists.Rd @@ -19,7 +19,7 @@ will error if multiple files are somehow matched.} being read. Useful for checking the function is doing what is expected, but can be turned off with \code{FALSE}. Can be set persistently with the option "azkit.info". If \code{NULL} then it will default to the value of -\link[rlang:is_interactive]{rlang::is_interactive} (ie \code{TRUE} for interactive sessions).} +\link[rlang:is_interactive]{rlang::is_interactive} (that is, \code{TRUE} for interactive sessions).} \item{path}{The path to the directory where \code{file} is located, as a string. Only needed if \code{file} does not already contain its full path. If file is diff --git a/man/check_nzchar.Rd b/man/check_nzchar.Rd new file mode 100644 index 0000000..72ae9b6 --- /dev/null +++ b/man/check_nzchar.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/azkit_helpers.R +\name{check_nzchar} +\alias{check_nzchar} +\title{Check if a supplied non-NULL value is a string with >0 characters} +\usage{ +check_nzchar(x, message, pf = parent.frame()) +} +\arguments{ +\item{x}{The object to be checked} + +\item{message}{A custom error message, as a string. Will be shown to the +user if the check does not pass. Can include \code{glue} variables and \code{{cli}} +semantic markup. Variable values will be searched for in the environment of +the caller function (not in the environment of \code{check_nzchar()}). This +makes it easier to include informative values in the message.} + +\item{pf}{Set as \code{\link[=parent.frame]{parent.frame()}} so variables in the caller environment can +be used in the custom error message.} +} +\description{ +Will error if x is equal to \code{""}, or if it is otherwise missing or invalid. +With the exception that if x is NULL, then NULL will be passed through. +} +\keyword{internal} diff --git a/man/check_scalar_type.Rd b/man/check_scalar_type.Rd index c3f6083..ef9f097 100644 --- a/man/check_scalar_type.Rd +++ b/man/check_scalar_type.Rd @@ -12,10 +12,8 @@ check_scalar_type(x, type, message, pf = parent.frame()) \item{type}{A string defining the R object type that \code{x} is checked to be} \item{message}{A custom error message, as a string. Will be shown to the -user if the predicate check does not succeed. Can include \code{glue} variables -and \code{{cli}} semantic markup. Variable values will be searched for in the -environment of the caller function (not in the environment of \code{check_vec()} -). This makes it easier to include informative values in the message.} +user if the predicate check does not succeed. Can include \code{glue}d variables +and \code{{cli}} semantic markup.} \item{pf}{Set as \link{parent.frame} so variables in the caller environment can be used in the custom error message.} @@ -28,6 +26,6 @@ Possible values for the \code{type} parameter are: "character", "logical", "list "integer", "double", "string", "bool", "bytes", "raw", "vector", "complex". } \seealso{ -\code{\link[=check_vec]{check_vec()}} +\link{check_that} } \keyword{internal} diff --git a/man/check_that.Rd b/man/check_that.Rd new file mode 100644 index 0000000..9140faf --- /dev/null +++ b/man/check_that.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/azkit_helpers.R +\name{check_that} +\alias{check_that} +\title{An alternative to stopifnot/assert_that etc} +\usage{ +check_that(x, predicate, message, pf = parent.frame()) +} +\arguments{ +\item{x}{The object to be checked} + +\item{predicate}{The predicate function used to check \code{x}} + +\item{message}{A custom error message, as a string. Will be shown to the +user if the predicate check does not succeed. Can include \code{glue}d variables +and \code{{cli}} semantic markup.} + +\item{pf}{Set as \code{\link[=parent.frame]{parent.frame()}} so variables in the caller environment can +be used in the custom error message.} +} +\description{ +If the predicate function is true of \code{x} then \code{x} is returned. Otherwise, +an error is thrown with a custom \code{message}. +} +\seealso{ +\link{check_vec} +} diff --git a/man/check_vec.Rd b/man/check_vec.Rd index d72ab8e..ea4698b 100644 --- a/man/check_vec.Rd +++ b/man/check_vec.Rd @@ -18,7 +18,7 @@ check_vec( \item{predicate}{The predicate function used to check elements of \code{x}} \item{message}{A custom error message, as a string. Will be shown to the -user if the predicate check does not succeed. Can include \code{glue} variables +user if the predicate check does not succeed. Can include \code{glue}d variables and \code{{cli}} semantic markup. Variable values will be searched for in the environment of the caller function (not in the environment of \code{check_vec()} ). This makes it easier to include informative values in the message.} diff --git a/man/get_container.Rd b/man/get_container.Rd index 3d378ae..7fe762a 100644 --- a/man/get_container.Rd +++ b/man/get_container.Rd @@ -11,11 +11,13 @@ get_container(container_name = NULL, ...) which means the function will look instead for a container name stored in the environment variable "AZ_CONTAINER"} -\item{...}{arguments to be passed through to \code{\link[=get_auth_token]{get_auth_token()}}} +\item{...}{arguments to be passed through to \link{get_auth_token}} } \value{ An Azure blob container (list object of class "blob_container") } \description{ -Use \link{list_container_names} to see a list of available containers +The environment variable "AZ_STORAGE_EP" must be set. This provides the URL +for the default Azure storage endpoint. +Use \link{list_container_names} to get a list of available container names. } diff --git a/tests/testthat/test-read_azure_files.R b/tests/testthat/test-read_azure_files.R index 06535c8..1f9e0fc 100644 --- a/tests/testthat/test-read_azure_files.R +++ b/tests/testthat/test-read_azure_files.R @@ -236,7 +236,7 @@ test_that("tdd of check_blob_exists", { dplyr::pull("name") stop_msg1 <- glue::glue("no matching {file_ext} file found") stop_msg2 <- glue::glue("multiple matching {file_ext} files found") - check_vec(filepath, rlang::is_character, stop_msg1) # check length > 0 + check_that(filepath, \(x) length(x) > 0, stop_msg1) # check length > 0 check_scalar_type(filepath, "character", stop_msg2) # check length == 1 } expect_error(check_blob_exists(support_container, "unmatched"), "matching") @@ -255,7 +255,7 @@ test_that("tdd of check_blob_exists", { dplyr::pull("name") stop_msg1 <- glue::glue("no matching {file_ext} file found") stop_msg2 <- glue::glue("multiple matching {file_ext} files found") - check_vec(filepath, rlang::is_character, stop_msg1) # check length > 0 + check_that(filepath, \(x) length(x) > 0, stop_msg1) # check length > 0 check_scalar_type(filepath, "character", stop_msg2) # check length == 1 filepath }