cotweet-nestful 0.0.3

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.
@@ -0,0 +1,2 @@
1
+ pkg
2
+ *.gem
@@ -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.
@@ -0,0 +1,75 @@
1
+ Nestful is a simple Ruby HTTP/REST client with a sane API.
2
+
3
+ ## Installation
4
+
5
+ sudo gem install nestful
6
+
7
+ ## Features
8
+
9
+ * Simple API
10
+ * File buffering
11
+ * Before/Progress/After Callbacks
12
+ * JSON & XML requests
13
+ * Multipart requests (file uploading)
14
+ * Resource API
15
+ * Proxy support
16
+ * SSL support
17
+
18
+ ## Options
19
+
20
+ Request options:
21
+
22
+ * headers (hash)
23
+ * params (hash)
24
+ * buffer (true/false)
25
+ * method (:get/:post/:put/:delete/:head)
26
+
27
+ Connection options:
28
+
29
+ * proxy
30
+ * user
31
+ * password
32
+ * auth_type
33
+ * timeout
34
+ * ssl_options
35
+
36
+ ## API
37
+
38
+ ### GET request
39
+
40
+ Nestful.get 'http://example.com' #=> "body"
41
+
42
+ ### POST request
43
+
44
+ Nestful.post 'http://example.com', :format => :form #=> "body"
45
+
46
+ ### Parameters
47
+
48
+ Nestful.get 'http://example.com', :params => {:nestled => {:params => 1}}
49
+
50
+ ### JSON request
51
+
52
+ Nestful.get 'http://example.com', :format => :json #=> {:json_hash => 1}
53
+ Nestful.json_get 'http://example.com' #=> {:json_hash => 1}
54
+ Nestful.post 'http://example.com', :format => :json, :params => {:q => 'test'} #=> {:json_hash => 1}
55
+
56
+ ### Resource
57
+
58
+ Nestful::Resource.new('http://example.com')['assets'][1].get(:format => :xml) #=> {:xml_hash => 1}
59
+
60
+ ### Buffer download, return Tempfile
61
+
62
+ Nestful.get 'http://example.com/file.jpg', :buffer => true #=> <File ...>
63
+
64
+ ### Callbacks
65
+
66
+ Nestful.get 'http://www.google.co.uk', :buffer => true, :progress => Proc.new {|conn, total, size| p total; p size }
67
+ Nestful::Request.before_request {|conn| }
68
+ Nestful::Request.after_request {|conn, response| }
69
+
70
+ ### Multipart post
71
+
72
+ Nestful.post 'http://example.com', :format => :multipart, :params => {:file => File.open('README')}
73
+
74
+ ## Credits
75
+ Large parts of the connection code were taken from ActiveResource
@@ -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.3
@@ -0,0 +1,43 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "tempfile"
4
+
5
+ require "active_support/core_ext"
6
+ require "active_support/inflector"
7
+
8
+ $:.unshift(File.dirname(__FILE__))
9
+
10
+ require "nestful/exceptions"
11
+ require "nestful/formats"
12
+ require "nestful/connection"
13
+ require "nestful/request/callbacks"
14
+ require "nestful/request"
15
+ require "nestful/resource"
16
+
17
+ module Nestful
18
+ extend self
19
+
20
+ def get(url, options = {})
21
+ Request.new(url, ({:method => :get}).merge(options)).execute
22
+ end
23
+
24
+ def post(url, options = {})
25
+ Request.new(url, ({:method => :post, :format => :form}).merge(options)).execute
26
+ end
27
+
28
+ def put(url, options = {})
29
+ Request.new(url, ({:method => :put}).merge(options)).execute
30
+ end
31
+
32
+ def delete(url, options = {})
33
+ Request.new(url, ({:method => :delete}).merge(options)).execute
34
+ end
35
+
36
+ def json_get(url, params = nil)
37
+ get(url, :format => :json, :params => params)
38
+ end
39
+
40
+ def json_post(url, params = nil)
41
+ post(url, :format => :json, :params => params)
42
+ end
43
+ end
@@ -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,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,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,79 @@
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("--" + 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: form-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
+ stream.write(EOL)
63
+ end
64
+
65
+ def create_field(key, value)
66
+ stream.write(%Q{Content-Disposition: form-data; name="#{key}"} + EOL)
67
+ stream.write(EOL)
68
+ stream.write(value)
69
+ stream.write(EOL)
70
+ end
71
+
72
+ def filename(body)
73
+ return body.original_filename if body.respond_to?(:original_filename)
74
+ return File.basename(body.path) if body.respond_to?(:path)
75
+ "Unknown"
76
+ end
77
+ end
78
+ end
79
+ 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,132 @@
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
+
28
+ if self.uri.query
29
+ populate_query_params
30
+ end
31
+ end
32
+
33
+ def format=(mime_or_format)
34
+ @format = mime_or_format.is_a?(Symbol) ?
35
+ Formats[mime_or_format].new : mime_or_format
36
+ end
37
+
38
+ def connection
39
+ conn = Connection.new(uri, format)
40
+ conn.proxy = proxy if proxy
41
+ conn.user = user if user
42
+ conn.password = password if password
43
+ conn.auth_type = auth_type if auth_type
44
+ conn.timeout = timeout if timeout
45
+ conn.ssl_options = ssl_options if ssl_options
46
+ conn
47
+ end
48
+
49
+ def uri
50
+ http_url = url.match(/^http/) ? url : "http://#{url}"
51
+ uri = URI.parse(http_url)
52
+ uri.path = "/" if uri.path.empty?
53
+ if format && format.extension
54
+ uri.path += ".#{format.extension}"
55
+ end
56
+ uri
57
+ end
58
+
59
+ def path
60
+ uri.path
61
+ end
62
+
63
+ def query_path
64
+ query_path = path
65
+ if params.any?
66
+ query_path += "?"
67
+ query_path += params.to_param
68
+ end
69
+ query_path
70
+ end
71
+
72
+ def execute
73
+ callback(:before_request, self)
74
+ result = nil
75
+ if [:post, :put].include?(method)
76
+ connection.send(method, path, encoded, headers) {|res| result = decoded(res) }
77
+ else
78
+ connection.send(method, query_path, headers) {|res| result = decoded(res) }
79
+ end
80
+ callback(:after_request, self, result)
81
+ result
82
+ end
83
+
84
+ protected
85
+ def encoded
86
+ format.encode(params.any? ? params : body)
87
+ end
88
+
89
+ def decoded(result)
90
+ if buffer
91
+ data = Tempfile.new("nfr.#{rand(1000)}")
92
+ size = 0
93
+ total = result.content_length
94
+
95
+ result.read_body {|chunk|
96
+ callback(:progress, self, total, size += chunk.size)
97
+ data.write(chunk)
98
+ }
99
+
100
+ data.rewind
101
+ data
102
+ else
103
+ data = result.body
104
+ format ? format.decode(data) : data
105
+ end
106
+ end
107
+
108
+ def populate_query_params
109
+ uri_query = self.uri.query.split("&").inject({}) {|hash, res|
110
+ key, value = res.split("=")
111
+ hash[key] = value
112
+ hash
113
+ }
114
+ self.params.merge!(uri_query)
115
+ end
116
+
117
+ def callbacks(type = nil)
118
+ @callbacks ||= {}
119
+ return @callbacks unless type
120
+ @callbacks[type] ||= []
121
+ end
122
+
123
+ def callback(type, *args)
124
+ procs = self.class.callbacks(type) + callbacks(type)
125
+ procs.compact.each {|c| c.call(*args) }
126
+ end
127
+ end
128
+
129
+ class Request
130
+ include Callbacks
131
+ end
132
+ 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,34 @@
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
+ return self if suburl.nil?
12
+ suburl = suburl.to_s
13
+ base = url
14
+ base += "/" unless base =~ /\/$/
15
+ self.class.new(URI.join(base, suburl).to_s)
16
+ end
17
+
18
+ def get(options = {})
19
+ Nestful.get(url, options.merge(@options))
20
+ end
21
+
22
+ def post(options = {})
23
+ Nestful.post(url, options.merge(@options))
24
+ end
25
+
26
+ def json_get(params = nil)
27
+ get(:format => :json, :params => params)
28
+ end
29
+
30
+ def json_post(params = nil)
31
+ post(:format => :json, :params => params)
32
+ end
33
+ end
34
+ end
@@ -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{cotweet-nestful}
8
+ s.version = "0.0.3"
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-06-08}
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/cotweet/nestful}
39
+ s.rdoc_options = ["--charset=UTF-8"]
40
+ s.require_paths = ["lib"]
41
+ s.rubygems_version = %q{1.3.7}
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::VERSION) >= Gem::Version.new('1.2.0') then
49
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.3.8"])
50
+ else
51
+ s.add_dependency(%q<activesupport>, [">= 2.3.8"])
52
+ end
53
+ else
54
+ s.add_dependency(%q<activesupport>, [">= 2.3.8"])
55
+ end
56
+ end
57
+
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cotweet-nestful
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 3
10
+ version: 0.0.3
11
+ platform: ruby
12
+ authors:
13
+ - Alex MacCaw
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-06-08 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activesupport
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 19
30
+ segments:
31
+ - 2
32
+ - 3
33
+ - 8
34
+ version: 2.3.8
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description: Simple Ruby HTTP/REST client with a sane API
38
+ email: info@eribium.org
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - README.markdown
45
+ files:
46
+ - .gitignore
47
+ - MIT-LICENSE
48
+ - README.markdown
49
+ - Rakefile
50
+ - VERSION
51
+ - lib/nestful.rb
52
+ - lib/nestful/connection.rb
53
+ - lib/nestful/exceptions.rb
54
+ - lib/nestful/formats.rb
55
+ - lib/nestful/formats/blank_format.rb
56
+ - lib/nestful/formats/form_format.rb
57
+ - lib/nestful/formats/json_format.rb
58
+ - lib/nestful/formats/multipart_format.rb
59
+ - lib/nestful/formats/xml_format.rb
60
+ - lib/nestful/request.rb
61
+ - lib/nestful/request/callbacks.rb
62
+ - lib/nestful/resource.rb
63
+ - nestful.gemspec
64
+ has_rdoc: true
65
+ homepage: http://github.com/cotweet/nestful
66
+ licenses: []
67
+
68
+ post_install_message:
69
+ rdoc_options:
70
+ - --charset=UTF-8
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ hash: 3
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ hash: 3
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ requirements: []
92
+
93
+ rubyforge_project:
94
+ rubygems_version: 1.3.7
95
+ signing_key:
96
+ specification_version: 3
97
+ summary: Simple Ruby HTTP/REST client with a sane API
98
+ test_files: []
99
+