classy_hash 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/classy_hash.rb +389 -107
- data/lib/classy_hash/generate.rb +61 -6
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4927a8047b123422e97f63378b847c94a97a17e
|
4
|
+
data.tar.gz: bcd0d314bb6a21956fd988e940122cb309d5ee2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5469c836aa915fbc73f8e5f813cf53f171d8da96968a40d3c116b159eaa0fde389dff32ee797a9b2b205c02c7022da440d40fe371af5e9743c4dd3170a5a1460
|
7
|
+
data.tar.gz: 797f7b0f12681b4a8febb6661334b3ad90aabcb6551af8707f4e1c9eb3211700b8cf73494c13d3f7750a3ed8c5276a13d6f603cf800b9a44fb0f8adabc90afa3
|
data/lib/classy_hash.rb
CHANGED
@@ -1,178 +1,460 @@
|
|
1
1
|
# Classy Hash: Keep Your Hashes Classy
|
2
2
|
# Created May 2014 by Mike Bourgeous, DeseretBook.com
|
3
|
-
# Copyright (C)
|
3
|
+
# Copyright (C)2016 Deseret Book and Contributors (see git history)
|
4
4
|
# See LICENSE and README.md for details.
|
5
|
+
# frozen_string_literal: true
|
6
|
+
|
7
|
+
require 'set'
|
8
|
+
require 'securerandom'
|
5
9
|
|
6
10
|
# This module contains the ClassyHash methods for making sure Ruby Hash objects
|
7
11
|
# match a given schema. ClassyHash runs fast by taking advantage of Ruby
|
8
12
|
# language features and avoiding object creation during validation.
|
9
13
|
module ClassyHash
|
10
|
-
#
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
elsif !(constraint.is_a?(Array) && constraint.include?(:optional))
|
20
|
-
self.raise_error(parent_path, key, "present")
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
nil
|
25
|
-
end
|
26
|
-
|
27
|
-
# As with #validate, but members not specified in the +schema+ are forbidden.
|
28
|
-
# Only the top-level schema is strictly validated. If +verbose+ is true, the
|
29
|
-
# names of unexpected keys will be included in the error message.
|
30
|
-
def self.validate_strict(hash, schema, verbose=false, parent_path=nil)
|
31
|
-
raise 'Must validate a Hash' unless hash.is_a?(Hash) # TODO: Allow validating other types by passing to #check_one?
|
32
|
-
raise 'Schema must be a Hash' unless schema.is_a?(Hash) # TODO: Allow individual element validations?
|
14
|
+
# Raised when a validation fails. Allows ClassyHash#validate_full to
|
15
|
+
# continue validation and gather all errors.
|
16
|
+
class SchemaViolationError < StandardError
|
17
|
+
# The list of errors passed to the constructor. Contains an Array of Hashes:
|
18
|
+
# [
|
19
|
+
# { full_path: ClassyHash.join_path(parent_path, key), message: "something the full_path was supposed to be" },
|
20
|
+
# ...
|
21
|
+
# ]
|
22
|
+
attr_reader :entries
|
33
23
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
else
|
39
|
-
raise 'Hash contains members not specified in schema'
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
# TODO: Strict validation for nested schemas as well
|
44
|
-
|
45
|
-
self.validate(hash, schema, parent_path)
|
46
|
-
end
|
47
|
-
|
48
|
-
# Raises an error unless the given +value+ matches one of the given multiple
|
49
|
-
# choice +constraints+.
|
50
|
-
def self.check_multi(key, value, constraints, parent_path=nil)
|
51
|
-
if constraints.length == 0
|
52
|
-
self.raise_error(parent_path, key, "a valid multiple choice constraint (array must not be empty)")
|
24
|
+
# Initializes a schema violation error with the given list of schema
|
25
|
+
# +errors+.
|
26
|
+
def initialize(errors = [])
|
27
|
+
@entries = errors
|
53
28
|
end
|
54
29
|
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
self.check_one(key, value, c, parent_path)
|
63
|
-
return
|
64
|
-
rescue => e
|
65
|
-
# Throw schema and array errors immediately
|
66
|
-
if (c.is_a?(Hash) && value.is_a?(Hash)) ||
|
67
|
-
(c.is_a?(Array) && value.is_a?(Array) && c.length == 1 && c.first.is_a?(Array))
|
68
|
-
raise e
|
30
|
+
# Joins all errors passed to the constructor into a comma-separated String.
|
31
|
+
def to_s
|
32
|
+
@msg ||= @entries.map{|e|
|
33
|
+
begin
|
34
|
+
"#{e[:full_path]} is not #{e[:message]}"
|
35
|
+
rescue
|
36
|
+
"ERR: #{e.inspect}"
|
69
37
|
end
|
70
|
-
|
38
|
+
}.join(', ')
|
71
39
|
end
|
72
|
-
|
73
|
-
self.raise_error(parent_path, key, "one of #{multiconstraint_string(constraints, value)}")
|
74
40
|
end
|
75
41
|
|
76
|
-
#
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
42
|
+
# Internal symbol representing the absence of a value for error message
|
43
|
+
# generation. Generated at runtime to prevent potential malicious use of the
|
44
|
+
# no-value symbol.
|
45
|
+
NO_VALUE = "__ch_no_value_#{SecureRandom.hex(10)}".to_sym
|
46
|
+
|
47
|
+
# Validates a +value+ against a ClassyHash +constraint+. Typically +value+
|
48
|
+
# is a Hash and +constraint+ is a ClassyHash schema.
|
49
|
+
#
|
50
|
+
# Returns false if validation fails and raise_errors was false. Otherwise
|
51
|
+
# returns true.
|
52
|
+
#
|
53
|
+
# Parameters:
|
54
|
+
# value - The Hash or other value to validate.
|
55
|
+
# constraint - The schema or single constraint against which to validate.
|
56
|
+
# :strict - If true, rejects Hashes with members not in the schema.
|
57
|
+
# Applies to the top level and to nested Hashes.
|
58
|
+
# :full - If true, gathers all invalid values. If false, stops checking at
|
59
|
+
# the first invalid value.
|
60
|
+
# :verbose - If true, the error message for failed strictness will include
|
61
|
+
# the names of the unexpected keys. Note that this can be a
|
62
|
+
# security risk if the key names are controlled by an attacker and
|
63
|
+
# the result is sent via HTTPS (see e.g. the CRIME attack).
|
64
|
+
# :raise_errors - If true, any errors will be raised. If false, they will
|
65
|
+
# be returned as a String. Default is true.
|
66
|
+
# :errors - Used internally for aggregating error messages. You can also
|
67
|
+
# pass in an Array here to collect any errors (useful if
|
68
|
+
# raise_errors is false). If you pass a non-empty array,
|
69
|
+
# validation will fail.
|
70
|
+
# :parent_path - Used internally for tracking the current validation path
|
71
|
+
# in error messages (e.g. :key1[:key2][0]).
|
72
|
+
# :key - Used internally for tracking the current validation key in error
|
73
|
+
# messages (e.g. :key1 or 0).
|
74
|
+
#
|
75
|
+
# Examples:
|
76
|
+
# ClassyHash.validate({a: 1}, {a: Integer})
|
77
|
+
# ClassyHash.validate(1, Integer)
|
78
|
+
def self.validate(value, constraint, strict: false, full: false, verbose: false,
|
79
|
+
raise_errors: true, errors: nil, parent_path: nil, key: NO_VALUE)
|
80
|
+
errors = [] if errors.nil? && (full || !raise_errors)
|
81
|
+
raise_below = raise_errors && !full
|
94
82
|
|
95
|
-
# Checks a single value against a single constraint.
|
96
|
-
def self.check_one(key, value, constraint, parent_path=nil)
|
97
83
|
case constraint
|
98
84
|
when Class
|
99
85
|
# Constrain value to be a specific class
|
100
86
|
if constraint == TrueClass || constraint == FalseClass
|
101
87
|
unless value == true || value == false
|
102
|
-
|
88
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
89
|
+
return false unless full
|
103
90
|
end
|
104
91
|
elsif !value.is_a?(constraint)
|
105
|
-
|
92
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
93
|
+
return false unless full
|
106
94
|
end
|
107
95
|
|
108
96
|
when Hash
|
109
97
|
# Recursively check nested Hashes
|
110
|
-
|
111
|
-
|
98
|
+
if !value.is_a?(Hash)
|
99
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
100
|
+
return false unless full
|
101
|
+
else
|
102
|
+
if strict
|
103
|
+
extra_keys = value.keys - constraint.keys
|
104
|
+
if extra_keys.any?
|
105
|
+
if verbose
|
106
|
+
msg = "valid: contains members #{extra_keys.map(&:inspect).join(', ')} not specified in schema"
|
107
|
+
else
|
108
|
+
msg = 'valid: contains members not specified in schema'
|
109
|
+
end
|
110
|
+
|
111
|
+
add_error(raise_below, errors, parent_path, key, msg, NO_VALUE)
|
112
|
+
return false unless full
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
parent_path = join_path(parent_path, key)
|
117
|
+
|
118
|
+
constraint.each do |k, c|
|
119
|
+
if value.include?(k)
|
120
|
+
# TODO: Benchmark how much slower allocating a state object is than
|
121
|
+
# passing lots of parameters?
|
122
|
+
res = self.validate(
|
123
|
+
value[k],
|
124
|
+
c,
|
125
|
+
strict: strict,
|
126
|
+
full: full,
|
127
|
+
verbose: verbose,
|
128
|
+
raise_errors: raise_below,
|
129
|
+
parent_path: parent_path,
|
130
|
+
key: k,
|
131
|
+
errors: errors
|
132
|
+
)
|
133
|
+
return false unless res || full
|
134
|
+
elsif !(c.is_a?(Array) && c.first == :optional)
|
135
|
+
add_error(raise_below, errors, parent_path, k, "present", NO_VALUE)
|
136
|
+
return false unless full
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
112
140
|
|
113
141
|
when Array
|
114
142
|
# Multiple choice or array validation
|
115
143
|
if constraint.length == 1 && constraint.first.is_a?(Array)
|
116
144
|
# Array validation
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
145
|
+
if !value.is_a?(Array)
|
146
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
147
|
+
return false unless full
|
148
|
+
else
|
149
|
+
constraints = constraint.first
|
150
|
+
value.each_with_index do |v, idx|
|
151
|
+
res = self.check_multi(
|
152
|
+
v,
|
153
|
+
constraints,
|
154
|
+
strict: strict,
|
155
|
+
full: full,
|
156
|
+
verbose: verbose,
|
157
|
+
raise_errors: raise_below,
|
158
|
+
parent_path: join_path(parent_path, key),
|
159
|
+
key: idx,
|
160
|
+
errors: errors
|
161
|
+
)
|
162
|
+
return false unless res || full
|
163
|
+
end
|
122
164
|
end
|
123
165
|
else
|
124
166
|
# Multiple choice
|
125
|
-
self.check_multi(
|
167
|
+
res = self.check_multi(
|
168
|
+
value,
|
169
|
+
constraint,
|
170
|
+
strict: strict,
|
171
|
+
full: full,
|
172
|
+
verbose: verbose,
|
173
|
+
raise_errors: raise_below,
|
174
|
+
parent_path: parent_path,
|
175
|
+
key: key,
|
176
|
+
errors: errors
|
177
|
+
)
|
178
|
+
return false unless res || full
|
126
179
|
end
|
127
180
|
|
128
181
|
when Regexp
|
129
182
|
# Constrain value to be a String matching a Regexp
|
130
183
|
unless value.is_a?(String) && value =~ constraint
|
131
|
-
|
184
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
185
|
+
return false unless full
|
132
186
|
end
|
133
187
|
|
134
188
|
when Proc
|
135
189
|
# User-specified validator
|
136
190
|
result = constraint.call(value)
|
137
191
|
if result != true
|
138
|
-
|
192
|
+
if result.is_a?(String)
|
193
|
+
add_error(raise_below, errors, parent_path, key, result, NO_VALUE)
|
194
|
+
else
|
195
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
196
|
+
end
|
197
|
+
return false unless full
|
139
198
|
end
|
140
199
|
|
141
200
|
when Range
|
142
201
|
# Range (with type checking for common classes)
|
202
|
+
range_type_valid = true
|
203
|
+
|
143
204
|
if constraint.min.is_a?(Integer) && constraint.max.is_a?(Integer)
|
144
|
-
|
205
|
+
unless value.is_a?(Integer)
|
206
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
207
|
+
return false unless full
|
208
|
+
range_type_valid = false
|
209
|
+
end
|
145
210
|
elsif constraint.min.is_a?(Numeric)
|
146
|
-
|
211
|
+
unless value.is_a?(Numeric)
|
212
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
213
|
+
return false unless full
|
214
|
+
range_type_valid = false
|
215
|
+
end
|
147
216
|
elsif constraint.min.is_a?(String)
|
148
|
-
|
217
|
+
unless value.is_a?(String)
|
218
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
219
|
+
return false unless full
|
220
|
+
range_type_valid = false
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
if range_type_valid && !constraint.cover?(value)
|
225
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
226
|
+
return false unless full
|
227
|
+
end
|
228
|
+
|
229
|
+
when Set
|
230
|
+
# Set/enumeration
|
231
|
+
unless constraint.include?(value)
|
232
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
233
|
+
return false unless full
|
149
234
|
end
|
150
235
|
|
151
|
-
|
152
|
-
|
236
|
+
when CH::G::Composite
|
237
|
+
constraint.constraints.each do |c|
|
238
|
+
result = self.validate(
|
239
|
+
value,
|
240
|
+
c,
|
241
|
+
strict: strict,
|
242
|
+
full: full,
|
243
|
+
verbose: verbose,
|
244
|
+
raise_errors: false,
|
245
|
+
parent_path: parent_path,
|
246
|
+
key: key,
|
247
|
+
errors: nil
|
248
|
+
)
|
249
|
+
|
250
|
+
if constraint.negate == result
|
251
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
252
|
+
return false unless full
|
253
|
+
break
|
254
|
+
end
|
153
255
|
end
|
154
256
|
|
155
257
|
when :optional
|
156
|
-
# Optional key marker in multiple choice validators
|
157
|
-
nil
|
258
|
+
# Optional key marker in multiple choice validators (do nothing)
|
158
259
|
|
159
260
|
else
|
160
261
|
# Unknown schema constraint
|
161
|
-
|
262
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
263
|
+
return false unless full
|
162
264
|
end
|
163
265
|
|
164
|
-
|
266
|
+
# If full was true, we need to raise here now that all the errors were
|
267
|
+
# gathered.
|
268
|
+
if raise_errors && errors && errors.any?
|
269
|
+
raise SchemaViolationError, errors
|
270
|
+
end
|
271
|
+
|
272
|
+
errors.nil? || errors.empty?
|
273
|
+
end
|
274
|
+
|
275
|
+
# Deprecated. Retained for compatibility with v0.1.x. Calls .validate with
|
276
|
+
# :strict set to true. If +verbose+ is true, the names of unexpected keys
|
277
|
+
# will be included in the error message.
|
278
|
+
def self.validate_strict(hash, schema, verbose=false, parent_path=nil)
|
279
|
+
validate(hash, schema, parent_path: parent_path, verbose: verbose, strict: true)
|
165
280
|
end
|
166
281
|
|
282
|
+
# Raises an error unless the given +value+ matches one of the given multiple
|
283
|
+
# choice +constraints+. Other parameters are used for internal state. If
|
284
|
+
# +full+ is true, the error message for an invalid value will include the
|
285
|
+
# errors for all of the failing components of the multiple choice constraint.
|
286
|
+
def self.check_multi(value, constraints, strict: nil, full: nil, verbose: nil, raise_errors: nil,
|
287
|
+
parent_path: nil, key: nil, errors: nil)
|
288
|
+
if constraints.length == 0 || constraints.length == 1 && constraints.first == :optional
|
289
|
+
return add_error(raise_errors, errors,
|
290
|
+
parent_path,
|
291
|
+
key,
|
292
|
+
"a valid multiple choice constraint (array must not be empty)",
|
293
|
+
NO_VALUE
|
294
|
+
)
|
295
|
+
end
|
296
|
+
|
297
|
+
# Optimize the common case of a direct class match
|
298
|
+
return true if constraints.include?(value.class)
|
299
|
+
|
300
|
+
local_errors = []
|
301
|
+
constraints.each do |c|
|
302
|
+
next if c == :optional
|
303
|
+
|
304
|
+
constraint_errors = []
|
305
|
+
|
306
|
+
# Only need one match to accept the value, so return if one is found
|
307
|
+
return true if self.validate(
|
308
|
+
value,
|
309
|
+
c,
|
310
|
+
strict: strict,
|
311
|
+
full: full,
|
312
|
+
verbose: verbose,
|
313
|
+
raise_errors: false,
|
314
|
+
parent_path: parent_path,
|
315
|
+
key: key,
|
316
|
+
errors: constraint_errors
|
317
|
+
)
|
318
|
+
|
319
|
+
local_errors << { constraint: c, errors: constraint_errors }
|
320
|
+
end
|
321
|
+
|
322
|
+
# Accumulate all errors if full, the constraint with the most similar keys
|
323
|
+
# and fewest errors if a Hash or Array, or just the constraint with the
|
324
|
+
# fewest errors otherwise. This doesn't always choose the intended
|
325
|
+
# constraint for error reporting, which would require a more complex
|
326
|
+
# algorithm.
|
327
|
+
#
|
328
|
+
# See https://github.com/deseretbook/classy_hash/pull/16#issuecomment-257484267
|
329
|
+
if full
|
330
|
+
local_errors.map!{|e| e[:errors] }
|
331
|
+
local_errors.flatten!
|
332
|
+
elsif value.is_a?(Hash)
|
333
|
+
# Prefer error messages from similar-looking hash constraints for hashes
|
334
|
+
local_errors = local_errors.min_by{|err|
|
335
|
+
c = err[:constraint]
|
336
|
+
e = err[:errors]
|
337
|
+
|
338
|
+
if c.is_a?(Hash)
|
339
|
+
keydiff = (c.keys | value.keys) - (c.keys & value.keys)
|
340
|
+
[ keydiff.length, e.length ]
|
341
|
+
else
|
342
|
+
[ 1<<30, e.length ] # Put non-hashes after hashes
|
343
|
+
end
|
344
|
+
}[:errors]
|
345
|
+
elsif value.is_a?(Array)
|
346
|
+
# Prefer error messages from array constraints for arrays
|
347
|
+
local_errors = local_errors.min_by{|err|
|
348
|
+
c = err[:constraint]
|
349
|
+
[
|
350
|
+
c.is_a?(Array) ? (c.first.is_a?(Array) ? 0 : 1) : 2,
|
351
|
+
err[:errors].length
|
352
|
+
]
|
353
|
+
}[:errors]
|
354
|
+
else
|
355
|
+
# FIXME: if full is false, e.length should always be 1 (or 2 if strict)
|
356
|
+
# Also, array and multiple-choice errors have lots of room for improvement
|
357
|
+
local_errors = local_errors.min_by{|e| e[:errors].length }[:errors]
|
358
|
+
end
|
359
|
+
|
360
|
+
errors.concat(local_errors) if errors
|
361
|
+
add_error(raise_errors, errors || local_errors, parent_path, key, constraints, value)
|
362
|
+
end
|
363
|
+
|
364
|
+
# Generates a String describing the +value+'s failure to match the
|
365
|
+
# +constraint+. The value itself should not be included in the string to
|
366
|
+
# avoid attacker-controlled plaintext. If +value+ is CH::NO_VALUE, then
|
367
|
+
# generic error messages will be used for constraints (e.g. Procs) that would
|
368
|
+
# otherwise have been value-dependent.
|
369
|
+
def self.constraint_string(constraint, value)
|
370
|
+
case constraint
|
371
|
+
when Hash
|
372
|
+
"a Hash matching {schema with keys #{constraint.keys.inspect}}"
|
373
|
+
|
374
|
+
when Class
|
375
|
+
if constraint == TrueClass || constraint == FalseClass
|
376
|
+
'true or false'
|
377
|
+
else
|
378
|
+
"a/an #{constraint}"
|
379
|
+
end
|
380
|
+
|
381
|
+
when Array
|
382
|
+
if constraint.length == 1 && constraint.first.is_a?(Array)
|
383
|
+
"an Array of #{constraint_string(constraint.first, NO_VALUE)}"
|
384
|
+
else
|
385
|
+
"one of #{constraint.map{|c| constraint_string(c, value) }.join(', ')}"
|
386
|
+
end
|
387
|
+
|
388
|
+
when Regexp
|
389
|
+
"a String matching #{constraint.inspect}"
|
390
|
+
|
391
|
+
when Proc
|
392
|
+
if value != NO_VALUE && (result = constraint.call(value)).is_a?(String)
|
393
|
+
result
|
394
|
+
else
|
395
|
+
"accepted by Proc"
|
396
|
+
end
|
397
|
+
|
398
|
+
when Range
|
399
|
+
base = "in range #{constraint.inspect}"
|
400
|
+
|
401
|
+
if constraint.min.is_a?(Integer) && constraint.max.is_a?(Integer)
|
402
|
+
"an Integer #{base}"
|
403
|
+
elsif constraint.min.is_a?(Numeric)
|
404
|
+
"a Numeric #{base}"
|
405
|
+
elsif constraint.min.is_a?(String)
|
406
|
+
"a String #{base}"
|
407
|
+
else
|
408
|
+
base
|
409
|
+
end
|
410
|
+
|
411
|
+
when Set
|
412
|
+
"an element of #{constraint.to_a.inspect}"
|
413
|
+
|
414
|
+
when CH::G::Composite
|
415
|
+
constraint.describe(value)
|
416
|
+
|
417
|
+
when :optional
|
418
|
+
"absent (marked as :optional)"
|
419
|
+
|
420
|
+
else
|
421
|
+
"a valid schema constraint: #{constraint.inspect}"
|
422
|
+
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
# Joins parent_path and key for display in error messages.
|
167
427
|
def self.join_path(parent_path, key)
|
168
|
-
|
428
|
+
if parent_path
|
429
|
+
"#{parent_path}[#{key.inspect}]"
|
430
|
+
elsif key == NO_VALUE
|
431
|
+
nil
|
432
|
+
else
|
433
|
+
key.inspect
|
434
|
+
end
|
169
435
|
end
|
170
436
|
|
171
|
-
# Raises an error indicating that the given +key+ under
|
172
|
-
# +parent_path+ fails because the value
|
173
|
-
|
174
|
-
|
175
|
-
|
437
|
+
# Raises or adds to +errors+ an error indicating that the given +key+ under
|
438
|
+
# the given +parent_path+ fails because the value is not valid. If
|
439
|
+
# +constraint+ is a String, then it will be used as the error message.
|
440
|
+
# Otherwise
|
441
|
+
#
|
442
|
+
# If +raise_errors+ is true, raises an error immediately. Otherwise adds an
|
443
|
+
# error to +errors+.
|
444
|
+
#
|
445
|
+
# See .constraint_string.
|
446
|
+
def self.add_error(raise_errors, errors, parent_path, key, constraint, value)
|
447
|
+
message = constraint.is_a?(String) ? constraint : constraint_string(constraint, value)
|
448
|
+
entry = { full_path: self.join_path(parent_path, key) || 'Top level', message: message }
|
449
|
+
|
450
|
+
if raise_errors
|
451
|
+
errors ||= []
|
452
|
+
errors << entry
|
453
|
+
raise SchemaViolationError, errors
|
454
|
+
else
|
455
|
+
errors << entry if errors
|
456
|
+
return false
|
457
|
+
end
|
176
458
|
end
|
177
459
|
end
|
178
460
|
|
data/lib/classy_hash/generate.rb
CHANGED
@@ -1,12 +1,69 @@
|
|
1
1
|
# Classy Hash extended validation generators
|
2
|
-
# Copyright (C)
|
2
|
+
# Copyright (C)2016 Deseret Book and Contributors (see git history)
|
3
|
+
# frozen_string_literal: true
|
3
4
|
|
4
5
|
module ClassyHash
|
5
6
|
# This module contains helpers that generate constraints for common
|
6
7
|
# ClassyHash validation tasks.
|
7
8
|
module Generate
|
8
|
-
#
|
9
|
-
|
9
|
+
# Used by the .all and .not generators. Do not use directly.
|
10
|
+
class Composite
|
11
|
+
# Array of constraints to apply together.
|
12
|
+
attr_reader :constraints
|
13
|
+
|
14
|
+
# True if the constraints must all not match, false if they must all
|
15
|
+
# match.
|
16
|
+
attr_reader :negate
|
17
|
+
|
18
|
+
# Initializes a composite constraint with the given Array of
|
19
|
+
# +constraints+. If +negate+ is true, then the constraints must all fail
|
20
|
+
# for the value to pass.
|
21
|
+
def initialize(constraints, negate = false)
|
22
|
+
raise 'No constraints were given' if constraints.empty?
|
23
|
+
|
24
|
+
@constraints = constraints
|
25
|
+
@negate = negate
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns a String describing the composite constraint failing against
|
29
|
+
# the given +value+.
|
30
|
+
def describe(value)
|
31
|
+
"#{negate ? 'none' : 'all'} of [#{CH.constraint_string(constraints, value)}]"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generates a constraint that requires a value to match *all* of the given
|
36
|
+
# constraints. If no constraints are given, always passes.
|
37
|
+
#
|
38
|
+
# Raises an error if no constraints are given.
|
39
|
+
#
|
40
|
+
# Example:
|
41
|
+
# schema = {
|
42
|
+
# a: CH::G.all(Integer, 1..100, CH::G.not(Set.new([7, 13])))
|
43
|
+
# }
|
44
|
+
# ClassyHash.validate({ a: 25 }, schema)
|
45
|
+
def self.all(*constraints)
|
46
|
+
Composite.new(constraints.freeze)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Generates a constraint that requires a value to match *none* of the given
|
50
|
+
# constraints.
|
51
|
+
#
|
52
|
+
# Raises an error if no constraints are given.
|
53
|
+
#
|
54
|
+
# Example:
|
55
|
+
# schema = {
|
56
|
+
# a: CH::G.not(Rational, BigDecimal)
|
57
|
+
# }
|
58
|
+
# ClassyHash.validate({ a: 1.25 }, schema)
|
59
|
+
def self.not(*constraints)
|
60
|
+
Composite.new(constraints.freeze, true)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Deprecated. Generates a ClassyHash constraint that ensures a value is
|
64
|
+
# equal to one of the arguments in +args+.
|
65
|
+
#
|
66
|
+
# For new schemas, consider creating a Set with the enumeration elements.
|
10
67
|
#
|
11
68
|
# Example:
|
12
69
|
# schema = {
|
@@ -14,9 +71,7 @@ module ClassyHash
|
|
14
71
|
# }
|
15
72
|
# ClassyHash.validate({ a: 1 }, schema)
|
16
73
|
def self.enum(*args)
|
17
|
-
|
18
|
-
args.include?(v) || "an element of #{args.inspect}"
|
19
|
-
}
|
74
|
+
Set.new(args)
|
20
75
|
end
|
21
76
|
|
22
77
|
# Generates a constraint that imposes a length limitation (an exact length
|
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: classy_hash
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Deseret Book
|
8
8
|
- Mike Bourgeous
|
9
|
+
- Git Contributors
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2016-08
|
13
|
+
date: 2016-11-08 00:00:00.000000000 Z
|
13
14
|
dependencies: []
|
14
15
|
description: |2
|
15
16
|
Classy Hash is a schema validator for Ruby Hashes. You provide a simple
|