api_resource 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile.lock +27 -29
  3. data/README.md +1 -1
  4. data/api_resource.gemspec +2 -4
  5. data/lib/api_resource/associations.rb +25 -5
  6. data/lib/api_resource/associations/has_one_remote_object_proxy.rb +2 -1
  7. data/lib/api_resource/associations/multi_object_proxy.rb +13 -4
  8. data/lib/api_resource/associations/single_object_proxy.rb +9 -2
  9. data/lib/api_resource/base.rb +12 -12
  10. data/lib/api_resource/conditions.rb +2 -0
  11. data/lib/api_resource/conditions/abstract_condition.rb +24 -5
  12. data/lib/api_resource/conditions/association_condition.rb +2 -1
  13. data/lib/api_resource/conditions/multi_object_association_condition.rb +1 -1
  14. data/lib/api_resource/conditions/single_object_association_condition.rb +1 -1
  15. data/lib/api_resource/connection.rb +1 -2
  16. data/lib/api_resource/finders.rb +41 -49
  17. data/lib/api_resource/finders/abstract_finder.rb +26 -10
  18. data/lib/api_resource/finders/multi_object_association_finder.rb +16 -5
  19. data/lib/api_resource/finders/resource_finder.rb +31 -15
  20. data/lib/api_resource/finders/single_finder.rb +45 -0
  21. data/lib/api_resource/finders/single_object_association_finder.rb +14 -4
  22. data/lib/api_resource/version.rb +1 -1
  23. data/spec/lib/associations_spec.rb +2 -0
  24. data/spec/lib/base_spec.rb +3 -0
  25. data/spec/lib/conditions/abstract_conditions_spec.rb +2 -2
  26. data/spec/lib/finders/multi_object_association_finder_spec.rb +2 -2
  27. data/spec/lib/finders/resource_finder_spec.rb +29 -40
  28. data/spec/lib/finders/single_object_association_finder_spec.rb +2 -2
  29. data/spec/lib/finders_spec.rb +38 -0
  30. data/spec/lib/prefixes_spec.rb +5 -8
  31. metadata +10 -39
data/.gitignore CHANGED
@@ -21,6 +21,10 @@ pkg
21
21
  .swp
22
22
  .swo
23
23
 
24
+
25
+ *.gem
26
+ pkg/
27
+
24
28
  # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
25
29
  #
26
30
  # * Create a file at ~/.gitignore
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- api_resource (0.5.1)
5
- activemodel
6
- activesupport
4
+ api_resource (0.6.0)
5
+ colorize
6
+ differ
7
7
  json
8
8
  log4r
9
9
  rails
@@ -12,12 +12,12 @@ PATH
12
12
  GEM
13
13
  remote: https://rubygems.org/
14
14
  specs:
15
- actionmailer (3.2.9)
16
- actionpack (= 3.2.9)
15
+ actionmailer (3.2.10)
16
+ actionpack (= 3.2.10)
17
17
  mail (~> 2.4.4)
18
- actionpack (3.2.9)
19
- activemodel (= 3.2.9)
20
- activesupport (= 3.2.9)
18
+ actionpack (3.2.10)
19
+ activemodel (= 3.2.10)
20
+ activesupport (= 3.2.10)
21
21
  builder (~> 3.0.0)
22
22
  erubis (~> 2.7.0)
23
23
  journey (~> 1.0.4)
@@ -25,18 +25,18 @@ GEM
25
25
  rack-cache (~> 1.2)
26
26
  rack-test (~> 0.6.1)
27
27
  sprockets (~> 2.2.1)
28
- activemodel (3.2.9)
29
- activesupport (= 3.2.9)
28
+ activemodel (3.2.10)
29
+ activesupport (= 3.2.10)
30
30
  builder (~> 3.0.0)
31
- activerecord (3.2.9)
32
- activemodel (= 3.2.9)
33
- activesupport (= 3.2.9)
31
+ activerecord (3.2.10)
32
+ activemodel (= 3.2.10)
33
+ activesupport (= 3.2.10)
34
34
  arel (~> 3.0.2)
35
35
  tzinfo (~> 0.3.29)
36
- activeresource (3.2.9)
37
- activemodel (= 3.2.9)
38
- activesupport (= 3.2.9)
39
- activesupport (3.2.9)
36
+ activeresource (3.2.10)
37
+ activemodel (= 3.2.10)
38
+ activesupport (= 3.2.10)
39
+ activesupport (3.2.10)
40
40
  i18n (~> 0.6)
41
41
  multi_json (~> 1.0)
42
42
  arel (3.0.2)
@@ -106,17 +106,17 @@ GEM
106
106
  rack
107
107
  rack-test (0.6.2)
108
108
  rack (>= 1.0)
109
- rails (3.2.9)
110
- actionmailer (= 3.2.9)
111
- actionpack (= 3.2.9)
112
- activerecord (= 3.2.9)
113
- activeresource (= 3.2.9)
114
- activesupport (= 3.2.9)
109
+ rails (3.2.10)
110
+ actionmailer (= 3.2.10)
111
+ actionpack (= 3.2.10)
112
+ activerecord (= 3.2.10)
113
+ activeresource (= 3.2.10)
114
+ activesupport (= 3.2.10)
115
115
  bundler (~> 1.0)
116
- railties (= 3.2.9)
117
- railties (3.2.9)
118
- actionpack (= 3.2.9)
119
- activesupport (= 3.2.9)
116
+ railties (= 3.2.10)
117
+ railties (3.2.10)
118
+ actionpack (= 3.2.10)
119
+ activesupport (= 3.2.10)
120
120
  rack-ssl (~> 1.3.2)
121
121
  rake (>= 0.8.7)
122
122
  rdoc (~> 3.4)
@@ -162,8 +162,6 @@ PLATFORMS
162
162
 
163
163
  DEPENDENCIES
164
164
  api_resource!
165
- colorize
166
- differ
167
165
  faker
168
166
  flay
169
167
  flog
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ApiResource
2
2
 
