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 +17 -0
- data/Gemfile +4 -0
- data/README.markdown +11 -37
- data/Rakefile +1 -14
- data/examples/resource.rb +6 -0
- data/lib/nestful/.DS_Store +0 -0
- data/lib/nestful/connection.rb +107 -245
- data/lib/nestful/endpoint.rb +42 -0
- data/lib/nestful/exceptions.rb +1 -1
- data/lib/nestful/formats/form_format.rb +3 -3
- data/lib/nestful/formats/json_format.rb +10 -9
- data/lib/nestful/formats/multipart_format.rb +9 -8
- data/lib/nestful/formats.rb +19 -16
- data/lib/nestful/helpers.rb +41 -0
- data/lib/nestful/request.rb +73 -96
- data/lib/nestful/resource.rb +144 -27
- data/lib/nestful/response/headers.rb +31 -0
- data/lib/nestful/response.rb +47 -0
- data/lib/nestful/version.rb +3 -0
- data/lib/nestful.rb +17 -30
- data/nestful.gemspec +14 -54
- metadata +21 -28
- data/lib/nestful/formats/blank_format.rb +0 -13
- data/lib/nestful/formats/text_format.rb +0 -17
- data/lib/nestful/formats/xml_format.rb +0 -34
- data/lib/nestful/oauth.rb +0 -24
- data/lib/nestful/request/callbacks.rb +0 -28
data/.gitignore
ADDED
data/Gemfile
ADDED
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
|
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', :
|
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
|
-
###
|
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
|
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
|
-
|
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
|
-
|
61
|
+
|
62
|
+
Large parts of the connection code were taken from ActiveResource
|
data/Rakefile
CHANGED
@@ -1,14 +1 @@
|
|
1
|
-
|
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"
|
Binary file
|
data/lib/nestful/connection.rb
CHANGED
@@ -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
|
-
|
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,
|
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
|
-
|
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 :
|
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 :
|
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
|
-
|
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
|
-
|
35
|
+
request(:delete, path, header, &block)
|
83
36
|
end
|
84
37
|
|
85
|
-
|
86
|
-
|
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
|
-
|
92
|
-
|
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
|
-
|
98
|
-
|
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
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
177
|
-
|
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
|
-
|
183
|
-
if
|
184
|
-
|
61
|
+
if body
|
62
|
+
if body.respond_to?(:read)
|
63
|
+
method.body_stream = body
|
185
64
|
else
|
186
|
-
|
65
|
+
method.body = body
|
187
66
|
end
|
188
|
-
end
|
189
|
-
|
190
|
-
def configure_http(http)
|
191
|
-
http = apply_ssl_options(http)
|
192
67
|
|
193
|
-
|
194
|
-
|
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
|
-
|
203
|
-
|
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
|
-
|
226
|
-
|
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
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
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
|
-
|
235
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
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
|
-
|
261
|
-
|
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
|
-
|
267
|
-
|
268
|
-
|
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
|
-
|
272
|
-
|
273
|
-
end
|
151
|
+
http
|
152
|
+
end
|
274
153
|
|
275
|
-
|
276
|
-
|
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
|
-
|
284
|
-
|
285
|
-
|
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
|
-
|
297
|
-
{
|
161
|
+
ssl_options.each do |key, value|
|
162
|
+
http.send("#{key}=", value)
|
298
163
|
end
|
299
164
|
|
300
|
-
|
301
|
-
|
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
|