remote_association 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|