lgierth-rack-mount 0.6.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +36 -0
  3. data/lib/rack/mount.rb +32 -0
  4. data/lib/rack/mount/analysis/frequency.rb +60 -0
  5. data/lib/rack/mount/analysis/histogram.rb +74 -0
  6. data/lib/rack/mount/analysis/splitting.rb +159 -0
  7. data/lib/rack/mount/code_generation.rb +117 -0
  8. data/lib/rack/mount/generatable_regexp.rb +210 -0
  9. data/lib/rack/mount/multimap.rb +53 -0
  10. data/lib/rack/mount/prefix.rb +36 -0
  11. data/lib/rack/mount/regexp_with_named_groups.rb +69 -0
  12. data/lib/rack/mount/route.rb +130 -0
  13. data/lib/rack/mount/route_set.rb +420 -0
  14. data/lib/rack/mount/strexp.rb +68 -0
  15. data/lib/rack/mount/strexp/parser.rb +160 -0
  16. data/lib/rack/mount/strexp/parser.y +34 -0
  17. data/lib/rack/mount/strexp/tokenizer.rb +83 -0
  18. data/lib/rack/mount/strexp/tokenizer.rex +12 -0
  19. data/lib/rack/mount/utils.rb +162 -0
  20. data/lib/rack/mount/vendor/multimap/multimap.rb +569 -0
  21. data/lib/rack/mount/vendor/multimap/multiset.rb +185 -0
  22. data/lib/rack/mount/vendor/multimap/nested_multimap.rb +158 -0
  23. data/lib/rack/mount/vendor/regin/regin.rb +75 -0
  24. data/lib/rack/mount/vendor/regin/regin/alternation.rb +40 -0
  25. data/lib/rack/mount/vendor/regin/regin/anchor.rb +4 -0
  26. data/lib/rack/mount/vendor/regin/regin/atom.rb +54 -0
  27. data/lib/rack/mount/vendor/regin/regin/character.rb +51 -0
  28. data/lib/rack/mount/vendor/regin/regin/character_class.rb +50 -0
  29. data/lib/rack/mount/vendor/regin/regin/collection.rb +77 -0
  30. data/lib/rack/mount/vendor/regin/regin/expression.rb +126 -0
  31. data/lib/rack/mount/vendor/regin/regin/group.rb +85 -0
  32. data/lib/rack/mount/vendor/regin/regin/options.rb +55 -0
  33. data/lib/rack/mount/vendor/regin/regin/parser.rb +520 -0
  34. data/lib/rack/mount/vendor/regin/regin/tokenizer.rb +246 -0
  35. data/lib/rack/mount/vendor/regin/regin/version.rb +3 -0
  36. data/lib/rack/mount/version.rb +3 -0
  37. metadata +140 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Joshua Peek
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,36 @@
1
+ = Rack::Mount
2
+
3
+ A stackable dynamic tree based Rack router.
4
+
5
+ Rack::Mount supports Rack's +X-Cascade+ convention to continue trying routes if the response returns +pass+. This allows multiple routes to be nested or stacked on top of each other. Since the application endpoint can trigger the router to continue matching, middleware can be used to add arbitrary conditions to any route. This allows you to route based on other request attributes, session information, or even data dynamically pulled from a database.
6
+
7
+ === Usage
8
+
9
+ Rack::Mount provides a plugin API to build custom DSLs on top of.
10
+
11
+ The API is extremely minimal and only 3 methods are exposed as the public API.
12
+
13
+ <tt>Rack::Mount::RouteSet#add_route</tt>:: builder method for adding routes to the set
14
+ <tt>Rack::Mount::RouteSet#call</tt>:: Rack compatible recognition and dispatching method
15
+ <tt>Rack::Mount::RouteSet#generate</tt>:: generates a route condition from identifiers or significant keys
16
+
17
+ === Example
18
+
19
+ require 'rack/mount'
20
+
21
+ Routes = Rack::Mount::RouteSet.new do |set|
22
+ # add_route takes a rack application and conditions to match with
23
+ #
24
+ # valid conditions methods are any method on Rack::Request
25
+ # the values to match against may be strings or regexps
26
+ #
27
+ # See Rack::Mount::RouteSet#add_route for more options.
28
+ set.add_route FooApp, { :request_method => 'GET', :path_info => %r{^/foo$} }, {}, :foo
29
+ end
30
+
31
+ # The route set itself is a simple rack app you mount
32
+ run Routes
33
+
34
+
35
+ # generate path for route named "foo"
36
+ Routes.generate(:path_info, :foo) #=> "/foo"
@@ -0,0 +1,32 @@
1
+ require 'rack'
2
+
3
+ module Rack #:nodoc:
4
+ # A stackable dynamic tree based Rack router.
5
+ #
6
+ # Rack::Mount supports Rack's Cascade style of trying several routes until
7
+ # it finds one that is not a 404. This allows multiple routes to be nested
8
+ # or stacked on top of each other. Since the application endpoint can
9
+ # trigger the router to continue matching, middleware can be used to add
10
+ # arbitrary conditions to any route. This allows you to route based on
11
+ # other request attributes, session information, or even data dynamically
12
+ # pulled from a database.
13
+ module Mount
14
+ autoload :CodeGeneration, 'rack/mount/code_generation'
15
+ autoload :GeneratableRegexp, 'rack/mount/generatable_regexp'
16
+ autoload :Multimap, 'rack/mount/multimap'
17
+ autoload :Prefix, 'rack/mount/prefix'
18
+ autoload :RegexpWithNamedGroups, 'rack/mount/regexp_with_named_groups'
19
+ autoload :Route, 'rack/mount/route'
20
+ autoload :RouteSet, 'rack/mount/route_set'
21
+ autoload :RoutingError, 'rack/mount/route_set'
22
+ autoload :Strexp, 'rack/mount/strexp'
23
+ autoload :Utils, 'rack/mount/utils'
24
+ autoload :Version, 'rack/mount/version'
25
+
26
+ module Analysis #:nodoc:
27
+ autoload :Frequency, 'rack/mount/analysis/frequency'
28
+ autoload :Histogram, 'rack/mount/analysis/histogram'
29
+ autoload :Splitting, 'rack/mount/analysis/splitting'
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,60 @@
1
+ require 'rack/mount/utils'
2
+
3
+ module Rack::Mount
4
+ module Analysis
5
+ class Frequency #:nodoc:
6
+ def initialize(*keys)
7
+ clear
8
+ keys.each { |key| self << key }
9
+ end
10
+
11
+ def clear
12
+ @raw_keys = []
13
+ @key_frequency = Analysis::Histogram.new
14
+ self
15
+ end
16
+
17
+ def <<(key)
18
+ raise ArgumentError unless key.is_a?(Hash)
19
+ @raw_keys << key
20
+ nil
21
+ end
22
+
23
+ def possible_keys
24
+ @possible_keys ||= begin
25
+ @raw_keys.map do |key|
26
+ key.inject({}) { |requirements, (method, requirement)|
27
+ process_key(requirements, method, requirement)
28
+ requirements
29
+ }
30
+ end
31
+ end
32
+ end
33
+
34
+ def process_key(requirements, method, requirement)
35
+ if requirement.is_a?(Regexp)
36
+ expression = Utils.parse_regexp(requirement)
37
+
38
+ if expression.is_a?(Regin::Expression) && expression.anchored_to_line?
39
+ expression = Regin::Expression.new(expression.reject { |e| e.is_a?(Regin::Anchor) })
40
+ return requirements[method] = expression.to_s if expression.literal?
41
+ end
42
+ end
43
+
44
+ requirements[method] = requirement
45
+ end
46
+
47
+ def report
48
+ @report ||= begin
49
+ possible_keys.each { |keys| keys.each_pair { |key, _| @key_frequency << key } }
50
+ return [] if @key_frequency.count <= 1
51
+ @key_frequency.keys_in_upper_quartile
52
+ end
53
+ end
54
+
55
+ def expire!
56
+ @possible_keys = @report = nil
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,74 @@
1
+ module Rack::Mount
2
+ module Analysis
3
+ class Histogram < Hash #:nodoc:
4
+ attr_reader :count
5
+
6
+ def initialize
7
+ @count = 0
8
+ super(0)
9
+ expire_caches!
10
+ end
11
+
12
+ def <<(value)
13
+ @count += 1
14
+ self[value] += 1 if value
15
+ expire_caches!
16
+ self
17
+ end
18
+
19
+ def sorted_by_frequency
20
+ sort_by { |_, value| value }.reverse!
21
+ end
22
+
23
+ def max
24
+ @max ||= values.max || 0
25
+ end
26
+
27
+ def min
28
+ @min ||= values.min || 0
29
+ end
30
+
31
+ def mean
32
+ @mean ||= calculate_mean
33
+ end
34
+
35
+ def standard_deviation
36
+ @standard_deviation ||= calculate_standard_deviation
37
+ end
38
+
39
+ def upper_quartile_limit
40
+ @upper_quartile_limit ||= calculate_upper_quartile_limit
41
+ end
42
+
43
+ def keys_in_upper_quartile
44
+ @keys_in_upper_quartile ||= compute_keys_in_upper_quartile
45
+ end
46
+
47
+ private
48
+ def calculate_mean
49
+ count / size
50
+ end
51
+
52
+ def calculate_variance
53
+ values.inject(0) { |sum, e| sum + (e - mean) ** 2 } / count.to_f
54
+ end
55
+
56
+ def calculate_standard_deviation
57
+ Math.sqrt(calculate_variance)
58
+ end
59
+
60
+ def calculate_upper_quartile_limit
61
+ mean + standard_deviation
62
+ end
63
+
64
+ def compute_keys_in_upper_quartile
65
+ sorted_by_frequency.select { |_, value| value >= upper_quartile_limit }.map! { |key, _| key }
66
+ end
67
+
68
+ def expire_caches!
69
+ @max = @min = @mean = @standard_deviation = nil
70
+ @keys_in_upper_quartile = nil
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,159 @@
1
+ require 'rack/mount/utils'
2
+
3
+ module Rack::Mount
4
+ module Analysis
5
+ class Splitting < Frequency
6
+ NULL = "\0".freeze
7
+
8
+ class Key < Struct.new(:method, :index, :separators)
9
+ def self.split(value, separator_pattern)
10
+ keys = value.split(separator_pattern)
11
+ keys.shift if keys[0] == ''
12
+ keys << NULL
13
+ keys
14
+ end
15
+
16
+ def call(cache, obj)
17
+ (cache[method] ||= self.class.split(obj.send(method), separators))[index]
18
+ end
19
+
20
+ def call_source(cache, obj)
21
+ "(#{cache}[:#{method}] ||= Analysis::Splitting::Key.split(#{obj}.#{method}, #{separators.inspect}))[#{index}]"
22
+ end
23
+
24
+ def inspect
25
+ "#{method}[#{index}].split(#{separators.inspect})"
26
+ end
27
+ end
28
+
29
+ def clear
30
+ @boundaries = {}
31
+ super
32
+ end
33
+
34
+ def <<(key)
35
+ super
36
+ key.each_pair do |k, v|
37
+ analyze_capture_boundaries(v, @boundaries[k] ||= Histogram.new)
38
+ end
39
+ end
40
+
41
+ def separators(key)
42
+ @separators ||= {}
43
+ @separators[key] ||= lookup_separators(key)
44
+ end
45
+ attr_writer :separators
46
+
47
+ def lookup_separators(key)
48
+ @boundaries[key].keys_in_upper_quartile
49
+ end
50
+
51
+ def process_key(requirements, method, requirement)
52
+ separators = separators(method)
53
+ if requirement.is_a?(Regexp) && separators.any?
54
+ generate_split_keys(requirement, separators).each_with_index do |value, index|
55
+ requirements[Key.new(method, index, Regexp.union(*separators))] = value
56
+ end
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ private
63
+ def analyze_capture_boundaries(regexp, boundaries) #:nodoc:
64
+ return boundaries unless regexp.is_a?(Regexp)
65
+
66
+ parts = Utils.parse_regexp(regexp)
67
+ parts.each_with_index do |part, index|
68
+ if part.is_a?(Regin::Group)
69
+ if index > 0
70
+ previous = parts[index-1]
71
+ if previous.is_a?(Regin::Character) && previous.literal?
72
+ boundaries << previous.to_s
73
+ end
74
+ end
75
+
76
+ if inside = part.expression[0]
77
+ if inside.is_a?(Regin::Character) && inside.literal?
78
+ boundaries << inside.to_s
79
+ end
80
+ end
81
+
82
+ if index < parts.length
83
+ following = parts[index+1]
84
+ if following.is_a?(Regin::Character) && following.literal?
85
+ boundaries << following.to_s
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ boundaries
92
+ end
93
+
94
+ def generate_split_keys(regexp, separators) #:nodoc:
95
+ segments = []
96
+ buf = nil
97
+ parts = Utils.parse_regexp(regexp)
98
+ parts.each_with_index do |part, index|
99
+ case part
100
+ when Regin::Anchor
101
+ if part.value == '$' || part.value == '\Z'
102
+ segments << join_buffer(buf, regexp) if buf
103
+ segments << NULL
104
+ buf = nil
105
+ break
106
+ end
107
+ when Regin::CharacterClass
108
+ break if separators.any? { |s| part.include?(s) }
109
+ buf = nil
110
+ segments << part.to_regexp(true)
111
+ when Regin::Character
112
+ if separators.any? { |s| part.include?(s) }
113
+ segments << join_buffer(buf, regexp) if buf
114
+ peek = parts[index+1]
115
+ if peek.is_a?(Regin::Character) && separators.include?(peek.value)
116
+ segments << ''
117
+ end
118
+ buf = nil
119
+ else
120
+ buf ||= Regin::Expression.new([])
121
+ buf += [part]
122
+ end
123
+ when Regin::Group
124
+ if part.quantifier == '?'
125
+ value = part.expression.first
126
+ if separators.any? { |s| value.include?(s) }
127
+ segments << join_buffer(buf, regexp) if buf
128
+ buf = nil
129
+ end
130
+ break
131
+ elsif part.quantifier == nil
132
+ break if separators.any? { |s| part.include?(s) }
133
+ buf = nil
134
+ segments << part.to_regexp(true)
135
+ else
136
+ break
137
+ end
138
+ else
139
+ break
140
+ end
141
+ end
142
+
143
+ while segments.length > 0 && (segments.last.nil? || segments.last == '')
144
+ segments.pop
145
+ end
146
+
147
+ segments
148
+ end
149
+
150
+ def join_buffer(parts, regexp)
151
+ if parts.literal?
152
+ parts.to_s
153
+ else
154
+ parts.to_regexp(true)
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,117 @@
1
+ module Rack::Mount
2
+ module CodeGeneration #:nodoc:
3
+ def _expired_recognize(env) #:nodoc:
4
+ raise 'route set not finalized'
5
+ end
6
+
7
+ def rehash
8
+ super
9
+ optimize_recognize!
10
+ end
11
+
12
+ private
13
+ def expire!
14
+ if @optimized_recognize_defined
15
+ remove_metaclass_method :recognize
16
+
17
+ class << self
18
+ alias_method :recognize, :_expired_recognize
19
+ end
20
+
21
+ @optimized_recognize_defined = false
22
+ end
23
+
24
+ super
25
+ end
26
+
27
+ def optimize_container_iterator(container)
28
+ Utils.debug "optimizing container - size #{container.size}"
29
+
30
+ body = []
31
+
32
+ container.each_with_index { |route, i|
33
+ body << "route = self[#{i}]"
34
+ body << 'matches = {}'
35
+ body << 'params = route.defaults.dup'
36
+
37
+ conditions = []
38
+ route.conditions.each do |method, condition|
39
+ b = []
40
+ if condition.is_a?(Regexp)
41
+ b << "if m = obj.#{method}.match(#{condition.inspect})"
42
+ b << "matches[:#{method}] = m"
43
+ if (named_captures = route.named_captures[method]) && named_captures.any?
44
+ b << 'captures = m.captures'
45
+ b << 'p = nil'
46
+ b << named_captures.map { |k, j| "params[#{k.inspect}] = p if p = captures[#{j}]" }.join('; ')
47
+ end
48
+ else
49
+ b << "if m = obj.#{method} == route.conditions[:#{method}]"
50
+ end
51
+ b << 'true'
52
+ b << 'end'
53
+ conditions << "(#{b.join('; ')})"
54
+ end
55
+
56
+ body << <<-RUBY
57
+ if #{conditions.join(' && ')}
58
+ yield route, matches, params
59
+ end
60
+ RUBY
61
+ }
62
+
63
+ container.instance_eval(<<-RUBY, __FILE__, __LINE__)
64
+ def optimized_each(obj)
65
+ #{body.join("\n")}
66
+ nil
67
+ end
68
+ RUBY
69
+ end
70
+
71
+ def optimize_recognize!
72
+ Utils.debug "optimizing recognize"
73
+
74
+ keys = @recognition_keys.map { |key|
75
+ if key.respond_to?(:call_source)
76
+ key.call_source(:cache, :obj)
77
+ else
78
+ "obj.#{key}"
79
+ end
80
+ }.join(', ')
81
+
82
+ @optimized_recognize_defined = true
83
+
84
+ remove_metaclass_method :recognize
85
+
86
+ instance_eval(<<-RUBY, __FILE__, __LINE__)
87
+ def recognize(obj)
88
+ cache = {}
89
+ container = @recognition_graph[#{keys}]
90
+ optimize_container_iterator(container) unless container.respond_to?(:optimized_each)
91
+
92
+ if block_given?
93
+ container.optimized_each(obj) do |route, matches, params|
94
+ yield route, matches, params
95
+ end
96
+ else
97
+ container.optimized_each(obj) do |route, matches, params|
98
+ return route, matches, params
99
+ end
100
+ end
101
+
102
+ nil
103
+ end
104
+ RUBY
105
+ end
106
+
107
+ # method_defined? can't distinguish between instance
108
+ # and meta methods. So we have to rescue if the method
109
+ # has not been defined in the metaclass yet.
110
+ def remove_metaclass_method(symbol)
111
+ metaclass = class << self; self; end
112
+ Utils.silence_debug { metaclass.send(:remove_method, symbol) }
113
+ rescue NameError => e
114
+ nil
115
+ end
116
+ end
117
+ end