Skip to content

Commit ec0fc21

Browse files
authored
Merge pull request #315 from senid231/180-add-shallow-path-for-belongs-to
add shallow path for belongs_to
2 parents 3c8edef + dfdbd64 commit ec0fc21

File tree

7 files changed

+155
-13
lines changed

7 files changed

+155
-13
lines changed

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,17 @@ articles.links.related
157157

158158
You can force nested resource paths for your models by using a `belongs_to` association.
159159

160-
**Note: Using belongs_to is only necessary for setting a nested path.**
160+
**Note: Using belongs_to is only necessary for setting a nested path unless you provide `shallow_path: true` option.**
161161

162162
```ruby
163163
module MyApi
164164
class Account < JsonApiClient::Resource
165165
belongs_to :user
166166
end
167+
168+
class Customer < JsonApiClient::Resource
169+
belongs_to :user, shallow_path: true
170+
end
167171
end
168172

169173
# try to find without the nested parameter
@@ -173,6 +177,28 @@ MyApi::Account.find(1)
173177
# makes request to /users/2/accounts/1
174178
MyApi::Account.where(user_id: 2).find(1)
175179
# => returns ResultSet
180+
181+
# makes request to /customers/1
182+
MyApi::Customer.find(1)
183+
# => returns ResultSet
184+
185+
# makes request to /users/2/customers/1
186+
MyApi::Customer.where(user_id: 2).find(1)
187+
# => returns ResultSet
188+
```
189+
190+
you can also override param name for `belongs_to` association
191+
192+
```ruby
193+
module MyApi
194+
class Account < JsonApiClient::Resource
195+
belongs_to :user, param: :customer_id
196+
end
197+
end
198+
199+
# makes request to /users/2/accounts/1
200+
MyApi::Account.where(customer_id: 2).find(1)
201+
# => returns ResultSet
176202
```
177203

178204
## Custom Methods

lib/json_api_client/associations/belongs_to.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,25 @@ module Associations
33
module BelongsTo
44
class Association < BaseAssociation
55
include Helpers::URI
6-
def param
7-
:"#{attr_name}_id"
6+
7+
attr_reader :param
8+
9+
def initialize(attr_name, klass, options = {})
10+
super
11+
@param = options.fetch(:param, :"#{attr_name}_id").to_sym
12+
@shallow_path = options.fetch(:shallow_path, false)
13+
end
14+
15+
def shallow_path?
16+
@shallow_path
817
end
918

1019
def to_prefix_path(formatter)
1120
"#{formatter.format(attr_name.to_s.pluralize)}/%{#{param}}"
1221
end
1322

1423
def set_prefix_path(attrs, formatter)
24+
return if shallow_path? && !attrs[param]
1525
attrs[param] = encode_part(attrs[param]) if attrs.key?(param)
1626
to_prefix_path(formatter) % attrs
1727
end

lib/json_api_client/helpers/associatable.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ module Associatable
77
class_attribute :associations, instance_accessor: false
88
self.associations = []
99
attr_accessor :__cached_associations
10+
attr_accessor :__belongs_to_params
1011
end
1112

1213
module ClassMethods
1314
def _define_association(attr_name, association_klass, options = {})
1415
attr_name = attr_name.to_sym
1516
association = association_klass.new(attr_name, self, options)
1617
self.associations += [association]
18+
end
19+
20+
def _define_relationship_methods(attr_name)
21+
attr_name = attr_name.to_sym
1722

1823
define_method(attr_name) do
1924
_cached_relationship(attr_name) do
@@ -31,17 +36,36 @@ def _define_association(attr_name, association_klass, options = {})
3136

3237
def belongs_to(attr_name, options = {})
3338
_define_association(attr_name, JsonApiClient::Associations::BelongsTo::Association, options)
39+
40+
param = associations.last.param
41+
define_method(param) do
42+
_belongs_to_params[param]
43+
end
44+
45+
define_method(:"#{param}=") do |value|
46+
_belongs_to_params[param] = value
47+
end
3448
end
3549

3650
def has_many(attr_name, options = {})
3751
_define_association(attr_name, JsonApiClient::Associations::HasMany::Association, options)
52+
_define_relationship_methods(attr_name)
3853
end
3954

4055
def has_one(attr_name, options = {})
4156
_define_association(attr_name, JsonApiClient::Associations::HasOne::Association, options)
57+
_define_relationship_methods(attr_name)
4258
end
4359
end
4460

61+
def _belongs_to_params
62+
self.__belongs_to_params ||= {}
63+
end
64+
65+
def _clear_belongs_to_params
66+
self.__belongs_to_params = {}
67+
end
68+
4569
def _cached_associations
4670
self.__cached_associations ||= {}
4771
end

lib/json_api_client/query/builder.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'active_support/all'
2+
13
module JsonApiClient
24
module Query
35
class Builder
@@ -64,8 +66,12 @@ def last
6466
paginate(page: 1, per_page: 1).pages.last.to_a.last
6567
end
6668

67-
def build
68-
klass.new(params)
69+
def build(attrs = {})
70+
klass.new @path_params.merge(attrs.symbolize_keys)
71+
end
72+
73+
def create(attrs = {})
74+
klass.create @path_params.merge(attrs.symbolize_keys)
6975
end
7076

7177
def params

lib/json_api_client/query/requestor.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ def initialize(klass)
1010

