diff --git a/DESCRIPTION b/DESCRIPTION index 2a42c09..852e0fb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -22,6 +22,7 @@ Imports: rlang RoxygenNote: 7.3.2 Suggests: + jsonlite, knitr, rmarkdown, testthat (>= 3.0.0) diff --git a/NAMESPACE b/NAMESPACE index 246f12a..6e9ba2e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,6 +9,7 @@ export(check_args) export(is_any) export(is_rdantic_model) export(model_config) +export(model_dump) export(model_field) export(model_validate) export(names_to_camel_case) diff --git a/R/base-model.R b/R/base-model.R index c7fde50..d8217a5 100644 --- a/R/base-model.R +++ b/R/base-model.R @@ -20,6 +20,7 @@ model_field <- function(fn, default = NA, alias = NULL, ...) { #' @param str_to_lower Convert all strings to lower case. #' @param ... **not used** at the moment #' @returns A model config object that can be used in [base_model()]. +#' @example examples/api/model-config.R #' @export model_config <- function(allow_extra = FALSE, str_to_lower = FALSE, ...) { @@ -226,36 +227,18 @@ check_assignment <- function(x, name, value) { # } # --- -# TODO: Deprecated?, use single functions as 'model_exclude_na' -model_dump <- function(obj, - exclude = NULL, - include = NULL, - exclude_na = FALSE, - exclude_null = FALSE, - by_alias = FALSE) { - fields <- model_fields(obj) - - if (is_not_null(exclude)) { - obj <- purrr::discard_at(obj, exclude) - } - - if (is_not_null(include)) { - obj <- purrr::keep_at(obj, include) - } - - if (isTRUE(exclude_na)) { - obj <- discard_this(obj, rlang::is_na) - } - - if (isTRUE(exclude_null)) { - obj <- discard_this(obj, rlang::is_null) - } - +#' Convert model to base list +#' @param obj An rdantic model object +#' @param by_alias Use aliases for names. +#' @param ... **not used** at the moment. +#' @returns base list object +#' @export +model_dump <- function(obj, by_alias = FALSE, ...) { if (isTRUE(by_alias)) { - obj <- dump_by_alias(obj, fields) + return(dump_by_alias(obj)) } - return(obj) + return(model_to_list(obj)) } # --- diff --git a/R/experimental.R b/R/experimental.R new file mode 100644 index 0000000..1c2c81b --- /dev/null +++ b/R/experimental.R @@ -0,0 +1,34 @@ +# --- +# TODO: Deprecated?, use single functions as 'model_exclude_na' +model_dump_ <- function(obj, + exclude = NULL, + include = NULL, + exclude_na = FALSE, + exclude_null = FALSE, + by_alias = FALSE) { + fields <- model_fields(obj) + + if (is_not_null(exclude)) { + obj <- purrr::discard_at(obj, exclude) + } + + if (is_not_null(include)) { + obj <- purrr::keep_at(obj, include) + } + + if (isTRUE(exclude_na)) { + obj <- discard_this(obj, rlang::is_na) + } + + if (isTRUE(exclude_null)) { + obj <- discard_this(obj, rlang::is_null) + } + + if (isTRUE(by_alias)) { + obj <- dump_by_alias(obj, fields) + } else { + obj <- model_to_list(obj) + } + + return(obj) +} diff --git a/R/helpers.R b/R/helpers.R index 7e8312b..773269a 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -91,13 +91,13 @@ dump_by_alias <- function(obj, fields = NULL) { fields <- model_fields(obj) } - l = list() + l <- list() for (name in names(obj)) { alias <- fields[[name]]$alias value <- obj[[name]] new_name <- ifelse(is.null(alias), name, alias) if (inherits(value, CLASS_RDANTIC)) { - l[[new_name]] <- by_alias(value) + l[[new_name]] <- dump_by_alias(value) } else { l[[new_name]] <- value } @@ -105,3 +105,17 @@ dump_by_alias <- function(obj, fields = NULL) { return(l) } + +# --- +model_to_list <- function(obj) { + l <- list() + for (name in names(obj)) { + value <- obj[[name]] + if (is.list(value)) { + l[[name]] <- model_to_list(value) + } else { + l[[name]] <- unclass(value) + } + } + return(l) +} diff --git a/examples/advanced-json.R b/examples/advanced-json.R new file mode 100644 index 0000000..50ff8cd --- /dev/null +++ b/examples/advanced-json.R @@ -0,0 +1,21 @@ +json_str <- '{ + "statusCode": 200, + "df": [ + {"aA": 1, "b": 2.2}, + {"aA": 4, "b": 4.5} + ] +}' + +df_model <- base_model( + a_a = is.integer, + b = is.double +) + +my_model <- base_model( + status_code = is.integer, + df = ~ is.data.frame(df_model(.x = .x)) +) + +jsonlite::fromJSON(json_str) |> + names_to_snake_case() |> + model_validate(my_model) diff --git a/examples/api/model-config.R b/examples/api/model-config.R new file mode 100644 index 0000000..25e0c1b --- /dev/null +++ b/examples/api/model-config.R @@ -0,0 +1,9 @@ +cfg <- model_config(str_to_lower = TRUE) + +my_model <- base_model( + bar = is.character, + foo = is.character, + .model_config = cfg +) + +my_model(bar = "FOO", foo = "BAR") diff --git a/man/base_model.Rd b/man/base_model.Rd index f7fecbf..79d11f6 100644 --- a/man/base_model.Rd +++ b/man/base_model.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/experimental.R +% Please edit documentation in R/base-model.R \name{base_model} \alias{base_model} \title{Create a model factory function} diff --git a/man/check_args.Rd b/man/check_args.Rd index 5e07ab5..b4566e9 100644 --- a/man/check_args.Rd +++ b/man/check_args.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/experimental.R +% Please edit documentation in R/base-model.R \name{check_args} \alias{check_args} \title{Check function arguments} diff --git a/man/model_config.Rd b/man/model_config.Rd index 3509041..7628c63 100644 --- a/man/model_config.Rd +++ b/man/model_config.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/experimental.R +% Please edit documentation in R/base-model.R \name{model_config} \alias{model_config} \title{Create a model config object} @@ -19,3 +19,14 @@ A model config object that can be used in \code{\link[=base_model]{base_model()} \description{ Create a model config object } +\examples{ +cfg <- model_config(str_to_lower = TRUE) + +my_model <- base_model( + bar = is.character, + foo = is.character, + .model_config = cfg +) + +my_model(bar = "FOO", foo = "BAR") +} diff --git a/man/model_dump.Rd b/man/model_dump.Rd new file mode 100644 index 0000000..726ed4f --- /dev/null +++ b/man/model_dump.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/base-model.R +\name{model_dump} +\alias{model_dump} +\title{Convert model to base list} +\usage{ +model_dump(obj, by_alias = FALSE, ...) +} +\arguments{ +\item{obj}{An rdantic model object} + +\item{by_alias}{Use aliases for names.} + +\item{...}{\strong{not used} at the moment.} +} +\value{ +base list object +} +\description{ +Convert model to base list +} diff --git a/man/model_field.Rd b/man/model_field.Rd index 11e5943..f00cef4 100644 --- a/man/model_field.Rd +++ b/man/model_field.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/experimental.R +% Please edit documentation in R/base-model.R \name{model_field} \alias{model_field} \title{Create a model field} diff --git a/man/model_validate.Rd b/man/model_validate.Rd index d917467..e0dd621 100644 --- a/man/model_validate.Rd +++ b/man/model_validate.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/experimental.R +% Please edit documentation in R/base-model.R \name{model_validate} \alias{model_validate} \title{Validate a list or a data frame} diff --git a/vignettes/rdantic.Rmd b/vignettes/rdantic.Rmd index e6205d4..bb2e34b 100644 --- a/vignettes/rdantic.Rmd +++ b/vignettes/rdantic.Rmd @@ -27,10 +27,12 @@ person_model <- base_model( age = is_scalar_integer ) -person_model( +(m <- person_model( name = "Lee", age = 100L -) +)) + +try(m$age <- 100) try(person_model(name = "Lee", age = 100)) @@ -112,6 +114,44 @@ external_data |> model_validate(api_model) ``` +## From JSON + +```{r} +json_str <- ' +{ + "a": 10, + "b": 20, + "l": [1, 2, 3] +}' + +json_model <- base_model( + a = is_scalar_integer, + b = is_scalar_integer, + l = is.integer +) + +jsonlite::fromJSON(json_str) |> + model_validate(json_model) + +json_str <- '{"a": 10, "b": 20.5, "l": [1, 2, 3]}' + +try(jsonlite::fromJSON(json_str) |> model_validate(json_model)) +``` + +## To JSON + +```{r} +my_model <- base_model( + status_code = model_field(is_scalar_integer, alias = "statusCode"), + x = ~ is.integer(.x) && length(.x) == 3, + y = ~ is.integer(.x) && length(.x) == 3 +) + +my_model(status_code = 200L, x = 1:3, y = 4:6) |> + model_dump(by_alias = TRUE) |> + jsonlite::toJSON(auto_unbox = TRUE) +``` + ## Type safety of function arguments ```{r}