Skip to content

Commit 75cc3b8

Browse files
Merge pull request #7 from eoda-dev/refactor/model-field-creation
Refactor/model field creation
2 parents 2ed99ce + 57006b7 commit 75cc3b8

23 files changed

+430
-295
lines changed

NAMESPACE

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@
33
S3method("$<-",typewriter)
44
S3method("[[<-",typewriter)
55
S3method(print,typewriter)
6-
export(Optional)
7-
export(Union)
86
export(base_model)
97
export(check_args)
108
export(dtype)
119
export(is_any)
1210
export(is_typewriter_model)
13-
export(model_config)
1411
export(model_dump)
1512
export(model_field)
1613
export(model_from_template)
1714
export(model_validate)
1815
export(names_to_camel_case)
1916
export(names_to_snake_case)
17+
export(optional)
2018
export(typed_struct)
2119
importFrom(utils,capture.output)
2220
importFrom(utils,modifyList)

R/base-model.R

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,26 @@ model_fields <- function(model_fn) {
77
#' Create a model field
88
#' @param fn A type check function.
99
#' @param default A default value for the field.
10+
#' @param optional Whether the field is optional.
1011
#' @param alias alias that can be used in [model_dump()]
11-
#' @param error_msg,... **not used** at the moment
12+
#' @param error_msg A custom error message.
13+
#' @param ... **not used** at the moment
1214
#' @returns A model field.
1315
#' @export
14-
model_field <- function(fn, default = NA, alias = NULL, error_msg = NULL, ...) {
15-
# l <- as.list(environment())
16-
# return(structure(c(l, list(...)), class = CLASS_MODEL_FIELD))
16+
model_field <- function(fn, default = NA, optional = FALSE, alias = NULL, error_msg = NULL, ...) {
1717
obj <- c(as.list(environment()), list(...))
1818
base_class <- class(obj)
1919
structure(obj, class = c(base_class, CLASS_MODEL_FIELD))
2020
}
2121

2222
# ---
23-
#' Create a model config object
24-
#' @param extra Whether to allow extra fields without type check.
25-
#' @param str_to_lower Convert all strings to lower case.
26-
#' @param ... **not used** at the moment
27-
#' @returns A model config object that can be used in [base_model()].
28-
#' @example examples/api/model-config.R
29-
#' @export
30-
model_config <- function(extra = c("ignore", "allow", "forbid"),
31-
str_to_lower = FALSE, ...) {
23+
# #' Create a model config object
24+
# #' DEPRECATED
25+
# #' @param extra Whether to allow extra fields without type check.
26+
# #' @param ... **not used** at the moment
27+
# #' @returns A model config object that can be used in [base_model()].
28+
# #' @example examples/api/model-config.R
29+
model_config <- function(extra = c("ignore", "allow", "forbid"), ...) {
3230
obj <- c(as.list(environment()), list(...))
3331
obj$extra <- match.arg(extra)
3432
base_class <- class(obj)
@@ -40,7 +38,6 @@ model_config <- function(extra = c("ignore", "allow", "forbid"),
4038
#' @param fields A named list of field definitions.
4139
#' @param ... Named arguments of field definitions.
4240
#' Normally either `fields` or `...` is supplied.
43-
#' @param .model_config See [model_config()].
4441
#' @param .model_pre_init A callback function that is executed before the type checks.
4542
#' @param .model_post_init A callback function that is executed after the type checks.
4643
#' @param .validators_before A named list of field validators
@@ -50,17 +47,21 @@ model_config <- function(extra = c("ignore", "allow", "forbid"),
5047
#' @param .strict_args_order If set to `TRUE`, the `.x` parameter
5148
#' of the returned model factory function will be the last function argument.
5249
#' This is useful if you want to pass the arguments unnamed.
50+
#' @param .allow_na Whether to allow `NA` values for all fields.
51+
#' @param .extra Whether to allow extra fields without type check.
5352
#' @returns A model factory function.
5453
#' @example examples/api/base-model.R
5554
#' @importFrom utils modifyList
5655
#' @export
5756
base_model <- function(fields = list(), ...,
58-
.model_config = model_config(),
5957
.model_pre_init = NULL,
6058
.model_post_init = NULL,
6159
.validators_before = list(),
6260
.validators_after = list(),
63-
.strict_args_order = FALSE) {
61+
.strict_args_order = FALSE,
62+
.allow_na = FALSE,
63+
.extra = c("ignore", "allow", "forbid")) {
64+
.extra <- match.arg(.extra)
6465
fields <- modifyList(fields, list(...), keep.null = TRUE)
6566
fields <- Map(function(.x) {
6667
if (inherits(.x, CLASS_MODEL_FUNCTION)) {
@@ -72,17 +73,12 @@ base_model <- function(fields = list(), ...,
7273
}
7374

7475
if (!inherits(.x, CLASS_MODEL_FIELD)) {
75-
.x <- model_field(fn = .x)
76-
}
77-
78-
if (is.character(.x$fn)) {
79-
.x$fn <- type_check_fn_from_str(.x$fn)
76+
.x <- as_model_field(.x)
8077
}
8178

8279
return(.x)
8380
}, fields)
8481

85-
# model_args <- purrr::map(fields, ~ .x$default)
8682
model_args <- Map(function(x) x$default, fields)
8783
fn_args <- c(alist(.x = NULL), model_args, alist(... = ))
8884
if (.strict_args_order) {
@@ -108,25 +104,25 @@ base_model <- function(fields = list(), ...,
108104
}
109105

110106
for (name in names(fields)) {
111-
type_check_fn <- rlang::as_function(fields[[name]]$fn)
112-
obj_value <- obj[[name]]
107+
field <- fields[[name]]
108+
check_type <- rlang::as_function(field$fn)
109+
value <- obj[[name]]
110+
if (.allow_na | field$optional) {
111+
if (length(value) == 1L && is.na(value)) next()
112+
}
113113

114-
if (!all(type_check_fn(obj_value))) {
114+
if (!all(check_type(value))) {
115115
errors[[name]] <- list(
116116
name = name,
117-
value = obj_value,
118-
type_check_fn = type_check_fn
117+
value = value,
118+
type_check_fn = check_type,
119+
msg = field$error_msg
119120
)
120121
}
121122
}
122123

123124
obj <- validate_fields(obj, .validators_after)
124125

125-
if (.model_config$str_to_lower) {
126-
# obj <- purrr::map_depth(obj, -1, str_to_lower)
127-
obj <- map_depth_base(obj, -1, str_to_lower)
128-
}
129-
130126
if (length(errors) > 0) {
131127
msg <- paste0(map_type_check_errors(errors), collapse = "\n")
132128
stop("Type check(s) failed\n", msg, domain = NA)
@@ -136,11 +132,11 @@ base_model <- function(fields = list(), ...,
136132
return(invisible(obj))
137133
}
138134

139-
if (.model_config$extra == "ignore") {
135+
if (.extra == "ignore") {
140136
obj <- obj[names(fields)]
141137
}
142138

143-
if (.model_config$extra == "forbid") {
139+
if (.extra == "forbid") {
144140
extra_fields <- !names(obj) %in% names(fields)
145141
if (any(extra_fields)) {
146142
stop("Forbidden field(s): ", paste(names(obj)[extra_fields], collapse = ", "))
@@ -221,11 +217,11 @@ print.typewriter <- function(x, ...) {
221217

222218
# ---
223219
check_assignment <- function(x, name, value) {
224-
fields <- model_fields(x)
225-
type_check_fn <- rlang::as_function(fields[[name]]$fn)
226-
fn_text <- get_fn_text(type_check_fn)
227-
if (isFALSE(type_check_fn(value))) {
228-
stop(paste0("Type check failed.\n", fn_text))
220+
field <- model_fields(x)[[name]]
221+
check_type <- rlang::as_function(field$fn)
222+
error_msg <- ifelse(is_not_null(field$error_msg), glue::glue(field$error_msg), get_fn_text(check_type))
223+
if (!check_type(value)) {
224+
stop(paste0("Type check failed.\n", error_msg))
229225
}
230226
}
231227

R/errors.R

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ get_fn_text <- function(fn) {
1010
# ---
1111
#' @importFrom utils capture.output str
1212
create_type_check_error_message <- function(error) {
13+
name <- error$name
1314
value <- error$value
1415
value_text <- paste0(capture.output(str(value)), collapse = "\n")
1516
fn <- error$type_check_fn
@@ -22,12 +23,12 @@ create_type_check_error_message <- function(error) {
2223
class_text <- paste(class(value), collapse = ", ")
2324

2425
msg <- c(
25-
glue::glue("# ---\nType check failed for '{error$name}'"),
26+
glue::glue("---\nType check failed for '{error$name}'"),
2627
paste("value:", value_text),
2728
paste("type:", typeof(value)),
2829
paste("class:", class_text),
2930
paste("length:", length(value)),
30-
paste("expected:", fn_text)
31+
ifelse(is.null(error$msg), paste("expected:", fn_text), glue::glue(error$msg))
3132
)
3233
return(msg)
3334
}

R/helpers.R

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ dump_by_alias <- function(obj, fields = NULL) {
106106
if (inherits(value, CLASS_MODEL)) {
107107
l[[new_name]] <- dump_by_alias(value)
108108
} else {
109-
l[[new_name]] <- value
109+
# l[[new_name]] <- value
110+
l[[new_name]] <- ifelse(is.null(value), list(NULL), value)
110111
}
111112
}
112113

@@ -150,3 +151,20 @@ map_depth_base <- function(.x, .depth, .f) {
150151
}))
151152
}
152153
}
154+
155+
# ---
156+
assign_values <- function(x, ...) {
157+
l <- list(...)
158+
for (name in names(l)) {
159+
value <- l[[name]]
160+
x[[name]] <- ifelse(is.null(value), list(NULL), value)
161+
}
162+
163+
x
164+
}
165+
166+
# ---
167+
assign_value <- function(x, name, value) {
168+
x[[name]] <- ifelse(is.null(value), list(NULL), value)
169+
x
170+
}

R/typed-struct.R

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# ---
22
#' Create a typed structure
3-
#' @param ... Type definitions (Type check functions)
3+
#' @param ... Type definitions.
4+
#' @param .allow_na Allow values to be initialized with `NA`.
45
#' @returns A type factory function.
56
#' @example examples/api/typed-struct.R
67
#' @export
7-
typed_struct <- function(...) {
8+
typed_struct <- function(..., .allow_na = FALSE) {
89
types <- list(...)
9-
base_model(types, .model_config = model_config(extra = "forbid"))
10+
base_model(types, .extra = "forbid", .allow_na = .allow_na)
1011
}

0 commit comments

Comments
 (0)