Skip to content

Commit dda66ea

Browse files
authored
Merge pull request #300 from senid231/smart-performance-optimization
define methods for properties and associations getter/setter
2 parents dbdb0f6 + 3048ea3 commit dda66ea

File tree

9 files changed

+193
-51
lines changed

9 files changed

+193
-51
lines changed

lib/json_api_client/associations/belongs_to.rb

-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
module JsonApiClient
22
module Associations
33
module BelongsTo
4-
extend ActiveSupport::Concern
5-
6-
module ClassMethods
7-
def belongs_to(attr_name, options = {})
8-
# self.associations = self.associations + [HasOne::Association.new(attr_name, self, options)]
9-
self.associations += [BelongsTo::Association.new(attr_name, self, options)]
10-
end
11-
end
12-
134
class Association < BaseAssociation
145
include Helpers::URI
156
def param

lib/json_api_client/associations/has_many.rb

-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
module JsonApiClient
22
module Associations
33
module HasMany
4-
extend ActiveSupport::Concern
5-
6-
module ClassMethods
7-
def has_many(attr_name, options = {})
8-
self.associations = self.associations + [HasMany::Association.new(attr_name, self, options)]
9-
end
10-
end
11-
124
class Association < BaseAssociation
135
end
146
end

lib/json_api_client/associations/has_one.rb

-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
module JsonApiClient
22
module Associations
33
module HasOne
4-
extend ActiveSupport::Concern
5-
6-
module ClassMethods
7-
def has_one(attr_name, options = {})
8-
self.associations += [HasOne::Association.new(attr_name, self, options)]
9-
end
10-
end
11-
124
class Association < BaseAssociation
135
def from_result_set(result_set)
146
result_set.first

lib/json_api_client/helpers.rb

+1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ module Helpers
44
autoload :Dirty, 'json_api_client/helpers/dirty'
55
autoload :DynamicAttributes, 'json_api_client/helpers/dynamic_attributes'
66
autoload :URI, 'json_api_client/helpers/uri'
7+
autoload :Associatable, 'json_api_client/helpers/associatable'
78
end
89
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
module JsonApiClient
2+
module Helpers
3+
module Associatable
4+
extend ActiveSupport::Concern
5+
6+
included do
7+
class_attribute :associations, instance_accessor: false
8+
self.associations = []
9+
attr_accessor :__cached_associations
10+
end
11+
12+
module ClassMethods
13+
def _define_association(attr_name, association_klass, options = {})
14+
attr_name = attr_name.to_sym
15+
association = association_klass.new(attr_name, self, options)
16+
self.associations += [association]
17+
18+
define_method(attr_name) do
19+
_cached_relationship(attr_name) do
20+
relationship_definition = relationship_definition_for(attr_name)
21+
return unless relationship_definition
22+
relationship_data_for(attr_name, relationship_definition)
23+
end
24+
end
25+
26+
define_method("#{attr_name}=") do |value|
27+
_clear_cached_relationship(attr_name)
28+
relationships.public_send("#{attr_name}=", value)
29+
end
30+
end
31+
32+
def belongs_to(attr_name, options = {})
33+
_define_association(attr_name, JsonApiClient::Associations::BelongsTo::Association, options)
34+
end
35+
36+
def has_many(attr_name, options = {})
37+
_define_association(attr_name, JsonApiClient::Associations::HasMany::Association, options)
38+
end
39+
40+
def has_one(attr_name, options = {})
41+
_define_association(attr_name, JsonApiClient::Associations::HasOne::Association, options)
42+
end
43+
end
44+
45+
def _cached_associations
46+
self.__cached_associations ||= {}
47+
end
48+
49+
def _clear_cached_relationships
50+
self.__cached_associations = {}
51+
end
52+
53+
def _clear_cached_relationship(attr_name)
54+
_cached_associations.delete(attr_name)
55+
end
56+
57+
def _cached_relationship(attr_name)
58+
return _cached_associations[attr_name] if _cached_associations.has_key?(attr_name)
59+
_cached_associations[attr_name] = yield
60+
end
61+
62+
end
63+
end
64+
end

