alice 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,6 @@
1
+ module Alice
2
+ module Error
3
+ class ConnectionFailed < StandardError; end
4
+ class ResourceNotFound < StandardError; end
5
+ end
6
+ end
@@ -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