josh-rack-mount 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +28 -0
- data/lib/rack/mount.rb +18 -0
- data/lib/rack/mount/const.rb +39 -0
- data/lib/rack/mount/exceptions.rb +5 -0
- data/lib/rack/mount/generation.rb +9 -0
- data/lib/rack/mount/generation/optimizations.rb +83 -0
- data/lib/rack/mount/generation/route.rb +108 -0
- data/lib/rack/mount/generation/route_set.rb +60 -0
- data/lib/rack/mount/mappers/merb.rb +143 -0
- data/lib/rack/mount/mappers/rails_classic.rb +164 -0
- data/lib/rack/mount/mappers/rails_draft.rb +179 -0
- data/lib/rack/mount/mappers/simple.rb +37 -0
- data/lib/rack/mount/nested_set.rb +106 -0
- data/lib/rack/mount/path_prefix.rb +20 -0
- data/lib/rack/mount/recognition.rb +8 -0
- data/lib/rack/mount/recognition/route.rb +94 -0
- data/lib/rack/mount/recognition/route_set.rb +54 -0
- data/lib/rack/mount/regexp_with_named_groups.rb +38 -0
- data/lib/rack/mount/request.rb +30 -0
- data/lib/rack/mount/route.rb +63 -0
- data/lib/rack/mount/route_set.rb +34 -0
- data/lib/rack/mount/utils.rb +149 -0
- data/rails/init.rb +2 -0
- metadata +86 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rack
|
2
|
+
module Mount
|
3
|
+
class PathPrefix #:nodoc:
|
4
|
+
def initialize(app, path_prefix = nil)
|
5
|
+
@app, @path_prefix = app, /^#{Regexp.escape(path_prefix)}/.freeze
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
path_info = Const::PATH_INFO
|
10
|
+
|
11
|
+
if env[path_info] =~ @path_prefix
|
12
|
+
env[path_info].sub!(@path_prefix, Const::EMPTY_STRING)
|
13
|
+
env[path_info] = Const::SLASH if env[path_info].empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
@app.call(env)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Mount
|
5
|
+
module Recognition
|
6
|
+
module Route #:nodoc:
|
7
|
+
def initialize(*args)
|
8
|
+
super
|
9
|
+
|
10
|
+
@path_keys = path_keys(@path, %w( / ))
|
11
|
+
@named_captures = named_captures(@path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
method = env[Const::REQUEST_METHOD]
|
16
|
+
path = env[Const::PATH_INFO]
|
17
|
+
|
18
|
+
if (@method.nil? || method == @method) && path =~ @path
|
19
|
+
routing_args, param_matches = @defaults.dup, $~.captures
|
20
|
+
@named_captures.each { |k, i|
|
21
|
+
if v = param_matches[i]
|
22
|
+
routing_args[k] = v
|
23
|
+
end
|
24
|
+
}
|
25
|
+
env[Const::RACK_ROUTING_ARGS] = routing_args
|
26
|
+
@app.call(env)
|
27
|
+
else
|
28
|
+
@throw
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def path_keys_at(index)
|
33
|
+
@path_keys[index]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
# Keys for inserting into NestedSet
|
38
|
+
# #=> ['people', /[0-9]+/, 'edit']
|
39
|
+
def path_keys(regexp, separators)
|
40
|
+
escaped_separators = separators.map { |s| Regexp.escape(s) }
|
41
|
+
separators = Regexp.compile(escaped_separators.join('|'))
|
42
|
+
segments = []
|
43
|
+
|
44
|
+
begin
|
45
|
+
Utils.extract_regexp_parts(regexp).each do |part|
|
46
|
+
raise ArgumentError if part.is_a?(Utils::Capture)
|
47
|
+
|
48
|
+
part = part.dup
|
49
|
+
part.gsub!(/\\\//, '/')
|
50
|
+
part.gsub!(/^\//, '')
|
51
|
+
|
52
|
+
scanner = StringScanner.new(part)
|
53
|
+
|
54
|
+
until scanner.eos?
|
55
|
+
unless s = scanner.scan_until(separators)
|
56
|
+
s = scanner.rest
|
57
|
+
scanner.terminate
|
58
|
+
end
|
59
|
+
|
60
|
+
s.gsub!(/\/$/, '')
|
61
|
+
segments << (clean_regexp?(s) ? s : nil)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
segments << Const::EOS_KEY
|
66
|
+
rescue ArgumentError
|
67
|
+
# generation failed somewhere, but lets take what we can get
|
68
|
+
end
|
69
|
+
|
70
|
+
# Pop off trailing nils
|
71
|
+
while segments.length > 0 && segments.last.nil?
|
72
|
+
segments.pop
|
73
|
+
end
|
74
|
+
|
75
|
+
segments.freeze
|
76
|
+
end
|
77
|
+
|
78
|
+
# Maps named captures to their capture index
|
79
|
+
# #=> { :controller => 0, :action => 1, :id => 2, :format => 4 }
|
80
|
+
def named_captures(regexp)
|
81
|
+
named_captures = {}
|
82
|
+
regexp.named_captures.each { |k, v|
|
83
|
+
named_captures[k.to_sym] = v.last - 1
|
84
|
+
}
|
85
|
+
named_captures.freeze
|
86
|
+
end
|
87
|
+
|
88
|
+
def clean_regexp?(source)
|
89
|
+
source =~ /^\w+$/
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Rack
|
2
|
+
module Mount
|
3
|
+
module Recognition
|
4
|
+
module RouteSet
|
5
|
+
DEFAULT_KEYS = [:method, [:path_keys_at, 0].freeze].freeze
|
6
|
+
DEFAULT_CATCH_STATUS = 404
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@catch = options.delete(:catch) || DEFAULT_CATCH_STATUS
|
10
|
+
@throw = Const::NOT_FOUND_RESPONSE.dup
|
11
|
+
@throw[0] = @catch
|
12
|
+
@throw.freeze
|
13
|
+
|
14
|
+
@recognition_keys = options.delete(:keys) || DEFAULT_KEYS
|
15
|
+
@recognition_keys.freeze
|
16
|
+
|
17
|
+
@recognition_graph = NestedSet.new
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_route(*args)
|
22
|
+
route = super
|
23
|
+
route.throw = @throw
|
24
|
+
|
25
|
+
keys = @recognition_keys.map { |key| route.send(*key) }
|
26
|
+
@recognition_graph[*keys] = route
|
27
|
+
|
28
|
+
route
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(env)
|
32
|
+
raise 'route set not finalized' unless frozen?
|
33
|
+
|
34
|
+
req = Request.new(env)
|
35
|
+
keys = @recognition_keys.map { |key| req.send(*key) }
|
36
|
+
@recognition_graph[*keys].each do |route|
|
37
|
+
result = route.call(env)
|
38
|
+
return result unless result[0] == @catch
|
39
|
+
end
|
40
|
+
@throw
|
41
|
+
end
|
42
|
+
|
43
|
+
def freeze
|
44
|
+
@recognition_graph.freeze
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
def height #:nodoc:
|
49
|
+
@recognition_graph.height
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Mount
|
5
|
+
unless Const::SUPPORTS_NAMED_CAPTURES
|
6
|
+
class RegexpWithNamedGroups < Regexp #:nodoc:
|
7
|
+
def self.new(regexp)
|
8
|
+
if regexp.is_a?(RegexpWithNamedGroups)
|
9
|
+
regexp
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :named_captures, :names
|
16
|
+
|
17
|
+
def initialize(regexp)
|
18
|
+
names = nil if names && !names.any?
|
19
|
+
regexp, @names = Utils.extract_named_captures(regexp)
|
20
|
+
|
21
|
+
@names = nil unless @names.any?
|
22
|
+
|
23
|
+
if @names
|
24
|
+
@named_captures = {}
|
25
|
+
@names.each_with_index { |n, i| @named_captures[n] = [i+1] if n }
|
26
|
+
end
|
27
|
+
|
28
|
+
(@named_captures ||= {}).freeze
|
29
|
+
(@names ||= []).freeze
|
30
|
+
|
31
|
+
super(regexp)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
else
|
35
|
+
RegexpWithNamedGroups = Regexp
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Rack
|
2
|
+
module Mount
|
3
|
+
class Request #:nodoc:
|
4
|
+
def initialize(env)
|
5
|
+
@env = env
|
6
|
+
end
|
7
|
+
|
8
|
+
def method
|
9
|
+
@method ||= @env[Const::REQUEST_METHOD] || Const::GET
|
10
|
+
end
|
11
|
+
|
12
|
+
def path
|
13
|
+
@path ||= @env[Const::PATH_INFO] || Const::SLASH
|
14
|
+
end
|
15
|
+
|
16
|
+
def path_keys_at(index)
|
17
|
+
path_keys[index]
|
18
|
+
end
|
19
|
+
|
20
|
+
def path_keys
|
21
|
+
@path_keys ||= begin
|
22
|
+
keys = path.split(%r{/|\.|\?})
|
23
|
+
keys.shift
|
24
|
+
keys << Const::EOS_KEY
|
25
|
+
keys
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Rack
|
2
|
+
module Mount
|
3
|
+
class Route #:nodoc:
|
4
|
+
module Base #:nodoc:
|
5
|
+
# TODO: Support any method on Request object
|
6
|
+
VALID_CONDITIONS = [:method, :path].freeze
|
7
|
+
|
8
|
+
attr_reader :app, :conditions, :defaults, :name
|
9
|
+
attr_reader :path, :method
|
10
|
+
attr_writer :throw
|
11
|
+
|
12
|
+
def initialize(app, conditions, defaults, name)
|
13
|
+
@app = app
|
14
|
+
validate_app!
|
15
|
+
|
16
|
+
@throw = Const::NOT_FOUND_RESPONSE
|
17
|
+
|
18
|
+
@name = name.to_sym if name
|
19
|
+
@defaults = (defaults || {}).freeze
|
20
|
+
|
21
|
+
@conditions = conditions
|
22
|
+
validate_conditions!
|
23
|
+
|
24
|
+
method = @conditions.delete(:method)
|
25
|
+
@method = method.to_s.upcase if method
|
26
|
+
|
27
|
+
path = @conditions.delete(:path)
|
28
|
+
if path.is_a?(Regexp)
|
29
|
+
@path = RegexpWithNamedGroups.new(path)
|
30
|
+
elsif path.is_a?(String)
|
31
|
+
path = "/#{path}" unless path =~ /^\//
|
32
|
+
@path = RegexpWithNamedGroups.compile("^#{path}$")
|
33
|
+
end
|
34
|
+
@path.freeze
|
35
|
+
|
36
|
+
@conditions.freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def validate_app!
|
41
|
+
unless @app.respond_to?(:call)
|
42
|
+
raise ArgumentError, 'app must be a valid rack application' \
|
43
|
+
' and respond to call'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_conditions!
|
48
|
+
unless @conditions.is_a?(Hash)
|
49
|
+
raise ArgumentError, 'conditions must be a Hash'
|
50
|
+
end
|
51
|
+
|
52
|
+
unless @conditions.keys.all? { |k| VALID_CONDITIONS.include?(k) }
|
53
|
+
raise ArgumentError, 'conditions may only include ' +
|
54
|
+
VALID_CONDITIONS.inspect
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
include Base
|
59
|
+
|
60
|
+
include Generation::Route, Recognition::Route
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Rack
|
2
|
+
module Mount
|
3
|
+
class RouteSet
|
4
|
+
module Base
|
5
|
+
def initialize(options = {})
|
6
|
+
if options.delete(:optimize) == true
|
7
|
+
extend Generation::Optimizations
|
8
|
+
end
|
9
|
+
|
10
|
+
if block_given?
|
11
|
+
yield self
|
12
|
+
freeze
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Builder method to add a route to the set
|
17
|
+
#
|
18
|
+
# <tt>app</tt>:: A valid Rack app to call if the conditions are met.
|
19
|
+
# <tt>conditions</tt>:: A hash of conditions to match against.
|
20
|
+
# Conditions may be expressed as strings or
|
21
|
+
# regexps to match against.
|
22
|
+
# <tt>defaults</tt>:: A hash of values that always gets merged in
|
23
|
+
# <tt>name</tt>:: Symbol identifier for the route used with named
|
24
|
+
# route generations
|
25
|
+
def add_route(app, conditions = {}, defaults = {}, name = nil)
|
26
|
+
Route.new(app, conditions, defaults, name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
include Base
|
30
|
+
|
31
|
+
include Generation::RouteSet, Recognition::RouteSet
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Mount
|
5
|
+
module Utils #:nodoc:
|
6
|
+
GLOB_REGEXP = /\/\\\*(\w+)$/
|
7
|
+
OPTIONAL_SEGMENT_REGEXP = /\\\((.+)\\\)/
|
8
|
+
SEGMENT_REGEXP = /(:([a-z](_?[a-z0-9])*))/
|
9
|
+
|
10
|
+
def convert_segment_string_to_regexp(str, requirements = {}, separators = [])
|
11
|
+
raise ArgumentError unless str.is_a?(String)
|
12
|
+
|
13
|
+
str = Regexp.escape(str.dup)
|
14
|
+
requirements = requirements || {}
|
15
|
+
str.replace("/#{str}") unless str =~ /^\//
|
16
|
+
|
17
|
+
re = ''
|
18
|
+
|
19
|
+
while m = (str.match(SEGMENT_REGEXP))
|
20
|
+
re << m.pre_match unless m.pre_match.empty?
|
21
|
+
if requirement = requirements[$2.to_sym]
|
22
|
+
re << Const::REGEXP_NAMED_CAPTURE % [$2, requirement.source]
|
23
|
+
else
|
24
|
+
re << Const::REGEXP_NAMED_CAPTURE % [$2, "[^#{separators.join}]+"]
|
25
|
+
end
|
26
|
+
str = m.post_match
|
27
|
+
end
|
28
|
+
|
29
|
+
re << str unless str.empty?
|
30
|
+
|
31
|
+
if m = re.match(GLOB_REGEXP)
|
32
|
+
re.sub!(GLOB_REGEXP, "/#{Const::REGEXP_NAMED_CAPTURE % [$1, '.*']}")
|
33
|
+
end
|
34
|
+
|
35
|
+
while re =~ OPTIONAL_SEGMENT_REGEXP
|
36
|
+
re.gsub!(OPTIONAL_SEGMENT_REGEXP, '(\1)?')
|
37
|
+
end
|
38
|
+
|
39
|
+
RegexpWithNamedGroups.new("^#{re}$")
|
40
|
+
end
|
41
|
+
module_function :convert_segment_string_to_regexp
|
42
|
+
|
43
|
+
class Capture < Array #:nodoc:
|
44
|
+
attr_reader :name, :optional
|
45
|
+
alias_method :optional?, :optional
|
46
|
+
|
47
|
+
def initialize(*args)
|
48
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
49
|
+
|
50
|
+
@name = options.delete(:name)
|
51
|
+
@name = @name.to_s if @name
|
52
|
+
|
53
|
+
@optional = options.delete(:optional) || false
|
54
|
+
|
55
|
+
super(args)
|
56
|
+
end
|
57
|
+
|
58
|
+
def ==(obj)
|
59
|
+
@name == obj.name && @optional == obj.optional && super
|
60
|
+
end
|
61
|
+
|
62
|
+
def optionalize!
|
63
|
+
@optional = true
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def named?
|
68
|
+
name && name != ''
|
69
|
+
end
|
70
|
+
|
71
|
+
def freeze
|
72
|
+
each { |e| e.freeze }
|
73
|
+
super
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def extract_regexp_parts(regexp)
|
78
|
+
unless regexp.is_a?(RegexpWithNamedGroups)
|
79
|
+
regexp = RegexpWithNamedGroups.new(regexp)
|
80
|
+
end
|
81
|
+
|
82
|
+
if regexp.source =~ /\?<([^>]+)>/
|
83
|
+
regexp, names = extract_named_captures(regexp)
|
84
|
+
else
|
85
|
+
names = regexp.names
|
86
|
+
end
|
87
|
+
source = regexp.source
|
88
|
+
|
89
|
+
source =~ /^\^/ ? source.gsub!(/^\^/, '') :
|
90
|
+
raise(ArgumentError, "#{source} needs to match the start of the string")
|
91
|
+
source.gsub!(/\$$/, '')
|
92
|
+
|
93
|
+
scanner = StringScanner.new(source)
|
94
|
+
stack = [[]]
|
95
|
+
|
96
|
+
capture_index = 0
|
97
|
+
until scanner.eos?
|
98
|
+
char = scanner.getch
|
99
|
+
cur = stack.last
|
100
|
+
|
101
|
+
if char == '('
|
102
|
+
name = names[capture_index]
|
103
|
+
capture = Capture.new(:name => name)
|
104
|
+
capture_index += 1
|
105
|
+
cur.push(capture)
|
106
|
+
stack.push(capture)
|
107
|
+
elsif char == ')'
|
108
|
+
capture = stack.pop
|
109
|
+
if scanner.peek(1) == '?'
|
110
|
+
scanner.pos += 1
|
111
|
+
capture.optionalize!
|
112
|
+
end
|
113
|
+
else
|
114
|
+
cur.push('') unless cur.last.is_a?(String)
|
115
|
+
cur.last << char
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
result = stack.pop
|
120
|
+
result.each { |e| e.freeze }
|
121
|
+
result
|
122
|
+
end
|
123
|
+
module_function :extract_regexp_parts
|
124
|
+
|
125
|
+
if Const::SUPPORTS_NAMED_CAPTURES
|
126
|
+
NAMED_CAPTURE_REGEXP = /\?<([^>]+)>/.freeze
|
127
|
+
else
|
128
|
+
NAMED_CAPTURE_REGEXP = /\?:<([^>]+)>/.freeze
|
129
|
+
end
|
130
|
+
|
131
|
+
def extract_named_captures(regexp)
|
132
|
+
source = Regexp.compile(regexp).source
|
133
|
+
names, scanner = [], StringScanner.new(source)
|
134
|
+
|
135
|
+
while scanner.skip_until(/\(/)
|
136
|
+
if scanner.scan(NAMED_CAPTURE_REGEXP)
|
137
|
+
names << scanner[1]
|
138
|
+
else
|
139
|
+
names << nil
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
source.gsub!(NAMED_CAPTURE_REGEXP, '')
|
144
|
+
return Regexp.compile(source), names
|
145
|
+
end
|
146
|
+
module_function :extract_named_captures
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|