lib/json_api_client/helpers/dynamic_attributes.rb

+1-6
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,7 @@ def has_attribute?(attr_name)
3939

4040
def method_missing(method, *args, &block)
4141
if has_attribute?(method)
42-
self.class.class_eval do
43-
define_method(method) do
44-
attributes[method]
45-
end
46-
end
47-
return send(method)
42+
return attributes[method]
4843
end
4944

5045
normalized_method = safe_key_formatter.unformat(method.to_s)

lib/json_api_client/resource.rb

+33-18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Resource
1212

1313
include Helpers::DynamicAttributes
1414
include Helpers::Dirty
15+
include Helpers::Associatable
1516

1617
attr_accessor :last_result_set,
1718
:links,
@@ -29,7 +30,6 @@ class Resource
2930
:relationship_linker,
3031
:read_only_attributes,
3132
:requestor_class,
32-
:associations,
3333
:json_key_format,
3434
:route_format,
3535
:request_params_class,
@@ -47,7 +47,6 @@ class Resource
4747
self.relationship_linker = Relationships::Relations
4848
self.read_only_attributes = [:id, :type, :links, :meta, :relationships]
4949
self.requestor_class = Query::Requestor
50-
self.associations = []
5150
self.request_params_class = RequestParams
5251
self.keep_request_params = false
5352
self.add_defaults_to_changes = false
@@ -58,10 +57,6 @@ class Resource
5857
#:underscored_route, :camelized_route, :dasherized_route, or custom
5958
self.route_format = :underscored_route
6059

61-
include Associations::BelongsTo
62-
include Associations::HasMany
63-
include Associations::HasOne
64-
6560
class << self
6661
extend Forwardable
6762
def_delegators :_new_scope, :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last
@@ -253,6 +248,12 @@ def member_endpoint(name, options = {})
253248
# @option options [Symbol] :default The default value for the property
254249
def property(name, options = {})
255250
schema.add(name, options)
251+
define_method(name) do
252+
attributes[name]
253+
end
254+
define_method("#{name}=") do |value|
255+
set_attribute(name, value)
256+
end
256257
end
257258

258259
# Declare multiple properties with the same optional options
@@ -430,8 +431,10 @@ def save
430431
self.attributes = updated.attributes
431432
self.links.attributes = updated.links.attributes
432433
self.relationships.attributes = updated.relationships.attributes
434+
self.relationships.last_result_set = last_result_set
433435
clear_changes_information
434436
self.relationships.clear_changes_information
437+
_clear_cached_relationships
435438
end
436439
true
437440
end
@@ -447,6 +450,9 @@ def destroy
447450
false
448451
else
449452
self.attributes.clear
453+
self.relationships.attributes.clear
454+
self.relationships.last_result_set = nil
455+
_clear_cached_relationships
450456
true
451457
end
452458
end
@@ -491,27 +497,36 @@ def setup_default_properties
491497
end
492498
end
493499

494-
def method_missing(method, *args)
495-
association = association_for(method)
496-
497-
return super unless association || (relationships && relationships.has_attribute?(method))
500+
def relationship_definition_for(name)
501+
relationships[name] if relationships && relationships.has_attribute?(name)
502+
end
498503

499-
return nil unless relationship_definitions = relationships[method]
504+
def included_data_for(name, relationship_definition)
505+
last_result_set.included.data_for(name, relationship_definition)
506+
end
500507

508+
def relationship_data_for(name, relationship_definition)
501509
# look in included data
502-
if relationship_definitions.key?("data")
503-
return last_result_set.included.data_for(method, relationship_definitions)
510+
if relationship_definition.key?("data")
511+
return included_data_for(name, relationship_definition)
504512
end
505513

506-
if association = association_for(method)
507-
# look for a defined relationship url
508-
if relationship_definitions["links"] && url = relationship_definitions["links"]["related"]
509-
return association.data(url)
510-
end
514+
url = relationship_definition["links"]["related"]
515+
if relationship_definition["links"] && url
516+
return association_for(name).data(url)
511517
end
518+
512519
nil
513520
end
514521

