nestful 0.0.1

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,2 @@
1
+ pkg
2
+ *.gem
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Alexander MacCaw (info@eribium.org)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,71 @@
1
+ Nestful is a simple Ruby HTTP/REST client with a sane API.
2
+
3
+ ## Features
4
+
5
+ * Simple API
6
+ * File buffering
7
+ * Before/Progress/After Callbacks
8
+ * JSON & XML requests
9
+ * Multipart requests (file uploading)
10
+ * Resource API
11
+ * Proxy support
12
+ * SSL support
13
+
14
+ ## Options
15
+
16
+ Request options:
17
+
18
+ * headers (hash)
19
+ * params (hash)
20
+ * buffer (true/false)
21
+ * method (:get/:post/:put/:delete/:head)
22
+
23
+ Connection options:
24
+
25
+ * proxy
26
+ * user
27
+ * password
28
+ * auth_type
29
+ * timeout
30
+ * ssl_options
31
+
32
+ ## API
33
+
34
+ ### GET request
35
+
36
+ Nestful.get 'http://example.com' #=> "body"
37
+
38
+ ### POST request
39
+
40
+ Nestful.post 'http://example.com', :format => :form #=> "body"
41
+
42
+ ### Parameters
43
+
44
+ Nestful.get 'http://example.com', :params => {:nestled => {:params => 1}}
45
+
46
+ ### JSON request
47
+
48
+ Nestful.get 'http://example.com', :format => :json #=> {:json_hash => 1}
49
+ Nestful.json_get 'http://example.com' #=> {:json_hash => 1}
50
+ Nestful.post 'http://example.com', :format => :json, :params => {:q => 'test'} #=> {:json_hash => 1}
51
+
52
+ ### Resource
53
+
54
+ Nestful::Resource.new('http://example.com')['assets'][1].get(:format => :xml) #=> {:xml_hash => 1}
55
+
56
+ ### Buffer download, return Tempfile
57
+
58
+ Nestful.get 'http://example.com/file.jpg', :buffer => true #=> <File ...>
59
+
60
+ ### Callbacks
61
+
62
+ Nestful.get 'http://www.google.co.uk', :buffer => true, :progress => Proc.new {|conn, total, size| p total; p size }
63
+ Nestful::Request.before_request {|conn| }
64
+ Nestful::Request.after_request {|conn, response| }
65
+
66
+ ### Multipart post
67
+
68
+ Nestful.post 'http://example.com', :format => :multipart, :params => {:file => File.open('README')}
69
+
70
+ ## Credits
71
+ Large parts of the connection code were taken from ActiveResource
data/Rakefile ADDED
@@ -0,0 +1,14 @@
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
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,306 @@
1
+ require 'net/https'
2
+ require 'date'
3
+ require 'time'
4
+ require 'uri'
5
+
6
+ module Nestful
7
+ class Connection
8
+
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
25
+
26
+ # The +site+ parameter is required and will set the +site+
27
+ # 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
32
+ self.site = site
33
+ self.format = format
34
+ end
35
+
36
+ # Set URI for remote service.
37
+ 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
41
+ end
42
+
43
+ # Set the proxy for remote service.
44
+ 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
66
+ end
67
+
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
+ def get(path, headers = {}, &block)
76
+ with_auth { request(:get, path, build_request_headers(headers, :get, self.site.merge(path)), &block) }
77
+ end
78
+
79
+ # Executes a DELETE request (see HTTP protocol documentation if unfamiliar).
80
+ # Used to delete resources.
81
+ def delete(path, headers = {}, &block)
82
+ with_auth { request(:delete, path, build_request_headers(headers, :delete, self.site.merge(path)), &block) }
83
+ end
84
+
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) }
89
+ end
90
+
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) }
95
+ end
96
+
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) }
101
+ end
102
+
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
143
+
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
175
+
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
181
+
182
+ def new_http
183
+ if @proxy
184
+ Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
185
+ else
186
+ Net::HTTP.new(@site.host, @site.port)
187
+ end
188
+ end
189
+
190
+ def configure_http(http)
191
+ http = apply_ssl_options(http)
192
+
193
+ # Net::HTTP timeouts default to 60 seconds.
194
+ if @timeout
195
+ http.open_timeout = @timeout
196
+ http.read_timeout = @timeout
197
+ end
198
+
199
+ http
200
+ end
201
+
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
223
+ end
224
+
225
+ def default_header
226
+ @default_header ||= {}
227
+ end
228
+
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
233
+
234
+ def response_auth_header
235
+ @response_auth_header ||= ""
236
+ end
237
+
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
247
+
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
258
+ end
259
+
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}")
265
+
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)}"
269
+ end
270
+
271
+ def client_nonce
272
+ Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
273
+ end
274
+
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
282
+
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
295
+
296
+ def http_format_header(http_method)
297
+ {HTTP_FORMAT_HEADER_NAMES[http_method] => format.mime_type}
298
+ end
299
+
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
305
+ end
306
+ end
@@ -0,0 +1,69 @@
1
+ module Nestful
2
+ class ConnectionError < StandardError # :nodoc:
3
+ attr_reader :response
4
+
5
+ def initialize(response, message = nil)
6
+ @response = response
7
+ @message = message
8
+ end
9
+
10
+ def to_s
11
+ message = "Failed."
12
+ message << " Response code = #{response.code}." if response.respond_to?(:code)
13
+ message << " Response message = #{response.message}." if response.respond_to?(:message)
14
+ message
15
+ end
16
+ end
17
+
18
+ # Raised when a Timeout::Error occurs.
19
+ class TimeoutError < ConnectionError
20
+ def initialize(message)
21
+ @message = message
22
+ end
23
+ def to_s; @message ;end
24
+ end
25
+
26
+ # Raised when a OpenSSL::SSL::SSLError occurs.
27
+ class SSLError < ConnectionError
28
+ def initialize(message)
29
+ @message = message
30
+ end
31
+ def to_s; @message ;end
32
+ end
33
+
34
+ # 3xx Redirection
35
+ class Redirection < ConnectionError # :nodoc:
36
+ def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
37
+ end
38
+
39
+ # 4xx Client Error
40
+ class ClientError < ConnectionError; end # :nodoc:
41
+
42
+ # 400 Bad Request
43
+ class BadRequest < ClientError; end # :nodoc
44
+
45
+ # 401 Unauthorized
46
+ class UnauthorizedAccess < ClientError; end # :nodoc
47
+
48
+ # 403 Forbidden
49
+ class ForbiddenAccess < ClientError; end # :nodoc
50
+
51
+ # 404 Not Found
52
+ class ResourceNotFound < ClientError; end # :nodoc:
53
+
54
+ # 409 Conflict
55
+ class ResourceConflict < ClientError; end # :nodoc:
56
+
57
+ # 410 Gone
58
+ class ResourceGone < ClientError; end # :nodoc:
59
+
60
+ # 5xx Server Error
61
+ class ServerError < ConnectionError; end # :nodoc:
62
+
63
+ # 405 Method Not Allowed
64
+ class MethodNotAllowed < ClientError # :nodoc:
65
+ def allowed_methods
66
+ @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,13 @@
1
+ module Nestful
2
+ module Formats
3
+ class BlankFormat < Format
4
+ def encode(params, options = nil)
5
+ raise "Choose an encoding format, such as :form"
6
+ end
7
+
8
+ def decode(body)
9
+ body
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module Nestful
2
+ module Formats
3
+ class FormFormat < Format
4
+ def mime_type
5
+ "application/x-www-form-urlencoded"
6
+ end
7
+
8
+ def encode(params, options = nil)
9
+ params.to_param
10
+ end
11
+
12
+ def decode(body)
13
+ body
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ require 'yaml'
2
+ require 'active_support/json'
3
+
4
+ module Nestful
5
+ module Formats
6
+ class JsonFormat < Format
7
+ def extension
8
+ "json"
9
+ end
10
+
11
+ def mime_type
12
+ "application/json"
13
+ end
14
+
15
+ def encode(hash, options = nil)
16
+ ActiveSupport::JSON.encode(hash, options)
17
+ end
18
+
19
+ def decode(json)
20
+ ActiveSupport::JSON.decode(json)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,77 @@
1
+ require "active_support/secure_random"
2
+
3
+ module Nestful
4
+ module Formats
5
+ class MultipartFormat < Format
6
+ EOL = "\r\n"
7
+
8
+ attr_reader :boundary, :stream
9
+
10
+ def initialize(*args)
11
+ super
12
+ @boundary = ActiveSupport::SecureRandom.hex(10)
13
+ @stream = Tempfile.new("nf.#{rand(1000)}")
14
+ @stream.binmode
15
+ end
16
+
17
+ def mime_type
18
+ %Q{multipart/form-data; boundary=#{boundary}}
19
+ end
20
+
21
+ def encode(params, options = nil, namespace = nil)
22
+ to_multipart(params)
23
+ stream.write(EOL + "--" + boundary + "--" + EOL)
24
+ stream.flush
25
+ stream.rewind
26
+ stream
27
+ end
28
+
29
+ def decode(body)
30
+ body
31
+ end
32
+
33
+ protected
34
+ def to_multipart(params, namespace = nil)
35
+ params.each do |key, value|
36
+ key = namespace ? "#{namespace}[#{key}]" : key
37
+
38
+ # Support nestled params
39
+ if value.is_a?(Hash)
40
+ to_multipart(value, key)
41
+ next
42
+ end
43
+
44
+ stream.write("--" + boundary + EOL)
45
+
46
+ if value.is_a?(File) || value.is_a?(StringIO)
47
+ create_file_field(key, value)
48
+ else
49
+ create_field(key, value)
50
+ end
51
+ end
52
+ end
53
+
54
+ def create_file_field(key, value)
55
+ stream.write(%Q{Content-Disposition: format-data; name="#{key}"; filename="#{filename(value)}"} + EOL)
56
+ stream.write(%Q{Content-Type: application/octet-stream} + EOL)
57
+ stream.write(%Q{Content-Transfer-Encoding: binary} + EOL)
58
+ stream.write(EOL)
59
+ while data = value.read(8124)
60
+ stream.write(data)
61
+ end
62
+ end
63
+
64
+ def create_field(key, value)
65
+ stream.write(%Q{Content-Disposition: form-data; name="#{key}"} + EOL)
66
+ stream.write(EOL)
67
+ stream.write(value)
68
+ end
69
+
70
+ def filename(body)
71
+ return body.original_filename if body.respond_to?(:original_filename)
72
+ return File.basename(body.path) if body.respond_to?(:path)
73
+ "Unknown"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,34 @@
1
+ require 'active_support/core_ext/hash/conversions'
2
+
3
+ module Nestful
4
+ module Formats
5
+ class XmlFormat < Format
6
+ def extension
7
+ "xml"
8
+ end
9
+
10
+ def mime_type
11
+ "application/xml"
12
+ end
13
+
14
+ def encode(hash, options={})
15
+ hash.to_xml(options)
16
+ end
17
+
18
+ def decode(xml)
19
+ from_xml_data(Hash.from_xml(xml))
20
+ end
21
+
22
+ private
23
+ # Manipulate from_xml Hash, because xml_simple is not exactly what we
24
+ # want for Active Resource.
25
+ def from_xml_data(data)
26
+ if data.is_a?(Hash) && data.keys.size == 1
27
+ data.values.first
28
+ else
29
+ data
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ module Nestful
2
+ module Formats
3
+ class Format
4
+ def extension
5
+ end
6
+
7
+ def mime_type
8
+ end
9
+
10
+ def encode(*args)
11
+ end
12
+
13
+ def decode(*args)
14
+ end
15
+ end
16
+
17
+ autoload :BlankFormat, 'nestful/formats/blank_format'
18
+ autoload :MultipartFormat, 'nestful/formats/multipart_format'
19
+ autoload :FormFormat, 'nestful/formats/form_format'
20
+ autoload :XmlFormat, 'nestful/formats/xml_format'
21
+ autoload :JsonFormat, 'nestful/formats/json_format'
22
+
23
+ # Lookup the format class from a mime type reference symbol. Example:
24
+ #
25
+ # Nestful::Formats[:xml] # => Nestful::Formats::XmlFormat
26
+ # Nestful::Formats[:json] # => Nestful::Formats::JsonFormat
27
+ def self.[](mime_type_reference)
28
+ Nestful::Formats.const_get(ActiveSupport::Inflector.camelize(mime_type_reference.to_s) + "Format")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ module Nestful
2
+ class Request
3
+ module Callbacks
4
+ CALLBACKS = [
5
+ :before_request,
6
+ :after_request,
7
+ :progress
8
+ ]
9
+
10
+ def self.included(base)
11
+ CALLBACKS.each do |callback|
12
+ base.instance_eval(<<-EOS, __FILE__, __LINE__ + 1)
13
+ def #{callback}(method = nil, &block)
14
+ callbacks(:#{callback}) << (method||block)
15
+ end
16
+ EOS
17
+
18
+ base.class_eval(<<-EOS, __FILE__, __LINE__ + 1)
19
+ def #{callback}(method = nil, &block)
20
+ callbacks(:#{callback}) << (method||block)
21
+ end
22
+ alias_method :#{callback}=, :#{callback}
23
+ EOS
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,119 @@
1
+ module Nestful
2
+ class Request
3
+ def self.callbacks(type = nil) #:nodoc:
4
+ @callbacks ||= {}
5
+ return @callbacks unless type
6
+ @callbacks[type] ||= []
7
+ end
8
+
9
+ attr_reader :url, :options, :format
10
+ attr_accessor :params, :body, :buffer, :method, :headers, :callbacks
11
+
12
+ # Connection options
13
+ attr_accessor :proxy, :user, :password, :auth_type, :timeout, :ssl_options
14
+
15
+ def initialize(url, options = {})
16
+ @url = url
17
+ @options = options
18
+ @options.each {|key, val|
19
+ method = "#{key}="
20
+ send(method, val) if respond_to?(method)
21
+ }
22
+ self.method ||= :get
23
+ self.format ||= :blank
24
+ self.headers ||= {}
25
+ self.params ||= {}
26
+ self.body ||= ''
27
+ end
28
+
29
+ def format=(mime_or_format)
30
+ @format = mime_or_format.is_a?(Symbol) ?
31
+ Formats[mime_or_format].new : mime_or_format
32
+ end
33
+
34
+ def connection
35
+ conn = Connection.new(uri, format)
36
+ conn.proxy = proxy if proxy
37
+ conn.user = user if user
38
+ conn.password = password if password
39
+ conn.auth_type = auth_type if auth_type
40
+ conn.timeout = timeout if timeout
41
+ conn.ssl_options = ssl_options if ssl_options
42
+ conn
43
+ end
44
+
45
+ def uri
46
+ http_url = url.match(/^http/) ? url : "http://#{url}"
47
+ uri = URI.parse(http_url)
48
+ uri.path = "/" if uri.path.empty?
49
+ if format && format.extension
50
+ uri.path += ".#{format.extension}"
51
+ end
52
+ uri
53
+ end
54
+
55
+ def path
56
+ uri.path
57
+ end
58
+
59
+ def query_path
60
+ query_path = path
61
+ if params.any?
62
+ query_path += "?"
63
+ query_path += params.to_param
64
+ end
65
+ query_path
66
+ end
67
+
68
+ def execute
69
+ callback(:before_request, self)
70
+ result = nil
71
+ if [:post, :put].include?(method)
72
+ connection.send(method, path, encoded, headers) {|res| result = decoded(res) }
73
+ else
74
+ connection.send(method, query_path, headers) {|res| result = decoded(res) }
75
+ end
76
+ callback(:after_request, self, result)
77
+ result
78
+ end
79
+
80
+ protected
81
+ def encoded
82
+ format.encode(params.any? ? params : body)
83
+ end
84
+
85
+ def decoded(result)
86
+ if buffer
87
+ data = Tempfile.new("nfr.#{rand(1000)}")
88
+ size = 0
89
+ total = result.content_length
90
+
91
+ result.read_body {|chunk|
92
+ callback(:progress, self, total, size += chunk.size)
93
+ data.write(chunk)
94
+ }
95
+
96
+ data.rewind
97
+ data
98
+ else
99
+ data = result.body
100
+ format ? format.decode(data) : data
101
+ end
102
+ end
103
+
104
+ def callbacks(type = nil)
105
+ @callbacks ||= {}
106
+ return @callbacks unless type
107
+ @callbacks[type] ||= []
108
+ end
109
+
110
+ def callback(type, *args)
111
+ procs = self.class.callbacks(type) + callbacks(type)
112
+ procs.compact.each {|c| c.call(*args) }
113
+ end
114
+ end
115
+
116
+ class Request
117
+ include Callbacks
118
+ end
119
+ end
@@ -0,0 +1,30 @@
1
+ module Nestful
2
+ class Resource
3
+ attr_reader :url
4
+
5
+ def initialize(url, options = {})
6
+ @url = url
7
+ @options = options
8
+ end
9
+
10
+ def [](suburl)
11
+ self.class.new(URI.join(url, suburl).to_s)
12
+ end
13
+
14
+ def get(options = {})
15
+ Nestful.get(url, options.merge(@options))
16
+ end
17
+
18
+ def post(options = {})
19
+ Nestful.post(url, options.merge(@options))
20
+ end
21
+
22
+ def json_get(params = nil)
23
+ get(:format => :json, :params => params)
24
+ end
25
+
26
+ def json_post(params = nil)
27
+ post(:format => :json, :params => params)
28
+ end
29
+ end
30
+ end
data/lib/nestful.rb ADDED
@@ -0,0 +1,44 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "tempfile"
4
+
5
+ require "active_support/core_ext/object/to_param"
6
+ require "active_support/core_ext/object/to_query"
7
+ require "active_support/inflector"
8
+
9
+ $:.unshift(File.dirname(__FILE__))
10
+
11
+ require "nestful/exceptions"
12
+ require "nestful/formats"
13
+ require "nestful/connection"
14
+ require "nestful/request/callbacks"
15
+ require "nestful/request"
16
+ require "nestful/resource"
17
+
18
+ module Nestful
19
+ extend self
20
+
21
+ def get(url, options = {})
22
+ Request.new(url, options.merge(:method => :get)).execute
23
+ end
24
+
25
+ def post(url, options = {})
26
+ Request.new(url, options.merge(:method => :post)).execute
27
+ end
28
+
29
+ def put(url, options = {})
30
+ Request.new(url, options.merge(:method => :put)).execute
31
+ end
32
+
33
+ def delete(url, options = {})
34
+ Request.new(url, options.merge(:method => :delete)).execute
35
+ end
36
+
37
+ def json_get(url, params = nil)
38
+ get(url, :format => :json, :params => params)
39
+ end
40
+
41
+ def json_post(url, params = nil)
42
+ post(url, :format => :json, :params => params)
43
+ end
44
+ end
data/nestful.gemspec ADDED
@@ -0,0 +1,57 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{nestful}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Alex MacCaw"]
12
+ s.date = %q{2010-04-21}
13
+ s.description = %q{Simple Ruby HTTP/REST client with a sane API}
14
+ s.email = %q{info@eribium.org}
15
+ s.extra_rdoc_files = [
16
+ "README.markdown"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "MIT-LICENSE",
21
+ "README.markdown",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "lib/nestful.rb",
25
+ "lib/nestful/connection.rb",
26
+ "lib/nestful/exceptions.rb",
27
+ "lib/nestful/formats.rb",
28
+ "lib/nestful/formats/blank_format.rb",
29
+ "lib/nestful/formats/form_format.rb",
30
+ "lib/nestful/formats/json_format.rb",
31
+ "lib/nestful/formats/multipart_format.rb",
32
+ "lib/nestful/formats/xml_format.rb",
33
+ "lib/nestful/request.rb",
34
+ "lib/nestful/request/callbacks.rb",
35
+ "lib/nestful/resource.rb",
36
+ "nestful.gemspec"
37
+ ]
38
+ s.homepage = %q{http://github.com/maccman/nestful}
39
+ s.rdoc_options = ["--charset=UTF-8"]
40
+ s.require_paths = ["lib"]
41
+ s.rubygems_version = %q{1.3.6}
42
+ s.summary = %q{Simple Ruby HTTP/REST client with a sane API}
43
+
44
+ if s.respond_to? :specification_version then
45
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
46
+ s.specification_version = 3
47
+
48
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
49
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0.beta"])
50
+ else
51
+ s.add_dependency(%q<activesupport>, [">= 3.0.0.beta"])
52
+ end
53
+ else
54
+ s.add_dependency(%q<activesupport>, [">= 3.0.0.beta"])
55
+ end
56
+ end
57
+
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nestful
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Alex MacCaw
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-21 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activesupport
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 3
29
+ - 0
30
+ - 0
31
+ - beta
32
+ version: 3.0.0.beta
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description: Simple Ruby HTTP/REST client with a sane API
36
+ email: info@eribium.org
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.markdown
43
+ files:
44
+ - .gitignore
45
+ - MIT-LICENSE
46
+ - README.markdown
47
+ - Rakefile
48
+ - VERSION
49
+ - lib/nestful.rb
50
+ - lib/nestful/connection.rb
51
+ - lib/nestful/exceptions.rb
52
+ - lib/nestful/formats.rb
53
+ - lib/nestful/formats/blank_format.rb
54
+ - lib/nestful/formats/form_format.rb
55
+ - lib/nestful/formats/json_format.rb
56
+ - lib/nestful/formats/multipart_format.rb
57
+ - lib/nestful/formats/xml_format.rb
58
+ - lib/nestful/request.rb
59
+ - lib/nestful/request/callbacks.rb
60
+ - lib/nestful/resource.rb
61
+ - nestful.gemspec
62
+ has_rdoc: true
63
+ homepage: http://github.com/maccman/nestful
64
+ licenses: []
65
+
66
+ post_install_message:
67
+ rdoc_options:
68
+ - --charset=UTF-8
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ requirements: []
86
+
87
+ rubyforge_project:
88
+ rubygems_version: 1.3.6
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: Simple Ruby HTTP/REST client with a sane API
92
+ test_files: []
93
+