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

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, message)
  # TODO: Ability to validate all keys
  raise "#{join_path(parent_path, key)} is not #{message}"
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