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