http_router 0.0.1
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/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
|