nestful 0.0.8 → 1.0.0.pre

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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nestful.gemspec
4
+ gemspec
data/README.markdown CHANGED
@@ -1,4 +1,4 @@
1
- Nestful is a simple Ruby HTTP/REST client with a sane API.
1
+ Nestful is a simple Ruby HTTP/REST client with a sane API.
2
2
 
3
3
  ## Installation
4
4
 
@@ -9,19 +9,18 @@ Nestful is a simple Ruby HTTP/REST client with a sane API.
9
9
  * Simple API
10
10
  * File buffering
11
11
  * Before/Progress/After Callbacks
12
- * JSON & XML requests
12
+ * JSON requests
13
13
  * Multipart requests (file uploading)
14
14
  * Resource API
15
15
  * Proxy support
16
16
  * SSL support
17
17
 
18
- ## Options
18
+ ## Request Options
19
19
 
20
20
  Request options:
21
21
 
22
22
  * headers (hash)
23
23
  * params (hash)
24
- * buffer (true/false)
25
24
  * method (:get/:post/:put/:delete/:head)
26
25
 
27
26
  Connection options:
@@ -34,55 +33,30 @@ Connection options:
34
33
  * ssl_options
35
34
 
36
35
  ## API
37
-
36
+
38
37
  ### GET request
39
38
 
40
39
  Nestful.get 'http://example.com' #=> "body"
41
40
 
42
41
  ### POST request
43
42
 
44
- Nestful.post 'http://example.com', :format => :form #=> "body"
45
-
46
- other supported mime-type formats are :json, :multipart, :xml
43
+ Nestful.post 'http://example.com', :params => {:foo => 'bar'}
44
+ Nestful.post 'http://example.com', :params => {:foo => 'bar'}, :format => :json
47
45
 
48
46
  ### Parameters
49
47
 
50
48
  Nestful.get 'http://example.com', :params => {:nestled => {:params => 1}}
51
49
 
52
- ### JSON request
53
-
54
- Nestful.get 'http://example.com', :format => :json #=> {:json_hash => 1}
55
- Nestful.json_get 'http://example.com' #=> {:json_hash => 1}
56
- Nestful.post 'http://example.com', :format => :json, :params => {:q => 'test'} #=> {:json_hash => 1}
57
-
58
- ### Resource
59
-
60
- The Resource class provides a single object to work with restful services. The following example does a GET request to the URL; http://example.com/assets/1/
61
-
62
- Nestful::Resource.new('http://example.com')['assets'][1].get(:format => :xml) #=> {:xml_hash => 1}
50
+ ### Endpoint
63
51
 
64
- The Resource class also supports, post, json_post and json_get methods.
52
+ The `Endpoint` class provides a single object to work with restful services. The following example does a GET request to the URL; http://example.com/assets/1/
65
53
 
66
- ### Buffer download, return Tempfile
67
-
68
- Nestful.get 'http://example.com/file.jpg', :buffer => true #=> <File ...>
69
-
70
- ### Callbacks
71
-
72
- Nestful.get 'http://www.google.co.uk', :buffer => true, :progress => Proc.new {|conn, total, size| p total; p size }
73
- Nestful::Request.before_request {|conn| }
74
- Nestful::Request.after_request {|conn, response| }
54
+ Nestful::Endpoint.new('http://example.com')['assets'][1].get
75
55
 
76
56
  ### Multipart post
77
57
 
78
58
  Nestful.post 'http://example.com', :format => :multipart, :params => {:file => File.open('README')}
79
-
80
- ### OAuth
81
-
82
- Nestful uses ROAuth for OAuth support - check out supported options: http://github.com/maccman/roauth
83
-
84
- require 'nestful/oauth'
85
- Nestful.get 'http://example.com', :oauth => {}
86
59
 
87
60
  ## Credits
