Module: ClassyHash
- Defined in:
- lib/utils/classy_hash.rb
Overview
This module contains the ClassyHash methods for making sure Ruby Hash objects match a given schema. ClassyHash runs fast by taking advantage of Ruby language features and avoiding object creation during validation.
Class Method Summary collapse
-
.check_multi(key, value, constraints, parent_path = nil) ⇒ Object
Raises an error unless the given
value
matches one of the given multiple choiceconstraints
. -
.check_one(key, value, constraint, parent_path = nil) ⇒ Object
Checks a single value against a single constraint.
- .join_path(parent_path, key) ⇒ Object
-
.multiconstraint_string(constraints) ⇒ Object
Generates a semi-compact String describing the given
constraints
. -
.raise_error(parent_path, key, message) ⇒ Object
Raises an error indicating that the given
key
under the givenparent_path
fails because the value “is not #<code>message</code>”. -
.validate(hash, schema, parent_path = nil) ⇒ Object
Validates a
hash
against aschema
. -
.validate_strict(hash, schema, verbose = false, parent_path = nil) ⇒ Object
As with #validate, but members not specified in the
schema
are forbidden.
Class Method Details
.check_multi(key, value, constraints, parent_path = nil) ⇒ Object
Raises an error unless the given value
matches one of the given multiple choice constraints
.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/utils/classy_hash.rb', line 50 def self.check_multi(key, value, constraints, parent_path=nil) if constraints.length == 0 self.raise_error(parent_path, key, "a valid multiple choice constraint (array must not be empty)") end # Optimize the common case of a direct class match return if constraints.include?(value.class) error = nil constraints.each do |c| next if c == :optional begin self.check_one(key, value, c, parent_path) return rescue => e # Throw schema and array errors immediately if (c.is_a?(Hash) && value.is_a?(Hash)) || (c.is_a?(Array) && value.is_a?(Array) && c.length == 1 && c.first.is_a?(Array)) raise e end end end self.raise_error(parent_path, key, "one of #{multiconstraint_string(constraints)}") end |
.check_one(key, value, constraint, parent_path = nil) ⇒ Object
Checks a single value against a single constraint.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/utils/classy_hash.rb', line 92 def self.check_one(key, value, constraint, parent_path=nil) case constraint when Class # Constrain value to be a specific class if constraint == TrueClass || constraint == FalseClass unless value == true || value == false self.raise_error(parent_path, key, "true or false") end elsif !value.is_a?(constraint) self.raise_error(parent_path, key, "a/an #{constraint}") end when Hash # Recursively check nested Hashes self.raise_error(parent_path, key, "a Hash") unless value.is_a?(Hash) self.validate(value, constraint, self.join_path(parent_path, key)) when Array # Multiple choice or array validation if constraint.length == 1 && constraint.first.is_a?(Array) # Array validation self.raise_error(parent_path, key, "an Array") unless value.is_a?(Array) constraints = constraint.first value.each_with_index do |v, idx| self.check_multi(idx, v, constraints, self.join_path(parent_path, key)) end else # Multiple choice self.check_multi(key, value, constraint, parent_path) end when Proc # User-specified validator result = constraint.call(value) if result != true self.raise_error(parent_path, key, result.is_a?(String) ? result : "accepted by Proc") end when Range # Range (with type checking for common classes) if constraint.min.is_a?(Integer) && constraint.max.is_a?(Integer) self.raise_error(parent_path, key, "an Integer") unless value.is_a?(Integer) elsif constraint.min.is_a?(Numeric) self.raise_error(parent_path, key, "a Numeric") unless value.is_a?(Numeric) elsif constraint.min.is_a?(String) self.raise_error(parent_path, key, "a String") unless value.is_a?(String) end unless constraint.cover?(value) self.raise_error(parent_path, key, "in range #{constraint.inspect}") end when :optional # Optional key marker in multiple choice validators nil else # Unknown schema constraint self.raise_error(parent_path, key, "a valid schema constraint: #{constraint.inspect}") end nil end |
.join_path(parent_path, key) ⇒ Object
157 158 159 |
# File 'lib/utils/classy_hash.rb', line 157 def self.join_path(parent_path, key) parent_path ? "#{parent_path}[#{key.inspect}]" : key.inspect end |
.multiconstraint_string(constraints) ⇒ Object
Generates a semi-compact String describing the given constraints
.
77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/utils/classy_hash.rb', line 77 def self.multiconstraint_string(constraints) constraints.map{|c| if c.is_a?(Hash) "{...schema...}" elsif c.is_a?(Array) "[#{self.multiconstraint_string(c)}]" elsif c == :optional nil else c.inspect end }.compact.join(', ') end |
.raise_error(parent_path, key, message) ⇒ Object
Raises an error indicating that the given key
under the given parent_path
fails because the value “is not #<code>message</code>”.
163 164 165 166 |
# File 'lib/utils/classy_hash.rb', line 163 def self.raise_error(parent_path, key, ) # TODO: Ability to validate all keys raise "#{self.join_path(parent_path, key)} is not #{}" end |
.validate(hash, schema, parent_path = nil) ⇒ Object
Validates a hash
against a schema
. The parent_path
parameter is used internally to generate error messages.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# File 'lib/utils/classy_hash.rb', line 12 def self.validate(hash, schema, parent_path=nil) raise 'Must validate a Hash' unless hash.is_a?(Hash) # TODO: Allow validating other types by passing to #check_one? raise 'Schema must be a Hash' unless schema.is_a?(Hash) # TODO: Allow individual element validations? schema.each do |key, constraint| if hash.include?(key) self.check_one(key, hash[key], constraint, parent_path) elsif !(constraint.is_a?(Array) && constraint.include?(:optional)) self.raise_error(parent_path, key, "present") end end nil end |
.validate_strict(hash, schema, verbose = false, parent_path = nil) ⇒ Object
As with #validate, but members not specified in the schema
are forbidden. Only the top-level schema is strictly validated. If verbose
is true, the names of unexpected keys will be included in the error message.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/utils/classy_hash.rb', line 30 def self.validate_strict(hash, schema, verbose=false, parent_path=nil) raise 'Must validate a Hash' unless hash.is_a?(Hash) # TODO: Allow validating other types by passing to #check_one? raise 'Schema must be a Hash' unless schema.is_a?(Hash) # TODO: Allow individual element validations? extra_keys = hash.keys - schema.keys unless extra_keys.empty? if verbose raise "Hash contains members (#{extra_keys.map(&:inspect).join(', ')}) not specified in schema" else raise 'Hash contains members not specified in schema' end end # TODO: Strict validation for nested schemas as well self.validate(hash, schema, parent_path) end |