pact 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4bc90d8d648301d05847644f43969759221c0eafa6e85823937cfad612eb3684
4
- data.tar.gz: b8e556a7f488cd74bea3ff7dab38c26f90904fabfe812c48d3f415da92749eed
3
+ metadata.gz: a7025748777b864b3e23efaf7ac0cbdd79f41e52300737651a2dc6f20023c44b
4
+ data.tar.gz: 27e691b749816d4cf746279bcb68f6fca740c1718b8213fa7d134ed8c9882e8c
5
5
  SHA512:
6
- metadata.gz: cdc816cbd75713eeaadb1546602d80f3b0eae0acde1dc6f760ce95424f429aaa305baa269510a63e6bf6594d344d60356de21755e31ec8088174912012619939
7
- data.tar.gz: 67723d32775313b7266e4f44e9bb947ea881dfa0bc0b4908bf1bf391cdac283c8bf77550529421a65c73bd47328801518f9ed47d0a958a3e0dbfd8f4b1057319
6
+ metadata.gz: 572d6976b61a36b63c46433bee409d0f45bb1fdb44139c1440f15b77d5c0ed7c6e15fd348d5ca392a1e1cf0dd0f0919d16bf54e129878e4a07bcd0dcee6fc517
7
+ data.tar.gz: ee765594f4b73945b8a9d2bbd1dfa199b6ac73863bd1aa066d1e795e71f87d5152c26ba057788e5c1bec2520345205137e03f8930767a570b6dc20a0c46dee45
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ <a name="v2.0.2"></a>
2
+ ### v2.0.2 (2026-08-12)
3
+
4
+ #### Bug Fixes
5
+
6
+ * ensure pactffi_init_with_log_level is called when instantiating pact ([efa5deb](/../../commit/efa5deb))
7
+ * consumer_version_selectors are silently discarded ([a0a9cf4](/../../commit/a0a9cf4))
8
+ * fail early if no pacts found for verification ([3546fdc](/../../commit/3546fdc))
9
+
10
+ <a name="v2.0.1"></a>
11
+ ### v2.0.1 (2026-05-29)
12
+
13
+ #### Bug Fixes
14
+
15
+ * **example**
16
+ * remove combustion dependency from examples ([281b284](/../../commit/281b284))
17
+
18
+ * align core_ext polyfills more closely with active_support implementations ([f08b8ce](/../../commit/f08b8ce))
19
+ * correct Object#blank? and Object#present? polyfill implementations ([53580b9](/../../commit/53580b9))
20
+ * add deep_dup polyfill alongside blank?/present? ([b116e14](/../../commit/b116e14))
21
+ * remove hard requirements on active_support and webmock ([e77cd34](/../../commit/e77cd34))
22
+ * support scalar request and response bodies in InteractionContents ([5440ed1](/../../commit/5440ed1))
23
+
1
24
  <a name="v2.0.0"></a>
2
25
  ### v2.0.0 (2026-03-18)
3
26
 
@@ -171,12 +171,10 @@ module Pact
171
171
  end
172
172
 
173
173
  def init_pact
174
+ PactFfi.init_with_log_level(Native::Logger::LOG_LEVELS[@pact_config.log_level])
174
175
  handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name)
175
176
  PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
176
177
  PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
177
-
178
- Pact::Native::Logger.log_to_stdout(@pact_config.log_level)
179
-
180
178
  handle
181
179
  end
182
180
 
@@ -82,7 +82,7 @@ module Pact
82
82
  end
83
83
 
84
84
  if body
85
- PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body)))
85
+ PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body).value))
86
86
  end
87
87
 
88
88
  self
@@ -97,7 +97,7 @@ module Pact
97
97
  end
98
98
 
99
99
  if body
100
- PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body)))
100
+ PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body).value))
101
101
  end
102
102
 
103
103
  self
@@ -139,12 +139,11 @@ module Pact
139
139
  end
140
140
 
141
141
  def init_pact
142
+ PactFfi.init_with_log_level(Native::Logger::LOG_LEVELS[@pact_config.log_level])
142
143
  handle = PactFfi.new_pact(pact_config.consumer_name, pact_config.provider_name)
143
144
  PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_#{pact_config.pact_specification}"])
144
145
  PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
