diff --git a/DESCRIPTION b/DESCRIPTION index f2d6d60..a29e31e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: connector Title: Connect to your data easily -Version: 0.0.4.9000 +Version: 0.0.4.9001 Authors@R: c( person("Cervan", "Girard", , "cgid@novonordisk.com", role = c("aut", "cre")), person("Aksel", "Thomsen", , "oath@novonordisk.com", role = "aut"), diff --git a/NAMESPACE b/NAMESPACE index c7ba534..1add441 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -38,6 +38,7 @@ S3method(write_cnt,connector_dbi) S3method(write_cnt,connector_fs) S3method(write_cnt,default) S3method(write_ext,csv) +S3method(write_ext,json) S3method(write_ext,parquet) S3method(write_ext,rds) S3method(write_ext,txt) @@ -63,6 +64,7 @@ export(remove_directory_cnt) export(tbl_cnt) export(upload_cnt) export(write_cnt) +export(write_datasources) export(write_ext) export(write_file) import(rlang) diff --git a/NEWS.md b/NEWS.md index ad19c52..fdc004a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ # connector dev - Connectors constructor builds the datasources attribute +- Ability to write datasources attribute to a configuration file - Create a new class for nested connectors objects, "nested_connectors" - Add README and vignette on how to extend connector diff --git a/R/connect.R b/R/connect.R index b3a86a4..55b754b 100644 --- a/R/connect.R +++ b/R/connect.R @@ -82,7 +82,7 @@ connect <- function(config = "_connector.yml", metadata = NULL, datasource = NUL checkmate::assert_logical(logging) if (!is.list(config)) { - if (get_file_ext(config) %in% c("yml", "yaml")) { + if (tools::file_ext(config) %in% c("yml", "yaml")) { config <- read_file(config, eval.expr = TRUE) } else { config <- read_file(config) diff --git a/R/connectors.R b/R/connectors.R index 9043d4f..529e2cb 100644 --- a/R/connectors.R +++ b/R/connectors.R @@ -28,7 +28,7 @@ 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.") } @@ -115,16 +115,20 @@ as_datasources <- function(...) { #' 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") +#' # Assume we have a 'mock_connectors' object with a 'datasources' attribute +#' mock_connectors <- structure(list(), class = "connectors" ) +#' attr(mock_connectors, "datasources") <- list(source1 = "data1", source2 = "data2") #' #' # Using the function -#' result <- datasources(my_connectors) +#' result <- datasources(mock_connectors) #' print(result) #' #' @export datasources <- function(connectors) { + if(!is_connectors(connectors)){ + cli::cli_abort("param connectors should be a connectors object.") + } + ds <- attr(connectors, "datasources") ds } @@ -149,3 +153,8 @@ nested_connectors <- function(...) { print.nested_connectors <- function(x, ...) { print_connectors(x, ...) } + +#' @noRd +is_connectors <- function(connectors){ + inherits(connectors, "connectors") +} diff --git a/R/conts_datasources.R b/R/conts_datasources.R index f6ab44e..117ea94 100644 --- a/R/conts_datasources.R +++ b/R/conts_datasources.R @@ -6,7 +6,7 @@ #' @param data A list of function calls as expressions. #' @return A list with a 'datasources' element containing the transformed backends. #' -#' +#' @noRd connectors_to_datasources <- function(data) { data[-1] |> as.list() |> @@ -19,17 +19,66 @@ connectors_to_datasources <- function(data) { transform_as_datasources() } +#' Write datasources attribute into a config file +#' +#' Reproduce your workflow by creating a config file based on a connectors +#' object and the associated datasource attributes. +#' +#' @param connectors A connectors object with associated "datasources" +#' attribute. +#' @param file path to the config file +#' +#' @return A config file with datasource attributes which can be reused in the +#' connect function +#' +#' @examples +#' +#' # Connect to the datasources specified in it +#' config <- system.file("config", "default_config.yml", package = "connector") +#' cnts <- connect(config) +#' +#' # Extract the datasources to a config file +#' yml_file <- tempfile(fileext = ".yml") +#' write_datasources(cnts, yml_file) +#' +#' # Reconnect using the new config file +#' re_connect <- connect(yml_file) +#' re_connect +#' +#' @export +write_datasources <- function(connectors, file) { + checkmate::assert_character(file, null.ok = FALSE, any.missing = FALSE) + if (!is_connectors(connectors)) { + cli::cli_abort("param 'connectors' should be a connectors object.") + } + # testing 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) + + ## Remove class for json to avoid S3 class problem + if (ext == "json") { + class(dts) <- NULL + } + + write_file(dts, file) +} + #' 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. +#' @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". +#' @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 +#' @noRd 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") @@ -49,15 +98,18 @@ transform_as_backend <- function(infos, name) { #' 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. +#' 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`. +#' @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. +#' @return A list with a single 'datasources' element containing all input +#' backends. #' -#' @keywords internal +#' @noRd transform_as_datasources <- function(bks) { as_datasources( list( @@ -77,6 +129,8 @@ transform_as_datasources <- function(bks) { #' \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} +#' @noRd +#' extract_function_info <- function(func_string) { # Parse the function string into an expression @@ -115,12 +169,14 @@ extract_function_info <- function(func_string) { #' Extract Base Information #' -#' Extracts the package name and function/class name from the full function name. +#' 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 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 +#' @noRd 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)) { @@ -158,7 +214,7 @@ extract_base_info <- function(full_func_name, is_r6) { #' @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 +#' @noRd get_standard_specific_info <- function(package_name, func_name) { func <- getExportedValue(package_name, func_name) formal_args <- names(formals(func)) @@ -172,7 +228,7 @@ get_standard_specific_info <- function(package_name, func_name) { #' @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 +#' @noRd get_r6_specific_info <- function(package_name, func_name) { class_obj <- getExportedValue(package_name, func_name) init_func <- class_obj$public_methods$initialize @@ -187,8 +243,8 @@ get_r6_specific_info <- function(package_name, func_name) { #' @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 -#' +#' @noRd +#' extract_and_process_params <- function(expr, formal_args) { # Extract parameters from the function call params <- call_args(expr) @@ -218,7 +274,7 @@ extract_and_process_params <- function(expr, formal_args) { #' #' @param params The extracted parameters from the function call. #' @return A list of processed parameters. -#' @keywords internal +#' @noRd process_ellipsis_params <- function(params) { unnamed_args <- params[names(params) == ""] named_args <- params[names(params) != ""] @@ -233,7 +289,7 @@ process_ellipsis_params <- function(params) { #' @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 +#' @noRd process_named_params <- function(params, formal_args) { unnamed_args <- params[names(params) == ""] named_args <- params[names(params) != ""] diff --git a/R/fs_read.R b/R/fs_read.R index 34361c7..0033c4b 100644 --- a/R/fs_read.R +++ b/R/fs_read.R @@ -10,7 +10,7 @@ #' @return the result of the reading function #' @export read_file <- function(path, ...) { - find_ext <- get_file_ext(path) + find_ext <- tools::file_ext(path) class(path) <- c(find_ext, class(path)) diff --git a/R/fs_write.R b/R/fs_write.R index 59ba09a..d5432fa 100644 --- a/R/fs_write.R +++ b/R/fs_write.R @@ -11,7 +11,7 @@ #' @return `write_file()`: [invisible()] file. #' @export write_file <- function(x, file, ...) { - find_ext <- get_file_ext(file) |> + find_ext <- tools::file_ext(file) |> assert_ext("write_ext") class(file) <- c(find_ext, class(file)) @@ -93,3 +93,12 @@ write_ext.yml <- function(file, x, ...) { #' @export write_ext.yaml <- write_ext.yml + +#' @description +#' * `json`: [jsonlite::write_json()] +#' +#' @rdname write_file +#' @export +write_ext.json <- function(file, x, ...) { + jsonlite::write_json(x = x, path = file, ...) +} diff --git a/R/utils_fileext.R b/R/utils_fileext.R deleted file mode 100644 index 4dbbe6e..0000000 --- a/R/utils_fileext.R +++ /dev/null @@ -1,14 +0,0 @@ -# Getting file extension -get_file_ext <- function(file_paths) { - vapply( - X = file_paths, - FUN = function(file_path) { - file_name <- basename(file_path) - file_parts <- strsplit(file_name, "\\.")[[1]] - file_extension <- ifelse(length(file_parts) == 1, "", utils::tail(file_parts, 1)) - return(file_extension) - }, - FUN.VALUE = character(1), - USE.NAMES = FALSE - ) -} diff --git a/_pkgdown.yml b/_pkgdown.yml index e643646..fa34e52 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -12,6 +12,7 @@ reference: - connector - connector_fs - connector_dbi + - nested_connectors - title: Connector functions contents: - matches(".*_cnt$") @@ -19,3 +20,7 @@ reference: contents: - read_file - write_file + - write_datasources + - title: Utils + contents: + - datasources diff --git a/man/connectors_to_datasources.Rd b/man/connectors_to_datasources.Rd deleted file mode 100644 index d58abdd..0000000 --- a/man/connectors_to_datasources.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% 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 index dbeb0c8..c01009f 100644 --- a/man/datasources.Rd +++ b/man/datasources.Rd @@ -21,12 +21,12 @@ 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") +# Assume we have a 'mock_connectors' object with a 'datasources' attribute +mock_connectors <- structure(list(), class = "connectors" ) +attr(mock_connectors, "datasources") <- list(source1 = "data1", source2 = "data2") # Using the function -result <- datasources(my_connectors) +result <- datasources(mock_connectors) print(result) } diff --git a/man/extract_and_process_params.Rd b/man/extract_and_process_params.Rd deleted file mode 100644 index bad76c4..0000000 --- a/man/extract_and_process_params.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% 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 deleted file mode 100644 index ae859b6..0000000 --- a/man/extract_base_info.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% 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 deleted file mode 100644 index c3a6f1f..0000000 --- a/man/extract_function_info.Rd +++ /dev/null @@ -1,22 +0,0 @@ -% 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 deleted file mode 100644 index 1cdc3fa..0000000 --- a/man/get_r6_specific_info.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% 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 deleted file mode 100644 index 3e24b76..0000000 --- a/man/get_standard_specific_info.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% 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/process_ellipsis_params.Rd b/man/process_ellipsis_params.Rd deleted file mode 100644 index 351fcf3..0000000 --- a/man/process_ellipsis_params.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% 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 deleted file mode 100644 index 995edae..0000000 --- a/man/process_named_params.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% 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 deleted file mode 100644 index b269d7f..0000000 --- a/man/transform_as_backend.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% 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 deleted file mode 100644 index 37eed99..0000000 --- a/man/transform_as_datasources.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% 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/man/write_datasources.Rd b/man/write_datasources.Rd new file mode 100644 index 0000000..61bfc52 --- /dev/null +++ b/man/write_datasources.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/conts_datasources.R +\name{write_datasources} +\alias{write_datasources} +\title{Write datasources attribute into a config file} +\usage{ +write_datasources(connectors, file) +} +\arguments{ +\item{connectors}{An object containing connectors with a "datasources" attribute.} + +\item{file}{path to the config file} +} +\value{ +A config file with the datasources attribute and can be reuse in the connect function +} +\description{ +Reproduce your workflow by creating a config file based on a connectors object and his datacrouces attribut +} +\examples{ + +# Connect to the datasources specified in it +config <- system.file("config", "default_config.yml", package = "connector") +cnts <- connect(config) + +# Extract the datasources to a config file +yml_file <- tempfile(fileext = ".yml") +write_datasources(cnts, yml_file) + +# Reconnect using the new config file +re_connect <- connect(yml_file) +re_connect + +} diff --git a/man/write_file.Rd b/man/write_file.Rd index 8fe6935..9e10b7d 100644 --- a/man/write_file.Rd +++ b/man/write_file.Rd @@ -9,6 +9,7 @@ \alias{write_ext.rds} \alias{write_ext.xpt} \alias{write_ext.yml} +\alias{write_ext.json} \title{Write files based on the extension} \usage{ write_file(x, file, ...) @@ -26,6 +27,8 @@ write_ext(file, x, ...) \method{write_ext}{xpt}(file, x, ...) \method{write_ext}{yml}(file, x, ...) + +\method{write_ext}{json}(file, x, ...) } \arguments{ \item{x}{Object to write} @@ -69,6 +72,10 @@ function to write the file is chosen depending on the file extension. \itemize{ \item \code{yml}/\code{yaml}: \code{\link[yaml:write_yaml]{yaml::write_yaml()}} } + +\itemize{ +\item \code{json}: \code{\link[jsonlite:read_json]{jsonlite::write_json()}} +} } \examples{ # Write CSV file diff --git a/tests/testthat/test-cnts_datasources.R b/tests/testthat/test-cnts_datasources.R new file mode 100644 index 0000000..7be8e9c --- /dev/null +++ b/tests/testthat/test-cnts_datasources.R @@ -0,0 +1,51 @@ +test_that("write_datasources works correctly", { + # Create test connector object + config <- system.file("config", "default_config.yml", package = "connector") + test_connectors <- connect(config) + + # Setup + valid_extensions <- c("yml", "yaml", "json", "rds") + temp_files <- purrr::map_chr(valid_extensions, ~tempfile(fileext = paste0(".", .x))) |> + purrr::set_names(valid_extensions) + temp_invalid <- tempfile(fileext = ".txt") + + # Test valid file extensions + purrr::walk(temp_files, ~ expect_no_error(write_datasources(test_connectors, .x))) + # Test file content + original_sources <- datasources(test_connectors) + written_sources <- read_file(temp_files["yml"]) + written_sources <- as_datasources(written_sources) + expect_equal(original_sources, written_sources) + + # Test invalid cases + invalid_inputs <- list( + list( + input = list(dummy = "data"), + file = temp_files["yml"], + error = "param 'connectors' should be a connectors object" + ), + list( + input = test_connectors, + file = temp_invalid, + error = "ext %in%" + ), + list( + input = test_connectors, + file = NULL, + error = "Must be of type 'character'" + ), + list( + input = test_connectors, + file = NA_character_, + error = "Contains missing values" + ) + ) + + purrr::walk(invalid_inputs, ~ expect_error( + write_datasources(.x$input, .x$file), + .x$error + )) + + # Cleanup + unlink(c(temp_files, temp_invalid)) +})