diff --git a/DESCRIPTION b/DESCRIPTION index ce7ac01b..a0df548c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: monty Title: Monte Carlo Models -Version: 0.2.3 +Version: 0.2.4 Authors@R: c(person("Rich", "FitzJohn", role = c("aut", "cre"), email = "rich.fitzjohn@gmail.com"), person("Wes", "Hinsley", role = "aut"), diff --git a/R/dsl-parse-error.R b/R/dsl-parse-error.R index 44c196f9..b9019376 100644 --- a/R/dsl-parse-error.R +++ b/R/dsl-parse-error.R @@ -11,32 +11,15 @@ ##' @param code The error code, as a string, in the form `Exxx` (a ##' capital "E" followed by three numbers) ##' +##' @param how How to explain the error. Options are `pretty` (render +##' pretty text in the console), `plain` (display plain text in the +##' console) and `link` (browse to the online help). +##' ##' @return Nothing, this is called for its side effect only ##' ##' @export -monty_dsl_error_explain <- function(code) { - ## See odin2 for the canonical implementation of this, we're just - ## shadowing it here really as our dsl is much simpler. Note that - ## error codes from monty are three numbers long, whereas they are - ## four long in odin so that it's easy (for us) to tell who the - ## error belongs to. - assert_scalar_character(code) - if (!grepl("^E[0-9]{3}$", code)) { - cli::cli_abort("Invalid code '{code}', should match 'Exxx'", - arg = "code") - } - txt <- dsl_errors[[code]] - if (is.null(txt)) { - cli::cli_abort( - c("Error '{code}' is undocumented", - i = paste("If you were directed here from an error message, please", - "let us know (e.g., file an issue or send us a message)")), - arg = "code") - } - url <- sprintf( - "https://mrc-ide.github.io/monty/articles/dsl-errors.html#%s", - tolower(code)) - utils::browseURL(url) +monty_dsl_error_explain <- function(code, how = "pretty") { + error_explain(dsl_errors, code, how) } @@ -70,7 +53,6 @@ cnd_footer.monty_parse_error <- function(cnd, ...) { ## to some number of spaces, probably. detail <- gsub(" ", "\u00a0", detail) - code <- cnd$code ## See https://cli.r-lib.org/reference/links.html#click-to-run-code ## RStudio will only run code in namespaced form diff --git a/R/import-standalone-error-render.R b/R/import-standalone-error-render.R new file mode 100644 index 00000000..f2033077 --- /dev/null +++ b/R/import-standalone-error-render.R @@ -0,0 +1,75 @@ +# Standalone file: do not edit by hand +# Source: https://github.com/reside-ic/reside.utils/blob/HEAD/R/standalone-error-render.R +# Generated by: usethis::use_standalone("reside-ic/reside.utils", "error-render") +# ---------------------------------------------------------------------- +# +# --- +# repo: reside/reside.utils +# file: standalone-errors-render.R +# dependencies: standalone-utils-assert.R +# imports: cli +# --- +error_explain <- function(errors, code, how, call = parent.frame()) { + assert_scalar_character(code, name = "code", call = call) + how <- match_value(how, c("pretty", "plain", "link"), + name = "how", call = call) + + if (!grepl(errors$pattern$complete, code)) { + cli::cli_abort( + "Invalid code '{code}', should match '{errors$pattern$hint}'", + arg = "code", call = call) + } + err <- errors$errors[[code]] + if (is.null(err)) { + cli::cli_abort( + c("Error '{code}' is undocumented", + i = paste("If you were directed here from an error message, please", + "let us know (e.g., file an issue or send us a message)")), + arg = "code", call = call) + } + if (how == "link") { + url <- sprintf("%s#%s",errors$url, tolower(code)) + utils::browseURL(url) + } else { + error_render(err, how == "pretty") + } +} + + +error_render <- function(err, pretty) { + render_paragraph <- function(el) { + cli::cli_par() + cli::cli_text(el$text) + } + + render_code_block <- function(el) { + cli::cli_par() + cli::cli_code(paste0(" ", el$text)) + } + + render_list <- function(el) { + cli::cli_par() + if (el$mode == "bullet") { + cli::cli_ul() + } else { + cli::cli_ol() + } + for (i in el$items) { + cli::cli_li(i$text) + } + } + + if (pretty) { + cli::cli_h1(err$code) + for (el in err$parsed) { + switch( + el$type, + paragraph = render_paragraph(el), + code_block = render_code_block(el), + list = render_list(el), + cli::cli_abort("Unknown error block type '{el$type}' while rendering")) + } + } else { + writeLines(c(sprintf("# %s", err$code), "", err$plain)) + } +} diff --git a/R/import-standalone-utils-assert.R b/R/import-standalone-utils-assert.R index 67adf3bf..441503d8 100644 --- a/R/import-standalone-utils-assert.R +++ b/R/import-standalone-utils-assert.R @@ -1,6 +1,6 @@ # Standalone file: do not edit by hand -# Source: https://github.com/reside-ic/reside.utils/blob/prototype/R/standalone-utils-assert.R -# Generated by: usethis::use_standalone("reside-ic/reside.utils", "utils-assert", ref = "prototype") +# Source: https://github.com/reside-ic/reside.utils/blob/HEAD/R/standalone-utils-assert.R +# Generated by: usethis::use_standalone("reside-ic/reside.utils", "utils-assert") # ---------------------------------------------------------------------- # # --- @@ -48,7 +48,7 @@ assert_integer <- function(x, name = deparse(substitute(x)), } if (!isTRUE(all.equal(x, rx, tolerance = tolerance))) { cli::cli_abort( - c("Exected '{name}' to be integer", + c("Expected '{name}' to be integer", i = paste("{cli::qty(length(x))}The provided", "{?value was/values were} numeric, but not very close", "to integer values")), @@ -56,7 +56,7 @@ assert_integer <- function(x, name = deparse(substitute(x)), } x <- as.integer(rx) } else { - cli::cli_abort("Exected '{name}' to be integer", call = call, arg = arg) + cli::cli_abort("Expected '{name}' to be integer", call = call, arg = arg) } invisible(x) } @@ -72,7 +72,7 @@ assert_logical <- function(x, name = deparse(substitute(x)), assert_nonmissing <- function(x, name = deparse(substitute(x)), - arg = name, call = parent.frame()) { + arg = name, call = parent.frame()) { if (anyNA(x)) { cli::cli_abort("Expected '{name}' to be non-NA", arg = arg, call = call) } @@ -81,7 +81,11 @@ assert_nonmissing <- function(x, name = deparse(substitute(x)), assert_scalar_character <- function(x, name = deparse(substitute(x)), - arg = name, call = parent.frame()) { + allow_null = FALSE, + arg = name, call = parent.frame()) { + if (allow_null && is.null(x)) { + return(invisible(x)) + } assert_scalar(x, name, arg = arg, call = call) assert_character(x, name, arg = arg, call = call) assert_nonmissing(x, name, arg = arg, call = call) @@ -89,7 +93,11 @@ assert_scalar_character <- function(x, name = deparse(substitute(x)), assert_scalar_numeric <- function(x, name = deparse(substitute(x)), + allow_null = FALSE, arg = name, call = parent.frame()) { + if (allow_null && is.null(x)) { + return(invisible(x)) + } assert_scalar(x, name, arg = arg, call = call) assert_numeric(x, name, arg = arg, call = call) assert_nonmissing(x, name, arg = arg, call = call) @@ -97,7 +105,11 @@ assert_scalar_numeric <- function(x, name = deparse(substitute(x)), assert_scalar_integer <- function(x, name = deparse(substitute(x)), - tolerance = NULL, arg = name, call = parent.frame()) { + tolerance = NULL, allow_null = FALSE, + arg = name, call = parent.frame()) { + if (allow_null && is.null(x)) { + return(invisible(x)) + } assert_scalar(x, name, arg = arg, call = call) assert_integer(x, name, tolerance = tolerance, arg = arg, call = call) assert_nonmissing(x, name, arg = arg, call = call) @@ -105,16 +117,23 @@ assert_scalar_integer <- function(x, name = deparse(substitute(x)), assert_scalar_logical <- function(x, name = deparse(substitute(x)), + allow_null = FALSE, arg = name, call = parent.frame()) { + if (allow_null && is.null(x)) { + return(invisible(x)) + } assert_scalar(x, name, arg = arg, call = call) assert_logical(x, name, arg = arg, call = call) assert_nonmissing(x, name, arg = arg, call = call) } -assert_scalar_size <- function(x, allow_zero = TRUE, +assert_scalar_size <- function(x, allow_zero = TRUE, allow_null = FALSE, name = deparse(substitute(x)), arg = name, call = parent.frame()) { + if (allow_null && is.null(x)) { + return(invisible(x)) + } assert_scalar_integer(x, name = name, arg = arg, call = call) assert_nonmissing(x, name, arg = arg, call = call) min <- if (allow_zero) 0 else 1 @@ -156,6 +175,48 @@ assert_list <- function(x, name = deparse(substitute(x)), arg = name, } +assert_scalar_positive_numeric <- function(x, allow_zero = TRUE, + name = deparse(substitute(x)), + arg = name, call = parent.frame()) { + assert_scalar_numeric(x, name = name, call = call) + if (allow_zero) { + if (x < 0) { + cli::cli_abort("'{name}' must be at least 0", arg = arg, call = call) + } + } else { + if (x <= 0) { + cli::cli_abort("'{name}' must be greater than 0", arg = arg, call = call) + } + } + invisible(x) +} + + +assert_scalar_positive_integer <- function(x, allow_zero = TRUE, + name = deparse(substitute(x)), + tolerance = NULL, arg = name, + call = parent.frame()) { + assert_scalar_integer(x, name, tolerance = tolerance, arg = arg, call = call) + min <- if (allow_zero) 0 else 1 + if (x < min) { + cli::cli_abort("'{name}' must be at least {min}", arg = arg, call = call) + } + invisible(x) +} + + +assert_raw <- function(x, len = NULL, name = deparse(substitute(x)), + arg = name, call = parent.frame()) { + if (!is.raw(x)) { + cli::cli_abort("'{name}' must be a raw vector", arg = arg, call = call) + } + if (!is.null(len)) { + assert_length(x, len, name = name, call = call) + } + invisible(x) +} + + assert_named <- function(x, unique = FALSE, name = deparse(substitute(x)), arg = name, call = parent.frame()) { nms <- names(x) @@ -179,7 +240,7 @@ assert_named <- function(x, unique = FALSE, name = deparse(substitute(x)), match_value <- function(x, choices, name = deparse(substitute(x)), arg = name, call = parent.frame()) { - assert_scalar_character(x, call = call, arg = arg) + assert_scalar_character(x, call = call, name = name, arg = arg) if (!(x %in% choices)) { choices_str <- paste(sprintf("'%s'", choices), collapse = ", ") cli::cli_abort(c("'{name}' must be one of {choices_str}", diff --git a/R/sysdata.rda b/R/sysdata.rda index 9c0dfbcf..4c9f4fd9 100644 Binary files a/R/sysdata.rda and b/R/sysdata.rda differ diff --git a/man/monty_dsl_error_explain.Rd b/man/monty_dsl_error_explain.Rd index 7e441c32..24727f2a 100644 --- a/man/monty_dsl_error_explain.Rd +++ b/man/monty_dsl_error_explain.Rd @@ -4,11 +4,15 @@ \alias{monty_dsl_error_explain} \title{Explain monty error} \usage{ -monty_dsl_error_explain(code) +monty_dsl_error_explain(code, how = "pretty") } \arguments{ \item{code}{The error code, as a string, in the form \code{Exxx} (a capital "E" followed by three numbers)} + +\item{how}{How to explain the error. Options are \code{pretty} (render +pretty text in the console), \code{plain} (display plain text in the +console) and \code{link} (browse to the online help).} } \value{ Nothing, this is called for its side effect only diff --git a/scripts/update_parse_errors b/scripts/update_parse_errors index 8af5d42c..436add51 100755 --- a/scripts/update_parse_errors +++ b/scripts/update_parse_errors @@ -1,34 +1,4 @@ #!/usr/bin/env Rscript - -read_errors <- function() { - path_rmd <- "vignettes/dsl-errors.Rmd" - txt <- readLines(path_rmd) - - re <- "^# `(E[0-9]{3})`$" - i <- grep(re, txt) - if (length(setdiff(grep("^# ", txt), i)) > 0) { - stop("Some headings don't match expected pattern") - } - - f <- function(from, to) { - ret <- txt[from:to] - while (ret[[1]] == "") { - ret <- ret[-1] - } - while (ret[[length(ret)]] == "") { - ret <- ret[-length(ret)] - } - ret - } - - ret <- Map(f, i + 1, c(i[-1] - 1, length(txt))) - names(ret) <- sub(re, "\\1", txt[i]) - ret -} - - -## We might parse this further, e.g., with commonmark, so that we can -## render this nicely to the console as cli would make this pretty -## easy really. -dsl_errors <- read_errors() +dsl_errors <- reside.utils::errors_parse( + "vignettes/dsl-errors.Rmd", "E[0-9]{3}", "monty::monty_dsl_error_explain") save(list = "dsl_errors", file = file.path("R/sysdata.rda"), version = 2) diff --git a/tests/testthat/test-dsl-parse.R b/tests/testthat/test-dsl-parse.R index eb825d64..ef1af335 100644 --- a/tests/testthat/test-dsl-parse.R +++ b/tests/testthat/test-dsl-parse.R @@ -202,31 +202,11 @@ test_that("report back invalid distribution calls", { test_that("can explain an error", { skip_if_not_installed("mockery") - mock_browse <- mockery::mock() - mockery::stub(monty_dsl_error_explain, "utils::browseURL", mock_browse) + mock_explain <- mockery::mock() + mockery::stub(monty_dsl_error_explain, "error_explain", mock_explain) monty_dsl_error_explain("E101") - mockery::expect_called(mock_browse, 1) + mockery::expect_called(mock_explain, 1) expect_equal( - mockery::mock_args(mock_browse)[[1]], - list("https://mrc-ide.github.io/monty/articles/dsl-errors.html#e101")) -}) - - -test_that("error if given invalid code", { - msg <- "Invalid code 'E01', should match 'Exxx'" - expect_error(monty_dsl_error_explain("E01"), - "Invalid code 'E01', should match 'Exxx'") - expect_error(monty_dsl_error_explain("e0001"), - "Invalid code 'e0001', should match 'Exxx'") - expect_error(monty_dsl_error_explain("E0001"), - "Invalid code 'E0001', should match 'Exxx'") - expect_error(monty_dsl_error_explain("anything"), - "Invalid code 'anything', should match 'Exxx'") -}) - - -test_that("error if given unknown code", { - expect_error( - monty_dsl_error_explain("E999"), - "Error 'E999' is undocumented") + mockery::mock_args(mock_explain)[[1]], + list(dsl_errors, "E101", "pretty")) })