remote_association 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/Gemfile.lock +6 -6
- data/LICENSE +1 -1
- data/README.md +54 -12
- data/lib/remote_association/active_record/relation.rb +107 -27
- data/lib/remote_association/belongs_to_remote.rb +18 -6
- data/lib/remote_association/has_many_remote.rb +72 -0
- data/lib/remote_association/has_one_remote.rb +10 -3
- data/lib/remote_association/version.rb +1 -1
- data/lib/remote_association.rb +6 -3
- data/spec/remote_association/belongs_to_remote_spec.rb +51 -5
- data/spec/remote_association/has_many_remote_spec.rb +84 -0
- data/spec/remote_association/has_one_remote_spec.rb +15 -3
- data/spec/remote_association/remote_association_spec.rb +38 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/tasks/db_setup.rake +1 -1
- metadata +7 -4
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
remote_association (0.0.
|
4
|
+
remote_association (0.0.3)
|
5
5
|
activerecord (~> 3.2)
|
6
6
|
activeresource (~> 3.2)
|
7
7
|
activesupport (~> 3.2)
|
@@ -24,22 +24,22 @@ GEM
|
|
24
24
|
i18n (~> 0.6)
|
25
25
|
multi_json (~> 1.0)
|
26
26
|
arel (3.0.2)
|
27
|
-
builder (3.0.
|
27
|
+
builder (3.0.3)
|
28
28
|
database_cleaner (0.8.0)
|
29
29
|
diff-lcs (1.1.3)
|
30
30
|
fakeweb (1.3.0)
|
31
|
-
i18n (0.6.
|
31
|
+
i18n (0.6.1)
|
32
32
|
multi_json (1.3.6)
|
33
|
-
pg (0.14.
|
33
|
+
pg (0.14.1)
|
34
34
|
rake (0.9.2.2)
|
35
35
|
rspec (2.11.0)
|
36
36
|
rspec-core (~> 2.11.0)
|
37
37
|
rspec-expectations (~> 2.11.0)
|
38
38
|
rspec-mocks (~> 2.11.0)
|
39
39
|
rspec-core (2.11.1)
|
40
|
-
rspec-expectations (2.11.
|
40
|
+
rspec-expectations (2.11.3)
|
41
41
|
diff-lcs (~> 1.1.3)
|
42
|
-
rspec-mocks (2.11.
|
42
|
+
rspec-mocks (2.11.3)
|
43
43
|
tzinfo (0.3.33)
|
44
44
|
|
45
45
|
PLATFORMS
|
data/LICENSE
CHANGED
@@ -16,7 +16,7 @@ included in all copies or substantial portions of the Software.
|
|
16
16
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
17
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
18
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
-
|
19
|
+
NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
21
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
22
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
# Remote Association
|
5
5
|
|
6
|
-
Add ```
|
6
|
+
Add ```has_one_remote```, ```has_many_remote```, and ```belongs_to_remote``` associations to models inherited from ActiveResource::Base
|
7
7
|
|
8
8
|
Say, you have Author and a service with Profiles. You can access profile of author as easy as `author.profile`.
|
9
9
|
|
@@ -20,16 +20,48 @@
|
|
20
20
|
## Example
|
21
21
|
|
22
22
|
```ruby
|
23
|
+
class UserGroup < ActiveResource::Base
|
24
|
+
self.site = 'http://example.com'
|
25
|
+
end
|
26
|
+
|
23
27
|
class Profile < ActiveResource::Base
|
24
|
-
self.site =
|
28
|
+
self.site = 'http://example.com'
|
29
|
+
end
|
30
|
+
|
31
|
+
class Badge < ActiveResource::Base
|
32
|
+
self.site = 'http://example.com'
|
25
33
|
end
|
34
|
+
|
26
35
|
class User < ActiveRecord::Base
|
27
36
|
include RemoteAssociation::Base
|
28
|
-
|
37
|
+
|
38
|
+
has_one_remote :profile
|
39
|
+
has_many_remote :badges
|
40
|
+
belongs_to_remote :group, class_name: 'UserGroup', foreign_key: :group_id, primary_key: 'search[id_in]'
|
29
41
|
end
|
30
42
|
|
31
|
-
User.first.profile
|
43
|
+
User.first.profile # => <Profile>
|
44
|
+
User.first.group # => <Group>
|
45
|
+
User.first.badges # => [<Badge>, <Badge>]
|
46
|
+
```
|
47
|
+
|
48
|
+
## Advanced usage
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# Will load associated objects, when we will need them
|
52
|
+
users = Users.scoped.includes_remote(:profile, :badges)
|
53
|
+
|
54
|
+
# just adding SQL condition to out users relation
|
55
|
+
users = users.where(active: true)
|
56
|
+
|
57
|
+
# add additional search condition for request to Profiles API
|
58
|
+
users = users.where_remote(profile: {search: {kind_in: ['Facebook', 'GitHub']}})
|
32
59
|
|
60
|
+
# time to do ordering and pagination...
|
61
|
+
users = users.offset.(100).limit(5).order('name ASC')
|
62
|
+
|
63
|
+
# Fetch 10 users from DB, fetch 10 Profiles and Avatars for those users
|
64
|
+
users = users.all
|
33
65
|
```
|
34
66
|
|
35
67
|
## Installation
|
@@ -38,14 +70,24 @@ Add this line to your application's Gemfile:
|
|
38
70
|
|
39
71
|
gem 'remote_association'
|
40
72
|
|
41
|
-
## TODO
|
42
|
-
|
43
|
-
Implement 'has_many_remote' analogie of 'AR.has_many'
|
44
|
-
|
45
73
|
## Contributing
|
46
74
|
|
47
75
|
1. Fork it
|
48
|
-
2.
|
49
|
-
|
50
|
-
|
51
|
-
|
76
|
+
2. Set up testing database via
|
77
|
+
|
78
|
+
rake spec:db:setup
|
79
|
+
|
80
|
+
3. Create your feature branch
|
81
|
+
|
82
|
+
git checkout -b my-new-feature
|
83
|
+
|
84
|
+
4. Add tests and run via `rspec`
|
85
|
+
5. Commit your changes
|
86
|
+
|
87
|
+
git commit -am 'Added some feature'
|
88
|
+
|
89
|
+
6. Push to the branch
|
90
|
+
|
91
|
+
git push origin my-new-feature
|
92
|
+
|
93
|
+
7. Create new Pull Request
|
@@ -1,61 +1,141 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
1
3
|
module ActiveRecord
|
2
4
|
class Relation
|
3
|
-
#
|
4
|
-
# The analogy is <tt>includes(*args)</tt> of ActiveRecord
|
5
|
+
# Queues loading of relations to ActiveModel models of models, selected by current relation.
|
6
|
+
# The full analogy is <tt>includes(*args)</tt> of ActiveRecord: all the realted objects will
|
7
|
+
# be loaded when one or all objects of relation are required.
|
5
8
|
#
|
6
9
|
# May raise <tt>RemoteAssociation::SettingsNotFoundError</tt> if one of args can't be found among
|
7
10
|
# Class.activeresource_relations settings
|
8
11
|
#
|
9
|
-
#
|
12
|
+
# Would not perform remote request if all associated foreign_keys of belongs_to_remote association are nil
|
13
|
+
#
|
14
|
+
# Returns self - <tt>ActiveRecord::Relation</tt>
|
10
15
|
#
|
11
16
|
# === Examples
|
12
17
|
#
|
13
|
-
# Author.scoped.includes_remote(:profile, :avatar)
|
18
|
+
# Author.scoped.includes_remote(:profile, :avatar).where(author_name: 'Tom').all
|
14
19
|
def includes_remote(*args)
|
15
20
|
args.each do |r|
|
16
21
|
settings = klass.activeresource_relations[r.to_sym]
|
17
22
|
raise RemoteAssociation::SettingsNotFoundError, "Can't find settings for #{r} association" if settings.blank?
|
18
23
|
|
19
|
-
|
20
|
-
foregin_key = settings[:foreign_key]
|
21
|
-
ar_class = settings[:class_name ].constantize
|
24
|
+
ar_class = settings[:class_name].constantize
|
22
25
|
|
23
|
-
|
24
|
-
|
26
|
+
remote_associations << OpenStruct.new(
|
27
|
+
ar_accessor: r.to_sym,
|
28
|
+
foreign_key: settings[:foreign_key],
|
29
|
+
ar_class: ar_class,
|
30
|
+
association_type: settings[:association_type]
|
31
|
+
)
|
25
32
|
end
|
26
33
|
|
27
|
-
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# Adds conditions (i.e. http query string parameters) to request of each remote API. Those are parameters to query string.
|
38
|
+
#
|
39
|
+
# Returns self - <tt>ActiveRecord::Relation</tt>
|
40
|
+
#
|
41
|
+
# === Example
|
42
|
+
#
|
43
|
+
# Author.scoped.includes_remote(:profile, :avatar).where_remote(profile: {search: {public: true}}, avatar: { primary: true }).all
|
44
|
+
#
|
45
|
+
# #=> Will do requests to:
|
46
|
+
# # * http://.../prefiles.json?author_id[]=1&author_id[]=N&search[public][]=true
|
47
|
+
# # * http://.../avatars.json?author_id[]=1&author_id[]=N&primary=true
|
48
|
+
def where_remote(conditions = {})
|
49
|
+
conditions.each do |association, conditions|
|
50
|
+
remote_conditions[association.to_sym] = remote_conditions[association.to_sym].deep_merge(conditions)
|
51
|
+
end
|
28
52
|
|
29
|
-
self
|
53
|
+
self
|
30
54
|
end
|
31
55
|
|
32
|
-
|
56
|
+
private
|
57
|
+
|
58
|
+
# Array of remote associations to load.
|
59
|
+
# It contains Hashes with settings for loader.
|
60
|
+
attr_accessor :remote_associations
|
61
|
+
|
62
|
+
def remote_associations
|
63
|
+
@remote_associations ||= []
|
64
|
+
end
|
65
|
+
|
66
|
+
# Hash of parameters to merge into API requests.
|
67
|
+
attr_accessor :remote_conditions
|
68
|
+
|
69
|
+
def remote_conditions
|
70
|
+
@remote_conditions ||= Hash.new({})
|
71
|
+
end
|
72
|
+
|
73
|
+
# A method proxy for exec_queries: it wraps around original one
|
74
|
+
# and preloads remote associations. Returns {Array} of fetched
|
75
|
+
# records, like original exec_queries.
|
76
|
+
def exec_queries_with_remote_associations
|
77
|
+
exec_queries_without_remote_associations
|
78
|
+
preload_remote_associations
|
79
|
+
@records
|
80
|
+
end
|
33
81
|
|
34
|
-
|
35
|
-
|
82
|
+
alias_method :exec_queries_without_remote_associations, :exec_queries
|
83
|
+
alias_method :exec_queries, :exec_queries_with_remote_associations
|
36
84
|
|
37
|
-
|
85
|
+
# Does heavy lifting on fetching remote associations from distant places:
|
86
|
+
# - checks, if remote_resources_loaded? already
|
87
|
+
# - iterates through remote_associations and loads objects for each one
|
88
|
+
def preload_remote_associations
|
89
|
+
return true if remote_resources_loaded?
|
38
90
|
|
39
|
-
|
40
|
-
|
91
|
+
remote_associations.each do |r|
|
92
|
+
case r.association_type
|
93
|
+
when :has_one_remote then fetch_and_join_for_has_any_remote(r)
|
94
|
+
when :has_many_remote then fetch_and_join_for_has_any_remote(r)
|
95
|
+
when :belongs_to_remote then fetch_and_join_for_belongs_to_remote(r)
|
41
96
|
end
|
42
97
|
end
|
43
98
|
|
44
|
-
|
45
|
-
|
99
|
+
set_remote_resources_loaded unless remote_associations.empty?
|
100
|
+
end
|
46
101
|
|
47
|
-
|
102
|
+
def fetch_and_join_for_has_any_remote(settings)
|
103
|
+
keys = @records.uniq.map(&:id)
|
48
104
|
|
49
|
-
|
50
|
-
|
51
|
-
|
105
|
+
remote_objects = fetch_remote_objects(settings.ar_class, keys, settings.ar_accessor)
|
106
|
+
|
107
|
+
@records.each do |record|
|
108
|
+
record.send("#{settings.ar_accessor}=", remote_objects.select { |s| s.send(settings.foreign_key) == record.id })
|
52
109
|
end
|
110
|
+
end
|
53
111
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
112
|
+
def fetch_and_join_for_belongs_to_remote(settings)
|
113
|
+
keys = @records.uniq.map {|r| r.send settings.foreign_key.to_sym }.compact
|
114
|
+
|
115
|
+
return if keys.empty?
|
116
|
+
|
117
|
+
remote_objects = fetch_remote_objects(settings.ar_class, keys, settings.ar_accessor)
|
118
|
+
|
119
|
+
@records.each do |record|
|
120
|
+
record.send("#{settings.ar_accessor}=", remote_objects.select { |s| record.send(settings.foreign_key) == s.id })
|
58
121
|
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def set_remote_resources_loaded
|
125
|
+
@remote_resources_loaded = true
|
126
|
+
@records.each do |record|
|
127
|
+
record.instance_variable_set(:@remote_resources_loaded, true)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def fetch_remote_objects(ar_class, keys, ar_accessor)
|
132
|
+
params = klass.build_params_hash(keys).deep_merge(remote_conditions[ar_accessor.to_sym])
|
133
|
+
ar_class.find(:all, :params => params )
|
134
|
+
end
|
135
|
+
|
136
|
+
def remote_resources_loaded?
|
137
|
+
!!@remote_resources_loaded
|
138
|
+
end
|
59
139
|
|
60
140
|
end
|
61
141
|
end
|
@@ -9,6 +9,7 @@ module RemoteAssociation
|
|
9
9
|
#
|
10
10
|
# [association]
|
11
11
|
# Returns the associated object. +nil+ is returned if none is found.
|
12
|
+
# When foreign_key value is nil, remote request would not be executed.
|
12
13
|
# [association=(associate)]
|
13
14
|
# Just setter, no saves.
|
14
15
|
#
|
@@ -34,15 +35,20 @@ module RemoteAssociation
|
|
34
35
|
# association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
|
35
36
|
# <tt>belongs_to_remote :favorite_person, :class_name => "Person"</tt> will use a foreign key
|
36
37
|
# of "favorite_person_id".
|
38
|
+
# [:primary_key]
|
39
|
+
# Specify the http query parameter to find associated object used for the association. By default this is <tt>id</tt>.
|
40
|
+
# Example:
|
41
|
+
# belongs_to_remote :firm, :primary_key => 'search[id_in]' #=> ...?firms.json?search%5Bid_in%5D%5B%5D=1
|
37
42
|
#
|
38
43
|
# Option examples:
|
39
|
-
#
|
40
|
-
#
|
44
|
+
# belongs_to_remote :firm, :foreign_key => "client_of"
|
45
|
+
# belongs_to_remote :author, :class_name => "Person", :foreign_key => "author_id"
|
41
46
|
def belongs_to_remote(remote_rel, options ={})
|
42
47
|
rel_options = {
|
43
48
|
class_name: remote_rel.to_s.classify,
|
44
49
|
foreign_key: remote_rel.to_s.foreign_key,
|
45
|
-
association_type: :belongs_to_remote
|
50
|
+
association_type: :belongs_to_remote,
|
51
|
+
primary_key: 'id'
|
46
52
|
}.merge(options.symbolize_keys)
|
47
53
|
|
48
54
|
add_activeresource_relation(remote_rel.to_sym, rel_options)
|
@@ -52,16 +58,22 @@ module RemoteAssociation
|
|
52
58
|
attr_accessor :#{remote_rel}
|
53
59
|
|
54
60
|
def #{remote_rel}
|
55
|
-
if
|
61
|
+
if remote_resources_loaded?
|
56
62
|
@#{remote_rel} ? @#{remote_rel}.first : nil
|
57
63
|
else
|
58
|
-
@#{remote_rel} ||= #{rel_options[:class_name]}.find(:first, params:
|
64
|
+
@#{remote_rel} ||= self.#{rel_options[:foreign_key]}.present? ? #{rel_options[:class_name]}.find(:first, params: self.class.build_params_hash(self.#{rel_options[:foreign_key]})) : nil
|
59
65
|
end
|
60
66
|
end
|
61
67
|
|
68
|
+
##
|
69
|
+
# Returns Hash with HTTP parameters to query remote API
|
70
|
+
def self.build_params_hash(keys)
|
71
|
+
keys = [keys] unless keys.kind_of?(Array)
|
72
|
+
{"#{rel_options[:primary_key]}" => keys}
|
73
|
+
end
|
74
|
+
|
62
75
|
RUBY
|
63
76
|
|
64
77
|
end
|
65
|
-
|
66
78
|
end
|
67
79
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module RemoteAssociation
|
2
|
+
module HasManyRemote
|
3
|
+
# Specifies a one-to-many association with another class. This method should only be used
|
4
|
+
# if this class is a kind of ActiveResource::Base and service for this resource can
|
5
|
+
# return some kind of foreign key.
|
6
|
+
#
|
7
|
+
# Methods will be added for retrieval and query for a single associated object, for which
|
8
|
+
# this object holds an id:
|
9
|
+
#
|
10
|
+
# [associations()]
|
11
|
+
# Returns the associated objects. +[]+ is returned if none is found.
|
12
|
+
# [associations=(associates)]
|
13
|
+
# Just setter, no saves.
|
14
|
+
#
|
15
|
+
# (+associations+ is replaced with the symbol passed as the first argument, so
|
16
|
+
# <tt>has_many_remote :authors</tt> would add among others <tt>authors.nil?</tt>.)
|
17
|
+
#
|
18
|
+
# === Example
|
19
|
+
#
|
20
|
+
# A Author class declares <tt>has_many_remote :profiles</tt>, which will add:
|
21
|
+
# * <tt>Author#profiles</tt> (similar to <tt>Profile.find(:all, params: { author_id: [author.id]})</tt>)
|
22
|
+
# * <tt>Author#profiles=(profile)</tt> (will set @profiles instance variable of Author# to profile value)
|
23
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
24
|
+
#
|
25
|
+
# === Options
|
26
|
+
#
|
27
|
+
# [:class_name]
|
28
|
+
# Specify the class name of the association. Use it only if that name can't be inferred
|
29
|
+
# from the association name. So <tt>has_many_remote :profiles</tt> will by default be linked to the Profile class, but
|
30
|
+
# if the real class name is SocialProfile, you'll have to specify it with this option.
|
31
|
+
# [:foreign_key]
|
32
|
+
# Specify the foreign key used for searching association on remote service. By default this is guessed to be the name
|
33
|
+
# of the current class with an "_id" suffix. So a class Author that defines a <tt>has_many_remote :profiles</tt>
|
34
|
+
# association will use "author_id" as the default <tt>:foreign_key</tt>.
|
35
|
+
# This key will be used in :get request. Example: <tt>GET http://example.com/profiles?author_id[]=1</tt>
|
36
|
+
#
|
37
|
+
# Option examples:
|
38
|
+
# has_many_remote :firms, :foreign_key => "client_of"
|
39
|
+
# has_many_remote :badges, :class_name => "Label", :foreign_key => "author_id"
|
40
|
+
def has_many_remote(remote_rel, options ={})
|
41
|
+
rel_options = {
|
42
|
+
class_name: remote_rel.to_s.singularize.classify,
|
43
|
+
foreign_key: self.model_name.to_s.foreign_key,
|
44
|
+
association_type: :has_many_remote
|
45
|
+
}.merge(options.symbolize_keys)
|
46
|
+
|
47
|
+
add_activeresource_relation(remote_rel.to_sym, rel_options)
|
48
|
+
|
49
|
+
class_eval <<-RUBY, __FILE__, __LINE__+1
|
50
|
+
|
51
|
+
attr_accessor :#{remote_rel}
|
52
|
+
|
53
|
+
def #{remote_rel} # def customers
|
54
|
+
if remote_resources_loaded? # if remote_resources_loaded?
|
55
|
+
@#{remote_rel} ? @#{remote_rel} : [] # @customers ? @customers : []
|
56
|
+
else # else
|
57
|
+
@#{remote_rel} ||= #{rel_options[:class_name]}. # @customers ||= Person.
|
58
|
+
find(:all, params: self.class.build_params_hash(self.id)) # find(:all, params: self.class.build_params_hash(self.id))
|
59
|
+
end # end
|
60
|
+
end # end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Returns Hash with HTTP parameters to query remote API
|
64
|
+
def self.build_params_hash(keys)
|
65
|
+
keys = [keys] unless keys.kind_of?(Array)
|
66
|
+
{"#{rel_options[:foreign_key]}" => keys}
|
67
|
+
end
|
68
|
+
|
69
|
+
RUBY
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -18,7 +18,7 @@ module RemoteAssociation
|
|
18
18
|
# === Example
|
19
19
|
#
|
20
20
|
# A Author class declares <tt>has_one_remote :profile</tt>, which will add:
|
21
|
-
# * <tt>
|
21
|
+
# * <tt>Author#profile</tt> (similar to <tt>Profile.find(:first, params: { author_id: [author.id]})</tt>)
|
22
22
|
# * <tt>Author#profile=(profile)</tt> (will set @profile instance variable of Author# to profile value)
|
23
23
|
# The declaration can also include an options hash to specialize the behavior of the association.
|
24
24
|
#
|
@@ -51,13 +51,20 @@ module RemoteAssociation
|
|
51
51
|
attr_accessor :#{remote_rel}
|
52
52
|
|
53
53
|
def #{remote_rel}
|
54
|
-
if
|
54
|
+
if remote_resources_loaded?
|
55
55
|
@#{remote_rel} ? @#{remote_rel}.first : nil
|
56
56
|
else
|
57
|
-
@#{remote_rel} ||= #{rel_options[:class_name]}.find(:first, params:
|
57
|
+
@#{remote_rel} ||= #{rel_options[:class_name]}.find(:first, params: self.class.build_params_hash(self.id))
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
+
##
|
62
|
+
# Returns Hash with HTTP parameters to query remote API
|
63
|
+
def self.build_params_hash(keys)
|
64
|
+
keys = [keys] unless keys.kind_of?(Array)
|
65
|
+
{"#{rel_options[:foreign_key]}" => keys}
|
66
|
+
end
|
67
|
+
|
61
68
|
RUBY
|
62
69
|
|
63
70
|
end
|
data/lib/remote_association.rb
CHANGED
@@ -3,6 +3,7 @@ require "active_support"
|
|
3
3
|
|
4
4
|
require "remote_association/version"
|
5
5
|
require "remote_association/has_one_remote"
|
6
|
+
require "remote_association/has_many_remote"
|
6
7
|
require "remote_association/belongs_to_remote"
|
7
8
|
require "remote_association/active_record/relation"
|
8
9
|
|
@@ -11,7 +12,8 @@ module RemoteAssociation
|
|
11
12
|
# Include this class to hav associations to ActiveResource models
|
12
13
|
#
|
13
14
|
# It will add methods to your class:
|
14
|
-
# * <tt>has_one_remote(name, *
|
15
|
+
# * <tt>has_one_remote(name, *options)</tt>
|
16
|
+
# * <tt>has_many_remote(name, *options)</tt>
|
15
17
|
# * <tt>activeresource_relations</tt>
|
16
18
|
# * <tt>add_activeresource_relation(name, options)</tt>
|
17
19
|
module Base
|
@@ -25,6 +27,7 @@ module RemoteAssociation
|
|
25
27
|
|
26
28
|
module ClassMethods
|
27
29
|
include RemoteAssociation::HasOneRemote
|
30
|
+
include RemoteAssociation::HasManyRemote
|
28
31
|
include RemoteAssociation::BelongsToRemote
|
29
32
|
|
30
33
|
# Adds settings of relation to ActiveResource model.
|
@@ -55,8 +58,8 @@ module RemoteAssociation
|
|
55
58
|
module InstanceMethods
|
56
59
|
|
57
60
|
# Returns <tt>true</tt> if associations to remote relations already loaded and set
|
58
|
-
def
|
59
|
-
@
|
61
|
+
def remote_resources_loaded?
|
62
|
+
@remote_resources_loaded
|
60
63
|
end
|
61
64
|
end
|
62
65
|
end
|
@@ -24,20 +24,57 @@ describe RemoteAssociation, "method :belongs_to_remote" do
|
|
24
24
|
include RemoteAssociation::Base
|
25
25
|
belongs_to_remote :user
|
26
26
|
end
|
27
|
-
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/users.json?id%5B%5D=1", body: @body
|
27
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/users.json?id%5B%5D=1", body: @body)
|
28
28
|
|
29
29
|
Profile.first.user.name.should eq('User A')
|
30
30
|
end
|
31
31
|
|
32
|
+
it 'returns nil if no object present' do
|
33
|
+
unset_const(:Profile)
|
34
|
+
class Profile < ActiveRecord::Base
|
35
|
+
include RemoteAssociation::Base
|
36
|
+
belongs_to_remote :user
|
37
|
+
end
|
38
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/users.json?id%5B%5D=1", body: [].to_json)
|
39
|
+
|
40
|
+
Profile.first.user.should be_nil
|
41
|
+
end
|
42
|
+
|
32
43
|
it 'should prefetch remote associations of models with defaults (single request)' do
|
33
44
|
add_profile(2, 2, "letter B")
|
34
45
|
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/users.json?id%5B%5D=1&id%5B%5D=2", body: @full_body)
|
35
46
|
|
36
|
-
profiles = Profile.scoped.includes_remote(:user)
|
47
|
+
profiles = Profile.scoped.includes_remote(:user).all
|
37
48
|
profiles.first.user.name.should eq('User A')
|
38
49
|
profiles.last.user.name.should eq('User B')
|
39
50
|
end
|
40
51
|
|
52
|
+
it "should not request remote collection in single request when all foreign_keys are nil" do
|
53
|
+
Profile.delete_all
|
54
|
+
add_profile(1, 'NULL', "A")
|
55
|
+
add_profile(2, 'NULL', "A")
|
56
|
+
profiles = Profile.scoped.includes_remote(:user).all
|
57
|
+
profiles.map(&:user).should eq [nil, nil]
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should not request remote data when foreign_key value is nil" do
|
61
|
+
profile = Profile.new(user_id: nil)
|
62
|
+
profile.user.should be_nil
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#build_params_hash" do
|
66
|
+
it "returns valid Hash of HTTP query string parameters" do
|
67
|
+
unset_const(:Profile)
|
68
|
+
class Profile < ActiveRecord::Base
|
69
|
+
include RemoteAssociation::Base
|
70
|
+
belongs_to_remote :user
|
71
|
+
end
|
72
|
+
|
73
|
+
Profile.build_params_hash(10).should eq({'id' => [10]})
|
74
|
+
Profile.build_params_hash([10, 13, 15]).should eq({'id' => [10, 13, 15]})
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
41
78
|
describe "has options:" do
|
42
79
|
it ":class_name - able to choose custom class of association" do
|
43
80
|
unset_const(:Profile)
|
@@ -50,8 +87,9 @@ describe RemoteAssociation, "method :belongs_to_remote" do
|
|
50
87
|
include RemoteAssociation::Base
|
51
88
|
belongs_to_remote :user, class_name: "CustomUser"
|
52
89
|
end
|
53
|
-
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/users.json?id%5B%5D=1", body: @body
|
90
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/users.json?id%5B%5D=1", body: @body)
|
54
91
|
end
|
92
|
+
|
55
93
|
it ":foreign_key - can set key to extract from it's model" do
|
56
94
|
unset_const(:Profile)
|
57
95
|
class Profile < ActiveRecord::Base
|
@@ -61,10 +99,18 @@ describe RemoteAssociation, "method :belongs_to_remote" do
|
|
61
99
|
end
|
62
100
|
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/users.json?id%5B%5D=1", body: @body)
|
63
101
|
end
|
102
|
+
|
103
|
+
it ":primary_key - can set key to query from remote API" do
|
104
|
+
unset_const(:Profile)
|
105
|
+
class Profile < ActiveRecord::Base
|
106
|
+
include RemoteAssociation::Base
|
107
|
+
belongs_to_remote :user, primary_key: 'search[id_in]'
|
108
|
+
end
|
109
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/users.json?search%5Bid_in%5D%5B%5D=1", body: @body)
|
110
|
+
end
|
111
|
+
|
64
112
|
after(:each) do
|
65
113
|
Profile.first.user.name.should eq('User A')
|
66
114
|
end
|
67
115
|
end
|
68
|
-
|
69
|
-
|
70
116
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RemoteAssociation, 'method :has_many_remote' do
|
4
|
+
before(:all) do
|
5
|
+
@body = [
|
6
|
+
{profile: {id: 1, user_id: 1, like: "letter A"}},
|
7
|
+
{profile: {id: 2, user_id: 1, like: "letter B"}}
|
8
|
+
].to_json
|
9
|
+
@full_body = [
|
10
|
+
{profile: {id: 1, user_id: 1, like: "letter A"}},
|
11
|
+
{profile: {id: 2, user_id: 1, like: "letter B"}},
|
12
|
+
{profile: {id: 3, user_id: 2, like: "letter C"}},
|
13
|
+
].to_json
|
14
|
+
end
|
15
|
+
|
16
|
+
before(:each) do
|
17
|
+
unset_const(:User)
|
18
|
+
unset_const(:Profile)
|
19
|
+
class User < ActiveRecord::Base
|
20
|
+
include RemoteAssociation::Base
|
21
|
+
has_many_remote :profiles
|
22
|
+
end
|
23
|
+
class Profile < ActiveResource::Base
|
24
|
+
self.site = REMOTE_HOST
|
25
|
+
end
|
26
|
+
|
27
|
+
add_user(1,"User A")
|
28
|
+
add_user(2,"User B")
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'uses default settings' do
|
32
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?user_id%5B%5D=1", body: @body )
|
33
|
+
User.first.profiles.map(&:like).should eq ["letter A", "letter B"]
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'returns empty Array if no objects present' do
|
37
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?user_id%5B%5D=1", body: [].to_json )
|
38
|
+
User.first.profiles.should eq []
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should prefetch remote associations of models with defaults (single request)' do
|
42
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?user_id%5B%5D=1&user_id%5B%5D=2", body: @full_body)
|
43
|
+
|
44
|
+
users = User.scoped.includes_remote(:profiles).all
|
45
|
+
users.first.profiles.map(&:like).should eq ["letter A", "letter B"]
|
46
|
+
users.last.profiles.map(&:like).should eq ["letter C"]
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#build_params_hash' do
|
50
|
+
it 'returns valid Hash of HTTP query string parameters' do
|
51
|
+
User.build_params_hash(10).should eq({'user_id' => [10]})
|
52
|
+
User.build_params_hash([10, 13, 15]).should eq({'user_id' => [10, 13, 15]})
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'options' do
|
57
|
+
it ":class_name" do
|
58
|
+
unset_const(:User)
|
59
|
+
unset_const(:CustomProfile)
|
60
|
+
class CustomProfile < ActiveResource::Base
|
61
|
+
self.site = REMOTE_HOST
|
62
|
+
self.element_name = "profile"
|
63
|
+
end
|
64
|
+
class User < ActiveRecord::Base
|
65
|
+
include RemoteAssociation::Base
|
66
|
+
has_many_remote :profiles, class_name: "CustomProfile"
|
67
|
+
end
|
68
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?user_id%5B%5D=1", body: @body )
|
69
|
+
end
|
70
|
+
|
71
|
+
it ":foreign_key" do
|
72
|
+
unset_const(:User)
|
73
|
+
class User < ActiveRecord::Base
|
74
|
+
include RemoteAssociation::Base
|
75
|
+
has_many_remote :profiles, foreign_key: 'search[login_id_in]'
|
76
|
+
end
|
77
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?search%5Blogin_id_in%5D%5B%5D=1", body: @body)
|
78
|
+
end
|
79
|
+
|
80
|
+
after(:each) do
|
81
|
+
User.first.profiles.map(&:like).should eq(['letter A', 'letter B'])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -30,14 +30,26 @@ describe RemoteAssociation, "method :has_one_remote" do
|
|
30
30
|
User.first.profile.like.should eq('letter A')
|
31
31
|
end
|
32
32
|
|
33
|
+
it 'returns nil if no object present' do
|
34
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?user_id%5B%5D=1", body: [].to_json )
|
35
|
+
User.first.profile.should be_nil
|
36
|
+
end
|
37
|
+
|
33
38
|
it 'should prefetch remote associations of models with defaults (single request)' do
|
34
39
|
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?user_id%5B%5D=1&user_id%5B%5D=2", body: @full_body)
|
35
40
|
|
36
|
-
users = User.scoped.includes_remote(:profile)
|
41
|
+
users = User.scoped.includes_remote(:profile).all
|
37
42
|
users.first.profile.like.should eq('letter A')
|
38
43
|
users.last.profile.like.should eq('letter B')
|
39
44
|
end
|
40
45
|
|
46
|
+
describe "#build_params_hash" do
|
47
|
+
it "returns valid Hash of HTTP query string parameters" do
|
48
|
+
User.build_params_hash(10).should eq({'user_id' => [10]})
|
49
|
+
User.build_params_hash([10, 13, 15]).should eq({'user_id' => [10, 13, 15]})
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
41
53
|
describe "has options:" do
|
42
54
|
it ":class_name - able to choose custom class of association" do
|
43
55
|
unset_const(:User)
|
@@ -56,9 +68,9 @@ describe RemoteAssociation, "method :has_one_remote" do
|
|
56
68
|
unset_const(:User)
|
57
69
|
class User < ActiveRecord::Base
|
58
70
|
include RemoteAssociation::Base
|
59
|
-
has_one_remote :profile, foreign_key:
|
71
|
+
has_one_remote :profile, foreign_key: 'search[login_id_in]'
|
60
72
|
end
|
61
|
-
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?
|
73
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?search%5Blogin_id_in%5D%5B%5D=1", body: @body)
|
62
74
|
end
|
63
75
|
after(:each) do
|
64
76
|
User.first.profile.like.should eq('letter A')
|
@@ -7,6 +7,7 @@ describe RemoteAssociation do
|
|
7
7
|
{profile: {id: 2, user_id: 2, like: "letter B"}}
|
8
8
|
]
|
9
9
|
end
|
10
|
+
|
10
11
|
before(:each) do
|
11
12
|
unset_const(:Profile)
|
12
13
|
unset_const(:User)
|
@@ -42,11 +43,47 @@ describe RemoteAssociation do
|
|
42
43
|
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/other_profiles.json?user_id%5B%5D=1&user_id%5B%5D=2", body: @profiles_json.to_json)
|
43
44
|
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?user_id%5B%5D=1&user_id%5B%5D=2", body: @profiles_json.to_json)
|
44
45
|
|
45
|
-
users = User.scoped.includes_remote(:profile, :other_profile)
|
46
|
+
users = User.scoped.includes_remote(:profile, :other_profile).all
|
46
47
|
users.first.profile.like.should eq('letter A')
|
47
48
|
users.last.profile.like.should eq('letter B')
|
48
49
|
users.first.other_profile.like.should eq('letter A')
|
49
50
|
users.last.other_profile.like.should eq('letter B')
|
50
51
|
end
|
51
52
|
|
53
|
+
it "should fetch remote objects right after ActiveRecord fetched array of data" do
|
54
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?user_id%5B%5D=2",
|
55
|
+
body: [{profile: {id: 2, user_id: 2, like: "letter B"}}].to_json)
|
56
|
+
|
57
|
+
t = User.arel_table
|
58
|
+
users = User.where(t[:name].matches('%User%')).includes_remote(:profile).
|
59
|
+
where(t[:name].matches('%B%')).all
|
60
|
+
users.map(&:profile).flatten.map(&:like).should eq(['letter B'])
|
61
|
+
end
|
62
|
+
|
63
|
+
it "can set additional conditions for API call" do
|
64
|
+
unset_const(:OtherProfile)
|
65
|
+
unset_const(:User)
|
66
|
+
class OtherProfile < ActiveResource::Base
|
67
|
+
self.site = REMOTE_HOST
|
68
|
+
end
|
69
|
+
class User < ActiveRecord::Base
|
70
|
+
include RemoteAssociation::Base
|
71
|
+
has_one_remote :profile
|
72
|
+
has_one_remote :other_profile
|
73
|
+
end
|
74
|
+
|
75
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?user_id%5B%5D=2&search%5Bcountry_equals%5D=Ukraine&search%5Bage_less%5D=30",
|
76
|
+
body: [{profile: {id: 2, user_id: 2, like: "letter B"}}].to_json)
|
77
|
+
FakeWeb.register_uri(:get, "#{REMOTE_HOST}/other_profiles.json?capitals=false&user_id%5B%5D=2",
|
78
|
+
body: [{other_profile: {id: 2, user_id: 2, like: "letter b"}}].to_json)
|
79
|
+
|
80
|
+
users = User.scoped.includes_remote(:profile, :other_profile).
|
81
|
+
where_remote(profile: {search: {country_equals: 'Ukraine'}}, other_profile: {capitals: false} ).
|
82
|
+
where(id: 2).
|
83
|
+
where_remote(profile: {search: {age_less: 30}}).all
|
84
|
+
users.first.profile.like.should eq('letter B')
|
85
|
+
users.first.other_profile.like.should eq('letter b')
|
86
|
+
|
87
|
+
end
|
88
|
+
|
52
89
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/tasks/db_setup.rake
CHANGED
@@ -29,7 +29,7 @@ DROP TABLE IF EXISTS "public"."profiles";
|
|
29
29
|
|
30
30
|
CREATE TABLE "public"."profiles" (
|
31
31
|
"id" int4 NOT NULL,
|
32
|
-
"user_id" int4
|
32
|
+
"user_id" int4,
|
33
33
|
"like" varchar(255) NOT NULL,
|
34
34
|
CONSTRAINT "profiles_pkey" PRIMARY KEY ("id") NOT DEFERRABLE INITIALLY IMMEDIATE
|
35
35
|
)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: remote_association
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-10-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -141,11 +141,13 @@ files:
|
|
141
141
|
- lib/remote_association.rb
|
142
142
|
- lib/remote_association/active_record/relation.rb
|
143
143
|
- lib/remote_association/belongs_to_remote.rb
|
144
|
+
- lib/remote_association/has_many_remote.rb
|
144
145
|
- lib/remote_association/has_one_remote.rb
|
145
146
|
- lib/remote_association/version.rb
|
146
147
|
- remote_association.gemspec
|
147
148
|
- spec/config/database.example.yml
|
148
149
|
- spec/remote_association/belongs_to_remote_spec.rb
|
150
|
+
- spec/remote_association/has_many_remote_spec.rb
|
149
151
|
- spec/remote_association/has_one_remote_spec.rb
|
150
152
|
- spec/remote_association/remote_association_spec.rb
|
151
153
|
- spec/spec_helper.rb
|
@@ -164,7 +166,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
164
166
|
version: '0'
|
165
167
|
segments:
|
166
168
|
- 0
|
167
|
-
hash:
|
169
|
+
hash: 2376249119482484222
|
168
170
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
169
171
|
none: false
|
170
172
|
requirements:
|
@@ -173,7 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
173
175
|
version: '0'
|
174
176
|
segments:
|
175
177
|
- 0
|
176
|
-
hash:
|
178
|
+
hash: 2376249119482484222
|
177
179
|
requirements: []
|
178
180
|
rubyforge_project:
|
179
181
|
rubygems_version: 1.8.24
|
@@ -183,6 +185,7 @@ summary: Adds relations to ActiveResource models
|
|
183
185
|
test_files:
|
184
186
|
- spec/config/database.example.yml
|
185
187
|
- spec/remote_association/belongs_to_remote_spec.rb
|
188
|
+
- spec/remote_association/has_many_remote_spec.rb
|
186
189
|
- spec/remote_association/has_one_remote_spec.rb
|
187
190
|
- spec/remote_association/remote_association_spec.rb
|
188
191
|
- spec/spec_helper.rb
|