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,20 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ActiveResource
4
+ module Callbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ CALLBACKS = [
8
+ :before_validation, :after_validation, :before_save, :around_save, :after_save,
9
+ :before_create, :around_create, :after_create, :before_update, :around_update,
10
+ :after_update, :before_destroy, :around_destroy, :after_destroy
11
+ ]
12
+
13
+ included do
14
+ extend ActiveModel::Callbacks
15
+ include ActiveModel::Validations::Callbacks
16
+
17
+ define_model_callbacks :save, :create, :update, :destroy
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,92 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require 'active_support/inflector'
3
+
4
+ module ActiveResource # :nodoc:
5
+ class Collection # :nodoc:
6
+ SELF_DEFINE_METHODS = [:to_a, :collect!, :map!]
7
+ include Enumerable
8
+ delegate :to_yaml, :all?, *(Array.instance_methods(false) - SELF_DEFINE_METHODS), :to => :to_a
9
+
10
+ # The array of actual elements returned by index actions
11
+ attr_accessor :elements, :resource_class, :original_params
12
+
13
+ # ActiveResource::Collection is a wrapper to handle parsing index responses that
14
+ # do not directly map to Rails conventions.
15
+ #
16
+ # You can define a custom class that inherets from ActiveResource::Collection
17
+ # in order to to set the elements instance.
18
+ #
19
+ # GET /posts.json delivers following response body:
20
+ # {
21
+ # posts: [
22
+ # {
23
+ # title: "ActiveResource now has associations",
24
+ # body: "Lorem Ipsum"
25
+ # },
26
+ # {...}
27
+ # ],
28
+ # next_page: "/posts.json?page=2"
29
+ # }
30
+ #
31
+ # A Post class can be setup to handle it with:
32
+ #
33
+ # class Post < ActiveResource::Base
34
+ # self.site = "http://example.com"
35
+ # self.collection_parser = PostCollection
36
+ # end
37
+ #
38
+ # And the collection parser:
39
+ #
40
+ # class PostCollection < ActiveResource::Collection
41
+ # attr_accessor :next_page
42
+ # def initialize(parsed = {})
43
+ # @elements = parsed['posts']
44
+ # @next_page = parsed['next_page']
45
+ # end
46
+ # end
47
+ #
48
+ # The result from a find method that returns multiple entries will now be a
49
+ # PostParser instance. ActiveResource::Collection includes Enumerable and
50
+ # instances can be iterated over just like an array.
51
+ # @posts = Post.find(:all) # => PostCollection:xxx
52
+ # @posts.next_page # => "/posts.json?page=2"
53
+ # @posts.map(&:id) # =>[1, 3, 5 ...]
54
+ #
55
+ # The initialize method will receive the ActiveResource::Formats parsed result
56
+ # and should set @elements.
57
+ def initialize(elements = [])
58
+ @elements = elements
59
+ end
60
+
61
+ def to_a
62
+ elements
63
+ end
64
+
65
+ def collect!
66
+ return elements unless block_given?
67
+ set = []
68
+ each { |o| set << yield(o) }
69
+ @elements = set
70
+ self
71
+ end
72
+ alias map! collect!
73
+
74
+ def first_or_create(attributes = {})
75
+ first || resource_class.create(original_params.update(attributes))
76
+ rescue NoMethodError
77
+ raise "Cannot create resource from resource type: #{resource_class.inspect}"
78
+ end
79
+
80
+ def first_or_initialize(attributes = {})
81
+ first || resource_class.new(original_params.update(attributes))
82
+ rescue NoMethodError
83
+ raise "Cannot build resource from resource type: #{resource_class.inspect}"
84
+ end
85
+
86
+ def where(clauses = {})
87
+ raise ArgumentError, "expected a clauses Hash, got #{clauses.inspect}" unless clauses.is_a? Hash
88
+ new_clauses = original_params.merge(clauses)
89
+ resource_class.where(new_clauses)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,299 @@
1
+ require 'active_support/core_ext/benchmark'
2
+ require 'active_support/core_ext/uri'
3
+ require 'active_support/core_ext/object/inclusion'
4
+ require 'net/https'
5
+ require 'date'
6
+ require 'time'
7
+ require 'uri'
8
+
9
+ module ActiveResource
10
+ # Class to handle connections to remote web services.
11
+ # This class is used by ActiveResource::Base to interface with REST
12
+ # services.
13
+ class Connection
14
+
15
+ HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept',
16
+ :put => 'Content-Type',
17
+ :post => 'Content-Type',
18
+ :patch => 'Content-Type',
19
+ :delete => 'Accept',
20
+ :head => 'Accept'
21
+ }
22
+
23
+ attr_reader :site, :user, :password, :auth_type, :timeout, :open_timeout, :read_timeout, :proxy, :ssl_options
24
+ attr_accessor :format
25
+
26
+ class << self
27
+ def requests
28
+ @@requests ||= []
29
+ end
30
+ end
31
+
32
+ # The +site+ parameter is required and will set the +site+
33
+ # attribute to the URI for the remote resource service.
34
+ def initialize(site, format = ActiveResource::Formats::JsonFormat)
35
+ raise ArgumentError, 'Missing site URI' unless site
36
+ @proxy = @user = @password = nil
37
+ self.site = site
38
+ self.format = format
39
+ end
40
+
41
+ # Set URI for remote service.
42
+ def site=(site)
43
+ @site = site.is_a?(URI) ? site : URI.parse(site)
44
+ @ssl_options ||= {} if @site.is_a?(URI::HTTPS)
45
+ @user = URI.parser.unescape(@site.user) if @site.user
46
+ @password = URI.parser.unescape(@site.password) if @site.password
47
+ end
48
+
49
+ # Set the proxy for remote service.
50
+ def proxy=(proxy)
51
+ @proxy = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
52
+ end
53
+
54
+ # Sets the user for remote service.
55
+ def user=(user)
56
+ @user = user
57
+ end
58
+
59
+ # Sets the password for remote service.
60
+ def password=(password)
61
+ @password = password
62
+ end
63
+
64
+ # Sets the auth type for remote service.
65
+ def auth_type=(auth_type)
66
+ @auth_type = legitimize_auth_type(auth_type)
67
+ end
68
+
69
+ # Sets the number of seconds after which HTTP requests to the remote service should time out.
70
+ def timeout=(timeout)
71
+ @timeout = timeout
72
+ end
73
+
74
+ # Sets the number of seconds after which HTTP connects to the remote service should time out.
75
+ def open_timeout=(timeout)
76
+ @open_timeout = timeout
77
+ end
78
+
79
+ # Sets the number of seconds after which HTTP read requests to the remote service should time out.
80
+ def read_timeout=(timeout)
81
+ @read_timeout = timeout
82
+ end
83
+
84
+ # Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'.
85
+ def ssl_options=(options)
86
+ @ssl_options = options
87
+ end
88
+
89
+ # Executes a GET request.
90
+ # Used to get (find) resources.
91
+ def get(path, headers = {})
92
+ with_auth { request(:get, path, build_request_headers(headers, :get, self.site.merge(path))) }
93
+ end
94
+
95
+ # Executes a DELETE request (see HTTP protocol documentation if unfamiliar).
96
+ # Used to delete resources.
97
+ def delete(path, headers = {})
98
+ with_auth { request(:delete, path, build_request_headers(headers, :delete, self.site.merge(path))) }
99
+ end
100
+
101
+ # Executes a PATCH request (see HTTP protocol documentation if unfamiliar).
102
+ # Used to update resources.
103
+ def patch(path, body = '', headers = {})
104
+ with_auth { request(:patch, path, body.to_s, build_request_headers(headers, :patch, self.site.merge(path))) }
105
+ end
106
+
107
+ # Executes a PUT request (see HTTP protocol documentation if unfamiliar).
108
+ # Used to update resources.
109
+ def put(path, body = '', headers = {})
110
+ with_auth { request(:put, path, body.to_s, build_request_headers(headers, :put, self.site.merge(path))) }
111
+ end
112
+
113
+ # Executes a POST request.
114
+ # Used to create new resources.
115
+ def post(path, body = '', headers = {})
116
+ with_auth { request(:post, path, body.to_s, build_request_headers(headers, :post, self.site.merge(path))) }
117
+ end
118
+
119
+ # Executes a HEAD request.
120
+ # Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
121
+ def head(path, headers = {})
122
+ with_auth { request(:head, path, build_request_headers(headers, :head, self.site.merge(path))) }
123
+ end
124
+
125
+ private
126
+ # Makes a request to the remote service.
127
+ def request(method, path, *arguments)
128
+ result = ActiveSupport::Notifications.instrument("request.active_resource") do |payload|
129
+ payload[:method] = method
130
+ payload[:request_uri] = "#{site.scheme}://#{site.host}:#{site.port}#{path}"
131
+ payload[:result] = http.send(method, path, *arguments)
132
+ end
133
+ handle_response(result)
134
+ rescue Timeout::Error => e
135
+ raise TimeoutError.new(e.message)
136
+ rescue OpenSSL::SSL::SSLError => e
137
+ raise SSLError.new(e.message)
138
+ end
139
+
140
+ # Handles response and error codes from the remote service.
141
+ def handle_response(response)
142
+ case response.code.to_i
143
+ when 301, 302, 303, 307
144
+ raise(Redirection.new(response))
145
+ when 200...400
146
+ response
147
+ when 400
148
+ raise(BadRequest.new(response))
149
+ when 401
150
+ raise(UnauthorizedAccess.new(response))
151
+ when 403
152
+ raise(ForbiddenAccess.new(response))
153
+ when 404
154
+ raise(ResourceNotFound.new(response))
155
+ when 405
156
+ raise(MethodNotAllowed.new(response))
157
+ when 409
158
+ raise(ResourceConflict.new(response))
159
+ when 410
160
+ raise(ResourceGone.new(response))
161
+ when 422
162
+ raise(ResourceInvalid.new(response))
163
+ when 401...500
164
+ raise(ClientError.new(response))
165
+ when 500...600
166
+ raise(ServerError.new(response))
167
+ else
168
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
169
+ end
170
+ end
171
+
172
+ # Creates new Net::HTTP instance for communication with the
173
+ # remote service and resources.
174
+ def http
175
+ configure_http(new_http)
176
+ end
177
+
178
+ def new_http
179
+ if @proxy
180
+ user = URI.parser.unescape(@proxy.user) if @proxy.user
181
+ password = URI.parser.unescape(@proxy.password) if @proxy.password
182
+ Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, user, password)
183
+ else
184
+ Net::HTTP.new(@site.host, @site.port)
185
+ end
186
+ end
187
+
188
+ def configure_http(http)
189
+ apply_ssl_options(http).tap do |https|
190
+ # Net::HTTP timeouts default to 60 seconds.
191
+ if defined? @timeout
192
+ https.open_timeout = @timeout
193
+ https.read_timeout = @timeout
194
+ end
195
+ https.open_timeout = @open_timeout if defined?(@open_timeout)
196
+ https.read_timeout = @read_timeout if defined?(@read_timeout)
197
+ end
198
+ end
199
+
200
+ def apply_ssl_options(http)
201
+ http.tap do |https|
202
+ # Skip config if site is already a https:// URI.
203
+ if defined? @ssl_options
204
+ http.use_ssl = true
205
+
206
+ # All the SSL options have corresponding http settings.
207
+ @ssl_options.each { |key, value| http.send "#{key}=", value }
208
+ end
209
+ end
210
+ end
211
+
212
+ def default_header
213
+ @default_header ||= {}
214
+ end
215
+
216
+ # Builds headers for request to remote service.
217
+ def build_request_headers(headers, http_method, uri)
218
+ authorization_header(http_method, uri).update(default_header).update(http_format_header(http_method)).update(headers)
219
+ end
220
+
221
+ def response_auth_header
222
+ @response_auth_header ||= ""
223
+ end
224
+
225
+ def with_auth
226
+ retried ||= false
227
+ yield
228
+ rescue UnauthorizedAccess => e
229
+ raise if retried || auth_type != :digest
230
+ @response_auth_header = e.response['WWW-Authenticate']
231
+ retried = true
232
+ retry
233
+ end
234
+
235
+ def authorization_header(http_method, uri)
236
+ if @user || @password
237
+ if auth_type == :digest
238
+ { 'Authorization' => digest_auth_header(http_method, uri) }
239
+ else
240
+ { 'Authorization' => 'Basic ' + ["#{@user}:#{@password}"].pack('m').delete("\r\n") }
241
+ end
242
+ else
243
+ {}
244
+ end
245
+ end
246
+
247
+ def digest_auth_header(http_method, uri)
248
+ params = extract_params_from_response
249
+
250
+ request_uri = uri.path
251
+ request_uri << "?#{uri.query}" if uri.query
252
+
253
+ ha1 = Digest::MD5.hexdigest("#{@user}:#{params['realm']}:#{@password}")
254
+ ha2 = Digest::MD5.hexdigest("#{http_method.to_s.upcase}:#{request_uri}")
255
+
256
+ params.merge!('cnonce' => client_nonce)
257
+ request_digest = Digest::MD5.hexdigest([ha1, params['nonce'], "0", params['cnonce'], params['qop'], ha2].join(":"))
258
+ "Digest #{auth_attributes_for(uri, request_digest, params)}"
259
+ end
260
+
261
+ def client_nonce
262
+ Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
263
+ end
264
+
265
+ def extract_params_from_response
266
+ params = {}
267
+ if response_auth_header =~ /^(\w+) (.*)/
268
+ $2.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
269
+ end
270
+ params
271
+ end
272
+
273
+ def auth_attributes_for(uri, request_digest, params)
274
+ auth_attrs =
275
+ [
276
+ %Q(username="#{@user}"),
277
+ %Q(realm="#{params['realm']}"),
278
+ %Q(qop="#{params['qop']}"),
279
+ %Q(uri="#{uri.path}"),
280
+ %Q(nonce="#{params['nonce']}"),
281
+ %Q(nc="0"),
282
+ %Q(cnonce="#{params['cnonce']}"),
283
+ %Q(response="#{request_digest}")]
284
+
285
+ auth_attrs << %Q(opaque="#{params['opaque']}") unless params['opaque'].blank?
286
+ auth_attrs.join(", ")
287
+ end
288
+
289
+ def http_format_header(http_method)
290
+ {HTTP_FORMAT_HEADER_NAMES[http_method] => format.mime_type}
291
+ end
292
+
293
+ def legitimize_auth_type(auth_type)
294
+ return :basic if auth_type.nil?
295
+ auth_type = auth_type.to_sym
296
+ auth_type.in?([:basic, :digest]) ? auth_type : :basic
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,127 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
3
+ module ActiveResource
4
+ # A module to support custom REST methods and sub-resources, allowing you to break out
5
+ # of the "default" REST methods with your own custom resource requests. For example,
6
+ # say you use Rails to expose a REST service and configure your routes with:
7
+ #
8
+ # map.resources :people, :new => { :register => :post },
9
+ # :member => { :promote => :put, :deactivate => :delete }
10
+ # :collection => { :active => :get }
11
+ #
12
+ # This route set creates routes for the following HTTP requests:
13
+ #
14
+ # POST /people/new/register.json # PeopleController.register
15
+ # PATCH/PUT /people/1/promote.json # PeopleController.promote with :id => 1
16
+ # DELETE /people/1/deactivate.json # PeopleController.deactivate with :id => 1
17
+ # GET /people/active.json # PeopleController.active
18
+ #
19
+ # Using this module, Active Resource can use these custom REST methods just like the
20
+ # standard methods.
21
+ #
22
+ # class Person < ActiveResource::Base
23
+ # self.site = "https://37s.sunrise.com"
24
+ # end
25
+ #
26
+ # Person.new(:name => 'Ryan').post(:register) # POST /people/new/register.json
27
+ # # => { :id => 1, :name => 'Ryan' }
28
+ #
29
+ # Person.find(1).put(:promote, :position => 'Manager') # PUT /people/1/promote.json
30
+ # Person.find(1).delete(:deactivate) # DELETE /people/1/deactivate.json
31
+ #
32
+ # Person.get(:active) # GET /people/active.json
33
+ # # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
34
+ #
35
+ module CustomMethods
36
+ extend ActiveSupport::Concern
37
+
38
+ included do
39
+ class << self
40
+ alias :orig_delete :delete
41
+
42
+ # Invokes a GET to a given custom REST method. For example:
43
+ #
44
+ # Person.get(:active) # GET /people/active.json
45
+ # # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
46
+ #
47
+ # Person.get(:active, :awesome => true) # GET /people/active.json?awesome=true
48
+ # # => [{:id => 1, :name => 'Ryan'}]
49
+ #
50
+ # Note: the objects returned from this method are not automatically converted
51
+ # into ActiveResource::Base instances - they are ordinary Hashes. If you are expecting
52
+ # ActiveResource::Base instances, use the <tt>find</tt> class method with the
53
+ # <tt>:from</tt> option. For example:
54
+ #
55
+ # Person.find(:all, :from => :active)
56
+ def get(custom_method_name, options = {})
57
+ hashified = format.decode(connection.get(custom_method_collection_url(custom_method_name, options), headers).body)
58
+ derooted = Formats.remove_root(hashified)
59
+ derooted.is_a?(Array) ? derooted.map { |e| Formats.remove_root(e) } : derooted
60
+ end
61
+
62
+ def post(custom_method_name, options = {}, body = '')
63
+ connection.post(custom_method_collection_url(custom_method_name, options), body, headers)
64
+ end
65
+
66
+ def patch(custom_method_name, options = {}, body = '')
67
+ connection.patch(custom_method_collection_url(custom_method_name, options), body, headers)
68
+ end
69
+
70
+ def put(custom_method_name, options = {}, body = '')
71
+ connection.put(custom_method_collection_url(custom_method_name, options), body, headers)
72
+ end
73
+
74
+ def delete(custom_method_name, options = {})
75
+ # Need to jump through some hoops to retain the original class 'delete' method
76
+ if custom_method_name.is_a?(Symbol)
77
+ connection.delete(custom_method_collection_url(custom_method_name, options), headers)
78
+ else
79
+ orig_delete(custom_method_name, options)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ module ClassMethods
86
+ def custom_method_collection_url(method_name, options = {})
87
+ prefix_options, query_options = split_options(options)
88
+ "#{prefix(prefix_options)}#{collection_name}/#{method_name}#{format_extension}#{query_string(query_options)}"
89
+ end
90
+ end
91
+
92
+ def get(method_name, options = {})
93
+ self.class.format.decode(connection.get(custom_method_element_url(method_name, options), self.class.headers).body)
94
+ end
95
+
96
+ def post(method_name, options = {}, body = nil)
97
+ request_body = body.blank? ? encode : body
98
+ if new?
99
+ connection.post(custom_method_new_element_url(method_name, options), request_body, self.class.headers)
100
+ else
101
+ connection.post(custom_method_element_url(method_name, options), request_body, self.class.headers)
102
+ end
103
+ end
104
+
105
+ def patch(method_name, options = {}, body = '')
106
+ connection.patch(custom_method_element_url(method_name, options), body, self.class.headers)
107
+ end
108
+
109
+ def put(method_name, options = {}, body = '')
110
+ connection.put(custom_method_element_url(method_name, options), body, self.class.headers)
111
+ end
112
+
113
+ def delete(method_name, options = {})
114
+ connection.delete(custom_method_element_url(method_name, options), self.class.headers)
115
+ end
116
+
117
+
118
+ private
119
+ def custom_method_element_url(method_name, options = {})
120
+ "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}#{self.class.format_extension}#{self.class.__send__(:query_string, options)}"
121
+ end
122
+
123
+ def custom_method_new_element_url(method_name, options = {})
124
+ "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}#{self.class.format_extension}#{self.class.__send__(:query_string, options)}"
125
+ end
126
+ end
127
+ end