Skip to content

Commit 758b20f

Browse files
committed
rails 8.1 support
1 parent 17772ce commit 758b20f

10 files changed

Lines changed: 107 additions & 44 deletions

File tree

.github/workflows/tests.yml

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22

33
on:
44
push:
5-
branches: [ 'master', 'release-0-8', 'release-0-9', 'release-0-10' ]
5+
branches: [ 'master', 'release-0-8', 'release-0-9', 'release-0-9-rails-8-1', 'release-0-10' ]
66
pull_request:
77
branches: ['**']
88

@@ -13,34 +13,40 @@ jobs:
1313
fail-fast: false
1414
matrix:
1515
ruby:
16-
- 2.6.6
17-
- 2.7.2
16+
- '3.4'
17+
- '3.3'
18+
- '3.2'
1819
rails:
19-
- 6.1.1
20-
- 6.0.3.4
21-
- 5.2.4.4
22-
- 5.1.7
23-
- 5.0.7.2
24-
- 4.2.11
20+
- '8.1'
21+
- '8.0'
22+
- '7.2'
23+
- '7.1'
24+
- '7.0'
25+
- '6.1'
2526
exclude:
26-
- ruby: 2.7.2
27-
rails: 5.0.7.2
28-
- ruby: 2.6.6
29-
rails: 5.0.7.2
30-
- ruby: 2.7.2
31-
rails: 4.2.11
32-
- ruby: 2.6.6
33-
rails: 4.2.11
27+
# Rails 6.1 and 7.0 predate these Rubies and do not run cleanly on them.
28+
- rails: '6.1'
29+
ruby: '3.4'
30+
- rails: '6.1'
31+
ruby: '3.3'
32+
- rails: '7.0'
33+
ruby: '3.4'
34+
- rails: '7.0'
35+
ruby: '3.3'
36+
- rails: '7.1'
37+
ruby: '3.4'
3438
env:
3539
RAILS_VERSION: ${{ matrix.rails }}
3640
name: Ruby ${{ matrix.ruby }} Rails ${{ matrix.rails }}
3741
steps:
38-
- uses: actions/checkout@v2
42+
- uses: actions/checkout@v4
3943
- name: Set up Ruby
4044
uses: ruby/setup-ruby@v1
4145
with:
4246
ruby-version: ${{ matrix.ruby }}
43-
- name: Install dependencies
44-
run: bundle install --jobs 4 --retry 3
47+
bundler-cache: true
48+
# The Gemfile is identical across matrix rows and only RAILS_VERSION
49+
# changes the resolution, so the cache must be keyed per Rails version.
50+
cache-version: rails-${{ matrix.rails }}
4551
- name: Run tests
4652
run: bundle exec rake test

Gemfile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ version = ENV['RAILS_VERSION'] || 'default'
1111
platforms :ruby do
1212
if version.start_with?('4.2', '5.0')
1313
gem 'sqlite3', '~> 1.3.13'
14-
else
14+
elsif version.start_with?('4', '5', '6', '7.0')
1515
gem 'sqlite3', '~> 1.4'
16+
else
17+
# Rails 7.1+ supports sqlite3 2.x, Rails 8 requires it.
18+
gem 'sqlite3', '>= 1.4'
1619
end
1720
end
1821

1922
case version
2023
when 'master'
2124
gem 'railties', { git: 'https://github.com/rails/rails.git' }
22-
gem 'arel', { git: 'https://github.com/rails/arel.git' }
2325
when 'default'
2426
gem 'railties', '>= 6.0'
2527
else

jsonapi-resources.gemspec

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ Gem::Specification.new do |spec|
2121

2222
spec.add_development_dependency 'bundler'
2323
spec.add_development_dependency 'rake'
24-
spec.add_development_dependency 'minitest'
24+
# Minitest 6 extracted `minitest/mock` into a separate gem and drops APIs this
25+
# suite relies on. Rails supports minitest >= 5.1, so stay on the 5.x line.
26+
spec.add_development_dependency 'minitest', '~> 5.1'
2527
spec.add_development_dependency 'minitest-spec-rails'
2628
spec.add_development_dependency 'simplecov'
2729
spec.add_development_dependency 'pry'

lib/jsonapi-resources.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require 'jsonapi/deprecation'
12
require 'jsonapi/naive_cache'
23
require 'jsonapi/compiled_json'
34
require 'jsonapi/resource'

lib/jsonapi/deprecation.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
require 'active_support/deprecation'
2+
3+
module JSONAPI
4+
# Rails 7.1 stopped `ActiveSupport::Deprecation` from being a singleton and
5+
# Rails 8.0 removed the class-level `.warn` / `.instance` methods entirely, so
6+
# each library is expected to own its deprecator instance.
7+
#
8+
# On Rails < 7.1 `.new` is private (the class still includes Singleton), which
9+
# is what we probe for to stay compatible with the older versions this branch
10+
# supports.
11+
def self.deprecator
12+
@deprecator ||= if ActiveSupport::Deprecation.respond_to?(:new)
13+
ActiveSupport::Deprecation.new('0.10', 'jsonapi-resources')
14+
else
15+
ActiveSupport::Deprecation
16+
end
17+
end
18+
end

