api_resource 0.6.21 → 0.6.22

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/Gemfile.lock +1 -1
  4. data/LICENSE.txt +22 -0
  5. data/README.md +16 -9
  6. data/docs/Attributes.md +64 -0
  7. data/docs/Caching.md +45 -0
  8. data/docs/GettingStarted.md +149 -0
  9. data/docs/Relationships.md +136 -0
  10. data/docs/ResourceDefinition.md +80 -0
  11. data/docs/Retrieval.md +279 -0
  12. data/docs/Serialization.md +56 -0
  13. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +2 -2
  14. data/lib/api_resource/attributes.rb +16 -4
  15. data/lib/api_resource/base.rb +98 -19
  16. data/lib/api_resource/conditions/abstract_condition.rb +241 -129
  17. data/lib/api_resource/conditions/include_condition.rb +7 -1
  18. data/lib/api_resource/conditions/pagination_condition.rb +37 -0
  19. data/lib/api_resource/conditions/where_condition.rb +19 -0
  20. data/lib/api_resource/conditions.rb +18 -2
  21. data/lib/api_resource/connection.rb +27 -13
  22. data/lib/api_resource/exceptions.rb +11 -11
  23. data/lib/api_resource/finders/abstract_finder.rb +176 -95
  24. data/lib/api_resource/finders/multi_object_association_finder.rb +10 -9
  25. data/lib/api_resource/finders/resource_finder.rb +59 -49
  26. data/lib/api_resource/finders/single_finder.rb +5 -6
  27. data/lib/api_resource/finders/single_object_association_finder.rb +52 -51
  28. data/lib/api_resource/finders.rb +1 -1
  29. data/lib/api_resource/formats/file_upload_format.rb +75 -0
  30. data/lib/api_resource/formats.rb +4 -1
  31. data/lib/api_resource/response.rb +108 -0
  32. data/lib/api_resource/scopes.rb +62 -5
  33. data/lib/api_resource/serializer.rb +1 -1
  34. data/lib/api_resource/typecasters/boolean_typecaster.rb +1 -0
  35. data/lib/api_resource/typecasters/integer_typecaster.rb +1 -0
  36. data/lib/api_resource/typecasters/time_typecaster.rb +12 -4
  37. data/lib/api_resource/version.rb +1 -1
  38. data/lib/api_resource.rb +1 -0
  39. data/spec/lib/associations/has_one_remote_object_proxy_spec.rb +4 -4
  40. data/spec/lib/associations_spec.rb +3 -3
  41. data/spec/lib/attributes_spec.rb +16 -1
  42. data/spec/lib/base_spec.rb +121 -39
  43. data/spec/lib/conditions/{abstract_conditions_spec.rb → abstract_condition_spec.rb} +23 -11
  44. data/spec/lib/conditions/pagination_condition_spec.rb +88 -0
  45. data/spec/lib/finders/multi_object_association_finder_spec.rb +55 -27
  46. data/spec/lib/finders/resource_finder_spec.rb +26 -2
  47. data/spec/lib/finders/single_object_association_finder_spec.rb +14 -6
  48. data/spec/lib/finders_spec.rb +81 -81
  49. data/spec/lib/observing_spec.rb +3 -4
  50. data/spec/lib/response_spec.rb +18 -0
  51. data/spec/lib/scopes_spec.rb +25 -1
  52. data/spec/lib/typecasters/boolean_typecaster_spec.rb +1 -1
  53. data/spec/lib/typecasters/integer_typecaster_spec.rb +1 -1
  54. data/spec/lib/typecasters/time_typecaster_spec.rb +6 -0
  55. data/spec/support/files/bg-awesome.jpg +0 -0
  56. data/spec/support/mocks/test_resource_mocks.rb +26 -16
  57. data/spec/support/requests/test_resource_requests.rb +27 -23
  58. metadata +24 -4
@@ -1,133 +1,245 @@
1
1
  module ApiResource