88
- Large parts of the connection code were taken from ActiveResource
61
+
62
+ Large parts of the connection code were taken from ActiveResource
data/Rakefile CHANGED
@@ -1,14 +1 @@
1
- begin
2
- require 'jeweler'
3
- Jeweler::Tasks.new do |gemspec|
4
- gemspec.name = "nestful"
5
- gemspec.summary = "Simple Ruby HTTP/REST client with a sane API"
6
- gemspec.email = "info@eribium.org"
7
- gemspec.homepage = "http://github.com/maccman/nestful"
8
- gemspec.description = "Simple Ruby HTTP/REST client with a sane API"
9
- gemspec.authors = ["Alex MacCaw"]
10
- gemspec.add_dependency("activesupport", ">= 3.0.0.beta")
11
- end
12
- rescue LoadError
13
- puts "Jeweler not available. Install it with: sudo gem install jeweler"
14
- end
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,6 @@
1
+ require 'nestful'
2
+
3
+ class Charge < Nestful::Resource
4
+ url 'https://api.stripe.com/v1/charges'
5
+ options :auth_type => :bearer, :password => 'sk_test_mkGsLqEW6SLnZa487HYfJVLf'
6
+ end
Binary file
@@ -1,306 +1,168 @@
1
1
  require 'net/https'
2
- require 'date'
3
- require 'time'
4
2
  require 'uri'
5
3
 
6
4
  module Nestful
7
5
  class Connection
6
+ UriParser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
8
7
 
9
- HTTP_FORMAT_HEADER_NAMES = {
10
- :get => 'Accept',
11
- :put => 'Content-Type',
12
- :post => 'Content-Type',
13
- :delete => 'Accept',
14
- :head => 'Accept'
15
- }
16
-
17
- attr_reader :site, :user, :password, :auth_type, :timeout, :proxy, :ssl_options
18
- attr_accessor :format
19
-
20
- class << self
21
- def requests
22
- @@requests ||= []
23
- end
24
- end
8
+ attr_reader :site, :auth_type, :timeout, :proxy, :ssl_options
25
9
 
26
10
  # The +site+ parameter is required and will set the +site+
27
11
  # attribute to the URI for the remote resource service.
28
- def initialize(site, format = Formats::XmlFormat.new)
29
- raise ArgumentError, 'Missing site URI' unless site
30
- @user = @password = nil
31
- @uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
12
+ def initialize(site, options = {})
32
13
  self.site = site
33
- self.format = format
14
+
15
+ options.each do |key, value|
16
+ self.send("#{key}=", value) unless value.nil?
17
+ end
34
18
  end
35
19
 
36
20
  # Set URI for remote service.
37
21
  def site=(site)
38
- @site = site.is_a?(URI) ? site : @uri_parser.parse(site)
39
- @user = @uri_parser.unescape(@site.user) if @site.user
40
- @password = @uri_parser.unescape(@site.password) if @site.password
22
+ @site = site.is_a?(URI) ? site : UriParser.parse(site)
41
23
  end
42
24
 
43
25
  # Set the proxy for remote service.
44
26
  def proxy=(proxy)
45
- @proxy = proxy.is_a?(URI) ? proxy : @uri_parser.parse(proxy)
46
- end
47
-
48
- # Sets the user for remote service.
49
- def user=(user)
50
- @user = user
51
- end
52
-
53
- # Sets the password for remote service.
54
- def password=(password)
55
- @password = password
56
- end
57
-
58
- # Sets the auth type for remote service.
59
- def auth_type=(auth_type)
60
- @auth_type = legitimize_auth_type(auth_type)
61
- end
62
-
63
- # Sets the number of seconds after which HTTP requests to the remote service should time out.
64
- def timeout=(timeout)
65
- @timeout = timeout
27
+ @proxy = proxy.is_a?(URI) ? proxy : UriParser.parse(proxy)
66
28
  end
67
29
 
68
- # Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'.
69
- def ssl_options=(opts={})
70
- @ssl_options = opts
71
- end
72
-
73
- # Executes a GET request.
74
- # Used to get (find) resources.
75
30
  def get(path, headers = {}, &block)
