nestful 1.0.7 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 39b78f8d5346095d86fb0c3f12840b2239aa06dc
4
- data.tar.gz: f05421aa7dbf52811fba5552315087aec3df3f12
3
+ metadata.gz: bd97aa5fb848801d10cc36bfcc8a1f1d3997393e
4
+ data.tar.gz: a7f428dd9f1d1f363e8834f5180511d2eeec7036
5
5
  SHA512:
6
- metadata.gz: db3c0bbefc9e9e5b031f070f4735b50dd2970adb6b5b50a7eb6787f5d4478b96cc08952c16a5d56fd4241a3a643637301b863e512b9131207149d6302895f9ec
7
- data.tar.gz: d6d34a808b5417bdde3a0e92aed856822e6835a577aec77d9d38cf04836f2b71fe9285bf526d4ca69be5c17afbd29139259d5523deae495c0d85b717cfd9c89a
6
+ metadata.gz: 28b12558ec65700945a6a594a42710174114e3f87c56350aca183265d3be2a1e41ffb24db9468a81dbe17618982733d401395e65c0ce65710c7578c1182d6244
7
+ data.tar.gz: 2e10eec7723fbfb4cb5f291b7a904c1ce0e1f81927a5705a8332e41d7f5d171bfe6af8007f23b7fcf6574059679b5fb79f76fa8be43117d3824f60a25e26bb21
data/Gemfile CHANGED
@@ -6,4 +6,5 @@ gemspec
6
6
  group :test do
7
7
  gem 'webmock'
8
8
  gem 'rake'
9
+ gem 'net-http-spy'
9
10
  end
@@ -53,45 +53,65 @@ module Nestful
53
53
  # Makes a request to the remote service.
54
54
  def request(method, path, *arguments)
55
55
  response = http.send(method, path, *arguments)
56
+ response.uri = URI.join(endpoint, path)
57
+
56
58
  handle_response(response)
57
59
 
58
- rescue Timeout::Error => e
60
+ rescue Timeout::Error, Net::OpenTimeout => e
59
61
  raise TimeoutError.new(e.message)
60
62
  rescue OpenSSL::SSL::SSLError => e
61
63
  raise SSLError.new(e.message)
64
+ rescue SocketError,
65
+ EOFError,
66
+ Net::HTTPBadResponse,
67
+ Net::HTTPHeaderSyntaxError,
68
+ Net::HTTPServerException,
69
+ Net::ProtocolError,
70
+ Errno::ECONNABORTED,
71
+ Errno::ECONNREFUSED,
72
+ Errno::ECONNRESET,
73
+ Errno::ETIMEDOUT,
74
+ Errno::ENETUNREACH,
75
+ Errno::EHOSTUNREACH,
76
+ Errno::EINVAL,
77
+ Errno::ENOPROTOOPT => e
78
+ raise ErrnoError.new(e.message)
79
+ rescue Zlib::DataError,
80
+ Zlib::BufError => e
81
+ raise ZlibError.new(e.message)
62
82
  end
63
83
 
64
84
  # Handles response and error codes from the remote service.
65
85
  def handle_response(response)
66
86
  case response.code.to_i
67
- when 301,302
68
- raise Redirection.new(response)
69
- when 200...400
70
- response
71
- when 400
72
- raise BadRequest.new(response)
73
- when 401
74
- raise UnauthorizedAccess.new(response)
75
- when 403
76
- raise ForbiddenAccess.new(response)
77
- when 404
78
- raise ResourceNotFound.new(response)
79
- when 405
80
- raise MethodNotAllowed.new(response)
81
- when 409
82
- raise ResourceConflict.new(response)
83
- when 410
84
- raise ResourceGone.new(response)
85
- when 422
86
- raise ResourceInvalid.new(response)
87
- when 401...500
88
- raise ClientError.new(response)
89
- when 500...600
90
- raise ServerError.new(response)
91
- else
92
- raise ConnectionError.new(
93
- response, "Unknown response code: #{response.code}"
94
- )
87
+ when 200...299
88
+ response
89
+ when 300..399
90
+ raise Redirection.new(response)
91
+ when 400
92
+ raise BadRequest.new(response)
93
+ when 401
94
+ raise UnauthorizedAccess.new(response)
95
+ when 403
96
+ raise ForbiddenAccess.new(response)
97
+ when 404
98
+ raise ResourceNotFound.new(response)
99
+ when 405
100
+ raise MethodNotAllowed.new(response)
101
+ when 409
102
+ raise ResourceConflict.new(response)
103
+ when 410
104
+ raise ResourceGone.new(response)
105
+ when 422
106
+ raise ResourceInvalid.new(response)
107
+ when 401...500
108
+ raise ClientError.new(response)
109
+ when 500...600
110
+ raise ServerError.new(response)
111
+ else
112
+ raise ResponseError.new(
113
+ response, "Unknown response code: #{response.code}"
114
+ )
95
115
  end
