diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24e1e2c1..5949c0ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/lorenzwalthert/precommit - rev: v0.4.3.9007 + rev: v0.4.3.9009 hooks: - id: style-files name: Style code with `styler` diff --git a/DESCRIPTION b/DESCRIPTION index 2908ce62..6608015d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -31,6 +31,7 @@ Imports: shinyjs, shinyvalidate (>= 0.1.3), stats, + teal.code (>= 0.6.0), teal.data (>= 0.7.0), teal.logger (>= 0.3.1), teal.widgets (>= 0.4.3), diff --git a/NAMESPACE b/NAMESPACE index 209b1855..bd169a15 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,12 +1,29 @@ # Generated by roxygen2: do not edit by hand +S3method(anyNA,type) +S3method(c,specification) +S3method(c,type) S3method(data_extract_multiple_srv,FilteredData) S3method(data_extract_multiple_srv,list) S3method(data_extract_multiple_srv,reactive) S3method(data_extract_srv,FilteredData) S3method(data_extract_srv,list) +S3method(determine,colData) +S3method(determine,datasets) +S3method(determine,default) +S3method(determine,list) +S3method(determine,specification) +S3method(determine,values) +S3method(determine,variables) +S3method(extract,default) +S3method(extract,teal_data) S3method(filter_spec_internal,default) S3method(filter_spec_internal,delayed_data) +S3method(is.delayed,default) +S3method(is.delayed,list) +S3method(is.delayed,specification) +S3method(is.delayed,type) +S3method(is.na,type) S3method(merge_expression_module,list) S3method(merge_expression_module,reactive) S3method(merge_expression_srv,list) @@ -19,6 +36,7 @@ S3method(print,delayed_select_spec) S3method(print,delayed_value_choices) S3method(print,delayed_variable_choices) S3method(print,filter_spec) +S3method(print,type) S3method(resolve,default) S3method(resolve,delayed_choices_selected) S3method(resolve,delayed_data_extract_spec) @@ -45,6 +63,9 @@ export(data_extract_spec) export(data_extract_srv) export(data_extract_ui) export(datanames_input) +export(datasets) +export(determine) +export(extract) export(filter_spec) export(first_choice) export(first_choices) @@ -55,21 +76,31 @@ export(get_extract_datanames) export(get_merge_call) export(get_relabel_call) export(is.choices_selected) +export(is.delayed) export(is_single_dataset) export(last_choice) export(last_choices) export(list_extract_spec) +export(mae_colData) export(merge_datasets) export(merge_expression_module) export(merge_expression_srv) +export(module_input_server) +export(module_input_ui) export(no_selected_as_NULL) export(resolve_delayed) +export(resolver) export(select_spec) export(select_spec.default) export(select_spec.delayed_data) export(split_by_sep) +export(update_spec) export(value_choices) +export(values) export(variable_choices) +export(variables) import(shiny) importFrom(dplyr,"%>%") importFrom(lifecycle,badge) +importFrom(methods,is) +importFrom(tidyselect,everything) diff --git a/R/delayed.R b/R/delayed.R new file mode 100644 index 00000000..cb9c70eb --- /dev/null +++ b/R/delayed.R @@ -0,0 +1,60 @@ +# Only delay if the type or object really needs it and is not already delayed +delay <- function(x) { + if (is.delayed(x)) { + attr(x, "delayed") <- TRUE + } + x +} + +#' Is the specification resolved? +#' +#' Check that the specification is resolved against a given data source. +#' @param specification Object to be evaluated. +#' @returns A single logical value. +#' @examples +#' is.delayed(1) +#' is.delayed(variables("df", "df")) +#' is.delayed(variables("df")) # Unknown selection +#' @export +is.delayed <- function(specification) { + UseMethod("is.delayed") +} + +#' @export +#' @method is.delayed default +is.delayed.default <- function(specification) { + # FIXME: A warning? + FALSE +} + +# Handling a list of transformers e1 | e2 +#' @export +#' @method is.delayed list +is.delayed.list <- function(specification) { + any(vapply(specification, is.delayed, logical(1L))) +} + +#' @export +#' @method is.delayed specification +is.delayed.specification <- function(specification) { + any(vapply(specification, is.delayed, logical(1L))) +} + +#' @export +#' @method is.delayed type +is.delayed.type <- function(specification) { + if (!is.na(specification)) { + return(!all(is.character(specification$choices)) || !all(is.character(specification$selected))) + } + FALSE +} + +resolved <- function(specification, type = is(specification)) { + s <- all(is.character(specification$choices)) && all(is.character(specification$selected)) + + if (!s && !all(specification$selected %in% specification$choices)) { + stop("Selected ", type, " not resolved.") + } + attr(specification, "delayed") <- NULL + specification +} diff --git a/R/extract.R b/R/extract.R new file mode 100644 index 00000000..6279ec6f --- /dev/null +++ b/R/extract.R @@ -0,0 +1,80 @@ +#' Internal method to extract data from different objects +#' +#' Required to resolve a specification into something usable (by comparing with the existing data). +#' Required by merging data based on a resolved specification. +#' @param x Object from which a subset/element is required. +#' @param variable Name of the element to be extracted. +#' @param ... Other arguments passed to the specific method. +#' @export +#' @examples +#' extract(iris, "Sepal.Length") +extract <- function(x, variable, ...) { + UseMethod("extract") +} + + +#' @export +extract.default <- function(x, variable, ..., drop = FALSE) { + if (length(dim(x)) == 2L || length(variable) > 1L) { + x[, variable, drop = drop] + } else { + x[[variable]] + } +} + +#' @export +extract.teal_data <- function(x, variable, ...) { + if (length(variable) > 1L) { + x[variable] + } else { + x[[variable]] + } +} + +# @export +# @method extract data.frame +# extract.data.frame <- function(x, variable) { +# # length(variable) == 1L +# x[, variable, drop = TRUE] +# } + +# @export +# extract.qenv <- function(x, variable) { +# x[[variable]] +# } + +# Get code to be evaluated & displayed by modules +extract_srv <- function(id, input, data) { + stopifnot(is.null(input$datasets)) + stopifnot(is.null(input$variables)) + moduleServer( + id, + function(input, output, session) { + obj <- extract(data, input$datasets) + method <- paste0("extract.", class(obj)) + method <- dynGet(method, ifnotfound = "extract.default", inherits = TRUE) + if (identical(method, "extract.default")) { + b <- get("extract.default") + } else { + b <- get(method) + } + # Extract definition + extract_f_def <- call("<-", x = as.name("extract"), value = b) + q <- teal.code::eval_code(data, code = extract_f_def) + + # Extraction happening: + # FIXME assumes only to variables used + output <- call("<-", + x = as.name(input$datasets), value = + substitute( + extract(obj, variables), + list( + obj = as.name(input$datasets), + variables = input$variables + ) + ) + ) + q <- teal.code::eval_code(q, code = output) + } + ) +} diff --git a/R/merge_dataframes.R b/R/merge_dataframes.R new file mode 100644 index 00000000..f64870f9 --- /dev/null +++ b/R/merge_dataframes.R @@ -0,0 +1,230 @@ +# Simplify multiple datasets & variables into the bare minimum necessary. +# This simplifies the number of extractions and merging required +consolidate_extraction <- function(...) { + if (...length() > 1) { + input_resolved <- list(...) + } else { + input_resolved <- ..1 + } + + # Assume the data is a data.frame so no other specifications types are present. + datasets <- lapply(input_resolved, function(x) { + x$datasets + }) + variables <- lapply(input_resolved, function(x) { + x$variables + }) + lapply(unique(datasets), + function(dataset, x, y) { + list( + "datasets" = dataset, + "variables" = unique(unlist(y[x == dataset])) + ) + }, + x = datasets, y = variables + ) +} + +# Function to add ids of data.frames to the output of modules to enable merging them. +add_ids <- function(input, data) { + jk <- teal.data::join_keys(data) + # If no join keys they should be on the input + if (!length(jk)) { + return(input) + } + + datasets <- lapply(input, function(x) { + x$datasets + }) + for (i in seq_along(input)) { + x <- input[[i]] + # Avoid adding as id something already present: No duplicating input. + ids <- setdiff(unique(unlist(jk[[x$datasets]])), x$variables) + input[[i]][["variables"]] <- c(x$variables, ids) + } + input +} + +# Find common ids to enable merging. +extract_ids <- function(input, data) { + jk <- teal.data::join_keys(data) + # No join_keys => input + if (!length(jk)) { + input <- unlist(input) + tab <- table(input) + out <- names(tab)[tab > 1] + + if (!length(out)) { + return(NULL) + } + return(out) + } + + l <- lapply(datasets, function(x, join_keys) { + unique(unlist(jk[[x]])) + }, join_keys = jk) + out <- unique(unlist(l)) +} + +merge_call_pair <- function(input_res, by, data, + merge_function = "dplyr::full_join") { + selections <- consolidate_extraction(input_res) + stopifnot(length(selections) == 2L) + datasets <- unique(unlist(lapply(selections, `[[`, "datasets"), FALSE, FALSE)) + stopifnot(length(datasets) >= 2) + if (is.reactive(data)) { + data <- data() + } + + if (is.null(by)) { + by <- extract_ids(input = selections, data) + } + + data <- add_library_call(merge_function, data) + + if (!missing(by) && length(by)) { + call_m <- as.call(c( + rlang::parse_expr(merge_function), + list( + x = as.name(datasets[1]), + y = as.name(datasets[2]), + by = by + ) + )) + } else { + call_m <- as.call(c( + rlang::parse_expr(merge_function), + list( + x = as.name(datasets[1]), + y = as.name(datasets[2]) + ) + )) + } + call_m +} + +merge_call_multiple <- function(input_res, ids, data, merge_function = "dplyr::full_join", + anl = "ANL") { + selections <- consolidate_extraction(input_res) + datasets <- unique(unlist(lapply(selections, `[[`, "datasets"), FALSE, FALSE)) + stopifnot(is.character(datasets) && length(datasets) >= 1L) + number_merges <- length(datasets) - 1L + if (is.reactive(data)) { + data <- data() + } + out <- vector("list", length = 2) + names(out) <- c("code", "specification") + + if (number_merges == 0L) { + dataset <- names(selections) + variables <- selections[[1]]$variables + final_call <- call( + "<-", as.name(anl), + call("dplyr::select", as.name(dataset), as.names(variables)) + ) + out$code <- teal.code::eval_code(data, final_call) + out$input <- input_res + return(out) + } + stopifnot( + "Number of arguments for type matches data" = length(merge_function) == number_merges || length(merge_function) == 1L + ) + if (!missing(ids) && !is.null(ids)) { + stopifnot("Number of arguments for ids matches data" = !(is.list(ids) && length(ids) == number_merges)) + } + if (length(merge_function) != number_merges) { + merge_function <- rep(merge_function, number_merges) + } + if (!missing(ids) && length(ids) != number_merges) { + ids <- rep(ids, number_merges) + } + + if (number_merges == 1L && missing(ids)) { + data <- add_library_call(merge_function, data) + previous <- merge_call_pair(selections, merge_function = merge_function, data = data) + final_call <- call("<-", x = as.name(anl), value = previous) + out$code <- teal.code::eval_code(data, final_call) + out$input <- input_res + return(out) + } else if (number_merges == 1L && !missing(ids)) { + data <- add_library_call(merge_function, data) + previous <- merge_call_pair(selections, by = ids, merge_function = merge_function, data = data) + final_call <- call("<-", x = as.name(anl), value = previous) + out$code <- teal.code::eval_code(data, final_call) + out$input <- input_res + return(out) + } + + for (merge_i in seq_len(number_merges)) { + if (merge_i == 1L) { + datasets_i <- seq_len(2) + if (!missing(ids)) { + ids <- ids[[merge_i]] + previous <- merge_call_pair(selections[datasets_i], + ids, + merge_function[merge_i], + data = data + ) + } else { + previous <- merge_call_pair(selections[datasets_i], + merge_function[merge_i], + data = data + ) + } + } else { + datasets_ids <- merge_i:(merge_i + 1L) + if (!missing(ids)) { + current <- merge_call_pair(selections[datasets_ids], + merge_function = merge_function[merge_i], data = data + ) + } else { + ids <- ids[[merge_i]] + current <- merge_call_pair(selections[datasets_ids], + ids, + merge_function = merge_function[merge_i], data = data + ) + } + } + previous <- call("%>%", as.name(previous), as.name(current)) + } + final_call <- call("<-", x = as.name(anl), value = previous) + out$code <- teal.code::eval_code(data, final_call) + out$input <- input_res + out +} + +merge_type_srv <- function(id, inputs, data, merge_function = "dplyr::full_join", anl_name = "ANL") { + checkmate::assert_list(inputs, names = "named") + stopifnot(make.names(anl_name) == anl_name) + + moduleServer( + id, + function(input, output, session) { + req(input) + resolved_spec <- reactive({ + resolved_spec <- lapply(names(inputs), function(x) { + # Return characters not reactives + module_input_server(x, inputs[[x]], data)() + }) + # Keep input names + names(resolved_spec) <- names(inputs) + resolved_spec + }) + td <- merge_call_multiple(resolved_spec(), NULL, + data, + merge_function = merge_function, anl = anl_name + ) + } + ) +} + +add_library_call <- function(merge_function, data) { + if (is.reactive(data)) { + data <- data() + } + if (grepl("::", merge_function, fixed = TRUE)) { + m <- strsplit(merge_function, split = "::", fixed = TRUE)[[1]] + data <- teal.code::eval_code(data, call("library", m[1])) + } + data +} diff --git a/R/module_input.R b/R/module_input.R new file mode 100644 index 00000000..65187c09 --- /dev/null +++ b/R/module_input.R @@ -0,0 +1,100 @@ +helper_input <- function(id, + label, + multiple = FALSE) { + shiny::selectInput( + id, + label, + choices = NULL, + selected = NULL, + multiple = multiple + ) +} + +#' @export +module_input_ui <- function(id, label, spec) { + ns <- NS(id) + input <- tagList( + a(label), + ) + + if (valid_specification(spec)) { + stop("Unexpected object used as specification.") + } + + l <- lapply(spec, function(x) { + helper_input(ns(is(x)), + paste("Select", is(x), collapse = " "), + multiple = is(x) != "datasets" + ) + }) + input <- tagList(input, l) +} + +#' @export +module_input_server <- function(id, spec, data) { + stopifnot(is.specification(spec)) + stopifnot(is.character(id)) + moduleServer(id, function(input, output, session) { + react_updates <- reactive({ + if (is.reactive(data)) { + d <- data() + } else { + d <- data + } + + if (!anyNA(spec) && is.delayed(spec)) { + spec <- resolver(spec, d) + } + + for (i in seq_along(names(input))) { + variable <- names(input)[i] + x <- input[[variable]] + update_is_empty <- !is.null(x) && all(!nzchar(x)) + if (update_is_empty) { + break + } + update_is_valid <- all(x %in% spec[[variable]][["choices"]]) + # Includes for adding or removing but not reordering + selection_is_new <- length(x) != length(spec[[variable]][["selected"]]) + if (update_is_valid && selection_is_new) { + spec <- update_spec(spec, variable, x) + spec <- resolver(spec, d) + } + } + spec + }) + + observe({ + spec <- req(react_updates()) + req(!is.delayed(spec)) + # Relies on order of arguments + for (i in seq_along(spec)) { + variable <- names(spec)[i] + + shiny::updateSelectInput( + session, + variable, + choices = unorig(spec[[variable]]$choices), + selected = unorig(spec[[variable]]$selected) + ) + # FIXME: set on gray the input + # FIXME: Hide input field if any type on specification cannot be solved + } + }) + + + # Full selection #### + react_selection <- reactive({ + spec <- req(react_updates()) + req(!is.delayed(spec)) + # FIXME: breaks with conditional specification: list(spec, spec) + selection <- vector("list", length(spec)) + names(selection) <- names(spec) + for (i in seq_along(spec)) { + variable <- names(spec)[i] + selection[[variable]] <- unorig(spec[[variable]]$selected) + } + selection + }) + }) +} diff --git a/R/resolver.R b/R/resolver.R new file mode 100644 index 00000000..0f4ca488 --- /dev/null +++ b/R/resolver.R @@ -0,0 +1,248 @@ +#' Resolve the specification +#' +#' Given the specification of some data to extract find if they are available or not. +#' The specification for selecting a variable shouldn't depend on the data of said variable. +#' @param spec A object extraction specification. +#' @param data The qenv where the specification is evaluated. +#' +#' @returns A specification but resolved: the names and selection is the name of the objects (if possible). +#' @export +#' +#' @examples +#' dataset1 <- datasets(where(is.data.frame)) +#' dataset2 <- datasets(where(is.matrix)) +#' spec <- c(dataset1, variables("a", "a")) +#' td <- within(teal.data::teal_data(), { +#' df <- data.frame(a = as.factor(LETTERS[1:5]), b = letters[1:5]) +#' df2 <- data.frame(a = LETTERS[1:5], b = 1:5) +#' m <- matrix() +#' }) +#' resolver(list(spec, dataset2), td) +#' resolver(dataset2, td) +#' resolver(spec, td) +#' spec <- c(dataset1, variables("a", where(is.character))) +#' resolver(spec, td) +resolver <- function(spec, data) { + if (!inherits(data, "qenv")) { + stop("Please use qenv() or teal_data() objects.") + } + if (!is.delayed(spec)) { + return(spec) + } + + stopifnot(is.list(spec) || is.specification(spec)) + if (is.type(spec)) { + spec <- list(spec) + names(spec) <- is(spec[[1]]) + class(spec) <- c("specification", class(spec)) + } + + det <- determine(spec, data) + if (is.null(names(det))) { + return(lapply(det, `[[`, 1)) + } else { + det$type + } +} + +#' A method that should take a type and resolve it. +#' +#' Generic that makes the minimal check on spec. +#' Responsible of subsetting/extract the data received and check that the type matches +#' @param type The specification to resolve. +#' @param data The minimal data required. +#' @return A list with two elements, the type resolved and the data extracted. +#' @keywords internal +#' @export +determine <- function(type, data, ...) { + stopifnot(is.type(type) || is.list(type) || is.specification(type)) + if (!is.delayed(type)) { + return(list(type = type, data = extract(data, unorig(type$selected)))) + } + UseMethod("determine") +} + +#' @export +determine.default <- function(type, data, ...) { + stop("There is not a specific method to pick choices.") +} + +#' @export +determine.list <- function(type, data, ...) { + if (is.list(type) && is.null(names(type))) { + l <- lapply(type, determine, data = data) + return(l) + } + + type <- eval_type_names(type, data) + type <- eval_type_select(type, data) + + list(type = type, data = extract(data, unorig(type$selected))) +} + +#' @export +determine.colData <- function(type, data, ...) { + if (!requireNamespace("SummarizedExperiment", quietly = TRUE)) { + stop("Requires SummarizedExperiment package from Bioconductor.") + } + data <- as.data.frame(colData(data)) + type <- eval_type_names(type, data) + + if (is.null(type$choices) || !length(type$choices)) { + stop("No ", toString(is(type)), " meet the specification.", call. = FALSE) + } + + type <- eval_type_select(type, data) + + list(type = type, data = extract(data, unorig(type$selected))) +} + +#' @export +determine.specification <- function(type, data, ...) { + stopifnot(inherits(data, "qenv")) + + # Adding some default specifications if they are missing + if ("values" %in% names(type) && !"variables" %in% names(type)) { + type <- append(type, list(variables = variables()), length(type) - 1) + } + + if ("variables" %in% names(type) && !"datasets" %in% names(type)) { + type <- append(type, list(variables = datasets()), length(type) - 1) + } + + d <- data + for (i in seq_along(type)) { + di <- determine(type[[i]], d) + # overwrite so that next type in line receives the corresponding data and specification + if (is.null(di$type)) { + next + } + type[[i]] <- di$type + d <- di$data + } + list(type = type, data = data) # It is the transform object resolved. +} + +#' @export +determine.datasets <- function(type, data, ...) { + if (is.null(data)) { + return(list(type = type, data = NULL)) + } else if (!inherits(data, "qenv")) { + stop("Please use qenv() or teal_data() objects.") + } + + # Assumes the object has colnames method (true for major object classes: DataFrame, tibble, Matrix, array) + # FIXME: What happens if colnames is null: colnames(array(dim = c(4, 2))) + type <- eval_type_names(type, data) + + if (is.null(type$choices) || !length(type$choices)) { + stop("No ", toString(is(type)), " meet the specification.", call. = FALSE) + } + + type <- eval_type_select(type, data) + + list(type = type, data = extract(data, unorig(type$selected))) +} + +#' @export +determine.variables <- function(type, data, ...) { + if (is.null(data)) { + return(list(type = type, data = NULL)) + } else if (length(dim(data)) != 2L) { + stop( + "Can't resolve variables from this object of class ", + toString(sQuote(class(data))) + ) + } + + if (ncol(data) <= 0L) { + stop("Can't pull variable: No variable is available.") + } + + type <- eval_type_names(type, data) + + if (is.null(type$choices) || !length(type$choices)) { + stop("No ", toString(is(type)), " meet the specification.", call. = FALSE) + } + + type <- eval_type_select(type, data) + + # Not possible to know what is happening + if (is.delayed(type)) { + return(list(type = type, data = NULL)) + } + # This works for matrices and data.frames of length 1 or multiple + # be aware of drop behavior on tibble vs data.frame + list(type = type, data = extract(data, unorig(type$selected))) +} + +#' @export +determine.values <- function(type, data, ...) { + if (!is.numeric(data)) { + d <- data + names(d) <- data + } else { + d <- data + } + sel <- selector(d, type$choices) + type$choices <- data[sel] + + + sel2 <- selector(d[sel], type$selected) + type$selected <- data[sel][sel2] + + # Not possible to know what is happening + if (is.delayed(type)) { + return(list(type = type, data = NULL)) + } + + list(type = type, data = data[sel]) +} + +orig <- function(x) { + attr(x, "original") +} + +unorig <- function(x) { + attr(x, "original") <- NULL + x +} + +eval_type_names <- function(type, data) { + orig_choices <- orig(type$choices) + if (length(orig_choices) == 1L) { + orig_choices <- orig_choices[[1L]] + } + + new_choices <- selector(data, type$choices) + + new_choices <- unique(names(new_choices)) + attr(new_choices, "original") <- orig_choices + + type$choices <- new_choices + + type +} + +eval_type_select <- function(type, data) { + stopifnot(is.character(type$choices)) + if (!is(data, "qenv")) { + data <- extract(data, type$choices) + } else { + # Do not extract; selection would be from the data extracted not from the names. + data <- data[type$choices] + } + orig_selected <- orig(type$selected) + if (length(orig_selected) == 1L) { + orig_selected <- orig_selected[[1L]] + } + + choices <- seq_along(type$choices) + names(choices) <- type$choices + new_selected <- names(selector(data, type$selected)) + + attr(new_selected, "original") <- orig_selected + type$selected <- new_selected + + type +} diff --git a/R/selector.R b/R/selector.R new file mode 100644 index 00000000..069f8628 --- /dev/null +++ b/R/selector.R @@ -0,0 +1,14 @@ +selector <- function(data, ...) { + if (is.environment(data)) { + # To keep the "order" of the names in the extraction: avoids suprises + data <- as.list(data)[names(data)] + } else if (length(dim(data)) == 2L) { + data <- as.data.frame(data) + } + + if (is.null(names(data))) { + stop("Can't extract the data.") + } + pos <- tidyselect::eval_select(expr = ..., data) + pos +} diff --git a/R/types.R b/R/types.R new file mode 100644 index 00000000..d1f02fc6 --- /dev/null +++ b/R/types.R @@ -0,0 +1,231 @@ +is.specification <- function(x) { + inherits(x, "specification") +} + + +valid_specification <- function(x) { + !((is.type(x) || is.specification(x))) +} + +na_type <- function(type) { + out <- NA_character_ + class(out) <- c(type, "type") + out +} + +is.type <- function(x) { + inherits(x, "type") +} + +#' @export +#' @method is.na type +is.na.type <- function(x) { + anyNA(unclass(x[c("names", "selected")])) +} + +#' @export +anyNA.type <- function(x, recursive = FALSE) { + anyNA(unclass(x[c("choices", "selected")]), recursive) +} + +type_helper <- function(choices, selected, type) { + out <- list(choices = choices, selected = selected) + class(out) <- c(type, "type", "list") + attr(out$choices, "original") <- choices + attr(out$selected, "original") <- selected + delay(out) +} + +#' @rdname types +#' @name Types +#' @title Type specification +#' @description +#' Define how to select and extract data +#' @param choices <[`tidy-select`][dplyr::dplyr_tidy_select]> One unquoted expression to be used to pick the choices. +#' @param selected <[`tidy-select`][dplyr::dplyr_tidy_select]> One unquoted expression to be used to pick from choices to be selected. +#' @returns An object of the same class as the function with two elements: names the content of x, and select. +#' @examples +#' datasets() +#' datasets("A") +#' c(datasets("A"), datasets("B")) +#' datasets(where(is.data.frame)) +#' c(datasets("A"), variables(where(is.numeric))) +NULL + +#' @importFrom tidyselect everything +#' @describeIn types Specify datasets. +#' @export +datasets <- function(choices = tidyselect::everything(), selected = 1) { + type_helper(rlang::enquo(choices), rlang::enquo(selected), "datasets") +} + +#' @describeIn types Specify variables. +#' @export +variables <- function(choices = tidyselect::everything(), selected = 1) { + type_helper(rlang::enquo(choices), rlang::enquo(selected), "variables") +} + +#' @describeIn types Specify colData. +#' @export +mae_colData <- function(choices = tidyselect::everything(), selected = 1) { + type_helper(rlang::enquo(choices), rlang::enquo(selected), "colData") +} + +#' @describeIn types Specify values. +#' @export +values <- function(choices = tidyselect::everything(), selected = 1) { + type_helper(rlang::enquo(choices), rlang::enquo(selected), "values") +} + +#' @export +c.specification <- function(...) { + l <- list(...) + types <- lapply(l, names) + typesc <- vapply(l, is.specification, logical(1L)) + if (!all(typesc)) { + stop("An object in position ", which(!typesc), " is not a specification.") + } + utypes <- unique(unlist(types, FALSE, FALSE)) + vector <- vector("list", length(utypes)) + names(vector) <- utypes + for (t in utypes) { + new_type <- vector("list", length = 2) + names(new_type) <- c("choices", "selected") + class(new_type) <- c("type", "list") + for (i in seq_along(l)) { + if (!t %in% names(l[[i]])) { + next + } + # Slower but less code duplication: + # new_type <- c(new_type, l[[i]][[t]]) + # then we need class(new_type) <- c(t, "type", "list") outside the loop + old_choices <- new_type$choices + old_selected <- new_type$selected + new_type$choices <- c(old_choices, l[[i]][[t]][["choices"]]) + attr(new_type$choices, "original") <- c(orig( + old_choices + ), orig(l[[i]][[t]][["names"]])) + new_type$selected <- c(old_selected, l[[i]][[t]][["selected"]]) + attr(new_type$selected, "original") <- c(orig(old_selected), orig(l[[i]][[t]][["selected"]])) + attr(new_type, "delayed") <- any(attr(new_type, "delayed"), attr(l[[i]], "delayed")) + } + orig_choices <- unique(orig(new_type$choices)) + new_type$choices <- unique(new_type$choices) + attr(new_type$choices, "original") <- orig_choices + + orig_selected <- unique(orig(new_type$selected)) + new_type$selected <- unique(new_type$selected) + attr(new_type$selected, "original") <- orig_selected + class(new_type) <- c(t, "type", "list") + vector[[t]] <- new_type + } + class(vector) <- c("specification", "list") + vector +} + +#' @export +c.type <- function(...) { + l <- list(...) + types <- lapply(l, is) + typesc <- vapply(l, is.type, logical(1L)) + if (!all(typesc)) { + stop("An object in position ", which(!typesc), " is not a type.") + } + utypes <- unique(unlist(types, FALSE, FALSE)) + vector <- vector("list", length(utypes)) + names(vector) <- utypes + for (t in utypes) { + new_type <- vector("list", length = 2) + names(new_type) <- c("choices", "selected") + for (i in seq_along(l)) { + if (!is(l[[i]], t)) { + next + } + old_choices <- new_type$choices + old_selected <- new_type$selected + new_type$choices <- c(old_choices, l[[i]][["choices"]]) + attr(new_type$choices, "original") <- c(orig( + old_choices + ), orig(l[[i]][["choices"]])) + new_type$selected <- unique(c(old_selected, l[[i]][["selected"]])) + attr(new_type$selected, "original") <- c(orig(old_selected), orig(l[[i]][["selected"]])) + } + orig_choices <- unique(orig(new_type$choices)) + orig_selected <- unique(orig(new_type$selected)) + + new_type$choices <- unique(new_type$choices) + if (length(new_type$choices) == 1) { + new_type$choices <- new_type$choices[[1]] + } + attr(new_type$choices, "original") <- orig_choices + + if (length(new_type$selected) == 1) { + new_type$selected <- new_type$selected[[1]] + } + attr(new_type$selected, "original") <- orig_selected + + class(new_type) <- c(t, "type", "list") + attr(new_type, "delayed") <- is.delayed(new_type) + vector[[t]] <- new_type + } + if (length(vector) == 1) { + return(vector[[1]]) + } + class(vector) <- c("specification", "list") + vector +} + +simplify_c <- function(x) { + unique(unlist(x, FALSE, FALSE)) +} + +#' @export +print.type <- function(x, ...) { + if (is.na(x)) { + cat("Nothing possible") + return(x) + } + + choices_fns <- count_functions(x$choices) + + msg_values <- character() + choices_values <- length(x$choices) - sum(choices_fns) + if (any(choices_fns)) { + msg_values <- paste0(msg_values, sum(choices_fns), " functions for possible choices.", + collapse = "\n" + ) + } + if (choices_values) { + msg_values <- paste0(msg_values, paste0(rlang::as_label(x$choices[!choices_fns]), collapse = ", "), + " as possible choices.", + collapse = "\n" + ) + } + + selected_fns <- count_functions(x$selected) + + msg_sel <- character() + sel_values <- length(x$selected) - sum(selected_fns) + if (any(selected_fns)) { + msg_sel <- paste0(msg_sel, sum(selected_fns), " functions to select.", + collapse = "\n" + ) + } + if (sel_values) { + msg_sel <- paste0(msg_sel, paste0(rlang::as_label(x$selected[!selected_fns]), collapse = ", "), + " selected.", + collapse = "\n" + ) + } + + cat(msg_values, msg_sel) + return(x) +} + +count_functions <- function(x) { + if (is.list(x)) { + vapply(x, is.function, logical(1L)) + } else { + FALSE + } +} diff --git a/R/update_spec.R b/R/update_spec.R new file mode 100644 index 00000000..5fcd0245 --- /dev/null +++ b/R/update_spec.R @@ -0,0 +1,102 @@ +#' Update a specification +#' +#' Update the specification for different selection. +#' @param spec A resolved specification such as one created with datasets and variables. +#' @param type Which type was updated? One of datasets, variables, values. +#' @param value What is the new selection? One that is a valid value for the given type and specification. +#' @return The specification with restored choices and selection if caused by the update. +#' @export +#' @examples +#' td <- within(teal.data::teal_data(), { +#' df <- data.frame( +#' A = as.factor(letters[1:5]), +#' Ab = LETTERS[1:5] +#' ) +#' df_n <- data.frame( +#' C = 1:5, +#' Ab = as.factor(letters[1:5]) +#' ) +#' }) +#' data_frames_factors <- c(datasets(where(is.data.frame)), variables(where(is.factor))) +#' res <- resolver(data_frames_factors, td) +#' update_spec(res, "datasets", "df_n") +#' # update_spec(res, "datasets", "error") +update_spec <- function(spec, type, value) { + if (!is.character(value)) { + stop( + "The updated value is not a character.", + "\nDo you attempt to set a new specification? Please open an issue." + ) + } + + if (valid_specification(spec)) { + stop("Unexpected object used as specification") + } + + if (is.null(names(spec))) { + updated_spec <- lapply(spec, update_s_spec, type = type, value = value) + class(updated_spec) <- class(spec) + return(updated_spec) + } + if (!is.null(names(spec))) { + updated_spec <- update_s_spec(spec, type, value) + } else if (is.type(spec)) { + updated_spec <- update_s_spec(spec, is(spec), value) + } + updated_spec +} + +#' @importFrom methods is +update_s_spec <- function(spec, type, value) { + if (is.type(spec)) { + l <- list(spec) + names(l) <- is(spec) + out <- update_s_spec(l, type, value) + return(out[[is(spec)]]) + } + + if (is.delayed(spec)) { + stop("Specification is not resolved (`!is.delayed(spec)`) can't update selections.") + } + + spec_types <- names(spec) + type <- match.arg(type, spec_types) + restart_types <- spec_types[seq_along(spec_types) > which(type == spec_types)] + + valid_names <- spec[[type]]$choices + + if (!is.list(valid_names) && all(value %in% valid_names)) { + original_select <- orig(spec[[type]]$selected) + spec[[type]][["selected"]] <- value + attr(spec[[type]][["selected"]], "original") <- original_select + } else if (!is.list(valid_names) && !all(value %in% valid_names)) { + original_select <- orig(spec[[type]]$selected) + + valid_values <- intersect(value, valid_names) + if (!length(valid_values)) { + stop("No valid value provided.") + } + if (!length(valid_values)) { + spec[[type]][["selected"]] <- original_select + } else { + spec[[type]][["selected"]] <- valid_values + } + attr(spec[[type]][["selected"]], "original") <- original_select + } else { + stop("It seems the specification needs to be resolved first.") + } + + # Restore to the original specs + for (type_restart in restart_types) { + if (is.na(spec[[type_restart]])) { + next + } + restored_specification <- type_helper( + orig(spec[[type_restart]]$choices), + orig(spec[[type_restart]]$selected), + type_restart + ) + spec[[type_restart]] <- restored_specification + } + spec +} diff --git a/inst/WORDLIST b/inst/WORDLIST index 69f70fc7..f6b2e6ca 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -1,10 +1,12 @@ CDISC -Forkers -Hoffmann -Shinylive -UI cloneable +colData +Forkers funder +Hoffmann preselected +qenv repo reproducibility +Shinylive +UI diff --git a/man/determine.Rd b/man/determine.Rd new file mode 100644 index 00000000..6d2b4fe9 --- /dev/null +++ b/man/determine.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/resolver.R +\name{determine} +\alias{determine} +\title{A method that should take a type and resolve it.} +\usage{ +determine(type, data, ...) +} +\arguments{ +\item{type}{The specification to resolve.} + +\item{data}{The minimal data required.} +} +\value{ +A list with two elements, the type resolved and the data extracted. +} +\description{ +Generic that makes the minimal check on spec. +Responsible of subsetting/extract the data received and check that the type matches +} +\keyword{internal} diff --git a/man/extract.Rd b/man/extract.Rd new file mode 100644 index 00000000..9c2ab252 --- /dev/null +++ b/man/extract.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract.R +\name{extract} +\alias{extract} +\title{Internal method to extract data from different objects} +\usage{ +extract(x, variable, ...) +} +\arguments{ +\item{x}{Object from which a subset/element is required.} + +\item{variable}{Name of the element to be extracted.} + +\item{...}{Other arguments passed to the specific method.} +} +\description{ +Required to resolve a specification into something usable (by comparing with the existing data). +Required by merging data based on a resolved specification. +} +\examples{ +extract(iris, "Sepal.Length") +} diff --git a/man/is.delayed.Rd b/man/is.delayed.Rd new file mode 100644 index 00000000..7a83ec6a --- /dev/null +++ b/man/is.delayed.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/delayed.R +\name{is.delayed} +\alias{is.delayed} +\title{Is the specification resolved?} +\usage{ +is.delayed(specification) +} +\arguments{ +\item{specification}{Object to be evaluated.} +} +\value{ +A single logical value. +} +\description{ +Check that the specification is resolved against a given data source. +} +\examples{ +is.delayed(1) +is.delayed(variables("df", "df")) +is.delayed(variables("df")) # Unknown selection +} diff --git a/man/resolver.Rd b/man/resolver.Rd new file mode 100644 index 00000000..108f2f18 --- /dev/null +++ b/man/resolver.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/resolver.R +\name{resolver} +\alias{resolver} +\title{Resolve the specification} +\usage{ +resolver(spec, data) +} +\arguments{ +\item{spec}{A object extraction specification.} + +\item{data}{The qenv where the specification is evaluated.} +} +\value{ +A specification but resolved: the names and selection is the name of the objects (if possible). +} +\description{ +Given the specification of some data to extract find if they are available or not. +The specification for selecting a variable shouldn't depend on the data of said variable. +} +\examples{ +dataset1 <- datasets(where(is.data.frame)) +dataset2 <- datasets(where(is.matrix)) +spec <- c(dataset1, variables("a", "a")) +td <- within(teal.data::teal_data(), { + df <- data.frame(a = as.factor(LETTERS[1:5]), b = letters[1:5]) + df2 <- data.frame(a = LETTERS[1:5], b = 1:5) + m <- matrix() +}) +resolver(list(spec, dataset2), td) +resolver(dataset2, td) +resolver(spec, td) +spec <- c(dataset1, variables("a", where(is.character))) +resolver(spec, td) +} diff --git a/man/types.Rd b/man/types.Rd new file mode 100644 index 00000000..2b47a81a --- /dev/null +++ b/man/types.Rd @@ -0,0 +1,47 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/types.R +\name{Types} +\alias{Types} +\alias{datasets} +\alias{variables} +\alias{mae_colData} +\alias{values} +\title{Type specification} +\usage{ +datasets(choices = tidyselect::everything(), selected = 1) + +variables(choices = tidyselect::everything(), selected = 1) + +mae_colData(choices = tidyselect::everything(), selected = 1) + +values(choices = tidyselect::everything(), selected = 1) +} +\arguments{ +\item{choices}{<\code{\link[dplyr:dplyr_tidy_select]{tidy-select}}> One unquoted expression to be used to pick the choices.} + +\item{selected}{<\code{\link[dplyr:dplyr_tidy_select]{tidy-select}}> One unquoted expression to be used to pick from choices to be selected.} +} +\value{ +An object of the same class as the function with two elements: names the content of x, and select. +} +\description{ +Define how to select and extract data +} +\section{Functions}{ +\itemize{ +\item \code{datasets()}: Specify datasets. + +\item \code{variables()}: Specify variables. + +\item \code{mae_colData()}: Specify colData. + +\item \code{values()}: Specify values. + +}} +\examples{ +datasets() +datasets("A") +c(datasets("A"), datasets("B")) +datasets(where(is.data.frame)) +c(datasets("A"), variables(where(is.numeric))) +} diff --git a/man/update_spec.Rd b/man/update_spec.Rd new file mode 100644 index 00000000..eb750cb4 --- /dev/null +++ b/man/update_spec.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/update_spec.R +\name{update_spec} +\alias{update_spec} +\title{Update a specification} +\usage{ +update_spec(spec, type, value) +} +\arguments{ +\item{spec}{A resolved specification such as one created with datasets and variables.} + +\item{type}{Which type was updated? One of datasets, variables, values.} + +\item{value}{What is the new selection? One that is a valid value for the given type and specification.} +} +\value{ +The specification with restored choices and selection if caused by the update. +} +\description{ +Update the specification for different selection. +} +\examples{ +td <- within(teal.data::teal_data(), { + df <- data.frame( + A = as.factor(letters[1:5]), + Ab = LETTERS[1:5] + ) + df_n <- data.frame( + C = 1:5, + Ab = as.factor(letters[1:5]) + ) +}) +data_frames_factors <- c(datasets(where(is.data.frame)), variables(where(is.factor))) +res <- resolver(data_frames_factors, td) +update_spec(res, "datasets", "df_n") +# update_spec(res, "datasets", "error") +} diff --git a/tests/testthat/test-delayed.R b/tests/testthat/test-delayed.R new file mode 100644 index 00000000..17c2ab8f --- /dev/null +++ b/tests/testthat/test-delayed.R @@ -0,0 +1,12 @@ +test_that("is.delayed works", { + d <- datasets("a") + v <- variables("b") + expect_true(is.delayed(d)) + expect_true(is.delayed(datasets("a", "a"))) + expect_true(is.delayed(v)) + expect_true(is.delayed(variables("b", "b"))) + expect_true(is.delayed(c(d, v))) + expect_false(is.delayed(1)) + da <- datasets(is.data.frame) + expect_true(is.delayed(da)) +}) diff --git a/tests/testthat/test-resolver.R b/tests/testthat/test-resolver.R new file mode 100644 index 00000000..af07bdc5 --- /dev/null +++ b/tests/testthat/test-resolver.R @@ -0,0 +1,186 @@ +f <- function(x) { + head(x, 1) +} + +test_that("resolver datasets works", { + df_head <- datasets("df") + df_first <- datasets("df") + matrices <- datasets(where(is.matrix)) + df_mean <- datasets("df", where(mean)) + median_mean <- datasets(where(median), where(mean)) + td <- within(teal.data::teal_data(), { + df <- data.frame(a = LETTERS[1:5], b = factor(letters[1:5]), c = factor(letters[1:5])) + m <- cbind(b = 1:5, c = 10:14) + m2 <- cbind(a = LETTERS[1:2], b = LETTERS[4:5]) + }) + expect_no_error(resolver(df_head, td)) + expect_no_error(resolver(df_first, td)) + out <- resolver(matrices, td) + expect_length(out$datasets$selected, 1L) # Because we use 1 + expect_error(expect_warning(resolver(df_mean, td))) + expect_error(resolver(median_mean, td)) +}) + +test_that("resolver variables works", { + df <- datasets("df") + matrices <- datasets(where(is.matrix)) + data_frames <- datasets(where(is.data.frame)) + var_a <- variables("a") + factors <- variables(where(is.factor)) + factors_head <- variables(where(is.factor), where(function(x) { + head(x, 1) + })) + var_matrices_head <- variables(where(is.matrix), where(function(x) { + head(x, 1) + })) + td <- within(teal.data::teal_data(), { + df <- data.frame(a = LETTERS[1:5], b = factor(letters[1:5]), c = factor(letters[1:5])) + m <- cbind(b = 1:5, c = 10:14) + m2 <- cbind(a = LETTERS[1:2], b = LETTERS[4:5]) + }) + + expect_no_error(resolver(c(df, var_a), td)) + expect_no_error(resolver(c(df, factors), td)) + expect_error(resolver(c(df, factors_head), td)) + expect_error(resolver(c(df, var_matrices_head), td)) + + expect_error(resolver(c(matrices, var_a), td)) + expect_error(resolver(c(matrices, factors), td)) + expect_error(resolver(c(matrices, factors_head), td)) + expect_error(resolver(c(matrices, var_matrices_head), td)) + + expect_no_error(resolver(c(data_frames, var_a), td)) + expect_no_error(resolver(c(data_frames, factors), td)) + expect_error(resolver(c(data_frames, factors_head), td)) + expect_error(resolver(c(data_frames, var_matrices_head), td)) +}) + +test_that("resolver with missing type works", { + td <- within(teal.data::teal_data(), { + i <- iris + }) + + r <- expect_no_error(resolver(variables(where(is.numeric)), td)) + expect_true(r$variables$selected == "i") +}) + +test_that("resolver values works", { + df <- datasets("df") + matrices <- datasets(where(is.matrix)) + data_frames <- datasets(where(is.data.frame)) + var_a <- variables("a") + factors <- variables(is.factor) + factors_head <- variables(where(is.factor), where(function(x) { + head(x, 1) + })) + var_matrices_head <- variables(where(is.matrix), where(function(x) { + head(x, 1) + })) + val_A <- values("A") + td <- within(teal.data::teal_data(), { + df <- data.frame(a = LETTERS[1:5], b = factor(letters[1:5]), c = factor(letters[1:5])) + m <- cbind(b = 1:5, c = 10:14) + m2 <- cbind(a = LETTERS[1:2], b = LETTERS[4:5]) + }) + expect_no_error(resolver(c(df, var_a, val_A), td)) +}) + +test_that("names and variables are reported", { + td <- within(teal.data::teal_data(), { + df <- data.frame( + A = as.factor(letters[1:5]), + Ab = LETTERS[1:5], + Abc = c(LETTERS[1:4], letters[1]) + ) + df2 <- data.frame( + A = 1:5, + B = 1:5 + ) + m <- matrix() + }) + d_df <- datasets("df") + upper_variables <- variables(where(function(x) { + x == toupper(x) + })) + df_upper_variables <- c(d_df, upper_variables) + expect_error(resolver(df_upper_variables, td)) + # This should select A and Ab: + # A because the name is all capital letters and + # Ab values is all upper case. + # expect_length(out$variables$choices, 2) + v_all_upper <- variables(where(function(x) { + all(x == toupper(x)) + })) + df_all_upper_variables <- c(d_df, v_all_upper) + expect_no_error(out <- resolver(df_all_upper_variables, td)) + expect_no_error(out <- resolver(c(datasets("df2"), v_all_upper), td)) + expect_length(out$variables$choices, 2L) + expect_no_error(out <- resolver(datasets(where(function(x) { + is.data.frame(x) && all(colnames(x) == toupper(colnames(x))) + })), td)) + expect_length(out$datasets$choices, 1L) + expect_no_error(out <- resolver(datasets(where(function(x) { + is.data.frame(x) || any(colnames(x) == toupper(colnames(x))) + })), td)) + expect_length(out$datasets$choices, 2L) +}) + +test_that("update_spec resolves correctly", { + td <- within(teal.data::teal_data(), { + df <- data.frame( + A = as.factor(letters[1:5]), + Ab = LETTERS[1:5] + ) + df_n <- data.frame( + C = 1:5, + Ab = as.factor(letters[1:5]) + ) + }) + data_frames_factors <- c(datasets(where(is.data.frame)), variables(where(is.factor))) + expect_false(is.null(attr(data_frames_factors$datasets$choices, "original"))) + expect_false(is.null(attr(data_frames_factors$datasets$selected, "original"))) + expect_false(is.null(attr(data_frames_factors$variables$choices, "original"))) + expect_false(is.null(attr(data_frames_factors$variables$selected, "original"))) + + expect_no_error(resolver(data_frames_factors, td)) +}) + +test_that("OR specifications resolves correctly", { + td <- within(teal.data::teal_data(), { + df <- data.frame(A = 1:5, B = LETTERS[1:5]) + m <- cbind(A = 1:5, B = 5:10) + }) + var_a <- variables("A") + df_a <- c(datasets(where(is.data.frame)), var_a) + matrix_a <- c(datasets(where(is.matrix)), var_a) + df_or_m_var_a <- list(df_a, matrix_a) + out <- resolver(df_or_m_var_a, td) + expect_true(all(vapply(out, is.specification, logical(1L)))) +}) + +test_that("OR specifications fail correctly", { + td <- within(teal.data::teal_data(), { + df <- data.frame(A = 1:5, B = LETTERS[1:5]) + m <- cbind(A = 1:5, B = 5:10) + }) + var_a <- variables("A") + df_a <- c(datasets(where(is.data.frame)), var_a) + matrix_a <- c(datasets(where(is.matrix)), var_a) + df_or_m_var_a <- list(df_a, matrix_a) + out <- resolver(df_or_m_var_a, td) + expect_error(update_spec(out, "variables", "B")) +}) + +test_that("OR update_spec filters specifications", { + td <- within(teal.data::teal_data(), { + df <- data.frame(A = 1:5, B = LETTERS[1:5]) + m <- cbind(A = 1:5, B = 5:10) + }) + var_a <- variables("A") + df_a <- c(datasets(where(is.data.frame)), var_a) + matrix_a <- c(datasets(where(is.matrix)), var_a) + df_or_m_var_a <- list(df_a, matrix_a) + resolved <- resolver(df_or_m_var_a, td) + # The second option is not possible to have it as df + expect_error(update_spec(resolved, "datasets", "df")) +}) diff --git a/tests/testthat/test-types.R b/tests/testthat/test-types.R new file mode 100644 index 00000000..dfb4dc0c --- /dev/null +++ b/tests/testthat/test-types.R @@ -0,0 +1,45 @@ +test_that("datasets", { + expect_no_error(dataset0 <- datasets("df", "df")) + expect_no_error(dataset1 <- datasets("df")) + expect_no_error(dataset2 <- datasets(where(is.matrix))) + expect_no_error(dataset3 <- datasets(where(is.data.frame))) +}) + +test_that("variables", { + expect_no_error(var0 <- variables("a", "a")) + expect_no_error(var1 <- variables("a")) + expect_no_error(var2 <- variables(where(is.factor))) + # Allowed to specify whatever we like, it is not until resolution that this raises errors + expect_no_error(var3 <- variables(where(is.factor), where(function(x) { + head(x, 1) + }))) + expect_no_error(var4 <- variables(where(is.matrix), where(function(x) { + head(x, 1) + }))) +}) + +test_that("raw combine of types", { + expect_equal(c(datasets("df")), datasets("df")) + expect_length(c(datasets("df"), variables("df")), 2L) + expect_length(c(datasets("df"), variables("df"), values("df")), 3L) +}) + +test_that("combine types", { + expect_no_error(c( + datasets(where(is.data.frame), selected = "df1"), + variables(where(is.numeric)) + )) +}) + +test_that("values", { + expect_no_error(val0 <- values("a", "a")) + expect_no_error(val1 <- values("a")) + expect_no_error(val2 <- values(where(is.factor))) + # Allowed to specify whatever we like, it is not until resolution that this raises errors + expect_no_error(val3 <- values(where(is.factor), function(x) { + head(x, 1) + })) + expect_no_error(val4 <- values(where(is.matrix), function(x) { + head(x, 1) + })) +})