76
- with_auth { request(:get, path, build_request_headers(headers, :get, self.site.merge(path)), &block) }
31
+ request(:get, path, headers, &block)
77
32
  end
78
33
 
79
- # Executes a DELETE request (see HTTP protocol documentation if unfamiliar).
80
- # Used to delete resources.
81
34
  def delete(path, headers = {}, &block)
82
- with_auth { request(:delete, path, build_request_headers(headers, :delete, self.site.merge(path)), &block) }
35
+ request(:delete, path, header, &block)
83
36
  end
84
37
 
85
- # Executes a PUT request (see HTTP protocol documentation if unfamiliar).
86
- # Used to update resources.
87
- def put(path, body = '', headers = {}, &block)
88
- with_auth { request(:put, path, body, build_request_headers(headers, :put, self.site.merge(path)), &block) }
38
+ def head(path, headers = {}, &block)
39
+ request(:head, path, headers, &block)
89
40
  end
90
41
 
91
- # Executes a POST request.
92
- # Used to create new resources.
93
- def post(path, body = '', headers = {}, &block)
94
- with_auth { request(:post, path, body, build_request_headers(headers, :post, self.site.merge(path)), &block) }
42
+ def put(path, body = '', headers = {}, &block)
43
+ request(:put, path, body, headers, &block)
95
44
  end
96
45
 
97
- # Executes a HEAD request.
98
- # Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
99
- def head(path, headers = {}, &block)
100
- with_auth { request(:head, path, build_request_headers(headers, :head, self.site.merge(path)), &block) }
46
+ def post(path, body = '', headers = {}, &block)
47
+ request(:post, path, body, headers, &block)
101
48
  end
102
49
 
103
- private
104
- # Makes a request to the remote service.
105
- def request(method, path, *arguments)
106
- body = nil
107
- body = arguments.shift if [:put, :post].include?(method)
108
- headers = arguments.shift || {}
109
-
110
- method = Net::HTTP.const_get(method.to_s.capitalize)
111
- method = method.new(path)
112
-
113
- if body
114
- if body.respond_to?(:read)
115
- method.body_stream = body
116
- else
117
- method.body = body
118
- end
119
-
120
- if body.respond_to?(:size)
121
- headers['Content-Length'] ||= body.size
122
- end
123
- end
124
-
125
- headers.each {|name, value|
126
- next unless value
127
- method.add_field(name, value)
128
- }
129
-
130
- http.start do |stream|
131
- stream.request(method) {|rsp|
132
- handle_response(rsp)
133
- yield(rsp) if block_given?
134
- rsp
135
- }
136
- end
137
-
138
- rescue Timeout::Error => e
139
- raise TimeoutError.new(e.message)
140
- rescue OpenSSL::SSL::SSLError => e
141
- raise SSLError.new(e.message)
142
- end
50
+ protected
143
51
 
144
- # Handles response and error codes from the remote service.
145
- def handle_response(response)
146
- case response.code.to_i
147
- when 301,302
148
- raise(Redirection.new(response))
149
- when 200...400
150
- response
151
- when 400
152
- raise(BadRequest.new(response))
153
- when 401
154
- raise(UnauthorizedAccess.new(response))
155
- when 403
156
- raise(ForbiddenAccess.new(response))
157
- when 404
158
- raise(ResourceNotFound.new(response))
159
- when 405
160
- raise(MethodNotAllowed.new(response))
161
- when 409
162
- raise(ResourceConflict.new(response))
163
- when 410
164
- raise(ResourceGone.new(response))
165
- when 422
166
- raise(ResourceInvalid.new(response))
167
- when 401...500
168
- raise(ClientError.new(response))
169
- when 500...600
170
- raise(ServerError.new(response))
171
- else
172
- raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
173
- end
174
- end
52
+ # Makes a request to the remote service.
53
+ def request(method, path, *arguments)
54
+ body = nil
55
+ body = arguments.shift if [:put, :post].include?(method)
56
+ headers = arguments.shift || {}
175
57
 
