Roman2K-web-service 0.1.1

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