96
116
  end
97
117
 
@@ -137,4 +157,4 @@ module Nestful
137
157
  http
138
158
  end
139
159
  end
140
- end
160
+ end
@@ -1,5 +1,18 @@
1
1
  module Nestful
2
- class ConnectionError < StandardError # :nodoc:
2
+ class Error < StandardError; end
3
+ ConnectionError = Error
4
+
5
+ class RequestError < Error
6
+ def initialize(message)
7
+ @message = message
8
+ end
9
+
10
+ def to_s
11
+ @message
12
+ end
13
+ end
14
+
15
+ class ResponseError < Error
3
16
  attr_reader :response
4
17
 
5
18
  def initialize(response, message = nil)
@@ -11,34 +24,43 @@ module Nestful
11
24
  message = "Failed."
12
25
  message << " Response code = #{response.code}." if response.respond_to?(:code)
13
26
  message << " Response message = #{response.message}." if response.respond_to?(:message)
14
- message << " Response Body = #{response.body}." if response.respond_to?(:body)
27
+
28
+ if response.respond_to?(:body)
29
+ # Error messages need to be in UTF-8
30
+ body = response.body.dup.to_s
31
+ body = body.encode('UTF-8', :invalid => :replace, :undef => :replace, :replace => '?')
32
+ message << " Response Body = #{body}."
33
+ end
34
+
15
35
  message
16
36
  end
17
37
  end
18
38
 
19
39
  # Raised when a Timeout::Error occurs.
20
- class TimeoutError < ConnectionError
21
- def initialize(message)
22
- @message = message
23
- end
24
- def to_s; @message ;end
40
+ class TimeoutError < RequestError
25
41
  end
26
42
 
27
43
  # Raised when a OpenSSL::SSL::SSLError occurs.
28
- class SSLError < ConnectionError
29
- def initialize(message)
30
- @message = message
31
- end
32
- def to_s; @message ;end
44
+ class SSLError < RequestError
45
+ end
46
+
47
+ class ErrnoError < RequestError
48
+ end
49
+
50
+ class ZlibError < RequestError
33
51
  end
34
52
 
35
53
  # 3xx Redirection
36
- class Redirection < ConnectionError # :nodoc:
54
+ class Redirection < ResponseError # :nodoc:
55
+ def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
56
+ end
57
+
58
+ class RedirectionLoop < ResponseError # :nodoc:
37
59
  def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
38
60
  end
39
61
 
40
62
  # 4xx Client Error
41
- class ClientError < ConnectionError; end # :nodoc:
63
+ class ClientError < ResponseError; end # :nodoc:
42
64
 
43
65
  # 400 Bad Request
44
66
  class BadRequest < ClientError; end # :nodoc
@@ -62,7 +84,7 @@ module Nestful
62
84
  class ResourceInvalid < ClientError; end # :nodoc:
63
85
 
64
86
  # 5xx Server Error
65
- class ServerError < ConnectionError; end # :nodoc:
87
+ class ServerError < ResponseError; end # :nodoc:
66
88
 
67
89
  # 405 Method Not Allowed
68
90
  class MethodNotAllowed < ClientError # :nodoc:
@@ -70,4 +92,4 @@ module Nestful
70
92
  @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
71
93
  end
72
94
  end
73
- end
95
+ end
@@ -38,6 +38,22 @@ module Nestful
38
38
  end
39
39
  end
40
40
 
41
+ def to_url_param(value, prefix = nil)
42
+ case value
43
+ when Array
44
+ value.map { |v|
45
+ to_url_param(v, "#{prefix}[]")
46
+ }.join("&")
47
+ when Hash
48
+ value.map { |k, v|
49
+ to_url_param(v, prefix ? "#{prefix}[#{uri_escape(k)}]" : uri_escape(k))
50
+ }.join("&")
51
+ else
52
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
53
+ "#{prefix}=#{uri_escape(value)}"
54
+ end
55
+ end
56
+
41
57
  def from_param(param)
42
58
  Rack::Utils.parse_nested_query(param)
43
59
  (value || '').split('&').each do |res|
@@ -59,7 +75,15 @@ module Nestful
59
75
  end
60
76
 
61
77
  def escape(s)
62
- URI.encode_www_form_component(s)
78
+ URI.encode_www_form_component(s.to_s)
79
+ end
80
+
81
+ ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/
82
+
83
+ def uri_escape(s)
84
+ s.to_s.gsub(ESCAPE_RE) {|match|
85
+ '%' + match.unpack('H2' * match.bytesize).join('%').upcase
86
+ }.tr(' ', '+')
63
87
  end
64
88
 
65
89
  if defined?(::Encoding)
@@ -105,4 +129,4 @@ module Nestful
105
129
  return params
106
130
  end
107
131
  end
108
- end
132
+ end
@@ -1,42 +1,38 @@
1
1
  module Nestful
2
2
  class Mash < Hash
3
- def self.get(*args)
4
- from_response(Nestful.get(*args))
3
+ def self.get(action = '', params = {}, options = {})
4
+ request(action, options.merge(:method => :get, :params => params))
5
5
  end
6
6
 
7
- def self.post(*args)
8
- from_response(Nestful.post(*args))
7
+ def self.put(action = '', params = {}, options = {})
8
+ request(action, options.merge(:method => :put, :params => params))
9
9
  end
10
10
 
11
- def self.put(*args)
12
- from_response(Nestful.put(*args))
11
+ def self.post(action = '', params = {}, options = {})
12
+ request(action, options.merge(:method => :post, :params => params))
13
13
  end
14
14
 
15
- def self.delete(*args)
16
- from_response(Nestful.delete(*args))
15
+ def self.delete(action = '', params = {}, options = {})
16
+ request(action, options.merge(:method => :delete, :params => params))
17
17
  end
18
18
 
19
- def self.request(*args)
20
- from_response(Nestful.request(*args))
19
+ def self.request(url, options = {})
20
+ self.new Request.new(url, options).execute
21
21
  end
22
22
 
23
- def self.from_response(response)
24
- case response.decoded
25
- when Hash
26
- self.new(response.decoded)
27
- when Array
28
- response.decoded.map {|v| self.new(v) }
23
+ def self.new(value = nil, *args)
24
+ if value.respond_to?(:each) &&
25
+ !value.respond_to?(:each_pair)
26
+ value.map {|v| super(v) }
29
27
  else
30
- response
28
+ super
31
29
  end
32
30
  end
33
31
 
34
32
  alias_method :to_s, :inspect
35
33
 
36
- attr_reader :response
37
-
38
34
  def initialize(source_hash = nil, default = nil, &blk)
39
- deep_update(source_hash) if source_hash
35
+ deep_update(source_hash.to_hash) if source_hash
40
36
  default ? super(default) : super(&blk)
41
37
  end
42
38
 
@@ -191,13 +187,11 @@ module Nestful
191
187
  case val
192
188
  when self.class
193
189
  val.dup
194
- when Hash
195
- duping ? val.dup : val
196
190
  when ::Hash
197
191
  val = val.dup if duping