3
- TODO: Write a gem description
3
+ [![Build Status](https://travis-ci.org/LifebookerInc/api_resource.png)](https://travis-ci.org/LifebookerInc/api_resource)
4
4
 
5
5
  ## Installation
6
6
 
data/api_resource.gemspec CHANGED
@@ -33,14 +33,12 @@ Gem::Specification.new do |gem|
33
33
  gem.add_development_dependency "rb-fsevent"
34
34
  gem.add_development_dependency "simplecov"
35
35
  # stuff that seems like crap
36
- gem.add_development_dependency "differ"
37
- gem.add_development_dependency "colorize"
38
36
  gem.add_development_dependency "sqlite3"
39
37
 
40
38
  gem.add_dependency "rails"
41
- gem.add_dependency "activemodel"
42
- gem.add_dependency "activesupport"
43
39
  gem.add_dependency "json"
44
40
  gem.add_dependency "rest-client"
45
41
  gem.add_dependency "log4r"
42
+ gem.add_dependency "differ"
43
+ gem.add_dependency "colorize"
46
44
  end
@@ -183,10 +183,19 @@ module ApiResource
183
183
  if self.ancestors.include?(ApiResource::Base)
184
184
  define_attribute_method(assoc_name)
185
185
  end
186
+
187
+ id_method_name = assoc_name.to_s.singularize + "_id"
188
+
189
+ if assoc_type.to_s == "has_many"
190
+ id_method_name += "s"
191
+ end
186
192
 
187
193
  # TODO: Come up with a better implementation for the foreign key thing
188
194
  # implement the rest of the active record methods, refactor this into something
189
- # a littel bit more sensible
195
+ # a little bit more sensible
196
+
197
+ # TODO: This should support saving the ids when they are modified and saving anything
198
+ # that is not created, associations need to be their own object
190
199
  self.class_eval <<-EOE, __FILE__, __LINE__ + 1
191
200
  def #{assoc_name}
192
201
  @attributes_cache[:#{assoc_name}] ||= begin
@@ -200,17 +209,28 @@ module ApiResource
200
209
  instance
201
210
  end
202
211
  end
203
- def #{assoc_name}=(val)
204
- # get old internal object
205
- unless self.#{assoc_name}.internal_object == val
206
- #{assoc_name}_will_change!
212
+ def #{assoc_name}=(val, force = true)
213
+ if !force
214
+ #{assoc_name}_will_change!
215
+ elsif self.#{assoc_name}.internal_object != val
216
+ #{assoc_name}_will_change!
207
217
  end
218
+ # This should not force a load
208
219
  self.#{assoc_name}.internal_object = val
209
220
  end
210
221
  def #{assoc_name}?
211
222
  self.#{assoc_name}.internal_object.present?
212
223
  end
213
224
 
225
+ def #{id_method_name}
226
+ @attributes_cache[:#{id_method_name}] ||=
227
+ @attributes[:#{id_method_name}] || self.#{assoc_name}.collect(&:id)
228
+ end
229
+
230
+ def #{id_method_name}=(val)
231
+ @attributes_cache[:#{id_method_name}] = val
232
+ end
233
+
214
234
  EOE
215
235
  end
216
236
 
@@ -17,8 +17,9 @@ module ApiResource
17
17
  end
18
18
 
19
19
  def load(opts = {})
20
+ res = Array.wrap(self.to_condition.load).first
20
21
  @loaded = true
21
- Array.wrap(self.to_condition.find).first
22
+ res
22
23
  end
23
24
 
24
25
  end
@@ -32,13 +32,19 @@ module ApiResource
32
32
 
33
33
  def internal_object=(contents)
34
34
  # if we were passed in a service uri, stop here
35
- return true if self.set_remote_path(contents)
35
+ # but if we have a service uri already then don't overwrite
36
+ unless self.remote_path.present?
37
+ return true if self.set_remote_path(contents)
38
+ end
36
39
 
37
40
  if contents.try(:first).is_a?(self.klass)
38
- return @internal_object = contents
41
+ @loaded = true
42
+ return @internal_object = contents
39
43
  elsif contents.instance_of?(self.class)
44
+ @loaded = true
40
45
  return @internal_object = contents.internal_object
41
46
  elsif contents.is_a?(Array)
47
+ @loaded = true
42
48
  return @internal_object = self.klass.instantiate_collection(
43
49
  contents
44
50
  )
@@ -70,12 +76,15 @@ module ApiResource
70
76
  protected
71
77
 
72
78
  def to_condition
73
- ApiResource::Conditions::MultiObjectAssociationCondition.new(self.klass, self.remote_path)
79
+ obj = nil
80
+ obj = self.internal_object if self.loaded?
81
+ ApiResource::Conditions::MultiObjectAssociationCondition.new(self.klass, self.remote_path, obj)
74
82
  end
75
83
 
76
84
  def load(opts = {})
85
+ res = self.to_condition.load
77
86
  @loaded = true
78
- self.to_condition.find
87
+ res
79
88
  end
80
89
 
81
90
  def set_remote_path(opts)
@@ -22,8 +22,10 @@ module ApiResource
22
22
 
23
23
  def internal_object=(contents)
24
24
  if contents.is_a?(self.klass) || contents.nil?
25
+ @loaded = true
25
26
  return @internal_object = contents
26
27
  elsif contents.is_a?(self.class)
28
+ @loaded = true
27
29
  return @internal_object = contents.internal_object
28
30
  # a Hash may be attributes and/or a service_uri
29
31
  elsif contents.is_a?(Hash)
@@ -32,6 +34,7 @@ module ApiResource
32
34
  self.class.remote_path_element.to_sym
33
35
  )
34
36
  if contents.present?
37
+ @loaded = true
35
38
  return @internal_object = self.klass.instantiate_record(contents)
36
39
  end
37
40
  else
@@ -58,13 +61,17 @@ module ApiResource
58
61
  protected
59
62
 
60
63
  def to_condition
61
- ApiResource::Conditions::SingleObjectAssociationCondition.new(self.klass, self.remote_path)
64
+ obj = nil
65
+ obj = self.internal_object if self.loaded?
66
+ ApiResource::Conditions::SingleObjectAssociationCondition.new(self.klass, self.remote_path, obj)
62
67
  end
63
68
 
64
69
  # Should make a proper conditions object and call find on it
70
+ # It MUST set loaded to true after calling load
65
71
  def load(opts = {})
72
+ res = self.to_condition.load
66
73
  @loaded = true
67
- @internal_object = self.to_condition.find
74
+ res
68
75
  end
69
76
 
70
77
 
@@ -341,6 +341,18 @@ module ApiResource
341
341
  connection.delete(element_path(id, options))
342
342
  end
343
343
 
344
+ # split an option hash into two hashes, one containing the prefix options,
345
+ # and the other containing the leftovers.
346
+ def split_options(options = {})
347
+ prefix_options, query_options = {}, {}
348
+ (options || {}).each do |key, value|
349
+ next if key.blank?
350
+ (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
351
+ end
352
+
353
+ [ prefix_options, query_options ]
354
+ end
355
+
344
356
  protected
345
357
  def method_missing(meth, *args, &block)
346
358
  # make one attempt to load remote attrs
@@ -372,18 +384,6 @@ module ApiResource
372
384
  "?#{options.to_query}" unless options.nil? || options.empty?
373
385
  end
374
386
 
375
- # split an option hash into two hashes, one containing the prefix options,
376
- # and the other containing the leftovers.
377
- def split_options(options = {})
378
- prefix_options, query_options = {}, {}
379
- (options || {}).each do |key, value|
380
- next if key.blank?
381
- (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
382
- end
383
-
384
- [ prefix_options, query_options ]
385
- end
386
-
387
387
  def uri_parser
388
388
  @uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
389
389
  end
@@ -16,6 +16,8 @@ module ApiResource
16
16
 
17
17
  def includes(*args)
18
18
 
19
+ self.load_resource_definition
20
+
19
21
  # everything in args must be an association
20
22
  args.each do |arg|
21
23
  unless self.association?(arg)
@@ -21,6 +21,8 @@ module ApiResource
21
21
  @klass = klass
22
22
 
23
23
  @conditions = args.with_indifferent_access
24
+
25
+ @klass.load_resource_definition
24
26
  end
25
27
 
26
28
  def each(&block)
@@ -48,14 +50,31 @@ module ApiResource
48
50
  end
49
51
 
50
52
  def internal_object
51
- @internal_object ||= begin
52
- @loaded = true
53
- self.instantiate_finder.find
53
+ return @internal_object if @loaded
54
+ @internal_object = self.instantiate_finder.load
55
+ @loaded = true
56
+ @internal_object
57
+ end
58
+
59
+ def all(*args)
60
+ if args.blank?
61
+ self.internal_object
62
+ else
63
+ self.find(*([:all] + args))
54
64
  end
55
65
  end
56
66
 
57
- alias_method :find, :internal_object
58
- alias_method :all, :internal_object
67
+ # implement find that accepts an optional
68
+ # condition object
69
+ def find(*args)
70
+ self.klass.find(*(args + [self]))
71
+ end
72
+
73
+ # TODO: review the hierarchy that makes this necessary
74
+ # consider changing it to alias method
75
+ def load
76
+ self.internal_object
77
+ end
59
78
 
60
79
  def loaded?
61
80
  @loaded == true
@@ -4,11 +4,12 @@ module ApiResource
4
4
 
5
5
  class AssociationCondition < AbstractCondition
6
6
 
7
- def initialize(klass, service_uri)
7
+ def initialize(klass, service_uri, internal_object= nil)
8
8
  super({}, klass)
9
9
 
10
10
  @assocaiton = true
11
11
  @remote_path = service_uri
12
+ @internal_object = internal_object
12
13
  end
13
14
 
14
15
  end
@@ -7,7 +7,7 @@ module ApiResource
7
7
  protected
8
8
 
9
9
  def instantiate_finder
10
- ApiResource::Finders::MultiObjectAssociationFinder.new(self.klass, self)
10
+ ApiResource::Finders::MultiObjectAssociationFinder.new(self.klass, self, @internal_object)
11
11
  end
12
12
 
13
13
  end
@@ -9,7 +9,7 @@ module ApiResource
9
9
  protected
10
10
 
11
11
  def instantiate_finder
12
- ApiResource::Finders::SingleObjectAssociationFinder.new(self.klass, self)
12
+ ApiResource::Finders::SingleObjectAssociationFinder.new(self.klass, self, @internal_object)
13
13
  end
14
14
 
15
15
  end
@@ -119,8 +119,7 @@ module ApiResource
119
119
  ActiveSupport::Notifications.instrument("request.api_resource") do |payload|
120
120
 
121
121
  # debug logging
122
- ApiResource.logger.debug("#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}")
123
-
122
+ ApiResource.logger.info("#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}")
124
123
  payload[:method] = method
125
124
  payload[:request_uri] = "#{site.scheme}://#{site.host}:#{site.port}#{path}"
126
125
  payload[:result] = http(path).send(method, *arguments)
@@ -7,6 +7,7 @@ module ApiResource
7
7
 
8
8
  autoload :AbstractFinder
9
9
  autoload :ResourceFinder
10
+ autoload :SingleFinder
10
11
  autoload :SingleObjectAssociationFinder
11
12
  autoload :MultiObjectAssociationFinder
12
13
 
@@ -16,22 +17,54 @@ module ApiResource
16
17
  # It accepts arguments of the form "scope", "options={}"
17
18
  # where options can be standard rails options or :expires_in.
18
19
  # If :expires_in is set, it caches it for expires_in seconds.
19
- def find(*arguments)
20
20
 
21
+ # Need to support the following cases
22
+ # => 1) Klass.find(1)
23
+ # => 2) Klass.find(:all, :params => {a => b})
24
+ # => 3) Klass.find(:first, :params => {a => b})
25
+ # => 4) Klass.includes(:assoc).find(1)
26
+ # => 5) Klass.active.find(1)
27
+ # => 6) Klass.includes(:assoc).find(:all, a => b)
28
+ def find(*arguments)
21
29
  # make sure we have class data before loading
22
30
  self.load_resource_definition
23
31
 
24
32
  scope = arguments.slice!(0)
25
33
  options = arguments.slice!(0) || {}
26
-
27
- expiry = options.delete(:expires_in) || ApiResource::Base.ttl || 0
34
+ cond = arguments.slice!(0)
35
+
36
+ # TODO: Make this into a class attribute properly (if it isn't already)
37
+ # this is a little bit of a hack because options can sometimes be a Condition
38
+ expiry = (options.is_a?(Hash) ? options.delete(:expires_in) : nil) || ApiResource::Base.ttl || 0
28
39
  ApiResource.with_ttl(expiry.to_f) do
29
40
  case scope
30
- when :all then find_every(options)
31
- when :first then find_every(options).first
32
- when :last then find_every(options).last
33
- when :one then find_one(options)
34
- else find_single(scope, options)
41
+ when :all, :first, :last
42
+ final_cond = ApiResource::Conditions::ScopeCondition.new({}, self)
43
+ # we need new conditions here to take into account options, which could
44
+ # either be a Condition object or a hash
45
+ if options.is_a?(Hash)
46
+ opts = options.with_indifferent_access.delete(:params) || options || {}
47
+ final_cond = ApiResource::Conditions::ScopeCondition.new(opts, self)
48
+ # cond may be nil
49
+ unless cond == nil # THIS MUST BE == NOT nil?
50
+ final_cond = cond.merge!(final_cond)
51
+ end
52
+ elsif options.is_a?(ApiResource::Conditions::AbstractCondition)
53
+ final_cond = options
54
+ end
55
+ # now final cond contains all the conditions we should need to pass to the finder
56
+ fnd = ApiResource::Finders::ResourceFinder.new(self, final_cond)
57
+ fnd.send(scope)
58
+ else
59
+ # in this case scope is the id we want to find, and options should be a condition object or nil
60
+ final_cond = ApiResource::Conditions::ScopeCondition.new({:id => scope}, self)
61
+ if options.is_a?(ApiResource::Conditions::AbstractCondition)
62
+ final_cond = options.merge!(final_cond)
63
+ elsif options.is_a?(Hash)
64
+ opts = options.with_indifferent_access.delete(:params) || options || {}
65
+ final_cond = ApiResource::Conditions::ScopeCondition.new(opts, self).merge!(final_cond)
66
+ end
67
+ ApiResource::Finders::SingleFinder.new(self, final_cond).load
35
68
  end
36
69
  end
37
70
  end
@@ -75,47 +108,6 @@ module ApiResource
75
108
  ret
76
109
  end
77
110
 
78
- private
79
-
80
- # Find every resource
81
- def find_every(options)
82
- begin
83
- case from = options[:from]
84
- when Symbol
85
- instantiate_collection(get(from, options[:params]))
86
- when String
87
- path = "#{from}#{query_string(options[:params])}"
88
- instantiate_collection(connection.get(path, headers) || [])
89
- else
90
- prefix_options, query_options = split_options(options[:params])
91
- path = collection_path(prefix_options, query_options)
92
- instantiate_collection( (connection.get(path, headers) || []))
93
- end
94
- rescue ApiResource::ResourceNotFound
95
- # Swallowing ResourceNotFound exceptions and return nil - as per
96
- # ActiveRecord.
97
- nil
98
- end
99
- end
100
-
101
- # Find a single resource from a one-off URL
102
- def find_one(options)
103
- case from = options[:from]
104
- when Symbol
105
- instantiate_record(get(from, options[:params]))
106
- when String
107
- path = "#{from}#{query_string(options[:params])}"
108
- instantiate_record(connection.get(path, headers))
109
- end
110
- end
111
-
112
- # Find a single resource from the default URL
113
- def find_single(scope, options)
114
- prefix_options, query_options = split_options(options[:params])
115
- path = element_path(scope, prefix_options, query_options)
116
- instantiate_record(connection.get(path, headers))
117
- end
118
-
119
111
  end
120
112
  end
121
113
 
@@ -9,16 +9,17 @@ module ApiResource
9
9
  attr_reader :found, :internal_object
10
10
 
11
11
  # TODO: Make this list longer since there are for sure more methods to delegate
12
- delegate :to_s, :inspect, :reload, :present?, :blank?, :size, :to => :internal_object
12
+ delegate :to_s, :inspect, :reload, :present?, :blank?, :size, :count, :to => :internal_object
13
13
 
14
14
  def initialize(klass, condition)
15
15
  @klass = klass
16
16
  @condition = condition
17
17
  @found = false
18
- @internal_object = nil
18
+
19
+ @klass.load_resource_definition
19
20
  end
20
21
 
21
- def find
22
+ def load
22
23
  raise NotImplementedError("Must be defined in a subclass")
23
24
  end
24
25
 
@@ -27,8 +28,16 @@ module ApiResource
27
28
  if instance_variable_defined?(:@internal_object)
28
29
  return instance_variable_get(:@internal_object)
29
30
  end
30
- # If we haven't tried to load then just call find
31
- self.find
31
+ # If we haven't tried to load then just call load
32
+ self.load
33
+ end
34
+
35
+ def all(*args)
36
+ if args.blank?
37
+ self.internal_object
38
+ else
39
+ self.klass.send(:all, *args)
40
+ end
32
41
  end
33
42
 
34
43
  # proxy unknown methods to the internal_object
@@ -51,9 +60,10 @@ module ApiResource
51
60
  id_hash = HashWithIndifferentAccess.new(id_hash)
52
61
  # load each individually
53
62
  self.condition.included_objects.inject(hsh) do |accum, assoc|
54
- accum[assoc.to_sym] = self.klass.association_class(assoc).find(
55
- :all,
56
- :id => id_hash[assoc])
63
+ accum[assoc.to_sym] = self.klass.association_class(assoc).all(
64
+ :params => {:ids => id_hash[assoc]}
65
+ )
66
+ accum
57
67
  end
58
68
 
59
69
  hsh
@@ -62,9 +72,15 @@ module ApiResource
62
72
  def apply_includes(objects, includes)
63
73
  Array.wrap(objects).each do |obj|
64
74
  includes.each_pair do |assoc, vals|
65
- ids_to_keep = obj.send(obj.class.association_foreign_key_field(assoc))
75
+ ids_to_keep = Array.wrap(obj.send(obj.class.association_foreign_key_field(assoc)))
66
76
  to_keep = vals.select{|elm| ids_to_keep.include?(elm.id)}
67
- obj.send("#{assoc}=", to_keep)
77
+ # if this is a single association take the first
78
+ # TODO: subclass instead of this
79
+ if self.klass.has_many?(assoc)
80
+ obj.send("#{assoc}=", to_keep, false)
81
+ else
82
+ obj.send("#{assoc}=", to_keep.first, false)
83
+ end
68
84
  end
69
85
  end
70
86
  end
@@ -4,17 +4,28 @@ module ApiResource
4
4
 
5
5
  class MultiObjectAssociationFinder < AbstractFinder
6
6
 
7
- def find
7
+ # If they pass in the internal object just skip the first
8
+ # step and apply the includes
9
+ def initialize(klass, condition, internal_object = nil)
10
+ super(klass, condition)
11
+
12
+ @internal_object = internal_object
13
+ end
14
+
15
+ def load
8
16
  # otherwise just instantiate the record
9
17
  unless self.condition.remote_path
10
18
  raise "Tried to load association without a remote path"
11
19
  end
12
20
 
13
- data = self.klass.connection.get(self.build_load_path)
14
- @loaded = true
15
- return [] if data.blank?
21
+ unless @internal_object
22
+ data = self.klass.connection.get(self.build_load_path)
23
+ return [] if data.blank?
16
24
 
17
- @internal_object = self.klass.instantiate_collection(data)
25
+ @internal_object = self.klass.instantiate_collection(data)
26
+ end
27
+
28
+ @loaded = true
18
29
 
19
30
  id_hash = self.condition.included_objects.inject({}) do |accum, assoc|
20
31
  accum[assoc] = @internal_object.collect do |obj|
@@ -6,26 +6,42 @@ module ApiResource
6
6
 
7
7
  # this is a little bit simpler, it's always a collection and does
8
8
  # not require a remote path
9
- def find
10
- @loaded = true
11
- @internal_object = self.klass.find(:all, self.condition.to_hash)
12
- return [] if @internal_object.blank?
13
-
14
- id_hash = self.condition.included_objects.inject({}) do |accum, assoc|
15
- accum[assoc] = @internal_object.collect do |obj|
16
- obj.send(self.klass.association_foreign_key_field(assoc))
9
+ def load
10
+ begin
11
+ @loaded = true
12
+ @internal_object = self.klass.connection.get(self.build_load_path)
13
+ return [] if @internal_object.blank?
14
+
15
+ @internal_object = self.klass.instantiate_collection(@internal_object)
16
+
17
+ id_hash = self.condition.included_objects.inject({}) do |accum, assoc|
18
+ accum[assoc] = @internal_object.collect do |obj|
19
+ obj.send(self.klass.association_foreign_key_field(assoc))
20
+ end
21
+ accum[assoc].flatten!
22
+ accum[assoc].uniq!
23
+ accum
17
24
  end
18
- accum[assoc].flatten!
19
- accum[assoc].uniq!
20
- accum
21
- end
22
- included_objects = self.load_includes(id_hash)
25
+ included_objects = self.load_includes(id_hash)
23
26
 
24
- self.apply_includes(@internal_object, included_objects)
27
+ self.apply_includes(@internal_object, included_objects)
25
28
 
26
- return @internal_object
29
+ return @internal_object
30
+ rescue ApiResource::ResourceNotFound
31
+ nil
32
+ end
33
+ @internal_object
27
34
  end
28
35
 
36
+ protected
37
+
38
+
39
+ # Find every resource
40
+ def build_load_path
41
+ prefix_opts, query_opts = self.klass.split_options(self.condition.to_hash)
42
+ self.klass.collection_path(prefix_opts, query_opts)
43
+ end
44
+
29
45
  end
30
46
 
31
47
  end
@@ -0,0 +1,45 @@
1
+ module ApiResource
2
+
3
+ module Finders
4
+
5
+ class SingleFinder < AbstractFinder
6
+
7
+ def load
8
+ data = self.klass.connection.get(self.build_load_path)
9
+ @loaded = true
10
+ return nil if data.blank?
11
+ @internal_object = self.klass.instantiate_record(data)
12
+ # now that the object is loaded, resolve the includes
13
+ id_hash = self.condition.included_objects.inject({}) do |accum, assoc|
14
+ accum[assoc] = Array.wrap(
15
+ @internal_object.send(
16
+ @internal_object.class.association_foreign_key_field(assoc)
17
+ )
18
+ )
19
+ accum
20
+ end
21
+
22
+ included_objects = self.load_includes(id_hash)
23
+
24
+ self.apply_includes(@internal_object, included_objects)
25
+
26
+ return @internal_object
27
+ end
28
+
29
+ protected
30
+
31
+ def build_load_path
32
+ if self.condition.to_hash["id"].nil?
33
+ raise "Invalid evaluation of a SingleFinder without an ID"
34
+ end
35
+ args = self.condition.to_hash
36
+ id = args.delete("id")
37
+ prefix_opts, query_opts = self.klass.split_options(args)
38
+ self.klass.element_path(id, prefix_opts, query_opts)
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -4,18 +4,28 @@ module ApiResource
4
4
 
5
5
  class SingleObjectAssociationFinder < AbstractFinder
6
6
 
7
+ def initialize(klass, condition, internal_object = nil)
8
+ super(klass, condition)
9
+
10
+ @internal_object = internal_object
11
+ end
12
+
7
13
  # since it is only a single object we can just load from
8
14
  # the service_uri and deal with includes
9
- def find
15
+ def load
10
16
  # otherwise just instantiate the record
11
17
  unless self.condition.remote_path
12
18
  raise "Tried to load association without a remote path"
13
19
  end
14
20
 
15
- data = self.klass.connection.get(self.build_load_path)
21
+ unless @internal_object
22
+ data = self.klass.connection.get(self.build_load_path)
23
+
24
+ return nil if data.blank?
25
+ @internal_object = self.klass.instantiate_record(data)
26
+ end
27
+
16
28
  @loaded = true
17
- return nil if data.blank?
18
- @internal_object = self.klass.instantiate_record(data)
19
29
  # now that the object is loaded, resolve the includes
20
30
  id_hash = self.condition.included_objects.inject({}) do |accum, assoc|
21
31
  accum[assoc] = Array.wrap(
@@ -1,3 +1,3 @@
1
1
  module ApiResource
2
- VERSION = "0.6.0"
2
+ VERSION = "0.6.1"
3
3
  end
@@ -729,6 +729,7 @@ describe "Associations" do
729
729
  TestAR.class_eval do
730
730
  belongs_to_remote :my_favorite_thing, :class_name => "TestClassYay"
731
731
  end
732
+ HasManyObject.reload_resource_definition
732
733
  end
733
734
  it "should define remote association types for AR" do
734
735
  [:has_many_remote, :belongs_to_remote, :has_one_remote].each do |assoc|
@@ -788,6 +789,7 @@ describe "Associations" do
788
789
  it "should attempt to load a collection of remote objects for a has_many relationship" do
789
790
  tar = TestAR.new
790
791
  tar.stubs(:id).returns(1)
792
+ HasManyObject.load_resource_definition
791
793
  HasManyObject.connection.expects(:get).with("/has_many_objects.json?test_ar_id=1").once.returns([{"name" => "testing"}])
792
794
  # load the test resource
793
795
  tar.has_many_objects.first.name.should eql "testing"
@@ -7,6 +7,8 @@ describe "Base" do
7
7
 
8
8
  before(:each) do
9
9
  TestResource.reload_resource_definition
10
+ HasOneObject.reload_resource_definition
11
+ HasManyObject.reload_resource_definition
10
12
  end
11
13
 
12
14
  context ".new_element_path" do
@@ -795,6 +797,7 @@ describe "Base" do
795
797
 
796
798
  before(:all) do
797
799
  HasOneObject.reload_resource_definition
800
+ HasManyObject.load_resource_definition
798
801
  end
799
802
 
800
803
  it "should know if it is persisted" do
@@ -61,7 +61,7 @@ describe "Conditions" do
61
61
  it "should create a resource finder when forced to load, and cache the result" do
62
62
  obj = TestResource.includes(:has_many_objects)
63
63
 
64
- ApiResource::Finders::ResourceFinder.expects(:new).with(TestResource, obj).returns(mock(:find => [1]))
64
+ ApiResource::Finders::ResourceFinder.expects(:new).with(TestResource, obj).returns(mock(:load => [1]))
65
65
  obj.internal_object.should eql([1])
66
66
  obj.all.should eql([1])
67
67
  obj.first.should eql(1)
@@ -70,7 +70,7 @@ describe "Conditions" do
70
70
  it "should proxy calls to enumerable and array methods to the loaded object" do
71
71
  obj = TestResource.includes(:has_many_objects)
72
72
 
73
- ApiResource::Finders::ResourceFinder.expects(:new).with(TestResource, obj).returns(mock(:find => [1,2]))
73
+ ApiResource::Finders::ResourceFinder.expects(:new).with(TestResource, obj).returns(mock(:load => [1,2]))
74
74
 
75
75
  obj.collect{|o| o * 2}.should eql([2,4])
76
76
  end
@@ -12,7 +12,7 @@ describe "MultiObjectAssociationFinder" do
12
12
  ApiResource::Finders::MultiObjectAssociationFinder.new(
13
13
  TestResource,
14
14
  stub(:remote_path => "test_resources", :to_query => "id[]=1&id[]=2", :blank_conditions? => false)
15
- ).find
15
+ ).load
16
16
  end
17
17
 
18
18
  it "should load a has many association properly" do
@@ -37,7 +37,7 @@ describe "MultiObjectAssociationFinder" do
37
37
  finder.expects(:load_includes).with(:has_many_objects => [1,2]).returns(5)
38
38
  finder.expects(:apply_includes).with([tr], 5).returns(6)
39
39
 
40
- finder.find.should eql([tr])
40
+ finder.load.should eql([tr])
41
41
  end
42
42
 
43
43
  end
@@ -4,83 +4,72 @@ describe "ResourceFinder" do
4
4
 
5
5
  before(:each) do
6
6
  TestResource.reload_resource_definition
7
+
7
8
  end
8
9
 
9
- it "should call find on the class normally without includes" do
10
- TestResource.expects(:find).with(:all, {})
10
+ it "should load normally without includes using connection.get" do
11
+ TestResource.connection.expects(:get).with("/test_resources.json")
11
12
 
12
13
  ApiResource::Finders::ResourceFinder.new(
13
14
  TestResource,
14
15
  mock(:to_hash => {})
15
- ).find
16
+ ).load
16
17
  end
17
18
 
18
- it "should pass conditions into the finder method" do
19
- TestResource.expects(:find).with(:all, {:id => [1,2,3]})
19
+ it "should pass conditions into the connection get method" do
20
+ TestResource.connection.expects(:get).with("/test_resources.json?ids%5B%5D=1&ids%5B%5D=2&ids%5B%5D=3")
20
21
 
21
22
  ApiResource::Finders::ResourceFinder.new(
22
23
  TestResource,
23
- mock(:to_hash => {:id => [1,2,3]})
24
- ).find
24
+ mock(:to_hash => {:ids => [1,2,3]})
25
+ ).load
25
26
  end
26
27
 
27
28
  it "should try to load includes if it finds an object" do
28
- obj_mock = mock(:id => 1)
29
+ obj_mock = {:id => 1}
29
30
 
30
- TestResource.expects(:find).with(:all, {}).returns([obj_mock])
31
+ TestResource.connection.expects(:get).with("/test_resources.json").returns([obj_mock])
31
32
 
32
33
  obj = ApiResource::Finders::ResourceFinder.new(
33
34
  TestResource,
34
35
  stub(:to_hash => {}, :eager_load? => false, :included_objects => [])
35
- ).find
36
+ ).load
36
37
 
37
- obj.first.id.should eql(1)
38
+ # obj.first.id.should eql(1)
38
39
  end
39
40
 
40
41
  it "should load and distribute includes among the returned objects" do
41
- inc_mock = stub(:id => 1)
42
- inc_mock2 = stub(:id => 2)
43
-
44
- tr = TestResource.new
45
- tr.stubs(:id).returns(1)
46
- tr.stubs(:has_many_object_ids).returns([1,2])
47
- tr.expects(:has_many_objects=).with([inc_mock, inc_mock2])
48
- tr.expects(:has_many_objects).returns([inc_mock, inc_mock2])
42
+ inc_mock = {:id => 1}
43
+ inc_mock2 = {:id => 2}
49
44
 
50
- TestResource.expects(:find).with(:all, {}).returns([tr])
45
+ TestResource.connection.expects(:get)
46
+ .with("/test_resources.json")
47
+ .returns([{:id => 1, :has_many_object_ids => [1,2]}])
51
48
 
52
- HasManyObject.expects(:find).with(:all, :id => [1,2]).returns([inc_mock, inc_mock2])
49
+ HasManyObject.connection.expects(:get)
50
+ .with("/has_many_objects.json?ids%5B%5D=1&ids%5B%5D=2")
51
+ .returns([inc_mock, inc_mock2])
53
52
 
54
53
  obj = ApiResource::Finders::ResourceFinder.new(
55
54
  TestResource,
56
55
  stub(:to_hash => {}, :eager_load? => true, :included_objects => [:has_many_objects])
57
- ).find
56
+ ).load
58
57
  obj.first.has_many_objects.collect(&:id).should eql([1,2])
59
58
  end
60
59
 
61
60
  it "should work with loading for multiple objects" do
62
- inc_mock = stub(:id => 1)
63
- inc_mock2 = stub(:id => 2)
64
-
65
- tr = TestResource.new
66
- tr.stubs(:id).returns(1)
67
- tr.stubs(:has_many_object_ids).returns([1])
68
- tr.expects(:has_many_objects=).with([inc_mock]).returns([inc_mock])
69
- tr.expects(:has_many_objects).returns([inc_mock])
70
-
71
- tr2 = TestResource.new
72
- tr2.stubs(:id).returns(2)
73
- tr2.stubs(:has_many_object_ids).returns([2])
74
- tr2.expects(:has_many_objects=).with([inc_mock2]).returns([inc_mock2])
75
- tr2.expects(:has_many_objects).returns([inc_mock2])
76
-
77
- TestResource.expects(:find).with(:all, {}).returns([tr, tr2])
78
- HasManyObject.expects(:find).with(:all, :id => [1,2]).returns([inc_mock2, inc_mock])
61
+ TestResource.connection.expects(:get).with("/test_resources.json").returns([
62
+ {:id => 1, :has_many_object_ids => [1]},
63
+ {:id => 2, :has_many_object_ids => [2]}
64
+ ])
65
+ HasManyObject.connection.expects(:get)
66
+ .with("/has_many_objects.json?ids%5B%5D=1&ids%5B%5D=2")
67
+ .returns([{:id => 1}, {:id => 2}])
79
68
 
80
69
  obj = ApiResource::Finders::ResourceFinder.new(
81
70
  TestResource,
82
71
  stub(:to_hash => {}, :eager_load? => true, :included_objects => [:has_many_objects])
83
- ).find
72
+ ).load
84
73
 
85
74
  obj.first.has_many_objects.collect(&:id).should eql([1])
86
75
  obj.second.has_many_objects.collect(&:id).should eql([2])
@@ -12,7 +12,7 @@ describe "SingleObjectAssociationFinder" do
12
12
  ApiResource::Finders::SingleObjectAssociationFinder.new(
13
13
  TestResource,
14
14
  stub(:remote_path => "test_resources", :to_query => "id[]=1&id[]=2", :blank_conditions? => false)
15
- ).find
15
+ ).load
16
16
  end
17
17
 
18
18
  it "should load a has many association properly" do
@@ -37,7 +37,7 @@ describe "SingleObjectAssociationFinder" do
37
37
  finder.expects(:load_includes).with(:has_many_objects => [1,2]).returns(5)
38
38
  finder.expects(:apply_includes).with(tr, 5).returns(6)
39
39
 
40
- finder.find.should eql(tr)
40
+ finder.load.should eql(tr)
41
41
  end
42
42
 
43
43
  end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe ApiResource::Finders do
4
+
5
+ before(:each) do
6
+ TestResource.reload_resource_definition
7
+ end
8
+
9
+ it "should be able to find a single object" do
10
+ TestResource.connection.expects(:get).with("/test_resources/1.json")
11
+
12
+ TestResource.find(1)
13
+ end
14
+
15
+ it "should be able to find with parameters, params syntax" do
16
+ TestResource.connection.expects(:get).with("/test_resources.json?active=true")
17
+ TestResource.all(:params => {:active => true})
18
+ end
19
+
20
+ it "should be able to find with parameters without the params syntax" do
21
+ TestResource.connection.expects(:get).with("/test_resources.json?active=true&passive=false")
22
+
23
+ TestResource.all(:active => true, :passive => false)
24
+ end
25
+
26
+ it "should be able to chain find on top of a scope" do
27
+ TestResource.connection.expects(:get).with("/test_resources.json?active=true&passive=true")
28
+ TestResource.active.all(:passive => true)
29
+ end
30
+
31
+ it "should be able to chain find on top of an includes call" do
32
+ TestResource.connection.expects(:get).with("/test_resources/1.json").returns({"id" => 1, "has_many_object_ids" => [1,2]})
33
+ HasManyObject.connection.expects(:get).with("/has_many_objects.json?ids%5B%5D=1&ids%5B%5D=2").returns([])
34
+
35
+ TestResource.includes(:has_many_objects).find(1)
36
+ end
37
+
38
+ end
@@ -15,8 +15,7 @@ describe "With Prefixes" do
15
15
  it "should use the prefix to find a single record when given as a param" do
16
16
  PrefixModel.connection.expects(:get)
17
17
  .with(
18
- "/foreign/123/prefix_models/456.json",
19
- instance_of(Hash)
18
+ "/foreign/123/prefix_models/456.json"
20
19
  )
21
20
  .returns({})
22
21
  PrefixModel.find(456, :params => {:foreign_key_id => 123})
@@ -25,8 +24,7 @@ describe "With Prefixes" do
25
24
  it "should not use the prefix to find a single record when not given as a param to avoid automatic failure" do
26
25
  PrefixModel.connection.expects(:get)
27
26
  .with(
28
- "/prefix_models/456.json",
29
- instance_of(Hash)
27
+ "/prefix_models/456.json"
30
28
  )
31
29
  .returns({})
32
30
  PrefixModel.find(456)
@@ -52,8 +50,7 @@ describe "With Prefixes" do
52
50
  it "should use the prefix to find records" do
53
51
  prefix_model.send(:connection).expects(:get)
54
52
  .with(
55
- "/foreign/123/prefix_models.json",
56
- instance_of(Hash)
53
+ "/foreign/123/prefix_models.json"
57
54
  )
58
55
  .returns([])
59
56
  PrefixModel.first(:params => {:foreign_key_id => 123})
@@ -62,8 +59,7 @@ describe "With Prefixes" do
62
59
  it "should not use the prefix to find records when not given as a param to avoid automatic failure" do
63
60
  prefix_model.send(:connection).expects(:get)
64
61
  .with(
65
- "/prefix_models.json",
66
- instance_of(Hash)
62
+ "/prefix_models.json"
67
63
  )
68
64
  .returns([])
69
65
  PrefixModel.first
@@ -94,6 +90,7 @@ describe "With Prefixes" do
94
90
  prefix_model.name = "changed name"
95
91
  prefix_model.send(:connection).expects(:put)
96
92
  .with(
93
+
97
94
  "/foreign/123/prefix_models/456.json",
98
95
  {"prefix_model" => {"name" => "changed name"}}.to_json,
99
96
  instance_of(Hash)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api_resource
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2013-01-02 00:00:00.000000000 Z
14
+ date: 2013-01-04 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rake
@@ -237,38 +237,6 @@ dependencies:
237
237
  - - ! '>='
238
238
  - !ruby/object:Gem::Version
239
239
  version: '0'
240
- - !ruby/object:Gem::Dependency
241
- name: differ
242
- requirement: !ruby/object:Gem::Requirement
243
- none: false
244
- requirements:
245
- - - ! '>='
246
- - !ruby/object:Gem::Version
247
- version: '0'
248
- type: :development
249
- prerelease: false
250
- version_requirements: !ruby/object:Gem::Requirement
251
- none: false
252
- requirements:
253
- - - ! '>='
254
- - !ruby/object:Gem::Version
255
- version: '0'
256
- - !ruby/object:Gem::Dependency
257
- name: colorize
258
- requirement: !ruby/object:Gem::Requirement
259
- none: false
260
- requirements:
261
- - - ! '>='
262
- - !ruby/object:Gem::Version
263
- version: '0'
264
- type: :development
265
- prerelease: false
266
- version_requirements: !ruby/object:Gem::Requirement
267
- none: false
268
- requirements:
269
- - - ! '>='
270
- - !ruby/object:Gem::Version
271
- version: '0'
272
240
  - !ruby/object:Gem::Dependency
273
241
  name: sqlite3
274
242
  requirement: !ruby/object:Gem::Requirement
@@ -302,7 +270,7 @@ dependencies:
302
270
  - !ruby/object:Gem::Version
303
271
  version: '0'
304
272
  - !ruby/object:Gem::Dependency
305
- name: activemodel
273
+ name: json
306
274
  requirement: !ruby/object:Gem::Requirement
307
275
  none: false
308
276
  requirements:
@@ -318,7 +286,7 @@ dependencies:
318
286
  - !ruby/object:Gem::Version
319
287
  version: '0'
320
288
  - !ruby/object:Gem::Dependency
321
- name: activesupport
289
+ name: rest-client
322
290
  requirement: !ruby/object:Gem::Requirement
323
291
  none: false
324
292
  requirements:
@@ -334,7 +302,7 @@ dependencies:
334
302
  - !ruby/object:Gem::Version
335
303
  version: '0'
336
304
  - !ruby/object:Gem::Dependency
337
- name: json
305
+ name: log4r
338
306
  requirement: !ruby/object:Gem::Requirement
339
307
  none: false
340
308
  requirements:
@@ -350,7 +318,7 @@ dependencies:
350
318
  - !ruby/object:Gem::Version
351
319
  version: '0'
352
320
  - !ruby/object:Gem::Dependency
353
- name: rest-client
321
+ name: differ
354
322
  requirement: !ruby/object:Gem::Requirement
355
323
  none: false
356
324
  requirements:
@@ -366,7 +334,7 @@ dependencies:
366
334
  - !ruby/object:Gem::Version
367
335
  version: '0'
368
336
  - !ruby/object:Gem::Dependency
369
- name: log4r
337
+ name: colorize
370
338
  requirement: !ruby/object:Gem::Requirement
371
339
  none: false
372
340
  requirements:
@@ -432,6 +400,7 @@ files:
432
400
  - lib/api_resource/finders/abstract_finder.rb
433
401
  - lib/api_resource/finders/multi_object_association_finder.rb
434
402
  - lib/api_resource/finders/resource_finder.rb
403
+ - lib/api_resource/finders/single_finder.rb
435
404
  - lib/api_resource/finders/single_object_association_finder.rb
436
405
  - lib/api_resource/formats.rb
437
406
  - lib/api_resource/formats/json_format.rb
@@ -464,6 +433,7 @@ files:
464
433
  - spec/lib/finders/multi_object_association_finder_spec.rb
465
434
  - spec/lib/finders/resource_finder_spec.rb
466
435
  - spec/lib/finders/single_object_association_finder_spec.rb
436
+ - spec/lib/finders_spec.rb
467
437
  - spec/lib/local_spec.rb
468
438
  - spec/lib/mocks_spec.rb
469
439
  - spec/lib/model_errors_spec.rb
@@ -523,6 +493,7 @@ test_files:
523
493
  - spec/lib/finders/multi_object_association_finder_spec.rb
524
494
  - spec/lib/finders/resource_finder_spec.rb
525
495
  - spec/lib/finders/single_object_association_finder_spec.rb
496
+ - spec/lib/finders_spec.rb
526
497
  - spec/lib/local_spec.rb
527
498
  - spec/lib/mocks_spec.rb
528
499
  - spec/lib/model_errors_spec.rb