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.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,6 @@
1
+ ## PROJECT::GENERAL
2
+ coverage
3
+ rdoc
4
+ pkg
5
+
6
+ ## PROJECT::SPECIFIC
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.
@@ -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.
@@ -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
@@ -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