2
2
 
3
- module Conditions
4
-
5
- class AbstractCondition
6
-
7
- include Enumerable
8
-
9
- attr_reader :conditions, :klass, :included_objects,
10
- :internal_object, :association, :remote_path
11
-
12
- # TODO: add the other load forcing methods here for collections
13
- delegate :[], :[]=, :<<, :first, :second, :last, :blank?, :nil?,
14
- :include?, :push, :pop, :+, :concat, :flatten, :flatten!, :compact,
15
- :compact!, :empty?, :fetch, :map, :reject, :reject!, :reverse,
16
- :select, :select!, :size, :sort, :sort!, :uniq, :uniq!, :to_a,
17
- :sample, :slice, :slice!, :count, :present?, :delete_if,
18
- :to => :internal_object
19
-
20
- # need to figure out what to do with args in the subclass,
21
- # parent is the set of scopes we have right now
22
- def initialize(args, klass)
23
- @klass = klass
24
-
25
- @conditions = args.with_indifferent_access
26
-
27
- @klass.load_resource_definition
28
- end
29
-
30
- def each(&block)
31
- self.internal_object.each(&block)
32
- end
33
-
34
- def blank_conditions?
35
- self.conditions.blank?
36
- end
37
-
38
- def eager_load?
39
- self.included_objects.present?
40
- end
41
-
42
- def included_objects
43
- Array.wrap(@included_objects)
44
- end
45
-
46
- def to_query
47
- CGI.unescape(to_query_safe_hash(self.to_hash).to_query)
48
- end
49
-
50
- def to_hash
51
- self.conditions.to_hash
52
- end
53
-
54
- def internal_object
55
- return @internal_object if @loaded
56
- @internal_object = self.instantiate_finder.load
57
- @loaded = true
58
- @internal_object
59
- end
60
-
61
- def all(*args)
62
- if args.blank?
63
- self.internal_object
64
- else
65
- self.find(*([:all] + args))
66
- end
67
- end
68
-
69
- # implement find that accepts an optional
70
- # condition object
71
- def find(*args)
72
- self.klass.find(*(args + [self]))
73
- end
74
-
75
- # TODO: review the hierarchy that makes this necessary
76
- # consider changing it to alias method
77
- def load
78
- self.internal_object
79
- end
80
-
81
- def loaded?
82
- @loaded == true
83
- end
84
-
85
- def reload
86
- if instance_variable_defined?(:@internal_object)
87
- remove_instance_variable(:@internal_object)
88
- end
89
- @loaded = false
90
- end
91
-
92
- def method_missing(sym, *args, &block)
93
- result = @klass.send(sym, *args, &block)
94
-
95
- if result.is_a?(ApiResource::Conditions::AbstractCondition)
96
- return self.dup.merge!(result)
97
- else
98
- return result
99
- end
100
- end
101
-
102
- def expires_in(time)
103
- ApiResource::Decorators::CachingDecorator.new(self, time)
104
- end
105
-
106
- # TODO: Remove the bang, this doesn't modify anything
107
- def merge!(cond)
108
- @included_objects = (@included_objects || []).concat(cond.included_objects || []).uniq
109
- @conditions = @conditions.merge(cond.to_hash)
110
- @association = cond.association || self.association
111
- @remote_path = self.remote_path ? self.remote_path : cond.remote_path
112
- return self
113
- end
114
-
115
- protected
116
-
117
- def instantiate_finder
118
- ApiResource::Finders::ResourceFinder.new(self.klass, self)
119
- end
120
-
121
- def to_query_safe_hash(hash)
122
- hash.each_pair do |k,v|
123
- hash[k] = to_query_safe_hash(v) if v.is_a?(Hash)
124
- hash[k] = true if v == {}
125
- end
126
- return hash
127
- end
128
-
129
- end
130
-
131
- end
3
+ module Conditions
4
+
5
+ class AbstractCondition
6
+
7
+ include Enumerable
8
+
9
+ # @!attribute [r] association
10
+ # @return [Boolean] Are we an association?
11
+ attr_reader :association
12
+
13
+ # @!attribute [r] conditions
14
+ # @return [Hash] Hash of conditions
15
+ attr_reader :conditions
16
+
17
+ # @!attribute [r] included_objects
18
+ # @return [Array<Symbol>] List of associations to eager load
19
+ attr_reader :included_objects
20
+
21
+ # @!attribute [r] internal_object
22
+ # @return [Array<ApiResource::Base>] Underlying objects we found
23
+ attr_reader :internal_object
24
+
25
+ # @!attribute [r] klass
26
+ # @return [Class] Owner class
27
+ attr_reader :klass
28
+
29
+ # @!attribute [r] remote_path
30
+ # @return [String] Path to hit when we find stuff
31
+ attr_reader :remote_path
32
+
33
+ # TODO: add the other load forcing methods here for collections
34
+ delegate :[], :[]=, :<<, :first, :second, :last, :blank?, :nil?,
35
+ :include?, :push, :pop, :+, :concat, :flatten, :flatten!, :compact,
36
+ :compact!, :empty?, :fetch, :map, :reject, :reject!, :reverse,
37
+ :select, :select!, :size, :sort, :sort!, :uniq, :uniq!, :to_a,
38
+ :sample, :slice, :slice!, :count, :present?, :delete_if,
39
+ :to => :internal_object
40
+
41
+ # need to figure out what to do with args in the subclass,
42
+ # parent is the set of scopes we have right now
43
+ def initialize(args, klass)
44
+ @klass = klass
45
+ @conditions = args.with_indifferent_access
46
+ @klass.load_resource_definition
47
+ end
48
+
49
+ def all(*args)
50
+ if args.blank?
51
+ self.internal_object
52
+ else
53
+ self.find(*([:all] + args))
54
+ end
55
+ end
56
+
57
+ #
58
+ # Is this a find without any conditions
59
+ #
60
+ # @return [Boolean]
61
+ def blank_conditions?
62
+ self.conditions.blank?
63
+ end
64
+
65
+ #
66
+ # Accessor for the current page if we
67
+ # are paginated. Returns 1 if we are not
68
+ # paginated or have an invalid page
69
+ #
70
+ # @return [Fixnum] [description]
71
+ def current_page
72
+ return 1 unless self.paginated?
73
+ return 1 if @conditions[:page].to_i < 1
74
+ return @conditions[:page].to_i
75
+ end
76
+
77
+ def each(&block)
78
+ self.internal_object.each(&block)
79
+ end
80
+
81
+ #
82
+ # Are we set up to eager load associations?
83
+ #
84
+ # @return [Boolean]
85
+ def eager_load?
86
+ self.included_objects.present?
87
+ end
88
+
89
+ def expires_in(time)
90
+ ApiResource::Decorators::CachingDecorator.new(self, time)
91
+ end
92
+
93
+ # implement find that accepts an optional
94
+ # condition object
95
+ def find(*args)
96
+ self.klass.find(*(args + [self]))
97
+ end
98
+
99
+ def included_objects
100
+ Array.wrap(@included_objects)
101
+ end
102
+
103
+ def internal_object
104
+ return @internal_object if @loaded
105
+ @internal_object = self.instantiate_finder
106
+ @internal_object.load
107
+ @loaded = true
108
+ @internal_object
109
+ end
110
+
111
+ # TODO: review the hierarchy that makes this necessary
112
+ # consider changing it to alias method
113
+ def load
114
+ self.internal_object
115
+ end
116
+
117
+ def loaded?
118
+ @loaded == true
119
+ end
120
+
121
+ # TODO: Remove the bang, this doesn't modify anything
122
+ def merge!(cond)
123
+
124
+ # merge included objects
125
+ @included_objects = self.included_objects | cond.included_objects
126
+
127
+ # handle pagination
128
+ if cond.paginated?
129
+ @paginated = true
130
+ end
131
+
132
+ # merge conditions
133
+ @conditions = @conditions.merge(cond.to_hash)
134
+
135
+ # handle associations
136
+ if cond.association
137
+ @association = true
138
+ end
139
+ # handle remote path copying
140
+ @remote_path ||= cond.remote_path
141
+
142
+ return self
143
+ end
144
+
145
+ #
146
+ # The offset we are currently at in our query
147
+ # Returns 0 if we are not paginated
148
+ #
149
+ # @return [Fixnum]
150
+ def offset
151
+ return 0 unless self.paginated?
152
+ prev_page = self.current_page.to_i - 1
153
+ prev_page * self.per_page
154
+ end
155
+
156
+ #
157
+ # Reader for whether or not we are paginated
158
+ #
159
+ # @return [Boolean]
160
+ def paginated?
161
+ @paginated
162
+ end
163
+
164
+ #
165
+ # Number of records per page if paginated
166
+ # Returns 1 if number is out of range or if pagination
167
+ # is not enabled
168
+ #
169
+ # @return [Fixnum]
170
+ def per_page
171
+ return 1 unless self.paginated?
172
+ return 1 if @conditions["per_page"].to_i < 1
173
+ return @conditions["per_page"].to_i
174
+ end
175
+
176
+ def reload
177
+ if instance_variable_defined?(:@internal_object)
178
+ remove_instance_variable(:@internal_object)
179
+ end
180
+ @loaded = false
181
+ end
182
+
183
+ def to_query
184
+ CGI.unescape(to_query_safe_hash(self.to_hash).to_query)
185
+ end
186
+
187
+ def to_hash
188
+ self.conditions.to_hash
189
+ end
190
+
191
+ #
192
+ # Total number of records found in the collection
193
+ # if it is paginated
194
+ #
195
+ # @return [Fixnum]
196
+ def total_entries
197
+ self.internal_object.total_entries
198
+ end
199
+
200
+ #
201
+ # The total number of pages in our collection
202
+ # or 1 if it is not paginated
203
+ #
204
+ # @return [Fixnum]
205
+ def total_pages
206
+ return 1 unless self.paginated?
207
+ return (self.total_entries / self.per_page.to_f).ceil
208
+ end
209
+
210
+ protected
211
+
212
+ #
213
+ # Proxy all calls to the base finder class
214
+ # @param sym [Symbol] Method name
215
+ # @param *args [Array<Mixed>] Args
216
+ # @param &block [Proc] Block
217
+ #
218
+ # @return [Mixed]
219
+ def method_missing(sym, *args, &block)
220
+ result = @klass.send(sym, *args, &block)
221
+
222
+ if result.is_a?(ApiResource::Conditions::AbstractCondition)
223
+ return self.dup.merge!(result)
224
+ else
225
+ return result
226
+ end
227
+ end
228
+
229
+ def instantiate_finder
230
+ ApiResource::Finders::ResourceFinder.new(self.klass, self)
231
+ end
232
+
233
+ def to_query_safe_hash(hash)
234
+ hash.each_pair do |k,v|
235
+ hash[k] = to_query_safe_hash(v) if v.is_a?(Hash)
236
+ hash[k] = true if v == {}
237
+ end
238
+ return hash
239
+ end
240
+
241
+ end
242
+
243
+ end
132
244
 
