activeresource-five 5.0.0

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.
@@ -0,0 +1,82 @@
1
+ module ActiveResource
2
+ class ConnectionError < StandardError # :nodoc:
3
+ attr_reader :response
4
+
5
+ def initialize(response, message = nil)
6
+ @response = response
7
+ @message = message
8
+ end
9
+
10
+ def to_s
11
+ message = "Failed."
12
+ message << " Response code = #{response.code}." if response.respond_to?(:code)
13
+ message << " Response message = #{response.message}." if response.respond_to?(:message)
14
+ message
15
+ end
16
+ end
17
+
18
+ # Raised when a Timeout::Error occurs.
19
+ class TimeoutError < ConnectionError
20
+ def initialize(message)
21
+ @message = message
22
+ end
23
+ def to_s; @message ;end
24
+ end
25
+
26
+ # Raised when a OpenSSL::SSL::SSLError occurs.
27
+ class SSLError < ConnectionError
28
+ def initialize(message)
29
+ @message = message
30
+ end
31
+ def to_s; @message ;end
32
+ end
33
+
34
+ # 3xx Redirection
35
+ class Redirection < ConnectionError # :nodoc:
36
+ def to_s
37
+ response['Location'] ? "#{super} => #{response['Location']}" : super
38
+ end
39
+ end
40
+
41
+ class MissingPrefixParam < ArgumentError # :nodoc:
42
+ end
43
+
44
+ # 4xx Client Error
45
+ class ClientError < ConnectionError # :nodoc:
46
+ end
47
+
48
+ # 400 Bad Request
49
+ class BadRequest < ClientError # :nodoc:
50
+ end
51
+
52
+ # 401 Unauthorized
53
+ class UnauthorizedAccess < ClientError # :nodoc:
54
+ end
55
+
56
+ # 403 Forbidden
57
+ class ForbiddenAccess < ClientError # :nodoc:
58
+ end
59
+
60
+ # 404 Not Found
61
+ class ResourceNotFound < ClientError # :nodoc:
62
+ end
63
+
64
+ # 409 Conflict
65
+ class ResourceConflict < ClientError # :nodoc:
66
+ end
67
+
68
+ # 410 Gone
69
+ class ResourceGone < ClientError # :nodoc:
70
+ end
71
+
72
+ # 5xx Server Error
73
+ class ServerError < ConnectionError # :nodoc:
74
+ end
75
+
76
+ # 405 Method Not Allowed
77
+ class MethodNotAllowed < ClientError # :nodoc:
78
+ def allowed_methods
79
+ @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveResource
2
+ module Formats
3
+ autoload :XmlFormat, 'active_resource/formats/xml_format'
4
+ autoload :JsonFormat, 'active_resource/formats/json_format'
5
+
6
+ # Lookup the format class from a mime type reference symbol. Example:
7
+ #
8
+ # ActiveResource::Formats[:xml] # => ActiveResource::Formats::XmlFormat
9
+ # ActiveResource::Formats[:json] # => ActiveResource::Formats::JsonFormat
10
+ def self.[](mime_type_reference)
11
+ ActiveResource::Formats.const_get(ActiveSupport::Inflector.camelize(mime_type_reference.to_s) + "Format")
12
+ end
13
+
14
+ def self.remove_root(data)
15
+ if data.is_a?(Hash) && data.keys.size == 1 && data.values.first.is_a?(Enumerable)
16
+ data.values.first
17
+ else
18
+ data
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/json'
2
+
3
+ module ActiveResource
4
+ module Formats
5
+ module JsonFormat
6
+ extend self
7
+
8
+ def extension
9
+ "json"
10
+ end
11
+
12
+ def mime_type
13
+ "application/json"
14
+ end
15
+
16
+ def encode(hash, options = nil)
17
+ ActiveSupport::JSON.encode(hash, options)
18
+ end
19
+
20
+ def decode(json)
21
+ Formats.remove_root(ActiveSupport::JSON.decode(json))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/core_ext/hash/conversions'
2
+
3
+ module ActiveResource
4
+ module Formats
5
+ module XmlFormat
6
+ extend self
7
+
8
+ def extension
9
+ "xml"
10
+ end
11
+
12
+ def mime_type
13
+ "application/xml"
14
+ end
15
+
16
+ def encode(hash, options={})
17
+ hash.to_xml(options)
18
+ end
19
+
20
+ def decode(xml)
21
+ Formats.remove_root(Hash.from_xml(xml))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,375 @@
1
+ require 'active_support/core_ext/kernel/reporting'
2
+ require 'active_support/core_ext/object/inclusion'
3
+
4
+ module ActiveResource
5
+ class InvalidRequestError < StandardError; end #:nodoc:
6
+
7
+ # One thing that has always been a pain with remote web services is testing. The HttpMock
8
+ # class makes it easy to test your Active Resource models by creating a set of mock responses to specific
9
+ # requests.
10
+ #
11
+ # To test your Active Resource model, you simply call the ActiveResource::HttpMock.respond_to
12
+ # method with an attached block. The block declares a set of URIs with expected input, and the output
13
+ # each request should return. The passed in block has any number of entries in the following generalized
14
+ # format:
15
+ #
16
+ # mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
17
+ #
18
+ # * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +patch+, +put+, +delete+ or
19
+ # +head+.
20
+ # * <tt>path</tt> - A string, starting with a "/", defining the URI that is expected to be
21
+ # called.
22
+ # * <tt>request_headers</tt> - Headers that are expected along with the request. This argument uses a
23
+ # hash format, such as <tt>{ "Content-Type" => "application/json" }</tt>. This mock will only trigger
24
+ # if your tests sends a request with identical headers.
25
+ # * <tt>body</tt> - The data to be returned. This should be a string of Active Resource parseable content,
26
+ # such as Json.
27
+ # * <tt>status</tt> - The HTTP response code, as an integer, to return with the response.
28
+ # * <tt>response_headers</tt> - Headers to be returned with the response. Uses the same hash format as
29
+ # <tt>request_headers</tt> listed above.
30
+ #
31
+ # In order for a mock to deliver its content, the incoming request must match by the <tt>http_method</tt>,
32
+ # +path+ and <tt>request_headers</tt>. If no match is found an +InvalidRequestError+ exception
33
+ # will be raised showing you what request it could not find a response for and also what requests and response
34
+ # pairs have been recorded so you can create a new mock for that request.
35
+ #
36
+ # ==== Example
37
+ # def setup
38
+ # @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
39
+ # ActiveResource::HttpMock.respond_to do |mock|
40
+ # mock.post "/people.json", {}, @matz, 201, "Location" => "/people/1.json"
41
+ # mock.get "/people/1.json", {}, @matz
42
+ # mock.put "/people/1.json", {}, nil, 204
43
+ # mock.delete "/people/1.json", {}, nil, 200
44
+ # end
45
+ # end
46
+ #
47
+ # def test_get_matz
48
+ # person = Person.find(1)
49
+ # assert_equal "Matz", person.name
50
+ # end
51
+ #
52
+ class HttpMock
53
+ class Responder #:nodoc:
54
+ def initialize(responses)
55
+ @responses = responses
56
+ end
57
+
58
+ [ :post, :patch, :put, :get, :delete, :head ].each do |method|
59
+ # def post(path, request_headers = {}, body = nil, status = 200, response_headers = {})
60
+ # @responses[Request.new(:post, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
61
+ # end
62
+ module_eval <<-EOE, __FILE__, __LINE__ + 1
63
+ def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
64
+ request = Request.new(:#{method}, path, nil, request_headers)
65
+ response = Response.new(body || "", status, response_headers)
66
+
67
+ delete_duplicate_responses(request)
68
+
69
+ @responses << [request, response]
70
+ end
71
+ EOE
72
+ end
73
+
74
+ private
75
+
76
+ def delete_duplicate_responses(request)
77
+ @responses.delete_if {|r| r[0] == request }
78
+ end
79
+ end
80
+
81
+ class << self
82
+
83
+ # Returns an array of all request objects that have been sent to the mock. You can use this to check
84
+ # if your model actually sent an HTTP request.
85
+ #
86
+ # ==== Example
87
+ # def setup
88
+ # @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
89
+ # ActiveResource::HttpMock.respond_to do |mock|
90
+ # mock.get "/people/1.json", {}, @matz
91
+ # end
92
+ # end
93
+ #
94
+ # def test_should_request_remote_service
95
+ # person = Person.find(1) # Call the remote service
96
+ #
97
+ # # This request object has the same HTTP method and path as declared by the mock
98
+ # expected_request = ActiveResource::Request.new(:get, "/people/1.json")
99
+ #
100
+ # # Assert that the mock received, and responded to, the expected request from the model
101
+ # assert ActiveResource::HttpMock.requests.include?(expected_request)
102
+ # end
103
+ def requests
104
+ @@requests ||= []
105
+ end
106
+
107
+ # Returns the list of requests and their mocked responses. Look up a
108
+ # response for a request using <tt>responses.assoc(request)</tt>.
109
+ def responses
110
+ @@responses ||= []
111
+ end
112
+
113
+ # Accepts a block which declares a set of requests and responses for the HttpMock to respond to in
114
+ # the following format:
115
+ #
116
+ # mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
117
+ #
118
+ # === Example
119
+ #
120
+ # @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
121
+ # ActiveResource::HttpMock.respond_to do |mock|
122
+ # mock.post "/people.json", {}, @matz, 201, "Location" => "/people/1.json"
123
+ # mock.get "/people/1.json", {}, @matz
124
+ # mock.put "/people/1.json", {}, nil, 204
125
+ # mock.delete "/people/1.json", {}, nil, 200
126
+ # end
127
+ #
128
+ # Alternatively, accepts a hash of <tt>{Request => Response}</tt> pairs allowing you to generate
129
+ # these the following format:
130
+ #
131
+ # ActiveResource::Request.new(method, path, body, request_headers)
132
+ # ActiveResource::Response.new(body, status, response_headers)
133
+ #
134
+ # === Example
135
+ #
136
+ # Request.new(method, path, nil, request_headers)
137
+ #
138
+ # @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
139
+ #
140
+ # create_matz = ActiveResource::Request.new(:post, '/people.json', @matz, {})
141
+ # created_response = ActiveResource::Response.new("", 201, {"Location" => "/people/1.json"})
142
+ # get_matz = ActiveResource::Request.new(:get, '/people/1.json', nil)
143
+ # ok_response = ActiveResource::Response.new("", 200, {})
144
+ #
145
+ # pairs = {create_matz => created_response, get_matz => ok_response}
146
+ #
147
+ # ActiveResource::HttpMock.respond_to(pairs)
148
+ #
149
+ # Note, by default, every time you call +respond_to+, any previous request and response pairs stored
150
+ # in HttpMock will be deleted giving you a clean slate to work on.
151
+ #
152
+ # If you want to override this behavior, pass in +false+ as the last argument to +respond_to+
153
+ #
154
+ # === Example
155
+ #
156
+ # ActiveResource::HttpMock.respond_to do |mock|
157
+ # mock.send(:get, "/people/1", {}, "JSON1")
158
+ # end
159
+ # ActiveResource::HttpMock.responses.length #=> 1
160
+ #
161
+ # ActiveResource::HttpMock.respond_to(false) do |mock|
162
+ # mock.send(:get, "/people/2", {}, "JSON2")
163
+ # end
164
+ # ActiveResource::HttpMock.responses.length #=> 2
165
+ #
166
+ # This also works with passing in generated pairs of requests and responses, again, just pass in false
167
+ # as the last argument:
168
+ #
169
+ # === Example
170
+ #
171
+ # ActiveResource::HttpMock.respond_to do |mock|
172
+ # mock.send(:get, "/people/1", {}, "JSON1")
173
+ # end
174
+ # ActiveResource::HttpMock.responses.length #=> 1
175
+ #
176
+ # get_matz = ActiveResource::Request.new(:get, '/people/1.json', nil)
177
+ # ok_response = ActiveResource::Response.new("", 200, {})
178
+ #
179
+ # pairs = {get_matz => ok_response}
180
+ #
181
+ # ActiveResource::HttpMock.respond_to(pairs, false)
182
+ # ActiveResource::HttpMock.responses.length #=> 2
183
+ #
184
+ # # If you add a response with an existing request, it will be replaced
185
+ #
186
+ # fail_response = ActiveResource::Response.new("", 404, {})
187
+ # pairs = {get_matz => fail_response}
188
+ #
189
+ # ActiveResource::HttpMock.respond_to(pairs, false)
190
+ # ActiveResource::HttpMock.responses.length #=> 2
191
+ #
192
+ def respond_to(*args) #:yields: mock
193
+ pairs = args.first || {}
194
+ reset! if args.last.class != FalseClass
195
+
196
+ if block_given?
197
+ yield Responder.new(responses)
198
+ else
199
+ delete_responses_to_replace pairs.to_a
200
+ responses.concat pairs.to_a
201
+ Responder.new(responses)
202
+ end
203
+ end
204
+
205
+ def delete_responses_to_replace(new_responses)
206
+ new_responses.each{|nr|
207
+ request_to_remove = nr[0]
208
+ @@responses = responses.delete_if{|r| r[0] == request_to_remove}
209
+ }
210
+ end
211
+
212
+ # Deletes all logged requests and responses.
213
+ def reset!
214
+ requests.clear
215
+ responses.clear
216
+ end
217
+
218
+ # Enables all ActiveResource::Connection instances to use real
219
+ # Net::HTTP instance instead of a mock.
220
+ def enable_net_connection!
221
+ @@net_connection_enabled = true
222
+ end
223
+
224
+ # Sets all ActiveResource::Connection to use HttpMock instances.
225
+ def disable_net_connection!
226
+ @@net_connection_enabled = false
227
+ end
228
+
229
+ # Checks if real requests can be used instead of the default mock used in tests.
230
+ def net_connection_enabled?
231
+ if defined?(@@net_connection_enabled)
232
+ @@net_connection_enabled
233
+ else
234
+ @@net_connection_enabled = false
235
+ end
236
+ end
237
+
238
+ def net_connection_disabled?
239
+ !net_connection_enabled?
240
+ end
241
+
242
+ end
243
+
244
+ # body? methods
245
+ { true => %w(post patch put),
246
+ false => %w(get delete head) }.each do |has_body, methods|
247
+ methods.each do |method|
248
+ # def post(path, body, headers)
249
+ # request = ActiveResource::Request.new(:post, path, body, headers)
250
+ # self.class.requests << request
251
+ # if response = self.class.responses.assoc(request)
252
+ # response[1]
253
+ # else
254
+ # raise InvalidRequestError.new("Could not find a response recorded for #{request.to_s} - Responses recorded are: - #{inspect_responses}")
255
+ # end
256
+ # end
257
+ module_eval <<-EOE, __FILE__, __LINE__ + 1
258
+ def #{method}(path, #{'body, ' if has_body}headers)
259
+ request = ActiveResource::Request.new(:#{method}, path, #{has_body ? 'body, ' : 'nil, '}headers)
260
+ self.class.requests << request
261
+ if response = self.class.responses.assoc(request)
262
+ response[1]
263
+ else
264
+ raise InvalidRequestError.new("Could not find a response recorded for \#{request.to_s} - Responses recorded are: \#{inspect_responses}")
265
+ end
266
+ end
267
+ EOE
268
+ end
269
+ end
270
+
271
+ def initialize(site) #:nodoc:
272
+ @site = site
273
+ end
274
+
275
+ def inspect_responses #:nodoc:
276
+ self.class.responses.map { |r| r[0].to_s }.inspect
277
+ end
278
+ end
279
+
280
+ class Request
281
+ attr_accessor :path, :method, :body, :headers
282
+
283
+ def initialize(method, path, body = nil, headers = {})
284
+ @method, @path, @body, @headers = method, path, body, headers
285
+ end
286
+
287
+ def ==(req)
288
+ path == req.path && method == req.method && headers_match?(req)
289
+ end
290
+
291
+ def to_s
292
+ "<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
293
+ end
294
+
295
+ private
296
+
297
+ def headers_match?(req)
298
+ # Ignore format header on equality if it's not defined
299
+ format_header = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[method]
300
+ if headers[format_header].present? || req.headers[format_header].blank?
301
+ headers == req.headers
302
+ else
303
+ headers.dup.merge(format_header => req.headers[format_header]) == req.headers
304
+ end
305
+ end
306
+ end
307
+
308
+ class Response
309
+ attr_accessor :body, :message, :code, :headers
310
+
311
+ def initialize(body, message = 200, headers = {})
312
+ @body, @message, @headers = body, message.to_s, headers
313
+ @code = @message[0,3].to_i
314
+
315
+ resp_cls = Net::HTTPResponse::CODE_TO_OBJ[@code.to_s]
316
+ if resp_cls && !resp_cls.body_permitted?
317
+ @body = nil
318
+ end
319
+
320
+ self['Content-Length'] = @body.nil? ? "0" : body.size.to_s
321
+
322
+ end
323
+
324
+ # Returns true if code is 2xx,
325
+ # false otherwise.
326
+ def success?
327
+ code.in?(200..299)
328
+ end
329
+
330
+ def [](key)
331
+ headers[key]
332
+ end
333
+
334
+ def []=(key, value)
335
+ headers[key] = value
336
+ end
337
+
338
+ # Returns true if the other is a Response with an equal body, equal message
339
+ # and equal headers. Otherwise it returns false.
340
+ def ==(other)
341
+ if (other.is_a?(Response))
342
+ other.body == body && other.message == message && other.headers == headers
343
+ else
344
+ false
345
+ end
346
+ end
347
+ end
348
+
349
+ class Connection
350
+ private
351
+ silence_warnings do
352
+ def http
353
+ if unstub_http?
354
+ @http = configure_http(new_http)
355
+ elsif stub_http?
356
+ @http = http_stub
357
+ end
358
+ @http ||= http_stub
359
+ end
360
+
361
+ def http_stub
362
+ HttpMock.new(@site)
363
+ end
364
+
365
+ def unstub_http?
366
+ HttpMock.net_connection_enabled? && defined?(@http) && @http.kind_of?(HttpMock)
367
+ end
368
+
369
+ def stub_http?
370
+ HttpMock.net_connection_disabled? && defined?(@http) && @http.kind_of?(Net::HTTP)
371
+ end
372
+
373
+ end
374
+ end
375
+ end