176
- # Creates new Net::HTTP instance for communication with the
177
- # remote service and resources.
178
- def http
179
- configure_http(new_http)
180
- end
58
+ method = Net::HTTP.const_get(method.to_s.capitalize)
59
+ method = method.new(path)
181
60
 
182
- def new_http
183
- if @proxy
184
- Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
61
+ if body
62
+ if body.respond_to?(:read)
63
+ method.body_stream = body
185
64
  else
186
- Net::HTTP.new(@site.host, @site.port)
65
+ method.body = body
187
66
  end
188
- end
189
-
190
- def configure_http(http)
191
- http = apply_ssl_options(http)
192
67
 
193
- # Net::HTTP timeouts default to 60 seconds.
194
- if @timeout
195
- http.open_timeout = @timeout
196
- http.read_timeout = @timeout
68
+ if body.respond_to?(:size)
69
+ headers['Content-Length'] ||= body.size
197
70
  end
198
-
199
- http
200
71
  end
201
72
 
202
- def apply_ssl_options(http)
203
- return http unless @site.is_a?(URI::HTTPS)
204
-
205
- http.use_ssl = true
206
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
207
- return http unless defined?(@ssl_options)
208
-
209
- http.ca_path = @ssl_options[:ca_path] if @ssl_options[:ca_path]
210
- http.ca_file = @ssl_options[:ca_file] if @ssl_options[:ca_file]
211
-
212
- http.cert = @ssl_options[:cert] if @ssl_options[:cert]
213
- http.key = @ssl_options[:key] if @ssl_options[:key]
214
-
215
- http.cert_store = @ssl_options[:cert_store] if @ssl_options[:cert_store]
216
- http.ssl_timeout = @ssl_options[:ssl_timeout] if @ssl_options[:ssl_timeout]
217
-
218
- http.verify_mode = @ssl_options[:verify_mode] if @ssl_options[:verify_mode]
219
- http.verify_callback = @ssl_options[:verify_callback] if @ssl_options[:verify_callback]
220
- http.verify_depth = @ssl_options[:verify_depth] if @ssl_options[:verify_depth]
221
-
222
- http
73
+ headers.each do |name, value|
74
+ next unless value
75
+ method.add_field(name, value)
223
76
  end
224
77
 
225
- def default_header
226
- @default_header ||= {}
78
+ http.start do |stream|
79
+ stream.request(method) do |rsp|
80
+ handle_response(rsp)
81
+ yield(rsp) if block_given?
82
+ rsp
83
+ end
227
84
  end
228
85
 
229
- # Builds headers for request to remote service.
230
- def build_request_headers(headers, http_method, uri)
231
- authorization_header(http_method, uri).update(default_header).update(http_format_header(http_method)).update(headers)
232
- end
86
+ rescue Timeout::Error => e
87
+ raise TimeoutError.new(e.message)
88
+ rescue OpenSSL::SSL::SSLError => e
89
+ raise SSLError.new(e.message)
90
+ end
233
91
 
234
- def response_auth_header
235
- @response_auth_header ||= ""
92
+ # Handles response and error codes from the remote service.
93
+ def handle_response(response)
94
+ case response.code.to_i
95
+ when 301,302
96
+ raise Redirection.new(response)
97
+ when 200...400
98
+ response
99
+ when 400
100
+ raise BadRequest.new(response)
101
+ when 401
102
+ raise UnauthorizedAccess.new(response)
103
+ when 403
104
+ raise ForbiddenAccess.new(response)
105
+ when 404
106
+ raise ResourceNotFound.new(response)
107
+ when 405
108
+ raise MethodNotAllowed.new(response)
109
+ when 409
110
+ raise ResourceConflict.new(response)
111
+ when 410
112
+ raise ResourceGone.new(response)
113
+ when 422
114
+ raise ResourceInvalid.new(response)
115
+ when 401...500
116
+ raise ClientError.new(response)
117
+ when 500...600
118
+ raise ServerError.new(response)
119
+ else
120
+ raise ConnectionError.new(
121
+ response, "Unknown response code: #{response.code}"
122
+ )
236
123
  end
