forked from RConsortium/S7
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvalid.R
150 lines (135 loc) · 4.36 KB
/
valid.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#' Validate an S7 object
#'
#' @description
#' `validate()` ensures that an S7 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, 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
#' you are certain a sequence of operations cannot produce an invalid object.
#' @param object An S7 object
#' @param fun A function to call on the object before validation.
#' @param recursive If `TRUE`, calls validator of parent classes recursively.
#' @param properties If `TRUE`, the default, checks property types before
#' executing the validator.
#' @returns Either `object` invisibly if valid, otherwise an error.
#' @export
#' @examples
#' # A range class might validate that the start is less than the end
#' Range <- new_class("Range",
#' properties = list(start = class_double, end = class_double),
#' validator = function(self) {
#' if (self@start >= self@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<-,
#' # which validates once at the end
#' rightwards <- function(r, x) {
#' props(r) <- list(start = r@start + x, end = r@end + x)
#' r
#' }
#' rightwards(r, 20)
validate <- function(object, recursive = TRUE, properties = TRUE) {
check_is_S7(object)
if (!is.null(attr(object, ".should_validate"))) {
return(invisible(object))
}
class <- S7_class(object)
# First, check property types - if these are incorrect, the validator
# is likely to return spurious errors
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)
}
}
# Next, recursively validate the object
errors <- character()
repeat {
error <- class_validate(class, object)
if (is.null(error)) {
} else if (is.character(error)) {
append(errors) <- error
} else {
stop(sprintf(
"%s validator must return NULL or a character, not <%s>.",
obj_desc(class), typeof(error)
))
}
if (!is_class(class) || !recursive) break
class <- class@parent
}
# If needed, report errors
if (length(errors) > 0) {
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_obj in class@properties) {
# Don't validate dynamic properties
if (!is.null(prop_obj$getter)) {
next
}
value <- prop(object, prop_obj$name)
errors <- c(errors, prop_validate(prop_obj, value))
}
errors
}
#' @rdname validate
#' @export
valid_eventually <- function(object, fun) {
old <- attr(object, ".should_validate")
attr(object, ".should_validate") <- FALSE
out <- fun(object)
attr(out, ".should_validate") <- old
validate(out)
out
}
#' @rdname validate
#' @export
valid_implicitly <- function(object, fun) {
old <- attr(object, ".should_validate")
attr(object, ".should_validate") <- FALSE
out <- fun(object)
attr(out, ".should_validate") <- old
out
}