198
- self.class.new(val)
199
- when Array
200
- val.collect{ |e| convert_value(e) }
192
+ Mash.new(val)
193
+ when ::Array
194
+ val.map {|e| convert_value(e) }
201
195
  else
202
196
  val
203
197
  end
@@ -1,24 +1,28 @@
1
1
  module Nestful
2
2
  class Request
3
3
  attr_reader :options, :format, :url
4
- attr_accessor :params, :body, :method, :headers
5
4
 
6
- # Connection options
7
- attr_accessor :proxy, :user, :password, :auth_type, :timeout, :ssl_options
5
+ attr_accessor :params, :body, :method, :headers,
6
+ :proxy, :user, :password,
7
+ :auth_type, :timeout, :ssl_options,
8
+ :max_attempts, :follow_redirection
8
9
 
9
10
  def initialize(url, options = {})
10
11
  @url = url.to_s
11
12
 
12
- @options = options
13
+ @options = {
14
+ :method => :get,
15
+ :params => {},
16
+ :headers => {},
17
+ :format => :form,
18
+ :max_attempts => 5,
19
+ :follow_redirection => true
20
+ }.merge(options)
21
+
13
22
  @options.each do |key, val|
14
23
  method = "#{key}="
15
24
  send(method, val) if respond_to?(method)
16
25
  end
17
-
18
- self.method ||= :get
19
- self.params ||= {}
20
- self.headers ||= {}
21
- self.format ||= :form
22
26
  end
23
27
 
24
28
  def format=(mime_or_format)
@@ -40,34 +44,77 @@ module Nestful
40
44
  @uri = URI.parse(url)
41
45
  @uri.path = '/' if @uri.path.empty?
42
46
 
43
- if @uri.query
44
- @params.merge!(Helpers.from_param(@uri.query))
45
- @uri.query = nil
46
- end
47
-
48
47
  @uri
49
48
  end
50
49
 
50
+ def uri_params
51
+ uri.query ? Helpers.from_param(uri.query) : {}
52
+ end
53
+
51
54
  def path
52
55
  uri.path
53
56
  end
54
57
 
55
- def execute
56
- if encoded?
57
- result = connection.send(method, path, encoded, build_headers)
58
- else
59
- result = connection.send(method, query_path, build_headers)
58
+ def query_path
59
+ query_path = path.dup
60
+ query_params = uri_params.dup
61
+ query_params.merge!(params) unless encoded?
62
+
63
+ if query_params.any?
64
+ query_path += '?' + Helpers.to_url_param(query_params)
60
65
  end
61
66
 
62
- Response.new(result)
67
+ query_path
68
+ end
69
+
70
+ def encoded?
71
+ [:post, :put].include?(method)
72
+ end
73
+
74
+ def encoded
75
+ params.any? ? format.encode(params) : body
76
+ end
63
77
 
64
- rescue Redirection => error
65
- self.url = error.response['Location']
66
- execute
78
+ def execute
79
+ with_redirection do
80
+ if encoded?
81
+ result = connection.send(method, query_path, encoded, build_headers)
82
+ else
83
+ result = connection.send(method, query_path, build_headers)
84
+ end
85
+
86
+ Response.new(result, uri)
87
+ end
67
88
  end
68
89
 
69
90
  protected
70
91
 
92
+ def with_redirection(&block)
93
+ attempts = 1
94
+
95
+ begin
96
+ yield
97
+ rescue Redirection => error
98
+ raise error unless follow_redirection
99
+
100
+ attempts += 1
101
+
102
+ raise error unless error.response['Location']
103
+ raise RedirectionLoop.new(error.response) if attempts > max_attempts
104
+
105
+ location = error.response['Location'].scrub
106
+ location = URI.parse(location)
107
+
108
+ # Path is relative
109
+ unless location.host
110
+ location = URI.join(uri, location)
111
+ end
112
+
113
+ self.url = location.to_s
114
+ retry
115
+ end
116
+ end
117
+
71
118
  def connection
