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
@@ -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
|