api_resource 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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