Skip to content

Commit d6066d3

Browse files
authored
Merge branch 'master' into fix/rails7-polymorphic-inverse-relationship-1473
2 parents 7cbcccd + 953ec76 commit d6066d3

6 files changed

Lines changed: 236 additions & 13 deletions

File tree

lib/jsonapi-resources.rb

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,10 @@
99
require 'jsonapi/cached_response_fragment'
1010
require 'jsonapi/response_document'
1111
require 'jsonapi/acts_as_resource_controller'
12-
if Rails::VERSION::MAJOR >= 6
13-
ActiveSupport.on_load(:action_controller_base) do
14-
require 'jsonapi/resource_controller'
15-
end
16-
else
17-
require 'jsonapi/resource_controller'
18-
end
12+
# Load resource_controller directly to avoid Zeitwerk autoloading issues in Rails 7.1+
13+
# Previously used ActiveSupport.on_load which caused race conditions with Zeitwerk
14+
# See: https://github.com/cerebris/jsonapi-resources/issues/1464
15+
require 'jsonapi/resource_controller'
1916
require 'jsonapi/resource_controller_metal'
2017
require 'jsonapi/resources/version'
2118
require 'jsonapi/configuration'

lib/jsonapi/configuration.rb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -328,13 +328,18 @@ def self.configure
328328
# Rails 7.2+ made ActiveSupport::Deprecation.warn a private method
329329
# This helper provides backward-compatible deprecation warnings
330330
def self.warn_deprecated(message)
331-
if defined?(ActiveSupport::Deprecation) && ActiveSupport::Deprecation.respond_to?(:warn)
332-
# Rails < 7.2
333-
ActiveSupport::Deprecation.warn(message)
331+
if defined?(ActiveSupport::Deprecation)
332+
begin
333+
# Try to call warn as a class method (Rails < 7.2)
334+
ActiveSupport::Deprecation.warn(message)
335+
rescue NoMethodError
336+
# Rails 7.2+: warn is now private, use instance method instead
337+
version = defined?(JSONAPI::Resources::VERSION) ? JSONAPI::Resources::VERSION : '0.11.0'
338+
deprecation = ActiveSupport::Deprecation.new(version, 'jsonapi-resources')
339+
deprecation.warn(message)
340+
end
334341
else
335-
# Rails 7.2+ or fallback - use standard warning with deprecation formatting
336-
# Rails 7.2 doesn't provide a public API for custom deprecation warnings
337-
# So we use Kernel#warn with a deprecation prefix
342+
# Fallback for environments without ActiveSupport
338343
warn "[DEPRECATION] #{message}"
339344
end
340345
end

test/fixtures/active_record.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2730,6 +2730,92 @@ class ArticleCommentResource < JSONAPI::Resource
27302730
has_one :commentable, polymorphic: true
27312731
end
27322732

