Module: ClassyHash
- Defined in:
- lib/puppet_x/enterprisemodules/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
. rubocop:disable Lint/NonLocalExitFromIterator
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/puppet_x/enterprisemodules/classy_hash.rb', line 60 def self.check_multi(key, value, constraints, parent_path = nil) raise_error(parent_path, key, 'a valid multiple choice constraint (array must not be empty)') if constraints.empty? # Optimize the common case of a direct class match return if constraints.include?(value.class) constraints.each do |c| next if c == :optional begin check_one(key, value, c, parent_path) return rescue StandardError => 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 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.
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 156 157 158 159 160 161 |
# File 'lib/puppet_x/enterprisemodules/classy_hash.rb', line 102 def self.check_one(key, value, constraint, parent_path = nil) case constraint when Class # Constrain value to be a specific class if [TrueClass, FalseClass].include?(constraint) raise_error(parent_path, key, 'true or false') unless [true, false].include?(value) elsif !value.is_a?(constraint) raise_error(parent_path, key, "a/an #{constraint}") end when Hash # Recursively check nested Hashes raise_error(parent_path, key, 'a Hash') unless value.is_a?(Hash) validate(value, constraint, join_path(parent_path, key)) when Array # Multiple choice or array validation if constraint.length == 1 && constraint.first.is_a?(Array) # Array validation raise_error(parent_path, key, 'an Array') unless value.is_a?(Array) constraints = constraint.first value.each_with_index do |v, idx| check_multi(idx, v, constraints, join_path(parent_path, key)) end else # Multiple choice check_multi(key, value, constraint, parent_path) end when Proc # User-specified validator result = constraint.call(value) if result != true 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) raise_error(parent_path, key, 'an Integer') unless value.is_a?(Integer) elsif constraint.min.is_a?(Numeric) raise_error(parent_path, key, 'a Numeric') unless value.is_a?(Numeric) elsif constraint.min.is_a?(String) raise_error(parent_path, key, 'a String') unless value.is_a?(String) end raise_error(parent_path, key, "in range #{constraint.inspect}") unless constraint.cover?(value) when :optional # Optional key marker in multiple choice validators nil else # Unknown schema constraint raise_error(parent_path, key, "a valid schema constraint: #{constraint.inspect}") end nil end |
.join_path(parent_path, key) ⇒ Object
163 164 165 |
# File 'lib/puppet_x/enterprisemodules/classy_hash.rb', line 163 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
.
86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/puppet_x/enterprisemodules/classy_hash.rb', line 86 def self.multiconstraint_string(constraints) constraints.map do |c| case c when Hash '{...schema...}' when Array "[#{multiconstraint_string(c)}]" when :optional nil else c.inspect end 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>”.
169 170 171 172 |
# File 'lib/puppet_x/enterprisemodules/classy_hash.rb', line 169 def self.raise_error(parent_path, key, ) # TODO: Ability to validate all keys raise "#{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.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# File 'lib/puppet_x/enterprisemodules/classy_hash.rb', line 17 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) check_one(key, hash[key], constraint, parent_path) elsif !(constraint.is_a?(Array) && constraint.include?(:optional)) 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. rubocop:disable Style/OptionalBooleanParameter
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/puppet_x/enterprisemodules/classy_hash.rb', line 36 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? # rubocop:disable Style/GuardClause 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 # rubocop:enable Style/GuardClause end # TODO: Strict validation for nested schemas as well validate(hash, schema, parent_path) end |