145
146
 
146
- Pact::Native::Logger.log_to_stdout(pact_config.log_level)
147
-
148
147
  handle
149
148
  end
150
149
 
@@ -17,36 +17,38 @@ module Pact
17
17
  end
18
18
 
19
19
  def initialize(contents_hash, format)
20
- init_hash(contents_hash, format).each_pair { |k, v| self[k] = v }
20
+ serialized = init_hash(contents_hash, format)
21
+ # A scalar body (plain string, integer, etc.) serializes to a non-Hash
22
+ # value that cannot be merged pair by pair; expose it via #value instead.
23
+ if serialized.is_a?(Hash)
24
+ serialized.each_pair { |k, v| self[k] = v }
25
+ else
26
+ @value = serialized
27
+ end
21
28
  @format = format
22
29
  end
23
30
 
31
+ def value
32
+ defined?(@value) ? @value : self
33
+ end
34
+
24
35
  private
25
36
 
26
37
  def serialize(hash, format)
27
38
  # serialize recursively
28
- return hash if hash.is_a?(String)
29
-
30
- if hash.is_a?(Pact::Matchers::Base)
31
- return hash.as_basic if format == :basic
32
- return hash.as_plugin if format == :plugin
33
- end
34
- if hash.is_a?(Pact::Generators::Base)
39
+ if hash.is_a?(Pact::Matchers::Base) || hash.is_a?(Pact::Generators::Base)
35
40
  return hash.as_basic if format == :basic
36
41
  return hash.as_plugin if format == :plugin
37
42
  end
43
+
44
+ return hash.map { |value| serialize(value, format) } if hash.is_a?(Array)
45
+
46
+ # A value that is not a collection or a matcher/generator has nothing to
47
+ # recurse into, so return it unchanged (string, integer, boolean, nil, ...).
48
+ return hash unless hash.is_a?(Hash)
49
+
38
50
  hash.each_pair do |key, value|
39
- next serialize(value, format) if value.is_a?(Hash)
40
- next hash[key] = value.map { |v| serialize(v, format) } if value.is_a?(Array)
41
-
42
- if value.is_a?(Pact::Matchers::Base)
43
- hash[key] = value.as_basic if format == :basic
44
- hash[key] = value.as_plugin if format == :plugin
45
- end
46
- if value.is_a?(Pact::Generators::Base)
47
- hash[key] = value.as_basic if format == :basic
48
- hash[key] = value.as_plugin if format == :plugin
49
- end
51
+ hash[key] = serialize(value, format)
50
52
  end
51
53
 
52
54
  hash
@@ -172,12 +172,10 @@ module Pact
172
172
  end
173
173
 
174
174
  def init_pact
175
+ PactFfi.init_with_log_level(Native::Logger::LOG_LEVELS[@pact_config.log_level])
175
176
  handle = PactFfi::MessageConsumer.new_message_pact(@pact_config.consumer_name, @pact_config.provider_name)
176
177
  PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
177
178
  PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
178
-
179
- Pact::Native::Logger.log_to_stdout(@pact_config.log_level)
180
-
181
179
  handle
182
180
  end
183
181
 
@@ -39,12 +39,11 @@ module Pact
39
39
  end
40
40
 
41
41
  def init_pact
42
+ PactFfi.init_with_log_level(Native::Logger::LOG_LEVELS[@log_level])
42
43
  handle = PactFfi.new_pact(consumer_name, provider_name)
43
44
  PactFfi.with_specification(handle, @pact_specification)
44
45
  PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
45
46
 
46
- Pact::Native::Logger.log_to_stdout(@log_level)
47
-
48
47
  handle
49
48
  end
50
49
  end
@@ -148,12 +148,10 @@ module Pact
148
148
  end
149
149
 
150
150
  def init_pact
151
+ PactFfi.init_with_log_level(Native::Logger::LOG_LEVELS[@pact_config.log_level])
151
152
  handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name)
152
153
  PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
153
154
  PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
154
-
155
- Pact::Native::Logger.log_to_stdout(@pact_config.log_level)
156
-
157
155
  handle
158
156
  end
159
157
 
@@ -170,12 +170,10 @@ module Pact
170
170
  end
171
171
 
172
172
  def init_pact