124
+ end
237
125
 
238
- def with_auth
239
- retried ||= false
240
- yield
241
- rescue UnauthorizedAccess => e
242
- raise if retried || auth_type != :digest
243
- @response_auth_header = e.response['WWW-Authenticate']
244
- retried = true
245
- retry
246
- end
126
+ # Creates new Net::HTTP instance for communication with the
127
+ # remote service and resources.
128
+ def http
129
+ configure_http(new_http)
130
+ end
247
131
 
248
- def authorization_header(http_method, uri)
249
- if @user || @password
250
- if auth_type == :digest
251
- { 'Authorization' => digest_auth_header(http_method, uri) }
252
- else
253
- { 'Authorization' => 'Basic ' + ["#{@user}:#{@password}"].pack('m').delete("\r\n") }
254
- end
255
- else
256
- {}
257
- end
132
+ def new_http
133
+ if proxy
134
+ Net::HTTP.new(site.host, site.port,
135
+ proxy.host, proxy.port,
136
+ proxy.user, proxy.password)
137
+ else
138
+ Net::HTTP.new(site.host, site.port)
258
139
  end
140
+ end
259
141
 
260
- def digest_auth_header(http_method, uri)
261
- params = extract_params_from_response
262
-
263
- ha1 = Digest::MD5.hexdigest("#{@user}:#{params['realm']}:#{@password}")
264
- ha2 = Digest::MD5.hexdigest("#{http_method.to_s.upcase}:#{uri.path}")
142
+ def configure_http(http)
143
+ http = apply_ssl_options(http)
265
144
 
266
- params.merge!('cnonce' => client_nonce)
267
- request_digest = Digest::MD5.hexdigest([ha1, params['nonce'], "0", params['cnonce'], params['qop'], ha2].join(":"))
268
- "Digest #{auth_attributes_for(uri, request_digest, params)}"
145
+ # Net::HTTP timeouts default to 60 seconds.
146
+ if timeout
147
+ http.open_timeout = timeout
148
+ http.read_timeout = timeout
269
149
  end
270
150
 
271
- def client_nonce
272
- Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
273
- end
151
+ http
152
+ end
274
153
 
275
- def extract_params_from_response
276
- params = {}
277
- if response_auth_header =~ /^(\w+) (.*)/
278
- $2.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
279
- end
280
- params
281
- end
154
+ def apply_ssl_options(http)
155
+ return http unless site.is_a?(URI::HTTPS)
282
156
 
283
- def auth_attributes_for(uri, request_digest, params)
284
- [
285
- %Q(username="#{@user}"),
286
- %Q(realm="#{params['realm']}"),
287
- %Q(qop="#{params['qop']}"),
288
- %Q(uri="#{uri.path}"),
289
- %Q(nonce="#{params['nonce']}"),
290
- %Q(nc="0"),
291
- %Q(cnonce="#{params['cnonce']}"),
292
- %Q(opaque="#{params['opaque']}"),
293
- %Q(response="#{request_digest}")].join(", ")
294
- end
157
+ http.use_ssl = true
158
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
159
+ return http unless ssl_options
295
160
 
296
- def http_format_header(http_method)
297
- {HTTP_FORMAT_HEADER_NAMES[http_method] => format.mime_type}
161
+ ssl_options.each do |key, value|
162
+ http.send("#{key}=", value)
298
163
  end
299
164
 
300
- def legitimize_auth_type(auth_type)
301
- return :basic if auth_type.nil?
302
- auth_type = auth_type.to_sym
303
- [:basic, :digest].include?(auth_type) ? auth_type : :basic
304
- end
165
+ http
166
+ end
305
167
  end
306
168
  end