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
- .compare(actual, expected) ⇒ Object
-
.compare_array(actual_value, expected_value) ⇒ Object
A list schema only ever matches an actual Array.
- .compare_class(actual_value, expected_value) ⇒ Object
-
.compare_exact_array(actual_value, expected_value) ⇒ Object
Any other array => element-by-element match, sizes must be equal.
-
.compare_interface_array(actual_value, expected_value) ⇒ Object
[{ ...interface... }] => every element must match the single interface.
- .compare_key_paths_and_values(keys, actual, expected) ⇒ Object
-
.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.
- .compare_regexp(actual_value, expected_value) ⇒ Object
- .compare_simple_value(actual_value, expected_value) ⇒ Object
-
.compare_typed_array(actual_value, expected_value) ⇒ Object
[SomeClass] => every element must be an instance of SomeClass.
- .compare_values(actual_value, expected_value) ⇒ Object
-
.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. -
.dig_path(data, key_path) ⇒ Object
Digs a key path without raising when an intermediate value is not a Hash.
- .interface?(expected_value) ⇒ Boolean
-
.match(actual, expected) ⇒ Object
Top-level comparison.
- .same_key_structure?(actual, expected) ⇒ Boolean
- .simple_type?(expected_value) ⇒ Boolean
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.
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 = expected_value.call raise ArgumentError, "schema Proc must return an options Hash, got #{.class}" unless .is_a?(Hash) Constraints.match(actual_value, ) 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.
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
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
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
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 |