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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: eb81cef4f4d479a0268add5597b92dd9cc2c38ed
4
- data.tar.gz: 476532b9fc8e6bb077d095e911cd529e9fdc9bc0
2
+ SHA256:
3
+ metadata.gz: 490881019a727e645eeae2ccd23f4b2312a04866009c6263ba2c4387b7835b62
4
+ data.tar.gz: 5fdaa401b537507dfc43d6b68d5491365e4de2a024b791b5e53d675f7a43a59a
5
5
  SHA512:
6
- metadata.gz: d747ebfcf3c9a615b6dcf40959857f2fbbe56b55d6ae8902da98dda0cfd187d706a4fa7b6db4fe549dce8bfe3e84a18e6a99ff1940e48a35757b11a3e76c9cfa
7
- data.tar.gz: a65a0978bc15a704acc880711163acf5b4132c094e5bd1e44b237278ab350f265efe3be529021de68f1c04a82c4a7fa2eb88d418fd97ed7f111691418327b415
6
+ metadata.gz: f4fb3144133993025ab2fe807171161098bdda173a55bce7290b73a428b956624ef08850e18edef3bc66b726dff07571cf834ecd5c54f65d83a18ce3973fb3a5
7
+ data.tar.gz: 977eba5e7d785419beec07acccdadf750fb8f99dbdce110d88013a224a7a33ce379746bff5652c5fb9e33d6b93f024aecbb592a8dfdb70965b0504f57a1553c7
@@ -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)2014 Deseret Book
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
- # Validates a +hash+ against a +schema+. The +parent_path+ parameter is used
11
- # internally to generate error messages.
12
- def self.validate(hash, schema, parent_path=nil)
13
- raise 'Must validate a Hash' unless hash.is_a?(Hash) # TODO: Allow validating other types by passing to #check_one?
14
- raise 'Schema must be a Hash' unless schema.is_a?(Hash) # TODO: Allow individual element validations?
15
-
16
- schema.each do |key, constraint|
17
- if hash.include?(key)
18
- self.check_one(key, hash[key], constraint, parent_path)
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
- # 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
- # Optimize the common case of a direct class match
56
- return if constraints.include?(value.class)
57
-
58
- error = nil
59
- constraints.each do |c|
60
- next if c == :optional
61
- begin
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
- end
38
+ }.join(', ')
71
39
  end
72
-
73
- self.raise_error(parent_path, key, "one of #{multiconstraint_string(constraints)}")
74
40
  end
75
41
 
76
- # Generates a semi-compact String describing the given +constraints+.
77
- def self.multiconstraint_string(constraints)
78
- constraints.map{|c|
79
- if c.is_a?(Hash)
80
- "{...schema...}"
81
- elsif c.is_a?(Array)
82
- "[#{self.multiconstraint_string(c)}]"
83
- elsif c == :optional
84
- nil
85
- else
86
- c.inspect
87
- end
88
- }.compact.join(', ')
89
- end
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
- self.raise_error(parent_path, key, "true or false")
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
- self.raise_error(parent_path, key, "a/an #{constraint}")
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
- self.raise_error(parent_path, key, "a Hash") unless value.is_a?(Hash)
107
- self.validate(value, constraint, self.join_path(parent_path, key))
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
- self.raise_error(parent_path, key, "an Array") unless value.is_a?(Array)
114
-
115
- constraints = constraint.first
116
- value.each_with_index do |v, idx|
117
- self.check_multi(idx, v, constraints, self.join_path(parent_path, key))
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(key, value, constraint, parent_path)
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
- self.raise_error(parent_path, key, "a String matching #{constraint.inspect}")
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
- self.raise_error(parent_path, key, result.is_a?(String) ? result : "accepted by Proc")
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
- self.raise_error(parent_path, key, "an Integer") unless value.is_a?(Integer)
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
- self.raise_error(parent_path, key, "a Numeric") unless value.is_a?(Numeric)
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
- self.raise_error(parent_path, key, "a String") unless value.is_a?(String)
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
- unless constraint.cover?(value)
148
- self.raise_error(parent_path, key, "in range #{constraint.inspect}")
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
- self.raise_error(parent_path, key, "a valid schema constraint: #{constraint.inspect}")
262
+ add_error(raise_below, errors, parent_path, key, constraint, value)
263
+ return false unless full
158
264
  end
159
265
 
160
- nil
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
- parent_path ? "#{parent_path}[#{key.inspect}]" : key.inspect
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 the given
168
- # +parent_path+ fails because the value "is not #{+message+}".
169
- def self.raise_error(parent_path, key, message)
170
- # TODO: Ability to validate all keys
171
- raise "#{self.join_path(parent_path, key)} is not #{message}"
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
 
@@ -1,12 +1,69 @@
1
1
  # Classy Hash extended validation generators
2
- # Copyright (C)2014 Deseret Book
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
- # Generates a ClassyHash constraint that ensures a value is equal to one of
9
- # the arguments in +args+.
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
- lambda {|v|
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.1.4
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: 2015-05-26 00:00:00.000000000 Z
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: mike@mikebourgeous.com
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: 1.9.3
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
- rubyforge_project:
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'