lib/jsonapi/resource.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'jsonapi/callbacks'
22
require 'jsonapi/relationship_builder'
3+
require 'jsonapi/deprecation'
34

45
module JSONAPI
56
class Resource
@@ -541,7 +542,7 @@ def attribute(attribute_name, options = {})
541542
check_reserved_attribute_name(attr)
542543

543544
if (attr == :id) && (options[:format].nil?)
544-
ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
545+
JSONAPI.deprecator.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
545546
end
546547

547548
check_duplicate_attribute_name(attr) if options[:format].nil?
@@ -581,7 +582,7 @@ def has_one(*attrs)
581582
end
582583

583584
def belongs_to(*attrs)
584-
ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
585+
JSONAPI.deprecator.warn "In #{name} you exposed a `has_one` relationship "\
585586
" using the `belongs_to` class method. We think `has_one`" \
586587
" is more appropriate. If you know what you're doing," \
587588
" and don't want to see this warning again, override the" \

lib/jsonapi/routing_ext.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def jsonapi_resource(*resources, &_block)
4444
options[:except] << :destroy unless options[:except].include?(:destroy) || options[:except].include?('destroy')
4545
end
4646

47-
resource @resource_type, options do
47+
resource @resource_type, **options do
4848
# :nocov:
4949
if @scope.respond_to? :[]=
5050
# Rails 4
@@ -56,8 +56,8 @@ def jsonapi_resource(*resources, &_block)
5656
jsonapi_relationships
5757
end
5858
else
59-
# Rails 5
60-
jsonapi_resource_scope(SingletonResource.new(@resource_type, api_only?, @scope[:shallow], options), @resource_type) do
59+
# Rails 5+
60+
jsonapi_resource_scope(SingletonResource.new(@resource_type, api_only?, @scope[:shallow], **options), @resource_type) do
6161
if block_given?
6262
yield
6363
else
@@ -119,7 +119,7 @@ def jsonapi_resources(*resources, &_block)
119119
options[:except] << :destroy unless options[:except].include?(:destroy) || options[:except].include?('destroy')
120120
end
121121

122-
resources @resource_type, options do
122+
resources @resource_type, **options do
123123
# :nocov:
124124
if @scope.respond_to? :[]=
125125
# Rails 4
@@ -130,8 +130,8 @@ def jsonapi_resources(*resources, &_block)
130130
jsonapi_relationships
131131
end
132132
else
133-
# Rails 5
134-
jsonapi_resource_scope(Resource.new(@resource_type, api_only?, @scope[:shallow], options), @resource_type) do
133+
# Rails 5+
134+
jsonapi_resource_scope(Resource.new(@resource_type, api_only?, @scope[:shallow], **options), @resource_type) do
135135
if block_given?
136136
yield
137137
else

test/fixtures/active_record.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,8 @@
287287

288288
create_table :related_things, force: true do |t|
289289
t.string :name
290-
t.references :from, references: :thing
291-
t.references :to, references: :thing
290+
t.references :from
291+
t.references :to
292292

293293
t.timestamps null: false
294294
end

test/integration/requests/request_test.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,10 @@ def test_put_invalid_json
477477

478478
assert_equal 400, status
479479
assert_equal 'Bad Request', json_response['errors'][0]['title']
480-
assert_match 'unexpected token at', json_response['errors'][0]['detail']
480+
# The detail is JSON::ParserError's message verbatim, and its wording changed
481+
# in newer json gem versions ("unexpected token at ..." became
482+
# "expected ',' or '}' after object value, got: ...").
483+
assert_match(/unexpected token at|expected ',' or '}'/, json_response['errors'][0]['detail'])
481484
end
482485

483486
def test_put_valid_json_but_array

test/test_helper.rb

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Ruby 3.1+ moved Logger to a separate gem and concurrent-ruby no longer requires
2+
# it, so it must be loaded before ActiveSupport or ActiveSupport::LoggerThreadSafeLevel
3+
# raises `uninitialized constant Logger` on Rails < 7.1.
4+
require 'logger'
15
require 'simplecov'
26
require 'database_cleaner'
37

@@ -20,7 +24,10 @@
2024
end
2125

2226
require 'active_record/railtie'
23-
require 'rails/test_help'
27+
require 'action_controller/railtie'
28+
# NOTE: `rails/test_help` is required after `TestApp.initialize!` (below). Since
29+
# Rails 8.1 it reads `Rails.application.config` at load time, so it cannot be
30+
# required before an application exists.
2431
require 'minitest/mock'
2532
require 'jsonapi-resources'
2633
require 'pry'
@@ -52,14 +59,18 @@ class TestApp < Rails::Application
5259
config.action_controller.action_on_unpermitted_parameters = :raise
5360

