josh-rack-mount 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,8 @@
1
+ module Rack
2
+ module Mount
3
+ module Recognition #:nodoc:
4
+ autoload :Route, 'rack/mount/recognition/route'
5
+ autoload :RouteSet, 'rack/mount/recognition/route_set'
6
+ end
7
+ end
8
+ 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