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
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
213 changes: 103 additions & 110 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -122,76 +122,125 @@ 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)
check_datanames <- check_modules_datanames_recursive(modules, datanames)
if (length(check_datanames)) {
modules_msg <- sapply(check_datanames, function(mod) {
sprintf(
"%s %s %s missing for module %s.",
`if`(length(mod$missing_datanames) > 1, "Datasets", "Dataset"),
toString(dQuote(mod$missing_datanames, q = FALSE)),
`if`(length(mod$missing_datanames) > 1, "are", "is"),
averissimo marked this conversation as resolved.
Show resolved Hide resolved
toString(dQuote(mod$label, q = FALSE))
)
})
sprintf(
"%s Datasets available in data: %s",
paste(modules_msg, collapse = "\n"),
toString(dQuote(datanames, q = FALSE))
)
} else {
TRUE
}
}

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) {
#' @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")
if (length(check_datanames)) {
averissimo marked this conversation as resolved.
Show resolved Hide resolved
shiny::tagList(
lapply(
check_datanames,
function(mod) {
tagList(
lapply(
result,
function(x) x$html(with_module_name = with_module_name)
)
tags$span(
tags$span(`if`(length(mod$missing_datanames) > 1, "Datasets", "Dataset")),
to_html_code_list(mod$missing_datanames),
tags$span(
paste0(
`if`(length(mod$missing_datanames) > 1, "are missing", "is missing"),
`if`(show_module_info, sprintf(" for tab '%s'.", mod$label), ".")
)
),
if (length(mod$datanames) >= 1) {
tagList(
tags$span(`if`(length(mod$datanames) > 1, "Datasets", "Dataset")),
tags$span("available in data:"),
tagList(
tags$span(
to_html_code_list(mod$datanames),
tags$span(".", .noWS = "outside"),
.noWS = c("outside")
)
)
)
} else {
tags$span("No datasets are available in data.")
}
),
tags$br(.noWS = "before")
)
}
)
} 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) {
tagList(
build_datanames_error_message(
if (with_module_name) modules$label,
datanames,
extra_datanames
),
tags$br(.noWS = "before")
)
}
)
}
}
}
check_datanames <- recursive_check_datanames(modules, datanames)
if (length(check_datanames)) {
check_datanames
)
} else {
TRUE
}
}

#' 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, function(mod) check_modules_datanames_recursive(mod, datanames)),
averissimo marked this conversation as resolved.
Show resolved Hide resolved
recursive = FALSE
)
} else {
missing_datanames <- setdiff(modules$datanames, c("all", datanames))
if (length(missing_datanames)) {
list(list(
label = modules$label,
dataname = modules$datanames,
averissimo marked this conversation as resolved.
Show resolved Hide resolved
missing_datanames = missing_datanames
))
}
}
}

#' Convert character vector to html code separated with commas and "and"
#' @noRd
to_html_code_list <- function(x) { # nolint: object_name.
averissimo marked this conversation as resolved.
Show resolved Hide resolved
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 ", ", "))
averissimo marked this conversation as resolved.
Show resolved Hide resolved
}
)
})
)
}


#' Check `datanames` in filters
#'
#' This function checks whether `datanames` in filters correspond to those in `data`,
Expand Down Expand Up @@ -340,59 +389,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\". Datasets 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\", \"b\" are missing for module \"example teal module\". Datasets available in data: \"mtcars\""
)
}
)
Expand Down
Loading