rack-mount 0.0.1 → 0.6.13

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.
Files changed (45) hide show
  1. data/README.rdoc +12 -4
  2. data/lib/rack/mount/analysis/frequency.rb +15 -6
  3. data/lib/rack/mount/analysis/histogram.rb +55 -6
  4. data/lib/rack/mount/analysis/splitting.rb +85 -71
  5. data/lib/rack/mount/code_generation.rb +117 -0
  6. data/lib/rack/mount/generatable_regexp.rb +95 -48
  7. data/lib/rack/mount/multimap.rb +2 -43
  8. data/lib/rack/mount/prefix.rb +12 -7
  9. data/lib/rack/mount/regexp_with_named_groups.rb +27 -7
  10. data/lib/rack/mount/route.rb +79 -18
  11. data/lib/rack/mount/route_set.rb +330 -22
  12. data/lib/rack/mount/strexp/parser.rb +0 -0
  13. data/lib/rack/mount/strexp/parser.y +34 -0
  14. data/lib/rack/mount/strexp/tokenizer.rb +83 -0
  15. data/lib/rack/mount/strexp/tokenizer.rex +12 -0
  16. data/lib/rack/mount/strexp.rb +54 -79
  17. data/lib/rack/mount/utils.rb +65 -174
  18. data/lib/rack/mount/vendor/multimap/multimap.rb +142 -39
  19. data/lib/rack/mount/vendor/multimap/multiset.rb +33 -1
  20. data/lib/rack/mount/vendor/multimap/nested_multimap.rb +18 -16
  21. data/lib/rack/mount/vendor/regin/regin/alternation.rb +40 -0
  22. data/lib/rack/mount/vendor/regin/regin/anchor.rb +4 -0
  23. data/lib/rack/mount/vendor/regin/regin/atom.rb +54 -0
  24. data/lib/rack/mount/vendor/regin/regin/character.rb +51 -0
  25. data/lib/rack/mount/vendor/regin/regin/character_class.rb +50 -0
  26. data/lib/rack/mount/vendor/regin/regin/collection.rb +77 -0
  27. data/lib/rack/mount/vendor/regin/regin/expression.rb +126 -0
  28. data/lib/rack/mount/vendor/regin/regin/group.rb +85 -0
  29. data/lib/rack/mount/vendor/regin/regin/options.rb +55 -0
  30. data/lib/rack/mount/vendor/regin/regin/parser.rb +521 -0
  31. data/lib/rack/mount/vendor/regin/regin/tokenizer.rb +0 -0
  32. data/lib/rack/mount/vendor/regin/regin/version.rb +3 -0
  33. data/lib/rack/mount/vendor/regin/regin.rb +75 -0
  34. data/lib/rack/mount/version.rb +3 -0
  35. data/lib/rack/mount.rb +13 -16
  36. metadata +73 -29
  37. data/lib/rack/mount/const.rb +0 -45
  38. data/lib/rack/mount/exceptions.rb +0 -3
  39. data/lib/rack/mount/generation/route.rb +0 -57
  40. data/lib/rack/mount/generation/route_set.rb +0 -163
  41. data/lib/rack/mount/meta_method.rb +0 -104
  42. data/lib/rack/mount/mixover.rb +0 -47
  43. data/lib/rack/mount/recognition/code_generation.rb +0 -99
  44. data/lib/rack/mount/recognition/route.rb +0 -59
  45. data/lib/rack/mount/recognition/route_set.rb +0 -88
data/README.rdoc CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A stackable dynamic tree based Rack router.
4
4
 
5
- Rack::Mount supports Rack's Cascade style of trying several routes until it finds one that is not a 404. 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.
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
6
 
7
7
  === Usage
8
8
 
@@ -12,17 +12,25 @@ The API is extremely minimal and only 3 methods are exposed as the public API.
12
12
 
13
13
  <tt>Rack::Mount::RouteSet#add_route</tt>:: builder method for adding routes to the set
14
14
  <tt>Rack::Mount::RouteSet#call</tt>:: Rack compatible recognition and dispatching method
15
- <tt>Rack::Mount::RouteSet#url</tt>:: generates path from identifiers or significant keys
15
+ <tt>Rack::Mount::RouteSet#generate</tt>:: generates a route condition from identifiers or significant keys
16
16
 
17
17
  === Example
18
18
 
