-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Validation improvements #149
Changes from 8 commits
5b99ad5
f159ca0
a2ab6b7
f2b2957
28c4941
2105da1
268e9d8
b000121
2bd44d5
f36abf1
f1afc91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,14 +80,16 @@ str.R7_property <- function(object, ..., nest.lev = 0) { | |
#' rather than throwing an error. | ||
#' - `prop<-` and `@<-` set a new value for the given property. | ||
#' - `props()` returns a list of all properties | ||
#' - `props<-` sets multiple properties at once, validating after all are set | ||
hadley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#' - `prop_names()` returns the names of the properties | ||
#' - `prop_exists(x, "prop")` returns `TRUE` iif `x` has property `prop`. | ||
#' | ||
#' @param object An object from a R7 class | ||
#' @param name The name of the parameter as a character. Partial matching | ||
#' is not performed. | ||
#' @param value A replacement value for the parameter. The object is | ||
#' automatically checked for validity after the replacement is done. | ||
#' @param value For `prop<-`, a replacement value for the property; | ||
#' for `props<-`, a named list of values. The object is automatically | ||
#' checked for validity after the replacement is done. | ||
#' @export | ||
#' @examples | ||
#' horse <- new_class("horse", properties = list( | ||
|
@@ -108,7 +110,7 @@ prop <- function(object, name) { | |
if (!inherits(object, "R7_object")) { | ||
hadley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
stop("`object` is not an <R7_object>") | ||
} else if (!prop_exists(object, name)) { | ||
stop(sprintf("Can't find property %s@%s", obj_desc(object), name)) | ||
stop(prop_error_unknown(object, name)) | ||
} else { | ||
prop_val(object, name) | ||
} | ||
|
@@ -172,6 +174,19 @@ props <- function(object) { | |
} | ||
} | ||
|
||
#' @rdname prop | ||
#' @export | ||
`props<-` <- function(object, value) { | ||
stopifnot(is.list(value)) | ||
|
||
for (name in names(value)) { | ||
prop(object, name, check = FALSE) <- value[[name]] | ||
} | ||
validate(object) | ||
|
||
object | ||
} | ||
|
||
#' @rdname prop | ||
#' @export | ||
prop_exists <- function(object, name) { | ||
|
@@ -188,29 +203,42 @@ prop_exists <- function(object, name) { | |
|
||
function(object, name, check = TRUE, value) { | ||
prop <- prop_obj(object, name) | ||
if (is.null(prop)) { | ||
stop(prop_error_unknown(object, name)) | ||
} | ||
|
||
if (!is.null(prop$setter) && !identical(setter_property, name)) { | ||
setter_property <<- name | ||
on.exit(setter_property <<- NULL, add = TRUE) | ||
object <- prop$setter(object, value) | ||
} else { | ||
if (isTRUE(check) && !class_inherits(value, prop$class)) { | ||
stop(sprintf("%s@%s must be of class %s, not %s", | ||
obj_desc(object), name, | ||
class_desc(prop$class), | ||
obj_desc(value) | ||
), call. = FALSE) | ||
stop(prop_error_type(object, name, prop$class, value), call. = FALSE) | ||
Comment on lines
215
to
+216
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you extract out a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about that too. It's just pretty marginal since it's only used in two places and the logic is v simple. |
||
} | ||
attr(object, name) <- value | ||
} | ||
|
||
if (isTRUE(check)) { | ||
validate(object) | ||
validate(object, properties = FALSE) | ||
} | ||
|
||
invisible(object) | ||
} | ||
}) | ||
|
||
prop_error_unknown <- function(object, prop_name) { | ||
sprintf("Can't find property %s@%s", obj_desc(object), prop_name) | ||
} | ||
|
||
prop_error_type <- function(object, prop_name, expected, actual) { | ||
hadley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sprintf("%s@%s must be of class %s, not %s", | ||
obj_desc(object), | ||
prop_name, | ||
class_desc(expected), | ||
obj_desc(actual) | ||
) | ||
} | ||
|
||
#' @rdname prop | ||
#' @usage object@name | ||
#' @export | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,118 @@ | ||
#' Validation of R7 objects | ||
#' Validate an R7 object | ||
#' | ||
#' [validate()] calls the validation of an R7 object. This is done | ||
#' automatically when creating new objects (at the end of [new_object]) and | ||
#' when setting any property. | ||
#' @description | ||
#' `validate()` ensures that an R7 object is valid by calling the `validator` | ||
#' provided in [new_class()]. This is done automatically when constructing new | ||
#' objects and when modifying properties. | ||
#' | ||
#' [valid_eventually()] disables validation of properties, runs a function on | ||
#' the object, then validates the object. | ||
#' `valid_eventually()` disables validation, modifies the object, then | ||
#' revalidates. This is useful when a sequence of operations would otherwise | ||
#' lead an object to be temporarily invalid, or when repeated property | ||
#' modification causes a performance bottleneck because the validator is | ||
#' relatively expensive. | ||
#' | ||
#' [valid_implicitly()] does the same but does not validate the object at the end. | ||
#' | ||
#' [valid_implicitly()] should only be used rarely in performance critical code | ||
#' where you are certain a sequence of operations cannot produce an invalid | ||
#' object. | ||
#' `valid_implicitly()` does the same but does not validate the object at the | ||
#' end. It should only be used rarely, and in performance critical code where | ||
#' you are certain a sequence of operations cannot produce an invalid object. | ||
#' @param object An R7 object | ||
#' @param fun A function to call on the object before validation. | ||
validate <- function(object) { | ||
#' @param properties If `TRUE`, the default, checks property types before | ||
#' executing the validator. | ||
#' @export | ||
#' @examples | ||
#' # A range class might validate that the start is less than the end | ||
#' Range <- new_class("Range", | ||
#' properties = list(start = "double", end = "double"), | ||
#' validator = function(object) { | ||
#' if (object@start >= object@end) "start must be smaller than end" | ||
#' } | ||
#' ) | ||
#' # You can't construct an invalid object: | ||
#' try(Range(1, 1)) | ||
#' | ||
#' # And you can't create an invalid object with @<- | ||
#' r <- Range(1, 2) | ||
#' try(r@end <- 1) | ||
#' | ||
#' # But what if you want to move a range to the right? | ||
#' rightwards <- function(r, x) { | ||
#' r@start <- r@start + x | ||
#' r@end <- r@end + x | ||
#' r | ||
#' } | ||
#' # This function doesn't work because it creates a temporarily invalid state | ||
#' try(rightwards(r, 10)) | ||
#' | ||
#' # This is the perfect use case for valid_eventually(): | ||
#' rightwards <- function(r, x) { | ||
#' valid_eventually(r, function(object) { | ||
#' object@start <- object@start + x | ||
#' object@end <- object@end + x | ||
#' object | ||
#' }) | ||
#' } | ||
#' rightwards(r, 10) | ||
#' | ||
#' # Alternatively, you can set multiple properties at once using props<- | ||
#' rightwards <- function(r, x) { | ||
hadley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#' props(r) <- list(start = r@start + x, end = r@end + x) | ||
#' r | ||
#' } | ||
#' rightwards(r, 20) | ||
validate <- function(object, properties = TRUE) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I were designing this function, this is where I'd put Plus this feels like a function which has a decent chance that we will add more arguments to it, and that would free us from caring about argument order. Not sure how we feel about this principle for R7 though, since it will be tied closely to base R There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, agreed with both points. But since this is going in base R (which doesn't use that pattern), and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding an unneeded There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Explicit formal arguments also improve code clarity. Users are free to follow whatever calling convention they like. |
||
if (!is.null(attr(object, ".should_validate"))) { | ||
return(invisible(object)) | ||
} | ||
|
||
obj_class <- object_class(object) | ||
class <- object_class(object) | ||
|
||
validator <- prop_safely(obj_class, "validator") | ||
if (is.null(validator)) { | ||
return(invisible(object)) | ||
# First, check property types - if these are incorrect, the validator | ||
# is likely to return spurious errors | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the errors in the object validator are likely to be spurious, should we just go ahead and error immediately if any properties are invalid? Maybe it could have a header of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see you've tried to do something like this by avoiding the object validator with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, good idea. |
||
if (properties) { | ||
errors <- validate_properties(object, class) | ||
if (length(errors) > 0) { | ||
bullets <- paste0("- ", errors, collapse = "\n") | ||
msg <- sprintf("%s object properties are invalid:\n%s", obj_desc(object), bullets) | ||
stop(msg, call. = FALSE) | ||
} | ||
} | ||
|
||
errors <- validator(object) | ||
# Next, recursively validate the object | ||
errors <- character() | ||
while(!is.null(class) && is_class(class)) { | ||
errors <- c(errors, class@validator(object)) | ||
class <- prop_safely(class, "parent") | ||
} | ||
|
||
# If needed, report errors | ||
if (length(errors) > 0) { | ||
msg <- sprintf("Invalid <%s> object:\n%s", obj_class@name, paste0("- ", errors, collapse = "\n")) | ||
bullets <- paste0("- ", errors, collapse = "\n") | ||
msg <- sprintf("%s object is invalid:\n%s", obj_desc(object), bullets) | ||
stop(msg, call. = FALSE) | ||
} | ||
|
||
invisible(object) | ||
} | ||
|
||
validate_properties <- function(object, class) { | ||
errors <- character() | ||
|
||
for (prop in class@properties) { | ||
# Only validate static properties | ||
if (!is.null(prop$getter) || !is.null(prop$setter)) { | ||
next | ||
} | ||
|
||
value <- prop(object, prop$name) | ||
if (!class_inherits(value, prop$class)) { | ||
errors <- c(errors, prop_error_type(object, prop$name, prop$class, value)) | ||
} | ||
prop | ||
} | ||
|
||
errors | ||
} | ||
|
||
#' @rdname validate | ||
#' @export | ||
valid_eventually <- function(object, fun) { | ||
|
@@ -44,6 +121,8 @@ valid_eventually <- function(object, fun) { | |
out <- fun(object) | ||
attr(out, ".should_validate") <- old | ||
validate(out) | ||
|
||
out | ||
hadley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
#' @rdname validate | ||
|
@@ -53,5 +132,6 @@ valid_implicitly <- function(object, fun) { | |
attr(object, ".should_validate") <- FALSE | ||
out <- fun(object) | ||
attr(out, ".should_validate") <- old | ||
invisible(out) | ||
|
||
out | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo here, validator->valid
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, thanks.