diff --git a/DESCRIPTION b/DESCRIPTION index 0b35faf6..1da341f8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -18,14 +18,14 @@ Imports: httr (>= 0.5), lifecycle (>= 1.0.3), magrittr, - rlang (>= 1.0.0), + rlang (>= 1.1.0), selectr, tibble, - withr, xml2 (>= 1.3) Suggests: covr, knitr, + R6, readr, repurrrsive, rmarkdown, @@ -37,7 +37,8 @@ VignetteBuilder: knitr Config/Needs/website: tidyverse/tidytemplate Config/testthat/edition: 3 +Config/testthat/parallel: true Encoding: UTF-8 Language: en-US Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.0 diff --git a/R/encoding.R b/R/encoding.R index 19adf75f..8e265a0e 100644 --- a/R/encoding.R +++ b/R/encoding.R @@ -57,7 +57,7 @@ repair_encoding <- function(x, from = NULL) { from <- best_guess$encoding conf <- best_guess$confidence * 100 if (conf < 50) { - stop("No guess has more than 50% confidence", call. = FALSE) + cli::cli_abort("No guess has more than 50% confidence") } inform(paste0("Best guess: ", from, " (", conf, "% confident)")) diff --git a/R/form.R b/R/form.R index 1bd0b718..d39f81f3 100644 --- a/R/form.R +++ b/R/form.R @@ -47,7 +47,10 @@ html_form.xml_nodeset <- function(x, base_url = NULL) { #' @export html_form.xml_node <- function(x, base_url = NULL) { - stopifnot(xml2::xml_name(x) == "form") + if (xml2::xml_name(x) != "form") { + cli::cli_abort("{.arg x} must be a
element.") + } + check_string(base_url, allow_null = TRUE) attr <- as.list(xml2::xml_attrs(x)) name <- attr$id %||% attr$name %||% "" # for human readers @@ -102,9 +105,9 @@ html_form_set <- function(form, ...) { for (field in names(new_values)) { type <- form$fields[[field]]$type %||% "non-input" if (type == "hidden") { - warn(paste0("Setting value of hidden field '", field, "'.")) + cli::cli_warn("Setting value of hidden field {.str {field}}.") } else if (type == "submit") { - abort(paste0("Can't change value of input with type submit: '", field, "'.")) + cli::cli_abort("Can't change value of input with type submit: {.str {field}}.") } form$fields[[field]]$value <- new_values[[field]] @@ -128,22 +131,22 @@ html_form_submit <- function(form, submit = NULL) { submission_submit(subm) } -submission_build <- function(form, submit) { +submission_build <- function(form, submit, error_call = caller_env()) { method <- form$method if (!(method %in% c("POST", "GET"))) { - warn(paste0("Invalid method (", method, "), defaulting to GET")) + cli::cli_warn("Invalid method ({method}), defaulting to GET.", call = error_call) method <- "GET" } if (length(form$action) == 0) { - abort("`form` doesn't contain a `action` attribute") + cli::cli_abort("`form` doesn't contain a `action` attribute.", call = error_call) } list( method = method, enctype = form$enctype, action = form$action, - values = submission_build_values(form, submit) + values = submission_build_values(form, submit, error_call = error_call) ) } @@ -155,9 +158,9 @@ submission_submit <- function(x, ...) { } } -submission_build_values <- function(form, submit = NULL) { +submission_build_values <- function(form, submit = NULL, error_call = caller_env()) { fields <- form$fields - submit <- submission_find_submit(fields, submit) + submit <- submission_find_submit(fields, submit, error_call = error_call) entry_list <- c(Filter(Negate(is_button), fields), list(submit)) entry_list <- Filter(function(x) !is.null(x$name), entry_list) @@ -172,7 +175,7 @@ submission_build_values <- function(form, submit = NULL) { as.list(out) } -submission_find_submit <- function(fields, idx) { +submission_find_submit <- function(fields, idx, error_call = caller_env()) { buttons <- Filter(is_button, fields) if (is.null(idx)) { @@ -180,25 +183,31 @@ submission_find_submit <- function(fields, idx) { list() } else { if (length(buttons) > 1) { - inform(paste0("Submitting with '", buttons[[1]]$name, "'")) + cli::cli_inform("Submitting with button {.str {buttons[[1]]$name}}.") } buttons[[1]] } } else if (is.numeric(idx) && length(idx) == 1) { if (idx < 1 || idx > length(buttons)) { - abort("Numeric `submit` out of range") + cli::cli_abort("Numeric {.arg submit} out of range.", call = error_call) } buttons[[idx]] } else if (is.character(idx) && length(idx) == 1) { if (!idx %in% names(buttons)) { - abort(c( - paste0("No found with name '", idx, "'."), - i = paste0("Possible values: ", paste0(names(buttons), collapse = ", ")) - )) + cli::cli_abort( + c( + "No found with name {.str {idx}}.", + i = "Possible values: {.str {names(buttons)}}." + ), + call = error_call + ) } buttons[[idx]] } else { - abort("`submit` must be NULL, a string, or a number.") + cli::cli_abort( + "{.arg submit} must be NULL, a string, or a number.", + call = error_call + ) } } @@ -326,10 +335,12 @@ format_list <- function(x, indent = 0) { paste0(spaces, formatted, collapse = "\n") } -check_fields <- function(form, values) { +check_fields <- function(form, values, error_call = caller_env()) { no_match <- setdiff(names(values), names(form$fields)) if (length(no_match) > 0) { - str <- paste("'", no_match, "'", collapse = ", ") - abort(paste0("Can't set value of fields that don't exist: ", str)) + cli::cli_abort( + "Can't set value of fields that don't exist: {.str {no_match}}.", + call = error_call + ) } } diff --git a/R/html.R b/R/html.R index c57ebcb0..8f2aef77 100644 --- a/R/html.R +++ b/R/html.R @@ -43,6 +43,9 @@ html_name <- function(x) { #' @export #' @importFrom xml2 xml_attr html_attr <- function(x, name, default = NA_character_) { + check_string(name) + check_string(default, allow_na = TRUE) + xml_attr(x, name, default = default) } diff --git a/R/import-standalone-obj-type.R b/R/import-standalone-obj-type.R new file mode 100644 index 00000000..8e3c07df --- /dev/null +++ b/R/import-standalone-obj-type.R @@ -0,0 +1,360 @@ +# Standalone file: do not edit by hand +# Source: +# ---------------------------------------------------------------------- +# +# --- +# repo: r-lib/rlang +# file: standalone-obj-type.R +# last-updated: 2023-05-01 +# license: https://unlicense.org +# imports: rlang (>= 1.1.0) +# --- +# +# ## Changelog +# +# 2023-05-01: +# - `obj_type_friendly()` now only displays the first class of S3 objects. +# +# 2023-03-30: +# - `stop_input_type()` now handles `I()` input literally in `arg`. +# +# 2022-10-04: +# - `obj_type_friendly(value = TRUE)` now shows numeric scalars +# literally. +# - `stop_friendly_type()` now takes `show_value`, passed to +# `obj_type_friendly()` as the `value` argument. +# +# 2022-10-03: +# - Added `allow_na` and `allow_null` arguments. +# - `NULL` is now backticked. +# - Better friendly type for infinities and `NaN`. +# +# 2022-09-16: +# - Unprefixed usage of rlang functions with `rlang::` to +# avoid onLoad issues when called from rlang (#1482). +# +# 2022-08-11: +# - Prefixed usage of rlang functions with `rlang::`. +# +# 2022-06-22: +# - `friendly_type_of()` is now `obj_type_friendly()`. +# - Added `obj_type_oo()`. +# +# 2021-12-20: +# - Added support for scalar values and empty vectors. +# - Added `stop_input_type()` +# +# 2021-06-30: +# - Added support for missing arguments. +# +# 2021-04-19: +# - Added support for matrices and arrays (#141). +# - Added documentation. +# - Added changelog. +# +# nocov start + +#' Return English-friendly type +#' @param x Any R object. +#' @param value Whether to describe the value of `x`. Special values +#' like `NA` or `""` are always described. +#' @param length Whether to mention the length of vectors and lists. +#' @return A string describing the type. Starts with an indefinite +#' article, e.g. "an integer vector". +#' @noRd +obj_type_friendly <- function(x, value = TRUE) { + if (is_missing(x)) { + return("absent") + } + + if (is.object(x)) { + if (inherits(x, "quosure")) { + type <- "quosure" + } else { + type <- class(x)[[1L]] + } + return(sprintf("a <%s> object", type)) + } + + if (!is_vector(x)) { + return(.rlang_as_friendly_type(typeof(x))) + } + + n_dim <- length(dim(x)) + + if (!n_dim) { + if (!is_list(x) && length(x) == 1) { + if (is_na(x)) { + return(switch( + typeof(x), + logical = "`NA`", + integer = "an integer `NA`", + double = + if (is.nan(x)) { + "`NaN`" + } else { + "a numeric `NA`" + }, + complex = "a complex `NA`", + character = "a character `NA`", + .rlang_stop_unexpected_typeof(x) + )) + } + + show_infinites <- function(x) { + if (x > 0) { + "`Inf`" + } else { + "`-Inf`" + } + } + str_encode <- function(x, width = 30, ...) { + if (nchar(x) > width) { + x <- substr(x, 1, width - 3) + x <- paste0(x, "...") + } + encodeString(x, ...) + } + + if (value) { + if (is.numeric(x) && is.infinite(x)) { + return(show_infinites(x)) + } + + if (is.numeric(x) || is.complex(x)) { + number <- as.character(round(x, 2)) + what <- if (is.complex(x)) "the complex number" else "the number" + return(paste(what, number)) + } + + return(switch( + typeof(x), + logical = if (x) "`TRUE`" else "`FALSE`", + character = { + what <- if (nzchar(x)) "the string" else "the empty string" + paste(what, str_encode(x, quote = "\"")) + }, + raw = paste("the raw value", as.character(x)), + .rlang_stop_unexpected_typeof(x) + )) + } + + return(switch( + typeof(x), + logical = "a logical value", + integer = "an integer", + double = if (is.infinite(x)) show_infinites(x) else "a number", + complex = "a complex number", + character = if (nzchar(x)) "a string" else "\"\"", + raw = "a raw value", + .rlang_stop_unexpected_typeof(x) + )) + } + + if (length(x) == 0) { + return(switch( + typeof(x), + logical = "an empty logical vector", + integer = "an empty integer vector", + double = "an empty numeric vector", + complex = "an empty complex vector", + character = "an empty character vector", + raw = "an empty raw vector", + list = "an empty list", + .rlang_stop_unexpected_typeof(x) + )) + } + } + + vec_type_friendly(x) +} + +vec_type_friendly <- function(x, length = FALSE) { + if (!is_vector(x)) { + abort("`x` must be a vector.") + } + type <- typeof(x) + n_dim <- length(dim(x)) + + add_length <- function(type) { + if (length && !n_dim) { + paste0(type, sprintf(" of length %s", length(x))) + } else { + type + } + } + + if (type == "list") { + if (n_dim < 2) { + return(add_length("a list")) + } else if (is.data.frame(x)) { + return("a data frame") + } else if (n_dim == 2) { + return("a list matrix") + } else { + return("a list array") + } + } + + type <- switch( + type, + logical = "a logical %s", + integer = "an integer %s", + numeric = , + double = "a double %s", + complex = "a complex %s", + character = "a character %s", + raw = "a raw %s", + type = paste0("a ", type, " %s") + ) + + if (n_dim < 2) { + kind <- "vector" + } else if (n_dim == 2) { + kind <- "matrix" + } else { + kind <- "array" + } + out <- sprintf(type, kind) + + if (n_dim >= 2) { + out + } else { + add_length(out) + } +} + +.rlang_as_friendly_type <- function(type) { + switch( + type, + + list = "a list", + + NULL = "`NULL`", + environment = "an environment", + externalptr = "a pointer", + weakref = "a weak reference", + S4 = "an S4 object", + + name = , + symbol = "a symbol", + language = "a call", + pairlist = "a pairlist node", + expression = "an expression vector", + + char = "an internal string", + promise = "an internal promise", + ... = "an internal dots object", + any = "an internal `any` object", + bytecode = "an internal bytecode object", + + primitive = , + builtin = , + special = "a primitive function", + closure = "a function", + + type + ) +} + +.rlang_stop_unexpected_typeof <- function(x, call = caller_env()) { + abort( + sprintf("Unexpected type <%s>.", typeof(x)), + call = call + ) +} + +#' Return OO type +#' @param x Any R object. +#' @return One of `"bare"` (for non-OO objects), `"S3"`, `"S4"`, +#' `"R6"`, or `"R7"`. +#' @noRd +obj_type_oo <- function(x) { + if (!is.object(x)) { + return("bare") + } + + class <- inherits(x, c("R6", "R7_object"), which = TRUE) + + if (class[[1]]) { + "R6" + } else if (class[[2]]) { + "R7" + } else if (isS4(x)) { + "S4" + } else { + "S3" + } +} + +#' @param x The object type which does not conform to `what`. Its +#' `obj_type_friendly()` is taken and mentioned in the error message. +#' @param what The friendly expected type as a string. Can be a +#' character vector of expected types, in which case the error +#' message mentions all of them in an "or" enumeration. +#' @param show_value Passed to `value` argument of `obj_type_friendly()`. +#' @param ... Arguments passed to [abort()]. +#' @inheritParams args_error_context +#' @noRd +stop_input_type <- function(x, + what, + ..., + allow_na = FALSE, + allow_null = FALSE, + show_value = TRUE, + arg = caller_arg(x), + call = caller_env()) { + # From standalone-cli.R + cli <- env_get_list( + nms = c("format_arg", "format_code"), + last = topenv(), + default = function(x) sprintf("`%s`", x), + inherit = TRUE + ) + + if (allow_na) { + what <- c(what, cli$format_code("NA")) + } + if (allow_null) { + what <- c(what, cli$format_code("NULL")) + } + if (length(what)) { + what <- oxford_comma(what) + } + if (inherits(arg, "AsIs")) { + format_arg <- identity + } else { + format_arg <- cli$format_arg + } + + message <- sprintf( + "%s must be %s, not %s.", + format_arg(arg), + what, + obj_type_friendly(x, value = show_value) + ) + + abort(message, ..., call = call, arg = arg) +} + +oxford_comma <- function(chr, sep = ", ", final = "or") { + n <- length(chr) + + if (n < 2) { + return(chr) + } + + head <- chr[seq_len(n - 1)] + last <- chr[n] + + head <- paste(head, collapse = sep) + + # Write a or b. But a, b, or c. + if (n > 2) { + paste0(head, sep, final, " ", last) + } else { + paste0(head, " ", final, " ", last) + } +} + +# nocov end diff --git a/R/import-standalone-types-check.R b/R/import-standalone-types-check.R new file mode 100644 index 00000000..6782d69b --- /dev/null +++ b/R/import-standalone-types-check.R @@ -0,0 +1,538 @@ +# Standalone file: do not edit by hand +# Source: +# ---------------------------------------------------------------------- +# +# --- +# repo: r-lib/rlang +# file: standalone-types-check.R +# last-updated: 2023-03-13 +# license: https://unlicense.org +# dependencies: standalone-obj-type.R +# imports: rlang (>= 1.1.0) +# --- +# +# ## Changelog +# +# 2023-03-13: +# - Improved error messages of number checkers (@teunbrand) +# - Added `allow_infinite` argument to `check_number_whole()` (@mgirlich). +# - Added `check_data_frame()` (@mgirlich). +# +# 2023-03-07: +# - Added dependency on rlang (>= 1.1.0). +# +# 2023-02-15: +# - Added `check_logical()`. +# +# - `check_bool()`, `check_number_whole()`, and +# `check_number_decimal()` are now implemented in C. +# +# - For efficiency, `check_number_whole()` and +# `check_number_decimal()` now take a `NULL` default for `min` and +# `max`. This makes it possible to bypass unnecessary type-checking +# and comparisons in the default case of no bounds checks. +# +# 2022-10-07: +# - `check_number_whole()` and `_decimal()` no longer treat +# non-numeric types such as factors or dates as numbers. Numeric +# types are detected with `is.numeric()`. +# +# 2022-10-04: +# - Added `check_name()` that forbids the empty string. +# `check_string()` allows the empty string by default. +# +# 2022-09-28: +# - Removed `what` arguments. +# - Added `allow_na` and `allow_null` arguments. +# - Added `allow_decimal` and `allow_infinite` arguments. +# - Improved errors with absent arguments. +# +# +# 2022-09-16: +# - Unprefixed usage of rlang functions with `rlang::` to +# avoid onLoad issues when called from rlang (#1482). +# +# 2022-08-11: +# - Added changelog. +# +# nocov start + +# Scalars ----------------------------------------------------------------- + +.standalone_types_check_dot_call <- .Call + +check_bool <- function(x, + ..., + allow_na = FALSE, + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x) && .standalone_types_check_dot_call(ffi_standalone_is_bool_1.0.7, x, allow_na, allow_null)) { + return(invisible(NULL)) + } + + stop_input_type( + x, + c("`TRUE`", "`FALSE`"), + ..., + allow_na = allow_na, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_string <- function(x, + ..., + allow_empty = TRUE, + allow_na = FALSE, + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + is_string <- .rlang_check_is_string( + x, + allow_empty = allow_empty, + allow_na = allow_na, + allow_null = allow_null + ) + if (is_string) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "a single string", + ..., + allow_na = allow_na, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +.rlang_check_is_string <- function(x, + allow_empty, + allow_na, + allow_null) { + if (is_string(x)) { + if (allow_empty || !is_string(x, "")) { + return(TRUE) + } + } + + if (allow_null && is_null(x)) { + return(TRUE) + } + + if (allow_na && (identical(x, NA) || identical(x, na_chr))) { + return(TRUE) + } + + FALSE +} + +check_name <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + is_string <- .rlang_check_is_string( + x, + allow_empty = FALSE, + allow_na = FALSE, + allow_null = allow_null + ) + if (is_string) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "a valid name", + ..., + allow_na = FALSE, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +IS_NUMBER_true <- 0 +IS_NUMBER_false <- 1 +IS_NUMBER_oob <- 2 + +check_number_decimal <- function(x, + ..., + min = NULL, + max = NULL, + allow_infinite = TRUE, + allow_na = FALSE, + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (missing(x)) { + exit_code <- IS_NUMBER_false + } else if (0 == (exit_code <- .standalone_types_check_dot_call( + ffi_standalone_check_number_1.0.7, + x, + allow_decimal = TRUE, + min, + max, + allow_infinite, + allow_na, + allow_null + ))) { + return(invisible(NULL)) + } + + .stop_not_number( + x, + ..., + exit_code = exit_code, + allow_decimal = TRUE, + min = min, + max = max, + allow_na = allow_na, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_number_whole <- function(x, + ..., + min = NULL, + max = NULL, + allow_infinite = FALSE, + allow_na = FALSE, + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (missing(x)) { + exit_code <- IS_NUMBER_false + } else if (0 == (exit_code <- .standalone_types_check_dot_call( + ffi_standalone_check_number_1.0.7, + x, + allow_decimal = FALSE, + min, + max, + allow_infinite, + allow_na, + allow_null + ))) { + return(invisible(NULL)) + } + + .stop_not_number( + x, + ..., + exit_code = exit_code, + allow_decimal = FALSE, + min = min, + max = max, + allow_na = allow_na, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +.stop_not_number <- function(x, + ..., + exit_code, + allow_decimal, + min, + max, + allow_na, + allow_null, + arg, + call) { + if (allow_decimal) { + what <- "a number" + } else { + what <- "a whole number" + } + + if (exit_code == IS_NUMBER_oob) { + min <- min %||% -Inf + max <- max %||% Inf + + if (min > -Inf && max < Inf) { + what <- sprintf("%s between %s and %s", what, min, max) + } else if (x < min) { + what <- sprintf("%s larger than or equal to %s", what, min) + } else if (x > max) { + what <- sprintf("%s smaller than or equal to %s", what, max) + } else { + abort("Unexpected state in OOB check", .internal = TRUE) + } + } + + stop_input_type( + x, + what, + ..., + allow_na = allow_na, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_symbol <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + if (is_symbol(x)) { + return(invisible(NULL)) + } + if (allow_null && is_null(x)) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "a symbol", + ..., + allow_na = FALSE, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_arg <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + if (is_symbol(x)) { + return(invisible(NULL)) + } + if (allow_null && is_null(x)) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "an argument name", + ..., + allow_na = FALSE, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_call <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + if (is_call(x)) { + return(invisible(NULL)) + } + if (allow_null && is_null(x)) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "a defused call", + ..., + allow_na = FALSE, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_environment <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + if (is_environment(x)) { + return(invisible(NULL)) + } + if (allow_null && is_null(x)) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "an environment", + ..., + allow_na = FALSE, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_function <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + if (is_function(x)) { + return(invisible(NULL)) + } + if (allow_null && is_null(x)) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "a function", + ..., + allow_na = FALSE, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_closure <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + if (is_closure(x)) { + return(invisible(NULL)) + } + if (allow_null && is_null(x)) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "an R function", + ..., + allow_na = FALSE, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_formula <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + if (is_formula(x)) { + return(invisible(NULL)) + } + if (allow_null && is_null(x)) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "a formula", + ..., + allow_na = FALSE, + allow_null = allow_null, + arg = arg, + call = call + ) +} + + +# Vectors ----------------------------------------------------------------- + +check_character <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + if (is_character(x)) { + return(invisible(NULL)) + } + if (allow_null && is_null(x)) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "a character vector", + ..., + allow_na = FALSE, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_logical <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + if (is_logical(x)) { + return(invisible(NULL)) + } + if (allow_null && is_null(x)) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "a logical vector", + ..., + allow_na = FALSE, + allow_null = allow_null, + arg = arg, + call = call + ) +} + +check_data_frame <- function(x, + ..., + allow_null = FALSE, + arg = caller_arg(x), + call = caller_env()) { + if (!missing(x)) { + if (is.data.frame(x)) { + return(invisible(NULL)) + } + if (allow_null && is_null(x)) { + return(invisible(NULL)) + } + } + + stop_input_type( + x, + "a data frame", + ..., + allow_null = allow_null, + arg = arg, + call = call + ) +} + +# nocov end diff --git a/R/selectors.R b/R/selectors.R index 6a92413d..b48e1a1a 100644 --- a/R/selectors.R +++ b/R/selectors.R @@ -86,21 +86,14 @@ html_element.default <- function(x, css, xpath) { xml2::xml_find_first(x, make_selector(css, xpath)) } -make_selector <- function(css, xpath) { - if (missing(css) && missing(xpath)) - stop("Please supply one of css or xpath", call. = FALSE) - if (!missing(css) && !missing(xpath)) - stop("Please supply css or xpath, not both", call. = FALSE) +make_selector <- function(css, xpath, error_call = caller_env()) { + check_exclusive(css, xpath, .call = error_call) if (!missing(css)) { - if (!is.character(css) && length(css) == 1) - stop("`css` must be a string") - + check_string(css, call = error_call) selectr::css_to_xpath(css, prefix = ".//") } else { - if (!is.character(xpath) && length(xpath) == 1) - stop("`xpath` must be a string") - + check_string(xpath, call = error_call) xpath } } diff --git a/R/session.R b/R/session.R index e1e9c543..0572c1f3 100644 --- a/R/session.R +++ b/R/session.R @@ -38,6 +38,8 @@ #' html_elements("p") #' } session <- function(url, ...) { + check_string(url) + session <- structure( list( handle = httr::handle(url), @@ -86,6 +88,8 @@ session_set_response <- function(x, response) { #' @rdname session session_jump_to <- function(x, url, ...) { check_session(x) + check_string(url) + url <- xml2::url_absolute(url, x$url) last_url <- x$url @@ -104,36 +108,33 @@ session_follow_link <- function(x, i, css, xpath, ...) { check_session(x) url <- find_href(x, i = i, css = css, xpath = xpath) - inform(paste0("Navigating to ", url)) + cli::cli_inform("Navigating to {.url {url}}.") session_jump_to(x, url, ...) } -find_href <- function(x, i, css, xpath) { - if (sum(!missing(i), !missing(css), !missing(xpath)) != 1) { - abort("Must supply exactly one of `i`, `css`, or `xpath`") - } +find_href <- function(x, i, css, xpath, error_call = caller_env()) { + check_exclusive(i, css, xpath, .call = error_call) if (!missing(i)) { - stopifnot(length(i) == 1) a <- html_elements(x, "a") - if (is.numeric(i)) { + if (is.numeric(i) && length(i) == 1) { out <- a[[i]] - } else if (is.character(i)) { + } else if (is.character(i) && length(i) == 1) { text <- html_text(a) match <- grepl(i, text, fixed = TRUE) if (!any(match)) { - stop("No links have text '", i, "'", call. = FALSE) + cli::cli_abort("No links have text {.str {i}}.", call = error_call) } out <- a[[which(match)[[1]]]] } else { - abort("`i` must a string or integer") + cli::cli_abort("{.arg i} must be a string or integer.", call = error_call) } } else { a <- html_elements(x, css = css, xpath = xpath) if (length(a) == 0) { - abort("No links matched `css`/`xpath`") + cli::cli_abort("No links matched `css`/`xpath`", call = error_call) } out <- a[[1]] } @@ -147,7 +148,7 @@ session_back <- function(x) { check_session(x) if (length(x$back) == 0) { - abort("Can't go back any further") + cli::cli_abort("Can't go back any further.") } url <- x$back[[1]] @@ -165,7 +166,7 @@ session_forward <- function(x) { check_session(x) if (length(x$forward) == 0) { - abort("Can't go forward any further") + cli::cli_abort("Can't go forward any further.") } url <- x$forward[[1]] @@ -208,7 +209,7 @@ session_submit <- function(x, form, submit = NULL, ...) { #' @export read_html.rvest_session <- function(x, ...) { if (!is_html(x$response)) { - abort("Page doesn't appear to be html.") + cli::cli_abort("Page doesn't appear to be html.") } env_cache(x$cache, "html", read_html(x$response, ..., base_url = x$url)) @@ -280,13 +281,16 @@ cookies.rvest_session <- function(x) { # helpers ----------------------------------------------------------------- -check_form <- function(x) { +check_form <- function(x, call = caller_env()) { if (!inherits(x, "rvest_form")) { - abort("`form` must be a single form produced by html_form()") + cli::cli_abort( + "{.arg form} must be a single form produced by {.fn html_form}.", + call = call + ) } } -check_session <- function(x) { +check_session <- function(x, call = caller_env()) { if (!inherits(x, "rvest_session")) { - abort("`x` must be produced by session()") + cli::cli_abort("{.arg x} must be produced by {.fn session}.", call = call) } } diff --git a/R/table.R b/R/table.R index bee96c2c..dca24952 100644 --- a/R/table.R +++ b/R/table.R @@ -64,6 +64,12 @@ html_table <- function(x, convert = TRUE ) { + check_bool(header, allow_na = TRUE) + check_bool(trim) + check_string(dec) + check_character(na.strings) + check_bool(convert) + UseMethod("html_table") } @@ -120,7 +126,8 @@ html_table.xml_node <- function(x, lifecycle::deprecate_warn( when = "1.0.0", what = "html_table(fill = )", - details = "An improved algorithm fills by default so it is no longer needed." + details = "An improved algorithm fills by default so it is no longer needed.", + user_env = caller_env(2) # S3 generic ) } diff --git a/R/text.R b/R/text.R index 0bdfaf7f..b64d1ec8 100644 --- a/R/text.R +++ b/R/text.R @@ -50,6 +50,7 @@ #' charToRaw(x2) #' @export html_text <- function(x, trim = FALSE) { + check_bool(trim) xml_text(x, trim = trim) } @@ -61,6 +62,8 @@ html_text <- function(x, trim = FALSE) { #' `"\ua0"`. This often causes confusion because it prints the same way as #' `" "`. html_text2 <- function(x, preserve_nbsp = FALSE) { + check_bool(preserve_nbsp) + UseMethod("html_text2") } diff --git a/inst/WORDLIST b/inst/WORDLIST index 7e5c2f12..6fc1f141 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -1,34 +1,30 @@ +CMD +Codecov +HyperText +IMDB +MDN +PBC +RoboBrowser +SelectorGadget +XPath arounds bookmarklet -Codecov -combinator -combinators colspan colspans +combinator +combinators config -configs -CMD css -http +funder httr -HyperText -IMDB innerText ith libxml magrittr -MDN nodeset -nodesets -prepending -RoboBrowser rowspan rowspans tibble +tibbles tidyverse -selectorgadget -SelectorGadget -starwars -stringi xpath -XPath diff --git a/tests/testthat/_snaps/form.md b/tests/testthat/_snaps/form.md index 0f9a7d99..61e0e938 100644 --- a/tests/testthat/_snaps/form.md +++ b/tests/testthat/_snaps/form.md @@ -27,13 +27,26 @@ Output [1] "form" +# validates its inputs + + Code + html_form(html_element(select, "button")) + Condition + Error in `html_form()`: + ! `x` must be a element. + Code + html_form(select, base_url = 1) + Condition + Error in `FUN()`: + ! `base_url` must be a single string or `NULL`, not the number 1. + # can set values of inputs Code form <- html_form_set(form, hidden = "abc") Condition Warning: - Setting value of hidden field 'hidden'. + Setting value of hidden field "hidden". # has informative errors @@ -41,23 +54,23 @@ html_form_set(form, text = "x") Condition Error in `html_form_set()`: - ! Can't change value of input with type submit: 'text'. + ! Can't change value of input with type submit: "text". --- Code html_form_set(form, missing = "x") Condition - Error in `check_fields()`: - ! Can't set value of fields that don't exist: ' missing ' + Error in `html_form_set()`: + ! Can't set value of fields that don't exist: "missing". # useful feedback on invalid forms Code submission_build(form, NULL) Condition - Error in `submission_build()`: - ! `form` doesn't contain a `action` attribute + Error: + ! `form` doesn't contain a `action` attribute. --- @@ -65,38 +78,38 @@ x <- submission_build(form, NULL) Condition Warning: - Invalid method (FOO), defaulting to GET + Invalid method (FOO), defaulting to GET. # handles multiple buttons Code vals <- submission_build_values(form, NULL) Message - Submitting with 'one' + Submitting with button "one". --- Code submission_build_values(form, 3L) Condition - Error in `submission_find_submit()`: - ! Numeric `submit` out of range + Error: + ! Numeric `submit` out of range. --- Code submission_build_values(form, "three") Condition - Error in `submission_find_submit()`: - ! No found with name 'three'. - i Possible values: one, two + Error: + ! No found with name "three". + i Possible values: "one" and "two". --- Code submission_build_values(form, TRUE) Condition - Error in `submission_find_submit()`: + Error: ! `submit` must be NULL, a string, or a number. # can submit using three primary techniques diff --git a/tests/testthat/_snaps/html.md b/tests/testthat/_snaps/html.md new file mode 100644 index 00000000..3ad82162 --- /dev/null +++ b/tests/testthat/_snaps/html.md @@ -0,0 +1,13 @@ +# validates inputs + + Code + html_attr(html, 1) + Condition + Error in `html_attr()`: + ! `name` must be a single string, not the number 1. + Code + html_attr(html, "id", 1) + Condition + Error in `html_attr()`: + ! `default` must be a single string or `NA`, not the number 1. + diff --git a/tests/testthat/_snaps/rename.md b/tests/testthat/_snaps/rename.md index c21d6d57..62040e21 100644 --- a/tests/testthat/_snaps/rename.md +++ b/tests/testthat/_snaps/rename.md @@ -52,7 +52,7 @@ `follow_link()` was deprecated in rvest 1.0.0. i Please use `session_follow_link()` instead. Message - Navigating to #container + Navigating to <#container>. Code s <- jump_to(s, "https://rvest.tidyverse.org/reference/index.html") Condition diff --git a/tests/testthat/_snaps/selectors.md b/tests/testthat/_snaps/selectors.md index b0872785..ef0ff0ff 100644 --- a/tests/testthat/_snaps/selectors.md +++ b/tests/testthat/_snaps/selectors.md @@ -4,7 +4,7 @@ make_selector() Condition Error: - ! Please supply one of css or xpath + ! One of `css` or `xpath` must be supplied. --- @@ -12,21 +12,21 @@ make_selector("a", "b") Condition Error: - ! Please supply css or xpath, not both + ! Exactly one of `css` or `xpath` must be supplied. --- Code make_selector(css = 1) Condition - Error in `make_selector()`: - ! `css` must be a string + Error: + ! `css` must be a single string, not the number 1. --- Code make_selector(xpath = 1) Condition - Error in `make_selector()`: - ! `xpath` must be a string + Error: + ! `xpath` must be a single string, not the number 1. diff --git a/tests/testthat/_snaps/session.md b/tests/testthat/_snaps/session.md index 4e53c3f1..ae4f055b 100644 --- a/tests/testthat/_snaps/session.md +++ b/tests/testthat/_snaps/session.md @@ -12,7 +12,7 @@ expect_true(is.session(s)) s <- session_follow_link(s, css = "p a") Message - Navigating to http://rstudio.com + Navigating to . Code session_history(s) Output @@ -30,35 +30,36 @@ # informative errors for bad inputs - `form` must be a single form produced by html_form() + `form` must be a single form produced by `html_form()`. --- - `x` must be produced by session() + `x` must be produced by `session()`. # can navigate back and forward - Can't go back any further + Can't go back any further. --- - Can't go forward any further + Can't go forward any further. # can find link by position, content, css, or xpath Code find_href(html, i = 1, css = "a") Condition - Error in `find_href()`: - ! Must supply exactly one of `i`, `css`, or `xpath` + Error: + ! Exactly one of `i`, `css`, or `xpath` must be supplied. + x `i` and `css` were supplied together. --- Code find_href(html, i = TRUE) Condition - Error in `find_href()`: - ! `i` must a string or integer + Error: + ! `i` must be a string or integer. --- @@ -66,13 +67,13 @@ find_href(html, i = "c") Condition Error: - ! No links have text 'c' + ! No links have text "c". --- Code find_href(html, css = "p a") Condition - Error in `find_href()`: + Error: ! No links matched `css`/`xpath` diff --git a/tests/testthat/_snaps/table.md b/tests/testthat/_snaps/table.md index 5d7cef58..f4f867de 100644 --- a/tests/testthat/_snaps/table.md +++ b/tests/testthat/_snaps/table.md @@ -81,8 +81,6 @@ Warning: The `fill` argument of `html_table()` is deprecated as of rvest 1.0.0. i An improved algorithm fills by default so it is no longer needed. - i The deprecated feature was likely used in the base package. - Please report the issue to the authors. Code . <- html_table(html, fill = TRUE) diff --git a/tests/testthat/test-form.R b/tests/testthat/test-form.R index 338011e8..0bf4099b 100644 --- a/tests/testthat/test-form.R +++ b/tests/testthat/test-form.R @@ -76,6 +76,19 @@ test_that("handles different encoding types", { expect_snapshot(convert_enctype("unknown")) }) +test_that("validates its inputs", { + select <- minimal_html("button test", ' + + + + ') + expect_snapshot(error = TRUE, { + html_form(html_element(select, "button")) + html_form(select, base_url = 1) + }) + +}) + # set -------------------------------------------------------------- test_that("can set values of inputs", { diff --git a/tests/testthat/test-html.R b/tests/testthat/test-html.R index 49cace49..46c6a456 100644 --- a/tests/testthat/test-html.R +++ b/tests/testthat/test-html.R @@ -9,3 +9,12 @@ test_that("forwards to xml2 functions", { expect_equal(html_children(p), html_elements(html, "i")) }) + +test_that("validates inputs", { + html <- minimal_html("

Hello children

") + + expect_snapshot(error = TRUE, { + html_attr(html, 1) + html_attr(html, "id", 1) + }) +})