alice 0.1.0
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 +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +76 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/alice.rb +76 -0
- data/lib/alice/adapter/net_http.rb +30 -0
- data/lib/alice/adapter/patron.rb +35 -0
- data/lib/alice/adapter/test.rb +94 -0
- data/lib/alice/adapter/typhoeus.rb +61 -0
- data/lib/alice/builder.rb +39 -0
- data/lib/alice/connection.rb +183 -0
- data/lib/alice/error.rb +6 -0
- data/lib/alice/middleware.rb +54 -0
- data/lib/alice/request.rb +77 -0
- data/lib/alice/request/active_support_json.rb +20 -0
- data/lib/alice/request/yajl.rb +18 -0
- data/lib/alice/response.rb +48 -0
- data/lib/alice/response/active_support_json.rb +22 -0
- data/lib/alice/response/yajl.rb +20 -0
- data/test/adapters/live_test.rb +157 -0
- data/test/adapters/test_middleware_test.rb +28 -0
- data/test/adapters/typhoeus_test.rb +28 -0
- data/test/connection_app_test.rb +52 -0
- data/test/connection_test.rb +167 -0
- data/test/env_test.rb +35 -0
- data/test/helper.rb +25 -0
- data/test/live_server.rb +34 -0
- data/test/request_middleware_test.rb +19 -0
- data/test/response_middleware_test.rb +19 -0
- metadata +114 -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,76 @@
|
|
1
|
+
= alice
|
2
|
+
|
3
|
+
Use a Rack app as an HTTP client library. This is another exploration in the
|
4
|
+
same vein as Faraday: http://github.com/technoweenie/faraday
|
5
|
+
|
6
|
+
Let me think: was I the same when I got up this morning? I almost think I can
|
7
|
+
remember feeling a little different. But if I'm not the same, the next
|
8
|
+
question is, Who in the world am I?
|
9
|
+
|
10
|
+
== Usage
|
11
|
+
|
12
|
+
conn = Alice::Connection.new(:url => 'http://sushi.com') do
|
13
|
+
use Alice::Request::Yajl # convert body to json with Yajl lib
|
14
|
+
use Alice::Adapter::Logger # log the request somewhere?
|
15
|
+
use Alice::Adapter::Typhoeus # make http request with typhoeus
|
16
|
+
use Alice::Response::Yajl # # parse body with yajl
|
17
|
+
|
18
|
+
# or use shortcuts
|
19
|
+
request :yajl # Alice::Request::Yajl
|
20
|
+
adapter :logger # Alice::Adapter::Logger
|
21
|
+
adapter :typhoeus # Alice::Adapter::Typhoeus
|
22
|
+
response :yajl # Alice::Response::Yajl
|
23
|
+
end
|
24
|
+
|
25
|
+
resp1 = conn.get '/nigiri/sake.json'
|
26
|
+
resp2 = conn.post do |req|
|
27
|
+
req.url "/nigiri.json", :page => 2
|
28
|
+
req[:content_type] = 'application/json'
|
29
|
+
req.body = {:name => 'Unagi'}
|
30
|
+
end
|
31
|
+
|
32
|
+
== Testing
|
33
|
+
|
34
|
+
test = Alice::Connection.new do
|
35
|
+
adapter :test do |stub|
|
36
|
+
stub.get '/nigiri/sake.json' do
|
37
|
+
[200, {}, 'hi world']
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
resp = test.get '/nigiri/sake.json'
|
43
|
+
resp.body # => 'hi world'
|
44
|
+
|
45
|
+
== Note on Patches/Pull Requests
|
46
|
+
|
47
|
+
* Fork the project.
|
48
|
+
* Make your feature addition or bug fix.
|
49
|
+
* Add tests for it. This is important so I don't break it in a
|
50
|
+
future version unintentionally.
|
51
|
+
* Commit, do not mess with rakefile, version, or history.
|
52
|
+
(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)
|
53
|
+
* Send me a pull request. Bonus points for topic branches.
|
54
|
+
|
55
|
+
== Running Tests
|
56
|
+
|
57
|
+
* Yajl is needed for tests :(
|
58
|
+
* Pass a LIVE env var to run it against a live server.
|
59
|
+
|
60
|
+
> ruby test/live_server.rb # start the server
|
61
|
+
> rake # no live tests
|
62
|
+
> LIVE=1 rake # run with http://localhost:4567
|
63
|
+
> LIVE=http://foobar.dev:4567 rake # run with http://foobar.dev:4567
|
64
|
+
|
65
|
+
== TODO
|
66
|
+
|
67
|
+
* Add curb/em-http support
|
68
|
+
* Add xml parsing
|
69
|
+
* Support timeouts, proxy servers, ssl options
|
70
|
+
* Add streaming requests and responses
|
71
|
+
* Add default middleware load out for common cases
|
72
|
+
* Add symbol => string index for mime types (:json => 'application/json')
|
73
|
+
|
74
|
+
== Copyright
|
75
|
+
|
76
|
+
Copyright (c) 2010 rick. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "alice"
|
8
|
+
gem.summary = %Q{HTTP/REST API client library using Rack-like middleware}
|
9
|
+
gem.description = %Q{HTTP/REST API client library using Rack-like middleware}
|
10
|
+
gem.email = "technoweenie@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/technoweenie/alice"
|
12
|
+
gem.authors = ["rick"]
|
13
|
+
gem.add_dependency "rack"
|
14
|
+
gem.add_dependency "addressable"
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/*_test.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/*_test.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :test => :check_dependencies
|
42
|
+
|
43
|
+
task :default => :test
|
44
|
+
|
45
|
+
require 'rake/rdoctask'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "alice #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/alice.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
|
3
|
+
module Alice
|
4
|
+
module AutoloadHelper
|
5
|
+
def register_lookup_modules(mods)
|
6
|
+
(@lookup_module_index ||= {}).update(mods)
|
7
|
+
end
|
8
|
+
|
9
|
+
def lookup_module(key)
|
10
|
+
return if !@lookup_module_index
|
11
|
+
const_get @lookup_module_index[key] || key
|
12
|
+
end
|
13
|
+
|
14
|
+
def autoload_all(prefix, options)
|
15
|
+
options.each do |const_name, path|
|
16
|
+
autoload const_name, File.join(prefix, path)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Loads each autoloaded constant. If thread safety is a concern, wrap
|
21
|
+
# this in a Mutex.
|
22
|
+
def load_autoloaded_constants
|
23
|
+
constants.each do |const|
|
24
|
+
const_get(const) if autoload?(const)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def all_loaded_constants
|
29
|
+
constants.map { |c| const_get(c) }.select { |a| a.loaded? }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
extend AutoloadHelper
|
34
|
+
|
35
|
+
autoload_all 'alice',
|
36
|
+
:Connection => 'connection',
|
37
|
+
:Middleware => 'middleware',
|
38
|
+
:Builder => 'builder',
|
39
|
+
:Request => 'request',
|
40
|
+
:Response => 'response',
|
41
|
+
:Error => 'error'
|
42
|
+
|
43
|
+
module Adapter
|
44
|
+
extend AutoloadHelper
|
45
|
+
autoload_all 'alice/adapter',
|
46
|
+
:NetHttp => 'net_http',
|
47
|
+
:Typhoeus => 'typhoeus',
|
48
|
+
:Patron => 'patron',
|
49
|
+
:Test => 'test'
|
50
|
+
|
51
|
+
register_lookup_modules \
|
52
|
+
:test => :Test,
|
53
|
+
:net_http => :NetHttp,
|
54
|
+
:typhoeus => :Typhoeus,
|
55
|
+
:patron => :patron,
|
56
|
+
:net_http => :NetHttp
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# not pulling in active-support JUST for this method.
|
61
|
+
class Object
|
62
|
+
# Yields <code>x</code> to the block, and then returns <code>x</code>.
|
63
|
+
# The primary purpose of this method is to "tap into" a method chain,
|
64
|
+
# in order to perform operations on intermediate results within the chain.
|
65
|
+
#
|
66
|
+
# (1..10).tap { |x| puts "original: #{x.inspect}" }.to_a.
|
67
|
+
# tap { |x| puts "array: #{x.inspect}" }.
|
68
|
+
# select { |x| x%2 == 0 }.
|
69
|
+
# tap { |x| puts "evens: #{x.inspect}" }.
|
70
|
+
# map { |x| x*x }.
|
71
|
+
# tap { |x| puts "squares: #{x.inspect}" }
|
72
|
+
def tap
|
73
|
+
yield self
|
74
|
+
self
|
75
|
+
end unless Object.respond_to?(:tap)
|
76
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
module Alice
|
3
|
+
module Adapter
|
4
|
+
class NetHttp < Middleware
|
5
|
+
def call(env)
|
6
|
+
process_body_for_request(env)
|
7
|
+
|
8
|
+
http = Net::HTTP.new(env[:url].host, env[:url].port)
|
9
|
+
full_path = full_path_for(env[:url].path, env[:url].query, env[:url].fragment)
|
10
|
+
http_resp = http.send_request(env[:method].to_s.upcase, full_path, env[:body], env[:request_headers])
|
11
|
+
|
12
|
+
raise Error::ResourceNotFound if http_resp.code == '404'
|
13
|
+
|
14
|
+
resp_headers = {}
|
15
|
+
http_resp.each_header do |key, value|
|
16
|
+
resp_headers[key] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
env.update \
|
20
|
+
:status => http_resp.code.to_i,
|
21
|
+
:response_headers => resp_headers,
|
22
|
+
:body => http_resp.body
|
23
|
+
|
24
|
+
@app.call env
|
25
|
+
rescue Errno::ECONNREFUSED
|
26
|
+
raise Error::ConnectionFailed, "connection refused"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Alice
|
2
|
+
module Adapter
|
3
|
+
class Patron < Middleware
|
4
|
+
begin
|
5
|
+
require 'patron'
|
6
|
+
rescue LoadError => e
|
7
|
+
self.load_error = e
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
process_body_for_request(env)
|
12
|
+
|
13
|
+
sess = ::Patron::Session.new
|
14
|
+
args = [env[:method], env[:url].to_s, env[:request_headers]]
|
15
|
+
if Alice::Connection::METHODS_WITH_BODIES.include?(env[:method])
|
16
|
+
args.insert(2, env[:body].to_s)
|
17
|
+
end
|
18
|
+
resp = sess.send *args
|
19
|
+
|
20
|
+
raise Alice::Error::ResourceNotFound if resp.status == 404
|
21
|
+
|
22
|
+
env.update \
|
23
|
+
:status => resp.status,
|
24
|
+
:response_headers => resp.headers.
|
25
|
+
inject({}) { |memo, (k, v)| memo.update(k.downcase => v) },
|
26
|
+
:body => resp.body
|
27
|
+
env[:response].finish(env)
|
28
|
+
|
29
|
+
@app.call env
|
30
|
+
rescue Errno::ECONNREFUSED
|
31
|
+
raise Error::ConnectionFailed, "connection refused"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Alice
|
2
|
+
module Adapter
|
3
|
+
# test = Alice::Connection.new do
|
4
|
+
# use Alice::Adapter::Test do |stub|
|
5
|
+
# stub.get '/nigiri/sake.json' do
|
6
|
+
# [200, {}, 'hi world']
|
7
|
+
# end
|
8
|
+
# end
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# resp = test.get '/nigiri/sake.json'
|
12
|
+
# resp.body # => 'hi world'
|
13
|
+
#
|
14
|
+
class Test < Middleware
|
15
|
+
def self.loaded?() false end
|
16
|
+
|
17
|
+
class Stubs
|
18
|
+
def initialize
|
19
|
+
# {:get => [Stub, Stub]}
|
20
|
+
@stack = {}
|
21
|
+
yield self if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty?
|
25
|
+
@stack.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def match(request_method, path)
|
29
|
+
return false if !@stack.key?(request_method)
|
30
|
+
@stack[request_method].detect { |stub| stub.matches?(path) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def get(path, &block)
|
34
|
+
new_stub(:get, path, block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def head(path, &block)
|
38
|
+
new_stub(:head, path, block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def post(path, &block)
|
42
|
+
new_stub(:post, path, block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def put(path, &block)
|
46
|
+
new_stub(:put, path, block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete(path, &block)
|
50
|
+
new_stub(:delete, path, block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def new_stub(request_method, path, block)
|
54
|
+
(@stack[request_method] ||= []) << Stub.new(path, block)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Stub < Struct.new(:path, :block)
|
59
|
+
def matches?(request_path)
|
60
|
+
request_path == path
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize app, &block
|
65
|
+
super(app)
|
66
|
+
configure(&block) if block
|
67
|
+
end
|
68
|
+
|
69
|
+
def configure
|
70
|
+
yield stubs
|
71
|
+
end
|
72
|
+
|
73
|
+
def stubs
|
74
|
+
@stubs ||= Stubs.new
|
75
|
+
end
|
76
|
+
|
77
|
+
def call(env)
|
78
|
+
if stub = stubs.match(env[:method], env[:url].path)
|
79
|
+
status, headers, body = stub.block.call(env)
|
80
|
+
env.update \
|
81
|
+
:status => status,
|
82
|
+
:response_headers => headers,
|
83
|
+
:body => body
|
84
|
+
else
|
85
|
+
env.update \
|
86
|
+
:status => 404,
|
87
|
+
:response_headers => {},
|
88
|
+
:body => 'no stubbed requests'
|
89
|
+
end
|
90
|
+
@app.call(env)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Alice
|
2
|
+
module Adapter
|
3
|
+
class Typhoeus < Middleware
|
4
|
+
self.supports_parallel_requests = true
|
5
|
+
|
6
|
+
def self.setup_parallel_manager(options = {})
|
7
|
+
options.empty? ? ::Typhoeus::Hydra.hydra : ::Typhoeus::Hydra.new(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'typhoeus'
|
12
|
+
rescue LoadError => e
|
13
|
+
self.load_error = e
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
process_body_for_request(env)
|
18
|
+
|
19
|
+
hydra = env[:parallel_manager] || self.class.setup_parallel_manager
|
20
|
+
req = ::Typhoeus::Request.new env[:url].to_s,
|
21
|
+
:method => env[:method],
|
22
|
+
:body => env[:body],
|
23
|
+
:headers => env[:request_headers]
|
24
|
+
|
25
|
+
req.on_complete do |resp|
|
26
|
+
raise Alice::Error::ResourceNotFound if resp.code == 404
|
27
|
+
env.update \
|
28
|
+
:status => resp.code,
|
29
|
+
:response_headers => parse_response_headers(resp.headers),
|
30
|
+
:body => resp.body
|
31
|
+
env[:response].finish(env)
|
32
|
+
end
|
33
|
+
|
34
|
+
hydra.queue req
|
35
|
+
|
36
|
+
if !env[:parallel_manager]
|
37
|
+
hydra.run
|
38
|
+
end
|
39
|
+
|
40
|
+
@app.call env
|
41
|
+
rescue Errno::ECONNREFUSED
|
42
|
+
raise Error::ConnectionFailed, "connection refused"
|
43
|
+
end
|
44
|
+
|
45
|
+
def in_parallel(options = {})
|
46
|
+
@hydra = ::Typhoeus::Hydra.new(options)
|
47
|
+
yield
|
48
|
+
@hydra.run
|
49
|
+
@hydra = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse_response_headers(header_string)
|
53
|
+
return {} unless header_string && !header_string.empty?
|
54
|
+
Hash[*header_string.split(/\r\n/).
|
55
|
+
tap { |a| a.shift }. # drop the HTTP status line
|
56
|
+
map! { |h| h.split(/:\s+/,2) }. # split key and value
|
57
|
+
map! { |(k, v)| [k.downcase, v] }.flatten!]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|