api_resource 0.4.1 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +37 -0
- data/Gemfile.lock +190 -0
- data/Guardfile +27 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/api_resource.gemspec +111 -0
- data/coverage/assets/0.5.3/app.js +88 -0
- data/coverage/assets/0.5.3/fancybox/blank.gif +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_close.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_loading.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_nav_left.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_nav_right.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_e.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_n.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_ne.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_nw.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_s.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_se.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_sw.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_w.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_left.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_main.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_over.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_right.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancybox-x.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancybox-y.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancybox.png +0 -0
- data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.css +363 -0
- data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.pack.js +44 -0
- data/coverage/assets/0.5.3/favicon_green.png +0 -0
- data/coverage/assets/0.5.3/favicon_red.png +0 -0
- data/coverage/assets/0.5.3/favicon_yellow.png +0 -0
- data/coverage/assets/0.5.3/highlight.css +129 -0
- data/coverage/assets/0.5.3/highlight.pack.js +1 -0
- data/coverage/assets/0.5.3/jquery-1.6.2.min.js +18 -0
- data/coverage/assets/0.5.3/jquery.dataTables.min.js +152 -0
- data/coverage/assets/0.5.3/jquery.timeago.js +141 -0
- data/coverage/assets/0.5.3/jquery.url.js +174 -0
- data/coverage/assets/0.5.3/loading.gif +0 -0
- data/coverage/assets/0.5.3/magnify.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/jquery-ui-1.8.4.custom.css +295 -0
- data/coverage/assets/0.5.3/stylesheet.css +383 -0
- data/coverage/index.html +3573 -0
- data/lib/api_resource.rb +130 -0
- data/lib/api_resource/association_activation.rb +19 -0
- data/lib/api_resource/associations.rb +218 -0
- data/lib/api_resource/associations/association_proxy.rb +116 -0
- data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +16 -0
- data/lib/api_resource/associations/dynamic_resource_scope.rb +23 -0
- data/lib/api_resource/associations/generic_scope.rb +68 -0
- data/lib/api_resource/associations/has_many_remote_object_proxy.rb +16 -0
- data/lib/api_resource/associations/has_many_through_remote_object_proxy.rb +13 -0
- data/lib/api_resource/associations/has_one_remote_object_proxy.rb +24 -0
- data/lib/api_resource/associations/multi_argument_resource_scope.rb +15 -0
- data/lib/api_resource/associations/multi_object_proxy.rb +84 -0
- data/lib/api_resource/associations/related_object_hash.rb +12 -0
- data/lib/api_resource/associations/relation_scope.rb +25 -0
- data/lib/api_resource/associations/resource_scope.rb +32 -0
- data/lib/api_resource/associations/scope.rb +132 -0
- data/lib/api_resource/associations/single_object_proxy.rb +82 -0
- data/lib/api_resource/attributes.rb +243 -0
- data/lib/api_resource/base.rb +717 -0
- data/lib/api_resource/callbacks.rb +45 -0
- data/lib/api_resource/connection.rb +195 -0
- data/lib/api_resource/core_extensions.rb +7 -0
- data/lib/api_resource/custom_methods.rb +117 -0
- data/lib/api_resource/decorators.rb +6 -0
- data/lib/api_resource/decorators/caching_decorator.rb +20 -0
- data/lib/api_resource/exceptions.rb +99 -0
- data/lib/api_resource/formats.rb +22 -0
- data/lib/api_resource/formats/json_format.rb +25 -0
- data/lib/api_resource/formats/xml_format.rb +36 -0
- data/lib/api_resource/local.rb +12 -0
- data/lib/api_resource/log_subscriber.rb +15 -0
- data/lib/api_resource/mocks.rb +285 -0
- data/lib/api_resource/model_errors.rb +82 -0
- data/lib/api_resource/observing.rb +27 -0
- data/lib/api_resource/railtie.rb +24 -0
- data/lib/api_resource/scopes.rb +48 -0
- data/nohup.out +63 -0
- data/spec/lib/api_resource_spec.rb +43 -0
- data/spec/lib/associations_spec.rb +751 -0
- data/spec/lib/attributes_spec.rb +191 -0
- data/spec/lib/base_spec.rb +655 -0
- data/spec/lib/callbacks_spec.rb +68 -0
- data/spec/lib/connection_spec.rb +137 -0
- data/spec/lib/local_spec.rb +20 -0
- data/spec/lib/mocks_spec.rb +74 -0
- data/spec/lib/model_errors_spec.rb +29 -0
- data/spec/lib/prefixes_spec.rb +107 -0
- data/spec/spec_helper.rb +82 -0
- data/spec/support/mocks/association_mocks.rb +63 -0
- data/spec/support/mocks/error_resource_mocks.rb +21 -0
- data/spec/support/mocks/prefix_model_mocks.rb +5 -0
- data/spec/support/mocks/test_resource_mocks.rb +44 -0
- data/spec/support/requests/association_requests.rb +31 -0
- data/spec/support/requests/error_resource_requests.rb +25 -0
- data/spec/support/requests/prefix_model_requests.rb +7 -0
- data/spec/support/requests/test_resource_requests.rb +38 -0
- data/spec/support/test_resource.rb +72 -0
- data/spec/tmp/DIR +0 -0
- data/spec/tmp/api_resource_test_db.sqlite +0 -0
- metadata +119 -3
@@ -0,0 +1,45 @@
|
|
1
|
+
module ApiResource
|
2
|
+
|
3
|
+
module Callbacks
|
4
|
+
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
|
9
|
+
extend ActiveModel::Callbacks
|
10
|
+
|
11
|
+
define_model_callbacks :save, :create, :update, :destroy
|
12
|
+
|
13
|
+
[:save, :create, :update, :destroy].each do |action|
|
14
|
+
alias_method_chain action, :callbacks
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def save_with_callbacks(*args)
|
20
|
+
run_callbacks :save do
|
21
|
+
save_without_callbacks(*args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_with_callbacks(*args)
|
26
|
+
run_callbacks :create do
|
27
|
+
create_without_callbacks(*args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def update_with_callbacks(*args)
|
32
|
+
run_callbacks :update do
|
33
|
+
update_without_callbacks(*args)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def destroy_with_callbacks(*args)
|
38
|
+
run_callbacks :destroy do
|
39
|
+
destroy_without_callbacks(*args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'active_support/core_ext/benchmark'
|
2
|
+
require 'rest_client'
|
3
|
+
require 'net/https'
|
4
|
+
require 'date'
|
5
|
+
require 'time'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
module ApiResource
|
9
|
+
# Class to handle connections to remote web services.
|
10
|
+
# This class is used by ActiveResource::Base to interface with REST
|
11
|
+
# services.
|
12
|
+
class Connection
|
13
|
+
|
14
|
+
HTTP_FORMAT_HEADER_NAMES = {
|
15
|
+
:get => 'Accept',
|
16
|
+
:put => 'Content-Type',
|
17
|
+
:post => 'Content-Type',
|
18
|
+
:delete => 'Accept',
|
19
|
+
:head => 'Accept'
|
20
|
+
}
|
21
|
+
|
22
|
+
attr_reader :site, :user, :password, :auth_type, :timeout, :proxy, :ssl_options
|
23
|
+
attr_accessor :format, :headers
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def requests
|
27
|
+
@@requests ||= []
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# The +site+ parameter is required and will set the +site+
|
32
|
+
# attribute to the URI for the remote resource service.
|
33
|
+
def initialize(site, format = ApiResource::Formats::JsonFormat, headers)
|
34
|
+
raise ArgumentError, 'Missing site URI' unless site
|
35
|
+
@user = @password = nil
|
36
|
+
@uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
37
|
+
self.site = site
|
38
|
+
self.format = format
|
39
|
+
self.headers = headers
|
40
|
+
end
|
41
|
+
|
42
|
+
# Set URI for remote service.
|
43
|
+
def site=(site)
|
44
|
+
@site = site.is_a?(URI) ? site : @uri_parser.parse(site)
|
45
|
+
@user = @uri_parser.unescape(@site.user) if @site.user
|
46
|
+
@password = @uri_parser.unescape(@site.password) if @site.password
|
47
|
+
end
|
48
|
+
|
49
|
+
# Sets the number of seconds after which HTTP requests to the remote service should time out.
|
50
|
+
def timeout=(timeout)
|
51
|
+
@timeout = timeout
|
52
|
+
end
|
53
|
+
|
54
|
+
def get(path, headers = self.headers)
|
55
|
+
# our site and headers for this request
|
56
|
+
site = self.site.merge(path)
|
57
|
+
headers = build_request_headers(headers, :get, site)
|
58
|
+
|
59
|
+
self.with_caching(path, headers) do
|
60
|
+
format.decode(request(:get, path, headers))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def delete(path, headers = self.headers)
|
65
|
+
request(:delete, path, build_request_headers(headers, :delete, self.site.merge(path)))
|
66
|
+
return true
|
67
|
+
end
|
68
|
+
|
69
|
+
def head(path, headers = self.headers)
|
70
|
+
request(:head, path, build_request_headers(headers, :head, self.site.merge(path)))
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def put(path, body = {}, headers = self.headers)
|
75
|
+
# If there's a file to send then we can't use JSON or XML
|
76
|
+
if !body.is_a?(String) && RestClient::Payload.has_file?(body)
|
77
|
+
format.decode(request(:put, path, body, build_request_headers(headers, :put, self.site.merge(path))))
|
78
|
+
else
|
79
|
+
format.decode(request(:put, path, body, build_request_headers(headers, :put, self.site.merge(path))))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def post(path, body = {}, headers = self.headers)
|
84
|
+
if !body.is_a?(String) && RestClient::Payload.has_file?(body)
|
85
|
+
format.decode(request(:post, path, body, build_request_headers(headers, :post, self.site.merge(path))))
|
86
|
+
else
|
87
|
+
format.decode(request(:post, path, body, build_request_headers(headers, :post, self.site.merge(path))))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
def cache_key(path, headers)
|
94
|
+
key = Digest::MD5.hexdigest([path, headers].to_s)
|
95
|
+
return "a-#{key}-#{ApiResource::Base.ttl}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def with_caching(path, data = {}, &block)
|
99
|
+
if ApiResource::Base.ttl.to_f > 0.0
|
100
|
+
key = self.cache_key(path, data)
|
101
|
+
ApiResource.cache.fetch(key, :expires_in => ApiResource::Base.ttl) do
|
102
|
+
yield
|
103
|
+
end
|
104
|
+
else
|
105
|
+
yield
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
# Makes a request to the remote service.
|
111
|
+
def request(method, path, *arguments)
|
112
|
+
handle_response(path) do
|
113
|
+
ActiveSupport::Notifications.instrument("request.api_resource") do |payload|
|
114
|
+
|
115
|
+
# debug logging
|
116
|
+
ApiResource.logger.debug("#{method.to_s.upcase} #{path}")
|
117
|
+
|
118
|
+
payload[:method] = method
|
119
|
+
payload[:request_uri] = "#{site.scheme}://#{site.host}:#{site.port}#{path}"
|
120
|
+
payload[:result] = http(path).send(method, *arguments)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Handles response and error codes from the remote service.
|
126
|
+
def handle_response(path, &block)
|
127
|
+
begin
|
128
|
+
result = yield
|
129
|
+
rescue RestClient::RequestTimeout
|
130
|
+
raise ApiResource::RequestTimeout.new("Request Time Out - Accessing #{path}}")
|
131
|
+
rescue Exception => error
|
132
|
+
if error.respond_to?(:http_code)
|
133
|
+
ApiResource.logger.error("#{self} accessing #{path}")
|
134
|
+
ApiResource.logger.error(error.message)
|
135
|
+
result = error.response
|
136
|
+
else
|
137
|
+
raise ApiResource::ConnectionError.new(nil, :message => "Unknown error #{error}")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
return propogate_response_or_error(result, result.code)
|
141
|
+
end
|
142
|
+
|
143
|
+
def propogate_response_or_error(response, code)
|
144
|
+
case code.to_i
|
145
|
+
when 301,302
|
146
|
+
raise ApiResource::Redirection.new(response)
|
147
|
+
when 200..400
|
148
|
+
response.body
|
149
|
+
when 400
|
150
|
+
raise ApiResource::BadRequest.new(response)
|
151
|
+
when 401
|
152
|
+
raise ApiResource::UnauthorizedAccess.new(response)
|
153
|
+
when 403
|
154
|
+
raise ApiResource::ForbiddenAccess.new(response)
|
155
|
+
when 404
|
156
|
+
raise ApiResource::ResourceNotFound.new(response)
|
157
|
+
when 405
|
158
|
+
raise ApiResource::MethodNotAllowed.new(response)
|
159
|
+
when 406
|
160
|
+
raise ApiResource::NotAccepatable.new(response)
|
161
|
+
when 409
|
162
|
+
raise ApiResource::ResourceNotFound.new(response)
|
163
|
+
when 410
|
164
|
+
raise ApiResource::ResourceGone.new(response)
|
165
|
+
when 422
|
166
|
+
raise ApiResource::UnprocessableEntity.new(response)
|
167
|
+
when 401..499
|
168
|
+
raise ApiResource::ClientError.new(response)
|
169
|
+
when 500..600
|
170
|
+
raise ApiResource::ServerError.new(response)
|
171
|
+
else
|
172
|
+
raise ApiResource::ConnectionError.new(response, :message => "Unknown response code: #{code}")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Creates new Net::HTTP instance for communication with the
|
177
|
+
# remote service and resources.
|
178
|
+
def http(path)
|
179
|
+
unless path =~ /\./
|
180
|
+
path += ".#{self.format.extension}"
|
181
|
+
end
|
182
|
+
RestClient::Resource.new("#{site.scheme}://#{site.host}:#{site.port}#{path}", {:timeout => ApiResource::Base.timeout, :open_timeout => ApiResource::Base.open_timeout})
|
183
|
+
end
|
184
|
+
|
185
|
+
def build_request_headers(headers, verb, uri)
|
186
|
+
http_format_header(verb).update(headers)
|
187
|
+
end
|
188
|
+
|
189
|
+
def http_format_header(verb)
|
190
|
+
{}.tap do |ret|
|
191
|
+
ret[HTTP_FORMAT_HEADER_NAMES[verb]] = format.mime_type
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
2
|
+
|
3
|
+
module ApiResource
|
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.xml # PeopleController.register
|
15
|
+
# PUT /people/1/promote.xml # PeopleController.promote with :id => 1
|
16
|
+
# DELETE /people/1/deactivate.xml # PeopleController.deactivate with :id => 1
|
17
|
+
# GET /people/active.xml # 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 = "http://37s.sunrise.i:3000"
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# Person.new(:name => 'Ryan).post(:register) # POST /people/new/register.xml
|
27
|
+
# # => { :id => 1, :name => 'Ryan' }
|
28
|
+
#
|
29
|
+
# Person.find(1).put(:promote, :position => 'Manager') # PUT /people/1/promote.xml
|
30
|
+
# Person.find(1).delete(:deactivate) # DELETE /people/1/deactivate.xml
|
31
|
+
#
|
32
|
+
# Person.get(:active) # GET /people/active.xml
|
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.xml
|
45
|
+
# # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
|
46
|
+
#
|
47
|
+
# Person.get(:active, :awesome => true) # GET /people/active.xml?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
|
+
connection.get(custom_method_collection_url(custom_method_name, options), headers)
|
58
|
+
end
|
59
|
+
|
60
|
+
def post(custom_method_name, options = {}, body = '')
|
61
|
+
connection.post(custom_method_collection_url(custom_method_name, options), body, headers)
|
62
|
+
end
|
63
|
+
|
64
|
+
def put(custom_method_name, options = {}, body = '')
|
65
|
+
connection.put(custom_method_collection_url(custom_method_name, options), body, headers)
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete(custom_method_name, options = {})
|
69
|
+
# Need to jump through some hoops to retain the original class 'delete' method
|
70
|
+
if custom_method_name.is_a?(Symbol)
|
71
|
+
connection.delete(custom_method_collection_url(custom_method_name, options), headers)
|
72
|
+
else
|
73
|
+
orig_delete(custom_method_name, options)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module ClassMethods
|
80
|
+
def custom_method_collection_url(method_name, options = {})
|
81
|
+
prefix_options, query_options = split_options(options)
|
82
|
+
"#{prefix(prefix_options)}#{collection_name}/#{method_name}.#{format.extension}#{query_string(query_options)}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def get(method_name, options = {})
|
87
|
+
connection.get(custom_method_element_url(method_name, options), self.class.headers)
|
88
|
+
end
|
89
|
+
|
90
|
+
def post(method_name, options = {}, body = nil)
|
91
|
+
request_body = body.blank? ? encode : body
|
92
|
+
if new?
|
93
|
+
connection.post(custom_method_new_element_url(method_name, options), request_body, self.class.headers)
|
94
|
+
else
|
95
|
+
connection.post(custom_method_element_url(method_name, options), request_body, self.class.headers)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def put(method_name, options = {}, body = '')
|
100
|
+
connection.put(custom_method_element_url(method_name, options), body, self.class.headers)
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete(method_name, options = {})
|
104
|
+
connection.delete(custom_method_element_url(method_name, options), self.class.headers)
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
private
|
109
|
+
def custom_method_element_url(method_name, options = {})
|
110
|
+
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}"
|
111
|
+
end
|
112
|
+
|
113
|
+
def custom_method_new_element_url(method_name, options = {})
|
114
|
+
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ApiResource
|
2
|
+
module Decorators
|
3
|
+
class CachingDecorator
|
4
|
+
attr_reader :owner
|
5
|
+
attr_reader :ttl
|
6
|
+
|
7
|
+
def initialize(owner, ttl)
|
8
|
+
@owner = owner
|
9
|
+
@ttl = ttl
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def method_missing(method_name, *args, &block)
|
14
|
+
ApiResource.with_ttl(self.ttl) do
|
15
|
+
self.owner.send(method_name, *args, &block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module ApiResource
|
2
|
+
class ConnectionError < StandardError # :nodoc:
|
3
|
+
|
4
|
+
cattr_accessor :http_code
|
5
|
+
|
6
|
+
attr_reader :response
|
7
|
+
|
8
|
+
def initialize(response, options = {})
|
9
|
+
@response = response
|
10
|
+
@message = options[:message]
|
11
|
+
@path = options[:path]
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
message = "Failed."
|
16
|
+
|
17
|
+
if response.respond_to?(:code)
|
18
|
+
message << " Response code = #{response.code}."
|
19
|
+
end
|
20
|
+
|
21
|
+
if response.respond_to?(:body)
|
22
|
+
begin
|
23
|
+
body = JSON.parse(response.body).pretty_inspect
|
24
|
+
rescue
|
25
|
+
body = "INVALID JSON: #{response.body}"
|
26
|
+
end
|
27
|
+
message << "\nResponse message = #{body}."
|
28
|
+
end
|
29
|
+
|
30
|
+
message << "\n#{@message}"
|
31
|
+
message << "\n#{@path}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def http_code
|
35
|
+
self.class.http_code
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
# Raised when a Timeout::Error occurs.
|
41
|
+
class RequestTimeout < ConnectionError
|
42
|
+
def initialize(message)
|
43
|
+
@message = message
|
44
|
+
end
|
45
|
+
def to_s; @message ;end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Raised when a OpenSSL::SSL::SSLError occurs.
|
49
|
+
class SSLError < ConnectionError
|
50
|
+
def initialize(message)
|
51
|
+
@message = message
|
52
|
+
end
|
53
|
+
def to_s; @message ;end
|
54
|
+
end
|
55
|
+
|
56
|
+
# 3xx Redirection
|
57
|
+
class Redirection < ConnectionError # :nodoc:
|
58
|
+
def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
|
59
|
+
end
|
60
|
+
|
61
|
+
# 4xx Client Error
|
62
|
+
class ClientError < ConnectionError; end # :nodoc:
|
63
|
+
|
64
|
+
# 400 Bad Request
|
65
|
+
class BadRequest < ClientError; self.http_code = 400; end # :nodoc
|
66
|
+
|
67
|
+
# 401 Unauthorized
|
68
|
+
class UnauthorizedAccess < ClientError; self.http_code = 401; end # :nodoc
|
69
|
+
|
70
|
+
# 403 Forbidden
|
71
|
+
class ForbiddenAccess < ClientError; self.http_code = 403; end # :nodoc
|
72
|
+
|
73
|
+
# 404 Not Found
|
74
|
+
class ResourceNotFound < ClientError; self.http_code = 404; end # :nodoc:
|
75
|
+
|
76
|
+
# 406 Not Acceptable
|
77
|
+
class NotAcceptable < ClientError; self.http_code = 406; end
|
78
|
+
|
79
|
+
# 409 Conflict
|
80
|
+
class ResourceConflict < ClientError; self.http_code = 409; end # :nodoc:
|
81
|
+
|
82
|
+
# 410 Gone
|
83
|
+
class ResourceGone < ClientError; self.http_code = 410; end # :nodoc:
|
84
|
+
|
85
|
+
class UnprocessableEntity < ClientError; self.http_code = 422; end
|
86
|
+
|
87
|
+
# 5xx Server Error
|
88
|
+
class ServerError < ConnectionError; self.http_code = 400; end # :nodoc:
|
89
|
+
|
90
|
+
# 405 Method Not Allowed
|
91
|
+
class MethodNotAllowed < ClientError # :nodoc:
|
92
|
+
|
93
|
+
self.http_code = 405
|
94
|
+
|
95
|
+
def allowed_methods
|
96
|
+
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|