alice 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rack/builder'
|
2
|
+
|
3
|
+
module Alice
|
4
|
+
# Possibly going to extend this a bit.
|
5
|
+
#
|
6
|
+
# Alice::Connection.new(:url => 'http://sushi.com') do
|
7
|
+
# request :yajl # Alice::Request::Yajl
|
8
|
+
# adapter :logger # Alice::Adapter::Logger
|
9
|
+
# response :yajl # Alice::Response::Yajl
|
10
|
+
# end
|
11
|
+
class Builder < Rack::Builder
|
12
|
+
def self.create_with_inner_app(&block)
|
13
|
+
inner = lambda do |env|
|
14
|
+
if !env[:parallel_manager]
|
15
|
+
env[:response].finish(env)
|
16
|
+
else
|
17
|
+
env[:response]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
Builder.new(&block).tap { |builder| builder.run(inner) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def request(key, *args, &block)
|
24
|
+
use_symbol Alice::Request, key, *args, &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def response(key, *args, &block)
|
28
|
+
use_symbol Alice::Response, key, *args, &block
|
29
|
+
end
|
30
|
+
|
31
|
+
def adapter(key, *args, &block)
|
32
|
+
use_symbol Alice::Adapter, key, *args, &block
|
33
|
+
end
|
34
|
+
|
35
|
+
def use_symbol(mod, key, *args, &block)
|
36
|
+
use mod.lookup_module(key), *args, &block
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Alice
|
5
|
+
class Connection
|
6
|
+
include Addressable, Rack::Utils
|
7
|
+
|
8
|
+
HEADERS = Hash.new { |h, k| k.respond_to?(:to_str) ? k : k.to_s.capitalize }.update \
|
9
|
+
:content_type => "Content-Type",
|
10
|
+
:content_length => "Content-Length",
|
11
|
+
:accept_charset => "Accept-Charset",
|
12
|
+
:accept_encoding => "Accept-Encoding"
|
13
|
+
HEADERS.values.each { |v| v.freeze }
|
14
|
+
|
15
|
+
METHODS = Set.new [:get, :post, :put, :delete, :head]
|
16
|
+
METHODS_WITH_BODIES = Set.new [:post, :put]
|
17
|
+
|
18
|
+
attr_accessor :host, :port, :scheme, :params, :headers, :parallel_manager
|
19
|
+
attr_reader :path_prefix, :builder
|
20
|
+
|
21
|
+
# :url
|
22
|
+
# :params
|
23
|
+
# :headers
|
24
|
+
def initialize(url = nil, options = {}, &block)
|
25
|
+
if url.is_a?(Hash)
|
26
|
+
options = url
|
27
|
+
url = options[:url]
|
28
|
+
end
|
29
|
+
@headers = HeaderHash.new
|
30
|
+
@params = {}
|
31
|
+
@parallel_manager = options[:parallel]
|
32
|
+
self.url_prefix = url if url
|
33
|
+
merge_params @params, options[:params] if options[:params]
|
34
|
+
merge_headers @headers, options[:headers] if options[:headers]
|
35
|
+
if block
|
36
|
+
@builder = Builder.create_with_inner_app(&block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def get(url = nil, headers = nil, &block)
|
41
|
+
run_request :get, url, nil, headers, &block
|
42
|
+
end
|
43
|
+
|
44
|
+
def post(url = nil, body = nil, headers = nil, &block)
|
45
|
+
run_request :post, url, body, headers, &block
|
46
|
+
end
|
47
|
+
|
48
|
+
def put(url = nil, body = nil, headers = nil, &block)
|
49
|
+
run_request :put, url, body, headers, &block
|
50
|
+
end
|
51
|
+
|
52
|
+
def head(url = nil, headers = nil, &block)
|
53
|
+
run_request :head, url, nil, headers, &block
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete(url = nil, headers = nil, &block)
|
57
|
+
run_request :delete, url, nil, headers, &block
|
58
|
+
end
|
59
|
+
|
60
|
+
def run_request(method, url, body, headers)
|
61
|
+
if !METHODS.include?(method)
|
62
|
+
raise ArgumentError, "unknown http method: #{method}"
|
63
|
+
end
|
64
|
+
|
65
|
+
Request.run(self, method) do |req|
|
66
|
+
req.url(url) if url
|
67
|
+
req.headers.update(headers) if headers
|
68
|
+
req.body = body if body
|
69
|
+
yield req if block_given?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def in_parallel?
|
74
|
+
!!@parallel_manager
|
75
|
+
end
|
76
|
+
|
77
|
+
def in_parallel(manager)
|
78
|
+
@parallel_manager = manager
|
79
|
+
yield
|
80
|
+
@parallel_manager && @parallel_manager.run
|
81
|
+
ensure
|
82
|
+
@parallel_manager = nil
|
83
|
+
end
|
84
|
+
|
85
|
+
# return the assembled Rack application for this instance.
|
86
|
+
def to_app
|
87
|
+
@builder.to_app
|
88
|
+
end
|
89
|
+
|
90
|
+
# Parses the giving url with Addressable::URI and stores the individual
|
91
|
+
# components in this connection. These components serve as defaults for
|
92
|
+
# requests made by this connection.
|
93
|
+
#
|
94
|
+
# conn = Alice::Connection.new { ... }
|
95
|
+
# conn.url_prefix = "https://sushi.com/api"
|
96
|
+
# conn.scheme # => https
|
97
|
+
# conn.path_prefix # => "/api"
|
98
|
+
#
|
99
|
+
# conn.get("nigiri?page=2") # accesses https://sushi.com/api/nigiri
|
100
|
+
#
|
101
|
+
def url_prefix=(url)
|
102
|
+
uri = URI.parse(url)
|
103
|
+
self.scheme = uri.scheme
|
104
|
+
self.host = uri.host
|
105
|
+
self.port = uri.port
|
106
|
+
self.path_prefix = uri.path
|
107
|
+
if uri.query && !uri.query.empty?
|
108
|
+
merge_params @params, parse_query(uri.query)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Ensures that the path prefix always has a leading / and no trailing /
|
113
|
+
def path_prefix=(value)
|
114
|
+
if value
|
115
|
+
value.chomp! "/"
|
116
|
+
value.replace "/#{value}" if value !~ /^\//
|
117
|
+
end
|
118
|
+
@path_prefix = value
|
119
|
+
end
|
120
|
+
|
121
|
+
# Takes a relative url for a request and combines it with the defaults
|
122
|
+
# set on the connection instance.
|
123
|
+
#
|
124
|
+
# conn = Alice::Connection.new { ... }
|
125
|
+
# conn.url_prefix = "https://sushi.com/api?token=abc"
|
126
|
+
# conn.scheme # => https
|
127
|
+
# conn.path_prefix # => "/api"
|
128
|
+
#
|
129
|
+
# conn.build_url("nigiri?page=2") # => https://sushi.com/api/nigiri?token=abc&page=2
|
130
|
+
# conn.build_url("nigiri", :page => 2) # => https://sushi.com/api/nigiri?token=abc&page=2
|
131
|
+
#
|
132
|
+
def build_url(url, params = nil)
|
133
|
+
uri = URI.parse(url.to_s)
|
134
|
+
uri.scheme ||= @scheme
|
135
|
+
uri.host ||= @host
|
136
|
+
uri.port ||= @port
|
137
|
+
if @path_prefix && uri.path !~ /^\//
|
138
|
+
uri.path = "#{@path_prefix.size > 1 ? @path_prefix : nil}/#{uri.path}"
|
139
|
+
end
|
140
|
+
replace_query(uri, params)
|
141
|
+
uri
|
142
|
+
end
|
143
|
+
|
144
|
+
def replace_query(uri, params)
|
145
|
+
url_params = @params.dup
|
146
|
+
if uri.query && !uri.query.empty?
|
147
|
+
merge_params url_params, parse_query(uri.query)
|
148
|
+
end
|
149
|
+
if params && !params.empty?
|
150
|
+
merge_params url_params, params
|
151
|
+
end
|
152
|
+
uri.query = url_params.empty? ? nil : build_query(url_params)
|
153
|
+
uri
|
154
|
+
end
|
155
|
+
|
156
|
+
# turns param keys into strings
|
157
|
+
def merge_params(existing_params, new_params)
|
158
|
+
new_params.each do |key, value|
|
159
|
+
existing_params[key.to_s] = value
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# turns headers keys and values into strings. Look up symbol keys in the
|
164
|
+
# the HEADERS hash.
|
165
|
+
#
|
166
|
+
# h = merge_headers(HeaderHash.new, :content_type => 'text/plain')
|
167
|
+
# h['Content-Type'] # = 'text/plain'
|
168
|
+
#
|
169
|
+
def merge_headers(existing_headers, new_headers)
|
170
|
+
new_headers.each do |key, value|
|
171
|
+
existing_headers[HEADERS[key]] = value.to_s
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Be sure to URI escape '+' symbols to %2B. Otherwise, they get interpreted
|
176
|
+
# as spaces.
|
177
|
+
def escape(s)
|
178
|
+
s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) do
|
179
|
+
'%' << $1.unpack('H2'*bytesize($1)).join('%').tap { |c| c.upcase! }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
data/lib/alice/error.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Alice
|
2
|
+
class Middleware
|
3
|
+
include Rack::Utils
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :load_error, :supports_parallel_requests
|
7
|
+
alias supports_parallel_requests? supports_parallel_requests
|
8
|
+
|
9
|
+
# valid parallel managers should respond to #run with no parameters.
|
10
|
+
# otherwise, return a short wrapper around it.
|
11
|
+
def setup_parallel_manager(options = {})
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.loaded?
|
17
|
+
@load_error.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(app = nil)
|
21
|
+
@app = app
|
22
|
+
end
|
23
|
+
|
24
|
+
# assume that query and fragment are already encoded properly
|
25
|
+
def full_path_for(path, query = nil, fragment = nil)
|
26
|
+
full_path = path.dup
|
27
|
+
if query && !query.empty?
|
28
|
+
full_path << "?#{query}"
|
29
|
+
end
|
30
|
+
if fragment && !fragment.empty?
|
31
|
+
full_path << "##{fragment}"
|
32
|
+
end
|
33
|
+
full_path
|
34
|
+
end
|
35
|
+
|
36
|
+
def process_body_for_request(env)
|
37
|
+
# if it's a string, pass it through
|
38
|
+
return if env[:body].nil? || env[:body].empty? || !env[:body].respond_to?(:each_key)
|
39
|
+
env[:request_headers]['Content-Type'] ||= 'application/x-www-form-urlencoded'
|
40
|
+
env[:body] = create_form_params(env[:body])
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_form_params(params, base = nil)
|
44
|
+
[].tap do |result|
|
45
|
+
params.each_key do |key|
|
46
|
+
key_str = base ? "#{base}[#{key}]" : key
|
47
|
+
value = params[key]
|
48
|
+
wee = (value.kind_of?(Hash) ? create_form_params(value, key_str) : "#{key_str}=#{escape(value.to_s)}")
|
49
|
+
result << wee
|
50
|
+
end
|
51
|
+
end.join("&")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Alice
|
2
|
+
# Used to setup urls, params, headers, and the request body in a sane manner.
|
3
|
+
#
|
4
|
+
# @connection.post do |req|
|
5
|
+
# req.url 'http://localhost', 'a' => '1' # 'http://localhost?a=1'
|
6
|
+
# req.headers['b'] = '2' # header
|
7
|
+
# req['b'] = '2' # header
|
8
|
+
# req.body = 'abc'
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
class Request < Struct.new(:path, :params, :headers, :body)
|
12
|
+
extend AutoloadHelper
|
13
|
+
autoload_all 'alice/request',
|
14
|
+
:Yajl => 'yajl',
|
15
|
+
:ActiveSupportJson => 'active_support_json'
|
16
|
+
|
17
|
+
register_lookup_modules \
|
18
|
+
:yajl => :Yajl,
|
19
|
+
:activesupport_json => :ActiveSupportJson,
|
20
|
+
:rails_json => :ActiveSupportJson,
|
21
|
+
:active_support_json => :ActiveSupportJson
|
22
|
+
|
23
|
+
def self.run(connection, request_method)
|
24
|
+
req = create
|
25
|
+
yield req if block_given?
|
26
|
+
req.run(connection, request_method)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.create
|
30
|
+
req = new(nil, {}, {}, nil)
|
31
|
+
yield req if block_given?
|
32
|
+
req
|
33
|
+
end
|
34
|
+
|
35
|
+
def url(path, params = {})
|
36
|
+
self.path = path
|
37
|
+
self.params = params
|
38
|
+
end
|
39
|
+
|
40
|
+
def [](key)
|
41
|
+
headers[key]
|
42
|
+
end
|
43
|
+
|
44
|
+
def []=(key, value)
|
45
|
+
headers[key] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
# ENV Keys
|
49
|
+
# :method - a symbolized request method (:get, :post)
|
50
|
+
# :body - the request body that will eventually be converted to a string.
|
51
|
+
# :url - Addressable::URI instance of the URI for the current request.
|
52
|
+
# :status - HTTP response status code
|
53
|
+
# :request_headers - hash of HTTP Headers to be sent to the server
|
54
|
+
# :response_headers - Hash of HTTP headers from the server
|
55
|
+
# :parallel_manager - sent if the connection is in parallel mode
|
56
|
+
# :response - the actual response object that stores the rack response
|
57
|
+
def to_env_hash(connection, request_method)
|
58
|
+
env_headers = connection.headers.dup
|
59
|
+
env_params = connection.params.dup
|
60
|
+
connection.merge_headers env_headers, headers
|
61
|
+
connection.merge_params env_params, params
|
62
|
+
|
63
|
+
{ :method => request_method,
|
64
|
+
:body => body,
|
65
|
+
:url => connection.build_url(path, env_params),
|
66
|
+
:request_headers => env_headers,
|
67
|
+
:parallel_manager => connection.parallel_manager,
|
68
|
+
:response => Response.new}
|
69
|
+
end
|
70
|
+
|
71
|
+
def run(connection, request_method)
|
72
|
+
app = connection.to_app
|
73
|
+
env = to_env_hash(connection, request_method)
|
74
|
+
app.call(env)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Alice
|
2
|
+
class Request::ActiveSupportJson < Alice::Middleware
|
3
|
+
begin
|
4
|
+
if !defined?(ActiveSupport::JSON)
|
5
|
+
require 'active_support'
|
6
|
+
end
|
7
|
+
|
8
|
+
rescue LoadError => e
|
9
|
+
self.load_error = e
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
env[:request_headers]['Content-Type'] = 'application/json'
|
14
|
+
if env[:body] && !env[:body].respond_to?(:to_str)
|
15
|
+
env[:body] = ActiveSupport::JSON.encode env[:body]
|
16
|
+
end
|
17
|
+
@app.call env
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Alice
|
2
|
+
class Request::Yajl < Alice::Middleware
|
3
|
+
begin
|
4
|
+
require 'yajl'
|
5
|
+
|
6
|
+
rescue LoadError => e
|
7
|
+
self.load_error = e
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
env[:request_headers]['Content-Type'] = 'application/json'
|
12
|
+
if env[:body] && !env[:body].respond_to?(:to_str)
|
13
|
+
env[:body] = Yajl::Encoder.encode env[:body]
|
14
|
+
end
|
15
|
+
@app.call env
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Alice
|
2
|
+
class Response
|
3
|
+
class Middleware < Alice::Middleware
|
4
|
+
self.load_error = :abstract
|
5
|
+
|
6
|
+
# Use a response callback in case the request is parallelized.
|
7
|
+
#
|
8
|
+
# env[:response].on_complete do |finished_env|
|
9
|
+
# finished_env[:body] = do_stuff_to(finished_env[:body])
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
def self.register_on_complete(env)
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
self.class.register_on_complete(env)
|
17
|
+
@app.call env
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
extend AutoloadHelper
|
22
|
+
autoload_all 'alice/response',
|
23
|
+
:Yajl => 'yajl',
|
24
|
+
:ActiveSupportJson => 'active_support_json'
|
25
|
+
|
26
|
+
register_lookup_modules \
|
27
|
+
:yajl => :Yajl,
|
28
|
+
:activesupport_json => :ActiveSupportJson,
|
29
|
+
:rails_json => :ActiveSupportJson,
|
30
|
+
:active_support_json => :ActiveSupportJson
|
31
|
+
attr_accessor :status, :headers, :body
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@status, @headers, @body = nil, nil, nil
|
35
|
+
@on_complete_callbacks = []
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_complete(&block)
|
39
|
+
@on_complete_callbacks << block
|
40
|
+
end
|
41
|
+
|
42
|
+
def finish(env)
|
43
|
+
@on_complete_callbacks.each { |c| c.call(env) }
|
44
|
+
@status, @headers, @body = env[:status], env[:response_headers], env[:body]
|
45
|
+
self
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|