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