173
+ PactFfi.init_with_log_level(Native::Logger::LOG_LEVELS[@pact_config.log_level])
173
174
  handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name)
174
175
  PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
175
176
  PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
176
-
177
- Pact::Native::Logger.log_to_stdout(@pact_config.log_level)
178
-
179
177
  handle
180
178
  end
181
179
 
@@ -157,12 +157,10 @@ module Pact
157
157
  end
158
158
 
159
159
  def init_pact
160
+ PactFfi.init_with_log_level(Native::Logger::LOG_LEVELS[@pact_config.log_level])
160
161
  handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name)
161
162
  PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
162
163
  PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
163
-
164
- Pact::Native::Logger.log_to_stdout(@pact_config.log_level)
165
-
166
164
  handle
167
165
  end
168
166
 
@@ -5,7 +5,7 @@ require 'pact/ffi/logger'
5
5
  module Pact
6
6
  module Native
7
7
  module Logger
8
- LOG_LEVELS = {
8
+ LOG_FILTERS = {
9
9
  off: PactFfi::FfiLogLevelFilter['LOG_LEVEL_OFF'],
10
10
  error: PactFfi::FfiLogLevelFilter['LOG_LEVEL_ERROR'],
11
11
  warn: PactFfi::FfiLogLevelFilter['LOG_LEVEL_WARN'],
@@ -14,10 +14,19 @@ module Pact
14
14
  trace: PactFfi::FfiLogLevelFilter['LOG_LEVEL_TRACE']
15
15
  }.freeze
16
16
 
17
+ LOG_LEVELS = {
18
+ off: PactFfi::FfiLogLevel['LOG_LEVEL_OFF'],
19
+ error: PactFfi::FfiLogLevel['LOG_LEVEL_ERROR'],
20
+ warn: PactFfi::FfiLogLevel['LOG_LEVEL_WARN'],
21
+ info: PactFfi::FfiLogLevel['LOG_LEVEL_INFO'],
22
+ debug: PactFfi::FfiLogLevel['LOG_LEVEL_DEBUG'],
23
+ trace: PactFfi::FfiLogLevel['LOG_LEVEL_TRACE']
24
+ }.freeze
25
+
17
26
  def self.log_to_stdout(log_level)
18
- raise 'invalid log level for PactFfi::FfiLogLevelFilter' unless LOG_LEVELS.key?(log_level)
27
+ raise 'invalid log level for PactFfi::FfiLogLevelFilter' unless LOG_FILTERS.key?(log_level)
19
28
 
20
- PactFfi::Logger.log_to_stdout(LOG_LEVELS[log_level]) unless log_level == :off
29
+ PactFfi::Logger.log_to_stdout(LOG_FILTERS[log_level]) unless log_level == :off
21
30
  end
22
31
  end
23
32
  end
@@ -3,6 +3,7 @@
3
3
  require 'pact/ffi/verifier'
4
4
  require 'pact/native/logger'
5
5
  require 'pact/native/blocking_verifier'
6
+ require 'json'
6
7
 
7
8
  module Pact
8
9
  module Provider
@@ -175,7 +176,7 @@ module Pact
175
176
  PactFfi::Verifier.set_provider_info(pact_handle, @pact_config.provider_name, '', '', 0, '')
176
177
  end
177
178
 
178
- def add_provider_transport(pact_handle)
179
+ def add_provider_transport(_pact_handle)
179
180
  raise PactImplementationRequired, 'Implement #add_provider_transport in a subclass'
180
181
  end
181
182
 
@@ -200,6 +201,7 @@ module Pact
200
201
  if @pact_config.pact_broker_proxy_url.blank? && @pact_config.pact_uri.blank?
201
202
  path = @pact_config.pact_dir || (defined?(Rails) ? Rails.root.join('pacts').to_s : 'pacts')
202
203
  logger.info("[verifier] pact broker url or pact uri is not set, using directory #{path} as a verification source") # rubocop:disable Layout/LineLength
204
+ ensure_provider_has_matching_pacts!(path)
203
205
  return PactFfi::Verifier.add_directory_source(handle, path)
204
206
  end
205
207
 
@@ -213,15 +215,15 @@ module Pact
213
215
  PactFfi::Verifier.add_file_source(handle, @pact_config.pact_uri)
214
216
  end
215
217
  else
216
- logger.info("[verifier] using pact broker url #{@pact_config.broker_url} with consumer selectors: #{JSON.dump(consumer_selectors)} as a verification source") # rubocop:disable Layout/LineLength
217
- consumer_selectors = [] if consumer_selectors.nil?
218
- filters = consumer_selectors.map do |selector|
218
+ selectors = consumer_selectors || []
219
+ logger.info("[verifier] using pact broker url #{@pact_config.broker_url} with consumer selectors: #{JSON.dump(selectors)} as a verification source") # rubocop:disable Layout/LineLength
220
+ filters = selectors.map do |selector|
219
221
  FFI::MemoryPointer.from_string(JSON.dump(selector).to_s)
220
222
  end
221
223
  filters_ptr = FFI::MemoryPointer.new(:pointer, filters.size + 1)
222
224
  filters_ptr.write_array_of_pointer(filters)
223
225
  PactFfi::Verifier.broker_source_with_selectors(handle, @pact_config.pact_broker_proxy_url,
224
- @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token, bool_to_int(@pact_config.enable_pending), @pact_config.include_wip_pacts_since, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch, filters_ptr, consumer_selectors.size, c_consumer_version_tags, c_consumer_version_tags_size) # rubocop:disable Layout/LineLength
226
+ @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token, bool_to_int(@pact_config.enable_pending), @pact_config.include_wip_pacts_since, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch, filters_ptr, selectors.size, c_consumer_version_tags, c_consumer_version_tags_size) # rubocop:disable Layout/LineLength
225
227
  end
