From 377e3fb847ca54a053a9059d83a140a3ae8e68f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Bryxi=CC=81?= Date: Fri, 13 Nov 2026 12:02:09 +0100 Subject: [PATCH 1/5] fix: depend on csv, a bundled gem from Ruby 3.4 The request parser and resources use CSV, which Ruby 3.4 no longer loads without it being in the bundle. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QduxfgWotKixa3ehszsbf2 --- jsonapi-resources.gemspec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jsonapi-resources.gemspec b/jsonapi-resources.gemspec index fe55e9f1..2ac679f7 100644 --- a/jsonapi-resources.gemspec +++ b/jsonapi-resources.gemspec @@ -31,4 +31,6 @@ Gem::Specification.new do |spec| spec.add_dependency 'activerecord', '>= 4.1' spec.add_dependency 'railties', '>= 4.1' spec.add_dependency 'concurrent-ruby' + # A bundled gem, no longer a default one, from Ruby 3.4. + spec.add_dependency 'csv' end From a7d2e854bff5ce73d39d210e9f6019432a93262b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Bryxi=CC=81?= Date: Fri, 13 Nov 2026 12:02:09 +0100 Subject: [PATCH 2/5] fix: warn through the gem's own deprecator Rails 7.2 removed ActiveSupport::Deprecation.warn, so declaring an id attribute or a belongs_to raised NoMethodError. An ActiveSupport::Deprecation instance works on every Rails the gem supports. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QduxfgWotKixa3ehszsbf2 --- lib/jsonapi-resources.rb | 1 + lib/jsonapi/deprecator.rb | 7 +++++++ lib/jsonapi/resource.rb | 12 ++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 lib/jsonapi/deprecator.rb diff --git a/lib/jsonapi-resources.rb b/lib/jsonapi-resources.rb index 035c4515..1cb5c36d 100644 --- a/lib/jsonapi-resources.rb +++ b/lib/jsonapi-resources.rb @@ -1,3 +1,4 @@ +require 'jsonapi/deprecator' require 'jsonapi/naive_cache' require 'jsonapi/compiled_json' require 'jsonapi/resource' diff --git a/lib/jsonapi/deprecator.rb b/lib/jsonapi/deprecator.rb new file mode 100644 index 00000000..78863786 --- /dev/null +++ b/lib/jsonapi/deprecator.rb @@ -0,0 +1,7 @@ +module JSONAPI + # Rails 7.2 removed the global ActiveSupport::Deprecation.warn; a gem + # warns through its own deprecator, which every supported Rails has. + def self.deprecator + @deprecator ||= ActiveSupport::Deprecation.new('0.10', 'jsonapi-resources') + end +end diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 6fa78dff..03c0f4e0 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -541,7 +541,7 @@ def attribute(attribute_name, options = {}) check_reserved_attribute_name(attr) if (attr == :id) && (options[:format].nil?) - ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.') + JSONAPI.deprecator.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.') end check_duplicate_attribute_name(attr) if options[:format].nil? @@ -581,11 +581,11 @@ def has_one(*attrs) end def belongs_to(*attrs) - ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\ - " using the `belongs_to` class method. We think `has_one`" \ - " is more appropriate. If you know what you're doing," \ - " and don't want to see this warning again, override the" \ - " `belongs_to` class method on your resource." + JSONAPI.deprecator.warn "In #{name} you exposed a `has_one` relationship "\ + " using the `belongs_to` class method. We think `has_one`" \ + " is more appropriate. If you know what you're doing," \ + " and don't want to see this warning again, override the" \ + " `belongs_to` class method on your resource." _add_relationship(Relationship::ToOne, *attrs) end From 9807e2d6ffe716833944e099f469300ecdfa6239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Bryxi=CC=81?= Date: Fri, 13 Nov 2026 12:02:09 +0100 Subject: [PATCH 3/5] test: run the suite on Rails 7.0 to 8.1 - Initialize the test app before rails/test_help, which reads the application's config while loading from Rails 7.1. - schema_format :none is gone from Rails 8.0; no schema is dumped instead. - represent_boolean_as_integer only exists on Rails 5.2. - fixture_path= became fixture_paths= (7.1) and was removed (7.2). - Forward keyword arguments to process/get explicitly (Ruby 3). - t.references never took a references: option; Rails 7.1 rejects it. - Pin minitest to 5: minitest 6 moved minitest/mock out. - Rails 8 needs sqlite3 >= 2.1. - Accept the json gem's newer parse error wording. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QduxfgWotKixa3ehszsbf2 --- Gemfile | 4 +- jsonapi-resources.gemspec | 3 +- test/fixtures/active_record.rb | 4 +- test/integration/requests/request_test.rb | 3 +- test/test_helper.rb | 47 ++++++++++++++++------- 5 files changed, 43 insertions(+), 18 deletions(-) diff --git a/Gemfile b/Gemfile index f08ca796..b1ae1ca6 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,8 @@ version = ENV['RAILS_VERSION'] || 'default' platforms :ruby do if version.start_with?('4.2', '5.0') gem 'sqlite3', '~> 1.3.13' + elsif version == 'default' || version.to_i >= 8 + gem 'sqlite3', '>= 2.1' else gem 'sqlite3', '~> 1.4' end @@ -21,7 +23,7 @@ when 'master' gem 'railties', { git: 'https://github.com/rails/rails.git' } gem 'arel', { git: 'https://github.com/rails/arel.git' } when 'default' - gem 'railties', '>= 6.0' + gem 'railties', '>= 7.0' else gem 'railties', "~> #{version}" end diff --git a/jsonapi-resources.gemspec b/jsonapi-resources.gemspec index 2ac679f7..30addda9 100644 --- a/jsonapi-resources.gemspec +++ b/jsonapi-resources.gemspec @@ -21,7 +21,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler' spec.add_development_dependency 'rake' - spec.add_development_dependency 'minitest' + # 6 moved minitest/mock out, and Rails before 8.1 doesn't support it. + spec.add_development_dependency 'minitest', '~> 5.0' spec.add_development_dependency 'minitest-spec-rails' spec.add_development_dependency 'simplecov' spec.add_development_dependency 'pry' diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index 746307c9..3dd8dcfe 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -287,8 +287,8 @@ create_table :related_things, force: true do |t| t.string :name - t.references :from, references: :thing - t.references :to, references: :thing + t.references :from + t.references :to t.timestamps null: false end diff --git a/test/integration/requests/request_test.rb b/test/integration/requests/request_test.rb index 797f588b..e39fa9e6 100644 --- a/test/integration/requests/request_test.rb +++ b/test/integration/requests/request_test.rb @@ -477,7 +477,8 @@ def test_put_invalid_json assert_equal 400, status assert_equal 'Bad Request', json_response['errors'][0]['title'] - assert_match 'unexpected token at', json_response['errors'][0]['detail'] + # The json gem's message; its wording changed in json 2.10. + assert_match(/unexpected token at|expected ',' or '}' after object value/, json_response['errors'][0]['detail']) end def test_put_valid_json_but_array diff --git a/test/test_helper.rb b/test/test_helper.rb index b51eeb6e..f3bbf8f6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -21,8 +21,6 @@ end require 'active_record/railtie' -require 'rails/test_help' -require 'minitest/mock' require 'jsonapi-resources' require 'pry' require 'memory_profiler' @@ -53,14 +51,19 @@ class TestApp < Rails::Application config.action_controller.action_on_unpermitted_parameters = :raise ActiveRecord::Schema.verbose = false - config.active_record.schema_format = :none + # The tables are created by fixtures/active_record.rb: no schema file to + # dump or keep in sync (:none, which said so, is gone from Rails 8.0). + config.active_record.schema_format = :none if Rails::VERSION::MAJOR < 8 + config.active_record.dump_schema_after_migration = false + config.active_record.maintain_test_schema = false if config.active_record.respond_to?(:maintain_test_schema=) config.active_support.test_order = :random if Rails::VERSION::MAJOR >= 5 config.active_support.halt_callback_chains_on_return_false = false config.active_record.time_zone_aware_types = [:time, :datetime] config.active_record.belongs_to_required_by_default = false - if Rails::VERSION::MINOR >= 2 + # Only Rails 5.2 has (and needs) this setting. + if Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR == 2 config.active_record.sqlite3.represent_boolean_as_integer = true end end @@ -78,10 +81,18 @@ class Engine < ::Rails::Engine end end +# The app (and its engines) are initialized before rails/test_help, as in a +# generated Rails app: from Rails 7.1 test_help reads Rails.application's +# config while it loads. +TestApp.initialize! + +require 'rails/test_help' +require 'minitest/mock' + # Monkeypatch ActionController::TestCase to delete the RAW_POST_DATA on subsequent calls in the same test. if Rails::VERSION::MAJOR >= 5 module ClearRawPostHeader - def process(action, *args) + def process(action, **args) @request.delete_header 'RAW_POST_DATA' super end @@ -209,8 +220,6 @@ def show_queries end end -TestApp.initialize! - require File.expand_path('../fixtures/active_record', __FILE__) module Pets @@ -444,6 +453,18 @@ class CatResource < JSONAPI::Resource # Ensure backward compatibility with Minitest 4 Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) +# fixture_path= became fixture_paths= in Rails 7.1 and is gone from 7.2. +module FixturesIn + def fixtures_in(path) + if respond_to?(:fixture_paths=) + self.fixture_paths = [path] + else + self.fixture_path = path + end + end +end +Minitest::Test.extend(FixturesIn) + class Minitest::Test include Helpers::Assertions include Helpers::ValueMatchers @@ -455,12 +476,12 @@ def run_in_transaction? true end - self.fixture_path = "#{Rails.root}/fixtures" + fixtures_in "#{Rails.root}/fixtures" fixtures :all end class ActiveSupport::TestCase - self.fixture_path = "#{Rails.root}/fixtures" + fixtures_in "#{Rails.root}/fixtures" fixtures :all setup do @routes = TestApp.routes @@ -468,7 +489,7 @@ class ActiveSupport::TestCase end class ActionDispatch::IntegrationTest - self.fixture_path = "#{Rails.root}/fixtures" + fixtures_in "#{Rails.root}/fixtures" fixtures :all def assert_jsonapi_response(expected_status, msg = nil) @@ -520,13 +541,13 @@ def assert_cacheable_jsonapi_get(url, cached_classes = :all) end class ActionController::TestCase - def assert_cacheable_get(action, *args) + def assert_cacheable_get(action, **args) assert_nil JSONAPI.configuration.resource_cache normal_queries = [] normal_query_callback = lambda {|_, _, _, _, payload| normal_queries.push payload[:sql] } ActiveSupport::Notifications.subscribed(normal_query_callback, 'sql.active_record') do - get action, *args + get action, **args end non_caching_response = json_response_sans_backtraces non_caching_status = response.status @@ -560,7 +581,7 @@ def assert_cacheable_get(action, *args) @controller = nil setup_controller_request_and_response @request.headers.merge!(orig_request_headers.dup) - get action, *args + get action, **args end end rescue Exception From 8ed4e6580523b64b55729eb76079e9ef19e5d094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Bryxi=CC=81?= Date: Fri, 13 Nov 2026 12:02:09 +0100 Subject: [PATCH 4/5] ci: test Rails 7.0 to 8.1 on Ruby 3.2 and 3.4 Keeps Rails 5.1 to 6.1 on Ruby 2.6 and 2.7, and runs on ubuntu-22.04, which setup-ruby still has Ruby 2.6 and 2.7 for. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QduxfgWotKixa3ehszsbf2 --- .github/workflows/tests.yml | 43 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b322ef42..38fc312f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,34 +8,35 @@ on: jobs: tests: - runs-on: ubuntu-latest + # 22.04: setup-ruby has no Ruby 2.6/2.7 builds for 24.04. + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: - ruby: - - 2.6.6 - - 2.7.2 - rails: - - 6.1.1 - - 6.0.3.4 - - 5.2.4.4 - - 5.1.7 - - 5.0.7.2 - - 4.2.11 - exclude: - - ruby: 2.7.2 - rails: 5.0.7.2 - - ruby: 2.6.6 - rails: 5.0.7.2 - - ruby: 2.7.2 - rails: 4.2.11 - - ruby: 2.6.6 - rails: 4.2.11 + include: + - { ruby: '2.6', rails: 5.1.7 } + - { ruby: '2.6', rails: 5.2.8.1 } + - { ruby: '2.6', rails: 6.0.6.1 } + - { ruby: '2.6', rails: 6.1.7.10 } + - { ruby: '2.7', rails: 5.2.8.1 } + - { ruby: '2.7', rails: 6.1.7.10 } + - { ruby: '2.7', rails: 7.0.10 } + - { ruby: '3.2', rails: 6.1.7.10 } + - { ruby: '3.2', rails: 7.0.10 } + - { ruby: '3.2', rails: 7.1.6 } + - { ruby: '3.2', rails: 7.2.4 } + - { ruby: '3.2', rails: 8.0.5.2 } + - { ruby: '3.2', rails: 8.1.4.1 } + - { ruby: '3.4', rails: 7.0.10 } + - { ruby: '3.4', rails: 7.1.6 } + - { ruby: '3.4', rails: 7.2.4 } + - { ruby: '3.4', rails: 8.0.5.2 } + - { ruby: '3.4', rails: 8.1.4.1 } env: RAILS_VERSION: ${{ matrix.rails }} name: Ruby ${{ matrix.ruby }} Rails ${{ matrix.rails }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: From 4b707cf9991a7568994fd27b3f016c002af9b180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Bryxi=CC=81?= Date: Fri, 13 Nov 2026 12:04:16 +0100 Subject: [PATCH 5/5] fix: answer validation errors with 422 without Rack 3.1's deprecation Rack 3.1 renamed :unprocessable_entity to :unprocessable_content and warns on the old name, which older Racks don't know. The number works on both. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QduxfgWotKixa3ehszsbf2 --- lib/jsonapi/exceptions.rb | 4 ++-- test/controllers/controller_test.rb | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/jsonapi/exceptions.rb b/lib/jsonapi/exceptions.rb index 6c44de76..97276406 100644 --- a/lib/jsonapi/exceptions.rb +++ b/lib/jsonapi/exceptions.rb @@ -473,7 +473,7 @@ def errors def json_api_error(attr_key, message) create_error_object(code: JSONAPI::VALIDATION_ERROR, - status: :unprocessable_entity, + status: 422, # :unprocessable_content from Rack 3.1, :unprocessable_entity before title: message, detail: detail(attr_key, message), source: { pointer: pointer(attr_key) }, @@ -507,7 +507,7 @@ def general_error?(attr_key) class SaveFailed < Error def errors [create_error_object(code: JSONAPI::SAVE_FAILED, - status: :unprocessable_entity, + status: 422, # :unprocessable_content from Rack 3.1, :unprocessable_entity before title: I18n.translate('jsonapi-resources.exceptions.save_failed.title', default: 'Save failed or was cancelled'), detail: I18n.translate('jsonapi-resources.exceptions.save_failed.detail', diff --git a/test/controllers/controller_test.rb b/test/controllers/controller_test.rb index c69b5eb8..dc764b3a 100644 --- a/test/controllers/controller_test.rb +++ b/test/controllers/controller_test.rb @@ -632,7 +632,7 @@ def test_create_link_to_missing_object } } - assert_response :unprocessable_entity + assert_response 422 # TODO: check if this validation is working assert_match /author - can't be blank/, response.body assert_nil response.location @@ -715,7 +715,7 @@ def test_create_with_invalid_data } } - assert_response :unprocessable_entity + assert_response 422 assert_equal "/data/relationships/author", json_response['errors'][0]['source']['pointer'] assert_equal "can't be blank", json_response['errors'][0]['title'] @@ -1867,7 +1867,7 @@ def test_delete_with_validation_error delete :destroy, params: { id: post.id } assert_equal "can't destroy me", json_response['errors'][0]['title'] - assert_response :unprocessable_entity + assert_response 422 end def test_delete_single @@ -2444,7 +2444,7 @@ def test_create_validations_missing_attribute } } - assert_response :unprocessable_entity + assert_response 422 assert_equal 2, json_response['errors'].size assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][0]['code'] assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][1]['code'] @@ -2466,7 +2466,7 @@ def test_update_validations_missing_attribute } } - assert_response :unprocessable_entity + assert_response 422 assert_equal 1, json_response['errors'].size assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][0]['code'] assert_match /name - can't be blank/, response.body @@ -3103,7 +3103,7 @@ def test_create_with_invalid_data } } - assert_response :unprocessable_entity + assert_response 422 assert_equal "/data/attributes/spouse-name", json_response['errors'][0]['source']['pointer'] assert_equal "can't be blank", json_response['errors'][0]['title'] @@ -3651,7 +3651,7 @@ def test_save_model_callbacks_fail } } - assert_response :unprocessable_entity + assert_response 422 assert_match /Save failed or was cancelled/, json_response['errors'][0]['detail'] end end