Skip to content

Commit

Permalink
add shallow path for belongs_to
Browse files Browse the repository at this point in the history
decouple attributes and path params

fix #180
  • Loading branch information
senid231 committed Oct 29, 2018
1 parent 3c8edef commit dfdbd64
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 13 deletions.
28 changes: 27 additions & 1 deletion README.md
Expand Up @@ -157,13 +157,17 @@ articles.links.related

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

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

```ruby
module MyApi
class Account < JsonApiClient::Resource
belongs_to :user
end

class Customer < JsonApiClient::Resource
belongs_to :user, shallow_path: true
end
end

# try to find without the nested parameter
Expand All @@ -173,6 +177,28 @@ MyApi::Account.find(1)
# makes request to /users/2/accounts/1
MyApi::Account.where(user_id: 2).find(1)
# => returns ResultSet

# makes request to /customers/1
MyApi::Customer.find(1)
# => returns ResultSet

# makes request to /users/2/customers/1
MyApi::Customer.where(user_id: 2).find(1)
# => returns ResultSet
```

you can also override param name for `belongs_to` association

```ruby
module MyApi
class Account < JsonApiClient::Resource
belongs_to :user, param: :customer_id
end
end

# makes request to /users/2/accounts/1
MyApi::Account.where(customer_id: 2).find(1)
# => returns ResultSet
```

## Custom Methods
Expand Down
14 changes: 12 additions & 2 deletions lib/json_api_client/associations/belongs_to.rb
Expand Up @@ -3,15 +3,25 @@ module Associations
module BelongsTo
class Association < BaseAssociation
include Helpers::URI
def param
:"#{attr_name}_id"

attr_reader :param

def initialize(attr_name, klass, options = {})
super
@param = options.fetch(:param, :"#{attr_name}_id").to_sym
@shallow_path = options.fetch(:shallow_path, false)
end

def shallow_path?
@shallow_path
end

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

def set_prefix_path(attrs, formatter)
return if shallow_path? && !attrs[param]
attrs[param] = encode_part(attrs[param]) if attrs.key?(param)
to_prefix_path(formatter) % attrs
end
Expand Down
24 changes: 24 additions & 0 deletions lib/json_api_client/helpers/associatable.rb
Expand Up @@ -7,13 +7,18 @@ module Associatable
class_attribute :associations, instance_accessor: false
self.associations = []
attr_accessor :__cached_associations
attr_accessor :__belongs_to_params
end

module ClassMethods
def _define_association(attr_name, association_klass, options = {})
attr_name = attr_name.to_sym
association = association_klass.new(attr_name, self, options)
self.associations += [association]
end

def _define_relationship_methods(attr_name)
attr_name = attr_name.to_sym

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

def belongs_to(attr_name, options = {})
_define_association(attr_name, JsonApiClient::Associations::BelongsTo::Association, options)

param = associations.last.param
define_method(param) do
_belongs_to_params[param]
end

define_method(:"#{param}=") do |value|
_belongs_to_params[param] = value
end
end

def has_many(attr_name, options = {})
_define_association(attr_name, JsonApiClient::Associations::HasMany::Association, options)
_define_relationship_methods(attr_name)
end

def has_one(attr_name, options = {})
_define_association(attr_name, JsonApiClient::Associations::HasOne::Association, options)
_define_relationship_methods(attr_name)
end
end

def _belongs_to_params
self.__belongs_to_params ||= {}
end

def _clear_belongs_to_params
self.__belongs_to_params = {}
end

def _cached_associations
self.__cached_associations ||= {}
end
Expand Down
10 changes: 8 additions & 2 deletions lib/json_api_client/query/builder.rb
@@ -1,3 +1,5 @@
require 'active_support/all'

module JsonApiClient
module Query
class Builder
Expand Down Expand Up @@ -64,8 +66,12 @@ def last
paginate(page: 1, per_page: 1).pages.last.to_a.last
end

def build
klass.new(params)
def build(attrs = {})
klass.new @path_params.merge(attrs.symbolize_keys)
end

def create(attrs = {})
klass.create @path_params.merge(attrs.symbolize_keys)
end

def params
Expand Down
6 changes: 3 additions & 3 deletions lib/json_api_client/query/requestor.rb
Expand Up @@ -10,14 +10,14 @@ def initialize(klass)

