Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The documentation of check_modules_datanames() #1395

Merged
merged 14 commits into from
Oct 29, 2024
2 changes: 1 addition & 1 deletion R/init.R
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ init <- function(data,

is_modules_ok <- check_modules_datanames(modules, ls(teal.code::get_env(data)))
if (!isTRUE(is_modules_ok) && length(unlist(extract_transformers(modules))) == 0) {
lapply(is_modules_ok$string, warning, call. = FALSE)
warning(is_modules_ok, call. = FALSE)
}

is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data)))
Expand Down
1 change: 1 addition & 0 deletions R/module_teal.R
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) {
data_load_status <- reactive({
if (inherits(data_pulled(), "teal_data")) {
"ok"
# todo: should we hide warnings on top for a data?
} else if (inherits(data, "teal_data_module")) {
"teal_data_module failed"
} else {
Expand Down
12 changes: 4 additions & 8 deletions R/module_teal_data.R
Original file line number Diff line number Diff line change
Expand Up @@ -222,15 +222,11 @@ srv_check_shiny_warnings <- function(id, data, modules) {
moduleServer(id, function(input, output, session) {
output$message <- renderUI({
if (inherits(data(), "teal_data")) {
is_modules_ok <- check_modules_datanames(modules = modules, datanames = ls(teal.code::get_env(data())))
is_modules_ok <- check_modules_datanames_html(
modules = modules, datanames = ls(teal.code::get_env(data()))
)
if (!isTRUE(is_modules_ok)) {
tags$div(
class = "teal-output-warning",
is_modules_ok$html(
# Show modules prefix on message only in teal_data_module tab
grepl(sprintf("data-teal_data_module-%s", id), session$ns(NULL), fixed = TRUE)
)
)
tags$div(is_modules_ok, class = "teal-output-warning")
}
}
})
Expand Down
198 changes: 89 additions & 109 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -122,76 +122,112 @@ report_card_template <- function(title, label, description = NULL, with_filter,

#' Check `datanames` in modules
#'
#' This function ensures specified `datanames` in modules match those in the data object,
#' returning error messages or `TRUE` for successful validation.
#' These functions check if specified `datanames` in modules match those in the data object,
#' returning error messages or `TRUE` for successful validation. Two functions return error message
#' in different forms:
#' - `check_modules_datanames` returns `character(1)` for basic assertion usage
#' - `check_modules_datanames_html` returns `shiny.tag.list` to display it in the app.
#'
#' @param modules (`teal_modules`) object
#' @param datanames (`character`) names of datasets available in the `data` object
#'
#' @return A `character(1)` containing error message or `TRUE` if validation passes.
#' @return `TRUE` if validation passes, otherwise `character(1)` or `shiny.tag.list`
#' @keywords internal
check_modules_datanames <- function(modules, datanames) {
checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"))
checkmate::assert_character(datanames)
out <- check_modules_datanames_html(modules, datanames)
if (inherits(out, "shiny.tag.list")) {
out_with_ticks <- gsub("<code>|</code>", "`", toString(out))
out_text <- gsub("<[^<>]+>", "", toString(out_with_ticks))
trimws(gsub("[[:space:]]+", " ", out_text))
} else {
out
}
}

recursive_check_datanames <- function(modules, datanames) {
# check teal_modules against datanames
if (inherits(modules, "teal_modules")) {
result <- lapply(modules$children, function(module) recursive_check_datanames(module, datanames = datanames))
result <- result[vapply(result, Negate(is.null), logical(1L))]
if (length(result) == 0) {
return(NULL)
}
list(
string = do.call(c, as.list(unname(sapply(result, function(x) x$string)))),
html = function(with_module_name = TRUE) {
tagList(
lapply(
result,
function(x) x$html(with_module_name = with_module_name)
#' @rdname check_modules_datanames
check_modules_datanames_html <- function(modules,
datanames) {
check_datanames <- check_modules_datanames_recursive(modules, datanames)
show_module_info <- inherits(modules, "teal_modules") # used in two contexts - module and app
if (!length(check_datanames)) {
return(TRUE)
}
shiny::tagList(
lapply(
check_datanames,
function(mod) {
tagList(
tags$span(
tags$span(if (length(mod$missing_datanames) == 1) "Dataset" else "Datasets"),
to_html_code_list(mod$missing_datanames),
tags$span(
paste0(
if (length(mod$missing_datanames) > 1) "are missing" else "is missing",
if (show_module_info) sprintf(" for module '%s'.", mod$label) else "."
)
)
)
}
)
} else {
extra_datanames <- setdiff(modules$datanames, c("all", datanames))
if (length(extra_datanames)) {
list(
string = build_datanames_error_message(
modules$label,
datanames,
extra_datanames,
tags = list(
span = function(..., .noWS = NULL) { # nolint: object_name
trimws(paste(..., sep = ifelse(is.null(.noWS), " ", ""), collapse = " "))
},
code = function(x) toString(dQuote(x, q = FALSE))
),
tagList = function(...) trimws(paste(...))
),
# Build HTML representation of the error message with <pre> formatting
html = function(with_module_name = TRUE) {
if (length(datanames) >= 1) {
tagList(
build_datanames_error_message(
if (with_module_name) modules$label,
datanames,
extra_datanames
),
tags$br(.noWS = "before")
tags$span(if (length(datanames) == 1) "Dataset" else "Datasets"),
tags$span("available in data:"),
tagList(
tags$span(
to_html_code_list(datanames),
tags$span(".", .noWS = "outside"),
.noWS = c("outside")
)
)
)
}
} else {
tags$span("No datasets are available in data.")
},
tags$br(.noWS = "before")
)
}
}
}
check_datanames <- recursive_check_datanames(modules, datanames)
if (length(check_datanames)) {
check_datanames
)
)
}

#' Recursively checks modules and returns list for every datanames mismatch between module and data
#' @noRd
check_modules_datanames_recursive <- function(modules, datanames) { # nolint: object_name_length
checkmate::assert_multi_class(modules, c("teal_module", "teal_modules"))
checkmate::assert_character(datanames)
if (inherits(modules, "teal_modules")) {
unlist(
lapply(modules$children, check_modules_datanames_recursive, datanames = datanames),
recursive = FALSE
)
} else {
TRUE
missing_datanames <- setdiff(modules$datanames, c("all", datanames))
if (length(missing_datanames)) {
list(list(
label = modules$label,
missing_datanames = missing_datanames
))
}
}
}

#' Convert character vector to html code separated with commas and "and"
#' @noRd
to_html_code_list <- function(x) {
checkmate::assert_character(x)
do.call(
tagList,
lapply(seq_along(x), function(.ix) {
tagList(
tags$code(x[.ix]),
if (.ix != length(x)) {
if (.ix == length(x) - 1) tags$span(" and ") else tags$span(", ", .noWS = "before")
}
)
})
)
}


#' Check `datanames` in filters
#'
#' This function checks whether `datanames` in filters correspond to those in `data`,
Expand Down Expand Up @@ -340,59 +376,3 @@ strip_style <- function(string) {
useBytes = TRUE
)
}

#' Convert character list to human readable html with commas and "and"
#' @noRd
paste_datanames_character <- function(x,
tags = list(span = shiny::tags$span, code = shiny::tags$code),
tagList = shiny::tagList) { # nolint: object_name.
checkmate::assert_character(x)
do.call(
tagList,
lapply(seq_along(x), function(.ix) {
tagList(
tags$code(x[.ix]),
if (.ix != length(x)) {
tags$span(ifelse(.ix == length(x) - 1, " and ", ", "))
}
)
})
)
}

#' Build datanames error string for error message
#'
#' tags and tagList are overwritten in arguments allowing to create strings for
#' logging purposes
#' @noRd
build_datanames_error_message <- function(label = NULL,
datanames,
extra_datanames,
tags = list(span = shiny::tags$span, code = shiny::tags$code),
tagList = shiny::tagList) { # nolint: object_name.
tags$span(
tags$span(ifelse(length(extra_datanames) > 1, "Datasets", "Dataset")),
paste_datanames_character(extra_datanames, tags, tagList),
tags$span(
paste0(
ifelse(length(extra_datanames) > 1, "are missing", "is missing"),
ifelse(is.null(label), ".", sprintf(" for tab '%s'.", label))
)
),
if (length(datanames) >= 1) {
tagList(
tags$span(ifelse(length(datanames) > 1, "Datasets", "Dataset")),
tags$span("available in data:"),
tagList(
tags$span(
paste_datanames_character(datanames, tags, tagList),
tags$span(".", .noWS = "outside"),
.noWS = c("outside")
)
)
)
} else {
tags$span("No datasets are available in data.")
}
)
}
14 changes: 11 additions & 3 deletions man/check_modules_datanames.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion tests/testthat/test-init.R
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,20 @@ testthat::test_that(
data = teal.data::teal_data(mtcars = mtcars),
modules = list(example_module(datanames = "iris"))
),
"Dataset \"iris\" is missing for tab 'example teal module'. Dataset available in data: \"mtcars\"."
"Dataset `iris` is missing for module 'example teal module'. Dataset available in data: `mtcars`."
)
}
)

testthat::test_that(
"init throws warning when datanames in modules incompatible w/ datanames in data and there is no transformers",
{
testthat::expect_warning(
init(
data = teal.data::teal_data(mtcars = mtcars),
modules = list(example_module(datanames = c("a", "b")))
),
"Datasets `a` and `b` are missing for module 'example teal module'. Dataset available in data: `mtcars`."
)
}
)
Expand Down
Loading
Loading