From f47ecb83ee7e6b63a0f75ae2e01f8ec363965637 Mon Sep 17 00:00:00 2001 From: Juan Guecaimburu Date: Thu, 27 Aug 2026 18:45:06 +0200 Subject: [PATCH 1/4] Drop the ActiveSupport dependency and load on Faraday 2 ActiveSupport was used for three string inflections: camelize for endpoint module names, lower camelcase for request keys and underscore for response keys. TransformHash implements them with the rules ActiveSupport applies when no acronyms are configured, so the wire format does not depend on the host application inflections or on the ActiveSupport version. Faraday 2 removed Faraday::Middleware.dependency and Faraday::Response::Middleware, and the gemspec allows faraday below 3.0, so the middleware no longer uses either. Faraday::Middleware dispatches on_complete from 1.2.0, so that is the new floor. The gem requires oj itself since dependency was what loaded it. --- active_campaign.gemspec | 3 +- lib/active_campaign.rb | 1 + lib/active_campaign/api.rb | 2 +- .../faraday/middleware/request.rb | 2 - .../faraday/middleware/response.rb | 4 +- lib/active_campaign/transform_hash.rb | 37 +++++++++++++++++-- 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/active_campaign.gemspec b/active_campaign.gemspec index 700cda6..006aaf4 100644 --- a/active_campaign.gemspec +++ b/active_campaign.gemspec @@ -29,7 +29,6 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] - spec.add_dependency 'activesupport', '>= 4.0', '< 8.0' - spec.add_dependency 'faraday', '>= 1.0', '< 3.0' + spec.add_dependency 'faraday', '>= 1.2', '< 3.0' spec.add_dependency 'oj', '>= 3.0', '< 4.0' end diff --git a/lib/active_campaign.rb b/lib/active_campaign.rb index 673e33f..b5d8ddb 100644 --- a/lib/active_campaign.rb +++ b/lib/active_campaign.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'faraday' +require 'oj' require 'active_campaign/version' require 'active_campaign/errors' diff --git a/lib/active_campaign/api.rb b/lib/active_campaign/api.rb index 724c484..9a2de2d 100644 --- a/lib/active_campaign/api.rb +++ b/lib/active_campaign/api.rb @@ -40,7 +40,7 @@ def endpoints(*endpoints) def endpoint(endpoint) require "active_campaign/api/#{endpoint}" - class_eval { include API.const_get(endpoint.to_s.camelize) } + class_eval { include API.const_get(TransformHash.camelize(endpoint.to_s)) } rescue LoadError, NameError raise DependencyMissing, endpoint end diff --git a/lib/active_campaign/faraday/middleware/request.rb b/lib/active_campaign/faraday/middleware/request.rb index dec1dee..997b7a4 100644 --- a/lib/active_campaign/faraday/middleware/request.rb +++ b/lib/active_campaign/faraday/middleware/request.rb @@ -9,8 +9,6 @@ module Middleware # @author Mikael Henriksson # class Request < ::Faraday::Middleware - dependency 'oj' - include TransformHash def initialize(app, headers) diff --git a/lib/active_campaign/faraday/middleware/response.rb b/lib/active_campaign/faraday/middleware/response.rb index bcd38f6..6b40559 100644 --- a/lib/active_campaign/faraday/middleware/response.rb +++ b/lib/active_campaign/faraday/middleware/response.rb @@ -12,9 +12,7 @@ module Middleware # # @author Mikael Henriksson # - class Response < ::Faraday::Response::Middleware - dependency 'oj' - + class Response < ::Faraday::Middleware include TransformHash # Override this to modify the environment after the response has finished. diff --git a/lib/active_campaign/transform_hash.rb b/lib/active_campaign/transform_hash.rb index a7fae40..756d92a 100644 --- a/lib/active_campaign/transform_hash.rb +++ b/lib/active_campaign/transform_hash.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/core_ext/string' - module ActiveCampaign # # Utility module for manipulating hashes, arrays and values @@ -35,7 +33,7 @@ def transform_keys(hash, *new_case) # @return [Symbol] the transformed key # def transform_key(key, *new_case) - key.to_s.public_send(*new_case).to_sym + inflect(key.to_s, *new_case).to_sym end # @@ -68,5 +66,38 @@ def transform_array(collection, *new_case) end end end + + # + # Changes the case of a string + # + # @param [String] string the string to change + # @param [Symbol] style `:underscore` or `:camelcase` + # @param [Symbol] first_letter `:upper` or `:lower`, only used by `:camelcase` + # + # @return [String] + # + def inflect(string, style, first_letter = :upper) + case style + when :underscore then underscore(string) + when :camelcase then camelize(string, first_letter) + else raise ArgumentError, "unknown case #{style.inspect}" + end + end + + # The rules below are the ActiveSupport ones without acronyms or namespace (::) handling. They live here + # so an application's inflection config can't change the keys sent to the API. + def underscore(string) + return string unless /[A-Z-]/.match?(string) + + string + .gsub(/([A-Z])(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { "#{Regexp.last_match(1) || Regexp.last_match(2)}_" } + .tr('-', '_') + .downcase + end + + def camelize(string, first_letter = :upper) + head = first_letter == :lower ? string.sub(/\A\w/, &:downcase) : string.sub(/\A[a-z\d]*/, &:capitalize) + head.gsub(/_([a-z\d]*)/i) { Regexp.last_match(1).capitalize } + end end end From d48a6af6f1c42c6c4e0c3bb6bc806728d95f8998 Mon Sep 17 00:00:00 2001 From: Juan Guecaimburu Date: Thu, 27 Aug 2026 18:45:06 +0200 Subject: [PATCH 2/4] Make the suite pass with raise_error in the stack The client raises ResourceNotFound on 404. Cleanup hooks that delete a resource the example already deleted tolerate that error, and the two irretrievable examples assert it. The create_pipeline and create_deal_stage hooks read the id from the wrong response key, so they only ever requested a delete with a blank id; they are removed. The recorded cassettes are unchanged. --- spec/lib/active_campaign/api/deal_stages_spec.rb | 4 ---- spec/lib/active_campaign/api/fields_spec.rb | 3 +-- spec/lib/active_campaign/api/pipelines_spec.rb | 4 ---- spec/lib/active_campaign/api/tags_spec.rb | 3 +-- spec/spec_helper.rb | 1 + spec/support/cleanup.rb | 15 +++++++++++++++ spec/support/shared_contexts.rb | 4 ++-- spec/support/shared_contexts/with_contact_tag.rb | 2 +- spec/support/shared_contexts/with_field.rb | 4 ++-- spec/support/shared_contexts/with_field_value.rb | 2 +- spec/support/shared_contexts/with_tag.rb | 2 +- 11 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 spec/support/cleanup.rb diff --git a/spec/lib/active_campaign/api/deal_stages_spec.rb b/spec/lib/active_campaign/api/deal_stages_spec.rb index 7c2806c..64fc10f 100644 --- a/spec/lib/active_campaign/api/deal_stages_spec.rb +++ b/spec/lib/active_campaign/api/deal_stages_spec.rb @@ -8,10 +8,6 @@ include_context 'with deal_stage params' - after do - client.delete_deal_stage(response.dig(:deal, :id)) - end - # rubocop:disable RSpec/ExampleLength it 'returns a deal_stage hash' do expect(response).to include_json( diff --git a/spec/lib/active_campaign/api/fields_spec.rb b/spec/lib/active_campaign/api/fields_spec.rb index 7393803..5819901 100644 --- a/spec/lib/active_campaign/api/fields_spec.rb +++ b/spec/lib/active_campaign/api/fields_spec.rb @@ -74,8 +74,7 @@ end it 'makes the custom field irretrievable' do - show_response = client.show_field(field_id) - expect(show_response).not_to include_json(field: expected_field_response) + expect { client.show_field(field_id) }.to raise_error(ActiveCampaign::ResourceNotFound) end end diff --git a/spec/lib/active_campaign/api/pipelines_spec.rb b/spec/lib/active_campaign/api/pipelines_spec.rb index 91b337c..1122eae 100644 --- a/spec/lib/active_campaign/api/pipelines_spec.rb +++ b/spec/lib/active_campaign/api/pipelines_spec.rb @@ -18,10 +18,6 @@ include_context 'with pipeline params' - after do - client.delete_pipeline(response.dig(:pipeline, :id)) - end - it 'returns a pipeline hash' do expect(response).to include_json( deal_group: { diff --git a/spec/lib/active_campaign/api/tags_spec.rb b/spec/lib/active_campaign/api/tags_spec.rb index 6e1182f..7f60262 100644 --- a/spec/lib/active_campaign/api/tags_spec.rb +++ b/spec/lib/active_campaign/api/tags_spec.rb @@ -64,8 +64,7 @@ end it 'makes the tag irretrievable' do - show_response = client.show_tag(tag_id) - expect(show_response).not_to include_json(tag: expected_tag_response) + expect { client.show_tag(tag_id) }.to raise_error(ActiveCampaign::ResourceNotFound) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5842cfe..741cbfc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,6 +8,7 @@ require 'active_campaign' +require_relative 'support/cleanup' require_relative 'support/shared_contexts' require_relative 'support/webmock' require_relative 'support/vcr' diff --git a/spec/support/cleanup.rb b/spec/support/cleanup.rb new file mode 100644 index 0000000..faa8c33 --- /dev/null +++ b/spec/support/cleanup.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# The client raises ResourceNotFound on 404, and cleanup hooks often delete a +# resource the example itself already deleted. +module Cleanup + def cleanup + yield + rescue ActiveCampaign::ResourceNotFound + nil + end +end + +RSpec.configure do |config| + config.include Cleanup +end diff --git a/spec/support/shared_contexts.rb b/spec/support/shared_contexts.rb index 0771376..5f4788c 100644 --- a/spec/support/shared_contexts.rb +++ b/spec/support/shared_contexts.rb @@ -31,7 +31,7 @@ end after do - client.delete_account(account_id) if account_id + cleanup { client.delete_account(account_id) } if account_id end end @@ -80,6 +80,6 @@ end after do - client.delete_account_contact(account_contact_id) if account_contact_id + cleanup { client.delete_account_contact(account_contact_id) } if account_contact_id end end diff --git a/spec/support/shared_contexts/with_contact_tag.rb b/spec/support/shared_contexts/with_contact_tag.rb index 650f771..c17bfa6 100644 --- a/spec/support/shared_contexts/with_contact_tag.rb +++ b/spec/support/shared_contexts/with_contact_tag.rb @@ -28,6 +28,6 @@ let(:contact_tag_id) { contact_tag[:id] } after do - client.delete_contact_tag(contact_tag_id) if contact_tag_id + cleanup { client.delete_contact_tag(contact_tag_id) } if contact_tag_id end end diff --git a/spec/support/shared_contexts/with_field.rb b/spec/support/shared_contexts/with_field.rb index 50700d2..8f1e8d8 100644 --- a/spec/support/shared_contexts/with_field.rb +++ b/spec/support/shared_contexts/with_field.rb @@ -39,7 +39,7 @@ let(:field_id) { field[:id] } after do - client.delete_field(field_id) if field_id + cleanup { client.delete_field(field_id) } if field_id end end @@ -162,6 +162,6 @@ let(:rel_id) { field_rel[:id] } after do - client.delete_field_rel(rel_id) if rel_id + cleanup { client.delete_field_rel(rel_id) } if rel_id end end diff --git a/spec/support/shared_contexts/with_field_value.rb b/spec/support/shared_contexts/with_field_value.rb index 647f1b4..7aa23fe 100644 --- a/spec/support/shared_contexts/with_field_value.rb +++ b/spec/support/shared_contexts/with_field_value.rb @@ -29,6 +29,6 @@ let(:field_value_id) { field_value[:id] } after do - client.delete_field_value(field_value_id) if field_value_id + cleanup { client.delete_field_value(field_value_id) } if field_value_id end end diff --git a/spec/support/shared_contexts/with_tag.rb b/spec/support/shared_contexts/with_tag.rb index 3a763c1..e170b49 100644 --- a/spec/support/shared_contexts/with_tag.rb +++ b/spec/support/shared_contexts/with_tag.rb @@ -27,6 +27,6 @@ let(:tag_id) { tag[:id] } after do - client.delete_tag(tag_id) if tag_id + cleanup { client.delete_tag(tag_id) } if tag_id end end From 5494d55037a49da15decfc8f900012321f15b475 Mon Sep 17 00:00:00 2001 From: Juan Guecaimburu Date: Thu, 27 Aug 2026 18:45:06 +0200 Subject: [PATCH 3/4] Run CI on Ruby 3.3 and 3.4 --- .github/workflows/rspec.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index b54afb7..943492d 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -2,23 +2,17 @@ name: RSpec on: [push, pull_request] jobs: tests: - env: - COV: true - runs-on: ubuntu-latest strategy: - fail-fast: true + fail-fast: false matrix: - # ruby: [2.5, 2.6, 2.7, jruby, truffleruby] - ruby: [2.5, 2.6, 2.7] + ruby: ['3.3', '3.4'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - - run: gem install bundler - - run: bundle install --jobs=$(nproc) --retry=$(nproc) - - run: bin/rspec - + bundler-cache: true + - run: bundle exec rspec From 90a47366c9c3f5c0569dbd4d9c4b8fc9b802a0bd Mon Sep 17 00:00:00 2001 From: Juan Guecaimburu Date: Thu, 27 Aug 2026 19:32:08 +0200 Subject: [PATCH 4/4] Remove the Lint workflow It ran on Ruby 2.6, where bundler can no longer be installed, so it has not executed rubocop or reek in years and only reports a failure on every push. --- .github/workflows/lint.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index cf7ef6c..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Lint -on: [push, pull_request] -jobs: - rubocop: - runs-on: ubuntu-latest - strategy: - fail-fast: true - - steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.6 - - run: gem install bundler - - run: bundle install --jobs=$(nproc) --retry=$(nproc) - - run: bin/rubocop -P - - reek: - runs-on: ubuntu-latest - strategy: - fail-fast: true - - steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.6 - - run: gem install bundler - - run: bundle install --jobs=$(nproc) --retry=$(nproc) - - run: bin/reek .