226
228
  end
227
229
 
@@ -254,6 +256,27 @@ module Pact
254
256
 
255
257
  [DEFAULT_CONSUMER_SELECTORS]
256
258
  end
259
+
260
+ def ensure_provider_has_matching_pacts!(path)
261
+ return unless @pact_config.fail_if_no_pacts_found
262
+
263
+ pact_paths = Dir.glob(File.join(path, '**', '*.json'))
264
+ raise VerifierError.new("No pact files found in #{path}") if pact_paths.empty?
265
+
266
+ return if pact_paths.any? { |p| pact_file_provider_name(p) == @pact_config.provider_name }
267
+
268
+ providers = pact_paths.map { |p| pact_file_provider_name(p) }.compact.uniq.sort
269
+ raise VerifierError.new(
270
+ "No pacts found for provider \"#{@pact_config.provider_name}\" in directory #{path}. " \
271
+ "Found providers: #{providers.join(', ')}"
272
+ )
273
+ end
274
+
275
+ def pact_file_provider_name(path)
276
+ JSON.parse(File.read(path)).dig('provider', 'name')
277
+ rescue StandardError
278
+ nil
279
+ end
257
280
  end
258
281
  end
259
282
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module WebmockHelpers
4
4
  def self.turned_off
5
- yield unless defined?(::WebMock)
5
+ return yield unless defined?(::WebMock)
6
6
 
7
7
  allow_net_connect = WebMock::Config.instance.allow_net_connect
