diff --git a/DESCRIPTION b/DESCRIPTION index 1761ced..5698fc2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: pacta.workflow.utils Title: Utility functions for PACTA workflows -Version: 0.0.0.9012 +Version: 0.0.0.9013 Authors@R: c(person(given = "Alex", family = "Axthelm", diff --git a/NAMESPACE b/NAMESPACE index cc7b000..8e677ef 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,6 +17,8 @@ export(git_tag_info) export(inherit_params) export(is_git_path) export(merge_lists) +export(modify_list_element) +export(modify_single_list_element) export(parse_params) export(parse_raw_params) importFrom(logger,log_debug) diff --git a/R/export_manifest.R b/R/export_manifest.R index cd57792..fcbc195 100644 --- a/R/export_manifest.R +++ b/R/export_manifest.R @@ -82,10 +82,20 @@ create_manifest <- function( return(manifest_list) } -# Check that arguments are nicely coercible to JSON. called for side effect of -# `stop` if not. +#' check_arg_type +#' +#' Check that arguments are nicely coercible to JSON. Primarily a check that +#' lists are composed of simple types (other lists, characters, +#' numeric/integers, or logicals). Called for side effect of `stop` if not. +#' +#' @param arg object to check. Lists will be checked recursively, and must be +#' named. +#' @return the same object, unchanged. Function will throw an error if objects +#' are not simple check_arg_type <- function(arg) { log_trace("Checking argument type") + # remove AsIs class if necessary + arg <- un_asis(arg) if (inherits(arg, "list")) { if ( length(arg) != length(names(arg)) || @@ -113,3 +123,17 @@ check_arg_type <- function(arg) { } return(arg) } + +#' un_asis +#' +#' Remove AsIs class from object +#' +#' @param x an object (with the `AsIs` class) +#' @return the same object, without AsIs class +un_asis <- function(x) { + if (inherits(x, "AsIs")) { + log_trace("Removing AsIs class from object") + class(x) <- class(x)[-match("AsIs", class(x))] + } + return(x) +} diff --git a/R/modify_list_element.R b/R/modify_list_element.R new file mode 100644 index 0000000..7b7041a --- /dev/null +++ b/R/modify_list_element.R @@ -0,0 +1,84 @@ +#' modify_list_element +#' +#' Apply a function to a one or more elements of a list, given the positions of +#' the elements. +#' +#' @param x List to modify +#' @param positions (List or vector) Position of elements to modify, as a +#' vector of indices +#' @param function_to_apply Function to apply to elements +#' @return modified list +#' @examples +# nolint start +#' test_list <- list(a = 1L, b = list(ba = 2L, bb = 2.2), c = "a") +#' results <- modify_list_element( +#' x = test_list, +#' position = list( +#' c("b", "bb"), +#' "a" +#' ), +#' function_to_apply = I +#' ) +#' results +# nolint end +#' @export +modify_list_element <- function( + x, + positions, + function_to_apply +) { + # cast as list, if not already (if a simple vector was passed in) + if (!is.list(positions)) { + positions <- list(positions) + } + for (position in positions) { + x <- modify_single_list_element( + x = x, + position = position, + function_to_apply = function_to_apply + ) + } + return(x) +} + +#' modify_single_list_element +#' +#' Apply a function to a single element of a list, givin a vector of list +#' indices. +#' +#' @param x List to modify +#' @param position Position of element to modify, as a vector of indices +#' @param function_to_apply Function to apply to element +#' @return modified list +#' @examples +# nolint start +#' test_list <- list(a = 1L, b = list(ba = 2L, bb = 2.2), c = "a") +#' results <- modify_single_list_element( +#' x = test_list, +#' position = c("b", "bb"), +#' function_to_apply = I +#' ) +#' results +# nolint end +#' @export +modify_single_list_element <- function( + x, + position, + function_to_apply +) { + if (length(position) == 1L) { + if (is.null(x[[position[[1L]]]])) { + log_warn("Element {position[[1L]]} is not found (NULL).") + warning("NULL list elements cannot be modified.") + } else { + x[[position]] <- function_to_apply(x[[position]]) + } + } else { + x[[position[[1L]]]] <- modify_single_list_element( + x = x[[position[[1L]]]], + position = position[-1L], + function_to_apply = function_to_apply + ) + } + return(invisible(x)) +} diff --git a/R/parse_params.R b/R/parse_params.R index cd415ad..1b833e8 100644 --- a/R/parse_params.R +++ b/R/parse_params.R @@ -6,16 +6,19 @@ #' @param inheritence_search_paths Paths to search for inherited parameters. #' See `inherit_params`. #' @param schema_file Path to JSON Schema file for validation. +#' @param force_array Path in params list to force casting as JSON array. +#' (Default empty) #' @return Parsed parameters as a standard R list. #' @export parse_params <- function( json, inheritence_search_paths = NULL, - schema_file = NULL + schema_file = NULL, + force_array = list() ) { log_trace("Parsing params.") if (length(json) == 1L && file.exists(json)) { - log_trace("Reading params from file: {json}.}") + log_trace("Reading params from file: {json}.") } else { log_trace("Reading params from string.") } @@ -25,6 +28,13 @@ parse_params <- function( inheritence_search_paths ) + # force array + full_params <- modify_list_element( + x = full_params, + positions = force_array, + function_to_apply = I + ) + if (!is.null(schema_file)) { if (requireNamespace("jsonvalidate", quietly = TRUE)) { log_trace("Validating parameters.") diff --git a/R/parse_raw_params.R b/R/parse_raw_params.R index c3c2fa0..7912d53 100644 --- a/R/parse_raw_params.R +++ b/R/parse_raw_params.R @@ -141,7 +141,8 @@ parse_raw_params <- function( params <- parse_params( json = json, inheritence_search_paths = inheritence_search_paths, - schema_file = schema_file + schema_file = schema_file, + force_array = c("portfolio", "files") ) return(params) diff --git a/man/check_arg_type.Rd b/man/check_arg_type.Rd new file mode 100644 index 0000000..6bd0a34 --- /dev/null +++ b/man/check_arg_type.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/export_manifest.R +\name{check_arg_type} +\alias{check_arg_type} +\title{check_arg_type} +\usage{ +check_arg_type(arg) +} +\arguments{ +\item{arg}{object to check. Lists will be checked recursively, and must be +named.} +} +\value{ +the same object, unchanged. Function will throw an error if objects +are not simple +} +\description{ +Check that arguments are nicely coercible to JSON. Primarily a check that +lists are composed of simple types (other lists, characters, +numeric/integers, or logicals). Called for side effect of \code{stop} if not. +} diff --git a/man/modify_list_element.Rd b/man/modify_list_element.Rd new file mode 100644 index 0000000..8f2f967 --- /dev/null +++ b/man/modify_list_element.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/modify_list_element.R +\name{modify_list_element} +\alias{modify_list_element} +\title{modify_list_element} +\usage{ +modify_list_element(x, positions, function_to_apply) +} +\arguments{ +\item{x}{List to modify} + +\item{positions}{(List or vector) Position of elements to modify, as a +vector of indices} + +\item{function_to_apply}{Function to apply to elements} +} +\value{ +modified list +} +\description{ +Apply a function to a one or more elements of a list, given the positions of +the elements. +} +\examples{ +test_list <- list(a = 1L, b = list(ba = 2L, bb = 2.2), c = "a") +results <- modify_list_element( + x = test_list, + position = list( + c("b", "bb"), + "a" + ), + function_to_apply = I +) +results +} diff --git a/man/modify_single_list_element.Rd b/man/modify_single_list_element.Rd new file mode 100644 index 0000000..28928ea --- /dev/null +++ b/man/modify_single_list_element.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/modify_list_element.R +\name{modify_single_list_element} +\alias{modify_single_list_element} +\title{modify_single_list_element} +\usage{ +modify_single_list_element(x, position, function_to_apply) +} +\arguments{ +\item{x}{List to modify} + +\item{position}{Position of element to modify, as a vector of indices} + +\item{function_to_apply}{Function to apply to element} +} +\value{ +modified list +} +\description{ +Apply a function to a single element of a list, givin a vector of list +indices. +} +\examples{ +test_list <- list(a = 1L, b = list(ba = 2L, bb = 2.2), c = "a") +results <- modify_single_list_element( + x = test_list, + position = c("b", "bb"), + function_to_apply = I +) +results +} diff --git a/man/parse_params.Rd b/man/parse_params.Rd index 0459d8b..412c16a 100644 --- a/man/parse_params.Rd +++ b/man/parse_params.Rd @@ -4,7 +4,12 @@ \alias{parse_params} \title{parse_params} \usage{ -parse_params(json, inheritence_search_paths = NULL, schema_file = NULL) +parse_params( + json, + inheritence_search_paths = NULL, + schema_file = NULL, + force_array = list() +) } \arguments{ \item{json}{JSON string or file path.} @@ -13,6 +18,9 @@ parse_params(json, inheritence_search_paths = NULL, schema_file = NULL) See \code{inherit_params}.} \item{schema_file}{Path to JSON Schema file for validation.} + +\item{force_array}{Path in params list to force casting as JSON array. +(Default empty)} } \value{ Parsed parameters as a standard R list. diff --git a/man/un_asis.Rd b/man/un_asis.Rd new file mode 100644 index 0000000..c42516e --- /dev/null +++ b/man/un_asis.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/export_manifest.R +\name{un_asis} +\alias{un_asis} +\title{un_asis} +\usage{ +un_asis(x) +} +\arguments{ +\item{x}{an object (with the \code{AsIs} class)} +} +\value{ +the same object, without AsIs class +} +\description{ +Remove AsIs class from object +} diff --git a/tests/testthat/test-modify_list_element.R b/tests/testthat/test-modify_list_element.R new file mode 100644 index 0000000..16bb014 --- /dev/null +++ b/tests/testthat/test-modify_list_element.R @@ -0,0 +1,216 @@ +## save current settings so that we can reset later +threshold <- logger::log_threshold() +appender <- logger::log_appender() +layout <- logger::log_layout() +on.exit({ + ## reset logger settings + logger::log_threshold(threshold) + logger::log_layout(layout) + logger::log_appender(appender) +}) + +logger::log_appender(logger::appender_stdout) +logger::log_threshold(logger::FATAL) +logger::log_layout(logger::layout_simple) + +test_that("modify_list_element correctly modifies a flat list", { + test_list <- list(a = 1L, b = 2.2, c = "a") + results <- modify_list_element( + x = test_list, + positions = "b", + function_to_apply = I + ) + expect_identical( + object = results, + expected = list(a = 1L, b = I(2.2), c = "a") + ) +}) + +test_that("modify_list_element accepts different functions", { + test_list <- list(a = 1L, b = 2.2, c = "a") + results <- modify_list_element( + x = test_list, + positions = "b", + function_to_apply = as.integer + ) + expect_identical( + object = results, + expected = list(a = 1L, b = 2L, c = "a") + ) +}) + +test_that("modify_list_element correctly modifies a nested list", { + test_list <- list(a = 1L, b = list(ba = 2L, bb = 2.2), c = "a") + results <- modify_list_element( + x = test_list, + positions = c("b", "bb"), + function_to_apply = I + ) + expect_identical( + object = results, + expected = list(a = 1L, b = list(ba = 2L, bb = I(2.2)), c = "a") + ) +}) + +test_that("modify_single_list_element modifies multiple elements", { + test_list <- list(a = 1L, b = list(ba = 2L, bb = 2.2), c = "a") + results <- modify_list_element( + x = test_list, + position = list( + c("b", "bb"), + "a" + ), + function_to_apply = I + ) + expect_identical( + object = results, + expected = list(a = I(1L), b = list(ba = 2L, bb = I(2.2)), c = "a") + ) +}) + +test_that("modify_list_element modifies list elements of nested list", { + test_list <- list(a = 1L, b = list(ba = 2L, bb = 2.2), c = "a") + results <- modify_list_element( + x = test_list, + positions = "b", + function_to_apply = I + ) + expect_identical( + object = results, + expected = list(a = 1L, b = I(list(ba = 2L, bb = 2.2)), c = "a") + ) +}) + +test_that("modify_list_element correctly modifies an deep nested list", { + test_list <- list( + a = 1L, + b = list( + c = 2L, + d = list( + e = 3L, + f = list( + g = 4L, + h = list( + i = 5L, + j = 6L + ) + ) + ) + ) + ) + results <- modify_list_element( + x = test_list, + positions = c("b", "d", "f", "h", "i"), + function_to_apply = I + ) + expect_identical( + object = results, + expected = list( + a = 1L, + b = list( + c = 2L, + d = list( + e = 3L, + f = list( + g = 4L, + h = list( + i = I(5L), + j = 6L + ) + ) + ) + ) + ) + ) +}) + +test_that("modify_list_element can accept positional arguments", { + test_list <- list( + a = 1L, + b = list( + c = 2L, + d = list( + e = 3L, + f = list( + g = 4L, + h = list( + i = 5L, + j = 6L + ) + ) + ) + ) + ) + results <- modify_list_element( + x = test_list, + positions = c(2L, 2L, 2L, 2L, 1L), + function_to_apply = I + ) + expect_identical( + object = results, + expected = list( + a = 1L, + b = list( + c = 2L, + d = list( + e = 3L, + f = list( + g = 4L, + h = list( + i = I(5L), + j = 6L + ) + ) + ) + ) + ) + ) +}) + +test_that("modify_list_element can modify multiple nested list elements", { + test_list <- list( + a = 1L, + b = list( + c = 2L, + d = list( + e = 3L, + f = list( + g = 4L, + h = list( + i = 5L, + j = 6L + ) + ) + ) + ) + ) + results <- modify_list_element( + x = test_list, + positions = list( + c("b", "d", "f", "h", "i"), + "a", + c("b", "c"), + c("b", "d", "f", "g") + ), + function_to_apply = I + ) + expect_identical( + object = results, + expected = list( + a = I(1L), + b = list( + c = I(2L), + d = list( + e = 3L, + f = list( + g = I(4L), + h = list( + i = I(5L), + j = 6L + ) + ) + ) + ) + ) + ) +}) diff --git a/tests/testthat/test-modify_single_list_element.R b/tests/testthat/test-modify_single_list_element.R new file mode 100644 index 0000000..04f903f --- /dev/null +++ b/tests/testthat/test-modify_single_list_element.R @@ -0,0 +1,170 @@ +## save current settings so that we can reset later +threshold <- logger::log_threshold() +appender <- logger::log_appender() +layout <- logger::log_layout() +on.exit({ + ## reset logger settings + logger::log_threshold(threshold) + logger::log_layout(layout) + logger::log_appender(appender) +}) + +logger::log_appender(logger::appender_stdout) +logger::log_threshold(logger::FATAL) +logger::log_layout(logger::layout_simple) + +test_that("modify_single_list_element correctly modifies a flat list", { + test_list <- list(a = 1L, b = 2.2, c = "a") + results <- modify_single_list_element( + x = test_list, + position = "b", + function_to_apply = I + ) + expect_identical( + object = results, + expected = list(a = 1L, b = I(2.2), c = "a") + ) +}) + +test_that("modify_single_list_element accepts different functions", { + test_list <- list(a = 1L, b = 2.2, c = "a") + results <- modify_single_list_element( + x = test_list, + position = "b", + function_to_apply = as.integer + ) + expect_identical( + object = results, + expected = list(a = 1L, b = 2L, c = "a") + ) +}) + +test_that("modify_single_list_element correctly modifies a nested list", { + test_list <- list(a = 1L, b = list(ba = 2L, bb = 2.2), c = "a") + results <- modify_single_list_element( + x = test_list, + position = c("b", "bb"), + function_to_apply = I + ) + expect_identical( + object = results, + expected = list(a = 1L, b = list(ba = 2L, bb = I(2.2)), c = "a") + ) +}) + +test_that("modify_single_list_element modifies list elements of nested list", { + test_list <- list(a = 1L, b = list(ba = 2L, bb = 2.2), c = "a") + results <- modify_single_list_element( + x = test_list, + position = "b", + function_to_apply = I + ) + expect_identical( + object = results, + expected = list(a = 1L, b = I(list(ba = 2L, bb = 2.2)), c = "a") + ) +}) + +test_that("modify_single_list_element correctly modifies an deep nested list", { + test_list <- list( + a = 1L, + b = list( + c = 2L, + d = list( + e = 3L, + f = list( + g = 4L, + h = list( + i = 5L, + j = 6L + ) + ) + ) + ) + ) + results <- modify_single_list_element( + x = test_list, + position = c("b", "d", "f", "h", "i"), + function_to_apply = I + ) + expect_identical( + object = results, + expected = list( + a = 1L, + b = list( + c = 2L, + d = list( + e = 3L, + f = list( + g = 4L, + h = list( + i = I(5L), + j = 6L + ) + ) + ) + ) + ) + ) +}) + +test_that("modify_single_list_element can accept positional arguments", { + test_list <- list( + a = 1L, + b = list( + c = 2L, + d = list( + e = 3L, + f = list( + g = 4L, + h = list( + i = 5L, + j = 6L + ) + ) + ) + ) + ) + results <- modify_single_list_element( + x = test_list, + position = c(2L, 2L, 2L, 2L, 1L), + function_to_apply = I + ) + expect_identical( + object = results, + expected = list( + a = 1L, + b = list( + c = 2L, + d = list( + e = 3L, + f = list( + g = 4L, + h = list( + i = I(5L), + j = 6L + ) + ) + ) + ) + ) + ) +}) + +test_that("modify_single_list_element warns if element not found", { + test_list <- list(a = 1L, b = 2.2, c = "a") + expect_warning( + object = { + results <- modify_single_list_element( + x = test_list, + position = "x", + function_to_apply = I + ) + }, + regexp = "^NULL list elements cannot be modified.$" + ) + expect_identical( + object = results, + expected = list(a = 1L, b = 2.2, c = "a") + ) +}) diff --git a/tests/testthat/test-parse_params.R b/tests/testthat/test-parse_params.R index 93ac926..64a806f 100644 --- a/tests/testthat/test-parse_params.R +++ b/tests/testthat/test-parse_params.R @@ -206,3 +206,29 @@ test_that("simple inheritence, pass as string, validation works", { ) ) }) + +test_that("Forcing scalar to array works", { + json_string <- '{ + "id": 1, + "name": "A green door", + "price": 12.50, + "tags": ["home"], + "supplier": "ACME Doors" + }' + results <- parse_params( + json = json_string, + inheritence_search_paths = base_params_dir, + schema_file = schema_file, + force_array = "tags" + ) + expect_identical( + object = results, + expected = list( + id = 1L, + name = "A green door", + price = 12.5, + tags = I("home"), + supplier = "ACME Doors" + ) + ) +}) diff --git a/tests/testthat/test-un_asis.R b/tests/testthat/test-un_asis.R new file mode 100644 index 0000000..aa6ec56 --- /dev/null +++ b/tests/testthat/test-un_asis.R @@ -0,0 +1,27 @@ +## save current settings so that we can reset later +threshold <- logger::log_threshold() +appender <- logger::log_appender() +layout <- logger::log_layout() +on.exit({ + ## reset logger settings + logger::log_threshold(threshold) + logger::log_layout(layout) + logger::log_appender(appender) +}) + +logger::log_appender(logger::appender_stdout) +logger::log_threshold(logger::FATAL) +logger::log_layout(logger::layout_simple) + +test_that("un_asis does not alter objects without AsIs class", { + x <- rnorm(10L) + results <- un_asis(x) + expect_identical(results, x) +}) + +test_that("un_asis removes AsIs class", { + x <- rnorm(10L) + x_asis <- I(x) + results <- un_asis(x_asis) + expect_identical(results, x) +})