72
119
  Connection.new(uri,
73
120
  :proxy => proxy,
@@ -99,23 +146,5 @@ module Nestful
99
146
  .merge(content_type_headers)
100
147
  .merge(headers)
101
148
  end
102
-
103
- def query_path
104
- query_path = path
105
-
106
- if params.any?
107
- query_path += '?' + Helpers.to_param(params)
108
- end
109
-
110
- query_path
111
- end
112
-
113
- def encoded?
114
- [:post, :put].include?(method)
115
- end
116
-
117
- def encoded
118
- params.any? ? format.encode(params) : body
119
- end
120
149
  end
121
- end
150
+ end
@@ -1,15 +1,20 @@
1
1
  module Nestful
2
2
  class Response
3
- attr_reader :response, :body, :headers, :parser
3
+ attr_reader :response, :location, :body, :headers, :parser
4
4
 
5
- def initialize(response, parser = nil)
5
+ def initialize(response, location = nil)
6
6
  @response = response
7
7
  @body = response.body
8
+ @location = location
8
9
  @headers = Headers.new(response.to_hash)
9
10
  @format = Formats.for(headers.content_type)
10
11
  @parser ||= @format && @format.new
11
12
  end
12
13
 
14
+ def to_s
15
+ body
16
+ end
17
+
13
18
  def as_json
14
19
  decoded
15
20
  end
@@ -46,4 +51,4 @@ module Nestful
46
51
  end
47
52
  end
48
53
 
49
- require 'nestful/response/headers'
54
+ require 'nestful/response/headers'
@@ -1,3 +1,3 @@
1
1
  module Nestful
2
- VERSION = "1.0.7"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -59,6 +59,12 @@ class TestRequest < MiniTest::Unit::TestCase
59
59
  assert_requested(:post, 'http://example.com/v1/tokens', :body => '{"card":{"number":4242,"exp_month":12}}', :headers => {'Content-Type' => 'application/json'})
60
60
  end
61
61
 
62
+ def test_form_and_query_params
63
+ stub_request(:any, 'http://example.com/v1/tokens?query=123')
64
+ Nestful::Request.new('http://example.com/v1/tokens?query=123', :method => :post, :params => {:number => 4242, :exp_month => 12}).execute
65
+ assert_requested(:post, 'http://example.com/v1/tokens?query=123', :body => 'number=4242&exp_month=12')
66
+ end
67
+
62
68
  def test_timeout
63
69
  skip # TODO
64
70
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nestful
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex MacCaw
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-07 00:00:00.000000000 Z
11
+ date: 2015-07-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -17,7 +17,7 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
- - .gitignore
20
+ - ".gitignore"
21
21
  - Gemfile
22
22
  - MIT-LICENSE
23
23
  - README.markdown
@@ -26,7 +26,6 @@ files:
26
26
  - examples/pagespeed.rb
27
27
  - examples/resource.rb
28
28
  - lib/nestful.rb
29
- - lib/nestful/.DS_Store
30
29
  - lib/nestful/connection.rb
31
30
  - lib/nestful/endpoint.rb
32
31
  - lib/nestful/exceptions.rb
@@ -54,17 +53,17 @@ require_paths:
54
53
  - lib
55
54
  required_ruby_version: !ruby/object:Gem::Requirement
56
55
  requirements:
57
- - - '>='
56
+ - - ">="
58
57
  - !ruby/object:Gem::Version
59
58
  version: '0'
60
59
  required_rubygems_version: !ruby/object:Gem::Requirement
61
60
  requirements:
62
- - - '>='
61
+ - - ">="
63
62
  - !ruby/object:Gem::Version
64
63
  version: '0'
65
64
  requirements: []
66
65
  rubyforge_project:
67
- rubygems_version: 2.0.3
66
+ rubygems_version: 2.2.2
68
67
  signing_key:
69
68
  specification_version: 4
70
69
  summary: Simple Ruby HTTP/REST client with a sane API
@@ -73,3 +72,4 @@ test_files:
73
72
  - test/nestful/test_request.rb
74
73
  - test/nestful/test_resource.rb
75
74
  - test/nestful/test_response.rb
75
+ has_rdoc:
Binary file