133
245
  end
@@ -4,7 +4,13 @@ module ApiResource
4
4
 
5
5
  class IncludeCondition < AbstractCondition
6
6
 
7
- def initialize(klass, incs)
7
+ #
8
+ # Constructor
9
+ #
10
+ # @param klass [Class] Finder
11
+ # @param incs [Array<Symbol>, Symbol] Associations to include
12
+ #
13
+ def initialize(klass, incs)
8
14
  super({}, klass)
9
15
  @included_objects = Array.wrap(incs)
10
16
  end
@@ -0,0 +1,37 @@
1
+ module ApiResource
2
+
3
+ module Conditions
4
+
5
+ #
6
+ # Class to handle pagination params, passing
7
+ # of pagination params along to the server, and the
8
+ # retrieval of the headers from the response
9
+ #
10
+ # @author [dlangevin]
11
+ #
12
+ class PaginationCondition < AbstractCondition
13
+
14
+ #
15
+ # Constructor - sets up the pagination options
16
+ # @param opts = {} [Hash] Pagination opts
17
+ # @option opts [Fixnum] :page (1) Page we are on
18
+ # @option opts [Fixnum] :per_page (10) Number per page
19
+ def initialize(klass, opts = {})
20
+ @page = opts[:page] || 1
21
+ @per_page = opts[:per_page] || 10
22
+ super({ page: @page, per_page: @per_page }, klass)
23
+ end
24
+
25
+ #
26
+ # Are we paginated?
27
+ #
28
+ # @return [Boolean] true
29
+ def paginated?
30
+ true
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,19 @@
1
+ module ApiResource
2
+
3
+ module Conditions
4
+
5
+ #
6
+ # Class to handle pagination params, passing
7
+ # of pagination params along to the server, and the
8
+ # retrieval of the headers from the response
9
+ #
10
+ # @author [dlangevin]
11
+ #
12
+ class WhereCondition < AbstractCondition
13
+ def initialize(klass, opts)
14
+ super(opts, klass)
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -7,10 +7,12 @@ module ApiResource
7
7
 
