faraday 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/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +88 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/faraday.gemspec +66 -0
- data/lib/faraday.rb +47 -0
- data/lib/faraday/adapter/mock_request.rb +68 -0
- data/lib/faraday/adapter/net_http.rb +22 -0
- data/lib/faraday/adapter/typhoeus.rb +55 -0
- data/lib/faraday/connection.rb +100 -0
- data/lib/faraday/error.rb +5 -0
- data/lib/faraday/response.rb +41 -0
- data/lib/faraday/response/yajl_response.rb +35 -0
- data/test/adapter/typhoeus_test.rb +26 -0
- data/test/adapter_test.rb +59 -0
- data/test/connection_test.rb +123 -0
- data/test/helper.rb +19 -0
- data/test/live_server.rb +10 -0
- data/test/response_test.rb +34 -0
- metadata +81 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 rick
|
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,88 @@
|
|
1
|
+
= faraday
|
2
|
+
|
3
|
+
Experiments in a REST API lib
|
4
|
+
|
5
|
+
Super alpha! Don't use it if you mind throwing away all the code when I change
|
6
|
+
the API on a whim.
|
7
|
+
|
8
|
+
This mess is gonna get raw, like sushi. So, haters to the left.
|
9
|
+
|
10
|
+
== Usage
|
11
|
+
|
12
|
+
# uses Net/HTTP, no response parsing
|
13
|
+
conn = Faraday::Connection.new("http://sushi.com")
|
14
|
+
conn.extend Faraday::Adapter::NetHttp
|
15
|
+
resp = conn.get("/sake.json")
|
16
|
+
resp.body # => %({"name":"Sake"})
|
17
|
+
|
18
|
+
# uses Net/HTTP, Yajl parsing
|
19
|
+
conn = Faraday::Connection.new("http://sushi.com")
|
20
|
+
conn.extend Faraday::Adapter::NetHttp
|
21
|
+
conn.response_class = Faraday::Response::YajlResponse
|
22
|
+
resp = conn.get("/sake.json")
|
23
|
+
resp.body # => {"name": "Sake"}
|
24
|
+
|
25
|
+
# uses Typhoeus, no response parsing
|
26
|
+
conn = Faraday::Connection.new("http://sushi.com")
|
27
|
+
conn.extend Faraday::Adapter::Typhoeus
|
28
|
+
resp = conn.get("/sake.json")
|
29
|
+
resp.body # => %({"name":"Sake"})
|
30
|
+
|
31
|
+
# uses Typhoeus, Yajl parsing, performs requests in parallel
|
32
|
+
conn = Faraday::Connection.new("http://sushi.com")
|
33
|
+
conn.extend Faraday::Adapter::Typhoeus
|
34
|
+
conn.response_class = Faraday::Response::YajlResponse
|
35
|
+
resp1, resp2 = nil, nil
|
36
|
+
conn.in_parallel do
|
37
|
+
resp1 = conn.get("/sake.json")
|
38
|
+
resp2 = conn.get("/unagi.json")
|
39
|
+
|
40
|
+
# requests have not been made yet
|
41
|
+
resp1.body # => nil
|
42
|
+
resp2.body # => nil
|
43
|
+
end
|
44
|
+
resp1.body # => {"name": "Sake"}
|
45
|
+
resp2.body # => {"name": "Unagi"}
|
46
|
+
|
47
|
+
== Testing
|
48
|
+
|
49
|
+
* Yajl is needed for tests :(
|
50
|
+
* Live Sinatra server is required for tests: `ruby test/live_server.rb` to start it.
|
51
|
+
|
52
|
+
=== Writing tests based on faraday
|
53
|
+
|
54
|
+
Using the MockRequest connection adapter you can implement your own test
|
55
|
+
connection class:
|
56
|
+
|
57
|
+
class TestConnection < Faraday::Connection
|
58
|
+
extend Faraday::Adapter::MockRequest
|
59
|
+
end
|
60
|
+
|
61
|
+
conn = TestConnection.new do |stub|
|
62
|
+
stub.get('/hello.json', { 'hi world' }
|
63
|
+
end
|
64
|
+
resp = conn.get '/hello.json'
|
65
|
+
resp.body # => 'hi world'
|
66
|
+
resp = conn.get '/whatever' # => <not stubbed, raises connection error>
|
67
|
+
|
68
|
+
== TODO
|
69
|
+
|
70
|
+
* other HTTP methods besides just GET
|
71
|
+
* gracefully skip tests for Yajl and other optional libraries if they don't exist.
|
72
|
+
* gracefully skip live http server tests if the sinatra server is not running.
|
73
|
+
* use Typhoeus' request mocking facilities in the Typhoeus adapter test
|
74
|
+
* lots of other crap
|
75
|
+
|
76
|
+
== Note on Patches/Pull Requests
|
77
|
+
|
78
|
+
* Fork the project.
|
79
|
+
* Make your feature addition or bug fix.
|
80
|
+
* Add tests for it. This is important so I don't break it in a
|
81
|
+
future version unintentionally.
|
82
|
+
* Commit, do not mess with rakefile, version, or history.
|
83
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
84
|
+
* Send me a pull request. Bonus points for topic branches.
|
85
|
+
|
86
|
+
== Copyright
|
87
|
+
|
88
|
+
Copyright (c) 2009 rick. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "faraday"
|
8
|
+
gem.summary = "HTTP/REST API client library"
|
9
|
+
gem.description = "HTTP/REST API client library with pluggable components"
|
10
|
+
gem.email = "technoweenie@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/technoweenie/faraday"
|
12
|
+
gem.authors = ["rick"]
|
13
|
+
end
|
14
|
+
Jeweler::GemcutterTasks.new
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
Rake::TestTask.new(:test) do |test|
|
21
|
+
test.libs << 'lib' << 'test'
|
22
|
+
test.pattern = 'test/**/*_test.rb'
|
23
|
+
test.verbose = true
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'rcov/rcovtask'
|
28
|
+
Rcov::RcovTask.new do |test|
|
29
|
+
test.libs << 'test'
|
30
|
+
test.pattern = 'test/**/test_*.rb'
|
31
|
+
test.verbose = true
|
32
|
+
end
|
33
|
+
rescue LoadError
|
34
|
+
task :rcov do
|
35
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
task :test => :check_dependencies
|
40
|
+
|
41
|
+
task :default => :test
|
42
|
+
|
43
|
+
require 'rake/rdoctask'
|
44
|
+
Rake::RDocTask.new do |rdoc|
|
45
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
46
|
+
|
47
|
+
rdoc.rdoc_dir = 'rdoc'
|
48
|
+
rdoc.title = "faraday #{version}"
|
49
|
+
rdoc.rdoc_files.include('README*')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/faraday.gemspec
ADDED
@@ -0,0 +1,66 @@
|
|
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{faraday}
|
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 = ["rick"]
|
12
|
+
s.date = %q{2009-12-19}
|
13
|
+
s.description = %q{HTTP/REST API client library with pluggable components}
|
14
|
+
s.email = %q{technoweenie@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"faraday.gemspec",
|
27
|
+
"lib/faraday.rb",
|
28
|
+
"lib/faraday/adapter/mock_request.rb",
|
29
|
+
"lib/faraday/adapter/net_http.rb",
|
30
|
+
"lib/faraday/adapter/typhoeus.rb",
|
31
|
+
"lib/faraday/connection.rb",
|
32
|
+
"lib/faraday/error.rb",
|
33
|
+
"lib/faraday/response.rb",
|
34
|
+
"lib/faraday/response/yajl_response.rb",
|
35
|
+
"test/adapter/typhoeus_test.rb",
|
36
|
+
"test/adapter_test.rb",
|
37
|
+
"test/connection_test.rb",
|
38
|
+
"test/helper.rb",
|
39
|
+
"test/live_server.rb",
|
40
|
+
"test/response_test.rb"
|
41
|
+
]
|
42
|
+
s.homepage = %q{http://github.com/technoweenie/faraday}
|
43
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
44
|
+
s.require_paths = ["lib"]
|
45
|
+
s.rubygems_version = %q{1.3.5}
|
46
|
+
s.summary = %q{HTTP/REST API client library}
|
47
|
+
s.test_files = [
|
48
|
+
"test/adapter/typhoeus_test.rb",
|
49
|
+
"test/adapter_test.rb",
|
50
|
+
"test/connection_test.rb",
|
51
|
+
"test/helper.rb",
|
52
|
+
"test/live_server.rb",
|
53
|
+
"test/response_test.rb"
|
54
|
+
]
|
55
|
+
|
56
|
+
if s.respond_to? :specification_version then
|
57
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
58
|
+
s.specification_version = 3
|
59
|
+
|
60
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
61
|
+
else
|
62
|
+
end
|
63
|
+
else
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
data/lib/faraday.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Faraday
|
2
|
+
# Loads each autoloaded constant. If thread safety is a concern, wrap
|
3
|
+
# this in a Mutex.
|
4
|
+
def self.load
|
5
|
+
constants.each do |const|
|
6
|
+
const_get(const) if autoload?(const)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
autoload :Connection, 'faraday/connection'
|
11
|
+
autoload :Response, 'faraday/response'
|
12
|
+
autoload :Error, 'faraday/error'
|
13
|
+
|
14
|
+
module Adapter
|
15
|
+
autoload :NetHttp, 'faraday/adapter/net_http'
|
16
|
+
autoload :Typhoeus, 'faraday/adapter/typhoeus'
|
17
|
+
autoload :MockRequest, 'faraday/adapter/mock_request'
|
18
|
+
|
19
|
+
# Names of available adapters. Should not actually load them.
|
20
|
+
def self.adapters
|
21
|
+
constants
|
22
|
+
end
|
23
|
+
|
24
|
+
# Array of Adapters. These have been loaded and confirmed to work (right gems, etc).
|
25
|
+
def self.loaded_adapters
|
26
|
+
adapters.map { |c| const_get(c) }.select { |a| a.loaded? }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# not pulling in active-support JUST for this method.
|
32
|
+
class Object
|
33
|
+
# Yields <code>x</code> to the block, and then returns <code>x</code>.
|
34
|
+
# The primary purpose of this method is to "tap into" a method chain,
|
35
|
+
# in order to perform operations on intermediate results within the chain.
|
36
|
+
#
|
37
|
+
# (1..10).tap { |x| puts "original: #{x.inspect}" }.to_a.
|
38
|
+
# tap { |x| puts "array: #{x.inspect}" }.
|
39
|
+
# select { |x| x%2 == 0 }.
|
40
|
+
# tap { |x| puts "evens: #{x.inspect}" }.
|
41
|
+
# map { |x| x*x }.
|
42
|
+
# tap { |x| puts "squares: #{x.inspect}" }
|
43
|
+
def tap
|
44
|
+
yield self
|
45
|
+
self
|
46
|
+
end unless Object.respond_to?(:tap)
|
47
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Faraday
|
2
|
+
module Adapter
|
3
|
+
module MockRequest
|
4
|
+
extend Faraday::Connection::Options
|
5
|
+
def self.loaded?() false end
|
6
|
+
|
7
|
+
include Faraday::Error # ConnectionFailed
|
8
|
+
|
9
|
+
class Stubs
|
10
|
+
def initialize
|
11
|
+
# {:get => [Stub, Stub]}
|
12
|
+
@stack = {}
|
13
|
+
yield self if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def empty?
|
17
|
+
@stack.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def match(request_method, path, request_headers)
|
21
|
+
return false if !@stack.key?(request_method)
|
22
|
+
@stack[request_method].detect { |stub| stub.matches?(path, request_headers) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(path, request_headers = {}, &block)
|
26
|
+
(@stack[:get] ||= []) << new_stub(path, request_headers, block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def new_stub(path, request_headers, block)
|
30
|
+
status, response_headers, body = block.call
|
31
|
+
Stub.new(path, request_headers, status, response_headers, body)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Stub < Struct.new(:path, :request_headers, :status, :response_headers, :body)
|
36
|
+
def matches?(request_path, headers)
|
37
|
+
return false if request_path != path
|
38
|
+
return true if request_headers.empty?
|
39
|
+
request_headers.each do |key, value|
|
40
|
+
return true if headers[key] == value
|
41
|
+
end
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize &block
|
47
|
+
super nil
|
48
|
+
yield stubs
|
49
|
+
end
|
50
|
+
|
51
|
+
def stubs
|
52
|
+
@stubs ||= Stubs.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def _get(uri, headers)
|
56
|
+
raise ConnectionFailed, "no stubbed requests" if stubs.empty?
|
57
|
+
if stub = @stubs.match(:get, uri.path, headers)
|
58
|
+
response_class.new do |resp|
|
59
|
+
resp.headers = stub.response_headers
|
60
|
+
resp.process stub.body
|
61
|
+
end
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
module Faraday
|
3
|
+
module Adapter
|
4
|
+
module NetHttp
|
5
|
+
extend Faraday::Connection::Options
|
6
|
+
|
7
|
+
def _get(uri, request_headers)
|
8
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
9
|
+
response_class.new do |resp|
|
10
|
+
http_resp = http.get(uri.path, request_headers) do |chunk|
|
11
|
+
resp.process(chunk)
|
12
|
+
end
|
13
|
+
http_resp.each_header do |key, value|
|
14
|
+
resp.headers[key] = value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
rescue Errno::ECONNREFUSED
|
18
|
+
raise Faraday::Error::ConnectionFailed, "connection refused"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Faraday
|
2
|
+
module Adapter
|
3
|
+
module Typhoeus
|
4
|
+
extend Faraday::Connection::Options
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'typhoeus'
|
8
|
+
|
9
|
+
def in_parallel?
|
10
|
+
!!@parallel_manager
|
11
|
+
end
|
12
|
+
|
13
|
+
def in_parallel(options = {})
|
14
|
+
setup_parallel_manager(options)
|
15
|
+
yield
|
16
|
+
run_parallel_requests
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_parallel_manager(options = {})
|
20
|
+
@parallel_manager ||= ::Typhoeus::Hydra.new(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_parallel_requests
|
24
|
+
@parallel_manager.run
|
25
|
+
@parallel_manager = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def _get(uri, request_headers)
|
29
|
+
response_class.new do |resp|
|
30
|
+
is_async = in_parallel?
|
31
|
+
setup_parallel_manager
|
32
|
+
req = ::Typhoeus::Request.new(uri.to_s, :headers => request_headers, :method => :get)
|
33
|
+
req.on_complete do |response|
|
34
|
+
resp.process!(response.body)
|
35
|
+
resp.headers = parse_response_headers(response.headers)
|
36
|
+
end
|
37
|
+
@parallel_manager.queue(req)
|
38
|
+
if !is_async then run_parallel_requests end
|
39
|
+
end
|
40
|
+
rescue Errno::ECONNREFUSED
|
41
|
+
raise Faraday::Error::ConnectionFailed, "connection refused"
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_response_headers(header_string)
|
45
|
+
Hash[*header_string.split(/\r\n/).
|
46
|
+
tap { |a| a.shift }. # drop the HTTP status line
|
47
|
+
map! { |h| h.split(/:\s+/,2) }. # split key and value
|
48
|
+
map! { |(k, v)| [k.downcase, v] }.flatten!]
|
49
|
+
end
|
50
|
+
rescue LoadError => e
|
51
|
+
self.load_error = e
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
module Faraday
|
3
|
+
class Connection
|
4
|
+
module Options
|
5
|
+
def load_error() @load_error end
|
6
|
+
def load_error=(v) @load_error = v end
|
7
|
+
def supports_async() @supports_async end
|
8
|
+
def supports_async=(v) @supports_async = v end
|
9
|
+
def loaded?() !@load_error end
|
10
|
+
alias supports_async? supports_async
|
11
|
+
end
|
12
|
+
|
13
|
+
include Addressable
|
14
|
+
|
15
|
+
attr_accessor :host, :port, :scheme
|
16
|
+
attr_reader :path_prefix
|
17
|
+
|
18
|
+
def initialize(url = nil)
|
19
|
+
@response_class = nil
|
20
|
+
if url
|
21
|
+
uri = URI.parse(url)
|
22
|
+
self.scheme = uri.scheme
|
23
|
+
self.host = uri.host
|
24
|
+
self.port = uri.port
|
25
|
+
self.path_prefix = uri.path
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Override in a subclass, or include an adapter
|
30
|
+
#
|
31
|
+
# def _get(uri, headers)
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
def get(url, params = {}, headers = {})
|
35
|
+
_get(build_uri(url, params), headers)
|
36
|
+
end
|
37
|
+
|
38
|
+
def response_class
|
39
|
+
@response_class || Response
|
40
|
+
end
|
41
|
+
|
42
|
+
def response_class=(v)
|
43
|
+
if v.respond_to?(:loaded?) && !v.loaded?
|
44
|
+
raise ArgumentError, "The response class: #{v.inspect} does not appear to be loaded."
|
45
|
+
end
|
46
|
+
@response_class = v
|
47
|
+
end
|
48
|
+
|
49
|
+
def in_parallel?
|
50
|
+
!!@parallel_manager
|
51
|
+
end
|
52
|
+
|
53
|
+
def in_parallel(options = {})
|
54
|
+
@parallel_manager = true
|
55
|
+
yield
|
56
|
+
@parallel_manager = false
|
57
|
+
end
|
58
|
+
|
59
|
+
def setup_parallel_manager(options = {})
|
60
|
+
end
|
61
|
+
|
62
|
+
def run_parallel_requests
|
63
|
+
end
|
64
|
+
|
65
|
+
def path_prefix=(value)
|
66
|
+
if value
|
67
|
+
value.chomp! "/"
|
68
|
+
value.replace "/#{value}" if value !~ /^\//
|
69
|
+
end
|
70
|
+
@path_prefix = value
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_uri(url, params = {})
|
74
|
+
uri = URI.parse(url)
|
75
|
+
uri.scheme ||= @scheme
|
76
|
+
uri.host ||= @host
|
77
|
+
uri.port ||= @port
|
78
|
+
if @path_prefix && uri.path !~ /^\//
|
79
|
+
uri.path = "#{@path_prefix.size > 1 ? @path_prefix : nil}/#{uri.path}"
|
80
|
+
end
|
81
|
+
query = params_to_query(params)
|
82
|
+
if !query.empty? then uri.query = query end
|
83
|
+
uri
|
84
|
+
end
|
85
|
+
|
86
|
+
def params_to_query(params)
|
87
|
+
params.inject([]) do |memo, (key, val)|
|
88
|
+
memo << "#{escape_for_querystring(key)}=#{escape_for_querystring(val)}"
|
89
|
+
end.join("&")
|
90
|
+
end
|
91
|
+
|
92
|
+
# Some servers convert +'s in URL query params to spaces.
|
93
|
+
# Go ahead and encode it.
|
94
|
+
def escape_for_querystring(s)
|
95
|
+
URI.encode_component(s, Addressable::URI::CharacterClasses::QUERY).tap do |escaped|
|
96
|
+
escaped.gsub! /\+/, "%2B"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Faraday
|
2
|
+
class Response < Struct.new(:headers, :body)
|
3
|
+
class << self
|
4
|
+
attr_accessor :load_error
|
5
|
+
def loaded?
|
6
|
+
!load_error
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
autoload :YajlResponse, 'faraday/response/yajl_response'
|
11
|
+
|
12
|
+
def initialize(headers = nil, body = nil)
|
13
|
+
super(headers || {}, body)
|
14
|
+
if block_given?
|
15
|
+
yield self
|
16
|
+
processed!
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO: process is a funky name. change it
|
21
|
+
# processes a chunk of the streamed body.
|
22
|
+
def process(chunk)
|
23
|
+
if !body
|
24
|
+
self.body = []
|
25
|
+
end
|
26
|
+
body << chunk
|
27
|
+
end
|
28
|
+
|
29
|
+
# Assume the given content is the full body, and not streamed.
|
30
|
+
def process!(full_body)
|
31
|
+
process(full_body)
|
32
|
+
processed!
|
33
|
+
end
|
34
|
+
|
35
|
+
# Signals the end of streamed content. Do whatever you need to clean up
|
36
|
+
# the streamed body.
|
37
|
+
def processed!
|
38
|
+
self.body = body.join if body.respond_to?(:join)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Faraday
|
2
|
+
class Response
|
3
|
+
class YajlResponse < Response
|
4
|
+
attr_reader :body
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'yajl'
|
8
|
+
|
9
|
+
def initialize(headers = nil, body = nil)
|
10
|
+
super
|
11
|
+
@parser = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def process(chunk)
|
15
|
+
if !@parser
|
16
|
+
@parser = Yajl::Parser.new
|
17
|
+
@parser.on_parse_complete = method(:object_parsed)
|
18
|
+
end
|
19
|
+
@parser << chunk
|
20
|
+
end
|
21
|
+
|
22
|
+
def processed!
|
23
|
+
@parser = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def object_parsed(obj)
|
27
|
+
@body = obj
|
28
|
+
end
|
29
|
+
|
30
|
+
rescue LoadError => e
|
31
|
+
self.load_error = e
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'helper'))
|
2
|
+
|
3
|
+
if Faraday::Adapter::Typhoeus.loaded?
|
4
|
+
class TyphoeusTest < Faraday::TestCase
|
5
|
+
describe "#parse_response_headers" do
|
6
|
+
before do
|
7
|
+
@conn = Object.new.extend(Faraday::Adapter::Typhoeus)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "leaves http status line out" do
|
11
|
+
headers = @conn.parse_response_headers("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n")
|
12
|
+
assert_equal %w(content-type), headers.keys
|
13
|
+
end
|
14
|
+
|
15
|
+
it "parses lower-cased header name and value" do
|
16
|
+
headers = @conn.parse_response_headers("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n")
|
17
|
+
assert_equal 'text/html', headers['content-type']
|
18
|
+
end
|
19
|
+
|
20
|
+
it "parses lower-cased header name and value with colon" do
|
21
|
+
headers = @conn.parse_response_headers("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nLocation: http://sushi.com/\r\n\r\n")
|
22
|
+
assert_equal 'http://sushi.com/', headers['location']
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
+
|
3
|
+
class AdapterTest < Faraday::TestCase
|
4
|
+
before do
|
5
|
+
@connection = Faraday::Connection.new(LIVE_SERVER)
|
6
|
+
end
|
7
|
+
|
8
|
+
Faraday::Adapter.loaded_adapters.each do |adapter|
|
9
|
+
describe "#get with #{adapter} adapter" do
|
10
|
+
before do
|
11
|
+
@connection.extend adapter
|
12
|
+
end
|
13
|
+
|
14
|
+
it "retrieves the response body" do
|
15
|
+
assert_equal 'hello world', @connection.get('hello_world').body
|
16
|
+
end
|
17
|
+
|
18
|
+
it "retrieves the response body with YajlResponse" do
|
19
|
+
@connection.response_class = Faraday::Response::YajlResponse
|
20
|
+
assert_equal [1,2,3], @connection.get('json').body
|
21
|
+
end
|
22
|
+
|
23
|
+
it "retrieves the response headers" do
|
24
|
+
assert_equal 'text/html', @connection.get('hello_world').headers['content-type']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "async requests" do
|
29
|
+
before do
|
30
|
+
@connection.extend adapter
|
31
|
+
end
|
32
|
+
|
33
|
+
it "clears parallel manager after running a single request" do
|
34
|
+
assert !@connection.in_parallel?
|
35
|
+
resp = @connection.get('hello_world')
|
36
|
+
assert !@connection.in_parallel?
|
37
|
+
assert_equal 'hello world', @connection.get('hello_world').body
|
38
|
+
end
|
39
|
+
|
40
|
+
it "uses parallel manager to run multiple json requests" do
|
41
|
+
resp1, resp2 = nil, nil
|
42
|
+
|
43
|
+
@connection.response_class = Faraday::Response::YajlResponse
|
44
|
+
@connection.in_parallel do
|
45
|
+
resp1 = @connection.get('json')
|
46
|
+
resp2 = @connection.get('json')
|
47
|
+
assert @connection.in_parallel?
|
48
|
+
if adapter.supports_async?
|
49
|
+
assert_nil resp1.body
|
50
|
+
assert_nil resp2.body
|
51
|
+
end
|
52
|
+
end
|
53
|
+
assert !@connection.in_parallel?
|
54
|
+
assert_equal [1,2,3], resp1.body
|
55
|
+
assert_equal [1,2,3], resp2.body
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
+
|
3
|
+
class ConnectionTest < Faraday::TestCase
|
4
|
+
describe "#initialize" do
|
5
|
+
it "parses @host out of given url" do
|
6
|
+
conn = Faraday::Connection.new "http://sushi.com"
|
7
|
+
assert_equal 'sushi.com', conn.host
|
8
|
+
end
|
9
|
+
|
10
|
+
it "parses nil @port out of given url" do
|
11
|
+
conn = Faraday::Connection.new "http://sushi.com"
|
12
|
+
assert_nil conn.port
|
13
|
+
end
|
14
|
+
|
15
|
+
it "parses @scheme out of given url" do
|
16
|
+
conn = Faraday::Connection.new "http://sushi.com"
|
17
|
+
assert_equal 'http', conn.scheme
|
18
|
+
end
|
19
|
+
|
20
|
+
it "parses @port out of given url" do
|
21
|
+
conn = Faraday::Connection.new "http://sushi.com:815"
|
22
|
+
assert_equal 815, conn.port
|
23
|
+
end
|
24
|
+
|
25
|
+
it "parses nil @path_prefix out of given url" do
|
26
|
+
conn = Faraday::Connection.new "http://sushi.com"
|
27
|
+
assert_equal '/', conn.path_prefix
|
28
|
+
end
|
29
|
+
|
30
|
+
it "parses @path_prefix out of given url" do
|
31
|
+
conn = Faraday::Connection.new "http://sushi.com/fish"
|
32
|
+
assert_equal '/fish', conn.path_prefix
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#build_uri" do
|
37
|
+
it "uses Connection#host as default URI host" do
|
38
|
+
conn = Faraday::Connection.new
|
39
|
+
conn.host = 'sushi.com'
|
40
|
+
uri = conn.build_uri("/sake.html")
|
41
|
+
assert_equal 'sushi.com', uri.host
|
42
|
+
end
|
43
|
+
|
44
|
+
it "uses Connection#port as default URI port" do
|
45
|
+
conn = Faraday::Connection.new
|
46
|
+
conn.port = 23
|
47
|
+
uri = conn.build_uri("http://sushi.com")
|
48
|
+
assert_equal 23, uri.port
|
49
|
+
end
|
50
|
+
|
51
|
+
it "uses Connection#scheme as default URI scheme" do
|
52
|
+
conn = Faraday::Connection.new 'http://sushi.com'
|
53
|
+
uri = conn.build_uri("/sake.html")
|
54
|
+
assert_equal 'http', uri.scheme
|
55
|
+
end
|
56
|
+
|
57
|
+
it "uses Connection#path_prefix to customize the path" do
|
58
|
+
conn = Faraday::Connection.new
|
59
|
+
conn.path_prefix = '/fish'
|
60
|
+
uri = conn.build_uri("sake.html")
|
61
|
+
assert_equal '/fish/sake.html', uri.path
|
62
|
+
end
|
63
|
+
|
64
|
+
it "uses '/' Connection#path_prefix to customize the path" do
|
65
|
+
conn = Faraday::Connection.new
|
66
|
+
conn.path_prefix = '/'
|
67
|
+
uri = conn.build_uri("sake.html")
|
68
|
+
assert_equal '/sake.html', uri.path
|
69
|
+
end
|
70
|
+
|
71
|
+
it "forces Connection#path_prefix to be absolute" do
|
72
|
+
conn = Faraday::Connection.new
|
73
|
+
conn.path_prefix = 'fish'
|
74
|
+
uri = conn.build_uri("sake.html")
|
75
|
+
assert_equal '/fish/sake.html', uri.path
|
76
|
+
end
|
77
|
+
|
78
|
+
it "ignores Connection#path_prefix trailing slash" do
|
79
|
+
conn = Faraday::Connection.new
|
80
|
+
conn.path_prefix = '/fish/'
|
81
|
+
uri = conn.build_uri("sake.html")
|
82
|
+
assert_equal '/fish/sake.html', uri.path
|
83
|
+
end
|
84
|
+
|
85
|
+
it "allows absolute URI to ignore Connection#path_prefix" do
|
86
|
+
conn = Faraday::Connection.new
|
87
|
+
conn.path_prefix = '/fish'
|
88
|
+
uri = conn.build_uri("/sake.html")
|
89
|
+
assert_equal '/sake.html', uri.path
|
90
|
+
end
|
91
|
+
|
92
|
+
it "parses url/params into #path" do
|
93
|
+
conn = Faraday::Connection.new
|
94
|
+
uri = conn.build_uri("http://sushi.com/sake.html")
|
95
|
+
assert_equal '/sake.html', uri.path
|
96
|
+
end
|
97
|
+
|
98
|
+
it "parses url/params into #query" do
|
99
|
+
conn = Faraday::Connection.new
|
100
|
+
uri = conn.build_uri("http://sushi.com/sake.html", 'a[b]' => '1 + 2')
|
101
|
+
assert_equal "a%5Bb%5D=1%20%2B%202", uri.query
|
102
|
+
end
|
103
|
+
|
104
|
+
it "parses url into #host" do
|
105
|
+
conn = Faraday::Connection.new
|
106
|
+
uri = conn.build_uri("http://sushi.com/sake.html")
|
107
|
+
assert_equal "sushi.com", uri.host
|
108
|
+
end
|
109
|
+
|
110
|
+
it "parses url into #port" do
|
111
|
+
conn = Faraday::Connection.new
|
112
|
+
uri = conn.build_uri("http://sushi.com/sake.html")
|
113
|
+
assert_nil uri.port
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "#params_to_query" do
|
118
|
+
it "converts hash of params to URI-escaped query string" do
|
119
|
+
conn = Faraday::Connection.new
|
120
|
+
assert_equal "a%5Bb%5D=1%20%2B%202", conn.params_to_query('a[b]' => '1 + 2')
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'context'
|
3
|
+
if ENV['LEFTRIGHT']
|
4
|
+
require 'leftright'
|
5
|
+
end
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
8
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
9
|
+
require 'faraday'
|
10
|
+
|
11
|
+
module Faraday
|
12
|
+
class TestCase < Test::Unit::TestCase
|
13
|
+
LIVE_SERVER = 'http://localhost:4567'
|
14
|
+
|
15
|
+
class TestConnection < Faraday::Connection
|
16
|
+
include Faraday::Adapter::MockRequest
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/test/live_server.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
+
|
3
|
+
class ResponseTest < Faraday::TestCase
|
4
|
+
describe "unloaded response class" do
|
5
|
+
it "is not allowed to be set" do
|
6
|
+
resp_class = Object.new
|
7
|
+
def resp_class.loaded?() false end
|
8
|
+
conn = Faraday::Connection.new
|
9
|
+
assert_raises ArgumentError do
|
10
|
+
conn.response_class = resp_class
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "TestConnection#get with default Faraday::Response class" do
|
16
|
+
it "returns Faraday::Response" do
|
17
|
+
conn = TestConnection.new do |stub|
|
18
|
+
stub.get('/hello') { [200, {}, 'hello world']}
|
19
|
+
end
|
20
|
+
resp = conn.get('/hello')
|
21
|
+
assert_equal 'hello world', resp.body
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "TestConnection#get with Faraday::YajlResponse class" do
|
26
|
+
it "returns string body" do
|
27
|
+
conn = TestConnection.new do |stub|
|
28
|
+
stub.get('/hello') { [200, {}, '[1,2,3]']}
|
29
|
+
end
|
30
|
+
conn.response_class = Faraday::Response::YajlResponse
|
31
|
+
assert_equal [1,2,3], conn.get('/hello').body
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: faraday
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- rick
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-19 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: HTTP/REST API client library with pluggable components
|
17
|
+
email: technoweenie@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.rdoc
|
25
|
+
files:
|
26
|
+
- .document
|
27
|
+
- .gitignore
|
28
|
+
- LICENSE
|
29
|
+
- README.rdoc
|
30
|
+
- Rakefile
|
31
|
+
- VERSION
|
32
|
+
- faraday.gemspec
|
33
|
+
- lib/faraday.rb
|
34
|
+
- lib/faraday/adapter/mock_request.rb
|
35
|
+
- lib/faraday/adapter/net_http.rb
|
36
|
+
- lib/faraday/adapter/typhoeus.rb
|
37
|
+
- lib/faraday/connection.rb
|
38
|
+
- lib/faraday/error.rb
|
39
|
+
- lib/faraday/response.rb
|
40
|
+
- lib/faraday/response/yajl_response.rb
|
41
|
+
- test/adapter/typhoeus_test.rb
|
42
|
+
- test/adapter_test.rb
|
43
|
+
- test/connection_test.rb
|
44
|
+
- test/helper.rb
|
45
|
+
- test/live_server.rb
|
46
|
+
- test/response_test.rb
|
47
|
+
has_rdoc: true
|
48
|
+
homepage: http://github.com/technoweenie/faraday
|
49
|
+
licenses: []
|
50
|
+
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options:
|
53
|
+
- --charset=UTF-8
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.3.5
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: HTTP/REST API client library
|
75
|
+
test_files:
|
76
|
+
- test/adapter/typhoeus_test.rb
|
77
|
+
- test/adapter_test.rb
|
78
|
+
- test/connection_test.rb
|
79
|
+
- test/helper.rb
|
80
|
+
- test/live_server.rb
|
81
|
+
- test/response_test.rb
|