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



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

def compare(actual, expected)
  return false unless actual.is_a?(Hash)

  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

A list schema only ever matches an actual Array. Without this guard the branches below call Array methods on whatever the response contained, so a null or a scalar where a list was expected raised NoMethodError instead of failing the match.



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/rspec/json_api/schema_match.rb', line 108

def compare_array(actual_value, expected_value)
  return false unless actual_value.is_a?(Array)

  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



72
73
74
# File 'lib/rspec/json_api/schema_match.rb', line 72

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.



138
139
140
141
142
143
144
# File 'lib/rspec/json_api/schema_match.rb', line 138

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) ? match(actual_value[index], elem) : compare_values(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).



131
132
133
134
135
# File 'lib/rspec/json_api/schema_match.rb', line 131

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



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

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

A schema Proc describes the constraints for a value; it is called without arguments and must return the option Hash. A Proc that expects the value as an argument is a common misreading of the DSL, and calling it here would raise a bare "wrong number of arguments" from deep in the matcher.

Raises:

  • (ArgumentError)


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

def compare_proc(actual_value, expected_value)
  if declares_value_parameter?(expected_value)
    raise ArgumentError,
          "schema Proc must take no arguments; " \
          "write -> { { lambda: ->(value) { ... } } } to test the value itself"
  end

  options = expected_value.call
  raise ArgumentError, "schema Proc must return an options Hash, got #{options.class}" unless options.is_a?(Hash)

  Constraints.match(actual_value, options)
end

.compare_regexp(actual_value, expected_value) ⇒ Object



76
77
78
# File 'lib/rspec/json_api/schema_match.rb', line 76

def compare_regexp(actual_value, expected_value)
  actual_value.is_a?(String) && expected_value.match?(actual_value)
end

.compare_simple_value(actual_value, expected_value) ⇒ Object



146
147
148
# File 'lib/rspec/json_api/schema_match.rb', line 146

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.



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

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



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

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

.declares_value_parameter?(callable) ⇒ Boolean

A non-lambda Proc reports its block parameters as optional, so proc { |value| ... } has to be caught on the parameter list rather than on arity. A bare splat states no expectation and is left alone.

Returns:

  • (Boolean)


100
101
102
# File 'lib/rspec/json_api/schema_match.rb', line 100

def declares_value_parameter?(callable)
  callable.parameters.any? { |type, _name| %i[req opt keyreq].include?(type) }
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.



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

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)


154
155
156
# File 'lib/rspec/json_api/schema_match.rb', line 154

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

.match(actual, expected) ⇒ Object

Top-level comparison. Applies shape guards to objects and collections, then uses the same value dispatch as nested schema values.



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

def match(actual, expected)
  case expected
  when Array
    compare_array(actual, expected)
  when Hash
    return false unless actual.is_a?(Hash)
    return false unless same_key_structure?(actual, expected)

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

.same_key_structure?(actual, expected) ⇒ Boolean

Returns:

  • (Boolean)


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

def same_key_structure?(actual, expected)
  Traversal.same_key_structure?(actual, expected)
end

.simple_type?(expected_value) ⇒ Boolean

Returns:

  • (Boolean)


150
151
152
# File 'lib/rspec/json_api/schema_match.rb', line 150

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