classy_hash 0.1.4 → 1.0.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 +5 -5
- data/lib/classy_hash.rb +389 -103
- data/lib/classy_hash/generate.rb +61 -6
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 490881019a727e645eeae2ccd23f4b2312a04866009c6263ba2c4387b7835b62
|
4
|
+
data.tar.gz: 5fdaa401b537507dfc43d6b68d5491365e4de2a024b791b5e53d675f7a43a59a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4fb3144133993025ab2fe807171161098bdda173a55bce7290b73a428b956624ef08850e18edef3bc66b726dff07571cf834ecd5c54f65d83a18ce3973fb3a5
|
7
|
+
data.tar.gz: 977eba5e7d785419beec07acccdadf750fb8f99dbdce110d88013a224a7a33ce379746bff5652c5fb9e33d6b93f024aecbb592a8dfdb70965b0504f57a1553c7
|
data/lib/classy_hash.rb
CHANGED
@@ -1,174 +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?
|
33
|
-
|
34
|
-
extra_keys = hash.keys - schema.keys
|
35
|
-
unless extra_keys.empty?
|
36
|
-
if verbose
|
37
|
-
raise "Hash contains members (#{extra_keys.map(&:inspect).join(', ')}) not specified in schema"
|
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
|
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
|
47
23
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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)}")
|
74
40
|
end
|
75
41
|
|
76
|
-
#
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
90
82
|
|
91
|
-
# Checks a single value against a single constraint.
|
92
|
-
def self.check_one(key, value, constraint, parent_path=nil)
|
93
83
|
case constraint
|
94
84
|
when Class
|
95
85
|
# Constrain value to be a specific class
|
96
86
|
if constraint == TrueClass || constraint == FalseClass
|
97
87
|
unless value == true || value == false
|
98
|
-
|
88
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
89
|
+
return false unless full
|
99
90
|
end
|
100
91
|
elsif !value.is_a?(constraint)
|
101
|
-
|
92
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
93
|
+
return false unless full
|
102
94
|
end
|
103
95
|
|
104
96
|
when Hash
|
105
97
|
# Recursively check nested Hashes
|
106
|
-
|
107
|
-
|
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
|
108
140
|
|
109
141
|
when Array
|
110
142
|
# Multiple choice or array validation
|
111
143
|
if constraint.length == 1 && constraint.first.is_a?(Array)
|
112
144
|
# Array validation
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
118
164
|
end
|
119
165
|
else
|
120
166
|
# Multiple choice
|
121
|
-
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
|
122
179
|
end
|
123
180
|
|
124
181
|
when Regexp
|
125
182
|
# Constrain value to be a String matching a Regexp
|
126
183
|
unless value.is_a?(String) && value =~ constraint
|
127
|
-
|
184
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
185
|
+
return false unless full
|
128
186
|
end
|
129
187
|
|
130
188
|
when Proc
|
131
189
|
# User-specified validator
|
132
190
|
result = constraint.call(value)
|
133
191
|
if result != true
|
134
|
-
|
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
|
135
198
|
end
|
136
199
|
|
137
200
|
when Range
|
138
201
|
# Range (with type checking for common classes)
|
202
|
+
range_type_valid = true
|
203
|
+
|
139
204
|
if constraint.min.is_a?(Integer) && constraint.max.is_a?(Integer)
|
140
|
-
|
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
|
141
210
|
elsif constraint.min.is_a?(Numeric)
|
142
|
-
|
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
|
143
216
|
elsif constraint.min.is_a?(String)
|
144
|
-
|
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
|
145
234
|
end
|
146
235
|
|
147
|
-
|
148
|
-
|
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
|
149
255
|
end
|
150
256
|
|
151
257
|
when :optional
|
152
|
-
# Optional key marker in multiple choice validators
|
153
|
-
nil
|
258
|
+
# Optional key marker in multiple choice validators (do nothing)
|
154
259
|
|
155
260
|
else
|
156
261
|
# Unknown schema constraint
|
157
|
-
|
262
|
+
add_error(raise_below, errors, parent_path, key, constraint, value)
|
263
|
+
return false unless full
|
158
264
|
end
|
159
265
|
|
160
|
-
|
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)
|
161
280
|
end
|
162
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.
|
163
427
|
def self.join_path(parent_path, key)
|
164
|
-
|
428
|
+
if parent_path
|
429
|
+
"#{parent_path}[#{key.inspect}]"
|
430
|
+
elsif key == NO_VALUE
|
431
|
+
nil
|
432
|
+
else
|
433
|
+
key.inspect
|
434
|
+
end
|
165
435
|
end
|
166
436
|
|
167
|
-
# Raises an error indicating that the given +key+ under
|
168
|
-
# +parent_path+ fails because the value
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
172
458
|
end
|
173
459
|
end
|
174
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,21 +1,24 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: classy_hash
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.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:
|
13
|
+
date: 2020-09-17 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
|
16
17
|
schema Hash, and Classy Hash will make sure your data matches, providing
|
17
18
|
helpful error messages if it doesn't.
|
18
|
-
email:
|
19
|
+
email:
|
20
|
+
- webdev@deseretbook.com
|
21
|
+
- mike@mikebourgeous.com
|
19
22
|
executables: []
|
20
23
|
extensions: []
|
21
24
|
extra_rdoc_files: []
|
@@ -34,15 +37,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
37
|
requirements:
|
35
38
|
- - ">="
|
36
39
|
- !ruby/object:Gem::Version
|
37
|
-
version:
|
40
|
+
version: '2.3'
|
38
41
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
42
|
requirements:
|
40
43
|
- - ">="
|
41
44
|
- !ruby/object:Gem::Version
|
42
45
|
version: '0'
|
43
46
|
requirements: []
|
44
|
-
|
45
|
-
rubygems_version: 2.2.2
|
47
|
+
rubygems_version: 3.1.2
|
46
48
|
signing_key:
|
47
49
|
specification_version: 4
|
48
50
|
summary: 'Classy Hash: Keep your Hashes classy; a Hash schema validator'
|