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 CHANGED
@@ -1,3 +1,4 @@
1
1
  *swp
2
2
  spec/config/database.yml
3
3
  pkg/*
4
+ .idea/
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- remote_association (0.0.2)
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.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.0)
31
+ i18n (0.6.1)
32
32
  multi_json (1.3.6)
33
- pg (0.14.0)
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.2)
40
+ rspec-expectations (2.11.3)
41
41
  diff-lcs (~> 1.1.3)
42
- rspec-mocks (2.11.2)
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
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
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 ```has_``` and ```belongs_``` associations to models inherited from ActiveResource::Base
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 = REMOTE_HOST
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
- has_one_remote :profile
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. Create your feature branch (`git checkout -b my-new-feature`)
49
- 3. Commit your changes (`git commit -am 'Added some feature'`)
50
- 4. Push to the branch (`git push origin my-new-feature`)
51
- 5. Create new Pull Request
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
- # Loads relations to ActiveModel models of models, selected by current relation.
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
- # Returns all the records matched by the options of the relation, same as <tt>all(*args)</tt>
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
- ar_accessor = r.to_sym
20
- foregin_key = settings[:foreign_key]
21
- ar_class = settings[:class_name ].constantize
24
+ ar_class = settings[:class_name].constantize
22
25
 
23
- fetch_and_join_for_has_one_remote(ar_accessor, foregin_key, ar_class) if settings[:association_type] == :has_one_remote
24
- fetch_and_join_for_belongs_to_remote(ar_accessor, foregin_key, ar_class) if settings[:association_type] == :belongs_to_remote
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
- set_remote_resources_prefetched
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.all
53
+ self
30
54
  end
31
55
 
32
- private
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
- def fetch_and_join_for_has_one_remote(ar_accessor, foregin_key, ar_class)
35
- keys = self.uniq.pluck(:id)
82
+ alias_method :exec_queries_without_remote_associations, :exec_queries
83
+ alias_method :exec_queries, :exec_queries_with_remote_associations
36
84
 
37
- remote_objects = ar_class.find(:all, :params => { foregin_key => keys })
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
- self.each do |u|
40
- u.send("#{ar_accessor}=", remote_objects.select {|s| s.send(foregin_key) == u.id })
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
- def fetch_and_join_for_belongs_to_remote(ar_accessor, foregin_key, ar_class)
45
- keys = self.uniq.pluck(foregin_key.to_sym)
99
+ set_remote_resources_loaded unless remote_associations.empty?
100
+ end
46
101
 
47
- remote_objects = ar_class.find(:all, :params => { :id => keys })
102
+ def fetch_and_join_for_has_any_remote(settings)
103
+ keys = @records.uniq.map(&:id)
48
104
 
49
- self.each do |u|
50
- u.send("#{ar_accessor}=", remote_objects.select {|s| u.send(foregin_key) == s.id })
51
- end
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
- def set_remote_resources_prefetched
55
- self.each do |u|
56
- u.instance_variable_set(:@remote_resources_prefetched, true)
57
- end
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
- # belongs_to :firm, :foreign_key => "client_of"
40
- # belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
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 remote_resources_prefetched?
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: { id: [self.#{rel_options[:foreign_key]}]})
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>Authort#profile</tt> (similar to <tt>Profile.find(:first, params: { author_id: [author.id]})</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 remote_resources_prefetched?
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: { #{rel_options[:foreign_key]}: [self.id]})
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
@@ -1,3 +1,3 @@
1
1
  module RemoteAssociation
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -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, *oprions)</tt>
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 remote_resources_prefetched?
59
- @remote_resources_prefetched
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: :login_id
71
+ has_one_remote :profile, foreign_key: 'search[login_id_in]'
60
72
  end
61
- FakeWeb.register_uri(:get, "#{REMOTE_HOST}/profiles.json?login_id%5B%5D=1", body: @body)
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
@@ -26,6 +26,7 @@ RSpec.configure do |config|
26
26
 
27
27
  config.after(:each) do
28
28
  DatabaseCleaner.clean
29
+ FakeWeb.clean_registry
29
30
  end
30
31
  end
31
32
 
@@ -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 NOT NULL,
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.2
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-08-24 00:00:00.000000000 Z
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: 3408694101896139864
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: 3408694101896139864
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