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.

@@ -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