http_router 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +60 -0
- data/Rakefile +32 -0
- data/VERSION +1 -0
- data/lib/http_router/glob.rb +18 -0
- data/lib/http_router/node.rb +107 -0
- data/lib/http_router/path.rb +38 -0
- data/lib/http_router/response.rb +20 -0
- data/lib/http_router/root.rb +103 -0
- data/lib/http_router/route.rb +102 -0
- data/lib/http_router/sinatra.rb +149 -0
- data/lib/http_router/variable.rb +26 -0
- data/lib/http_router.rb +250 -0
- data/lib/rack/uri_escape.rb +38 -0
- data/spec/generate_spec.rb +54 -0
- data/spec/rack/dispatch_spec.rb +113 -0
- data/spec/rack/generate_spec.rb +27 -0
- data/spec/rack/route_spec.rb +70 -0
- data/spec/recognize_spec.rb +176 -0
- data/spec/sinatra/recognize_spec.rb +140 -0
- data/spec/spec.opts +7 -0
- data/spec/spec_helper.rb +24 -0
- metadata +88 -0
data/README.rdoc
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
= HTTP Router
|
2
|
+
|
3
|
+
== Introduction
|
4
|
+
|
5
|
+
When I wrote Usher, I made a few compromised in design that I wasn't totally happy with. More and more features got added to it, and eventually, it became harder to maintain. I took a few moments to work in Node.js, and wrote a router there called Sherpa, which I was happier with. But I felt that by losing more abstraction, and tackling just the problem of HTTP routing, I could come up with something even better.
|
6
|
+
|
7
|
+
== Features
|
8
|
+
|
9
|
+
* Supports variables, and globbing.
|
10
|
+
* Regex support for variables.
|
11
|
+
* Request condition support.
|
12
|
+
* Partial matches.
|
13
|
+
* Supports interstitial variables (e.g. /my-:variable-brings.all.the.boys/yard).
|
14
|
+
* Very fast and small code base (~600 loc).
|
15
|
+
* Sinatra compatibility.
|
16
|
+
|
17
|
+
== Usage
|
18
|
+
|
19
|
+
=== <tt>HttpRouter.new</tt>
|
20
|
+
|
21
|
+
Takes the following options:
|
22
|
+
|
23
|
+
* <tt>:default_app</tt> - The default #call made on non-matches. Defaults to a 404 generator.
|
24
|
+
* <tt>:ignore_trailing_slash</tt> - Ignores the trailing slash when matching. Defaults to true.
|
25
|
+
* <tt>:redirect_trailing_slash</tt> - Redirect on trailing slash matches to non-trailing slash paths. Defaults to false.
|
26
|
+
|
27
|
+
=== <tt>#add(name, options)</tt>
|
28
|
+
|
29
|
+
Maps a route. The format for variables in paths is:
|
30
|
+
:variable
|
31
|
+
*glob
|
32
|
+
|
33
|
+
Everything else is treated literally. Optional parts are surrounded by brackets. Partially matching paths have a trailing <tt>*</tt>. Optional trailing slash matching is done with <tt>/?</tt>.
|
34
|
+
|
35
|
+
Once you have a route object, use <tt>HttpRouter::Route#to</tt> to add a destination and <tt>HttpRouter::Route#name</tt> to name it.
|
36
|
+
|
37
|
+
e.g.
|
38
|
+
|
39
|
+
r = HttpRouter.new
|
40
|
+
r.add('/test/:variable(.:format)').name(:my_test_path).to {|env| [200, {}, "Hey dude #{env['router.params'][:variable]}"]}
|
41
|
+
r.add('/test').redirect_to("http://www.google.com")
|
42
|
+
r.add('/static').serves_static_from('/my_file_system')
|
43
|
+
|
44
|
+
As well, you can support regex matching and request conditions. To add a regex match, use <tt>:matches_with => { :id => /\d+/ }</tt>.
|
45
|
+
To match on a request condition you can use <tt>:conditions => {:request_method => %w(POST HEAD)}</tt>.
|
46
|
+
|
47
|
+
There are convenience methods HttpRouter#get, HttpRouter#post, etc for each request method.
|
48
|
+
|
49
|
+
=== <tt>#url(name or route, *args)</tt>
|
50
|
+
|
51
|
+
Generates a route. The args can either be a hash, a list, or a mix of both.
|
52
|
+
|
53
|
+
=== <tt>#call(env or Rack::Request)</tt>
|
54
|
+
|
55
|
+
Recognizes and dispatches the request.
|
56
|
+
|
57
|
+
=== <tt>#recognize(env or Rack::Request)</tt>
|
58
|
+
|
59
|
+
Only performs recognition.
|
60
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |s|
|
4
|
+
s.name = "http_router"
|
5
|
+
s.description = s.summary = "A kick-ass HTTP router for use in Rack & Sinatra"
|
6
|
+
s.email = "joshbuddy@gmail.com"
|
7
|
+
s.homepage = "http://github.com/joshbuddy/http_router"
|
8
|
+
s.authors = ["Joshua Hull"]
|
9
|
+
s.files = FileList["[A-Z]*", "{lib,spec}/**/*"]
|
10
|
+
end
|
11
|
+
Jeweler::GemcutterTasks.new
|
12
|
+
rescue LoadError
|
13
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'spec'
|
17
|
+
require 'spec/rake/spectask'
|
18
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
19
|
+
t.spec_opts ||= []
|
20
|
+
t.ruby_opts << "-rrubygems"
|
21
|
+
t.ruby_opts << "-Ilib"
|
22
|
+
t.ruby_opts << "-rhttp_router"
|
23
|
+
t.ruby_opts << "-rspec/spec_helper"
|
24
|
+
t.spec_opts << "--options" << "spec/spec.opts"
|
25
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'code_stats'
|
30
|
+
CodeStats::Tasks.new
|
31
|
+
rescue LoadError
|
32
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class HttpRouter
|
2
|
+
class Glob < Variable
|
3
|
+
def matches(parts, whole_path)
|
4
|
+
if @matches_with && match = @matches_with.match(parts.first)
|
5
|
+
params = [parts.shift]
|
6
|
+
while !parts.empty? and match = @matches_with.match(parts.first)
|
7
|
+
params << parts.shift
|
8
|
+
end
|
9
|
+
whole_path.replace(parts.join('/'))
|
10
|
+
params
|
11
|
+
else
|
12
|
+
params = parts.dup
|
13
|
+
parts.clear
|
14
|
+
params
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class HttpRouter
|
2
|
+
class Node
|
3
|
+
attr_accessor :value, :variable, :catchall, :request_node
|
4
|
+
attr_reader :linear, :lookup
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
reset!
|
8
|
+
end
|
9
|
+
|
10
|
+
def reset!
|
11
|
+
@linear = []
|
12
|
+
@lookup = {}
|
13
|
+
@catchall = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(val)
|
17
|
+
if val.is_a?(Variable)
|
18
|
+
if val.matches_with
|
19
|
+
new_node = Node.new
|
20
|
+
@linear << [val, new_node]
|
21
|
+
new_node
|
22
|
+
else
|
23
|
+
@catchall ||= Node.new
|
24
|
+
@catchall.variable = val
|
25
|
+
@catchall
|
26
|
+
end
|
27
|
+
elsif val.is_a?(Regexp)
|
28
|
+
@linear << [val, Node.new]
|
29
|
+
@linear.last.last
|
30
|
+
else
|
31
|
+
@lookup[val] ||= Node.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_request_methods(options)
|
36
|
+
if options && options[:conditions]
|
37
|
+
current_nodes = [@request_node ||= RequestNode.new]
|
38
|
+
request_options = options[:conditions]
|
39
|
+
RequestNode::RequestMethods.each do |method|
|
40
|
+
current_nodes.each_with_index do |current_node, current_node_index|
|
41
|
+
if request_options[method] #we care about the method
|
42
|
+
if current_node # and we have to pay attention to what currently is there.
|
43
|
+
unless current_node.request_method
|
44
|
+
current_node.request_method = method
|
45
|
+
end
|
46
|
+
|
47
|
+
case RequestNode::RequestMethods.index(method) <=> RequestNode::RequestMethods.index(current_node.request_method)
|
48
|
+
when 0 #use this node
|
49
|
+
if request_options[method].is_a?(Regexp)
|
50
|
+
current_node = RequestNode.new
|
51
|
+
current_node.linear << [request_options[method], current_node]
|
52
|
+
elsif request_options[method].is_a?(Array)
|
53
|
+
current_nodes[current_node_index] = request_options[method].map{|val| current_node.lookup[val] ||= RequestNode.new}
|
54
|
+
else
|
55
|
+
current_nodes[current_node_index] = (current_node.lookup[request_options[method]] ||= RequestNode.new)
|
56
|
+
end
|
57
|
+
when 1 #this node is farther ahead
|
58
|
+
current_nodes[current_node_index] = (current_node.catchall ||= RequestNode.new)
|
59
|
+
redo
|
60
|
+
when -1 #this method is more important than the current node
|
61
|
+
new_node = RequestNode.new
|
62
|
+
new_node.request_method = method
|
63
|
+
new_node.catchall = current_node
|
64
|
+
current_nodes[current_node_index] = new_node
|
65
|
+
redo
|
66
|
+
end
|
67
|
+
else
|
68
|
+
current_nodes[current_node_index] = RequestNode.new
|
69
|
+
redo
|
70
|
+
end
|
71
|
+
elsif !current_node
|
72
|
+
@request_node = RequestNode.new
|
73
|
+
current_nodes[current_node_index] = @request_node
|
74
|
+
redo
|
75
|
+
else
|
76
|
+
current_node.catchall ||= RequestNode.new
|
77
|
+
end
|
78
|
+
end
|
79
|
+
current_nodes.flatten!
|
80
|
+
end
|
81
|
+
if @value
|
82
|
+
target_node = @request_node
|
83
|
+
while target_node.request_method
|
84
|
+
target_node = (target_node.catchall ||= RequestNode.new)
|
85
|
+
end
|
86
|
+
target_node.value = @value
|
87
|
+
@value = nil
|
88
|
+
end
|
89
|
+
current_nodes
|
90
|
+
elsif @request_node
|
91
|
+
current_node = @request_node
|
92
|
+
while current_node.request_method
|
93
|
+
current_node = (current_node.catchall ||= RequestNode.new)
|
94
|
+
end
|
95
|
+
[current_node]
|
96
|
+
else
|
97
|
+
[self]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class RequestNode < Node
|
103
|
+
RequestMethods = [:request_method, :host, :port, :scheme]
|
104
|
+
attr_accessor :request_method
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class HttpRouter
|
2
|
+
class Path
|
3
|
+
attr_reader :parts, :extension
|
4
|
+
attr_accessor :route
|
5
|
+
def initialize(path, parts, extension)
|
6
|
+
@path, @parts, @extension = path, parts, extension
|
7
|
+
@eval_path = path.gsub(/[:\*]([a-zA-Z0-9_]+)/) {"\#{args.shift || (options && options[:#{$1}]) || raise(MissingParameterException.new(\"missing parameter #{$1}\"))}" }
|
8
|
+
instance_eval "
|
9
|
+
def raw_url(args,options)
|
10
|
+
\"#{@eval_path}\"
|
11
|
+
end
|
12
|
+
"
|
13
|
+
end
|
14
|
+
|
15
|
+
def url(args, options)
|
16
|
+
path = raw_url(args, options)
|
17
|
+
raise TooManyParametersException.new unless args.empty?
|
18
|
+
Rack::Utils.uri_escape!(path)
|
19
|
+
path
|
20
|
+
end
|
21
|
+
|
22
|
+
def variables
|
23
|
+
unless @variables
|
24
|
+
@variables = @parts.select{|p| p.is_a?(Variable)}
|
25
|
+
@variables << @extension if @extension.is_a?(Variable)
|
26
|
+
end
|
27
|
+
@variables
|
28
|
+
end
|
29
|
+
|
30
|
+
def variable_names
|
31
|
+
variables.map{|v| v.name}
|
32
|
+
end
|
33
|
+
|
34
|
+
def matches_extension?(extension)
|
35
|
+
@extension.nil? || @extension === (extension)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class HttpRouter
|
2
|
+
class Response < Struct.new(:path, :params, :extension, :matched_path, :remaining_path)
|
3
|
+
attr_reader :params_as_hash, :route
|
4
|
+
|
5
|
+
def initialize(path, params, extension, matched_path, remaining_path)
|
6
|
+
raise if matched_path.nil?
|
7
|
+
super
|
8
|
+
@params_as_hash = path.variable_names.zip(params).inject({}) {|h, (k,v)| h[k] = v; h }
|
9
|
+
@params_as_hash[path.extension.name] = extension if path.extension && path.extension.is_a?(Variable)
|
10
|
+
end
|
11
|
+
|
12
|
+
def route
|
13
|
+
path.route
|
14
|
+
end
|
15
|
+
|
16
|
+
def partial_match?
|
17
|
+
remaining_path
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
class HttpRouter
|
2
|
+
class Root < Node
|
3
|
+
def initialize(base)
|
4
|
+
@base = base
|
5
|
+
reset!
|
6
|
+
end
|
7
|
+
|
8
|
+
def find(request)
|
9
|
+
path = request.path_info.dup
|
10
|
+
path.slice!(-1) if @base.ignore_trailing_slash? && path[-1] == ?/
|
11
|
+
path.gsub!(/\.([^\/\.]+)$/, '')
|
12
|
+
extension = $1
|
13
|
+
parts = @base.split(path)
|
14
|
+
parts << '' if path[path.size - 1] == ?/
|
15
|
+
|
16
|
+
current_node = self
|
17
|
+
params = []
|
18
|
+
while current_node
|
19
|
+
break if current_node.nil? || (current_node.value && current_node.value.route.partially_match?) || parts.empty?
|
20
|
+
unless current_node.linear.empty?
|
21
|
+
whole_path = parts.join('/')
|
22
|
+
next_node = current_node.linear.find do |(tester, node)|
|
23
|
+
if tester.is_a?(Regexp) and match = whole_path.match(tester)
|
24
|
+
whole_path.slice!(0,match[0].size)
|
25
|
+
parts.replace(@base.split(whole_path))
|
26
|
+
node
|
27
|
+
elsif new_params = tester.matches(parts, whole_path)
|
28
|
+
params << new_params
|
29
|
+
node
|
30
|
+
else
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
if next_node
|
35
|
+
current_node = next_node.last
|
36
|
+
next
|
37
|
+
end
|
38
|
+
end
|
39
|
+
if match = current_node.lookup[parts.first]
|
40
|
+
parts.shift
|
41
|
+
current_node = match
|
42
|
+
elsif current_node.catchall
|
43
|
+
params << current_node.catchall.variable.matches(parts, whole_path)
|
44
|
+
parts.shift
|
45
|
+
current_node = current_node.catchall
|
46
|
+
elsif parts.size == 1 && parts.first == '' && current_node && (current_node.value && current_node.value.route.trailing_slash_ignore?)
|
47
|
+
parts.shift
|
48
|
+
elsif current_node.request_node
|
49
|
+
break
|
50
|
+
else
|
51
|
+
current_node = nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
if current_node && current_node.request_node
|
56
|
+
current_node = current_node.request_node
|
57
|
+
while current_node
|
58
|
+
previous_node = current_node
|
59
|
+
break if current_node.nil? || current_node.is_a?(RoutingError) || current_node.value
|
60
|
+
request_value = request.send(current_node.request_method)
|
61
|
+
unless current_node.linear.empty?
|
62
|
+
next_node = current_node.linear.find do |(regexp, node)|
|
63
|
+
regexp === request_value
|
64
|
+
end
|
65
|
+
if next_node
|
66
|
+
current_node = next_node.last
|
67
|
+
next
|
68
|
+
end
|
69
|
+
end
|
70
|
+
current_node = current_node.lookup[request_value] || current_node.catchall
|
71
|
+
if current_node.nil?
|
72
|
+
current_node = previous_node.request_method == :request_method ? RoutingError.new(405, {"Allow" => previous_node.lookup.keys.join(", ")}) : nil
|
73
|
+
else
|
74
|
+
current_node
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
if current_node.is_a?(RoutingError)
|
79
|
+
current_node
|
80
|
+
elsif current_node && current_node.value
|
81
|
+
if parts.empty?
|
82
|
+
post_match(current_node.value, params, extension, request.path_info)
|
83
|
+
elsif current_node.value.route.partially_match?
|
84
|
+
rest = '/' << parts.join('/') << (extension ? ".#{extension}" : '')
|
85
|
+
|
86
|
+
post_match(current_node.value, params, nil, request.path_info[0, request.path_info.size - rest.size], rest)
|
87
|
+
else
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
else
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def post_match(path, params, extension, matched_path, remaining_path = nil)
|
96
|
+
if path.route.partially_match? || path.matches_extension?(extension)
|
97
|
+
Response.new(path, params, extension, matched_path, remaining_path)
|
98
|
+
else
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
class HttpRouter
|
2
|
+
class Route
|
3
|
+
attr_reader :dest, :paths
|
4
|
+
attr_accessor :trailing_slash_ignore, :partially_match, :default_values
|
5
|
+
|
6
|
+
def initialize(base, default_values)
|
7
|
+
@base, @default_values = base, default_values
|
8
|
+
@paths = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def name(name)
|
12
|
+
@name = name
|
13
|
+
@base.routes[name] = self
|
14
|
+
end
|
15
|
+
|
16
|
+
def named
|
17
|
+
@name
|
18
|
+
end
|
19
|
+
|
20
|
+
def to(dest = nil, &block)
|
21
|
+
@dest = dest || block
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def match_partially!(match = true)
|
26
|
+
@partially_match = match
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def redirect(path, status = 302)
|
31
|
+
unless (300..399).include?(status)
|
32
|
+
raise ArgumentError, "Status has to be an integer between 300 and 399"
|
33
|
+
end
|
34
|
+
to { |env|
|
35
|
+
params = env['router.params']
|
36
|
+
response = ::Rack::Response.new
|
37
|
+
response.redirect(eval(%|"#{path}"|), status)
|
38
|
+
response.finish
|
39
|
+
}
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def serves_static_from(root)
|
44
|
+
if File.directory?(root)
|
45
|
+
match_partially!
|
46
|
+
to ::Rack::File.new(root)
|
47
|
+
else
|
48
|
+
to proc{|env| env['PATH_INFO'] = File.basename(root); ::Rack::File.new(File.dirname(root)).call(env)}
|
49
|
+
end
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def trailing_slash_ignore?
|
54
|
+
@trailing_slash_ignore
|
55
|
+
end
|
56
|
+
|
57
|
+
def partially_match?
|
58
|
+
@partially_match
|
59
|
+
end
|
60
|
+
|
61
|
+
def url(*args)
|
62
|
+
options = args.last.is_a?(Hash) ? args.pop : nil
|
63
|
+
path = matching_path(args.empty? ? options : args)
|
64
|
+
raise UngeneratableRouteException.new unless path
|
65
|
+
path.url(args, options)
|
66
|
+
end
|
67
|
+
|
68
|
+
def matching_path(params)
|
69
|
+
if @paths.size == 1
|
70
|
+
@paths.first
|
71
|
+
else
|
72
|
+
if params.is_a?(Array)
|
73
|
+
@paths.each do |path|
|
74
|
+
if path.variables.size == params.size
|
75
|
+
return path
|
76
|
+
end
|
77
|
+
end
|
78
|
+
nil
|
79
|
+
else
|
80
|
+
maximum_matched_route = nil
|
81
|
+
maximum_matched_params = -1
|
82
|
+
@paths.each do |path|
|
83
|
+
param_count = 0
|
84
|
+
path.variables.each do |variable|
|
85
|
+
if params && params.key?(variable.name)
|
86
|
+
param_count += 1
|
87
|
+
else
|
88
|
+
param_count = -1
|
89
|
+
break
|
90
|
+
end
|
91
|
+
end
|
92
|
+
if (param_count != -1 && param_count > maximum_matched_params)
|
93
|
+
maximum_matched_params = param_count;
|
94
|
+
maximum_matched_route = path;
|
95
|
+
end
|
96
|
+
end
|
97
|
+
maximum_matched_route
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..')
|
2
|
+
require 'http_router'
|
3
|
+
|
4
|
+
class HttpRouter
|
5
|
+
module Interface
|
6
|
+
class Sinatra
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
::Sinatra.send(:include, Extension)
|
10
|
+
end
|
11
|
+
|
12
|
+
module Extension
|
13
|
+
|
14
|
+
def self.registered(app)
|
15
|
+
app.send(:include, Extension)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(base)
|
19
|
+
base.extend ClassMethods
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate(name, *params)
|
23
|
+
self.class.generate(name, *params)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def route!(base=self.class, pass_block=nil)
|
28
|
+
if base.router and match = base.router.recognize(@request)
|
29
|
+
if match.is_a?(RoutingError)
|
30
|
+
route_eval {
|
31
|
+
match.headers.each{|k,v| response[k] = v}
|
32
|
+
status match.status
|
33
|
+
}
|
34
|
+
else
|
35
|
+
@block_params = match.params
|
36
|
+
(@params ||= {}).merge!(match.params_as_hash)
|
37
|
+
pass_block = catch(:pass) do
|
38
|
+
route_eval(&match.route.dest)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Run routes defined in superclass.
|
44
|
+
if base.superclass.respond_to?(:router)
|
45
|
+
route! base.superclass, pass_block
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
route_eval(&pass_block) if pass_block
|
50
|
+
|
51
|
+
route_missing
|
52
|
+
end
|
53
|
+
|
54
|
+
module ClassMethods
|
55
|
+
|
56
|
+
def new(*args, &bk)
|
57
|
+
configure! unless @_configured
|
58
|
+
super(*args, &bk)
|
59
|
+
end
|
60
|
+
|
61
|
+
def route(verb, path, options={}, &block)
|
62
|
+
name = options.delete(:name)
|
63
|
+
options[:conditions] ||= {}
|
64
|
+
options[:conditions][:request_method] = verb
|
65
|
+
options[:conditions][:host] = options.delete(:host) if options.key?(:host)
|
66
|
+
|
67
|
+
define_method "#{verb} #{path}", &block
|
68
|
+
unbound_method = instance_method("#{verb} #{path}")
|
69
|
+
block =
|
70
|
+
if block.arity != 0
|
71
|
+
lambda { unbound_method.bind(self).call(*@block_params) }
|
72
|
+
else
|
73
|
+
lambda { unbound_method.bind(self).call }
|
74
|
+
end
|
75
|
+
|
76
|
+
invoke_hook(:route_added, verb, path, block)
|
77
|
+
|
78
|
+
route = router.add(path, options).to(block)
|
79
|
+
route.name(name) if name
|
80
|
+
route
|
81
|
+
end
|
82
|
+
|
83
|
+
def router
|
84
|
+
@router ||= HttpRouter.new
|
85
|
+
block_given? ? yield(@router) : @router
|
86
|
+
end
|
87
|
+
|
88
|
+
def generate(name, *params)
|
89
|
+
router.url(name, *params)
|
90
|
+
end
|
91
|
+
|
92
|
+
def reset!
|
93
|
+
router.reset!
|
94
|
+
super
|
95
|
+
end
|
96
|
+
|
97
|
+
def configure!
|
98
|
+
configure :development do
|
99
|
+
error 404 do
|
100
|
+
content_type 'text/html'
|
101
|
+
|
102
|
+
(<<-HTML).gsub(/^ {17}/, '')
|
103
|
+
<!DOCTYPE html>
|
104
|
+
<html>
|
105
|
+
<head>
|
106
|
+
<style type="text/css">
|
107
|
+
body { text-align:center;font-family:helvetica,arial;font-size:22px;
|
108
|
+
color:#888;margin:20px}
|
109
|
+
#c {margin:0 auto;width:500px;text-align:left}
|
110
|
+
</style>
|
111
|
+
</head>
|
112
|
+
<body>
|
113
|
+
<h2>Sinatra doesn't know this ditty.</h2>
|
114
|
+
<div id="c">
|
115
|
+
Try this:
|
116
|
+
<pre>#{request.request_method.downcase} '#{request.path_info}' do\n "Hello World"\nend</pre>
|
117
|
+
</div>
|
118
|
+
</body>
|
119
|
+
</html>
|
120
|
+
HTML
|
121
|
+
end
|
122
|
+
error 405 do
|
123
|
+
content_type 'text/html'
|
124
|
+
|
125
|
+
(<<-HTML).gsub(/^ {17}/, '')
|
126
|
+
<!DOCTYPE html>
|
127
|
+
<html>
|
128
|
+
<head>
|
129
|
+
<style type="text/css">
|
130
|
+
body { text-align:center;font-family:helvetica,arial;font-size:22px;
|
131
|
+
color:#888;margin:20px}
|
132
|
+
#c {margin:0 auto;width:500px;text-align:left}
|
133
|
+
</style>
|
134
|
+
</head>
|
135
|
+
<body>
|
136
|
+
<h2>Sinatra sorta knows this ditty, but the request method is not allowed.</h2>
|
137
|
+
</body>
|
138
|
+
</html>
|
139
|
+
HTML
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
@_configured = true
|
144
|
+
end
|
145
|
+
end # ClassMethods
|
146
|
+
end # Extension
|
147
|
+
end # Sinatra
|
148
|
+
end # Interface
|
149
|
+
end # HttpRouter
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class HttpRouter
|
2
|
+
class Variable
|
3
|
+
attr_reader :name, :matches_with
|
4
|
+
|
5
|
+
def initialize(base, name, matches_with = nil)
|
6
|
+
@base = base
|
7
|
+
@name = name
|
8
|
+
@matches_with = matches_with
|
9
|
+
end
|
10
|
+
|
11
|
+
def matches(parts, whole_path)
|
12
|
+
if @matches_with.nil?
|
13
|
+
parts.first
|
14
|
+
elsif @matches_with && match = @matches_with.match(whole_path)
|
15
|
+
whole_path.slice!(0, match[0].size)
|
16
|
+
parts.replace(@base.split(whole_path))
|
17
|
+
match[0]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def ===(part)
|
22
|
+
@matches_with.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|