8
8
  autoload :AbstractCondition
9
9
  autoload :AssociationCondition
10
- autoload :SingleObjectAssociationCondition
11
- autoload :MultiObjectAssociationCondition
12
10
  autoload :IncludeCondition
11
+ autoload :MultiObjectAssociationCondition
12
+ autoload :PaginationCondition
13
+ autoload :SingleObjectAssociationCondition
13
14
  autoload :ScopeCondition
15
+ autoload :WhereCondition
14
16
 
15
17
  module ClassMethods
16
18
 
@@ -30,6 +32,20 @@ module ApiResource
30
32
 
31
33
  end
32
34
 
35
+ def paginate(opts = {})
36
+ self.load_resource_definition
37
+
38
+ # Everything looks good so just create the scope
39
+ ApiResource::Conditions::PaginationCondition.new(self, opts)
40
+
41
+ end
42
+
43
+ def where(opts = {})
44
+ self.load_resource_definition
45
+
46
+ ApiResource::Conditions::WhereCondition.new(self, opts)
47
+ end
48
+
33
49
  end
34
50
 
35
51
  end
@@ -62,17 +62,28 @@ module ApiResource
62
62
  headers = build_request_headers(headers, :get, site)
63
63
 
64
64
  self.with_caching(path, headers) do
65
- format.decode(request(:get, path, {}, headers))
65
+ request(:get, path, {}, headers)
66
66
  end
