activeresource 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activeresource might be problematic. Click here for more details.
- data/CHANGELOG +216 -0
- data/README +165 -0
- data/Rakefile +133 -0
- data/lib/active_resource.rb +47 -0
- data/lib/active_resource/base.rb +872 -0
- data/lib/active_resource/connection.rb +157 -0
- data/lib/active_resource/custom_methods.rb +105 -0
- data/lib/active_resource/formats.rb +14 -0
- data/lib/active_resource/formats/json_format.rb +23 -0
- data/lib/active_resource/formats/xml_format.rb +34 -0
- data/lib/active_resource/http_mock.rb +136 -0
- data/lib/active_resource/validations.rb +288 -0
- data/lib/active_resource/version.rb +9 -0
- data/lib/activeresource.rb +1 -0
- data/test/abstract_unit.rb +10 -0
- data/test/authorization_test.rb +82 -0
- data/test/base/custom_methods_test.rb +96 -0
- data/test/base/equality_test.rb +43 -0
- data/test/base/load_test.rb +111 -0
- data/test/base_errors_test.rb +48 -0
- data/test/base_test.rb +454 -0
- data/test/base_test.rb.rej +17 -0
- data/test/connection_test.rb +161 -0
- data/test/debug.log +5477 -0
- data/test/fixtures/beast.rb +14 -0
- data/test/fixtures/person.rb +3 -0
- data/test/fixtures/street_address.rb +4 -0
- data/test/format_test.rb +42 -0
- data/test/setter_trap.rb +27 -0
- metadata +87 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'date'
|
3
|
+
require 'time'
|
4
|
+
require 'uri'
|
5
|
+
require 'benchmark'
|
6
|
+
|
7
|
+
module ActiveResource
|
8
|
+
class ConnectionError < StandardError # :nodoc:
|
9
|
+
attr_reader :response
|
10
|
+
|
11
|
+
def initialize(response, message = nil)
|
12
|
+
@response = response
|
13
|
+
@message = message
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# 3xx Redirection
|
22
|
+
class Redirection < ConnectionError # :nodoc:
|
23
|
+
def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
|
24
|
+
end
|
25
|
+
|
26
|
+
# 4xx Client Error
|
27
|
+
class ClientError < ConnectionError; end # :nodoc:
|
28
|
+
|
29
|
+
# 404 Not Found
|
30
|
+
class ResourceNotFound < ClientError; end # :nodoc:
|
31
|
+
|
32
|
+
# 409 Conflict
|
33
|
+
class ResourceConflict < ClientError; end # :nodoc:
|
34
|
+
|
35
|
+
# 5xx Server Error
|
36
|
+
class ServerError < ConnectionError; end # :nodoc:
|
37
|
+
|
38
|
+
# 405 Method Not Allowed
|
39
|
+
class MethodNotAllowed < ClientError # :nodoc:
|
40
|
+
def allowed_methods
|
41
|
+
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Class to handle connections to remote web services.
|
46
|
+
# This class is used by ActiveResource::Base to interface with REST
|
47
|
+
# services.
|
48
|
+
class Connection
|
49
|
+
attr_reader :site
|
50
|
+
attr_accessor :format
|
51
|
+
|
52
|
+
class << self
|
53
|
+
def requests
|
54
|
+
@@requests ||= []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# The +site+ parameter is required and will set the +site+
|
59
|
+
# attribute to the URI for the remote resource service.
|
60
|
+
def initialize(site, format = ActiveResource::Formats[:xml])
|
61
|
+
raise ArgumentError, 'Missing site URI' unless site
|
62
|
+
self.site = site
|
63
|
+
self.format = format
|
64
|
+
end
|
65
|
+
|
66
|
+
# Set URI for remote service.
|
67
|
+
def site=(site)
|
68
|
+
@site = site.is_a?(URI) ? site : URI.parse(site)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Execute a GET request.
|
72
|
+
# Used to get (find) resources.
|
73
|
+
def get(path, headers = {})
|
74
|
+
format.decode(request(:get, path, build_request_headers(headers)).body)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
|
78
|
+
# Used to delete resources.
|
79
|
+
def delete(path, headers = {})
|
80
|
+
request(:delete, path, build_request_headers(headers))
|
81
|
+
end
|
82
|
+
|
83
|
+
# Execute a PUT request (see HTTP protocol documentation if unfamiliar).
|
84
|
+
# Used to update resources.
|
85
|
+
def put(path, body = '', headers = {})
|
86
|
+
request(:put, path, body.to_s, build_request_headers(headers))
|
87
|
+
end
|
88
|
+
|
89
|
+
# Execute a POST request.
|
90
|
+
# Used to create new resources.
|
91
|
+
def post(path, body = '', headers = {})
|
92
|
+
request(:post, path, body.to_s, build_request_headers(headers))
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
private
|
97
|
+
# Makes request to remote service.
|
98
|
+
def request(method, path, *arguments)
|
99
|
+
logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
|
100
|
+
result = nil
|
101
|
+
time = Benchmark.realtime { result = http.send(method, path, *arguments) }
|
102
|
+
logger.info "--> #{result.code} #{result.message} (#{result.body.length}b %.2fs)" % time if logger
|
103
|
+
handle_response(result)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Handles response and error codes from remote service.
|
107
|
+
def handle_response(response)
|
108
|
+
case response.code.to_i
|
109
|
+
when 301,302
|
110
|
+
raise(Redirection.new(response))
|
111
|
+
when 200...400
|
112
|
+
response
|
113
|
+
when 404
|
114
|
+
raise(ResourceNotFound.new(response))
|
115
|
+
when 405
|
116
|
+
raise(MethodNotAllowed.new(response))
|
117
|
+
when 409
|
118
|
+
raise(ResourceConflict.new(response))
|
119
|
+
when 422
|
120
|
+
raise(ResourceInvalid.new(response))
|
121
|
+
when 401...500
|
122
|
+
raise(ClientError.new(response))
|
123
|
+
when 500...600
|
124
|
+
raise(ServerError.new(response))
|
125
|
+
else
|
126
|
+
raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Creates new Net::HTTP instance for communication with
|
131
|
+
# remote service and resources.
|
132
|
+
def http
|
133
|
+
http = Net::HTTP.new(@site.host, @site.port)
|
134
|
+
http.use_ssl = @site.is_a?(URI::HTTPS)
|
135
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
|
136
|
+
http
|
137
|
+
end
|
138
|
+
|
139
|
+
def default_header
|
140
|
+
@default_header ||= { 'Content-Type' => format.mime_type }
|
141
|
+
end
|
142
|
+
|
143
|
+
# Builds headers for request to remote service.
|
144
|
+
def build_request_headers(headers)
|
145
|
+
authorization_header.update(default_header).update(headers)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Sets authorization header; authentication information is pulled from credentials provided with site URI.
|
149
|
+
def authorization_header
|
150
|
+
(@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {})
|
151
|
+
end
|
152
|
+
|
153
|
+
def logger #:nodoc:
|
154
|
+
ActiveResource::Base.logger
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# A module to support custom REST methods and sub-resources, allowing you to break out
|
2
|
+
# of the "default" REST methods with your own custom resource requests. For example,
|
3
|
+
# say you use Rails to expose a REST service and configure your routes with:
|
4
|
+
#
|
5
|
+
# map.resources :people, :new => { :register => :post },
|
6
|
+
# :element => { :promote => :put, :deactivate => :delete }
|
7
|
+
# :collection => { :active => :get }
|
8
|
+
#
|
9
|
+
# This route set creates routes for the following http requests:
|
10
|
+
#
|
11
|
+
# POST /people/new/register.xml #=> PeopleController.register
|
12
|
+
# PUT /people/1/promote.xml #=> PeopleController.promote with :id => 1
|
13
|
+
# DELETE /people/1/deactivate.xml #=> PeopleController.deactivate with :id => 1
|
14
|
+
# GET /people/active.xml #=> PeopleController.active
|
15
|
+
#
|
16
|
+
# Using this module, Active Resource can use these custom REST methods just like the
|
17
|
+
# standard methods.
|
18
|
+
#
|
19
|
+
# class Person < ActiveResource::Base
|
20
|
+
# self.site = "http://37s.sunrise.i:3000"
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# Person.new(:name => 'Ryan).post(:register) # POST /people/new/register.xml
|
24
|
+
# # => { :id => 1, :name => 'Ryan' }
|
25
|
+
#
|
26
|
+
# Person.find(1).put(:promote, :position => 'Manager') # PUT /people/1/promote.xml
|
27
|
+
# Person.find(1).delete(:deactivate) # DELETE /people/1/deactivate.xml
|
28
|
+
#
|
29
|
+
# Person.get(:active) # GET /people/active.xml
|
30
|
+
# # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
|
31
|
+
#
|
32
|
+
module ActiveResource
|
33
|
+
module CustomMethods
|
34
|
+
def self.included(within)
|
35
|
+
within.class_eval do
|
36
|
+
extend ActiveResource::CustomMethods::ClassMethods
|
37
|
+
include ActiveResource::CustomMethods::InstanceMethods
|
38
|
+
|
39
|
+
class << self
|
40
|
+
alias :orig_delete :delete
|
41
|
+
|
42
|
+
def get(method_name, options = {})
|
43
|
+
connection.get(custom_method_collection_url(method_name, options), headers)
|
44
|
+
end
|
45
|
+
|
46
|
+
def post(method_name, options = {}, body = '')
|
47
|
+
connection.post(custom_method_collection_url(method_name, options), body, headers)
|
48
|
+
end
|
49
|
+
|
50
|
+
def put(method_name, options = {}, body = '')
|
51
|
+
connection.put(custom_method_collection_url(method_name, options), body, headers)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Need to jump through some hoops to retain the original class 'delete' method
|
55
|
+
def delete(custom_method_name, options = {})
|
56
|
+
if (custom_method_name.is_a?(Symbol))
|
57
|
+
connection.delete(custom_method_collection_url(custom_method_name, options), headers)
|
58
|
+
else
|
59
|
+
orig_delete(custom_method_name, options)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module ClassMethods
|
67
|
+
def custom_method_collection_url(method_name, options = {})
|
68
|
+
prefix_options, query_options = split_options(options)
|
69
|
+
"#{prefix(prefix_options)}#{collection_name}/#{method_name}.xml#{query_string(query_options)}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
module InstanceMethods
|
74
|
+
def get(method_name, options = {})
|
75
|
+
connection.get(custom_method_element_url(method_name, options), self.class.headers)
|
76
|
+
end
|
77
|
+
|
78
|
+
def post(method_name, options = {}, body = '')
|
79
|
+
if new?
|
80
|
+
connection.post(custom_method_new_element_url(method_name, options), (body.nil? ? to_xml : body), self.class.headers)
|
81
|
+
else
|
82
|
+
connection.post(custom_method_element_url(method_name, options), body, self.class.headers)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def put(method_name, options = {}, body = '')
|
87
|
+
connection.put(custom_method_element_url(method_name, options), body, self.class.headers)
|
88
|
+
end
|
89
|
+
|
90
|
+
def delete(method_name, options = {})
|
91
|
+
connection.delete(custom_method_element_url(method_name, options), self.class.headers)
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
private
|
96
|
+
def custom_method_element_url(method_name, options = {})
|
97
|
+
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}.xml#{self.class.send!(:query_string, options)}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def custom_method_new_element_url(method_name, options = {})
|
101
|
+
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}.xml#{self.class.send!(:query_string, options)}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ActiveResource
|
2
|
+
module Formats
|
3
|
+
# Lookup the format class from a mime type reference symbol. Example:
|
4
|
+
#
|
5
|
+
# ActiveResource::Formats[:xml] # => ActiveResource::Formats::XmlFormat
|
6
|
+
# ActiveResource::Formats[:json] # => ActiveResource::Formats::JsonFormat
|
7
|
+
def self.[](mime_type_reference)
|
8
|
+
ActiveResource::Formats.const_get(mime_type_reference.to_s.camelize + "Format")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'active_resource/formats/xml_format'
|
14
|
+
require 'active_resource/formats/json_format'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveResource
|
2
|
+
module Formats
|
3
|
+
module JsonFormat
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def extension
|
7
|
+
"json"
|
8
|
+
end
|
9
|
+
|
10
|
+
def mime_type
|
11
|
+
"application/json"
|
12
|
+
end
|
13
|
+
|
14
|
+
def encode(hash)
|
15
|
+
hash.to_json
|
16
|
+
end
|
17
|
+
|
18
|
+
def decode(json)
|
19
|
+
ActiveSupport::JSON.decode(json)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActiveResource
|
2
|
+
module Formats
|
3
|
+
module XmlFormat
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def extension
|
7
|
+
"xml"
|
8
|
+
end
|
9
|
+
|
10
|
+
def mime_type
|
11
|
+
"application/xml"
|
12
|
+
end
|
13
|
+
|
14
|
+
def encode(hash)
|
15
|
+
hash.to_xml
|
16
|
+
end
|
17
|
+
|
18
|
+
def decode(xml)
|
19
|
+
from_xml_data(Hash.from_xml(xml))
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
# Manipulate from_xml Hash, because xml_simple is not exactly what we
|
24
|
+
# want for ActiveResource.
|
25
|
+
def from_xml_data(data)
|
26
|
+
if data.is_a?(Hash) && data.keys.size == 1
|
27
|
+
data.values.first
|
28
|
+
else
|
29
|
+
data
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'active_resource/connection'
|
2
|
+
|
3
|
+
module ActiveResource
|
4
|
+
class InvalidRequestError < StandardError; end #:nodoc:
|
5
|
+
|
6
|
+
class HttpMock
|
7
|
+
class Responder
|
8
|
+
def initialize(responses)
|
9
|
+
@responses = responses
|
10
|
+
end
|
11
|
+
|
12
|
+
for method in [ :post, :put, :get, :delete ]
|
13
|
+
module_eval <<-EOE
|
14
|
+
def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
15
|
+
@responses[Request.new(:#{method}, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
|
16
|
+
end
|
17
|
+
EOE
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def requests
|
23
|
+
@@requests ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
def responses
|
27
|
+
@@responses ||= {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def respond_to(pairs = {})
|
31
|
+
reset!
|
32
|
+
pairs.each do |(path, response)|
|
33
|
+
responses[path] = response
|
34
|
+
end
|
35
|
+
|
36
|
+
if block_given?
|
37
|
+
yield Responder.new(responses)
|
38
|
+
else
|
39
|
+
Responder.new(responses)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def reset!
|
44
|
+
requests.clear
|
45
|
+
responses.clear
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
for method in [ :post, :put ]
|
50
|
+
module_eval <<-EOE
|
51
|
+
def #{method}(path, body, headers)
|
52
|
+
request = ActiveResource::Request.new(:#{method}, path, body, headers)
|
53
|
+
self.class.requests << request
|
54
|
+
self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for: \#{request.inspect}"))
|
55
|
+
end
|
56
|
+
EOE
|
57
|
+
end
|
58
|
+
|
59
|
+
for method in [ :get, :delete ]
|
60
|
+
module_eval <<-EOE
|
61
|
+
def #{method}(path, headers)
|
62
|
+
request = ActiveResource::Request.new(:#{method}, path, nil, headers)
|
63
|
+
self.class.requests << request
|
64
|
+
self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for: \#{request.inspect}"))
|
65
|
+
end
|
66
|
+
EOE
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(site)
|
70
|
+
@site = site
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Request
|
75
|
+
attr_accessor :path, :method, :body, :headers
|
76
|
+
|
77
|
+
def initialize(method, path, body = nil, headers = {})
|
78
|
+
@method, @path, @body, @headers = method, path, body, headers.dup
|
79
|
+
@headers.update('Content-Type' => 'application/xml')
|
80
|
+
end
|
81
|
+
|
82
|
+
def ==(other_request)
|
83
|
+
other_request.hash == hash
|
84
|
+
end
|
85
|
+
|
86
|
+
def eql?(other_request)
|
87
|
+
self == other_request
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_s
|
91
|
+
"<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
|
92
|
+
end
|
93
|
+
|
94
|
+
def hash
|
95
|
+
"#{path}#{method}#{headers}".hash
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class Response
|
100
|
+
attr_accessor :body, :message, :code, :headers
|
101
|
+
|
102
|
+
def initialize(body, message = 200, headers = {})
|
103
|
+
@body, @message, @headers = body, message.to_s, headers
|
104
|
+
@code = @message[0,3].to_i
|
105
|
+
end
|
106
|
+
|
107
|
+
def success?
|
108
|
+
(200..299).include?(code)
|
109
|
+
end
|
110
|
+
|
111
|
+
def [](key)
|
112
|
+
headers[key]
|
113
|
+
end
|
114
|
+
|
115
|
+
def []=(key, value)
|
116
|
+
headers[key] = value
|
117
|
+
end
|
118
|
+
|
119
|
+
def ==(other)
|
120
|
+
if (other.is_a?(Response))
|
121
|
+
other.body == body && other.message == message && other.headers == headers
|
122
|
+
else
|
123
|
+
false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class Connection
|
129
|
+
private
|
130
|
+
silence_warnings do
|
131
|
+
def http
|
132
|
+
@http ||= HttpMock.new(@site)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|