api_resource 0.6.18 → 0.6.19
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -8
- data/Guardfile +5 -17
- data/api_resource.gemspec +4 -7
- data/lib/api_resource.rb +20 -19
- data/lib/api_resource/associations.rb +39 -23
- data/lib/api_resource/associations/association_proxy.rb +14 -13
- data/lib/api_resource/attributes.rb +555 -156
- data/lib/api_resource/base.rb +376 -305
- data/lib/api_resource/connection.rb +22 -12
- data/lib/api_resource/finders.rb +17 -18
- data/lib/api_resource/finders/single_finder.rb +1 -1
- data/lib/api_resource/mocks.rb +37 -31
- data/lib/api_resource/scopes.rb +70 -12
- data/lib/api_resource/serializer.rb +264 -0
- data/lib/api_resource/typecast.rb +13 -2
- data/lib/api_resource/typecasters/unknown_typecaster.rb +33 -0
- data/lib/api_resource/version.rb +1 -1
- data/spec/lib/associations/has_many_remote_object_proxy_spec.rb +3 -3
- data/spec/lib/associations_spec.rb +49 -94
- data/spec/lib/attributes_spec.rb +40 -56
- data/spec/lib/base_spec.rb +290 -382
- data/spec/lib/callbacks_spec.rb +6 -6
- data/spec/lib/connection_spec.rb +20 -20
- data/spec/lib/finders_spec.rb +14 -0
- data/spec/lib/mocks_spec.rb +9 -9
- data/spec/lib/prefixes_spec.rb +4 -5
- data/spec/lib/scopes_spec.rb +98 -0
- data/spec/lib/serializer_spec.rb +156 -0
- data/spec/spec_helper.rb +1 -4
- data/spec/support/test_resource.rb +1 -1
- metadata +14 -38
- data/spec/tmp/DIR +0 -0
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'active_support/core_ext/benchmark'
|
2
|
-
require '
|
2
|
+
require 'httpclient'
|
3
3
|
require 'net/https'
|
4
4
|
require 'date'
|
5
5
|
require 'time'
|
@@ -51,7 +51,7 @@ module ApiResource
|
|
51
51
|
@timeout = timeout
|
52
52
|
end
|
53
53
|
|
54
|
-
# make a
|
54
|
+
# make a get request
|
55
55
|
# @return [String] response.body raises an
|
56
56
|
# ApiResource::ConnectionError if we
|
57
57
|
# have a timeout, general exception, or
|
@@ -62,7 +62,7 @@ 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
|
+
format.decode(request(:get, path, {}, headers))
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
@@ -84,7 +84,7 @@ module ApiResource
|
|
84
84
|
response = request(
|
85
85
|
:put,
|
86
86
|
path,
|
87
|
-
body,
|
87
|
+
format.encode(body),
|
88
88
|
build_request_headers(headers, :put, self.site.merge(path))
|
89
89
|
)
|
90
90
|
# handle blank response and return true
|
@@ -111,7 +111,7 @@ module ApiResource
|
|
111
111
|
request(
|
112
112
|
:post,
|
113
113
|
path,
|
114
|
-
body,
|
114
|
+
format.encode(body),
|
115
115
|
build_request_headers(headers, :post, self.site.merge(path))
|
116
116
|
)
|
117
117
|
)
|
@@ -143,13 +143,18 @@ module ApiResource
|
|
143
143
|
# if result.code is not within 200..399
|
144
144
|
def request(method, path, *arguments)
|
145
145
|
handle_response(path) do
|
146
|
+
unless path =~ /\./
|
147
|
+
path += ".#{self.format.extension}"
|
148
|
+
end
|
146
149
|
ActiveSupport::Notifications.instrument("request.api_resource") do |payload|
|
147
|
-
|
148
150
|
# debug logging
|
149
151
|
ApiResource.logger.info("#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}")
|
150
152
|
payload[:method] = method
|
151
153
|
payload[:request_uri] = "#{site.scheme}://#{site.host}:#{site.port}#{path}"
|
152
|
-
payload[:result] = http
|
154
|
+
payload[:result] = http.send(
|
155
|
+
method,
|
156
|
+
"#{site.scheme}://#{site.host}:#{site.port}#{path}",
|
157
|
+
*arguments)
|
153
158
|
end
|
154
159
|
end
|
155
160
|
end
|
@@ -158,7 +163,7 @@ module ApiResource
|
|
158
163
|
def handle_response(path, &block)
|
159
164
|
begin
|
160
165
|
result = yield
|
161
|
-
rescue
|
166
|
+
rescue HTTPClient::TimeoutError
|
162
167
|
raise ApiResource::RequestTimeout.new("Request Time Out - Accessing #{path}}")
|
163
168
|
rescue Exception => error
|
164
169
|
if error.respond_to?(:http_code)
|
@@ -207,11 +212,16 @@ module ApiResource
|
|
207
212
|
|
208
213
|
# Creates new Net::HTTP instance for communication with the
|
209
214
|
# remote service and resources.
|
210
|
-
def http
|
211
|
-
|
212
|
-
|
215
|
+
def http
|
216
|
+
# TODO: Deal with proxies and such
|
217
|
+
unless @http
|
218
|
+
@http = HTTPClient.new
|
219
|
+
# TODO: This should be on the class level
|
220
|
+
@http.connect_timeout = ApiResource::Base.open_timeout
|
221
|
+
@http.receive_timeout = ApiResource::Base.timeout
|
213
222
|
end
|
214
|
-
|
223
|
+
|
224
|
+
return @http
|
215
225
|
end
|
216
226
|
|
217
227
|
def build_request_headers(headers, verb, uri)
|
data/lib/api_resource/finders.rb
CHANGED
@@ -13,9 +13,9 @@ module ApiResource
|
|
13
13
|
|
14
14
|
module ClassMethods
|
15
15
|
|
16
|
-
# This decides which finder method to call.
|
16
|
+
# This decides which finder method to call.
|
17
17
|
# It accepts arguments of the form "scope", "options={}"
|
18
|
-
# where options can be standard rails options or :expires_in.
|
18
|
+
# where options can be standard rails options or :expires_in.
|
19
19
|
# If :expires_in is set, it caches it for expires_in seconds.
|
20
20
|
|
21
21
|
# Need to support the following cases
|
@@ -36,13 +36,12 @@ module ApiResource
|
|
36
36
|
expiry = @expiry
|
37
37
|
ApiResource.with_ttl(expiry.to_f) do
|
38
38
|
if numeric_find
|
39
|
-
if single_find && (@conditions.blank_conditions? ||
|
39
|
+
if single_find && (@conditions.blank_conditions? || nested_find_only?)
|
40
40
|
# If we have no conditions or they are only prefixes or
|
41
|
-
# includes, and only one argument (not a word) then we
|
41
|
+
# includes, and only one argument (not a word) then we
|
42
42
|
# only have a single item to find.
|
43
43
|
# e.g. Class.includes(:association).find(1)
|
44
44
|
# Class.find(1)
|
45
|
-
@scope = @scope.first if @scope.is_a?(Array)
|
46
45
|
final_cond = @conditions.merge!(ApiResource::Conditions::ScopeCondition.new({:id => @scope}, self))
|
47
46
|
|
48
47
|
ApiResource::Finders::SingleFinder.new(self, final_cond).load
|
@@ -51,17 +50,12 @@ module ApiResource
|
|
51
50
|
# Class.includes(:association).find(1,2)
|
52
51
|
# Class.find(1,2)
|
53
52
|
# Class.active.find(1)
|
54
|
-
if Array.wrap(@scope).size == 1 && @scope.is_a?(Array)
|
55
|
-
@scope = @scope.first
|
56
|
-
end
|
57
|
-
|
58
53
|
fnd = @conditions.merge!(ApiResource::Conditions::ScopeCondition.new({:find => {:ids => @scope}}, self))
|
59
54
|
fnd.send(:all)
|
60
55
|
end
|
61
56
|
else
|
62
57
|
# e.g. Class.scope(1).first
|
63
58
|
# Class.first
|
64
|
-
@scope = @scope.first if @scope.is_a?(Array)
|
65
59
|
new_condition = @scope == :all ? {} : {@scope => true}
|
66
60
|
|
67
61
|
final_cond = @conditions.merge!ApiResource::Conditions::ScopeCondition.new(new_condition, self)
|
@@ -69,7 +63,6 @@ module ApiResource
|
|
69
63
|
fnd = ApiResource::Finders::ResourceFinder.new(self, final_cond)
|
70
64
|
fnd.send(@scope)
|
71
65
|
end
|
72
|
-
|
73
66
|
end
|
74
67
|
end
|
75
68
|
|
@@ -95,7 +88,7 @@ module ApiResource
|
|
95
88
|
end
|
96
89
|
|
97
90
|
def instantiate_collection(collection)
|
98
|
-
collection.collect{|record|
|
91
|
+
collection.collect{|record|
|
99
92
|
instantiate_record(record)
|
100
93
|
}
|
101
94
|
end
|
@@ -151,23 +144,29 @@ module ApiResource
|
|
151
144
|
|
152
145
|
# Conditions sometimes call find, passing themselves as the last arg.
|
153
146
|
if args.last.is_a?(ApiResource::Conditions::AbstractCondition)
|
154
|
-
cond
|
147
|
+
cond = args.slice!(args.length - 1)
|
155
148
|
else
|
156
|
-
cond
|
149
|
+
cond = nil
|
157
150
|
end
|
158
151
|
|
159
152
|
# Support options being passed in as a hash.
|
160
|
-
options
|
153
|
+
options = args.extract_options!
|
161
154
|
if options.blank?
|
162
155
|
options = nil
|
163
156
|
end
|
164
157
|
|
165
|
-
@expiry
|
158
|
+
@expiry = (options.is_a?(Hash) ? options.delete(:expires_in) : nil) || ApiResource::Base.ttl || 0
|
166
159
|
|
167
160
|
combine_conditions(options, cond)
|
168
161
|
|
169
162
|
# Remaining args are the scope.
|
170
|
-
@scope
|
163
|
+
@scope = args
|
164
|
+
|
165
|
+
if Array.wrap(@scope).size == 1 && @scope.is_a?(Array)
|
166
|
+
@scope = @scope.first
|
167
|
+
end
|
168
|
+
|
169
|
+
true
|
171
170
|
end
|
172
171
|
|
173
172
|
def combine_conditions(options, condition)
|
@@ -192,7 +191,7 @@ module ApiResource
|
|
192
191
|
@conditions = final_cond
|
193
192
|
end
|
194
193
|
|
195
|
-
def
|
194
|
+
def nested_find_only?
|
196
195
|
if @conditions.blank_conditions?
|
197
196
|
return false
|
198
197
|
else
|
data/lib/api_resource/mocks.rb
CHANGED
@@ -1,49 +1,55 @@
|
|
1
1
|
require 'api_resource'
|
2
2
|
|
3
3
|
module ApiResource
|
4
|
-
|
4
|
+
|
5
5
|
module Mocks
|
6
|
-
|
6
|
+
|
7
7
|
@@endpoints = {}
|
8
8
|
@@path = nil
|
9
9
|
|
10
|
-
# A simple interface class to change the new connection to look like the
|
10
|
+
# A simple interface class to change the new connection to look like the
|
11
11
|
# old activeresource connection
|
12
12
|
class Interface
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
|
14
|
+
|
15
|
+
def get(path, *args, &block)
|
16
|
+
uri = URI.parse(path)
|
17
|
+
path = uri.path + (uri.query.present? ? "?#{uri.query}" : '')
|
18
|
+
Connection.get(path, *args, &block)
|
16
19
|
end
|
17
|
-
|
18
|
-
|
19
|
-
Connection.send(:get, @path, *args, &block)
|
20
|
+
def post(path, *args, &block)
|
21
|
+
Connection.post(process_path(path), *args, &block)
|
20
22
|
end
|
21
|
-
def
|
22
|
-
Connection.
|
23
|
+
def put(path, *args, &block)
|
24
|
+
Connection.put(process_path(path), *args, &block)
|
23
25
|
end
|
24
|
-
def
|
25
|
-
Connection.
|
26
|
+
def delete(path, *args, &block)
|
27
|
+
Connection.delete(process_path(path), *args, &block)
|
26
28
|
end
|
27
|
-
def
|
28
|
-
Connection.
|
29
|
+
def head(path, *args, &block)
|
30
|
+
Connection.head(process_path(path), *args, &block)
|
29
31
|
end
|
30
|
-
|
31
|
-
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def process_path(path)
|
36
|
+
uri = URI.parse(path)
|
37
|
+
return uri.path
|
32
38
|
end
|
33
39
|
end
|
34
40
|
|
35
|
-
# set ApiResource's http
|
41
|
+
# set ApiResource's http
|
36
42
|
def self.init
|
37
43
|
::ApiResource::Connection.class_eval do
|
38
44
|
private
|
39
45
|
alias_method :http_without_mock, :http
|
40
|
-
def http
|
41
|
-
Interface.new
|
46
|
+
def http
|
47
|
+
Interface.new
|
42
48
|
end
|
43
49
|
end
|
44
50
|
end
|
45
|
-
|
46
|
-
# set ApiResource's http
|
51
|
+
|
52
|
+
# set ApiResource's http
|
47
53
|
def self.remove
|
48
54
|
::ApiResource::Connection.class_eval do
|
49
55
|
private
|
@@ -114,7 +120,7 @@ module ApiResource
|
|
114
120
|
|
115
121
|
return {:responses => nil, :params => nil}
|
116
122
|
end
|
117
|
-
|
123
|
+
|
118
124
|
|
119
125
|
private
|
120
126
|
def self.with_path_and_format(path, format, &block)
|
@@ -123,13 +129,13 @@ module ApiResource
|
|
123
129
|
@@path, @@format = nil, nil
|
124
130
|
ret
|
125
131
|
end
|
126
|
-
# define the
|
132
|
+
# define the
|
127
133
|
[:post, :put, :get, :delete, :head].each do |verb|
|
128
134
|
instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
129
135
|
def #{verb}(response_body, opts = {}, &block)
|
130
136
|
|
131
137
|
raise Exception.new("Must be called from within an endpoint block") unless @@path
|
132
|
-
opts = opts.reverse_merge({:status_code => 200, :response_headers => {}, :params => {}})
|
138
|
+
opts = opts.reverse_merge({:status_code => 200, :response_headers => {}, :params => {}})
|
133
139
|
|
134
140
|
@@endpoints[@@path] << [MockRequest.new(:#{verb}, @@path, :params => opts[:params], :format => @@format), MockResponse.new(response_body, :status_code => opts[:status_code], :headers => opts[:response_headers], :format => @@format, &block)]
|
135
141
|
end
|
@@ -138,7 +144,7 @@ module ApiResource
|
|
138
144
|
|
139
145
|
class MockResponse
|
140
146
|
attr_reader :body, :headers, :code, :format, :block
|
141
|
-
def initialize(body, opts = {}, &block)
|
147
|
+
def initialize(body, opts = {}, &block)
|
142
148
|
opts = opts.reverse_merge({:headers => {}, :status_code => 200})
|
143
149
|
@body = body
|
144
150
|
@headers = opts[:headers]
|
@@ -197,7 +203,7 @@ module ApiResource
|
|
197
203
|
@headers["Content-Length"] = @body.blank? ? "0" : @body.size.to_s
|
198
204
|
end
|
199
205
|
|
200
|
-
#
|
206
|
+
#
|
201
207
|
def typecast_values(data)
|
202
208
|
if data.is_a?(Hash)
|
203
209
|
data.each_pair do |k,v|
|
@@ -240,8 +246,8 @@ module ApiResource
|
|
240
246
|
self.requests = []
|
241
247
|
|
242
248
|
# body? methods
|
243
|
-
{ true => %w(post put),
|
244
|
-
false => %w(
|
249
|
+
{ true => %w(post put get),
|
250
|
+
false => %w(delete head) }.each do |has_body, methods|
|
245
251
|
methods.each do |method|
|
246
252
|
# def post(path, body, headers)
|
247
253
|
# request = ApiResource::Request.new(:post, path, body, headers)
|
@@ -249,7 +255,7 @@ module ApiResource
|
|
249
255
|
# if response = LifebookerClient::Mocks.find_response(request)
|
250
256
|
# response
|
251
257
|
# else
|
252
|
-
# raise InvalidRequestError.new("Could not find a response
|
258
|
+
# raise InvalidRequestError.new("Could not find a response
|
253
259
|
# recorded for #{request.to_s} - Responses recorded are: -
|
254
260
|
# #{inspect_responses}")
|
255
261
|
# end
|
@@ -261,7 +267,7 @@ module ApiResource
|
|
261
267
|
request = MockRequest.new(:#{method}, path, opts)
|
262
268
|
self.requests << request
|
263
269
|
if response = Mocks.find_response(request)
|
264
|
-
response[:response].tap{|resp|
|
270
|
+
response[:response].tap{|resp|
|
265
271
|
resp.generate_response(
|
266
272
|
request.params
|
267
273
|
.with_indifferent_access
|
data/lib/api_resource/scopes.rb
CHANGED
@@ -2,17 +2,17 @@ module ApiResource
|
|
2
2
|
module Scopes
|
3
3
|
|
4
4
|
extend ActiveSupport::Concern
|
5
|
-
|
5
|
+
|
6
6
|
module ClassMethods
|
7
7
|
# TODO: calling these methods should force loading of the resource definition
|
8
8
|
def scopes
|
9
9
|
return self.related_objects[:scopes]
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def scope?(name)
|
13
13
|
self.related_objects[:scopes].has_key?(name.to_sym)
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def scope_attributes(name)
|
17
17
|
raise "No such scope #{name}" unless self.scope?(name)
|
18
18
|
self.related_objects[:scopes][name.to_sym]
|
@@ -27,15 +27,15 @@ module ApiResource
|
|
27
27
|
def scope(scope_name, scope_definition)
|
28
28
|
|
29
29
|
unless scope_definition.is_a?(Hash)
|
30
|
-
raise ArgumentError, "Expecting an attributes hash given #{scope_definition.inspect}"
|
30
|
+
raise ArgumentError, "Expecting an attributes hash given #{scope_definition.inspect}"
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
self.related_objects[:scopes][scope_name.to_sym] = scope_definition
|
34
34
|
|
35
35
|
self.class_eval do
|
36
36
|
|
37
37
|
define_singleton_method(scope_name) do |*args|
|
38
|
-
|
38
|
+
|
39
39
|
arg_names = scope_definition.keys
|
40
40
|
arg_types = scope_definition.values
|
41
41
|
|
@@ -44,10 +44,10 @@ module ApiResource
|
|
44
44
|
}
|
45
45
|
|
46
46
|
arg_names.each_with_index do |arg_name, i|
|
47
|
-
|
47
|
+
|
48
48
|
# If we are dealing with a scope with multiple args
|
49
49
|
if arg_types[i] == :rest
|
50
|
-
finder_opts[scope_name][arg_name] =
|
50
|
+
finder_opts[scope_name][arg_name] =
|
51
51
|
args.slice(i, args.count)
|
52
52
|
# Else we are only dealing with a single argument
|
53
53
|
else
|
@@ -67,19 +67,77 @@ module ApiResource
|
|
67
67
|
end
|
68
68
|
end
|
69
69
|
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Apply scopes from params based on our resource
|
73
|
+
# definition
|
74
|
+
#
|
75
|
+
def add_scopes(params, base = self)
|
76
|
+
# scopes are stored as strings but we want to allow
|
77
|
+
params = params.with_indifferent_access
|
78
|
+
base = self.add_static_scopes(params, base)
|
79
|
+
return self.add_dynamic_scopes(params, base)
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
def add_static_scopes(params, base)
|
85
|
+
self.static_scopes.each do |name|
|
86
|
+
if params[name].present?
|
87
|
+
base = base.send(name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
return base
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_dynamic_scopes(params, base)
|
94
|
+
self.dynamic_scopes.each_pair do |name, args|
|
95
|
+
next if params[name].blank?
|
96
|
+
caller_args = []
|
97
|
+
args.each_pair do |subkey, type|
|
98
|
+
if type == :req || params[name][subkey].present?
|
99
|
+
caller_args << params[name][subkey]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
base = base.send(name, *caller_args)
|
103
|
+
end
|
104
|
+
return base
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Wrapper method to define all scopes from the resource definition
|
109
|
+
#
|
110
|
+
# @return [Boolean] true
|
111
|
+
def define_all_scopes
|
112
|
+
if self.resource_definition["scopes"]
|
113
|
+
self.resource_definition["scopes"].each_pair do |name, opts|
|
114
|
+
self.scope(name, opts)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
true
|
118
|
+
end
|
119
|
+
|
120
|
+
def dynamic_scopes
|
121
|
+
self.scopes.select { |name, args| args.present? }
|
122
|
+
end
|
123
|
+
|
124
|
+
def static_scopes
|
125
|
+
self.scopes.select { |name, args| args.blank? }.keys
|
126
|
+
end
|
127
|
+
|
70
128
|
end
|
71
|
-
|
129
|
+
|
72
130
|
def scopes
|
73
131
|
return self.class.scopes
|
74
132
|
end
|
75
|
-
|
133
|
+
|
76
134
|
def scope?(name)
|
77
135
|
return self.class.scope?(name)
|
78
136
|
end
|
79
|
-
|
137
|
+
|
80
138
|
def scope_attributes(name)
|
81
139
|
return self.class.scope_attributes(name)
|
82
140
|
end
|
83
|
-
|
141
|
+
|
84
142
|
end
|
85
143
|
end
|