Roman2K-web-service 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ module WebService
2
+ module NamedRequestMethods
3
+ def post(*args, &block)
4
+ request(:post, *args, &block).data
5
+ end
6
+
7
+ def get(*args, &block)
8
+ request(:get, *args, &block).data
9
+ end
10
+
11
+ def put(*args, &block)
12
+ request(:put, *args, &block).data
13
+ end
14
+
15
+ def delete(*args, &block)
16
+ request(:delete, *args, &block).data
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,155 @@
1
+ module WebService
2
+ class RemoteCollection
3
+ ARGUMENT_LAYOUT_FOR_REQUEST = [ [Integer, /^[^\/]/], # ID
4
+ [Symbol, /^\//], # Action
5
+ Hash ].freeze # Body
6
+
7
+ # http://github.com/thoughtbot/suspenders/tree/master/config/initializers/errors.rb
8
+ HTTP_ERRORS = [ EOFError,
9
+ Errno::EINVAL,
10
+ Errno::ECONNRESET,
11
+ Net::ProtocolError,
12
+ Net::HTTPBadResponse,
13
+ Net::HTTPHeaderSyntaxError ].freeze
14
+
15
+ attr_reader :resource_class
16
+ attr_reader :nesting
17
+
18
+ include ResponseHandling
19
+
20
+ def initialize(resource_details, nesting=[])
21
+ @resource_class, @nesting = resource_details, nesting
22
+ end
23
+
24
+ def request(method, *args)
25
+ id, action, body = recognize(ARGUMENT_LAYOUT_FOR_REQUEST, *args)
26
+
27
+ url = build_url_for(id, action)
28
+ content_type, body = perform_adjustments_for_body!(body, method, url)
29
+ request = instantiate_request_for(method, url)
30
+ request.content_type = content_type if content_type
31
+ WebService.logger.info do
32
+ "#{method.to_s.upcase} #{url.obfuscate}#{" (#{body.length} bytes)" if body}"
33
+ end
34
+ response, elapsed = handle_connection_errors do
35
+ benchmark do
36
+ open_http_connection_to(url) do |conn|
37
+ conn.request(request, body).extend ResponseDataUnserialization
38
+ end
39
+ end
40
+ end
41
+ WebService.logger.info do
42
+ status = "=> %d %s" % [response.code, response.message]
43
+ length = "(%.2f KB)" % [response.content_length / 1024.0] if response.content_length
44
+ time = "[%d ms]" % [elapsed * 1000]
45
+ [status, length, time].compact.join(' ')
46
+ end
47
+ handle_response(response)
48
+ end
49
+
50
+ def with_nesting(further_nesting)
51
+ self.class.new(resource_class, nesting + further_nesting)
52
+ end
53
+
54
+ private
55
+
56
+ # Handle the body differently depending on the request method.
57
+ def build_url_for(id, action)
58
+ if resource_class.singleton
59
+ raise ArgumentError, "singleton resources do not require an ID parameter" if id
60
+ else
61
+ id ||= implicit_id if respond_to?(:implicit_id)
62
+ end
63
+
64
+ url = ensure_url_copy(resource_class.site)
65
+ segments = [url.path]
66
+ nesting.each do |res_name, id_for_association|
67
+ segments << res_name.to_s.pluralize
68
+ raise "attribute `#{res_name}_id' is missing" unless id_for_association
69
+ segments << id_for_association
70
+ end
71
+ segments << (resource_class.singleton ? resource_class.element_name : resource_class.element_name.pluralize)
72
+ segments << (CGI.escape(id.to_s) if id) << action
73
+ url.path = segments.compact.join('/').squeeze('/')
74
+ return url
75
+ end
76
+
77
+ def instantiate_request_for(method, url)
78
+ request = Net::HTTP.const_get(method.to_s.capitalize).new([url.path, url.query].compact.join('?'))
79
+ request.basic_auth(*resource_class.credentials) if resource_class.credentials
80
+ request['Accept'] = ["application/json", "application/xml"]
81
+ return request
82
+ end
83
+
84
+ def perform_adjustments_for_body!(body, method, url)
85
+ case method
86
+ when :post, :put
87
+ ["application/json", body.to_json]
88
+ else
89
+ url.query = body.to_query if body.respond_to?(:to_query) && body.method(:to_query).arity <= 0
90
+ [nil, nil]
91
+ end
92
+ end
93
+
94
+ def open_http_connection_to(url)
95
+ http = Net::HTTP.new(url.host, url.port)
96
+ http.open_timeout = http.read_timeout = 30
97
+ http.use_ssl = url.kind_of?(URI::HTTPS)
98
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
99
+ http.start do |conn|
100
+ yield conn
101
+ end
102
+ end
103
+
104
+ def handle_connection_errors
105
+ yield
106
+ rescue Timeout::Error
107
+ raise TimeoutError.new($!.message)
108
+ rescue *HTTP_ERRORS
109
+ raise ConnectionError.new(nil, $!.message)
110
+ end
111
+
112
+ private # utilities
113
+
114
+ def recognize(patterns, *objects)
115
+ patterns.map do |pattern|
116
+ if pos = objects.index { |object| case object; when *pattern; true; end }
117
+ objects.delete_at(pos)
118
+ end
119
+ end
120
+ end
121
+
122
+ def benchmark
123
+ result = nil
124
+ elapsed = Benchmark.realtime do
125
+ result = yield
126
+ end
127
+ return result, elapsed
128
+ end
129
+
130
+ def ensure_url_copy(url)
131
+ url = url.url if url.respond_to?(:url)
132
+ url.kind_of?(URI) ? url.dup : URI.parse(url)
133
+ end
134
+
135
+ module ResponseDataUnserialization
136
+ def data
137
+ return @data if defined? @data
138
+ @data = parse_data
139
+ end
140
+
141
+ private
142
+
143
+ def parse_data
144
+ case content_type
145
+ when /json/
146
+ ActiveSupport::JSON.decode(body)
147
+ when /xml/
148
+ Hash.from_xml(body)
149
+ else
150
+ body.blank? ? nil : body
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,177 @@
1
+ module WebService
2
+ class Resource
3
+ class_inheritable_attr_accessor :site, :credentials
4
+ class_inheritable_attr_accessor :element_name, :singleton
5
+
6
+ class << self
7
+ include NamedRequestMethods
8
+ include CRUDOperations
9
+
10
+ def credentials
11
+ super || site.credentials
12
+ end
13
+
14
+ def element_name
15
+ super || self.element_name = begin
16
+ klass = self
17
+ while klass.name.to_s.empty?
18
+ klass = klass.superclass
19
+ raise NameError, "cannot determine element name from anonymous class" unless klass < Resource
20
+ end
21
+ klass.name.to_s.demodulize.underscore
22
+ end
23
+ end
24
+
25
+ def belongs_to(*resource_names)
26
+ @belongs_to ||= []
27
+ @belongs_to |= resource_names
28
+ @belongs_to
29
+ end
30
+
31
+ def has_many(*resource_names)
32
+ resource_names.each do |res_name|
33
+ class_eval <<-RUBY
34
+ def #{res_name}
35
+ association_collection_from_name(#{res_name.to_sym.inspect})
36
+ end
37
+ def #{res_name}=(collection)
38
+ #{res_name}.cache = collection
39
+ end
40
+ RUBY
41
+ end
42
+ end
43
+
44
+ def has_one(*resource_names)
45
+ resource_names.each do |res_name|
46
+ forwardings =
47
+ { "#{res_name}" => :first,
48
+ "build_#{res_name}" => :build }
49
+
50
+ forwardings.each do |from, to|
51
+ class_eval <<-RUBY
52
+ def #{from}(*args, &block)
53
+ association_collection_from_name(#{res_name.to_sym.inspect}, :singleton => true).#{to}(*args, &block)
54
+ end
55
+ RUBY
56
+ end
57
+ end
58
+ end
59
+
60
+ protected
61
+
62
+ delegate :request, :to => :remote_collection
63
+
64
+ def remote_collection
65
+ @remote_collection ||= RemoteCollection.new(self)
66
+ end
67
+ end
68
+
69
+ include AttributeAccessors
70
+ include NamedRequestMethods
71
+ include CRUDOperations
72
+
73
+ alias to_hash :attributes
74
+
75
+ def to_s
76
+ [self.class, saved? ? "[#{id}]" : "(new)"].join
77
+ end
78
+
79
+ def inspect
80
+ type_with_id = [self.class, saved? ? "[#{id}]" : "(new)"].join
81
+ displayable_attributes_pairs =
82
+ attributes.map { |name, value|
83
+ value = value[0, 22] + '...' if String === value && value.length > 25
84
+ "#{name}=#{value.inspect}" unless name == 'id'
85
+ }.compact.sort
86
+ "#<#{type_with_id}#{displayable_attributes_pairs.map { |pair| " " + pair }.join}>"
87
+ end
88
+
89
+ def ==(other)
90
+ self.class === other && self.attributes == other.attributes
91
+ end
92
+
93
+ def save
94
+ resource = if saved? then update((id unless self.class.singleton), attributes) else create(attributes) end
95
+ self.attributes = resource.attributes if resource
96
+ return self
97
+ end
98
+
99
+ def saved?
100
+ respond_to?(:id) && id
101
+ end
102
+
103
+ def reload
104
+ raise ResourceNotSaved unless saved?
105
+ id = self.id
106
+ [attribute_registry, association_registry].each &:clear
107
+ self.attributes = get(id)
108
+ return self
109
+ end
110
+
111
+ def destroy
112
+ delete
113
+ return self
114
+ end
115
+
116
+ protected # for CRUDOperations
117
+
118
+ delegate :request, :to => :remote_collection
119
+
120
+ def remote_collection
121
+ @remote_collection ||=
122
+ (@basic_remote_collection || self.class.instance_eval { remote_collection }).
123
+ with_nesting(nesting).
124
+ extend(ImplicitID).set_related_resource(self)
125
+ end
126
+
127
+ def resource_class
128
+ self.class
129
+ end
130
+
131
+ protected
132
+
133
+ def association_collection_from_name(name, options={})
134
+ @association_collections ||= {}
135
+ @association_collections[name.to_sym] ||= build_association_collection_from_name(name, options)
136
+ end
137
+
138
+ private
139
+
140
+ def nesting
141
+ self.class.belongs_to.map { |res_name| [res_name, send("#{res_name}_id")] }
142
+ end
143
+
144
+ def nesting_up_to_self
145
+ nesting + [[self.class.element_name, id]]
146
+ end
147
+
148
+ def build_association_collection_from_name(name, options={})
149
+ if options[:singleton]
150
+ klass = name.to_s.camelize.constantize
151
+ klass.singleton ? klass : Class.new(klass) { self.singleton = true }
152
+ else
153
+ name.to_s.singularize.camelize.constantize
154
+ end.
155
+ instance_eval { remote_collection }.
156
+ with_nesting(nesting_up_to_self).
157
+ extend NamedRequestMethods, CRUDOperations, NestingAsImplicitAttributes
158
+ end
159
+
160
+ module NestingAsImplicitAttributes
161
+ def implicit_attributes
162
+ nesting.inject({}) { |attributes, (res_name, id)| attributes.update("#{res_name}_id" => id) }
163
+ end
164
+ end
165
+
166
+ module ImplicitID
167
+ def set_related_resource(resource)
168
+ @related_resource = resource
169
+ self
170
+ end
171
+
172
+ def implicit_id
173
+ @related_resource.id if @related_resource.saved?
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,148 @@
1
+ # Copyright (c) 2006 David Heinemeier Hansson
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ # Extracted from ActiveResource 79f55de9c5e3ff1f8d9e767c5af21ba31be4cfba.
23
+ module WebService
24
+ module ResponseHandling
25
+ module Exceptions
26
+ class ConnectionError < StandardError # :nodoc:
27
+ attr_reader :response
28
+
29
+ def initialize(response, message = nil)
30
+ @response = response
31
+ @message = message
32
+ end
33
+
34
+ def to_s
35
+ "Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
36
+ end
37
+ end
38
+
39
+ # Raised when a Timeout::Error occurs.
40
+ class TimeoutError < ConnectionError
41
+ def initialize(message)
42
+ @message = message
43
+ end
44
+ def to_s; @message ;end
45
+ end
46
+
47
+ # 3xx Redirection
48
+ class Redirection < ConnectionError # :nodoc:
49
+ def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
50
+ end
51
+
52
+ # 4xx Client Error
53
+ class ClientError < ConnectionError; end # :nodoc:
54
+
55
+ # 400 Bad Request
56
+ class BadRequest < ClientError; end # :nodoc
57
+
58
+ # 401 Unauthorized
59
+ class UnauthorizedAccess < ClientError; end # :nodoc
60
+
61
+ # 403 Forbidden
62
+ class ForbiddenAccess < ClientError; end # :nodoc
63
+
64
+ # 404 Not Found
65
+ class ResourceNotFound < ClientError; end # :nodoc:
66
+
67
+ # 406 Not Acceptable
68
+ class NotAcceptable < ClientError; end # :nodoc:
69
+
70
+ # 409 Conflict
71
+ class ResourceConflict < ClientError; end # :nodoc:
72
+
73
+ # 422 Unprocessable Entity
74
+ class ResourceInvalid < ClientError; end #:nodoc:
75
+
76
+ # 5xx Server Error
77
+ class ServerError < ConnectionError; end # :nodoc:
78
+
79
+ # 502 Bad Gateway
80
+ class BadGateway < ServerError; end # :nodoc:
81
+
82
+ # 503 Service Unavailable
83
+ class ServiceUnavailable < ServerError; end # :nodoc:
84
+
85
+ # 504 Gateway Timeout
86
+ class GatewayTimeout < ServerError; end # :nodoc:
87
+
88
+ # 405 Method Not Allowed
89
+ class MethodNotAllowed < ClientError # :nodoc:
90
+ def allowed_methods
91
+ @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
92
+ end
93
+ end
94
+ end
95
+ include Exceptions
96
+
97
+ # Fix ConnectionError message.
98
+ ConnectionError.class_eval do
99
+ def to_s
100
+ returning "Failed" do |message|
101
+ message << " with #{response.code}" if response && response.respond_to?(:code) && response.code
102
+ message << " (#{response.message})" if response && response.respond_to?(:message) && !response.message.to_s.strip.empty?
103
+ message << ": #{@message}" if @message
104
+ end
105
+ end
106
+ end
107
+
108
+ protected
109
+
110
+ # Handles response and error codes from remote service.
111
+ def handle_response(response)
112
+ case response.code.to_i
113
+ when 301,302
114
+ raise Redirection.new(response)
115
+ when 200...400
116
+ response
117
+ when 400
118
+ raise BadRequest.new(response)
119
+ when 401
120
+ raise UnauthorizedAccess.new(response)
121
+ when 403
122
+ raise ForbiddenAccess.new(response)
123
+ when 404
124
+ raise ResourceNotFound.new(response)
125
+ when 405
126
+ raise MethodNotAllowed.new(response)
127
+ when 406
128
+ raise NotAcceptable.new(response)
129
+ when 409
130
+ raise ResourceConflict.new(response)
131
+ when 422
132
+ raise ResourceInvalid.new(response)
133
+ when 401...500
134
+ raise ClientError.new(response)
135
+ when 502
136
+ raise BadGateway.new(response)
137
+ when 503
138
+ raise ServiceUnavailable.new(response)
139
+ when 504
140
+ raise GatewayTimeout.new(response)
141
+ when 500...600
142
+ raise ServerError.new(response)
143
+ else
144
+ raise ConnectionError.new(response, "Unknown response code: #{response.code}")
145
+ end
146
+ end
147
+ end
148
+ end