From 049e26078c3320b6ff64ea799cc944efe3e16322 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Mon, 19 Oct 2026 18:49:29 +0200 Subject: [PATCH] make matchers composable --- lib/json_spec/matchers/be_json_eql.rb | 1 + lib/json_spec/matchers/have_json_path.rb | 1 + lib/json_spec/matchers/have_json_size.rb | 1 + lib/json_spec/matchers/have_json_type.rb | 1 + lib/json_spec/matchers/include_json.rb | 1 + spec/json_spec/matchers/be_json_eql_spec.rb | 5 +++ .../json_spec/matchers/have_json_path_spec.rb | 5 +++ .../json_spec/matchers/have_json_size_spec.rb | 5 +++ .../json_spec/matchers/have_json_type_spec.rb | 5 +++ spec/json_spec/matchers/include_json_spec.rb | 5 +++ spec/spec_helper.rb | 35 +++++++++++++++++++ 11 files changed, 65 insertions(+) diff --git a/lib/json_spec/matchers/be_json_eql.rb b/lib/json_spec/matchers/be_json_eql.rb index 533469c..66a8bfa 100644 --- a/lib/json_spec/matchers/be_json_eql.rb +++ b/lib/json_spec/matchers/be_json_eql.rb @@ -4,6 +4,7 @@ class BeJsonEql include JsonSpec::Helpers include JsonSpec::Exclusion include JsonSpec::Messages + include RSpec::Matchers::Composable attr_reader :expected, :actual diff --git a/lib/json_spec/matchers/have_json_path.rb b/lib/json_spec/matchers/have_json_path.rb index 9647030..e2069ec 100644 --- a/lib/json_spec/matchers/have_json_path.rb +++ b/lib/json_spec/matchers/have_json_path.rb @@ -2,6 +2,7 @@ module JsonSpec module Matchers class HaveJsonPath include JsonSpec::Helpers + include RSpec::Matchers::Composable def initialize(path) @path = path diff --git a/lib/json_spec/matchers/have_json_size.rb b/lib/json_spec/matchers/have_json_size.rb index c8dc169..d312675 100644 --- a/lib/json_spec/matchers/have_json_size.rb +++ b/lib/json_spec/matchers/have_json_size.rb @@ -3,6 +3,7 @@ module Matchers class HaveJsonSize include JsonSpec::Helpers include JsonSpec::Messages + include RSpec::Matchers::Composable def initialize(size) @expected = size diff --git a/lib/json_spec/matchers/have_json_type.rb b/lib/json_spec/matchers/have_json_type.rb index f61e2bd..dc37165 100644 --- a/lib/json_spec/matchers/have_json_type.rb +++ b/lib/json_spec/matchers/have_json_type.rb @@ -3,6 +3,7 @@ module Matchers class HaveJsonType include JsonSpec::Helpers include JsonSpec::Messages + include RSpec::Matchers::Composable def initialize(type) @classes = type_to_classes(type) diff --git a/lib/json_spec/matchers/include_json.rb b/lib/json_spec/matchers/include_json.rb index 1072e7f..a631727 100644 --- a/lib/json_spec/matchers/include_json.rb +++ b/lib/json_spec/matchers/include_json.rb @@ -4,6 +4,7 @@ class IncludeJson include JsonSpec::Helpers include JsonSpec::Exclusion include JsonSpec::Messages + include RSpec::Matchers::Composable def initialize(expected_json = nil) @expected_json = expected_json diff --git a/spec/json_spec/matchers/be_json_eql_spec.rb b/spec/json_spec/matchers/be_json_eql_spec.rb index d88521e..9b38ce9 100644 --- a/spec/json_spec/matchers/be_json_eql_spec.rb +++ b/spec/json_spec/matchers/be_json_eql_spec.rb @@ -108,4 +108,9 @@ JsonSpec.directory = files_path %({ "value" : "from_file" }).should be_json_eql.to_file("one.json") end + + it_behaves_like "a composable matcher" do + let(:matching) { be_json_eql({ bar: [2, 3], foo: 1 }.to_json) } + let(:not_matching) { be_json_eql({ foo: 1, bar: [2, 4] }.to_json) } + end end diff --git a/spec/json_spec/matchers/have_json_path_spec.rb b/spec/json_spec/matchers/have_json_path_spec.rb index 3990a04..76316d0 100644 --- a/spec/json_spec/matchers/have_json_path_spec.rb +++ b/spec/json_spec/matchers/have_json_path_spec.rb @@ -26,4 +26,9 @@ matcher.matches?(%({"id":1,"json":"spec"})) matcher.description.should == %(have JSON path "json") end + + it_behaves_like "a composable matcher" do + let(:matching) { have_json_path("bar") } + let(:not_matching) { have_json_path("baz") } + end end diff --git a/spec/json_spec/matchers/have_json_size_spec.rb b/spec/json_spec/matchers/have_json_size_spec.rb index 5fa8561..db18428 100644 --- a/spec/json_spec/matchers/have_json_size_spec.rb +++ b/spec/json_spec/matchers/have_json_size_spec.rb @@ -56,4 +56,9 @@ expect { matcher.matches?(%({"id":1,"json":12345})) }.to raise_error(JsonSpec::EnumerableExpected, "Enumerable expected, got 12345") end + + it_behaves_like "a composable matcher" do + let(:matching) { have_json_size(2).at_path("bar") } + let(:not_matching) { have_json_size(3).at_path("bar") } + end end diff --git a/spec/json_spec/matchers/have_json_type_spec.rb b/spec/json_spec/matchers/have_json_type_spec.rb index 91adc45..4d8f69b 100644 --- a/spec/json_spec/matchers/have_json_type_spec.rb +++ b/spec/json_spec/matchers/have_json_type_spec.rb @@ -87,4 +87,9 @@ null.should have_json_type(:null) end end + + it_behaves_like "a composable matcher" do + let(:matching) { have_json_type(Integer).at_path("foo") } + let(:not_matching) { have_json_type(Integer).at_path("bar") } + end end diff --git a/spec/json_spec/matchers/include_json_spec.rb b/spec/json_spec/matchers/include_json_spec.rb index 8af325b..3ac38c7 100644 --- a/spec/json_spec/matchers/include_json_spec.rb +++ b/spec/json_spec/matchers/include_json_spec.rb @@ -93,4 +93,9 @@ JsonSpec.directory = files_path %({"one":{"value":"from_file"},"four":{"five":6}}).should include_json.from_file("one.json") end + + it_behaves_like "a composable matcher" do + let(:matching) { include_json([2, 3].to_json) } + let(:not_matching) { include_json([2, 4].to_json) } + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7108fed..0d49a94 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,6 +18,41 @@ end end +RSpec.shared_examples "a composable matcher" do + let(:json) { '{"foo":1,"bar":[2,3]}' } + + def fail_matching(message = nil) + raise_error(RSpec::Expectations::ExpectationNotMetError, message) + end + + it "works inside include" do + expect([json]).to include(matching) + expect([json]).not_to include(not_matching) + end + + it "works inside match" do + expect([json]).to match([matching]) + expect([json]).not_to match([not_matching]) + end + + it "combines using and" do + expect(json).to matching.and(matching) + expect { expect(json).to matching.and(not_matching) }.to fail_matching + expect { expect(json).to not_matching.and(matching) }.to fail_matching + end + + it "combines using or" do + expect(json).to not_matching.or(matching) + expect(json).to matching.or(not_matching) + expect { expect(json).to not_matching.or(not_matching) }.to fail_matching + end + + it "uses its description in composed failure message" do + expect { expect([json]).not_to include(matching) }.to fail_matching(include(matching.description)) + expect { expect([json]).to include(not_matching) }.to fail_matching(include(not_matching.description)) + end +end + def files_path File.expand_path("../support/files", __FILE__) end