# expects a record
def create(record)
request(:post, klass.path(record.attributes), {
request(:post, klass.path(record.path_attributes), {
body: { data: record.as_json_api },
params: record.request_params.to_params
})
end

def update(record)
request(:patch, resource_path(record.attributes), {
request(:patch, resource_path(record.path_attributes), {
body: { data: record.as_json_api },
params: record.request_params.to_params
})
Expand All @@ -30,7 +30,7 @@ def get(params = {})
end

def destroy(record)
request(:delete, resource_path(record.attributes))
request(:delete, resource_path(record.path_attributes))
end

def linked(path)
Expand Down
13 changes: 8 additions & 5 deletions lib/json_api_client/resource.rb
Expand Up @@ -318,7 +318,8 @@ def initialize(params = {})
@destroyed = nil
self.links = self.class.linker.new(params.delete(:links) || {})
self.relationships = self.class.relationship_linker.new(self.class, params.delete(:relationships) || {})
self.attributes = self.class.default_attributes.merge(params)
self.attributes = self.class.default_attributes.merge params.except(*self.class.prefix_params)
self.__belongs_to_params = params.slice(*self.class.prefix_params)

setup_default_properties

Expand Down Expand Up @@ -465,6 +466,7 @@ def destroy
mark_as_destroyed!
self.relationships.last_result_set = nil
_clear_cached_relationships
_clear_belongs_to_params
true
end
end
Expand Down Expand Up @@ -498,6 +500,10 @@ def reset_request_select!(*resource_types)
self
end

def path_attributes
_belongs_to_params.merge attributes.slice('id').symbolize_keys
end

protected

def setup_default_properties
Expand Down Expand Up @@ -566,10 +572,7 @@ def association_for(name)
end

def non_serializing_attributes
[
self.class.read_only_attributes,
self.class.prefix_params.map(&:to_s)
].flatten
self.class.read_only_attributes
end

def attributes_for_serialization
Expand Down
73 changes: 73 additions & 0 deletions test/unit/association_test.rb
Expand Up @@ -13,6 +13,10 @@ class Specified < TestResource
has_many :bars, class_name: "Owner"
end

class Shallowed < TestResource
belongs_to :foo, class_name: "Property", shallow_path: true
end

class PrefixedOwner < TestResource
has_many :prefixed_properties
end
Expand Down Expand Up @@ -630,6 +634,14 @@ def test_belongs_to_path
assert_equal("foos/%D0%99%D0%A6%D0%A3%D0%9A%D0%95%D0%9D/specifieds", Specified.path({foo_id: 'ЙЦУКЕН'}))
end

def test_belongs_to_shallowed_path
assert_equal([:foo_id], Shallowed.prefix_params)
assert_equal "shalloweds", Shallowed.path({})
assert_equal("foos/%{foo_id}/shalloweds", Shallowed.path)
assert_equal("foos/1/shalloweds", Shallowed.path({foo_id: 1}))
assert_equal("foos/%D0%99%D0%A6%D0%A3%D0%9A%D0%95%D0%9D/shalloweds", Shallowed.path({foo_id: 'ЙЦУКЕН'}))
end

def test_find_belongs_to
stub_request(:get, "http://example.com/foos/1/specifieds")
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
Expand All @@ -642,6 +654,30 @@ def test_find_belongs_to
assert_equal(1, specifieds.length)
end

def test_find_belongs_to_shallowed
stub_request(:get, "http://example.com/foos/1/shalloweds")
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
data: [
{ id: 1, type: "shalloweds", attributes: { name: "nested" } }
]
}.to_json)

stub_request(:get, "http://example.com/shalloweds")
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
data: [
{ id: 1, type: "shalloweds", attributes: { name: "global" } }
]
}.to_json)

nested_records = Shallowed.where(foo_id: 1).all
assert_equal(1, nested_records.length)
assert_equal("nested", nested_records.first.name)

global_records = Shallowed.all
assert_equal(1, global_records.length)
assert_equal("global", global_records.first.name)
end

def test_can_handle_creating
stub_request(:post, "http://example.com/foos/10/specifieds")
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
Expand All @@ -657,6 +693,28 @@ def test_can_handle_creating
})
end

def test_can_handle_creating_shallowed
stub_request(:post, "http://example.com/foos/10/shalloweds")
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
data: { id: 12, type: "shalloweds", attributes: { name: "nested" } }
}.to_json)

stub_request(:post, "http://example.com/shalloweds")
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
data: { id: 13, type: "shalloweds", attributes: { name: "global" } }
}.to_json)

Shallowed.create({
:id => 12,
:foo_id => 10,
:name => "nested"
})
Shallowed.create({
:id => 13,
:name => "global"
})
end

def test_find_belongs_to_params_unchanged
stub_request(:get, "http://example.com/foos/1/specifieds")
.to_return(headers: {
Expand Down Expand Up @@ -692,4 +750,19 @@ def test_nested_create
Specified.create(foo_id: 1)
end

def test_nested_create_from_scope
stub_request(:post, "http://example.com/foos/1/specifieds")
.to_return(headers: {
content_type: "application/vnd.api+json"
}, body: {
data: {
id: 1,
name: "Jeff Ching",
bars: [{id: 1, attributes: {address: "123 Main St."}}]
}
}.to_json)

Specified.where(foo_id: 1).create
end

end

0 comments on commit dfdbd64

Please sign in to comment.