nestful 0.0.8 → 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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