josh-rack-mount 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/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
|