From 7c2c7cc00409bbe43e6a24ee144bca17ff92e1a7 Mon Sep 17 00:00:00 2001 From: Cervangirard <37626302+Cervangirard@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:08:04 +0100 Subject: [PATCH] Feat/add md ds to connectors (#55) * feat: add datasource feature * doc: style the pkg & update docs * fix: print method for object cnts_ & namespace/import --------- Signed-off-by: Cervangirard <37626302+Cervangirard@users.noreply.github.com> --- NAMESPACE | 14 +- NEWS.md | 4 + R/connect.R | 71 ++--- R/connect_utils.R | 2 - R/connector-package.R | 1 + R/connector.R | 8 +- R/connectors.R | 98 ++++++- R/conts_datasources.R | 252 ++++++++++++++++++ R/dbi_backend_tools.R | 2 +- R/fs.R | 1 - R/fs_backend_tools.R | 2 +- R/fs_read.R | 1 - R/generic_backend.R | 10 +- R/utils_files.R | 3 - dev/connector_config_yaml.qmd | 42 ++- dev/meta_param.R | 217 ++++++++++++++- man/connectors_to_datasources.Rd | 18 ++ man/datasources.Rd | 32 +++ man/extract_and_process_params.Rd | 20 ++ man/extract_base_info.Rd | 20 ++ man/extract_function_info.Rd | 22 ++ man/get_r6_specific_info.Rd | 20 ++ man/get_standard_specific_info.Rd | 20 ++ man/nested_connectors.Rd | 17 ++ man/process_ellipsis_params.Rd | 18 ++ man/process_named_params.Rd | 20 ++ man/transform_as_backend.Rd | 21 ++ man/transform_as_datasources.Rd | 20 ++ tests/testthat/_snaps/connector.md | 2 +- tests/testthat/{ => configs}/config_json.json | 0 tests/testthat/helper.R | 1 + tests/testthat/test-connect.R | 10 +- tests/testthat/test-connector.R | 2 +- tests/testthat/test-extract_function_info.R | 55 ++++ tests/testthat/test-transform_function.R | 51 ++++ 35 files changed, 1010 insertions(+), 87 deletions(-) create mode 100644 R/conts_datasources.R create mode 100644 man/connectors_to_datasources.Rd create mode 100644 man/datasources.Rd create mode 100644 man/extract_and_process_params.Rd create mode 100644 man/extract_base_info.Rd create mode 100644 man/extract_function_info.Rd create mode 100644 man/get_r6_specific_info.Rd create mode 100644 man/get_standard_specific_info.Rd create mode 100644 man/nested_connectors.Rd create mode 100644 man/process_ellipsis_params.Rd create mode 100644 man/process_named_params.Rd create mode 100644 man/transform_as_backend.Rd create mode 100644 man/transform_as_datasources.Rd rename tests/testthat/{ => configs}/config_json.json (100%) create mode 100644 tests/testthat/test-extract_function_info.R create mode 100644 tests/testthat/test-transform_function.R diff --git a/NAMESPACE b/NAMESPACE index 06cdcb1..c7ba534 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,7 +9,9 @@ S3method(download_cnt,default) S3method(list_content_cnt,connector_dbi) S3method(list_content_cnt,connector_fs) S3method(list_content_cnt,default) +S3method(print,cnts_datasources) S3method(print,connectors) +S3method(print,nested_connectors) S3method(read_cnt,connector_dbi) S3method(read_cnt,connector_fs) S3method(read_cnt,default) @@ -48,9 +50,11 @@ export(connector_dbi) export(connector_fs) export(connectors) export(create_directory_cnt) +export(datasources) export(disconnect_cnt) export(download_cnt) export(list_content_cnt) +export(nested_connectors) export(read_cnt) export(read_ext) export(read_file) @@ -61,13 +65,5 @@ export(upload_cnt) export(write_cnt) export(write_ext) export(write_file) -importFrom(R6,R6Class) -importFrom(checkmate,assert_list) -importFrom(cli,cli_abort) -importFrom(cli,cli_alert) -importFrom(cli,cli_code) -importFrom(cli,cli_inform) -importFrom(cli,cli_text) -importFrom(jsonlite,read_json) +import(rlang) importFrom(options,define_option) -importFrom(rlang,set_names) diff --git a/NEWS.md b/NEWS.md index 6839aa7..ad19c52 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,11 @@ + # connector dev +- Connectors constructor builds the datasources attribute +- Create a new class for nested connectors objects, "nested_connectors" - Add README and vignette on how to extend connector + # connector 0.0.4 (2024-12-03) ### Migration: diff --git a/R/connect.R b/R/connect.R index 8d91ea9..b3a86a4 100644 --- a/R/connect.R +++ b/R/connect.R @@ -93,18 +93,19 @@ connect <- function(config = "_connector.yml", metadata = NULL, datasource = NUL names(config) <- purrr::map(config, "name") cnts <- config |> purrr::map(\(x) connect(x, metadata, datasource, set_env)) - return(do.call(connectors, cnts)) + + return(do.call(nested_connectors, cnts)) } # Replace metadata if needed - if(!is.null(metadata)){ + if (!is.null(metadata)) { zephyr::msg( c("Replace some metadata informations...") ) config[["metadata"]] <- change_to_new_metadata( - old_metadata = config[["metadata"]], - new_metadata = metadata - ) + old_metadata = config[["metadata"]], + new_metadata = metadata + ) } connections <- config |> @@ -113,7 +114,7 @@ connect <- function(config = "_connector.yml", metadata = NULL, datasource = NUL filter_config(datasource = datasource) |> connect_from_config() - if(logging){ + if (logging) { rlang::check_installed("connector.logger") connections <- connector.logger::add_logs(connections) } @@ -128,20 +129,31 @@ connect_from_config <- function(config) { purrr::map(create_connection) |> rlang::set_names(purrr::map_chr(config$datasources, list("name", 1))) + ## clean datasources + # unlist name of datasource + for(i in seq_along(config$datasources)){ + config$datasources[[i]]$name <- config$datasources[[i]]$name[[1]] + } + + connections$datasources <- as_datasources(config["datasources"]) + do.call(what = connectors, args = connections) } #' @noRd -info_config <- function(config){ - msg_ <- c(">" = "{.strong {config$name}}", - "*" = "{config$backend$type}", - "*" = "{config$backend[!names(config$backend) %in% 'type']}" +info_config <- function(config) { + msg_ <- c( + ">" = "{.strong {config$name}}", + "*" = "{config$backend$type}", + "*" = "{config$backend[!names(config$backend) %in% 'type']}" ) cli::cat_rule() zephyr::msg( - c("Connection to:", - msg_), + c( + "Connection to:", + msg_ + ), msg_fun = cli::cli_bullets ) } @@ -150,19 +162,18 @@ info_config <- function(config){ #' @param config [list] The configuration of a single connection #' @noRd create_connection <- function(config) { - info_config(config) switch(config$backend$type, - "connector_fs" = { - create_backend_fs(config$backend) - }, - "connector_dbi" = { - create_backend_dbi(config$backend) - }, - { - create_backend(config$backend) - } + "connector_fs" = { + create_backend_fs(config$backend) + }, + "connector_dbi" = { + create_backend_dbi(config$backend) + }, + { + create_backend(config$backend) + } ) } @@ -305,20 +316,20 @@ assert_config <- function(config, env = parent.frame()) { var <- paste0("datasources", y) checkmate::assert_list(x, .var.name = var, add = val) checkmate::assert_names(names(x), - type = "unique", must.include = c("name", "backend"), - .var.name = var, add = val + type = "unique", must.include = c("name", "backend"), + .var.name = var, add = val ) checkmate::assert_character(x[["name"]], - len = 1, - .var.name = paste0(var, ".name"), add = val + len = 1, + .var.name = paste0(var, ".name"), add = val ) checkmate::assert_list(x[["backend"]], - names = "unique", - .var.name = paste0(var, ".backend"), add = val + names = "unique", + .var.name = paste0(var, ".backend"), add = val ) checkmate::assert_character(x[["backend"]][["type"]], - len = 1, - .var.name = paste0(var, ".backend.type"), add = val + len = 1, + .var.name = paste0(var, ".backend.type"), add = val ) } ) diff --git a/R/connect_utils.R b/R/connect_utils.R index 47ec5da..f84d2f7 100644 --- a/R/connect_utils.R +++ b/R/connect_utils.R @@ -3,8 +3,6 @@ #' @param old_metadata [list] a list of element to be replace #' @param new_metadata [list] a list of element to replace old's #' -#' @importFrom checkmate assert_list -#' #' @return [list] a updated list with new data #' @noRd change_to_new_metadata <- function(old_metadata, new_metadata) { diff --git a/R/connector-package.R b/R/connector-package.R index a65cf64..637bc24 100644 --- a/R/connector-package.R +++ b/R/connector-package.R @@ -1,4 +1,5 @@ #' @keywords internal +#' @import rlang "_PACKAGE" ## usethis namespace: start diff --git a/R/connector.R b/R/connector.R index 07656e3..fb3b79d 100644 --- a/R/connector.R +++ b/R/connector.R @@ -38,7 +38,6 @@ #' #' read_cnt(cnt_my_class) #' -#' @importFrom R6 R6Class #' @export connector <- R6::R6Class( @@ -122,7 +121,12 @@ print_cnt <- function(connector_object) { which() |> utils::head(1) - specs <- if(R6::is.R6(connector_object)) {connector_object$.__enclos_env__$.__active__} else {NULL} + specs <- if (R6::is.R6(connector_object)) { + connector_object$.__enclos_env__$.__active__ + } else { + NULL + } + if (length(specs) == 0) { specs <- NULL } diff --git a/R/connectors.R b/R/connectors.R index 19e14d9..9043d4f 100644 --- a/R/connectors.R +++ b/R/connectors.R @@ -27,15 +27,34 @@ #' @export connectors <- function(...) { x <- rlang::list2(...) + ds_ <- x[["datasources"]] + + if (!is.null(ds_) & !inherits(ds_, "cnts_datasources")) { + cli::cli_abort("'datasources' is a reserved name. It cannot be used as a name for a data source.") + } + + if (is.null(ds_)) { + cnts <- substitute(rlang::list2(...)) + datasources <- connectors_to_datasources(cnts) + } else { + datasources <- ds_ + } + checkmate::assert_list(x = x, names = "named") structure( - x, - class = c("connectors") + x[names(x) != "datasources"], + class = c("connectors"), + datasources = datasources ) } #' @export print.connectors <- function(x, ...) { + print_connectors(x, ...) +} + +#' @noRd +print_connectors <- function(x, ...) { classes <- x |> lapply(\(x) class(x)[[1]]) |> unlist() @@ -55,3 +74,78 @@ print.connectors <- function(x, ...) { ) return(invisible(x)) } + +#' @export +print.cnts_datasources <- function(x, ...) { + cli::cli_h1("Datasources") + + for(ds in x[["datasources"]]) { + cli::cli_h2(ds$name) + cli::cli_ul() + cli::cli_li("Backend Type: {.val {ds$backend$type}}") + for (param_name in names(ds$backend)[names(ds$backend) != "type"]) { + cli::cli_li("{param_name}: {.val {ds$backend[[param_name]]}}") + } + cli::cli_end() + cli::cli_end() + } + + return(x) +} + +#' @noRd +as_datasources <- function(...) { + structure( + ..., + class = "cnts_datasources" + ) +} + +#' Extract data sources from connectors +#' +#' This function extracts the "datasources" attribute from a connectors object. +#' +#' @param connectors An object containing connectors with a "datasources" attribute. +#' +#' @return An object containing the data sources extracted from the "datasources" attribute. +#' +#' @details +#' The function uses the `attr()` function to access the "datasources" attribute +#' of the `connectors` object. It directly returns this attribute without any +#' modification. +#' +#' @examples +#' # Assume we have a 'my_connectors' object with a 'datasources' attribute +#' my_connectors <- list() +#' attr(my_connectors, "datasources") <- list(source1 = "data1", source2 = "data2") +#' +#' # Using the function +#' result <- datasources(my_connectors) +#' print(result) +#' +#' @export +datasources <- function(connectors) { + ds <- attr(connectors, "datasources") + ds +} + +#' Create a nested connectors object +#' +#' This function creates a nested connectors object from the provided arguments. +#' +#' @param ... Any number of connectors object. +#' +#' @return A list with class "nested_connectors" containing the provided arguments. +#' @export +nested_connectors <- function(...) { + x <- rlang::list2(...) + structure( + x, + class = c("nested_connectors") + ) +} + +#' @export +print.nested_connectors <- function(x, ...) { + print_connectors(x, ...) +} diff --git a/R/conts_datasources.R b/R/conts_datasources.R new file mode 100644 index 0000000..f6ab44e --- /dev/null +++ b/R/conts_datasources.R @@ -0,0 +1,252 @@ +#' Transform Test Data to Datasources +#' +#' This function takes a list of function calls, extracts their information, +#' transforms them into backends, and finally wraps them in a datasources structure. +#' +#' @param data A list of function calls as expressions. +#' @return A list with a 'datasources' element containing the transformed backends. +#' +#' +connectors_to_datasources <- function(data) { + data[-1] |> + as.list() |> + purrr::imap(~ { + deparse(.x) |> + extract_function_info() |> + transform_as_backend(.y) + }) |> + unname() |> + transform_as_datasources() +} + +#' Transform Clean Function Info to Backend Format +#' +#' This function takes the output of `extract_function_info` and transforms it +#' into a backend format suitable for further processing or API integration. +#' +#' @param infos A list with class "clean_fct_info", typically the output of `extract_function_info`. +#' @param name A character string representing the name to be assigned to the backend. +#' +#' @return A list representing the backend, with 'name' and 'backend' components or an error if the input is not of class "clean_fct_info". +#' +#' @keywords internal +transform_as_backend <- function(infos, name) { + if (!inherits(infos, "clean_fct_info")) { + cli::cli_abort("You should use the extract_function_info function before calling this function") + } + + bk <- list( + name = name, + backend = list( + type = paste0(infos$package_name, "::", infos$function_name) + ) + ) + + bk$backend[names(infos$parameters)] <- infos$parameters + + return(bk) +} + +#' Transform Multiple Backends to Datasources Format +#' +#' This function takes a list of backends (typically created by `transform_as_backend`) +#' and wraps them in a 'datasources' list. This is useful for creating a structure +#' that represents multiple data sources or backends. +#' +#' @param bks A list of backends, each typically created by `transform_as_backend`. +#' +#' @return A list with a single 'datasources' element containing all input backends. +#' +#' @keywords internal +transform_as_datasources <- function(bks) { + as_datasources( + list( + datasources = bks + ) + ) +} + +#' Extract Function Information +#' +#' This function extracts detailed information about a function call, +#' including its name, package, parameters, and whether it's an R6 class constructor. +#' +#' @param func_string A character string representing the function call. +#' @return A list with class "clean_fct_info" containing: +#' \item{function_name}{The name of the function or R6 class} +#' \item{parameters}{A list of parameters passed to the function} +#' \item{is_r6}{A boolean indicating whether it's an R6 class constructor} +#' \item{package_name}{The name of the package containing the function} +extract_function_info <- function(func_string) { + # Parse the function string into an expression + + expr <- parse_expr(func_string) + full_func_name <- expr_text(expr[[1]]) + + # Check if it's an R6 class constructor + is_r6 <- endsWith(full_func_name, "$new") + + # Extract basic information (package and function names) + base_info <- extract_base_info(full_func_name, is_r6) + + # Get specific details based on whether it's R6 or standard function + specific_info <- if (is_r6) { + get_r6_specific_info(base_info$package_name, base_info$func_name) + } else { + get_standard_specific_info(base_info$package_name, base_info$func_name) + } + + # Extract and process the parameters from the function call + params <- extract_and_process_params(expr, specific_info$formal_args) + + # Construct and return the final result + structure( + purrr::compact( + list( + function_name = base_info$func_name, + parameters = params, + is_r6 = is_r6, + package_name = base_info$package_name + ) + ), + class = "clean_fct_info" + ) +} + +#' Extract Base Information +#' +#' Extracts the package name and function/class name from the full function name. +#' +#' @param full_func_name The full name of the function (potentially including package). +#' @param is_r6 Boolean indicating whether it's an R6 class constructor. +#' @return A list with package_name and func_name. +#' @keywords internal +extract_base_info <- function(full_func_name, is_r6) { + # Check if the function name includes a package specification + if (grepl("::", full_func_name, fixed = TRUE)) { + parts <- strsplit(full_func_name, "::")[[1]] + package_name <- parts[1] + func_name <- parts[2] + } else { + package_name <- NULL + func_name <- full_func_name + } + + # For R6, remove the "$new" suffix from the function name + if (is_r6) { + func_name <- sub("\\$new$", "", func_name) + } + + # If package name is not specified, try to determine it + if (is.null(package_name)) { + package_name <- if (is_r6) { + # For R6, get the package from the class's parent environment + getNamespaceName(get(func_name)$parent_env) + } else { + # For standard functions, get the package from the function's environment + getNamespaceName(environment(get(func_name))) + } + } + + list(package_name = package_name, func_name = func_name) +} + +#' Get Standard Function Specific Information +#' +#' Retrieves the function object and its formal arguments for standard functions. +#' +#' @param package_name The name of the package containing the function. +#' @param func_name The name of the function. +#' @return A list with the function object and its formal arguments. +#' @keywords internal +get_standard_specific_info <- function(package_name, func_name) { + func <- getExportedValue(package_name, func_name) + formal_args <- names(formals(func)) + list(func = func, formal_args = formal_args) +} + +#' Get R6 Class Specific Information +#' +#' Retrieves the initialize method and its formal arguments for R6 classes. +#' +#' @param package_name The name of the package containing the R6 class. +#' @param func_name The name of the R6 class. +#' @return A list with the initialize method and its formal arguments. +#' @keywords internal +get_r6_specific_info <- function(package_name, func_name) { + class_obj <- getExportedValue(package_name, func_name) + init_func <- class_obj$public_methods$initialize + formal_args <- names(formals(init_func)) + list(func = init_func, formal_args = formal_args) +} + +#' Extract and Process Parameters +#' +#' Extracts parameters from the function call and processes them. +#' +#' @param expr The parsed expression of the function call. +#' @param formal_args The formal arguments of the function. +#' @return A list of processed parameters. +#' @keywords internal +#' +extract_and_process_params <- function(expr, formal_args) { + # Extract parameters from the function call + params <- call_args(expr) + + # Convert symbols to strings and evaluate expressions + params <- purrr::map(params, ~ { + if (is_symbol(.x)) { + as.character(.x) + } else if(is_call(.x)){ + as.character(deparse(.x)) + } else { + as.character(.x) + } + }) + + # Process parameters based on whether the function uses ... or not + if (formal_args[1] == "...") { + process_ellipsis_params(params) + } else { + process_named_params(params, formal_args) + } +} + +#' Process Parameters for Functions with Ellipsis +#' +#' Handles parameter processing for functions that use ... in their arguments. +#' +#' @param params The extracted parameters from the function call. +#' @return A list of processed parameters. +#' @keywords internal +process_ellipsis_params <- function(params) { + unnamed_args <- params[names(params) == ""] + named_args <- params[names(params) != ""] + unnamed_args <- unlist(unnamed_args) + c(named_args, list("..." = unnamed_args)) +} + +#' Process Named Parameters +#' +#' Handles parameter processing for functions with named arguments. +#' +#' @param params The extracted parameters from the function call. +#' @param formal_args The formal arguments of the function. +#' @return A list of processed parameters. +#' @keywords internal +process_named_params <- function(params, formal_args) { + unnamed_args <- params[names(params) == ""] + named_args <- params[names(params) != ""] + + # Match unnamed arguments to their formal argument names + if (length(unnamed_args) != 0) { + u_formal_args <- formal_args[!formal_args %in% names(params)] + u_formal_args <- u_formal_args[u_formal_args != "..."] + u_formal_args <- u_formal_args[1:length(unnamed_args)] + names(unnamed_args) <- u_formal_args + } else { + unnamed_args <- NULL + } + + c(named_args, unnamed_args) +} diff --git a/R/dbi_backend_tools.R b/R/dbi_backend_tools.R index 1b547b6..b1837a8 100644 --- a/R/dbi_backend_tools.R +++ b/R/dbi_backend_tools.R @@ -5,7 +5,7 @@ #' @noRd #' @examples #' yaml_file <- system.file("config", "default_config.yml", package = "connector") -#' yaml_content <- yaml::read_yaml(yaml_file, eval.expr=TRUE) +#' yaml_content <- yaml::read_yaml(yaml_file, eval.expr = TRUE) #' #' only_one <- yaml_content[["datasources"]][[2]][["backend"]] #' diff --git a/R/fs.R b/R/fs.R index b6a22e1..6433d7a 100644 --- a/R/fs.R +++ b/R/fs.R @@ -47,7 +47,6 @@ connector_fs <- R6::R6Class( #' @param extra_class [character] Extra class to be added #' Checked using [checkmate::assert_directory_exists]. initialize = function(path, extra_class = NULL) { - private$.path <- path super$initialize(extra_class = extra_class) }, diff --git a/R/fs_backend_tools.R b/R/fs_backend_tools.R index 9d40d22..30eaa5d 100644 --- a/R/fs_backend_tools.R +++ b/R/fs_backend_tools.R @@ -5,7 +5,7 @@ #' @noRd #' @examples #' yaml_file <- system.file("config", "default_config.yml", package = "connector") -#' yaml_content <- yaml::read_yaml(yaml_file, eval.expr=TRUE) +#' yaml_content <- yaml::read_yaml(yaml_file, eval.expr = TRUE) #' #' only_one <- yaml_content[["datasources"]][[1]][["backend"]] #' diff --git a/R/fs_read.R b/R/fs_read.R index eb1b5cd..34361c7 100644 --- a/R/fs_read.R +++ b/R/fs_read.R @@ -123,7 +123,6 @@ read_ext.yaml <- read_ext.yml #' * `json`: [jsonlite::read_json()] #' #' @rdname read_file -#' @importFrom jsonlite read_json #' @export read_ext.json <- function(path, ...) { jsonlite::read_json(path = path, ...) diff --git a/R/generic_backend.R b/R/generic_backend.R index dd6c6a1..31eb533 100644 --- a/R/generic_backend.R +++ b/R/generic_backend.R @@ -6,7 +6,7 @@ #' @noRd #' @examples #' yaml_file <- system.file("config", "example_for_generic.yml", package = "connector") -#' yaml_content <- yaml::read_yaml(yaml_file, eval.expr=TRUE) +#' yaml_content <- yaml::read_yaml(yaml_file, eval.expr = TRUE) #' #' only_one <- yaml_content[["datasources"]][[1]][["backend"]] #' @@ -73,9 +73,11 @@ try_connect <- function(connect_fct, params_from_user) { connect_ <- try(do.call(connect_fct, params_from_user), silent = TRUE) if (inherits(connect_, "try-error")) { - c("Problem in connection to the backend:", - connect_) |> - cli::cli_abort() + c( + "Problem in connection to the backend:", + connect_ + ) |> + cli::cli_abort() } return(connect_) diff --git a/R/utils_files.R b/R/utils_files.R index c71ef74..ddc9078 100644 --- a/R/utils_files.R +++ b/R/utils_files.R @@ -54,8 +54,6 @@ assert_ext <- function(ext, method) { #' Error extension #' Function to call when no method is found for the extension -#' @importFrom cli cli_abort -#' @importFrom rlang set_names #' @noRd error_extension <- function() { ext_supp <- supported_fs() |> @@ -70,7 +68,6 @@ error_extension <- function() { } #' Example for creating a new method for reading files -#' @importFrom cli cli_inform cli_alert cli_code cli_text #' @noRd #' @examples #' example_read_ext() diff --git a/dev/connector_config_yaml.qmd b/dev/connector_config_yaml.qmd index 3f5b468..4c76e67 100644 --- a/dev/connector_config_yaml.qmd +++ b/dev/connector_config_yaml.qmd @@ -17,30 +17,30 @@ Read and understand the yaml file ```{r} yaml_file <- system.file("config", "default_config.yml", package = "connector") -yaml_content <- yaml::read_yaml(yaml_file, eval.expr=TRUE) +yaml_content <- yaml::read_yaml(yaml_file, eval.expr = TRUE) class(yaml_content) print(yaml_content) -extract_element <- function(yaml_content, element){ - yaml_content[[element]] +extract_element <- function(yaml_content, element) { + yaml_content[[element]] } -extract_metadata <- function(yaml_content){ - extract_element(yaml_content, "metadata") +extract_metadata <- function(yaml_content) { + extract_element(yaml_content, "metadata") } -extract_connections <- function(yaml_content){ - extract_element(yaml_content, "connections") +extract_connections <- function(yaml_content) { + extract_element(yaml_content, "connections") } -extract_datasources <- function(yaml_content){ - extract_element(yaml_content, "datasources") +extract_datasources <- function(yaml_content) { + extract_element(yaml_content, "datasources") } -extract_backends <- function(yaml_content){ - extract_element(yaml_content, "backend") +extract_backends <- function(yaml_content) { + extract_element(yaml_content, "backend") } extract_datasources(yaml_content) @@ -60,8 +60,7 @@ my_backend <- only_one |> #### Create the backend for fs -test <- create_backend_fs(yaml_content, only_one$backend, only_one$con ) - +test <- create_backend_fs(yaml_content, only_one$backend, only_one$con) ``` #### Second try for connector_db @@ -71,7 +70,7 @@ test <- create_backend_fs(yaml_content, only_one$backend, only_one$con ) only_dbi_backend <- extract_connections(yaml_content)[[3]] ## Extract fct -my_backend <- only_dbi_backend +my_backend <- only_dbi_backend #### Create the backend for dbi @@ -79,7 +78,7 @@ backend <- only_dbi_backend$backend name <- only_dbi_backend$con -test_dbi <- create_backend_dbi(yaml_content, only_dbi_backend$backend, only_dbi_backend$con ) +test_dbi <- create_backend_dbi(yaml_content, only_dbi_backend$backend, only_dbi_backend$con) class(test_dbi$general_dbi) ``` @@ -89,7 +88,6 @@ class(test_dbi$general_dbi) ```{r} - ``` @@ -99,15 +97,15 @@ class(test_dbi$general_dbi) ```{r} # read yaml file yaml_file <- system.file("config", "default_config.yml", package = "connector") -yaml_content <- yaml::read_yaml(yaml_file, eval.expr=TRUE) +yaml_content <- yaml::read_yaml(yaml_file, eval.expr = TRUE) # create the connections connect <- connect_from_yaml(yaml_content) -print.Connector <- function(x){ - print("Connector object") - print(names(x)) +print.Connector <- function(x) { + print("Connector object") + print(names(x)) } print(connect) @@ -115,14 +113,14 @@ print(connect) ## write and read for a system file connect$adam$read("adsl.csv") -connect$adam$write(data.frame(a=1:10, b=11:20), "example.csv") +connect$adam$write(data.frame(a = 1:10, b = 11:20), "example.csv") ## write and read for a dbi connection connect$sdtm$write(iris, "iris") connect$sdtm$read("iris") -## Manipulate a table with the database +## Manipulate a table with the database connect$sdtm$tbl("iris") |> dplyr::filter(Sepal.Length > 5) diff --git a/dev/meta_param.R b/dev/meta_param.R index 1ce41a7..22e777b 100644 --- a/dev/meta_param.R +++ b/dev/meta_param.R @@ -2,15 +2,22 @@ pkgload::load_all() ## if metadata exists -yaml_file <- read_file(system.file("config", "default_config.yml", package = "connector"), eval.expr = TRUE) + +yaml_file <- system.file("config", "default_config.yml", package = "connector") sans_metadata <- list(datasources = list(ok = "test")) old_metadata <- yaml_file[["metadata"]] +test <- connect(config = yaml_file, logging = FALSE) +test$adam$list_content_cnt() +test$adam ## example of metadata list +connectors( + adam = connector_dbi$new() +) new_metadata <- list( trial = "test", @@ -36,4 +43,210 @@ new_metadata <- list( something_new = "ok" ) -config["metadata"] <- change_to_new_md(old_metadata) \ No newline at end of file +config["metadata"] <- change_to_new_md(old_metadata) + +## Try to add some extra info + +test <- substitute(list( + adam = connector_fs$new(path = "dev"), + adam2 = connector_fs$new("dev") +) +) +as.character(test[2]) + +test2 <- as.character(test) +names(test2) <- names(test) +test2 + +clean_test2 <- test2[-1] +as.list(clean_test2) + +ok <- "connector_fs$new(path = \"dev\")" +func_string <- "connector_fs$new(extra_class = \"dev\" ,\"dev\")" + +library(rlang) +library(purrr) + +#' Extrait les informations d'une fonction à partir d'une chaîne de caractères +#' +#' @param func_string La chaîne de caractères représentant l'appel de fonction +#' @return Une liste contenant les informations extraites de la fonction +extract_function_info <- function(func_string) { + # Convertir la chaîne en expression + expr <- parse_expr(func_string) + + # Extraire le nom complet de la fonction + full_func_name <- expr_text(expr[[1]]) + # Séparer le package et le nom de la fonction + if (grepl("::", full_func_name, fixed = TRUE)) { + parts <- strsplit(full_func_name, "::")[[1]] + package_name <- parts[1] + func_name <- parts[2] + } else { + package_name <- NULL + func_name <- full_func_name + } + + # Vérifier si c'est une fonction R6 + is_r6 <- endsWith(func_name, "$new") + if (is_r6) { + func_name <- sub("\\$new$", "", func_name) + } + + if(is.null(package_name)){ + if(is_r6){ + package_name <- getNamespaceName(get(func_name)$parent_env) + }else{ + package_name <- getNamespaceName(environment(get(func_name))) + } + } + + # Obtenir la fonction + func <- getExportedValue(package_name, func_name) + + + # Obtenir les arguments formels + if(is_r6){ + formal_args <- names(formals(func$public_methods$initialize)) + }else{ + formal_args <- names(formals(func)) + } + + + # Extraire les paramètres de l'appel + params <- call_args(expr) + + # Convertir les symboles en chaînes et évaluer les expressions + params <- map(params, ~ if(is_symbol(.x)) as_string(.x) else eval_tidy(.x)) + + # Nommer les paramètres non nommés selon l'ordre des arguments + if(formal_args[1] == "..."){ + unnamed_args <- params[names(params) == ""] + named_args <- params[names(params) != ""] + unnamed_args <- unlist(unnamed_args) + unnamed_args <- list("..." = unnamed_args) + }else{ + unnamed_args <- params[names(params) == ""] + named_args <- params[names(params) != ""] + if(length(unnamed_args) != 0){ + u_formal_args <- formal_args[!formal_args %in% names(params)] + u_formal_args <- u_formal_args[u_formal_args != "..."] + u_formal_args <- u_formal_args[1:length(unnamed_args)] + names(unnamed_args) <- u_formal_args + }else{ + unnamed_args <- NULL + } + } + + params <- c(named_args, unnamed_args) + + # Créer et retourner la liste résultante + structure( + compact( + list( + function_name = func_name, + parameters = params, + is_r6 = is_r6, + package_name = package_name + ) +), class = "clean_fct_info") + +} + +func_string <- "ggplot2::ggplot(mapping = ggplot2::aes(x = Species, y = Sepal.Length), data = iris)" +print(result1) + +func_string <- 'base::paste("Hello", sep = " ", "World")' +print(result2) +as.character(rlang::parse_expr(func_string)[[1]][2]) + +func_string <- 'mean(c(1, 2, 3), na.rm = TRUE)' +print(result4) + +func_string <- "connector_fs$new(extra_class = \"dev\" ,\"dev\")" + + +infos <- extract_function_info(func_string = func_string) + +transform_as_backend <- function(infos, name) { + if (!inherits(infos, "clean_fct_info")) { + cli::cli_abort("You should use the extract_function_info fct before") + } + + bk <- list( + name = name, + backend = list( + type = paste0(infos$package_name, "::", infos$function_name) + ) +) + bk$backend[names(infos$parameters)] <- infos$parameters + + return(bk) +} + + + +transform_as_datasources <- function(bks){ + list( + datasources = bks + ) +} + +test <- substitute( + list( + adam = connector_fs$new(path = "dev"), + adam2 = connector_fs$new(extra_class = "dev" ,"dev") + ) +) + +test <- test[-1] +test <- as.list(test) + +purrr::imap(test, ~ deparse(.x) |> + extract_function_info() |> + transform_as_backend(.y)) |> + unname() |> + transform_as_datasources() |> + yaml::write_yaml("test.yml") + +connect("test.yml") + +# function to write the datasources attribute to ymal/json/rds +write_datasources <- function(connectors, file, format = "yaml") { + # tesating extension of file + ext <- tools::file_ext(file) + stopifnot(ext %in% c("yaml", "yml", "json", "rds")) + ## using our own write function from connector + dts <- datasources(connectors) + write_file(dts, file) +} + +test <- connect(config = yaml_file, logging = FALSE) + +ok <- datasources(test) + + + +str(datasources(test)) +attr(test, "datasources") + + +write_datasources(connectors = test, file = "test.yml") + +testthat::expect_equal(datasources(test), datasources(test2)) + +## test 2 + +test2 <- connectors( + adam = connector_fs$new(path = "dev"), + adam2 = connector_fs$new(extra_class = "dev" ,"dev") +) + +str(datasources(test2)) +write_datasources(connectors = test2, file = "test2.yml") + + +test_check <- connect(config = "test.yml", logging = FALSE) + +test_check$adam +test_check$adam2 diff --git a/man/connectors_to_datasources.Rd b/man/connectors_to_datasources.Rd new file mode 100644 index 0000000..d58abdd --- /dev/null +++ b/man/connectors_to_datasources.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{connectors_to_datasources} +\alias{connectors_to_datasources} +\title{Transform Test Data to Datasources} +\usage{ +connectors_to_datasources(data) +} +\arguments{ +\item{data}{A list of function calls as expressions.} +} +\value{ +A list with a 'datasources' element containing the transformed backends. +} +\description{ +This function takes a list of function calls, extracts their information, +transforms them into backends, and finally wraps them in a datasources structure. +} diff --git a/man/datasources.Rd b/man/datasources.Rd new file mode 100644 index 0000000..dbeb0c8 --- /dev/null +++ b/man/datasources.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/connectors.R +\name{datasources} +\alias{datasources} +\title{Extract data sources from connectors} +\usage{ +datasources(connectors) +} +\arguments{ +\item{connectors}{An object containing connectors with a "datasources" attribute.} +} +\value{ +An object containing the data sources extracted from the "datasources" attribute. +} +\description{ +This function extracts the "datasources" attribute from a connectors object. +} +\details{ +The function uses the \code{attr()} function to access the "datasources" attribute +of the \code{connectors} object. It directly returns this attribute without any +modification. +} +\examples{ +# Assume we have a 'my_connectors' object with a 'datasources' attribute +my_connectors <- list() +attr(my_connectors, "datasources") <- list(source1 = "data1", source2 = "data2") + +# Using the function +result <- datasources(my_connectors) +print(result) + +} diff --git a/man/extract_and_process_params.Rd b/man/extract_and_process_params.Rd new file mode 100644 index 0000000..bad76c4 --- /dev/null +++ b/man/extract_and_process_params.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{extract_and_process_params} +\alias{extract_and_process_params} +\title{Extract and Process Parameters} +\usage{ +extract_and_process_params(expr, formal_args) +} +\arguments{ +\item{expr}{The parsed expression of the function call.} + +\item{formal_args}{The formal arguments of the function.} +} +\value{ +A list of processed parameters. +} +\description{ +Extracts parameters from the function call and processes them. +} +\keyword{internal} diff --git a/man/extract_base_info.Rd b/man/extract_base_info.Rd new file mode 100644 index 0000000..ae859b6 --- /dev/null +++ b/man/extract_base_info.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{extract_base_info} +\alias{extract_base_info} +\title{Extract Base Information} +\usage{ +extract_base_info(full_func_name, is_r6) +} +\arguments{ +\item{full_func_name}{The full name of the function (potentially including package).} + +\item{is_r6}{Boolean indicating whether it's an R6 class constructor.} +} +\value{ +A list with package_name and func_name. +} +\description{ +Extracts the package name and function/class name from the full function name. +} +\keyword{internal} diff --git a/man/extract_function_info.Rd b/man/extract_function_info.Rd new file mode 100644 index 0000000..c3a6f1f --- /dev/null +++ b/man/extract_function_info.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{extract_function_info} +\alias{extract_function_info} +\title{Extract Function Information} +\usage{ +extract_function_info(func_string) +} +\arguments{ +\item{func_string}{A character string representing the function call.} +} +\value{ +A list with class "clean_fct_info" containing: +\item{function_name}{The name of the function or R6 class} +\item{parameters}{A list of parameters passed to the function} +\item{is_r6}{A boolean indicating whether it's an R6 class constructor} +\item{package_name}{The name of the package containing the function} +} +\description{ +This function extracts detailed information about a function call, +including its name, package, parameters, and whether it's an R6 class constructor. +} diff --git a/man/get_r6_specific_info.Rd b/man/get_r6_specific_info.Rd new file mode 100644 index 0000000..1cdc3fa --- /dev/null +++ b/man/get_r6_specific_info.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{get_r6_specific_info} +\alias{get_r6_specific_info} +\title{Get R6 Class Specific Information} +\usage{ +get_r6_specific_info(package_name, func_name) +} +\arguments{ +\item{package_name}{The name of the package containing the R6 class.} + +\item{func_name}{The name of the R6 class.} +} +\value{ +A list with the initialize method and its formal arguments. +} +\description{ +Retrieves the initialize method and its formal arguments for R6 classes. +} +\keyword{internal} diff --git a/man/get_standard_specific_info.Rd b/man/get_standard_specific_info.Rd new file mode 100644 index 0000000..3e24b76 --- /dev/null +++ b/man/get_standard_specific_info.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{get_standard_specific_info} +\alias{get_standard_specific_info} +\title{Get Standard Function Specific Information} +\usage{ +get_standard_specific_info(package_name, func_name) +} +\arguments{ +\item{package_name}{The name of the package containing the function.} + +\item{func_name}{The name of the function.} +} +\value{ +A list with the function object and its formal arguments. +} +\description{ +Retrieves the function object and its formal arguments for standard functions. +} +\keyword{internal} diff --git a/man/nested_connectors.Rd b/man/nested_connectors.Rd new file mode 100644 index 0000000..1017a3c --- /dev/null +++ b/man/nested_connectors.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/connectors.R +\name{nested_connectors} +\alias{nested_connectors} +\title{Create a nested connectors object} +\usage{ +nested_connectors(...) +} +\arguments{ +\item{...}{Any number of connectors object.} +} +\value{ +A list with class "nested_connectors" containing the provided arguments. +} +\description{ +This function creates a nested connectors object from the provided arguments. +} diff --git a/man/process_ellipsis_params.Rd b/man/process_ellipsis_params.Rd new file mode 100644 index 0000000..351fcf3 --- /dev/null +++ b/man/process_ellipsis_params.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{process_ellipsis_params} +\alias{process_ellipsis_params} +\title{Process Parameters for Functions with Ellipsis} +\usage{ +process_ellipsis_params(params) +} +\arguments{ +\item{params}{The extracted parameters from the function call.} +} +\value{ +A list of processed parameters. +} +\description{ +Handles parameter processing for functions that use ... in their arguments. +} +\keyword{internal} diff --git a/man/process_named_params.Rd b/man/process_named_params.Rd new file mode 100644 index 0000000..995edae --- /dev/null +++ b/man/process_named_params.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{process_named_params} +\alias{process_named_params} +\title{Process Named Parameters} +\usage{ +process_named_params(params, formal_args) +} +\arguments{ +\item{params}{The extracted parameters from the function call.} + +\item{formal_args}{The formal arguments of the function.} +} +\value{ +A list of processed parameters. +} +\description{ +Handles parameter processing for functions with named arguments. +} +\keyword{internal} diff --git a/man/transform_as_backend.Rd b/man/transform_as_backend.Rd new file mode 100644 index 0000000..b269d7f --- /dev/null +++ b/man/transform_as_backend.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{transform_as_backend} +\alias{transform_as_backend} +\title{Transform Clean Function Info to Backend Format} +\usage{ +transform_as_backend(infos, name) +} +\arguments{ +\item{infos}{A list with class "clean_fct_info", typically the output of \code{extract_function_info}.} + +\item{name}{A character string representing the name to be assigned to the backend.} +} +\value{ +A list representing the backend, with 'name' and 'backend' components or an error if the input is not of class "clean_fct_info". +} +\description{ +This function takes the output of \code{extract_function_info} and transforms it +into a backend format suitable for further processing or API integration. +} +\keyword{internal} diff --git a/man/transform_as_datasources.Rd b/man/transform_as_datasources.Rd new file mode 100644 index 0000000..37eed99 --- /dev/null +++ b/man/transform_as_datasources.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{transform_as_datasources} +\alias{transform_as_datasources} +\title{Transform Multiple Backends to Datasources Format} +\usage{ +transform_as_datasources(bks) +} +\arguments{ +\item{bks}{A list of backends, each typically created by \code{transform_as_backend}.} +} +\value{ +A list with a single 'datasources' element containing all input backends. +} +\description{ +This function takes a list of backends (typically created by \code{transform_as_backend}) +and wraps them in a 'datasources' list. This is useful for creating a structure +that represents multiple data sources or backends. +} +\keyword{internal} diff --git a/tests/testthat/_snaps/connector.md b/tests/testthat/_snaps/connector.md index 1eca357..c925b58 100644 --- a/tests/testthat/_snaps/connector.md +++ b/tests/testthat/_snaps/connector.md @@ -4,5 +4,5 @@ connector_obj Message - $test + $test diff --git a/tests/testthat/config_json.json b/tests/testthat/configs/config_json.json similarity index 100% rename from tests/testthat/config_json.json rename to tests/testthat/configs/config_json.json diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index 212dfa7..96c239b 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -2,3 +2,4 @@ yaml_file <- system.file("config", "default_config.yml", package = "connector") yaml_file_env <- system.file("config", "test_env_config.yml", package = "connector") yaml_content_raw <- yaml::read_yaml(yaml_file, eval.expr = TRUE) yaml_content_parsed <- connector:::parse_config(yaml_content_raw) + diff --git a/tests/testthat/test-connect.R b/tests/testthat/test-connect.R index 8a84924..1c63ad4 100644 --- a/tests/testthat/test-connect.R +++ b/tests/testthat/test-connect.R @@ -103,12 +103,11 @@ testthat::test_that("Using a list instead of yaml", { testthat::test_that("Using a json instead of yaml", { # using json file - connect(test_path("config_json.json")) |> + connect(test_path( "configs", "config_json.json")) |> expect_no_error() }) testthat::test_that("Using and uptade metadata", { - test_list <- connect(yaml_content_raw, metadata = list(extra_class = "test_from_metadata")) |> expect_no_error() @@ -129,18 +128,19 @@ test_that("Add logs to connectors object",{ # Don't test the logic of connector.logger because it is not the purpose of connector cnts <- connect(yaml_file, logging = TRUE) - lapply(cnts, function(x){ + + lapply(cnts, function(x) { expect_s3_class(x, "connector") expect_true( all( c("read_cnt", "write_cnt", "remove_cnt", "list_content_cnt") %in% names(x$.__enclos_env__$self) - ) ) + ) expect_equal(class(x$read_cnt), "function") }) - lapply(cnts, function(x){ + lapply(cnts, function(x) { expect_s3_class(x, "connector_logger") }) }) diff --git a/tests/testthat/test-connector.R b/tests/testthat/test-connector.R index 6dcb7f5..ddb116f 100644 --- a/tests/testthat/test-connector.R +++ b/tests/testthat/test-connector.R @@ -1,6 +1,6 @@ test_that("can create Connector object", { connector_obj <- connectors( - test = "test" + test = connector_fs$new(path = tempdir()) ) expect_s3_class(connector_obj, "connectors") diff --git a/tests/testthat/test-extract_function_info.R b/tests/testthat/test-extract_function_info.R new file mode 100644 index 0000000..de84673 --- /dev/null +++ b/tests/testthat/test-extract_function_info.R @@ -0,0 +1,55 @@ +test_that("extract_function_info works for standard functions", { + result <- extract_function_info("stats::lm(formula = y ~ x, data = df)") + + expect_s3_class(result, "clean_fct_info") + expect_equal(result$function_name, "lm") + expect_equal(result$package_name, "stats") + expect_false(result$is_r6) + expect_equal(result$parameters, list(formula = "y ~ x", data = "df")) +}) + +test_that("extract_function_info works for R6 class constructors", { + # Mocking an R6 class for testing + call_ <- deparse(substitute(connector$new(extra_class = "test"))) + result <- extract_function_info(call_) + + expect_s3_class(result, "clean_fct_info") + expect_equal(result$function_name, "connector") + expect_true(result$is_r6) + expect_equal(result$parameters, list(extra_class = "test")) +}) + +test_that("extract_base_info correctly extracts package and function names", { + result1 <- extract_base_info("stats::lm", FALSE) + expect_equal(result1, list(package_name = "stats", func_name = "lm")) + + result2 <- extract_base_info("connector$new", TRUE) + expect_equal(result2, list(package_name = c(name = "connector"), func_name = "connector")) +}) + +test_that("get_standard_specific_info works correctly", { + result <- get_standard_specific_info("stats", "lm") + expect_type(result$func, "closure") + expect_true("formula" %in% result$formal_args) +}) + +test_that("extract_and_process_params handles named and unnamed parameters", { + expr <- rlang::parse_expr("lm(y ~ x, data = df, 42)") + formal_args <- c("formula", "data", "subset", "weights") + + result <- extract_and_process_params(expr, formal_args) + expect_equal(result, list(data = "df", formula = "y ~ x", subset = "42")) +}) + +test_that("process_ellipsis_params handles ... correctly", { + params <- list(x = 1, y = 2, 3, 4) + result <- process_ellipsis_params(params) + expect_equal(result, list(x = 1, y = 2, ... = structure(c(3, 4), names = c("", "")))) +}) + +test_that("process_named_params matches unnamed args correctly", { + params <- list(x = 1, z = 2, 3) + formal_args <- c("x", "y", "z") + result <- process_named_params(params, formal_args) + expect_equal(result, list(x = 1, z = 2, y = 3)) +}) diff --git a/tests/testthat/test-transform_function.R b/tests/testthat/test-transform_function.R new file mode 100644 index 0000000..d547747 --- /dev/null +++ b/tests/testthat/test-transform_function.R @@ -0,0 +1,51 @@ +# Mock function to create a clean_fct_info object +create_mock_clean_fct_info <- function() { + structure( + list( + function_name = "lm", + parameters = list( + formula = "y ~ x", + data = "df" + ), + package_name = "stats" + ), + class = "clean_fct_info" + ) +} + +test_that("transform_as_backend works correctly", { + # Test 1: Correct input + mock_info <- create_mock_clean_fct_info() + result <- transform_as_backend(mock_info, "linear_model") + + expect_type(result, "list") + expect_equal(result$name, "linear_model") + expect_equal(result$backend$type, "stats::lm") + expect_equal(result$backend$formula, "y ~ x") + expect_equal(result$backend$data, "df") + + # Test 2: Incorrect input class + expect_error( + transform_as_backend(list(), "wrong_input"), + "You should use the extract_function_info function before calling this function" + ) +}) + +test_that("transform_as_datasources works correctly", { + # Create mock backends + mock_info1 <- create_mock_clean_fct_info() + mock_info2 <- create_mock_clean_fct_info() + mock_info2$function_name <- "glm" + + backend1 <- transform_as_backend(mock_info1, "linear_model") + backend2 <- transform_as_backend(mock_info2, "logistic_model") + + # Test + result <- transform_as_datasources(list(backend1, backend2)) + + expect_type(result, "list") + expect_named(result, "datasources") + expect_length(result$datasources, 2) + expect_equal(result$datasources[[1]]$name, "linear_model") + expect_equal(result$datasources[[2]]$name, "logistic_model") +})