api_resource 0.6.21 → 0.6.22

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