19
19
  require 'rack/mount'
20
+
20
21
  Routes = Rack::Mount::RouteSet.new do |set|
21
22
  # add_route takes a rack application and conditions to match with
22
- # conditions may be strings or regexps
23
+ #
24
+ # valid conditions methods are any method on Rack::Request
25
+ # the values to match against may be strings or regexps
26
+ #
23
27
  # See Rack::Mount::RouteSet#add_route for more options.
24
- set.add_route FooApp, :method => 'get' :path => %{/foo}
28
+ set.add_route FooApp, { :request_method => 'GET', :path_info => %r{^/foo$} }, {}, :foo
25
29
  end
26
30
 
27
31
  # The route set itself is a simple rack app you mount
28
32
  run Routes
33
+
34
+
35
+ # generate path for route named "foo"
36
+ Routes.generate(:path_info, :foo) #=> "/foo"
@@ -1,8 +1,8 @@
1
+ require 'rack/mount/utils'
2
+
1
3
  module Rack::Mount
2
4
  module Analysis
3
5
  class Frequency #:nodoc:
4
- extend Mixover
5
-
6
6
  def initialize(*keys)
7
7
  clear
8
8
  keys.each { |key| self << key }
@@ -33,19 +33,28 @@ module Rack::Mount
33
33
 
34
34
  def process_key(requirements, method, requirement)
35
35
  if requirement.is_a?(Regexp)
36
- requirements[method] = Utils.extract_static_regexp(requirement)
37
- else
38
- requirements[method] = requirement
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
39
42
  end
43
+
44
+ requirements[method] = requirement
40
45
  end
41
46
 
42
47
  def report
43
48
  @report ||= begin
44
49
  possible_keys.each { |keys| keys.each_pair { |key, _| @key_frequency << key } }
45
50
  return [] if @key_frequency.count <= 1
46
- @key_frequency.select_upper
51
+ @key_frequency.keys_in_upper_quartile
47
52
  end
48
53
  end
54
+
55
+ def expire!
56
+ @possible_keys = @report = nil
57
+ end
49
58
  end
50
59
  end
51
60
  end
@@ -6,20 +6,69 @@ module Rack::Mount
6
6
  def initialize
7
7
  @count = 0
8
8
  super(0)
9
+ expire_caches!
9
10
  end
10
11
 
11
12
  def <<(value)
12
13
  @count += 1
13
14
  self[value] += 1 if value
15
+ expire_caches!
16
+ self
14
17
  end
15
18
 
16
- def select_upper
17
- values = sort_by { |_, value| value }
18
- values.reverse!
19
- values = values.select { |_, value| value >= count / size }
20
- values.map! { |key, _| key }
21
- values
19
+ def sorted_by_frequency
20
+ sort_by { |_, value| value }.reverse!
22
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
23
72
  end
24
73
  end
25
74
  end
@@ -1,24 +1,28 @@
1
+ require 'rack/mount/utils'
2
+
1
3
  module Rack::Mount
2
4
  module Analysis
3
- module Splitting
4
- class Key < Array
5
- def initialize(method, index, separators)
6
- replace([method, index, separators])
7
- end
5
+ class Splitting < Frequency
6
+ NULL = "\0".freeze
8
7
 
8
+ class Key < Struct.new(:method, :index, :separators)
9
9
  def self.split(value, separator_pattern)
10
10
  keys = value.split(separator_pattern)
11
- keys.shift if keys[0] == Const::EMPTY_STRING
12
- keys << Const::NULL
11
+ keys.shift if keys[0] == ''
12
+ keys << NULL
13
13
  keys
14
14
  end
15
15
 
16
16
  def call(cache, obj)
17
- (cache[self[0]] ||= self.class.split(obj.send(self[0]), self[2]))[self[1]]
17
+ (cache[method] ||= self.class.split(obj.send(method), separators))[index]
18
18
  end
19
19
 
20
20
  def call_source(cache, obj)
21
- "(#{cache}[:#{self[0]}] ||= Analysis::Splitting::Key.split(#{obj}.#{self[0]}, #{self[2].inspect}))[#{self[1]}]"
21
+ "(#{cache}[:#{method}] ||= Analysis::Splitting::Key.split(#{obj}.#{method}, #{separators.inspect}))[#{index}]"
22
+ end
23
+
24
+ def inspect
25
+ "#{method}[#{index}].split(#{separators.inspect})"
22
26
  end
