slash 0.4.4
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/MIT-LICENSE +20 -0
- data/README.rdoc +11 -0
- data/Rakefile +29 -0
- data/lib/slash.rb +11 -0
- data/lib/slash/connection.rb +122 -0
- data/lib/slash/exceptions.rb +66 -0
- data/lib/slash/formats.rb +67 -0
- data/lib/slash/json.rb +16 -0
- data/lib/slash/nethttp.rb +153 -0
- data/lib/slash/peanuts.rb +24 -0
- data/lib/slash/resource.rb +195 -0
- data/lib/slash/typhoeus.rb +86 -0
- data/spec/resource_spec.rb +15 -0
- metadata +159 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Igor Gunko
|
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.rdoc
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
== slash
|
2
|
+
@resource = Slash::Resource.new('http://api.something.com',
|
3
|
+
Slash::Formats.xml(:codec => Slash::Formats::PeanutsXML.new(Response)),
|
4
|
+
rewrite_options(options).update(:key => API_KEY))
|
5
|
+
|
6
|
+
cats = @resource['cats']
|
7
|
+
cats.get
|
8
|
+
cats.create(:name => 'Tom')
|
9
|
+
cats[123].update(:name => 'Tim')
|
10
|
+
cats[123].destroy
|
11
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
$KCODE = 'u'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/clean'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'spec/rake/spectask'
|
9
|
+
|
10
|
+
Rake::GemPackageTask.new(Gem::Specification.load('slash.gemspec')) do |p|
|
11
|
+
p.need_tar = true
|
12
|
+
p.need_zip = true
|
13
|
+
end
|
14
|
+
|
15
|
+
Rake::RDocTask.new do |rdoc|
|
16
|
+
files =['README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb']
|
17
|
+
rdoc.rdoc_files.add(files)
|
18
|
+
rdoc.main = "README.rdoc" # page to start on
|
19
|
+
rdoc.title = "Slash Documentation"
|
20
|
+
rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
|
21
|
+
rdoc.options << '--line-numbers'
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Run specs'
|
25
|
+
task :test => :spec
|
26
|
+
|
27
|
+
Spec::Rake::SpecTask.new do |t|
|
28
|
+
t.spec_files = FileList['spec/**/*.rb']
|
29
|
+
end
|
data/lib/slash.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'addressable/uri'
|
3
|
+
require 'slash/exceptions'
|
4
|
+
|
5
|
+
|
6
|
+
module Slash
|
7
|
+
class Queue
|
8
|
+
end
|
9
|
+
|
10
|
+
class Connection
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_accessor :timeout, :proxy
|
14
|
+
|
15
|
+
# Execute a GET request.
|
16
|
+
# Used to get (find) resources.
|
17
|
+
def get(uri, options = {}, &block)
|
18
|
+
request(:get, uri, options, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
|
22
|
+
# Used to delete resources.
|
23
|
+
def delete(uri, options = {}, &block)
|
24
|
+
request(:delete, uri, options, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Execute a PUT request.
|
28
|
+
# Used to update resources.
|
29
|
+
def put(uri, options = {}, &block)
|
30
|
+
request(:put, uri, options, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Execute a POST request.
|
34
|
+
# Used to create new resources.
|
35
|
+
def post(uri, options = {}, &block)
|
36
|
+
request(:post, uri, options, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Execute a HEAD request.
|
40
|
+
# Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
|
41
|
+
def head(uri, options = {}, &block)
|
42
|
+
request(:head, uri, options, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def prepare_request(uri, options)
|
47
|
+
case options[:auth]
|
48
|
+
when nil, :basic
|
49
|
+
user, password = uri.normalized_user, uri.normalized_password
|
50
|
+
options.headers['Authorization'] = 'Basic ' + ["#{user}:#{ password}"].pack('m').delete("\r\n") if user || password
|
51
|
+
else
|
52
|
+
raise ArgumentError, 'unsupported auth'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Handles response and error codes from remote service.
|
57
|
+
def handle_response(response)
|
58
|
+
response.exception = case response.code.to_i
|
59
|
+
when 301,302
|
60
|
+
Redirection.new(response)
|
61
|
+
when 200...400
|
62
|
+
nil
|
63
|
+
when 400
|
64
|
+
BadRequest.new(response)
|
65
|
+
when 401
|
66
|
+
UnauthorizedAccess.new(response)
|
67
|
+
when 403
|
68
|
+
ForbiddenAccess.new(response)
|
69
|
+
when 404
|
70
|
+
ResourceNotFound.new(response)
|
71
|
+
when 405
|
72
|
+
MethodNotAllowed.new(response)
|
73
|
+
when 409
|
74
|
+
ResourceConflict.new(response)
|
75
|
+
when 410
|
76
|
+
ResourceGone.new(response)
|
77
|
+
when 422
|
78
|
+
ResourceInvalid.new(response)
|
79
|
+
when 401...500
|
80
|
+
ClientError.new(response)
|
81
|
+
when 500...600
|
82
|
+
ServerError.new(response)
|
83
|
+
else
|
84
|
+
ConnectionError.new(response, "Unknown response code: #{response.code}")
|
85
|
+
end
|
86
|
+
response
|
87
|
+
end
|
88
|
+
|
89
|
+
def check_and_raise(response)
|
90
|
+
raise response.exception if response.exception
|
91
|
+
response
|
92
|
+
end
|
93
|
+
|
94
|
+
def logger #:nodoc:
|
95
|
+
Slash.logger
|
96
|
+
end
|
97
|
+
|
98
|
+
class << self
|
99
|
+
attr_writer :default
|
100
|
+
|
101
|
+
def default(&block)
|
102
|
+
@default = block if block_given?
|
103
|
+
@default
|
104
|
+
end
|
105
|
+
|
106
|
+
def create_default
|
107
|
+
if @default.respond_to?(:new)
|
108
|
+
return @default.new
|
109
|
+
else
|
110
|
+
return @default.call
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
self.default { NetHttpConnection.new }
|
116
|
+
end
|
117
|
+
|
118
|
+
autoload :NetHttpConnection, 'slash/nethttp'
|
119
|
+
|
120
|
+
autoload :TyphoeusConnection, 'slash/typhoeus'
|
121
|
+
autoload :TyphoeusQueue, 'slash/typhoeus'
|
122
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Slash
|
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
|
+
"Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Raised when a Timeout::Error occurs.
|
16
|
+
class TimeoutError < ConnectionError
|
17
|
+
def initialize(message)
|
18
|
+
@message = message
|
19
|
+
end
|
20
|
+
def to_s; @message ;end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Raised when a OpenSSL::SSL::SSLError occurs.
|
24
|
+
class SSLError < ConnectionError
|
25
|
+
def initialize(message)
|
26
|
+
@message = message
|
27
|
+
end
|
28
|
+
def to_s; @message ;end
|
29
|
+
end
|
30
|
+
|
31
|
+
# 3xx Redirection
|
32
|
+
class Redirection < ConnectionError # :nodoc:
|
33
|
+
def to_s; response.headers['Location'] ? "#{super} => #{response.headers['Location']}" : super; end
|
34
|
+
end
|
35
|
+
|
36
|
+
# 4xx Client Error
|
37
|
+
class ClientError < ConnectionError; end # :nodoc:
|
38
|
+
|
39
|
+
# 400 Bad Request
|
40
|
+
class BadRequest < ClientError; end # :nodoc
|
41
|
+
|
42
|
+
# 401 Unauthorized
|
43
|
+
class UnauthorizedAccess < ClientError; end # :nodoc
|
44
|
+
|
45
|
+
# 403 Forbidden
|
46
|
+
class ForbiddenAccess < ClientError; end # :nodoc
|
47
|
+
|
48
|
+
# 404 Not Found
|
49
|
+
class ResourceNotFound < ClientError; end # :nodoc:
|
50
|
+
|
51
|
+
# 409 Conflict
|
52
|
+
class ResourceConflict < ClientError; end # :nodoc:
|
53
|
+
|
54
|
+
# 410 Gone
|
55
|
+
class ResourceGone < ClientError; end # :nodoc:
|
56
|
+
|
57
|
+
# 5xx Server Error
|
58
|
+
class ServerError < ConnectionError; end # :nodoc:
|
59
|
+
|
60
|
+
# 405 Method Not Allowed
|
61
|
+
class MethodNotAllowed < ClientError # :nodoc:
|
62
|
+
def allowed_methods
|
63
|
+
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Slash
|
2
|
+
module Formats
|
3
|
+
autoload :JSON, 'slash/json'
|
4
|
+
autoload :PeanutsXML, 'slash/peanuts'
|
5
|
+
|
6
|
+
def self.xml(options = {})
|
7
|
+
Format.new({:mime => 'application/xml'}.update(options))
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.json(options = {})
|
11
|
+
options = {:mime => 'application/json'}.update(options)
|
12
|
+
options[:codec] ||= JSON
|
13
|
+
Format.new(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
class Format
|
17
|
+
attr_reader :mime, :codec
|
18
|
+
|
19
|
+
def initialize(options)
|
20
|
+
@codec = options[:codec]
|
21
|
+
@mime = options[:mime] || (@codec.respond_to?(:mime) ? @codec.mime : nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
def prepare_request(options)
|
25
|
+
headers = options[:headers]
|
26
|
+
headers['Accept'] = mime if mime
|
27
|
+
data = options.delete(:data)
|
28
|
+
if data
|
29
|
+
options[:body] = codec.encode(data)
|
30
|
+
headers['Content-Type'] = mime if mime
|
31
|
+
end
|
32
|
+
options
|
33
|
+
end
|
34
|
+
|
35
|
+
def interpret_response(response)
|
36
|
+
bs = response.body_stream
|
37
|
+
bs && codec.decode(bs)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class WithSuffix < Format
|
42
|
+
attr_reader :suffix
|
43
|
+
|
44
|
+
def initialize(mime, suffix, codec)
|
45
|
+
super(mime, codec)
|
46
|
+
@suffix = suffix
|
47
|
+
end
|
48
|
+
|
49
|
+
def prepare_request(options, &block)
|
50
|
+
options[:path] += suffix if suffix
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.xml(options = {})
|
55
|
+
WithSuffix.new(options.fetch(:mime, 'application/xml'),
|
56
|
+
options.fetch(:suffix, '.xml'),
|
57
|
+
options.fetch(:codec))
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.json(options = {})
|
61
|
+
WithSuffix.new(options.fetch(:mime, 'application/json'),
|
62
|
+
options.fetch(:suffix, '.json'),
|
63
|
+
options.fetch(:codec) { JSON })
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/slash/json.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'date'
|
3
|
+
require 'time'
|
4
|
+
require 'benchmark'
|
5
|
+
require 'stringio'
|
6
|
+
require 'slash/connection'
|
7
|
+
|
8
|
+
|
9
|
+
module Slash
|
10
|
+
# Class to handle connections to remote web services.
|
11
|
+
# This class is used by ActiveResource::Base to interface with REST
|
12
|
+
# services.
|
13
|
+
class NetHttpConnection < Connection
|
14
|
+
@@request_types = {
|
15
|
+
:get => Net::HTTP::Get,
|
16
|
+
:post => Net::HTTP::Post,
|
17
|
+
:put => Net::HTTP::Put,
|
18
|
+
:delete => Net::HTTP::Delete
|
19
|
+
}
|
20
|
+
|
21
|
+
def self.request_types
|
22
|
+
@@request_types
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :proxy, :ssl_options
|
26
|
+
|
27
|
+
# Set the proxy for remote service.
|
28
|
+
def proxy=(proxy)
|
29
|
+
@http = nil
|
30
|
+
@proxy = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set the number of seconds after which HTTP requests to the remote service should time out.
|
34
|
+
def timeout=(timeout)
|
35
|
+
@timeout = timeout
|
36
|
+
configure_http(@http) if @http
|
37
|
+
end
|
38
|
+
|
39
|
+
# Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'.
|
40
|
+
def ssl_options=(opts={})
|
41
|
+
@ssl_options = opts
|
42
|
+
configure_http(@http) if @http
|
43
|
+
end
|
44
|
+
|
45
|
+
def request(method, uri, options = {})
|
46
|
+
raise ArgumentError, 'this connection does not support async mode' if options[:async]
|
47
|
+
|
48
|
+
options = options.dup
|
49
|
+
prepare_request(uri, options)
|
50
|
+
|
51
|
+
rqtype = @@request_types[method] || raise(ArgumentError, "Unsupported method #{method}")
|
52
|
+
params = options[:params]
|
53
|
+
if !params.blank?
|
54
|
+
if [:post, :put].include?(method)
|
55
|
+
form_data = params
|
56
|
+
else
|
57
|
+
uri = uri.dup
|
58
|
+
uri.query_values = (uri.query_values(:notation => :flat) || {}).to_mash.update(params)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
rq = rqtype.new(uri.query.blank? ? uri.path : "#{uri.path}?#{uri.query}", options[:headers])
|
62
|
+
rq.form_data = form_data if form_data
|
63
|
+
rq.body = options[:body] if options[:body]
|
64
|
+
|
65
|
+
resp = http_request(uri, rq)
|
66
|
+
if block_given?
|
67
|
+
yield resp
|
68
|
+
else
|
69
|
+
check_and_raise(resp)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
# Makes request to remote service.
|
75
|
+
def http_request(uri, rq)
|
76
|
+
logger.debug "#{rq.method.to_s.upcase} #{uri}" if logger
|
77
|
+
result = nil
|
78
|
+
ms = 1000 * Benchmark.realtime { result = http(uri).request(rq) }
|
79
|
+
logger.debug "--> %d %s (%d %.0fms)" % [result.code, result.message, result.body ? result.body.length : 0, ms] if logger
|
80
|
+
augment_response(result)
|
81
|
+
rescue Timeout::Error => e
|
82
|
+
raise TimeoutError.new(e.message)
|
83
|
+
rescue OpenSSL::SSL::SSLError => e
|
84
|
+
raise SSLError.new(e.message)
|
85
|
+
end
|
86
|
+
|
87
|
+
def augment_response(response)
|
88
|
+
class << response
|
89
|
+
attr_accessor :exception
|
90
|
+
alias headers to_hash
|
91
|
+
def body_stream
|
92
|
+
body && StringIO.new(body)
|
93
|
+
end
|
94
|
+
def success?
|
95
|
+
exception.nil?
|
96
|
+
end
|
97
|
+
end
|
98
|
+
handle_response(response)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Creates new Net::HTTP instance for communication with
|
102
|
+
# remote service and resources.
|
103
|
+
def http(uri)
|
104
|
+
if !@http || @host != uri.normalized_host || @port != uri.inferred_port || @scheme != uri.normalized_scheme
|
105
|
+
@host, @port, @scheme = uri.normalized_host, uri.inferred_port, uri.normalized_scheme
|
106
|
+
@http = configure_http(new_http)
|
107
|
+
end
|
108
|
+
@http
|
109
|
+
end
|
110
|
+
|
111
|
+
def new_http
|
112
|
+
if @proxy
|
113
|
+
Net::HTTP.new(@host, @port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
|
114
|
+
else
|
115
|
+
Net::HTTP.new(@host, @port)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def configure_http(http)
|
120
|
+
http = apply_ssl_options(http)
|
121
|
+
|
122
|
+
# Net::HTTP timeouts default to 60 seconds.
|
123
|
+
if @timeout
|
124
|
+
http.open_timeout = http.read_timeout = @timeout
|
125
|
+
end
|
126
|
+
|
127
|
+
http
|
128
|
+
end
|
129
|
+
|
130
|
+
def apply_ssl_options(http)
|
131
|
+
return http unless @scheme == 'https'
|
132
|
+
|
133
|
+
http.use_ssl = true
|
134
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
135
|
+
return http unless defined?(@ssl_options)
|
136
|
+
|
137
|
+
http.ca_path = @ssl_options[:ca_path] if @ssl_options[:ca_path]
|
138
|
+
http.ca_file = @ssl_options[:ca_file] if @ssl_options[:ca_file]
|
139
|
+
|
140
|
+
http.cert = @ssl_options[:cert] if @ssl_options[:cert]
|
141
|
+
http.key = @ssl_options[:key] if @ssl_options[:key]
|
142
|
+
|
143
|
+
http.cert_store = @ssl_options[:cert_store] if @ssl_options[:cert_store]
|
144
|
+
http.ssl_timeout = @ssl_options[:ssl_timeout] if @ssl_options[:ssl_timeout]
|
145
|
+
|
146
|
+
http.verify_mode = @ssl_options[:verify_mode] if @ssl_options[:verify_mode]
|
147
|
+
http.verify_callback = @ssl_options[:verify_callback] if @ssl_options[:verify_callback]
|
148
|
+
http.verify_depth = @ssl_options[:verify_depth] if @ssl_options[:verify_depth]
|
149
|
+
|
150
|
+
http
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'peanuts'
|
2
|
+
require 'slash/formats'
|
3
|
+
|
4
|
+
module Slash
|
5
|
+
module Formats
|
6
|
+
class PeanutsXML
|
7
|
+
attr_reader :response_type
|
8
|
+
attr_accessor :to_xml_options, :from_xml_options
|
9
|
+
|
10
|
+
def initialize(response_type, from_xml_options = {}, to_xml_options = {})
|
11
|
+
@response_type = response_type
|
12
|
+
@to_xml_options, @from_xml_options = to_xml_options, from_xml_options
|
13
|
+
end
|
14
|
+
|
15
|
+
def encode(data)
|
16
|
+
data.to_xml(:string, to_xml_options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def decode(data)
|
20
|
+
response_type.from_xml(data, from_xml_options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'addressable/uri'
|
3
|
+
require 'slash/connection'
|
4
|
+
require 'slash/formats'
|
5
|
+
|
6
|
+
|
7
|
+
module Slash
|
8
|
+
class Resource
|
9
|
+
class Response
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def initialize(result, response, exception)
|
13
|
+
@result, @response, @exception = result, response, exception
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :result, :response, :exception
|
17
|
+
|
18
|
+
def_delegators :response, :code, :headers
|
19
|
+
|
20
|
+
def result!
|
21
|
+
raise exception if exception
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
def success?
|
26
|
+
exception.nil?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
extend Forwardable
|
31
|
+
|
32
|
+
attr_accessor :connection, :uri, :params, :headers, :user, :password, :timeout, :proxy
|
33
|
+
|
34
|
+
def_delegator :uri, :path
|
35
|
+
def_delegator :uri, :query_values, :query
|
36
|
+
|
37
|
+
def user_agent
|
38
|
+
headers['User-Agent']
|
39
|
+
end
|
40
|
+
|
41
|
+
def user_agent=(value)
|
42
|
+
headers['User-Agent'] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.new!(*args, &block)
|
46
|
+
r = allocate
|
47
|
+
r.send(:initialize!, *args, &block)
|
48
|
+
r
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(connection, uri, options = {})
|
52
|
+
@connection = connection
|
53
|
+
@uri = Addressable::URI.parse(uri)
|
54
|
+
query = options[:query]
|
55
|
+
unless query.blank?
|
56
|
+
@uri = @uri.dup
|
57
|
+
uq = @uri.query_values
|
58
|
+
@uri.query_values = uq ? uq.merge(query) : query
|
59
|
+
end
|
60
|
+
@params, @headers = (options[:params] || {}).to_mash, options[:headers] || {}
|
61
|
+
self.user_agent ||= options[:user_agent] || Slash::USER_AGENT
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize!(from, options)
|
65
|
+
@connection = from.connection
|
66
|
+
options = _merge(from, options)
|
67
|
+
@uri, @params, @headers = options[:uri], options[:params], options[:headers]
|
68
|
+
end
|
69
|
+
private :initialize!
|
70
|
+
|
71
|
+
def slash(options = {})
|
72
|
+
self.class.new!(self, options)
|
73
|
+
end
|
74
|
+
|
75
|
+
def [](path)
|
76
|
+
slash(:path => path)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Execute a GET request.
|
80
|
+
# Used to get (find) resources.
|
81
|
+
def get(options = {}, &block)
|
82
|
+
request(options.merge(:method => :get), &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
|
86
|
+
# Used to delete resources.
|
87
|
+
def delete(options = {}, &block)
|
88
|
+
request(options.merge(:method => :delete), &block)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Execute a PUT request.
|
92
|
+
# Used to update resources.
|
93
|
+
def put(options = {}, &block)
|
94
|
+
request(options.merge(:method => :put), &block)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Execute a POST request.
|
98
|
+
# Used to create new resources.
|
99
|
+
def post(options = {}, &block)
|
100
|
+
request(options.merge(:method => :post), &block)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Execute a HEAD request.
|
104
|
+
# Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
|
105
|
+
def head(options = {}, &block)
|
106
|
+
request(options.merge(:method => :head), &block)
|
107
|
+
end
|
108
|
+
|
109
|
+
def request(options)
|
110
|
+
rq = prepare_request(merge(options))
|
111
|
+
connection.request(rq.delete(:method), rq.delete(:uri), rq) do |response|
|
112
|
+
resp = handle_response(response)
|
113
|
+
block_given? ? yield(resp) : resp
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
def prepare_request(options)
|
119
|
+
options[:body] = options.delete(:data).to_s
|
120
|
+
options
|
121
|
+
end
|
122
|
+
|
123
|
+
def handle_response(response)
|
124
|
+
begin
|
125
|
+
exception = response.exception
|
126
|
+
rescue => e
|
127
|
+
exception = e
|
128
|
+
end
|
129
|
+
Response.new(prepare_result(response), response, exception)
|
130
|
+
end
|
131
|
+
|
132
|
+
def prepare_result(response)
|
133
|
+
response.body
|
134
|
+
end
|
135
|
+
|
136
|
+
def merge(options, &block)
|
137
|
+
_merge(self, options, &block)
|
138
|
+
end
|
139
|
+
|
140
|
+
def _merge(from, options)
|
141
|
+
options = options.dup
|
142
|
+
path, query, params, headers = options[:path], options[:query], options[:params], options[:headers]
|
143
|
+
|
144
|
+
u = options[:uri] = from.uri.dup
|
145
|
+
|
146
|
+
uq = u.query_values(:notation => :flat)
|
147
|
+
uq = uq ? (query ? uq.to_mash.merge(query) : uq) : query
|
148
|
+
if path
|
149
|
+
upath = u.path
|
150
|
+
u.path = upath + '/' unless upath =~ /\/\z/
|
151
|
+
u.join!(path)
|
152
|
+
end
|
153
|
+
if uq
|
154
|
+
u.query_values = uq
|
155
|
+
else
|
156
|
+
u.query = nil
|
157
|
+
end
|
158
|
+
|
159
|
+
p = options[:params] = from.params.dup
|
160
|
+
p.merge!(params) unless params.blank?
|
161
|
+
|
162
|
+
h = options[:headers] = from.headers.dup
|
163
|
+
h.merge!(headers) unless headers.blank?
|
164
|
+
|
165
|
+
options
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class SimpleResource < Resource
|
170
|
+
attr_accessor :format
|
171
|
+
|
172
|
+
def initialize(uri, options = {})
|
173
|
+
super(options[:connection] || create_connection, uri, options)
|
174
|
+
self.format = options[:format]
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
def initialize!(from, options)
|
179
|
+
super
|
180
|
+
self.format = from.format
|
181
|
+
end
|
182
|
+
|
183
|
+
def create_connection
|
184
|
+
Connection.create_default
|
185
|
+
end
|
186
|
+
|
187
|
+
def prepare_request(options)
|
188
|
+
format ? format.prepare_request(options) : super
|
189
|
+
end
|
190
|
+
|
191
|
+
def prepare_result(response)
|
192
|
+
response.success? && format ? format.interpret_response(response) : super
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'typhoeus'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'stringio'
|
4
|
+
require 'slash/connection'
|
5
|
+
|
6
|
+
|
7
|
+
module Slash
|
8
|
+
class TyphoeusQueue < Queue
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def initialize(hydra_or_options = nil)
|
12
|
+
case hydra_or_options
|
13
|
+
when nil
|
14
|
+
@hydra = Typhoeus::Hydra.new
|
15
|
+
@hydra.disable_memoization
|
16
|
+
when Hash
|
17
|
+
@hydra = Typhoeus::Hydra.new(hydra_or_options)
|
18
|
+
@hydra.disable_memoization
|
19
|
+
else
|
20
|
+
@hydra = hydra_or_options
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_accessor :hydra
|
25
|
+
|
26
|
+
def_delegator :hydra, :queue, :submit
|
27
|
+
def_delegator :hydra, :run
|
28
|
+
end
|
29
|
+
|
30
|
+
class TyphoeusConnection < Connection
|
31
|
+
def initialize(options = {})
|
32
|
+
@queue = options[:queue] || TyphoeusQueue.new
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_accessor :queue
|
36
|
+
|
37
|
+
def request(method, uri, options = {})
|
38
|
+
options = options.dup
|
39
|
+
prepare_request(uri, options)
|
40
|
+
|
41
|
+
params, headers = options[:params], options[:headers]
|
42
|
+
rq = Typhoeus::Request.new(uri.to_s,
|
43
|
+
:method => method,
|
44
|
+
:headers => headers,
|
45
|
+
:params => !params.blank? ? params.inject({}) {|h, x| h[x[0].to_s] = x[1] || ''; h } : nil,
|
46
|
+
:body => options[:body],
|
47
|
+
:timeout => options[:timeout] || timeout,
|
48
|
+
:user_agent => headers['User-Agent']
|
49
|
+
)
|
50
|
+
ret = nil
|
51
|
+
rq.on_complete do |response|
|
52
|
+
if logger
|
53
|
+
logger.debug "%s %s --> %d (%d %.0fs)" % [rq.method.to_s.upcase, rq.url,
|
54
|
+
response.code, response.body ? response.body.length : 0, response.time]
|
55
|
+
end
|
56
|
+
ret = response = augment_response(response)
|
57
|
+
ret = yield response if block_given?
|
58
|
+
response
|
59
|
+
end
|
60
|
+
async = options[:async]
|
61
|
+
queue = [true, false, nil].include?(async) ? self.queue : async
|
62
|
+
queue.submit(rq)
|
63
|
+
if async
|
64
|
+
queue
|
65
|
+
else
|
66
|
+
queue.run
|
67
|
+
block_given? ? ret : check_and_raise(rq.handled_response)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def augment_response(response)
|
73
|
+
class << response
|
74
|
+
attr_accessor :exception
|
75
|
+
def body_stream
|
76
|
+
body && StringIO.new(body)
|
77
|
+
end
|
78
|
+
def success?
|
79
|
+
exception.nil?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
handle_response(response)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: slash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 4
|
8
|
+
- 4
|
9
|
+
version: 0.4.4
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Igor Gunko
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-10 00:00:00 +02:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: extlib
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
- 9
|
30
|
+
- 14
|
31
|
+
version: 0.9.14
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: addressable
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 2
|
43
|
+
- 1
|
44
|
+
- 1
|
45
|
+
version: 2.1.1
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rspec
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 1
|
57
|
+
- 2
|
58
|
+
- 8
|
59
|
+
version: 1.2.8
|
60
|
+
type: :development
|
61
|
+
version_requirements: *id003
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: typhoeus
|
64
|
+
prerelease: false
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
- 1
|
72
|
+
- 22
|
73
|
+
version: 0.1.22
|
74
|
+
type: :development
|
75
|
+
version_requirements: *id004
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: peanuts
|
78
|
+
prerelease: false
|
79
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ~>
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
segments:
|
84
|
+
- 2
|
85
|
+
- 1
|
86
|
+
- 1
|
87
|
+
version: 2.1.1
|
88
|
+
type: :development
|
89
|
+
version_requirements: *id005
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: json
|
92
|
+
prerelease: false
|
93
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ~>
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
segments:
|
98
|
+
- 1
|
99
|
+
- 2
|
100
|
+
- 2
|
101
|
+
version: 1.2.2
|
102
|
+
type: :development
|
103
|
+
version_requirements: *id006
|
104
|
+
description: " REST-client\n"
|
105
|
+
email: tekmon@gmail.com
|
106
|
+
executables: []
|
107
|
+
|
108
|
+
extensions: []
|
109
|
+
|
110
|
+
extra_rdoc_files:
|
111
|
+
- README.rdoc
|
112
|
+
- MIT-LICENSE
|
113
|
+
files:
|
114
|
+
- README.rdoc
|
115
|
+
- MIT-LICENSE
|
116
|
+
- Rakefile
|
117
|
+
- lib/slash.rb
|
118
|
+
- lib/slash/exceptions.rb
|
119
|
+
- lib/slash/connection.rb
|
120
|
+
- lib/slash/nethttp.rb
|
121
|
+
- lib/slash/typhoeus.rb
|
122
|
+
- lib/slash/resource.rb
|
123
|
+
- lib/slash/formats.rb
|
124
|
+
- lib/slash/json.rb
|
125
|
+
- lib/slash/peanuts.rb
|
126
|
+
has_rdoc: true
|
127
|
+
homepage: http://github.com/omg/slash
|
128
|
+
licenses: []
|
129
|
+
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options:
|
132
|
+
- --line-numbers
|
133
|
+
- --main
|
134
|
+
- README.rdoc
|
135
|
+
require_paths:
|
136
|
+
- lib
|
137
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
segments:
|
142
|
+
- 0
|
143
|
+
version: "0"
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
segments:
|
149
|
+
- 0
|
150
|
+
version: "0"
|
151
|
+
requirements: []
|
152
|
+
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 1.3.6
|
155
|
+
signing_key:
|
156
|
+
specification_version: 2
|
157
|
+
summary: REST-client
|
158
|
+
test_files:
|
159
|
+
- spec/resource_spec.rb
|