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 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