From 787727b449fbfc29726e3d357eff5c9cbf75c8bb Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Tue, 26 May 2026 16:39:47 +0530 Subject: [PATCH 01/10] fix: remove hard requirements on active_support and webmock active_support was never declared as a runtime dependency but pact used blank?/present? in 70+ places, forcing every consumer to pre-require it. Replace with a minimal built-in polyfill (lib/pact/support/blank.rb) that is skipped automatically if active_support (or anything else) has already defined these methods. webmock was missing a `return` before the early yield in WebmockHelpers.turned_off, causing it to fall through to WebMock::Config even when WebMock was not loaded. Adding the return fixes the crash, making webmock a truly optional integration dependency. Document both as optional in the gemspec. Co-Authored-By: Claude Sonnet 4.6 --- lib/pact.rb | 5 +++ .../rspec/support/webmock/webmock_helpers.rb | 2 +- lib/pact/support/blank.rb | 40 +++++++++++++++++++ pact.gemspec | 6 +++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 lib/pact/support/blank.rb diff --git a/lib/pact.rb b/lib/pact.rb index 01756361..11981aa3 100644 --- a/lib/pact.rb +++ b/lib/pact.rb @@ -5,6 +5,10 @@ require 'pact/railtie' if defined?(Rails::Railtie) +# Load blank?/present? polyfill before Zeitwerk eager-loads the rest of the gem. +# Skipped if active_support (or anything else) has already defined these methods. +require_relative 'pact/support/blank' + module Pact class Error < StandardError; end @@ -41,6 +45,7 @@ def self.configuration loader.ignore("#{__dir__}/pact/version.rb") loader.ignore("#{__dir__}/pact/rspec.rb") loader.ignore("#{__dir__}/pact/rspec") +loader.ignore("#{__dir__}/pact/support/blank.rb") loader.ignore("#{__dir__}/pact/railtie.rb") unless defined?(Rails::Railtie) loader.setup diff --git a/lib/pact/rspec/support/webmock/webmock_helpers.rb b/lib/pact/rspec/support/webmock/webmock_helpers.rb index 57526ddd..615c3733 100644 --- a/lib/pact/rspec/support/webmock/webmock_helpers.rb +++ b/lib/pact/rspec/support/webmock/webmock_helpers.rb @@ -2,7 +2,7 @@ module WebmockHelpers def self.turned_off - yield unless defined?(::WebMock) + return yield unless defined?(::WebMock) allow_net_connect = WebMock::Config.instance.allow_net_connect allow_localhost = WebMock::Config.instance.allow_localhost diff --git a/lib/pact/support/blank.rb b/lib/pact/support/blank.rb new file mode 100644 index 00000000..7e28c50f --- /dev/null +++ b/lib/pact/support/blank.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Minimal blank?/present? polyfill for when active_support is not loaded. +# Skipped entirely if Object already responds to blank? (e.g. active_support is present). +unless Object.method_defined?(:blank?) + class NilClass + def blank? = true + def present? = false + end + + class FalseClass + def blank? = true + def present? = false + end + + class TrueClass + def blank? = false + def present? = true + end + + class String + def blank? = empty? || strip.empty? + def present? = !blank? + end + + class Array + def blank? = empty? + def present? = !empty? + end + + class Hash + def blank? = empty? + def present? = !empty? + end + + class Object + def blank? = !self + def present? = !!self + end +end diff --git a/pact.gemspec b/pact.gemspec index d9a139ac..b43acdfb 100644 --- a/pact.gemspec +++ b/pact.gemspec @@ -27,6 +27,12 @@ Gem::Specification.new do |gem| 'documentation_uri' => 'https://github.com/pact-foundation/pact-ruby/blob/master/README.md' } + # Optional runtime dependencies (not required by pact itself): + # active_support — if already loaded, its blank?/present? take precedence over + # pact's built-in polyfill (lib/pact/support/blank.rb). + # webmock — only needed for WebMock integration via WebmockHelpers. + # Require it yourself before using that helper. + # Core dependencies (code loading) gem.add_dependency 'zeitwerk', '~> 2.3' # For Pact support via Pact Rust Core From 1c99c724e9c5d7b658f3d091802182d0fd2654f0 Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Tue, 26 May 2026 17:07:57 +0530 Subject: [PATCH 02/10] refactor: use require instead of require_relative for pact/support/blank Co-Authored-By: Claude Sonnet 4.6 --- lib/pact.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pact.rb b/lib/pact.rb index 11981aa3..44fae2d1 100644 --- a/lib/pact.rb +++ b/lib/pact.rb @@ -7,7 +7,7 @@ # Load blank?/present? polyfill before Zeitwerk eager-loads the rest of the gem. # Skipped if active_support (or anything else) has already defined these methods. -require_relative 'pact/support/blank' +require 'pact/support/blank' module Pact class Error < StandardError; end From bfce698c880a0774721407e63e96977ecb1ef920 Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Wed, 27 May 2026 23:04:29 +0530 Subject: [PATCH 03/10] fix: add deep_dup polyfill alongside blank?/present? active_support's deep_dup is used in 8 places in the gem (matchers, consumer interaction builder). Add a minimal Object/Array/Hash implementation to lib/pact/support/blank.rb so active_support is not required for this either. Prompted by review feedback from @YOU54F. Co-Authored-By: Claude Sonnet 4.6 --- lib/pact/support/blank.rb | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/pact/support/blank.rb b/lib/pact/support/blank.rb index 7e28c50f..13bca571 100644 --- a/lib/pact/support/blank.rb +++ b/lib/pact/support/blank.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true -# Minimal blank?/present? polyfill for when active_support is not loaded. -# Skipped entirely if Object already responds to blank? (e.g. active_support is present). +# Minimal active_support polyfills for blank?/present? and deep_dup. +# Each block is skipped entirely if the method is already defined +# (e.g. active_support is loaded by the host application). + unless Object.method_defined?(:blank?) class NilClass def blank? = true @@ -38,3 +40,25 @@ def blank? = !self def present? = !!self end end + +unless Object.method_defined?(:deep_dup) + class Object + def deep_dup + dup + rescue TypeError + self + end + end + + class Array + def deep_dup + map(&:deep_dup) + end + end + + class Hash + def deep_dup + each_with_object({}) { |(k, v), h| h[k.deep_dup] = v.deep_dup } + end + end +end From f463cc59ba9605ef7fa42510bcc95e1e3de781c2 Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Thu, 28 May 2026 17:25:53 +0530 Subject: [PATCH 04/10] refactor: rename blank.rb to core_ext.rb The file now covers both blank?/present? and deep_dup polyfills, so core_ext.rb better reflects its scope (mirrors active_support/core_ext convention). Co-Authored-By: Claude Sonnet 4.6 --- lib/pact.rb | 4 ++-- lib/pact/support/{blank.rb => core_ext.rb} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename lib/pact/support/{blank.rb => core_ext.rb} (100%) diff --git a/lib/pact.rb b/lib/pact.rb index 44fae2d1..0499ce8f 100644 --- a/lib/pact.rb +++ b/lib/pact.rb @@ -7,7 +7,7 @@ # Load blank?/present? polyfill before Zeitwerk eager-loads the rest of the gem. # Skipped if active_support (or anything else) has already defined these methods. -require 'pact/support/blank' +require 'pact/support/core_ext' module Pact class Error < StandardError; end @@ -45,7 +45,7 @@ def self.configuration loader.ignore("#{__dir__}/pact/version.rb") loader.ignore("#{__dir__}/pact/rspec.rb") loader.ignore("#{__dir__}/pact/rspec") -loader.ignore("#{__dir__}/pact/support/blank.rb") +loader.ignore("#{__dir__}/pact/support/core_ext.rb") loader.ignore("#{__dir__}/pact/railtie.rb") unless defined?(Rails::Railtie) loader.setup diff --git a/lib/pact/support/blank.rb b/lib/pact/support/core_ext.rb similarity index 100% rename from lib/pact/support/blank.rb rename to lib/pact/support/core_ext.rb From 4248f57ff214b009aa5b0bf3470d1333116d4792 Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Thu, 28 May 2026 17:26:31 +0530 Subject: [PATCH 05/10] docs: update pact.rb comment to reflect core_ext scope Co-Authored-By: Claude Sonnet 4.6 --- lib/pact.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pact.rb b/lib/pact.rb index 0499ce8f..75159569 100644 --- a/lib/pact.rb +++ b/lib/pact.rb @@ -5,8 +5,9 @@ require 'pact/railtie' if defined?(Rails::Railtie) -# Load blank?/present? polyfill before Zeitwerk eager-loads the rest of the gem. -# Skipped if active_support (or anything else) has already defined these methods. +# Load core_ext polyfills (blank?/present?, deep_dup) before Zeitwerk eager-loads +# the rest of the gem. Each polyfill is skipped if already defined by active_support +# or any other library loaded by the host application. require 'pact/support/core_ext' module Pact From 9cd0773f566ec0785f2da5071c118d0c54bdea32 Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Thu, 28 May 2026 18:41:34 +0530 Subject: [PATCH 06/10] fix: correct Object#blank? and Object#present? polyfill implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Object#blank? was `!self`, ignoring objects that respond to empty?. Should be `respond_to?(:empty?) ? !!empty? : !self` to match active_support. Object#present? was `!!self`, which is always true for any non-nil/non-false object regardless of blank? — inconsistent with classes that override blank?. Should be `!blank?`. Co-Authored-By: Claude Sonnet 4.6 --- lib/pact/support/core_ext.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pact/support/core_ext.rb b/lib/pact/support/core_ext.rb index 13bca571..fbbbd9e1 100644 --- a/lib/pact/support/core_ext.rb +++ b/lib/pact/support/core_ext.rb @@ -36,8 +36,8 @@ def present? = !empty? end class Object - def blank? = !self - def present? = !!self + def blank? = respond_to?(:empty?) ? !!empty? : !self + def present? = !blank? end end From 08f34c260851157a367321e3853ce1392594c5a7 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Thu, 28 May 2026 19:10:41 +0100 Subject: [PATCH 07/10] chore: add presence method to Object polyfill (for non activesupport) --- lib/pact/support/core_ext.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pact/support/core_ext.rb b/lib/pact/support/core_ext.rb index fbbbd9e1..8d4fec90 100644 --- a/lib/pact/support/core_ext.rb +++ b/lib/pact/support/core_ext.rb @@ -38,6 +38,10 @@ def present? = !empty? class Object def blank? = respond_to?(:empty?) ? !!empty? : !self def present? = !blank? + + def presence + self if present? + end end end From 3fde9f88ea614d9d1bd1f1fa6704915131efca0c Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Fri, 29 May 2026 10:41:23 +0530 Subject: [PATCH 08/10] fix: align core_ext polyfills more closely with active_support implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit blank?/present?: - String#blank? now uses /\A[[:space:]]*\z/ regex to correctly handle Unicode whitespace (e.g.  ) instead of strip.empty? which is ASCII-only - Add Symbol#blank? aliased to empty? (was missing, active_support covers it) - Array/Hash#blank? use alias_method like active_support instead of explicit def - Object#blank? fallback returns false instead of !self (equivalent in practice but matches active_support intent more clearly) deep_dup: - Hash#deep_dup now starts from dup to preserve hash metadata (default values, default procs, compare_by_identity state); optimises String/Symbol keys to avoid unnecessary key re-insertion - Add Module#deep_dup: named modules return self, anonymous modules are duped Co-Authored-By: Claude Sonnet 4.6 --- lib/pact/support/core_ext.rb | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/pact/support/core_ext.rb b/lib/pact/support/core_ext.rb index 8d4fec90..5a83db3f 100644 --- a/lib/pact/support/core_ext.rb +++ b/lib/pact/support/core_ext.rb @@ -21,22 +21,32 @@ def present? = true end class String - def blank? = empty? || strip.empty? + BLANK_RE = /\A[[:space:]]*\z/ + + def blank? + empty? || BLANK_RE.match?(self) + end + + def present? = !blank? + end + + class Symbol + alias_method :blank?, :empty? def present? = !blank? end class Array - def blank? = empty? + alias_method :blank?, :empty? def present? = !empty? end class Hash - def blank? = empty? + alias_method :blank?, :empty? def present? = !empty? end class Object - def blank? = respond_to?(:empty?) ? !!empty? : !self + def blank? = respond_to?(:empty?) ? !!empty? : false def present? = !blank? def presence @@ -62,7 +72,22 @@ def deep_dup class Hash def deep_dup - each_with_object({}) { |(k, v), h| h[k.deep_dup] = v.deep_dup } + hash = dup + each_pair do |key, value| + if ::String === key || ::Symbol === key + hash[key] = value.deep_dup + else + hash.delete(key) + hash[key.deep_dup] = value.deep_dup + end + end + hash + end + end + + class Module + def deep_dup + name.nil? ? super : self end end end From 851757161dfb1cadb86263b4b12a4db0715459b0 Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Fri, 29 May 2026 10:44:58 +0530 Subject: [PATCH 09/10] fix(example): remove combustion dependency from examples combustion was only needed to boot Rails/ActionController and pull in active_support. Now that pact ships its own core_ext polyfills, it is no longer required. Remove it from both Gemfiles and replace rails_helper.rb with a comment explaining the change. Also removes webmock from animal-service Gemfile (was only there as a workaround for the WebmockHelpers bug now fixed upstream). Co-Authored-By: Claude Sonnet 4.6 --- example/animal-service/Gemfile | 3 --- .../spec/pact/consumers/http_spec.rb | 1 + example/animal-service/spec/rails_helper.rb | 12 ++---------- example/zoo-app/Gemfile | 2 -- example/zoo-app/spec/rails_helper.rb | 15 +++------------ 5 files changed, 6 insertions(+), 27 deletions(-) diff --git a/example/animal-service/Gemfile b/example/animal-service/Gemfile index 7dedc652..3cf1b39d 100644 --- a/example/animal-service/Gemfile +++ b/example/animal-service/Gemfile @@ -5,9 +5,6 @@ group :development, :test do gem 'pact-ffi', '~> 0.4.28' # added for pact-ruby FFI support gem 'pry' gem 'rspec' - # required for pact-ruby - gem 'combustion' - gem 'webmock' end gem 'rack' diff --git a/example/animal-service/spec/pact/consumers/http_spec.rb b/example/animal-service/spec/pact/consumers/http_spec.rb index 05874bae..cb787a5b 100644 --- a/example/animal-service/spec/pact/consumers/http_spec.rb +++ b/example/animal-service/spec/pact/consumers/http_spec.rb @@ -12,6 +12,7 @@ http_pact_provider 'Animal Service', opts: { pact_dir: File.expand_path('../../../../zoo-app/spec/pacts', __dir__), http_port: 9292, + provider_setup_port: 9100, app: AnimalService::Api } diff --git a/example/animal-service/spec/rails_helper.rb b/example/animal-service/spec/rails_helper.rb index 597dbf5e..704419da 100644 --- a/example/animal-service/spec/rails_helper.rb +++ b/example/animal-service/spec/rails_helper.rb @@ -1,12 +1,4 @@ # frozen_string_literal: true -require "combustion" -begin - Combustion.initialize! :action_controller do - config.log_level = :fatal if ENV["LOG"].to_s.empty? - end -rescue => e - # Fail fast if application couldn't be loaded - warn "💥 Failed to load the app: #{e.message}\n#{e.backtrace.join("\n")}" - exit(1) -end +# Previously bootstrapped a combustion/Rails app to pull in active_support. +# No longer needed — pact now ships its own blank?/present?/deep_dup polyfills. diff --git a/example/zoo-app/Gemfile b/example/zoo-app/Gemfile index 5d8c04b3..8d6777b5 100644 --- a/example/zoo-app/Gemfile +++ b/example/zoo-app/Gemfile @@ -6,8 +6,6 @@ group :development, :test do gem 'pact-ffi', '~> 0.4.28' # added for pact-ruby FFI support gem 'pry' gem 'rspec' - # required for pact-ruby - gem 'combustion' end gem 'rake' diff --git a/example/zoo-app/spec/rails_helper.rb b/example/zoo-app/spec/rails_helper.rb index 751b9a62..704419da 100644 --- a/example/zoo-app/spec/rails_helper.rb +++ b/example/zoo-app/spec/rails_helper.rb @@ -1,13 +1,4 @@ -require "combustion" +# frozen_string_literal: true -begin - Combustion.initialize! :action_controller do - config.log_level = :fatal if ENV["LOG"].to_s.empty? - config.i18n.available_locales = %i[en] - config.i18n.default_locale = :en - end -rescue => e - # Fail fast if application couldn't be loaded - warn "💥 Failed to load the app: #{e.message}\n#{e.backtrace.join("\n")}" - exit(1) -end +# Previously bootstrapped a combustion/Rails app to pull in active_support. +# No longer needed — pact now ships its own blank?/present?/deep_dup polyfills. From 9c3023bcbcaa4fb71867365ce46f40cb8a7a19a0 Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Fri, 29 May 2026 10:51:22 +0530 Subject: [PATCH 10/10] Revert "fix(example): remove combustion dependency from examples" This reverts commit 851757161dfb1cadb86263b4b12a4db0715459b0. --- example/animal-service/Gemfile | 3 +++ .../spec/pact/consumers/http_spec.rb | 1 - example/animal-service/spec/rails_helper.rb | 12 ++++++++++-- example/zoo-app/Gemfile | 2 ++ example/zoo-app/spec/rails_helper.rb | 15 ++++++++++++--- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/example/animal-service/Gemfile b/example/animal-service/Gemfile index 3cf1b39d..7dedc652 100644 --- a/example/animal-service/Gemfile +++ b/example/animal-service/Gemfile @@ -5,6 +5,9 @@ group :development, :test do gem 'pact-ffi', '~> 0.4.28' # added for pact-ruby FFI support gem 'pry' gem 'rspec' + # required for pact-ruby + gem 'combustion' + gem 'webmock' end gem 'rack' diff --git a/example/animal-service/spec/pact/consumers/http_spec.rb b/example/animal-service/spec/pact/consumers/http_spec.rb index cb787a5b..05874bae 100644 --- a/example/animal-service/spec/pact/consumers/http_spec.rb +++ b/example/animal-service/spec/pact/consumers/http_spec.rb @@ -12,7 +12,6 @@ http_pact_provider 'Animal Service', opts: { pact_dir: File.expand_path('../../../../zoo-app/spec/pacts', __dir__), http_port: 9292, - provider_setup_port: 9100, app: AnimalService::Api } diff --git a/example/animal-service/spec/rails_helper.rb b/example/animal-service/spec/rails_helper.rb index 704419da..597dbf5e 100644 --- a/example/animal-service/spec/rails_helper.rb +++ b/example/animal-service/spec/rails_helper.rb @@ -1,4 +1,12 @@ # frozen_string_literal: true -# Previously bootstrapped a combustion/Rails app to pull in active_support. -# No longer needed — pact now ships its own blank?/present?/deep_dup polyfills. +require "combustion" +begin + Combustion.initialize! :action_controller do + config.log_level = :fatal if ENV["LOG"].to_s.empty? + end +rescue => e + # Fail fast if application couldn't be loaded + warn "💥 Failed to load the app: #{e.message}\n#{e.backtrace.join("\n")}" + exit(1) +end diff --git a/example/zoo-app/Gemfile b/example/zoo-app/Gemfile index 8d6777b5..5d8c04b3 100644 --- a/example/zoo-app/Gemfile +++ b/example/zoo-app/Gemfile @@ -6,6 +6,8 @@ group :development, :test do gem 'pact-ffi', '~> 0.4.28' # added for pact-ruby FFI support gem 'pry' gem 'rspec' + # required for pact-ruby + gem 'combustion' end gem 'rake' diff --git a/example/zoo-app/spec/rails_helper.rb b/example/zoo-app/spec/rails_helper.rb index 704419da..751b9a62 100644 --- a/example/zoo-app/spec/rails_helper.rb +++ b/example/zoo-app/spec/rails_helper.rb @@ -1,4 +1,13 @@ -# frozen_string_literal: true +require "combustion" -# Previously bootstrapped a combustion/Rails app to pull in active_support. -# No longer needed — pact now ships its own blank?/present?/deep_dup polyfills. +begin + Combustion.initialize! :action_controller do + config.log_level = :fatal if ENV["LOG"].to_s.empty? + config.i18n.available_locales = %i[en] + config.i18n.default_locale = :en + end +rescue => e + # Fail fast if application couldn't be loaded + warn "💥 Failed to load the app: #{e.message}\n#{e.backtrace.join("\n")}" + exit(1) +end