classy_hash 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c00ef449de055dd11f7d65c7b69078d335a5faac
4
- data.tar.gz: 08492cddaf3d56b469fd326290c4668dcf3d6dea
3
+ metadata.gz: c4927a8047b123422e97f63378b847c94a97a17e
4
+ data.tar.gz: bcd0d314bb6a21956fd988e940122cb309d5ee2a
5
5
  SHA512:
6
- metadata.gz: b0eb3ad07f5843c3c477165b74553103bc3f250b09742dffd0c6b6157bc639b46e1fbf83825eda66b2d8deb1369f375e02e8ab27c1e644958bfc27fe3ec9e6ed
7
- data.tar.gz: 79a18a2a41913d4d25b8ce8b15f47d5a4caac5b85ee91e77a762e9034f010496a28ddc2e83fecf86442ba10e0625c24ab32f3afb9663310ea5efbb5cb9b1c144
6
+ metadata.gz: 5469c836aa915fbc73f8e5f813cf53f171d8da96968a40d3c116b159eaa0fde389dff32ee797a9b2b205c02c7022da440d40fe371af5e9743c4dd3170a5a1460
7
+ data.tar.gz: 797f7b0f12681b4a8febb6661334b3ad90aabcb6551af8707f4e1c9eb3211700b8cf73494c13d3f7750a3ed8c5276a13d6f603cf800b9a44fb0f8adabc90afa3
@@ -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)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?
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
- 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
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
- # 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, value)}")
74
40
  end
75
41
 
76
- # Generates a semi-compact String describing the given +constraints+.
77
- def self.multiconstraint_string(constraints, value)
78
- constraints.map{|c|
79
- if c.is_a?(Hash)
80
- "{...schema...}"
81
- elsif c.is_a?(Array)
82
- "[#{self.multiconstraint_string(c, value)}]"
83
- elsif c.is_a?(Proc)
84
- c.call(value) || "a value accepted by #{c.inspect}"
85
- elsif c == :optional
86
- nil
87
- elsif c == TrueClass || c == FalseClass
88
- 'true or false'
89
- else
90
- c.inspect
91
- end
92
- }.compact.join(', ')
93
- 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
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
- 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
103
90
  end
104
91
  elsif !value.is_a?(constraint)
105
- 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
106
94
  end
107
95
 
108
96
  when Hash
109
97
  # Recursively check nested Hashes
110
- self.raise_error(parent_path, key, "a Hash") unless value.is_a?(Hash)
111
- 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
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
- self.raise_error(parent_path, key, "an Array") unless value.is_a?(Array)
118
-
119
- constraints = constraint.first
120
- value.each_with_index do |v, idx|
121
- 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
122
164
  end
123
165
  else
124
166
  # Multiple choice
125
- 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
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
- 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
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
- 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
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
- 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
145
210
  elsif constraint.min.is_a?(Numeric)
146
- 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
147
216
  elsif constraint.min.is_a?(String)
148
- 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
149
234
  end
150
235
 
151
- unless constraint.cover?(value)
152
- 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
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
- 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
162
264
  end
163
265
 
164
- 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)
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
- 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
169
435
  end
170
436
 
171
- # Raises an error indicating that the given +key+ under the given
172
- # +parent_path+ fails because the value "is not #{+message+}".
173
- def self.raise_error(parent_path, key, message)
174
- # TODO: Ability to validate all keys
175
- 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
176
458
  end
177
459
  end
178
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,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: classy_hash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
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-03 00:00:00.000000000 Z
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