Skip to content
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

Merged
merged 11 commits into from
Feb 10, 2022
18 changes: 14 additions & 4 deletions R/class.R
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,20 @@ is_class <- function(x) inherits(x, "R7_class")
#'
#' @param constructor The constructor function. This is optional, unless
#' you want to control which properties can be set on constructor.
#' @param validator A function used to determine whether or not an object
#' is valid. This is called automatically after construction, and
#' whenever any property is set. It should return `NULL` if the object is
#' valid, and otherwise return a character vector of problems.
#' @param validator A function taking a single argument, the object to validate.
#'
#' The job of a validator is to determine whether the object is valid,
#' i.e. if the current property values form an allowed combination. The
#' types of the properties are always automatically validator so the job of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always automatically validator

typo here, validator->valid

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, thanks.

#' the validator is to verify that the value of individual properties is
#' ok (i.e. maybe a property should have length 1, or should always be
#' positive), or that the combination of values of multiple properties is ok.
#' It is called after construction and whenever any property is set.
#'
#' The validator should return `NULL` if the object is valid. If not, it
#' should return a character vector where each element describes a single
#' problem. It's generally helpful to report as many problems at once
#' as possible.
#' @param properties A list specifying the properties (data) that
#' every object of the class will possess. Each property can either be
#' a named string (specifying the class), or a call to [new_property()],
Expand Down
31 changes: 18 additions & 13 deletions R/valid.R
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
#' Validate an R7 object
#'
#' @description
#' `validate()` calls the validator of an R7 object. This is done automatically
#' when creating new objects (at the end of [new_object()]) and when setting
#' any property with [prop<-].
#' `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, modifies the object, then
#' revalidates. This is useful when a sequence of operations would otherwise
#' lead an object to be temporarily invalid.
#' 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. It should only be used rarely, and in performance critical code where
Expand Down Expand Up @@ -68,22 +70,24 @@ validate <- function(object, properties = TRUE) {
# is likely to return spurious errors
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 Invalid <obj> properties:?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 length(errors) == 0 branch. Hmm, I think I still prefer an immediate error - but only because I would mention in the header that it was the properties that were invalid (I don't feel strongly about this if you want to keep it as is)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good idea.

if (properties) {
errors <- validate_properties(object, class)
} else {
errors <- character()
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)
}
}

# Next, recursively validate the object
if (length(errors) == 0) {
while(!is.null(class) && is_class(class)) {
errors <- c(errors, class@validator(object))
class <- prop_safely(class, "parent")
}
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) {
bullets <- paste0("- ", errors, collapse = "\n")
msg <- sprintf("Invalid %s object:\n%s", obj_desc(object), bullets)
msg <- sprintf("%s object is invalid:\n%s", obj_desc(object), bullets)
stop(msg, call. = FALSE)
}

Expand Down Expand Up @@ -128,5 +132,6 @@ valid_implicitly <- function(object, fun) {
attr(object, ".should_validate") <- FALSE
out <- fun(object)
attr(out, ".should_validate") <- old
invisible(out)

out
}
2 changes: 1 addition & 1 deletion R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ new_base_class <- function(name) {
validator = function(object) {
data <- unclass(object)
if (!name %in% .class2(data)) {
sprintf("`.data` must be <%s> not %s", name, obj_desc(data))
sprintf("Underlying data must be <%s> not %s", name, obj_desc(data))
}
}
)
Expand Down
18 changes: 14 additions & 4 deletions man/new_class.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions man/validate.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions tests/testthat/_snaps/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@

# constructor types check their values

Invalid <integer> object:
- `.data` must be <integer> not <character>
<integer> object is invalid:
- Underlying data must be <integer> not <character>

2 changes: 1 addition & 1 deletion tests/testthat/_snaps/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Code
range(start = "x", end = "y")
Error <simpleError>
Invalid <range> object:
<range> object properties are invalid:
- <range>@start must be of class <integer> or <double>, not <character>
- <range>@end must be of class <integer> or <double>, not <character>

Expand Down
12 changes: 6 additions & 6 deletions tests/testthat/_snaps/valid.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
attr(obj, "x") <- -1
validate(obj)
Error <simpleError>
Invalid <klass> object:
<klass> object is invalid:
- x must be positive
Code
attr(obj, "x") <- "y"
validate(obj)
Error <simpleError>
Invalid <klass> object:
<klass> object properties are invalid:
- <klass>@x must be of class <double>, not <character>

---
Expand All @@ -21,14 +21,14 @@
attr(obj, "x") <- -1
validate(obj)
Error <simpleError>
Invalid <klass2> object:
<klass2> object is invalid:
- x must be positive
Code
attr(obj, "x") <- "y"
attr(obj, "z") <- "y"
validate(obj)
Error <simpleError>
Invalid <klass2> object:
<klass2> object properties are invalid:
- <klass2>@x must be of class <double>, not <character>
- <klass2>@z must be of class <double>, not <character>

Expand All @@ -37,6 +37,6 @@
Code
validate(x)
Error <simpleError>
Invalid <Double> object:
- `.data` must be <double> not <character>
<Double> object is invalid:
- Underlying data must be <double> not <character>