Skip to content

Commit fd06792

Browse files
takaokoujiclaude
andcommitted
Add PORO support in find_fragments for 0.9.x compatibility
In 0.10+, find_fragments uses ActiveRecord-specific methods (records.pluck) which fails for PORO (Plain Old Ruby Object) models. This change adds automatic detection of non-ActiveRecord models and falls back to a find_by_key-based implementation. Detection: _model_class.respond_to?(:all) Fallback: find_fragments_for_non_active_record uses find_by_key This eliminates the need for PORO resources to override find_fragments, maintaining 0.9.x compatibility where create/find_by_key worked without the subsequent re-fetch via find_fragments. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e1576b2 commit fd06792

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

lib/jsonapi/active_relation_resource.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ def find_to_populate_by_keys(keys, options = {})
9696
# the ResourceInstances matching the filters, sorting, and pagination rules along with any request
9797
# additional_field values
9898
def find_fragments(filters, options = {})
99+
# PORO compatibility: if _model_class doesn't respond to :all, it's not ActiveRecord
100+
# Fall back to find_by_key-based implementation for 0.9.x compatibility
101+
unless _model_class.respond_to?(:all)
102+
return find_fragments_for_non_active_record(filters, options)
103+
end
104+
99105
include_directives = options.fetch(:include_directives, {})
100106
resource_klass = self
101107

@@ -368,6 +374,22 @@ def join_relationship(records:, relationship:, resource_type: nil, join_type: :i
368374
records.merge(relationship_records)
369375
end
370376

377+
# Fallback implementation of find_fragments for non-ActiveRecord models (PORO)
378+
# This provides 0.9.x compatibility by using find_by_key instead of records.pluck
379+
def find_fragments_for_non_active_record(filters, options)
380+
context = options[:context]
381+
primary_keys = options.fetch(:primary_keys, nil) || filters[_primary_key]
382+
return {} if primary_keys.blank?
383+
384+
fragments = {}
385+
Array(primary_keys).each do |key|
386+
resource = find_by_key(key, context: context)
387+
identity = JSONAPI::ResourceIdentity.new(self, key)
388+
fragments[identity] = JSONAPI::ResourceFragment.new(identity, resource: resource)
389+
end
390+
fragments
391+
end
392+
371393
protected
372394

373395
def to_one_relationships_for_linkage(include_related)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
require File.expand_path('../../../test_helper', __FILE__)
2+
3+
# PORO (Plain Old Ruby Object) for testing
4+
class PoroModel
5+
attr_accessor :id, :name, :created_at
6+
7+
def initialize(id, name)
8+
@id = id
9+
@name = name
10+
@created_at = Time.now
11+
end
12+
13+
# Note: No .all method - this is a PORO, not ActiveRecord
14+
end
15+
16+
# Resource for PORO model
17+
class PoroModelResource < JSONAPI::Resource
18+
model_name 'PoroModel'
19+
attributes :name
20+
21+
class << self
22+
def find_by_key(key, options = {})
23+
context = options[:context]
24+
model = PoroModel.new(key, "PORO Item #{key}")
25+
new(model, context)
26+
end
27+
28+
def create(context)
29+
model = PoroModel.new(SecureRandom.uuid, "New PORO")
30+
new(model, context)
31+
end
32+
end
33+
end
34+
35+
class PoroResourceTest < ActiveSupport::TestCase
36+
def test_poro_model_does_not_respond_to_all
37+
# Verify our test PORO doesn't have .all method
38+
refute PoroModel.respond_to?(:all),
39+
"PORO model should not respond to :all"
40+
end
41+
42+
def test_find_fragments_works_with_poro
43+
# find_fragments should work with PORO by falling back to find_by_key
44+
filters = {}
45+
options = {
46+
context: {},
47+
primary_keys: ['key1', 'key2']
48+
}
49+
50+
fragments = PoroModelResource.find_fragments(filters, options)
51+
52+
assert_equal 2, fragments.length, "Should return 2 fragments"
53+
54+
# Verify fragments have correct structure
55+
fragments.each do |identity, fragment|
56+
assert_kind_of JSONAPI::ResourceIdentity, identity
57+
assert_kind_of JSONAPI::ResourceFragment, fragment
58+
assert_not_nil fragment.resource, "Fragment should have resource for PORO"
59+
end
60+
end
61+
62+
def test_find_fragments_returns_empty_for_poro_without_keys
63+
filters = {}
64+
options = {
65+
context: {},
66+
primary_keys: nil
67+
}
68+
69+
fragments = PoroModelResource.find_fragments(filters, options)
70+
71+
assert_equal({}, fragments, "Should return empty hash when no keys")
72+
end
73+
74+
def test_find_fragments_with_single_key_for_poro
75+
filters = {}
76+
options = {
77+
context: {},
78+
primary_keys: 'single_key'
79+
}
80+
81+
fragments = PoroModelResource.find_fragments(filters, options)
82+
83+
assert_equal 1, fragments.length, "Should return 1 fragment"
84+
end
85+
86+
def test_active_record_resource_still_uses_original_find_fragments
87+
# Ensure ActiveRecord resources still use the optimized pluck-based implementation
88+
# This test verifies we didn't break existing behavior
89+
filters = { PostResource._primary_key => [1, 2] }
90+
options = { context: {} }
91+
92+
# PostResource uses ActiveRecord, should work normally
93+
fragments = PostResource.find_fragments(filters, options)
94+
95+
assert_equal 2, fragments.length
96+
fragments.each do |identity, fragment|
97+
assert_kind_of JSONAPI::ResourceIdentity, identity
98+
assert_kind_of JSONAPI::ResourceFragment, fragment
99+
end
100+
end
101+
end

0 commit comments

Comments
 (0)