From 5a3b751f3906bd43daf8d33671ff76c076ba4f43 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Wed, 3 Jun 2026 13:59:24 -0700 Subject: [PATCH] fix: support scalar request and response bodies in InteractionContents Signed-off-by: Sai Asish Y --- lib/pact/consumer/http_interaction_builder.rb | 4 +- lib/pact/consumer/interaction_contents.rb | 40 ++++++++++--------- .../lib/consumer/interaction_contents_spec.rb | 16 ++++++++ 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/lib/pact/consumer/http_interaction_builder.rb b/lib/pact/consumer/http_interaction_builder.rb index cb383aa4..9878583a 100644 --- a/lib/pact/consumer/http_interaction_builder.rb +++ b/lib/pact/consumer/http_interaction_builder.rb @@ -82,7 +82,7 @@ def with_request(method: nil, path: nil, query: {}, headers: {}, body: nil) end if body - PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body))) + PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body).value)) end self @@ -97,7 +97,7 @@ def will_respond_with(status: nil, headers: {}, body: nil) end if body - PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body))) + PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body).value)) end self diff --git a/lib/pact/consumer/interaction_contents.rb b/lib/pact/consumer/interaction_contents.rb index 04ec0e6c..4303ba50 100644 --- a/lib/pact/consumer/interaction_contents.rb +++ b/lib/pact/consumer/interaction_contents.rb @@ -17,36 +17,38 @@ def self.plugin(contents_hash) end def initialize(contents_hash, format) - init_hash(contents_hash, format).each_pair { |k, v| self[k] = v } + serialized = init_hash(contents_hash, format) + # A scalar body (plain string, integer, etc.) serializes to a non-Hash + # value that cannot be merged pair by pair; expose it via #value instead. + if serialized.is_a?(Hash) + serialized.each_pair { |k, v| self[k] = v } + else + @value = serialized + end @format = format end + def value + defined?(@value) ? @value : self + end + private def serialize(hash, format) # serialize recursively - return hash if hash.is_a?(String) - - if hash.is_a?(Pact::Matchers::Base) - return hash.as_basic if format == :basic - return hash.as_plugin if format == :plugin - end - if hash.is_a?(Pact::Generators::Base) + if hash.is_a?(Pact::Matchers::Base) || hash.is_a?(Pact::Generators::Base) return hash.as_basic if format == :basic return hash.as_plugin if format == :plugin end + + return hash.map { |value| serialize(value, format) } if hash.is_a?(Array) + + # A value that is not a collection or a matcher/generator has nothing to + # recurse into, so return it unchanged (string, integer, boolean, nil, ...). + return hash unless hash.is_a?(Hash) + hash.each_pair do |key, value| - next serialize(value, format) if value.is_a?(Hash) - next hash[key] = value.map { |v| serialize(v, format) } if value.is_a?(Array) - - if value.is_a?(Pact::Matchers::Base) - hash[key] = value.as_basic if format == :basic - hash[key] = value.as_plugin if format == :plugin - end - if value.is_a?(Pact::Generators::Base) - hash[key] = value.as_basic if format == :basic - hash[key] = value.as_plugin if format == :plugin - end + hash[key] = serialize(value, format) end hash diff --git a/spec/lib/consumer/interaction_contents_spec.rb b/spec/lib/consumer/interaction_contents_spec.rb index 52fc2659..686118dd 100644 --- a/spec/lib/consumer/interaction_contents_spec.rb +++ b/spec/lib/consumer/interaction_contents_spec.rb @@ -30,4 +30,20 @@ .to eq('{"str":{"pact:matcher:type":"regex","value":"str","regex":"(?-mix:.*)"},"bool":{"pact:matcher:type":"boolean","value":true},"num":{"pact:matcher:type":"number","value":1},"nested":{"pact:matcher:type":"type","value":[{"a":1,"b":"2"}],"min":1}}') # rubocop:disable Layout/LineLength end end + + context 'with a scalar body' do + it 'returns a plain string unchanged' do + expect(described_class.basic('plain text').value).to eq('plain text') + end + + it 'returns an integer unchanged' do + expect(described_class.basic(42).value).to eq(42) + end + end + + context 'with an array of scalars nested in a hash' do + it 'serializes without raising' do + expect(described_class.basic({ some_array: [42] }).to_json).to eq('{"some_array":[42]}') + end + end end