5461
ActiveRecord::Schema.verbose = false
55-
config.active_record.schema_format = :none
62+
# The test schema is defined in test/fixtures/active_record.rb and loaded by
63+
# hand, so Rails must not try to maintain or verify it. This used to be spelled
64+
# `schema_format = :none`, but Rails 8.1 rejects anything but :ruby / :sql.
65+
config.active_record.maintain_test_schema = false
5666
config.active_support.test_order = :random
5767

5868
if Rails::VERSION::MAJOR >= 5
5969
config.active_support.halt_callback_chains_on_return_false = false
6070
config.active_record.time_zone_aware_types = [:time, :datetime]
6171
config.active_record.belongs_to_required_by_default = false
62-
if Rails::VERSION::MINOR >= 2
72+
# Removed in Rails 6.1; only ever needed on 5.2.
73+
if Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR >= 2
6374
config.active_record.sqlite3.represent_boolean_as_integer = true
6475
end
6576
end
@@ -80,7 +91,10 @@ class Engine < ::Rails::Engine
8091
# Monkeypatch ActionController::TestCase to delete the RAW_POST_DATA on subsequent calls in the same test.
8192
if Rails::VERSION::MAJOR >= 5
8293
module ClearRawPostHeader
83-
def process(action, *args)
94+
# `process` takes keyword arguments (method:, params:, ...), so they must be
95+
# captured and forwarded as keywords rather than collapsed into a positional
96+
# hash by `*args`.
97+
def process(action, *args, **kwargs)
8498
@request.delete_header 'RAW_POST_DATA'
8599
super
86100
end
@@ -210,6 +224,8 @@ def show_queries
210224

211225
TestApp.initialize!
212226

227+
require 'rails/test_help'
228+
213229
require File.expand_path('../fixtures/active_record', __FILE__)
214230

215231
module Pets
@@ -443,6 +459,17 @@ class CatResource < JSONAPI::Resource
443459
# Ensure backward compatibility with Minitest 4
444460
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
445461

462+
# `fixture_path` (singular) was deprecated in Rails 7.1 and removed in Rails 8.0
463+
# in favour of `fixture_paths` (plural, an array).
464+
def set_fixture_path(klass)
465+
path = "#{Rails.root}/fixtures"
466+
if klass.respond_to?(:fixture_paths=)
467+
klass.fixture_paths = [path]
468+
else
469+
klass.fixture_path = path
470+
end
471+
end
472+
446473
class Minitest::Test
447474
include Helpers::Assertions
448475
include Helpers::ValueMatchers
@@ -454,20 +481,20 @@ def run_in_transaction?
454481
true
455482
end
456483

457-
self.fixture_path = "#{Rails.root}/fixtures"
484+
set_fixture_path(self)
458485
fixtures :all
459486
end
460487

461488
class ActiveSupport::TestCase
462-
self.fixture_path = "#{Rails.root}/fixtures"
489+
set_fixture_path(self)
463490
fixtures :all
464491
setup do
465492
@routes = TestApp.routes
466493
end
467494
end
468495

469496
class ActionDispatch::IntegrationTest
470-
self.fixture_path = "#{Rails.root}/fixtures"
497+
set_fixture_path(self)
471498
fixtures :all
472499

473500
def assert_jsonapi_response(expected_status, msg = nil)
@@ -519,13 +546,16 @@ def assert_cacheable_jsonapi_get(url, cached_classes = :all)
519546
end
520547

521548
class ActionController::TestCase
522-
def assert_cacheable_get(action, *args)
549+
# `get` takes keyword arguments (params:, headers:, ...), so they must be
550+
# captured and forwarded as keywords rather than collapsed into a positional
551+
# hash by `*args`.
552+
def assert_cacheable_get(action, *args, **kwargs)
523553
assert_nil JSONAPI.configuration.resource_cache
524554

525555
normal_queries = []
526556
normal_query_callback = lambda {|_, _, _, _, payload| normal_queries.push payload[:sql] }
527557
ActiveSupport::Notifications.subscribed(normal_query_callback, 'sql.active_record') do
528-
get action, *args
558+
get action, *args, **kwargs
529559
end
530560
non_caching_response = json_response_sans_backtraces
531561
non_caching_status = response.status
@@ -559,7 +589,7 @@ def assert_cacheable_get(action, *args)
559589
@controller = nil
560590
setup_controller_request_and_response
561591
@request.headers.merge!(orig_request_headers.dup)
562-
get action, *args
592+
get action, *args, **kwargs
563593
end
564594
end
565595
rescue Exception

0 commit comments

Comments
 (0)