2733+
# Models and Resources for testing Issue #1467: ActiveModel-based resources
2734+
# ActiveModel class without ActiveRecord
2735+
class SimpleModel
2736+
include ActiveModel::Model
2737+
2738+
attr_accessor :id, :name, :description
2739+
2740+
def initialize(attributes = {})
2741+
@id = attributes[:id]
2742+
@name = attributes[:name]
2743+
@description = attributes[:description]
2744+
end
2745+
2746+
# Simple in-memory store for testing
2747+
@@store = {}
2748+
2749+
def self.reset_store!
2750+
@@store = {}
2751+
end
2752+
2753+
def self.create(attributes)
2754+
model = new(attributes)
2755+
@@store[model.id] = model
2756+
model
2757+
end
2758+
2759+
def self.find(id)
2760+
@@store[id]
2761+
end
2762+
2763+
def self.all
2764+
@@store.values
2765+
end
2766+
2767+
def self.find_by(attributes)
2768+
all.find { |model| attributes.all? { |key, value| model.send(key) == value } }
2769+
end
2770+
end
2771+
2772+
class SimpleModelResource < JSONAPI::BasicResource
2773+
model_name 'SimpleModel'
2774+
model_hint model: SimpleModel, resource: :simple_model
2775+
2776+
attribute :name
2777+
attribute :description
2778+
2779+
# Override find_by_key for non-ActiveRecord models
2780+
def self.find_by_key(key, options = {})
2781+
model = _model_class.find(key.to_i)
2782+
return nil unless model
2783+
new(model, options[:context])
2784+
end
2785+
2786+
# Override find_fragments for non-ActiveRecord models
2787+
def self.find_fragments(filters, options = {})
2788+
models = if filters.empty?
2789+
_model_class.all
2790+
else
2791+
# Simple filtering implementation
2792+
_model_class.all.select do |model|
2793+
filters.all? { |key, value| model.send(key).to_s == value.to_s }
2794+
end
2795+
end
2796+
2797+
models.each_with_object({}) do |model, hash|
2798+
resource = new(model, options[:context])
2799+
hash[resource.identity] = {
2800+
identity: resource.identity,
2801+
cache: nil,
2802+
attributes: nil,
2803+
related: nil
2804+
}
2805+
end
2806+
end
2807+
end
2808+
2809+
# This resource uses the default JSONAPI::Resource (ActiveRelationResource)
2810+
# which will fail with ActiveModel models - reproduces Issue #1467
2811+
class BrokenActiveModelResource < JSONAPI::Resource
2812+
model_name 'SimpleModel'
2813+
model_hint model: SimpleModel, resource: :broken_active_model
2814+
2815+
attribute :name
2816+
attribute :description
2817+
end
2818+
27332819
### PORO Data - don't do this in a production app
27342820
$breed_data = BreedData.new
27352821
$breed_data.add(Breed.new(0, 'persian'))
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require File.expand_path('../../../test_helper', __FILE__)
2+
3+
# Test for Issue #1465: ActiveSupport::Deprecation private method error in Rails 7.2
4+
# https://github.com/cerebris/jsonapi-resources/issues/1465
5+
#
6+
# Rails 7.2 made ActiveSupport::Deprecation.warn a private method
7+
# This test ensures our warn_deprecated helper handles both old and new Rails versions
8+
9+
class DeprecationTest < ActiveSupport::TestCase
10+
def test_warn_deprecated_does_not_raise_error
11+
# This should not raise NoMethodError: private method `warn' called
12+
assert_nothing_raised do
13+
JSONAPI.warn_deprecated('Test deprecation warning')
14+
end
15+
end
16+
17+
def test_warn_deprecated_with_activerecord_present
18+
# Ensure the warning works when ActiveSupport::Deprecation is available
19+
skip 'ActiveSupport::Deprecation not available' unless defined?(ActiveSupport::Deprecation)
20+
21+
assert_nothing_raised do
22+
JSONAPI.warn_deprecated('Test deprecation with ActiveSupport')
23+
end
24+
end
25+
26+
def test_warn_deprecated_with_multiple_calls
27+
# Test that multiple calls don't cause issues
28+
# This is especially important for Rails 7.2 compatibility
29+
assert_nothing_raised do
30+
3.times do |i|
31+
JSONAPI.warn_deprecated("Test deprecation warning #{i}")
32+
end
33+
end
34+
end
35+
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
require File.expand_path('../../../test_helper', __FILE__)
2+
3+
# Test for Issue #1467: Demonstrate the problem with ActiveRelationResource and ActiveModel
4+
# https://github.com/cerebris/jsonapi-resources/issues/1467
5+
#
6+
# This test shows that using JSONAPI::Resource (which inherits from ActiveRelationResource)
7+
# with an ActiveModel-based model will fail because ActiveRelationResource expects
8+
# ActiveRecord methods like 'where'
9+
10+
class ActiveModelBrokenTest < ActiveSupport::TestCase
11+
def setup
12+
# Seed some test data in the mock store
13+
SimpleModel.reset_store!
14+
@model1 = SimpleModel.create(id: 1, name: 'Test 1', description: 'First test')
15+
@model2 = SimpleModel.create(id: 2, name: 'Test 2', description: 'Second test')
16+
end
17+
18+
def teardown
19+
SimpleModel.reset_store!
20+
end
21+
22+
# This test demonstrates the problem: JSONAPI::Resource (ActiveRelationResource)
23+
# attempts to use ActiveRecord methods on ActiveModel models
24+
def test_activerelation_resource_fails_with_activemodel
25+
resource_klass = BrokenActiveModelResource
26+
context = {}
27+
28+
# This WILL raise NoMethodError because ActiveRelationResource expects ActiveRecord methods
29+
# The error could be 'order', 'where', or other ActiveRecord-specific methods
30+
error = assert_raises(NoMethodError) do
31+
fragments = resource_klass.find_fragments({}, { context: context })
32+
end
33+
34+
# Verify it's trying to use ActiveRecord-like query methods
35+
assert_match(/undefined method.*(order|where|limit|offset)/, error.message)
36+
end
37+
end
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
require File.expand_path('../../../test_helper', __FILE__)
2+
3+
# Test for Issue #1467: ActiveModel-based resources fail with 'where' method error
4+
# https://github.com/cerebris/jsonapi-resources/issues/1467
5+
#
6+
# This test reproduces the scenario where:
7+
# - A model uses ActiveModel::Model instead of ActiveRecord::Base
8+
# - The resource should work with BasicResource or proper configuration
9+
# - In v0.10.7+, these resources fail with "undefined method 'where'"
10+
11+
class ActiveModelResourceTest < ActiveSupport::TestCase
12+
def setup
13+
# Seed some test data in the mock store
14+
SimpleModel.reset_store!
15+
@model1 = SimpleModel.create(id: 1, name: 'Test 1', description: 'First test')
16+
@model2 = SimpleModel.create(id: 2, name: 'Test 2', description: 'Second test')
17+
end
18+
19+
def teardown
20+
SimpleModel.reset_store!
21+
end
22+
23+
# Test that ActiveModel-based resources can be found without using ActiveRecord methods
24+
def test_find_activemodel_resources
25+
resource_klass = SimpleModelResource
26+
context = {}
27+
28+
# This should not raise "undefined method 'where'"
29+
assert_nothing_raised do
30+
fragments = resource_klass.find_fragments({}, { context: context })
31+
assert fragments.any?, "Should find SimpleModel fragments"
32+
assert_equal 2, fragments.size, "Should find 2 fragments"
33+
end
34+
end
35+
36+
# Test that ActiveModel-based resources can be filtered
37+
def test_filter_activemodel_resources
38+
resource_klass = SimpleModelResource
39+
context = {}
40+
41+
# Test with a filter
42+
filters = { name: 'Test 1' }
43+
44+
# This should not attempt to call _model_class.where(name: 'Test 1')
45+
assert_nothing_raised do
46+
fragments = resource_klass.find_fragments(filters, { context: context })
47+
assert_equal 1, fragments.size, "Should find 1 filtered fragment"
48+
end
49+
end
50+
51+
# Test that ActiveModel-based resources work with find_by_key
52+
def test_find_by_key_activemodel_resources
53+
resource_klass = SimpleModelResource
54+
context = {}
55+
56+
# find_by_key should work for ActiveModel resources
57+
assert_nothing_raised do
58+
resource = resource_klass.find_by_key(1, context: context)
59+
assert resource, "Should find resource by key"
60+
assert_equal 'Test 1', resource.name
61+
end
62+
end
63+
end

0 commit comments

Comments
 (0)