nestful 1.0.7 → 1.1.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.
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