Class: Core::Filtering::SchemaValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/core/filtering/hash_against_schema_validator.rb

Constant Summary collapse

MAX_RECURSION_DEPTH =
50

Instance Method Summary collapse

Constructor Details

#initialize(schema: {}, payload: {}, error_id: '') ⇒ SchemaValidator

Returns a new instance of SchemaValidator.



17
18
19
20
21
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 17

def initialize(schema: {}, payload: {}, error_id: '')
  @schema = schema
  @payload = payload
  @error_id = error_id
end

Instance Method Details

#exceeded_recursion_depth?(recursion_depth) ⇒ Boolean

Returns:

  • (Boolean)


96
97
98
99
100
101
102
103
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 96

def exceeded_recursion_depth?(recursion_depth)
  if recursion_depth >= MAX_RECURSION_DEPTH
    Utility::Logger.warn("Recursion depth for filtering validation exceeded. (Max recursion depth: #{MAX_RECURSION_DEPTH})")
    return true
  end

  false
end

#fields_constraint_violation(fields) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 161

def fields_constraint_violation(fields)
  {
    :state => Core::Filtering::ValidationStatus::INVALID,
    :errors => [
      {
        :ids => [@error_id],
        :messages => ["A fields constraint was violated for fields: '#{fields[:values].map { |v| v[:name] }}'."]
      }
    ]
  }
end

#fields_constraints_violated?(fields, payload) ⇒ Boolean

Returns:

  • (Boolean)


62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 62

def fields_constraints_violated?(fields, payload)
  return false if !fields.present? || !fields.is_a?(Hash)

  constraints = fields[:constraints]
  constraints = constraints.is_a?(Array) ? constraints : [constraints]

  constraints.each do |constraint|
    return true unless constraint.call(payload)
  end

  false
end

#is_required_value_missing?(snippet_field_value) ⇒ Boolean

Returns:

  • (Boolean)


114
115
116
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 114

def is_required_value_missing?(snippet_field_value)
  !snippet_field_value.present?
end

#required_value_missing(field) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 125

def required_value_missing(field)
  {
    :state => Core::Filtering::ValidationStatus::INVALID,
    :errors => [
      {
        :ids => [@error_id],
        :messages => ["Required value missing for field '#{field}'."]
      }
    ]
  }
end

#type_error_present?(field_name, schema_type, actual_value) ⇒ Boolean

Returns:

  • (Boolean)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 75

def type_error_present?(field_name, schema_type, actual_value)
  if schema_type.is_a?(Proc)
    result = schema_type.call(actual_value)

    # could already have a custom error message
    if result.is_a?(Array)
      is_valid, error_msg = result

      return !is_valid, error_msg
    end

    # could only return a single boolean
    return !result, 'Custom type matcher validation failed.'
  end

  error_msg = "Expected field type '#{schema_type}' for field '#{field_name}', but got value '#{actual_value.inspect}' of type '#{actual_value.class}'."
  return true, error_msg unless actual_value.is_a?(schema_type)

  false
end

#unexpected_errorObject



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 173

def unexpected_error
  {
    :state => Core::Filtering::ValidationStatus::INVALID,
    :errors => [
      {
        :ids => [@error_id],
        :messages => ['Unexpected error. Check logs for details.']
      }
    ]
  }
end

#unexpected_field(expected_fields, actual_fields) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 137

def unexpected_field(expected_fields, actual_fields)
  {
    :state => Core::Filtering::ValidationStatus::INVALID,
    :errors => [
      {
        :ids => [@error_id],
        :messages => ["Encountered unexpected fields '#{actual_fields}'. Expected: '#{expected_fields}'."]
      }
    ]
  }
end

#unexpected_field_present?(actual_field_names, expected_field_names) ⇒ Boolean

Returns:

  • (Boolean)


105
106
107
108
109
110
111
112
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 105

def unexpected_field_present?(actual_field_names, expected_field_names)
  return false unless actual_field_names.present?

  difference = actual_field_names - expected_field_names

  # we have field names, which we didn't expect
  !difference.empty?
end

#valid_snippetObject



118
119
120
121
122
123
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 118

def valid_snippet
  {
    :state => Core::Filtering::ValidationStatus::VALID,
    :errors => []
  }
end

#validate_against_schema(schema = @schema, payload = @payload, recursion_depth = 0) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 23

def validate_against_schema(schema = @schema, payload = @payload, recursion_depth = 0)
  # Prevent unintentional/intentional SystemStackErrors/crashes
  return unexpected_error if exceeded_recursion_depth?(recursion_depth)

  return valid_snippet unless schema.present?

  schema_fields = schema[:fields].is_a?(Hash) ? schema.dig(:fields, :values) : schema[:fields]
  snippet_field_names = payload&.keys&.map(&:to_s)
  schema_field_names = schema_fields.map { |field| field[:name] }

  return unexpected_field(schema_field_names, snippet_field_names) if unexpected_field_present?(snippet_field_names, schema_field_names)

  return fields_constraint_violation(schema[:fields]) if fields_constraints_violated?(schema[:fields], payload)

  schema_fields.each do |field|
    name = field[:name]
    type = field[:type]
    optional = field[:optional] || false

    snippet_field_value = payload.nil? ? nil : payload.with_indifferent_access[name]

    next if optional && (snippet_field_value.nil? || !snippet_field_value.present?)

    return required_value_missing(name) if is_required_value_missing?(snippet_field_value)

    type_error_present, error_message = type_error_present?(name, type, snippet_field_value)

    return wrong_type(error_message) if type_error_present

    if field[:fields].present?
      validation_result = validate_against_schema(field, snippet_field_value, recursion_depth + 1)

      return validation_result unless validation_result[:state] == Core::Filtering::ValidationStatus::VALID
    end
  end

  valid_snippet
end

#wrong_type(error_message) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
# File 'lib/core/filtering/hash_against_schema_validator.rb', line 149

def wrong_type(error_message)
  {
    :state => Core::Filtering::ValidationStatus::INVALID,
    :errors => [
      {
        :ids => [@error_id],
        :messages => [error_message]
      }
    ]
  }
end