1111
# expects a record
1212
def create(record)
13-
request(:post, klass.path(record.attributes), {
13+
request(:post, klass.path(record.path_attributes), {
1414
body: { data: record.as_json_api },
1515
params: record.request_params.to_params
1616
})
1717
end
1818

1919
def update(record)
20-
request(:patch, resource_path(record.attributes), {
20+
request(:patch, resource_path(record.path_attributes), {
2121
body: { data: record.as_json_api },
2222
params: record.request_params.to_params
2323
})
@@ -30,7 +30,7 @@ def get(params = {})
3030
end
3131

3232
def destroy(record)
33-
request(:delete, resource_path(record.attributes))
33+
request(:delete, resource_path(record.path_attributes))
3434
end
3535

3636
def linked(path)

lib/json_api_client/resource.rb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,8 @@ def initialize(params = {})
318318
@destroyed = nil
319319
self.links = self.class.linker.new(params.delete(:links) || {})
320320
self.relationships = self.class.relationship_linker.new(self.class, params.delete(:relationships) || {})
321-
self.attributes = self.class.default_attributes.merge(params)
321+
self.attributes = self.class.default_attributes.merge params.except(*self.class.prefix_params)
322+
self.__belongs_to_params = params.slice(*self.class.prefix_params)
322323

323324
setup_default_properties
324325

@@ -465,6 +466,7 @@ def destroy
465466
mark_as_destroyed!
466467
self.relationships.last_result_set = nil
467468
_clear_cached_relationships
469+
_clear_belongs_to_params
468470
true
469471
end
470472
end
@@ -498,6 +500,10 @@ def reset_request_select!(*resource_types)
498500
self
499501
end
500502

503+
def path_attributes
504+
_belongs_to_params.merge attributes.slice('id').symbolize_keys
505+
end
506+
501507
protected
502508

503509
def setup_default_properties
@@ -566,10 +572,7 @@ def association_for(name)
566572
end
567573

568574
def non_serializing_attributes
569-
[
570-
self.class.read_only_attributes,
571-
self.class.prefix_params.map(&:to_s)
572-
].flatten
575+
self.class.read_only_attributes
573576
end
574577

575578
def attributes_for_serialization

test/unit/association_test.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ class Specified < TestResource
1313
has_many :bars, class_name: "Owner"
1414
end
1515

16+
class Shallowed < TestResource
17+
belongs_to :foo, class_name: "Property", shallow_path: true
18+
end
19+
1620
class PrefixedOwner < TestResource
1721
has_many :prefixed_properties
1822
end
@@ -630,6 +634,14 @@ def test_belongs_to_path
630634
assert_equal("foos/%D0%99%D0%A6%D0%A3%D0%9A%D0%95%D0%9D/specifieds", Specified.path({foo_id: 'ЙЦУКЕН'}))
631635
end
632636

637+
def test_belongs_to_shallowed_path
638+
assert_equal([:foo_id], Shallowed.prefix_params)
639+
assert_equal "shalloweds", Shallowed.path({})
640+
assert_equal("foos/%{foo_id}/shalloweds", Shallowed.path)
641+
assert_equal("foos/1/shalloweds", Shallowed.path({foo_id: 1}))
642+
assert_equal("foos/%D0%99%D0%A6%D0%A3%D0%9A%D0%95%D0%9D/shalloweds", Shallowed.path({foo_id: 'ЙЦУКЕН'}))
643+
end
644+
633645
def test_find_belongs_to
634646
stub_request(:get, "http://example.com/foos/1/specifieds")
635647
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
@@ -642,6 +654,30 @@ def test_find_belongs_to
642654
assert_equal(1, specifieds.length)
643655
end
644656

657+
def test_find_belongs_to_shallowed
658+
stub_request(:get, "http://example.com/foos/1/shalloweds")
659+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
660+
data: [
661+
{ id: 1, type: "shalloweds", attributes: { name: "nested" } }
662+
]
663+
}.to_json)
664+
665+
stub_request(:get, "http://example.com/shalloweds")
666+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
667+
data: [
668+
{ id: 1, type: "shalloweds", attributes: { name: "global" } }
669+
]
670+
}.to_json)
671+
672+
nested_records = Shallowed.where(foo_id: 1).all
673+
assert_equal(1, nested_records.length)
674+
assert_equal("nested", nested_records.first.name)
675+
676+
global_records = Shallowed.all
677+
assert_equal(1, global_records.length)
678+
assert_equal("global", global_records.first.name)
679+
end
680+
645681
def test_can_handle_creating
646682
stub_request(:post, "http://example.com/foos/10/specifieds")
647683
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
@@ -657,6 +693,28 @@ def test_can_handle_creating
657693
})
658694
end
659695

696+
def test_can_handle_creating_shallowed
697+
stub_request(:post, "http://example.com/foos/10/shalloweds")
698+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
699+
data: { id: 12, type: "shalloweds", attributes: { name: "nested" } }
700+
}.to_json)
701+
702+
stub_request(:post, "http://example.com/shalloweds")
703+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
704+
data: { id: 13, type: "shalloweds", attributes: { name: "global" } }
705+
}.to_json)
706+
707+
Shallowed.create({
708+
:id => 12,
709+
:foo_id => 10,
710+
:name => "nested"
711+
})
712+
Shallowed.create({
713+
:id => 13,
714+
:name => "global"
715+
})
716+
end
717+
660718
def test_find_belongs_to_params_unchanged
661719
stub_request(:get, "http://example.com/foos/1/specifieds")
662720
.to_return(headers: {
@@ -692,4 +750,19 @@ def test_nested_create
692750
Specified.create(foo_id: 1)
693751
end
694752

753+
def test_nested_create_from_scope
754+
stub_request(:post, "http://example.com/foos/1/specifieds")
755+
.to_return(headers: {
756+
content_type: "application/vnd.api+json"
757+
}, body: {
758+
data: {
759+
id: 1,
760+
name: "Jeff Ching",
761+
bars: [{id: 1, attributes: {address: "123 Main St."}}]
762+
}
763+
}.to_json)
764+
765+
Specified.where(foo_id: 1).create
766+
end
767+
695768
end

0 commit comments

Comments
 (0)