67
67
  end
68
68
 
69
69
  def delete(path, headers = self.headers)
70
- request(:delete, path, {}, build_request_headers(headers, :delete, self.site.merge(path)))
70
+ request(
71
+ :delete,
72
+ path,
73
+ {},
74
+ build_request_headers(headers, :delete, self.site.merge(path))
75
+ )
71
76
  return true
72
77
  end
73
78
 
74
79
  def head(path, headers = self.headers)
75
- request(:head, path, {}, build_request_headers(headers, :head, self.site.merge(path)))
80
+ request(
81
+ :head,
82
+ path,
83
+ {},
84
+ build_request_headers(headers, :head, self.site.merge(path))
85
+ )
86
+ return true
76
87
  end
77
88
 
78
89
  # make a put request
@@ -97,7 +108,7 @@ module ApiResource
97
108
  "[DEPRECATION] Returning a response body from a PUT " +
98
109
  "is deprecated. \n#{response.pretty_inspect} was returned."
99
110
  )
100
- return format.decode(response)
111
+ return response
101
112
  end
102
113
  end
103
114
 
@@ -107,13 +118,11 @@ module ApiResource
107
118
  # have a timeout, general exception, or
108
119
  # if result.code is not within 200..399
109
120
  def post(path, body = {}, headers = self.headers)
