Module: RSpec::JsonApi::SchemaMatch

Defined in:
lib/rspec/json_api/schema_match.rb

Overview

SchemaMatch compares parsed JSON (a Hash, an Array, or a scalar) against an expected schema. It is the single entry point behind the match_json_schema matcher: callers hand it the actual and expected values and it dispatches on shape internally, so the matcher does not need to know whether it is looking at an object or a collection.

Class Method Summary collapse

Class Method Details

.compare(actual, expected) ⇒ Object



35
36
37
38
39
40
41
# File 'lib/rspec/json_api/schema_match.rb', line 35

def compare(actual, expected)
  return false if actual.blank? && expected.present?

  keys = Traversal.deep_key_paths(expected) | Traversal.deep_key_paths(actual)

  compare_key_paths_and_values(keys, actual, expected)
end

.compare_array(actual_value, expected_value) ⇒ Object



86
87
88
89
90
91
92
93
94
# File 'lib/rspec/json_api/schema_match.rb', line 86

def compare_array(actual_value, expected_value)
  if simple_type?(expected_value)
    compare_typed_array(actual_value, expected_value)
  elsif interface?(expected_value)
    compare_interface_array(actual_value, expected_value)
  else
    compare_exact_array(actual_value, expected_value)
  end
end

.compare_class(actual_value, expected_value) ⇒ Object



74
75
76
# File 'lib/rspec/json_api/schema_match.rb', line 74

def compare_class(actual_value, expected_value)
  actual_value.instance_of?(expected_value)
end

.compare_exact_array(actual_value, expected_value) ⇒ Object

Any other array => element-by-element match, sizes must be equal.



114
115
116
117
118
119
120
# File 'lib/rspec/json_api/schema_match.rb', line 114

def compare_exact_array(actual_value, expected_value)
  return false if actual_value&.size != expected_value&.size

  expected_value.each_with_index.all? do |elem, index|
    elem.is_a?(Hash) ? compare(actual_value[index], elem) : compare_simple_value(actual_value[index], elem)
  end
end

.compare_interface_array(actual_value, expected_value) ⇒ Object

{ …interface… }

> every element must match the single interface.

Elements go through match (not compare) so each one is held to the same key-structure guard as a top-level object; otherwise an element with an extra null-valued key would slip through (nil == nil).



107
108
109
110
111
# File 'lib/rspec/json_api/schema_match.rb', line 107

def compare_interface_array(actual_value, expected_value)
  interface = expected_value[0]

  actual_value.all? { |elem| match(elem, interface) }
end

.compare_key_paths_and_values(keys, actual, expected) ⇒ Object



43
44
45
46
47
48
49
50
# File 'lib/rspec/json_api/schema_match.rb', line 43

def compare_key_paths_and_values(keys, actual, expected)
  keys.all? do |key_path|
    actual_value = dig_path(actual, key_path)
    expected_value = dig_path(expected, key_path)

    compare_values(actual_value, expected_value)
  end
end

.compare_proc(actual_value, expected_value) ⇒ Object



82
83
84
# File 'lib/rspec/json_api/schema_match.rb', line 82

def compare_proc(actual_value, expected_value)
  Constraints.match(actual_value, expected_value.call)
end

.compare_regexp(actual_value, expected_value) ⇒ Object



78
79
80
# File 'lib/rspec/json_api/schema_match.rb', line 78

def compare_regexp(actual_value, expected_value)
  expected_value.match?(actual_value.to_s)
end

.compare_simple_value(actual_value, expected_value) ⇒ Object



122
123
124
# File 'lib/rspec/json_api/schema_match.rb', line 122

def compare_simple_value(actual_value, expected_value)
  actual_value == expected_value
end

.compare_typed_array(actual_value, expected_value) ⇒ Object

SomeClass

> every element must be an instance of SomeClass.



97
98
99
100
101
# File 'lib/rspec/json_api/schema_match.rb', line 97

def compare_typed_array(actual_value, expected_value)
  type = expected_value[0]

  actual_value.all? { |elem| compare_class(elem, type) }
end

.compare_values(actual_value, expected_value) ⇒ Object



64
65
66
67
68
69
70
71
72
# File 'lib/rspec/json_api/schema_match.rb', line 64

def compare_values(actual_value, expected_value)
  case expected_value
  when Class  then compare_class(actual_value, expected_value)
  when Regexp then compare_regexp(actual_value, expected_value)
  when Proc   then compare_proc(actual_value, expected_value)
  when Array  then compare_array(actual_value, expected_value)
  else             compare_simple_value(actual_value, expected_value)
  end
end

.dig_path(data, key_path) ⇒ Object

Digs a key path without raising when an intermediate value is not a Hash. Plain Hash#dig raises TypeError if it walks into a scalar (e.g. a schema expects a nested object but the actual value is a String), so a mismatch would crash instead of failing the match.



56
57
58
59
60
61
62
# File 'lib/rspec/json_api/schema_match.rb', line 56

def dig_path(data, key_path)
  key_path.reduce(data) do |value, key|
    break nil unless value.is_a?(Hash)

    value[key]
  end
end

.interface?(expected_value) ⇒ Boolean

Returns:

  • (Boolean)


130
131
132
# File 'lib/rspec/json_api/schema_match.rb', line 130

def interface?(expected_value)
  expected_value.size == 1 && expected_value[0].is_a?(Hash)
end

.match(actual, expected) ⇒ Object

Top-level comparison. Applies the shape guards (class equality and, for objects, key-set equality) before recursing.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/rspec/json_api/schema_match.rb', line 15

def match(actual, expected)
  return false unless actual.instance_of?(expected.class)

  case expected
  when Array
    compare_array(actual, expected)
  when Hash
    return false unless same_key_structure?(actual, expected)

    compare(actual, expected)
  else
    compare_simple_value(actual, expected)
  end
end

.same_key_structure?(actual, expected) ⇒ Boolean

Returns:

  • (Boolean)


30
31
32
33
# File 'lib/rspec/json_api/schema_match.rb', line 30

def same_key_structure?(actual, expected)
  Traversal.deep_sort(Traversal.deep_keys(actual)) ==
    Traversal.deep_sort(Traversal.deep_keys(expected))
end

.simple_type?(expected_value) ⇒ Boolean

Returns:

  • (Boolean)


126
127
128
# File 'lib/rspec/json_api/schema_match.rb', line 126

def simple_type?(expected_value)
  expected_value.size == 1 && expected_value[0].instance_of?(Class)
end