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.
@@ -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