agile-proxy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.bowerrc +3 -0
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.rubocop.yml +36 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +267 -0
- data/Guardfile +20 -0
- data/LICENSE +22 -0
- data/README.md +93 -0
- data/Rakefile +13 -0
- data/agile-proxy.gemspec +50 -0
- data/assets/index.html +39 -0
- data/assets/ui/app/HttpFlexibleProxyApi.js +31 -0
- data/assets/ui/app/app.js +1 -0
- data/assets/ui/app/controller/Stubs.js +64 -0
- data/assets/ui/app/controller/main.js +12 -0
- data/assets/ui/app/directive/AppEnhancedFormElement.js +21 -0
- data/assets/ui/app/directive/AppFor.js +16 -0
- data/assets/ui/app/directive/AppResponseEditor.js +54 -0
- data/assets/ui/app/model/RequestSpec.js +6 -0
- data/assets/ui/app/routes.js +10 -0
- data/assets/ui/app/service/Dialog.js +49 -0
- data/assets/ui/app/service/DomId.js +10 -0
- data/assets/ui/app/service/Error.js +7 -0
- data/assets/ui/app/service/Stub.js +36 -0
- data/assets/ui/app/view/404.html +2 -0
- data/assets/ui/app/view/dialog/error.html +10 -0
- data/assets/ui/app/view/dialog/yesNo.html +8 -0
- data/assets/ui/app/view/responses/editForm.html +78 -0
- data/assets/ui/app/view/status.html +1 -0
- data/assets/ui/app/view/stubs.html +19 -0
- data/assets/ui/app/view/stubs/edit.html +58 -0
- data/assets/ui/css/main.css +3 -0
- data/bin/agile_proxy +113 -0
- data/bower.json +27 -0
- data/config.yml +6 -0
- data/db.yml +10 -0
- data/db/migrations/20140818110800_create_users.rb +9 -0
- data/db/migrations/20140818134700_create_applications.rb +10 -0
- data/db/migrations/20140818135200_create_request_specs.rb +13 -0
- data/db/migrations/20140821115300_create_responses.rb +14 -0
- data/db/migrations/20140823082900_add_method_to_request_specs.rb +7 -0
- data/db/migrations/20140823083900_rename_request_spec_columns.rb +8 -0
- data/db/migrations/20141031072100_add_url_type_to_request_specs.rb +8 -0
- data/db/migrations/20141105125600_add_conditions_to_request_specs.rb +7 -0
- data/db/migrations/20141106083100_add_username_and_password_to_applications.rb +8 -0
- data/db/migrations/20141119143800_add_record_to_applications.rb +7 -0
- data/db/migrations/20141119174300_create_recordings.rb +18 -0
- data/db/schema.rb +78 -0
- data/examples/README.md +1 -0
- data/examples/facebook_api.html +59 -0
- data/examples/tumblr_api.html +22 -0
- data/lib/agile_proxy.rb +8 -0
- data/lib/agile_proxy/api/applications.rb +77 -0
- data/lib/agile_proxy/api/recordings.rb +52 -0
- data/lib/agile_proxy/api/request_specs.rb +85 -0
- data/lib/agile_proxy/api/root.rb +41 -0
- data/lib/agile_proxy/config.rb +63 -0
- data/lib/agile_proxy/handlers/handler.rb +43 -0
- data/lib/agile_proxy/handlers/proxy_handler.rb +110 -0
- data/lib/agile_proxy/handlers/request_handler.rb +57 -0
- data/lib/agile_proxy/handlers/stub_handler.rb +113 -0
- data/lib/agile_proxy/mitm.crt +22 -0
- data/lib/agile_proxy/mitm.key +27 -0
- data/lib/agile_proxy/model/application.rb +20 -0
- data/lib/agile_proxy/model/recording.rb +16 -0
- data/lib/agile_proxy/model/request_spec.rb +47 -0
- data/lib/agile_proxy/model/response.rb +56 -0
- data/lib/agile_proxy/model/user.rb +17 -0
- data/lib/agile_proxy/proxy_connection.rb +113 -0
- data/lib/agile_proxy/route.rb +106 -0
- data/lib/agile_proxy/router.rb +99 -0
- data/lib/agile_proxy/server.rb +85 -0
- data/lib/agile_proxy/servers/api.rb +41 -0
- data/lib/agile_proxy/servers/request_spec.rb +30 -0
- data/lib/agile_proxy/version.rb +6 -0
- data/load_proxy.js +39 -0
- data/log/.gitkeep +0 -0
- data/spec/common_helper.rb +32 -0
- data/spec/fixtures/test-server.crt +15 -0
- data/spec/fixtures/test-server.key +15 -0
- data/spec/integration/helpers/request_spec_helper.rb +60 -0
- data/spec/integration/specs/lib/server_spec.rb +407 -0
- data/spec/integration_spec_helper.rb +18 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/test_server.rb +75 -0
- data/spec/unit/agile_proxy/api/applications_spec.rb +102 -0
- data/spec/unit/agile_proxy/api/common_helper.rb +31 -0
- data/spec/unit/agile_proxy/api/recordings_spec.rb +115 -0
- data/spec/unit/agile_proxy/api/request_specs_spec.rb +159 -0
- data/spec/unit/agile_proxy/handlers/handler_spec.rb +8 -0
- data/spec/unit/agile_proxy/handlers/proxy_handler_spec.rb +138 -0
- data/spec/unit/agile_proxy/handlers/request_handler_spec.rb +55 -0
- data/spec/unit/agile_proxy/handlers/stub_handler_spec.rb +154 -0
- data/spec/unit/agile_proxy/model/recording_spec.rb +0 -0
- data/spec/unit/agile_proxy/model/request_spec_spec.rb +45 -0
- data/spec/unit/agile_proxy/model/response_spec.rb +38 -0
- data/spec/unit/agile_proxy/server_spec.rb +88 -0
- data/spec/unit/agile_proxy/servers/api_spec.rb +31 -0
- data/spec/unit/agile_proxy/servers/request_spec_spec.rb +32 -0
- metadata +618 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'application'
|
2
|
+
module AgileProxy
|
3
|
+
#
|
4
|
+
# = The recording model
|
5
|
+
#
|
6
|
+
# When an application is set to allow recording, every HTTP(s) request/response cycle coming through the proxy
|
7
|
+
# will create an instance of this model and persist it to the database.
|
8
|
+
#
|
9
|
+
# An API is then available to access this data via REST for the UI or test suite etc...
|
10
|
+
#
|
11
|
+
class Recording < ActiveRecord::Base
|
12
|
+
belongs_to :application
|
13
|
+
serialize :request_headers, JSON
|
14
|
+
serialize :response_headers, JSON
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative 'application'
|
2
|
+
require_relative 'user'
|
3
|
+
require_relative 'response'
|
4
|
+
module AgileProxy
|
5
|
+
#
|
6
|
+
# = The Request Spec model
|
7
|
+
# The request spec is an input/output specification that incoming HTTP(s) requests are matched against.
|
8
|
+
#
|
9
|
+
# It uses the action dispatch router to do this matching,
|
10
|
+
# which is fed data from the database as it's input - i.e. its routing
|
11
|
+
# table is generated on the fly.
|
12
|
+
#
|
13
|
+
# This model is responsible not only for retrieving and persisting these
|
14
|
+
# request specifications, but also for creating the response
|
15
|
+
#
|
16
|
+
class RequestSpec < ActiveRecord::Base
|
17
|
+
belongs_to :application
|
18
|
+
belongs_to :user
|
19
|
+
belongs_to :response
|
20
|
+
accepts_nested_attributes_for :response
|
21
|
+
validates_inclusion_of :url_type, in: %w(url regex)
|
22
|
+
def initialize(attrs = {})
|
23
|
+
attrs[:http_method] = attrs.delete(:method) if attrs.key?(:method)
|
24
|
+
super
|
25
|
+
end
|
26
|
+
# The conditions are at present, stored as a JSON string. This is editable as a string in the UI, and therefore
|
27
|
+
# accessible using 'conditions' as normal.
|
28
|
+
# This method returns a JSON decoded version of this as a HASH
|
29
|
+
# @return [Hash] decoded conditions
|
30
|
+
def conditions_json
|
31
|
+
ActiveSupport::JSON.decode(conditions)
|
32
|
+
end
|
33
|
+
# This method's output is a 'rack' response, but its input is not.
|
34
|
+
# When the router has determined that this request spec is the one that is going to be sent to the client,
|
35
|
+
# it will call this method with the request's parameters, headers and body.
|
36
|
+
#
|
37
|
+
# if no response has been specified, an empty body will be returned,
|
38
|
+
# otherwise a 'rack' version of the response is returned
|
39
|
+
# @param params [Hash] the request parameters
|
40
|
+
# @param headers [Hash] The request headers
|
41
|
+
# @param body [String] The request body
|
42
|
+
# @return [Array] The rack response
|
43
|
+
def call(params, headers, body)
|
44
|
+
response.nil? ? [204, { 'Content-Type' => 'text/plain' }, ''] : response.to_rack(params, headers, body)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'flavour_saver'
|
2
|
+
require 'flavour_saver/runtime'
|
3
|
+
module AgileProxy
|
4
|
+
#
|
5
|
+
# = The associated response for the RequestSpec
|
6
|
+
#
|
7
|
+
# An instance of this class is expected to be stored alongside every RequestSpec.
|
8
|
+
#
|
9
|
+
# It is responsible for the following :-
|
10
|
+
#
|
11
|
+
# 1. Retrieving response
|
12
|
+
# 2. Persisting responses
|
13
|
+
# 3. Providing a 'rack' ouput of the response
|
14
|
+
# 4. Convenient setters for code, body and content_type
|
15
|
+
# 5. Template parsing
|
16
|
+
class Response < ActiveRecord::Base
|
17
|
+
PROTECTED_HEADERS = ['Content-Type']
|
18
|
+
has_many :request_specs
|
19
|
+
serialize :headers, JSON
|
20
|
+
# A convenient setter for the content_type within the header
|
21
|
+
# @param val [String] The content type
|
22
|
+
def content_type=(val)
|
23
|
+
write_attribute(:content_type, val)
|
24
|
+
headers.merge!('Content-Type' => val)
|
25
|
+
val
|
26
|
+
end
|
27
|
+
# Provides the response as a 'rack' response
|
28
|
+
#
|
29
|
+
# If the response is a template (by specifying is_template as true), the output will
|
30
|
+
# have its template values parsed and replaced with
|
31
|
+
# data from the input_params, input_headers and input_body
|
32
|
+
# Otherwise, the body of the output is sent as is.
|
33
|
+
# @param input_params [Hash] The input parameters as a hash
|
34
|
+
# @param _input_headers [Hash] The input headers as a hash
|
35
|
+
# @param _input_body [String] The input body
|
36
|
+
# @return [Array] A 'rack' response array (status, headers, body)
|
37
|
+
def to_rack(input_params, _input_headers, _input_body)
|
38
|
+
output_headers = headers.clone
|
39
|
+
output_content = content
|
40
|
+
output_status_code = status_code
|
41
|
+
if is_template
|
42
|
+
data = OpenStruct.new input_params
|
43
|
+
begin
|
44
|
+
template = Tilt['handlebars'].new { output_content }
|
45
|
+
output_content = template.render data
|
46
|
+
rescue ::FlavourSaver::UnknownHelperException => ex
|
47
|
+
method = ex.message.match(/Template context doesn't respond to method "(.*)"/)[1]
|
48
|
+
output_content = "Missing var or method '#{method}' in data."
|
49
|
+
output_status_code = 500
|
50
|
+
end
|
51
|
+
end
|
52
|
+
EventMachine::Synchrony.sleep(delay) if delay > 0
|
53
|
+
[output_status_code, output_headers, output_content]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'application'
|
2
|
+
require 'active_record'
|
3
|
+
module AgileProxy
|
4
|
+
#
|
5
|
+
# = An API User
|
6
|
+
#
|
7
|
+
# The API access to the system is multi user in that each user
|
8
|
+
# can have many applications and each application can have many stubs etc...
|
9
|
+
#
|
10
|
+
# This class is responsible for :-
|
11
|
+
# 1. Retrieving users from storage
|
12
|
+
# 2. Persisting users to storage
|
13
|
+
# 3. Providing a list of applications that the user owns
|
14
|
+
class User < ActiveRecord::Base
|
15
|
+
has_many :applications
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'http/parser'
|
4
|
+
require 'em-http'
|
5
|
+
require 'evma_httpserver'
|
6
|
+
require 'em-synchrony'
|
7
|
+
require 'stringio'
|
8
|
+
require 'rack'
|
9
|
+
|
10
|
+
module AgileProxy
|
11
|
+
#
|
12
|
+
# = The Proxy Connection
|
13
|
+
#
|
14
|
+
# This class is the event machine connection used by the proxy. Every request creates a new instance of this
|
15
|
+
class ProxyConnection < EventMachine::Connection
|
16
|
+
attr_accessor :handler
|
17
|
+
def post_init
|
18
|
+
@parser = Http::Parser.new(self)
|
19
|
+
end
|
20
|
+
|
21
|
+
def receive_data(data)
|
22
|
+
@parser << data
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_message_begin
|
26
|
+
@headers = nil
|
27
|
+
@body = ''
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_headers_complete(headers)
|
31
|
+
@headers = headers
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_body(chunk)
|
35
|
+
@body << chunk
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_message_complete
|
39
|
+
if @parser.http_method == 'CONNECT'
|
40
|
+
restart_with_ssl(@parser.request_url)
|
41
|
+
else
|
42
|
+
if @ssl
|
43
|
+
uri = URI.parse(@parser.request_url)
|
44
|
+
@url = "https://#{@ssl}#{[uri.path, uri.query].compact.join('?')}"
|
45
|
+
else
|
46
|
+
@url = @parser.request_url
|
47
|
+
end
|
48
|
+
handle_request
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def restart_with_ssl(url)
|
55
|
+
@ssl = url
|
56
|
+
@parser = Http::Parser.new(self)
|
57
|
+
@original_headers = @headers.clone
|
58
|
+
send_data("HTTP/1.0 200 Connection established\r\nProxy-agent: Http-Flexible-Proxy/0.0.0\r\n\r\n")
|
59
|
+
start_tls(
|
60
|
+
private_key_file: File.expand_path('../mitm.key', __FILE__),
|
61
|
+
cert_chain_file: File.expand_path('../mitm.crt', __FILE__)
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def handle_request
|
66
|
+
EM.synchrony do
|
67
|
+
request = ActionDispatch::Request.new(env)
|
68
|
+
request.params # This will populate action_dispatch.request.parameters
|
69
|
+
handler.call(env).tap do |response|
|
70
|
+
send_response(response)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def env
|
78
|
+
return @__env if @__env
|
79
|
+
fake_input_buffer = StringIO.new(@body)
|
80
|
+
fake_error_buffer = StringIO.new
|
81
|
+
url_parsed = URI.parse(@url)
|
82
|
+
@__env = {
|
83
|
+
'rack.input' => Rack::Lint::InputWrapper.new(fake_input_buffer),
|
84
|
+
'rack.errors' => Rack::Lint::ErrorWrapper.new(fake_error_buffer),
|
85
|
+
'REQUEST_METHOD' => @parser.http_method,
|
86
|
+
'REQUEST_PATH' => url_parsed.path,
|
87
|
+
'PATH_INFO' => url_parsed.path,
|
88
|
+
'QUERY_STRING' => url_parsed.query || '',
|
89
|
+
'REQUEST_URI' => url_parsed.path + (url_parsed.query || ''),
|
90
|
+
'rack.url_scheme' => url_parsed.scheme,
|
91
|
+
'CONTENT_LENGTH' => @body.length,
|
92
|
+
'SERVER_NAME' => url_parsed.host,
|
93
|
+
'SERVER_PORT' => url_parsed.port
|
94
|
+
|
95
|
+
}
|
96
|
+
@headers.merge(@original_headers || {}).each do |name, value|
|
97
|
+
converted_name = "HTTP_#{name.gsub(/-/, '_').upcase}"
|
98
|
+
@__env[converted_name] = value
|
99
|
+
end
|
100
|
+
@__env['CONTENT_TYPE'] = @__env.delete('HTTP_CONTENT_TYPE') if @__env.key?('HTTP_CONTENT_TYPE')
|
101
|
+
@__env['CONTENT_LENGTH'] = @__env.delete('HTTP_CONTENT_LENGTH') if @__env.key?('HTTP_CONTENT_LENGTH')
|
102
|
+
@__env
|
103
|
+
end
|
104
|
+
|
105
|
+
def send_response(response)
|
106
|
+
res = EM::DelegatedHttpResponse.new(self)
|
107
|
+
res.status = response[0]
|
108
|
+
res.headers = response[1]
|
109
|
+
res.content = response[2]
|
110
|
+
res.send_response
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module AgileProxy
|
2
|
+
#
|
3
|
+
# An instance of this class represents a route within the system.
|
4
|
+
#
|
5
|
+
class Route
|
6
|
+
attr_accessor :request_method, :pattern, :app, :constraints, :name
|
7
|
+
|
8
|
+
PATH_INFO = 'PATH_INFO'.freeze
|
9
|
+
ROUTE_PARAMS = 'rack.route_params'.freeze
|
10
|
+
QUERY_STRING = 'QUERY_STRING'.freeze
|
11
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
12
|
+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
13
|
+
HEAD = 'HEAD'.freeze
|
14
|
+
GET = 'GET'.freeze
|
15
|
+
POST = 'POST'.freeze
|
16
|
+
PUT = 'PUT'.freeze
|
17
|
+
DELETE = 'DELETE'.freeze
|
18
|
+
|
19
|
+
DEFAULT_WILDCARD_NAME = :paths
|
20
|
+
WILDCARD_PATTERN = /\/\*(.*)/.freeze
|
21
|
+
NAMED_SEGMENTS_PATTERN = /\/:([^$\/]+)/.freeze
|
22
|
+
NAMED_SEGMENTS_REPLACEMENT_PATTERN = /\/:([^$\/]+)/.freeze
|
23
|
+
DOT = '.'.freeze
|
24
|
+
|
25
|
+
def initialize(request_method, pattern, app, options = {})
|
26
|
+
fail ArgumentError, 'pattern cannot be blank' if pattern.to_s.strip.empty?
|
27
|
+
fail ArgumentError, 'app must be callable' unless app.respond_to?(:call)
|
28
|
+
@request_method = request_method
|
29
|
+
@pattern = pattern
|
30
|
+
@app = app
|
31
|
+
@constraints = options && options[:constraints]
|
32
|
+
@name = options && options[:as]
|
33
|
+
end
|
34
|
+
|
35
|
+
def regexp
|
36
|
+
@regexp ||= compile
|
37
|
+
end
|
38
|
+
|
39
|
+
def compile
|
40
|
+
pattern_match = pattern.match(WILDCARD_PATTERN)
|
41
|
+
src = if pattern_match
|
42
|
+
@wildcard_name = if pattern_match[1].to_s.strip.empty?
|
43
|
+
DEFAULT_WILDCARD_NAME
|
44
|
+
else
|
45
|
+
pattern_match[1].to_sym
|
46
|
+
end
|
47
|
+
pattern.gsub(WILDCARD_PATTERN, '(?:/(.*)|)')
|
48
|
+
else
|
49
|
+
pattern_match = pattern.match(NAMED_SEGMENTS_PATTERN)
|
50
|
+
p = if pattern_match
|
51
|
+
pattern.gsub(NAMED_SEGMENTS_REPLACEMENT_PATTERN, '/(?<\1>[^.$/]+)')
|
52
|
+
else
|
53
|
+
pattern
|
54
|
+
end
|
55
|
+
p + '(?:\.(?<format>.*))?'
|
56
|
+
end
|
57
|
+
Regexp.new("\\A#{src}\\Z")
|
58
|
+
end
|
59
|
+
|
60
|
+
def match(env)
|
61
|
+
request_method = env[REQUEST_METHOD]
|
62
|
+
request_method = GET if request_method == HEAD
|
63
|
+
path = env[PATH_INFO]
|
64
|
+
qs = env[QUERY_STRING]
|
65
|
+
return nil unless request_method == self.request_method
|
66
|
+
fail ArgumentError, 'path is required' if path.to_s.strip.empty?
|
67
|
+
path_match = path.match(regexp)
|
68
|
+
return unless path_match
|
69
|
+
params = if @wildcard_name
|
70
|
+
{ @wildcard_name => path_match[1].to_s.split('/') }
|
71
|
+
else
|
72
|
+
Hash[path_match.names.map(&:to_sym).zip(path_match.captures)]
|
73
|
+
end
|
74
|
+
params.merge!(::Rack::Utils.parse_nested_query(qs).symbolize_keys) unless qs.nil? || qs.empty?
|
75
|
+
params.merge! env[FORM_HASH] if env.key? FORM_HASH
|
76
|
+
params.delete(:format) if params.key?(:format) && params[:format].nil?
|
77
|
+
|
78
|
+
params if meets_constraints(params)
|
79
|
+
end
|
80
|
+
|
81
|
+
def meets_constraints(params)
|
82
|
+
if constraints
|
83
|
+
constraints.each do |param, constraint|
|
84
|
+
unless params.symbolize_keys[param.to_sym].to_s.match(constraint)
|
85
|
+
return false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
def eql?(other)
|
93
|
+
other.is_a?(self.class) &&
|
94
|
+
other.request_method == request_method &&
|
95
|
+
other.pattern == pattern &&
|
96
|
+
other.app == app &&
|
97
|
+
other.constraints == constraints
|
98
|
+
end
|
99
|
+
|
100
|
+
alias_method :==, :eql?
|
101
|
+
|
102
|
+
def hash
|
103
|
+
request_method.hash ^ pattern.hash ^ app.hash ^ constraints.hash
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'agile_proxy/route'
|
2
|
+
|
3
|
+
module AgileProxy
|
4
|
+
#
|
5
|
+
# A rack router used for selecting a 'request spec' from a shortlist
|
6
|
+
#
|
7
|
+
class Router
|
8
|
+
VERSION = '0.4.0'
|
9
|
+
|
10
|
+
HEAD = 'HEAD'.freeze
|
11
|
+
GET = 'GET'.freeze
|
12
|
+
POST = 'POST'.freeze
|
13
|
+
PUT = 'PUT'.freeze
|
14
|
+
DELETE = 'DELETE'.freeze
|
15
|
+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
16
|
+
PATH_INFO = 'PATH_INFO'.freeze
|
17
|
+
ROUTE_PARAMS = 'rack.route_params'.freeze
|
18
|
+
QUERY_STRING = 'QUERY_STRING'.freeze
|
19
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
20
|
+
|
21
|
+
def initialize(&block)
|
22
|
+
@named_routes = {}
|
23
|
+
routes(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](route_name)
|
27
|
+
@named_routes[route_name]
|
28
|
+
end
|
29
|
+
|
30
|
+
def routes(&block)
|
31
|
+
instance_eval(&block) if block
|
32
|
+
@routes
|
33
|
+
end
|
34
|
+
|
35
|
+
def get(route_spec)
|
36
|
+
route(GET, route_spec)
|
37
|
+
end
|
38
|
+
|
39
|
+
def post(route_spec)
|
40
|
+
route(POST, route_spec)
|
41
|
+
end
|
42
|
+
|
43
|
+
def put(route_spec)
|
44
|
+
route(PUT, route_spec)
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(route_spec)
|
48
|
+
route(DELETE, route_spec)
|
49
|
+
end
|
50
|
+
|
51
|
+
def route(method, route_spec)
|
52
|
+
route = Route.new(
|
53
|
+
method,
|
54
|
+
route_spec.first.first,
|
55
|
+
route_spec.first.last,
|
56
|
+
route_spec.reject { |k, _| k == route_spec.first.first }
|
57
|
+
)
|
58
|
+
@routes ||= []
|
59
|
+
@routes << route
|
60
|
+
if route_spec && route_spec[:as]
|
61
|
+
# Using ||= so the first route with that name will be returned
|
62
|
+
@named_routes[route_spec[:as].to_sym] ||= route_spec.first.first
|
63
|
+
end
|
64
|
+
route
|
65
|
+
end
|
66
|
+
|
67
|
+
def call(env)
|
68
|
+
app = match(env)
|
69
|
+
if app
|
70
|
+
app.call(env)
|
71
|
+
else
|
72
|
+
not_found(env)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def match(env)
|
77
|
+
routes.each do |route|
|
78
|
+
params = route.match(env)
|
79
|
+
if params
|
80
|
+
env[ROUTE_PARAMS] = params
|
81
|
+
return route.app
|
82
|
+
end
|
83
|
+
end
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def not_found(env)
|
88
|
+
body = "<h1>Not Found</h1><p>No route matches #{env[REQUEST_METHOD]} #{env[PATH_INFO]}</p>"
|
89
|
+
[
|
90
|
+
404,
|
91
|
+
{
|
92
|
+
'Content-Type' => 'text/html',
|
93
|
+
'Content-Length' => body.length.to_s
|
94
|
+
},
|
95
|
+
[body]
|
96
|
+
]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|