522+
def method_missing(method, *args)
523+
relationship_definition = relationship_definition_for(method)
524+
525+
return super unless relationship_definition
526+
527+
relationship_data_for(method, relationship_definition)
528+
end
529+
515530
def respond_to_missing?(symbol, include_all = false)
516531
return true if relationships && relationships.has_attribute?(symbol)
517532
return true if association_for(symbol)

test/unit/resource_test.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ def test_formatted_key_accessors
8686
def test_associations_as_params
8787
article = Article.new(foo: 'bar', 'author' => {'type' => 'authors', 'id' => 1})
8888
assert_equal(article.foo, 'bar')
89-
assert_equal(article.attributes['author']['type'], 'authors')
90-
assert_equal(article.attributes['author']['id'], 1)
89+
assert_equal({'type' => 'authors', 'id' => 1}, article.relationships.author)
90+
assert article.relationships.attribute_changed?(:author)
9191
end
9292

9393
def test_default_params_overrideable

test/unit/updating_test.rb

+92
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,49 @@ def test_can_update_single_relationship
212212
assert article.save
213213
end
214214

215+
def test_can_update_single_relationship_via_setter
216+
articles = Article.find(1)
217+
article = articles.first
218+
219+
stub_request(:patch, "http://example.com/articles/1")
220+
.with(headers: {content_type: "application/vnd.api+json", accept: "application/vnd.api+json"}, body: {
221+
data: {
222+
id: "1",
223+
type: "articles",
224+
relationships: {
225+
author: {
226+
data: {
227+
type: "people",
228+
id: "1"
229+
}
230+
}
231+
},
232+
attributes: {}
233+
}
234+
}.to_json)
235+
.to_return(headers: {status: 200, content_type: "application/vnd.api+json"}, body: {
236+
data: {
237+
type: "articles",
238+
id: "1",
239+
attributes: {
240+
title: "Rails is Omakase"
241+
},
242+
relationships: {
243+
author: {
244+
links: {
245+
self: "/articles/1/links/author",
246+
related: "/articles/1/author",
247+
},
248+
data: { type: "people", id: "1" }
249+
}
250+
}
251+
}
252+
}.to_json)
253+
254+
article.author = Person.new(id: "1")
255+
assert article.save
256+
end
257+
215258
def test_can_update_single_relationship_with_all_attributes_dirty
216259
articles = Article.find(1)
217260
article = articles.first
@@ -313,6 +356,55 @@ def test_can_update_has_many_relationships
313356
assert article.save
314357
end
315358

359+
def test_can_update_has_many_relationships_via_setter
360+
articles = Article.find(1)
361+
article = articles.first
362+
363+
stub_request(:patch, "http://example.com/articles/1")
364+
.with(headers: {content_type: "application/vnd.api+json", accept: "application/vnd.api+json"}, body: {
365+
data: {
366+
id: "1",
367+
type: "articles",
368+
relationships: {
369+
comments: {
370+
data: [{
371+
type: "comments",
372+
id: "2"
373+
},{
374+
type: "comments",
375+
id: "3"
376+
}]
377+
}
378+
},
379+
attributes: {}
380+
}
381+
}.to_json)
382+
.to_return(headers: {status: 200, content_type: "application/vnd.api+json"}, body: {
383+
data: {
384+
id: "1",
385+
type: "articles",
386+
relationships: {
387+
author: {
388+
links: {
389+
self: "/articles/1/links/author",
390+
related: "/articles/1/author",
391+
},
392+
data: { type: "people", id: "1" }
393+
}
394+
},
395+
attributes: {
396+
title: "Rails is Omakase"
397+
}
398+
}
399+
}.to_json)
400+
401+
article.comments = [
402+
Comment.new(id: "2"),
403+
Comment.new(id: "3")
404+
]
405+
assert article.save
406+
end
407+
316408
def test_can_update_has_many_relationships_with_all_attributes_dirty
317409
articles = Article.find(1)
318410
article = articles.first

0 commit comments

Comments
 (0)