8
8
  allow_localhost = WebMock::Config.instance.allow_localhost
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Minimal active_support polyfills for blank?/present? and deep_dup.
4
+ # Each block is skipped entirely if the method is already defined
5
+ # (e.g. active_support is loaded by the host application).
6
+
7
+ unless Object.method_defined?(:blank?)
8
+ class NilClass
9
+ def blank? = true
10
+ def present? = false
11
+ end
12
+
13
+ class FalseClass
14
+ def blank? = true
15
+ def present? = false
16
+ end
17
+
18
+ class TrueClass
19
+ def blank? = false
20
+ def present? = true
21
+ end
22
+
23
+ class String
24
+ BLANK_RE = /\A[[:space:]]*\z/
25
+
26
+ def blank?
27
+ empty? || BLANK_RE.match?(self)
28
+ end
29
+
30
+ def present? = !blank?
31
+ end
32
+
33
+ class Symbol
34
+ alias_method :blank?, :empty?
35
+ def present? = !blank?
36
+ end
37
+
38
+ class Array
39
+ alias_method :blank?, :empty?
40
+ def present? = !empty?
41
+ end
42
+
43
+ class Hash
44
+ alias_method :blank?, :empty?
45
+ def present? = !empty?
46
+ end
47
+
48
+ class Object
49
+ def blank? = respond_to?(:empty?) ? !!empty? : false
50
+ def present? = !blank?
51
+
52
+ def presence
53
+ self if present?
54
+ end
55
+ end
56
+ end
57
+
58
+ unless Object.method_defined?(:deep_dup)
59
+ class Object
60
+ def deep_dup
61
+ dup
62
+ rescue TypeError
63
+ self
64
+ end
65
+ end
66
+
67
+ class Array
68
+ def deep_dup
69
+ map(&:deep_dup)
70
+ end
71
+ end
72
+
73
+ class Hash
74
+ def deep_dup
75
+ hash = dup
76
+ each_pair do |key, value|
77
+ if ::String === key || ::Symbol === key
78
+ hash[key] = value.deep_dup
79
+ else
80
+ hash.delete(key)
81
+ hash[key.deep_dup] = value.deep_dup
82
+ end
83
+ end
84
+ hash
85
+ end
86
+ end
87
+
88
+ class Module
89
+ def deep_dup
90
+ name.nil? ? super : self
91
+ end
92
+ end
93
+ end
data/lib/pact/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # Remember to bump pact-provider-proxy when this changes major version
2
2
  module Pact
3
- VERSION = "2.0.0"
3
+ VERSION = "2.0.2"
4
4
  end
data/lib/pact.rb CHANGED
@@ -5,6 +5,11 @@ require 'pact/ffi'
5
5
 
6
6
  require 'pact/railtie' if defined?(Rails::Railtie)
7
7
 
8
+ # Load core_ext polyfills (blank?/present?, deep_dup) before Zeitwerk eager-loads
9
+ # the rest of the gem. Each polyfill is skipped if already defined by active_support
10
+ # or any other library loaded by the host application.
11
+ require 'pact/support/core_ext'
12
+
8
13
  module Pact
9
14
  class Error < StandardError; end
10
15
 
@@ -41,6 +46,7 @@ loader.tag = 'pact'
41
46
  loader.ignore("#{__dir__}/pact/version.rb")
42
47
  loader.ignore("#{__dir__}/pact/rspec.rb")
43
48
  loader.ignore("#{__dir__}/pact/rspec")
49
+ loader.ignore("#{__dir__}/pact/support/core_ext.rb")
44
50
  loader.ignore("#{__dir__}/pact/railtie.rb") unless defined?(Rails::Railtie)
45
51
 
46
52
  loader.setup
data/pact.gemspec CHANGED
@@ -27,6 +27,12 @@ Gem::Specification.new do |gem|
27
27
  'documentation_uri' => 'https://github.com/pact-foundation/pact-ruby/blob/master/README.md'
28
28
  }
29
29
 
30
+ # Optional runtime dependencies (not required by pact itself):
31
+ # active_support — if already loaded, its blank?/present? take precedence over
32
+ # pact's built-in polyfill (lib/pact/support/blank.rb).
33
+ # webmock — only needed for WebMock integration via WebmockHelpers.
34
+ # Require it yourself before using that helper.
35
+
30
36
  # Core dependencies (code loading)
31
37
  gem.add_dependency 'zeitwerk', '~> 2.3'
32
38
  # For Pact support via Pact Rust Core
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pact
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Fraser
@@ -487,6 +487,7 @@ files:
487
487
  - lib/pact/rspec/support/pact_provider_helpers.rb
488
488
  - lib/pact/rspec/support/waterdrop/pact_waterdrop_client.rb
489
489
  - lib/pact/rspec/support/webmock/webmock_helpers.rb
490
+ - lib/pact/support/core_ext.rb
490
491
  - lib/pact/tasks/pact.rake
491
492
  - lib/pact/version.rb
492
493
  - pact.gemspec
@@ -512,7 +513,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
512
513
  - !ruby/object:Gem::Version
513
514
  version: '0'
514
515
  requirements: []
515
- rubygems_version: 4.0.8
516
+ rubygems_version: 4.0.18
516
517
  specification_version: 4
517
518
  summary: Enables consumer driven contract testing, providing a mock service and DSL
518
519
  for the consumer project, and interaction playback and verification for the service