23
27
  end
24
28
 
@@ -35,7 +39,13 @@ module Rack::Mount
35
39
  end
36
40
 
37
41
  def separators(key)
38
- @boundaries[key].select_upper
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
39
49
  end
40
50
 
41
51
  def process_key(requirements, method, requirement)
@@ -53,69 +63,81 @@ module Rack::Mount
53
63
  def analyze_capture_boundaries(regexp, boundaries) #:nodoc:
54
64
  return boundaries unless regexp.is_a?(Regexp)
55
65
 
56
- parts = Utils.extract_regexp_parts(regexp) rescue []
66
+ parts = Utils.parse_regexp(regexp)
57
67
  parts.each_with_index do |part, index|
58
- break if part == Const::NULL
59
-
60
- if index > 0
61
- previous = parts[index-1]
62
- if previous.is_a?(Utils::Capture)
63
- previous = Utils.extract_static_regexp(previous.last_part) rescue nil
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
64
74
  end
65
- boundaries << previous[-1..-1] if previous.is_a?(String)
66
- end
67
75
 
68
- if index < parts.length
69
- following = parts[index+1]
70
- if following.is_a?(Utils::Capture)
71
- following = Utils.extract_static_regexp(following.first_part) rescue nil
76
+ if inside = part.expression[0]
77
+ if inside.is_a?(Regin::Character) && inside.literal?
78
+ boundaries << inside.to_s
79
+ end
72
80
  end
73
- if following.is_a?(String) && following != Const::NULL
74
- boundaries << following[0..0] == '\\' ? following[1..1] : following[0..0]
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
75
87
  end
76
88
  end
77
89
  end
90
+
78
91
  boundaries
79
92
  end
80
93
 
81
94
  def generate_split_keys(regexp, separators) #:nodoc:
82
- escaped_separators = separators.map { |s| Regexp.escape(s) }
83
- separators_regexp = Regexp.union(*escaped_separators)
84
- segments, previous = [], nil
85
- regexp_options = regexp.options
86
-
87
- begin
88
- Utils.extract_regexp_parts(regexp).each do |part|
89
- if part.respond_to?(:optional?) && part.optional?
90
- if escaped_separators.include?(part.first)
91
- append_to_segments!(segments, previous, separators, regexp_options)
92
- end
93
-
94
- raise ArgumentError
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
95
106
  end
96
-
97
- append_to_segments!(segments, previous, separators, regexp_options)
98
- previous = nil
99
-
100
- if part == Const::NULL
101
- segments << Const::NULL
102
- raise ArgumentError
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]
103
122
  end
104
-
105
- if part.is_a?(Utils::Capture)
106
- source = part.map { |p| p.to_s }.join
107
- append_to_segments!(segments, source, separators, regexp_options)
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)
108
135
  else
109
- parts = part.split(separators_regexp)
110
- parts.shift if parts[0] == Const::EMPTY_STRING
111
- previous = parts.pop
112
- parts.each { |p| append_to_segments!(segments, p, separators, regexp_options) }
136
+ break
113
137
  end
138
+ else
139
+ break
114
140
  end
115
-
116
- append_to_segments!(segments, previous, separators, regexp_options)
117
- rescue ArgumentError
118
- # generation failed somewhere, but lets take what we can get
119
141
  end
120
142
 
121
143
  while segments.length > 0 && (segments.last.nil? || segments.last == '')
@@ -125,21 +147,13 @@ module Rack::Mount
125
147
  segments
126
148
  end
127
149
 
128
- def append_to_segments!(segments, s, separators, regexp_options) #:nodoc:
129
- return unless s
130
- separators.each do |separator|
131
- if s.gsub(/\[[^\]]+\]/, '').include?(separator)
132
- raise ArgumentError
133
- end
134
-
135
- if Regexp.compile("\\A#{s}\\Z") =~ separator
136
- raise ArgumentError
137
- end
150
+ def join_buffer(parts, regexp)
151
+ if parts.literal?
152
+ parts.to_s
153
+ else
154
+ parts.to_regexp(true)
138
155
  end
139
-
140
- static = Utils.extract_static_regexp(s, regexp_options)
141
- segments << (static.is_a?(String) ? static : static)
142
156
  end
143
- end
157
+ end
144
158
  end
145
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