110
- format.decode(
111
- request(
112
- :post,
113
- path,
114
- format.encode(body),
115
- build_request_headers(headers, :post, self.site.merge(path))
116
- )
121
+ request(
122
+ :post,
123
+ path,
124
+ format.encode(body),
125
+ build_request_headers(headers, :post, self.site.merge(path))
117
126
  )
118
127
  end
119
128
 
@@ -171,7 +180,12 @@ module ApiResource
171
180
  ApiResource.logger.error(error.message)
172
181
  result = error.response
173
182
  else
174
- raise ApiResource::ConnectionError.new(nil, :message => "Unknown error #{error}")
183
+ exception = ApiResource::ConnectionError.new(
184
+ nil,
185
+ :message => "Unknown error #{error}"
186
+ )
187
+ exception.set_backtrace(error.backtrace)
188
+ raise exception
175
189
  end
176
190
  end
177
191
  return propogate_response_or_error(result, result.code)
@@ -182,7 +196,7 @@ module ApiResource
182
196
  when 301,302
183
197
  raise ApiResource::Redirection.new(response)
184
198
  when 200..399
185
- response.body
199
+ return ApiResource::Response.new(response)
186
200
  when 400
187
201
  raise ApiResource::BadRequest.new(response)
188
202
  when 401
@@ -1,8 +1,8 @@
1
1
  module ApiResource
2
2
  class ConnectionError < StandardError # :nodoc:
3
-
3
+
4
4
  cattr_accessor :http_code
5
-
5
+
6
6
  attr_reader :response
7
7
 
8
8
  def initialize(response, options = {})
@@ -15,9 +15,9 @@ module ApiResource
15
15
  message = "Failed."
16
16
 
17
17
  if response.respond_to?(:code)
18
- message << " Response code = #{response.code}."
18
+ message << " Response code = #{response.code}."
19
19
  end
20
-
20
+
21
21
  if response.respond_to?(:body)
22
22
  begin
23
23
  body = JSON.parse(response.body).pretty_inspect
@@ -26,15 +26,15 @@ module ApiResource
26
26
  end
27
27
  message << "\nResponse message = #{body}."
28
28
  end
29
-
29
+
30
30
  message << "\n#{@message}"
31
31
  message << "\n#{@path}"
32
32
  end
33
-
33
+
34
34
  def http_code
35
35
  self.class.http_code
36
36
  end
37
-
37
+
38
38
  end
39
39
 
40
40
  # Raised when a Timeout::Error occurs.
@@ -72,7 +72,7 @@ module ApiResource
72
72
 
73
73
  # 404 Not Found
74
74
  class ResourceNotFound < ClientError; self.http_code = 404; end # :nodoc:
75
-
75
+
76
76
  # 406 Not Acceptable
77
77
  class NotAcceptable < ClientError; self.http_code = 406; end
78
78
 
@@ -81,7 +81,7 @@ module ApiResource
81
81
 
82
82
  # 410 Gone
83
83
  class ResourceGone < ClientError; self.http_code = 410; end # :nodoc:
84
-
84
+
85
85
  class UnprocessableEntity < ClientError; self.http_code = 422; end
86
86
 
87
87
  # 5xx Server Error
@@ -89,9 +89,9 @@ module ApiResource
89
89
 
90
90
  # 405 Method Not Allowed
91
91
  class MethodNotAllowed < ClientError # :nodoc:
92
-
92
+
93
93
  self.http_code = 405
94
-
94
+
95
95
  def allowed_methods
96
96
  @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
97
97
  end