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,163 @@
1
+ require 'rack/mount/utils'
2
+ require 'forwardable'
3
+
4
+ module Rack::Mount
5
+ module Generation
6
+ module RouteSet
7
+ # Adds generation related concerns to RouteSet.new.
8
+ def initialize(*args)
9
+ @named_routes = {}
10
+ @generation_key_analyzer = Analysis::Frequency.new
11
+
12
+ super
13
+ end
14
+
15
+ # Adds generation aspects to RouteSet#add_route.
16
+ def add_route(*args)
17
+ route = super
18
+ @named_routes[route.name] = route if route.name
19
+ @generation_key_analyzer << route.generation_keys
20
+ route
21
+ end
22
+
23
+ # Generates path from identifiers or significant keys.
24
+ #
25
+ # To generate a url by named route, pass the name in as a +Symbol+.
26
+ # url(:dashboard) # => "/dashboard"
27
+ #
28
+ # Additional parameters can be passed in as a hash
29
+ # url(:people, :id => "1") # => "/people/1"
30
+ #
31
+ # If no name route is given, it will fall back to a slower
32
+ # generation search.
33
+ # url(:controller => "people", :action => "show", :id => "1")
34
+ # # => "/people/1"
35
+ def url(*args)
36
+ named_route, params, recall = extract_params!(*args)
37
+
38
+ params = URISegment.wrap_values(params)
39
+ recall = URISegment.wrap_values(recall)
40
+
41
+ unless result = generate(:path_info, named_route, params, recall)
42
+ return
43
+ end
44
+
45
+ uri, params = result
46
+ params.each do |k, v|
47
+ if v._value
48
+ params[k] = v._value
49
+ else
50
+ params.delete(k)
51
+ end
52
+ end
53
+
54
+ uri << "?#{Utils.build_nested_query(params)}" if uri && params.any?
55
+ uri
56
+ end
57
+
58
+ def generate(method, *args) #:nodoc:
59
+ raise 'route set not finalized' unless @generation_graph
60
+
61
+ named_route, params, recall = extract_params!(*args)
62
+ merged = recall.merge(params)
63
+ route = nil
64
+
65
+ if named_route
66
+ if route = @named_routes[named_route.to_sym]
67
+ recall = route.defaults.merge(recall)
68
+ url = route.generate(method, params, recall)
69
+ [url, params]
70
+ else
71
+ raise RoutingError, "#{named_route} failed to generate from #{params.inspect}"
72
+ end
73
+ else
74
+ keys = @generation_keys.map { |key|
75
+ if k = merged[key]
76
+ k.to_s
77
+ else
78
+ nil
79
+ end
80
+ }
81
+ @generation_graph[*keys].each do |r|
82
+ if url = r.generate(method, params, recall)
83
+ return [url, params]
84
+ end
85
+ end
86
+
87
+ raise RoutingError, "No route matches #{params.inspect}"
88
+ end
89
+ end
90
+
91
+ def rehash #:nodoc:
92
+ @generation_keys = build_generation_keys
93
+ @generation_graph = build_generation_graph
94
+
95
+ super
96
+ end
97
+
98
+ private
99
+ def expire!
100
+ @generation_keys = @generation_graph = nil
101
+ super
102
+ end
103
+
104
+ def build_generation_graph
105
+ build_nested_route_set(@generation_keys) { |k, i|
106
+ if k = @generation_key_analyzer.possible_keys[i][k]
107
+ k.to_s
108
+ else
109
+ nil
110
+ end
111
+ }
112
+ end
113
+
114
+ def build_generation_keys
115
+ @generation_key_analyzer.report
116
+ end
117
+
118
+ def extract_params!(*args)
119
+ case args.length
120
+ when 3
121
+ named_route, params, recall = args
122
+ when 2
123
+ if args[0].is_a?(Hash) && args[1].is_a?(Hash)
124
+ params, recall = args
125
+ else
126
+ named_route, params = args
127
+ end
128
+ when 1
129
+ if args[0].is_a?(Hash)
130
+ params = args[0]
131
+ else
132
+ named_route = args[0]
133
+ end
134
+ else
135
+ raise ArgumentError
136
+ end
137
+
138
+ named_route ||= nil
139
+ params ||= {}
140
+ recall ||= {}
141
+
142
+ [named_route, params.dup, recall.dup]
143
+ end
144
+
145
+ class URISegment < Struct.new(:_value)
146
+ def self.wrap_values(hash)
147
+ hash.inject({}) { |h, (k, v)| h[k] = new(v); h }
148
+ end
149
+
150
+ extend Forwardable
151
+ def_delegators :_value, :==, :eql?, :hash
152
+
153
+ def to_param
154
+ @to_param ||= begin
155
+ v = _value.respond_to?(:to_param) ? _value.to_param : _value
156
+ Utils.escape_uri(v)
157
+ end
158
+ end
159
+ alias_method :to_s, :to_param
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,104 @@
1
+ module Rack::Mount
2
+ class MetaMethod #:nodoc:
3
+ class Block < Array #:nodoc:
4
+ def initialize(*parts)
5
+ replace(parts)
6
+ yield(self) if block_given?
7
+ end
8
+
9
+ def multiline?
10
+ length > 1
11
+ end
12
+
13
+ def inspect(indented = 2)
14
+ return Const::EMPTY_STRING if empty?
15
+ space = ' ' * indented
16
+ space + map { |p|
17
+ if p.is_a?(Condition)
18
+ p.inspect(indented)
19
+ else
20
+ p
21
+ end
22
+ }.join("\n#{space}")
23
+ end
24
+
25
+ def to_str
26
+ map { |p| p.to_str }.join('; ')
27
+ end
28
+ end
29
+
30
+ class Condition #:nodoc:
31
+ attr_accessor :body, :else
32
+
33
+ def initialize(*conditions)
34
+ @conditions = conditions.map { |c| c.is_a?(Block) ? c : Block.new(c) }
35
+ @body = Block.new
36
+ @else = Block.new
37
+ yield(@body) if block_given?
38
+ end
39
+
40
+ def <<(condition)
41
+ @conditions << condition
42
+ end
43
+
44
+ def inspect(indented = 2)
45
+ return @body.inspect(indented) if @conditions.empty?
46
+ space = ' ' * indented
47
+ str = 'if '
48
+ str << @conditions.map { |b|
49
+ b.multiline? ?
50
+ "begin\n#{b.inspect(indented + 4)}\n#{space} end" :
51
+ b.inspect(0)
52
+ }.join(' && ')
53
+ str << "\n#{@body.inspect(indented + 2)}" if @body.any?
54
+ if @else.any?
55
+ str << "\n#{space}else\n#{@else.inspect(indented + 2)}"
56
+ end
57
+ str << "\n#{space}end"
58
+ str
59
+ end
60
+
61
+ def to_str
62
+ return @body.to_str if @conditions.empty?
63
+ str = 'if '
64
+ str << @conditions.map { |b|
65
+ b.multiline? ? "(#{b.to_str})" : b.to_str
66
+ }.join(' && ')
67
+ str << "; #{@body.to_str}" if @body.any?
68
+ if @else.any?
69
+ str << "; else; #{@else.to_str}"
70
+ end
71
+ str << "; end"
72
+ str
73
+ end
74
+ end
75
+
76
+ def initialize(sym, *args)
77
+ @sym = sym
78
+ @args = args
79
+ @body = Block.new
80
+ end
81
+
82
+ def <<(line)
83
+ @body << line
84
+ end
85
+
86
+ def inspect
87
+ str = ""
88
+ str << "def #{@sym}"
89
+ str << "(#{@args.join(', ')})" if @args.any?
90
+ str << "\n#{@body.inspect}" if @body.any?
91
+ str << "\nend\n"
92
+ str
93
+ end
94
+
95
+ def to_str
96
+ str = []
97
+ str << "def #{@sym}"
98
+ str << "(#{@args.join(', ')})" if @args.any?
99
+ str << "\n#{@body.to_str}" if @body.any?
100
+ str << "\nend"
101
+ str.join
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,47 @@
1
+ module Rack::Mount
2
+ # A mixin that changes the behavior of +include+. Instead of modules
3
+ # being chained as a superclass, they are mixed into the objects
4
+ # metaclass. This allows mixins to be stacked ontop of the instance
5
+ # methods.
6
+ module Mixover
7
+ module InstanceMethods #:nodoc:
8
+ def dup
9
+ obj = super
10
+ included_modules = (class << self; included_modules; end) - (class << obj; included_modules; end)
11
+ included_modules.reverse.each { |mod| obj.extend(mod) }
12
+ obj
13
+ end
14
+ end
15
+
16
+ # Replaces include with a lazy version.
17
+ def include(*mod)
18
+ (@included_modules ||= []).push(*mod)
19
+ end
20
+
21
+ def new(*args, &block) #:nodoc:
22
+ obj = allocate
23
+ obj.extend(InstanceMethods)
24
+ (@included_modules ||= []).each { |mod| obj.extend(mod) }
25
+ obj.send(:initialize, *args, &block)
26
+ obj
27
+ end
28
+
29
+ # Create a new class without an included module.
30
+ def new_without_module(mod, *args, &block)
31
+ old_included_modules = (@included_modules ||= []).dup
32
+ @included_modules.delete(mod)
33
+ new(*args, &block)
34
+ ensure
35
+ @included_modules = old_included_modules
36
+ end
37
+
38
+ # Create a new class temporarily with a module.
39
+ def new_with_module(mod, *args, &block)
40
+ old_included_modules = (@included_modules ||= []).dup
41
+ include(mod)
42
+ new(*args, &block)
43
+ ensure
44
+ @included_modules = old_included_modules
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,94 @@
1
+ begin
2
+ require 'nested_multimap'
3
+ rescue LoadError
4
+ $: << File.expand_path(File.join(File.dirname(__FILE__), 'vendor/multimap'))
5
+ require 'nested_multimap'
6
+ end
7
+
8
+ module Rack::Mount
9
+ class Multimap < NestedMultimap #:nodoc:
10
+ def self.[](*args)
11
+ map = super
12
+ map.instance_variable_set('@fuzz', {})
13
+ map
14
+ end
15
+
16
+ def initialize(default = [])
17
+ @fuzz = {}
18
+ super
19
+ end
20
+
21
+ def initialize_copy(original)
22
+ @fuzz = original.instance_variable_get('@fuzz').dup
23
+ super
24
+ end
25
+
26
+ def store(*args)
27
+ keys = args.dup
28
+ value = keys.pop
29
+ key = keys.shift
30
+
31
+ raise ArgumentError, 'wrong number of arguments (1 for 2)' unless value
32
+
33
+ unless key.respond_to?(:=~)
34
+ raise ArgumentError, "unsupported key: #{args.first.inspect}"
35
+ end
36
+
37
+ if key.is_a?(Regexp)
38
+ @fuzz[value] = key
39
+ if keys.empty?
40
+ hash_each_pair { |k, l| l << value if k =~ key }
41
+ self.default << value
42
+ else
43
+ hash_each_pair { |k, _|
44
+ if k =~ key
45
+ args[0] = k
46
+ super(*args)
47
+ end
48
+ }
49
+
50
+ self.default = self.class.new(default) unless default.is_a?(self.class)
51
+ default[*keys.dup] = value
52
+ end
53
+ else
54
+ super(*args)
55
+ end
56
+ end
57
+ alias_method :[]=, :store
58
+
59
+ def freeze
60
+ @fuzz.clear
61
+ @fuzz = nil
62
+ super
63
+ end
64
+
65
+ undef :index, :invert
66
+
67
+ def height
68
+ containers_with_default.max { |a, b| a.length <=> b.length }.length
69
+ end
70
+
71
+ def average_height
72
+ lengths = containers_with_default.map { |e| e.length }
73
+ lengths.inject(0) { |sum, len| sum += len }.to_f / lengths.size
74
+ end
75
+
76
+ protected
77
+ def update_container(key) #:nodoc:
78
+ super do |container|
79
+ if container.is_a?(self.class)
80
+ container.each_container_with_default do |c|
81
+ c.delete_if do |value|
82
+ (requirement = @fuzz[value]) && key !~ requirement
83
+ end
84
+ end
85
+ else
86
+ container.delete_if do |value|
87
+ (requirement = @fuzz[value]) && key !~ requirement
88
+ end
89
+ end
90
+ yield container
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,31 @@
1
+ require 'rack/mount/utils'
2
+
3
+ module Rack::Mount
4
+ class Prefix #:nodoc:
5
+ KEY = 'rack.mount.prefix'.freeze
6
+
7
+ def initialize(app, prefix = nil)
8
+ @app, @prefix = app, prefix.freeze
9
+ freeze
10
+ end
11
+
12
+ def call(env)
13
+ if prefix = env[KEY] || @prefix
14
+ old_path_info = env[Const::PATH_INFO].dup
15
+ old_script_name = env[Const::SCRIPT_NAME].dup
16
+
17
+ begin
18
+ env[Const::PATH_INFO] = Utils.normalize_path(env[Const::PATH_INFO].sub(prefix, Const::EMPTY_STRING))
19
+ env[Const::PATH_INFO] = Const::EMPTY_STRING if env[Const::PATH_INFO] == Const::SLASH
20
+ env[Const::SCRIPT_NAME] = Utils.normalize_path(env[Const::SCRIPT_NAME].to_s + prefix)
21
+ @app.call(env)
22
+ ensure
23
+ env[Const::PATH_INFO] = old_path_info
24
+ env[Const::SCRIPT_NAME] = old_script_name
25
+ end
26
+ else
27
+ @app.call(env)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,99 @@
1
+ require 'rack/mount/meta_method'
2
+
3
+ module Rack::Mount
4
+ module Recognition
5
+ module CodeGeneration #:nodoc:
6
+ def _expired_call(env) #:nodoc:
7
+ raise 'route set not finalized'
8
+ end
9
+
10
+ def rehash
11
+ super
12
+ optimize_call!
13
+ end
14
+
15
+ private
16
+ def expire!
17
+ class << self
18
+ alias_method :call, :_expired_call
19
+ end
20
+
21
+ super
22
+ end
23
+
24
+ def optimize_container_iterator(container)
25
+ m = MetaMethod.new(:optimized_each, :req)
26
+ m << 'env = req.env'
27
+
28
+ container.each_with_index { |route, i|
29
+ path_info_unanchored = route.conditions[:path_info] &&
30
+ !Utils.regexp_anchored?(route.conditions[:path_info])
31
+ m << "route = self[#{i}]"
32
+ m << 'routing_args = route.defaults.dup'
33
+
34
+ m << matchers = MetaMethod::Condition.new do |body|
35
+ body << "env[#{@parameters_key.inspect}] = routing_args"
36
+ body << "response = route.app.call(env)"
37
+ body << "return response unless response[0].to_i == 417"
38
+ end
39
+
40
+ route.conditions.each do |method, condition|
41
+ matchers << MetaMethod::Block.new do |matcher|
42
+ matcher << c = MetaMethod::Condition.new("m = req.#{method}.match(#{condition.inspect})") do |b|
43
+ b << "matches = m.captures" if route.named_captures[method].any?
44
+ route.named_captures[method].each do |k, i|
45
+ b << MetaMethod::Condition.new("p = matches[#{i}]") do |c2|
46
+ c2 << "routing_args[#{k.inspect}] = Utils.unescape_uri(p)"
47
+ end
48
+ end
49
+ if method == :path_info && !Utils.regexp_anchored?(condition)
50
+ b << "env[Prefix::KEY] = m.to_s"
51
+ end
52
+ b << "true"
53
+ end
54
+ c.else = MetaMethod::Block.new("false")
55
+ end
56
+ end
57
+ }
58
+
59
+ m << 'nil'
60
+ # puts "\n#{m.inspect}"
61
+ container.instance_eval(m, __FILE__, __LINE__)
62
+ end
63
+
64
+ def optimize_call!
65
+ method = MetaMethod.new(:call, :env)
66
+
67
+ if @routes.empty?
68
+ method << 'env[Const::EXPECT] != Const::CONTINUE ? Const::NOT_FOUND_RESPONSE : Const::EXPECTATION_FAILED_RESPONSE'
69
+ else
70
+ method << 'begin'
71
+ method << 'set_expectation = env[Const::EXPECT] != Const::CONTINUE'
72
+ method << 'env[Const::EXPECT] = Const::CONTINUE if set_expectation'
73
+
74
+ method << 'env[Const::PATH_INFO] = Utils.normalize_path(env[Const::PATH_INFO])'
75
+ method << "req = #{@request_class.name}.new(env)"
76
+ cache = false
77
+ keys = @recognition_keys.map { |key|
78
+ if key.is_a?(Array)
79
+ cache = true
80
+ key.call_source(:cache, :req)
81
+ else
82
+ "req.#{key}"
83
+ end
84
+ }.join(', ')
85
+ method << 'cache = {}' if cache
86
+ method << "container = @recognition_graph[#{keys}]"
87
+ method << "optimize_container_iterator(container) unless container.respond_to?(:optimized_each)"
88
+ method << "container.optimized_each(req) || (set_expectation ? Const::NOT_FOUND_RESPONSE : Const::EXPECTATION_FAILED_RESPONSE)"
89
+ method << 'ensure'
90
+ method << 'env.delete(Const::EXPECT) if set_expectation'
91
+ method << 'end'
92
+ end
93
+
94
+ # puts "\n#{method.inspect}"
95
+ instance_eval(method, __FILE__, __LINE__)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,59 @@
1
+ require 'rack/mount/prefix'
2
+
3
+ module Rack::Mount
4
+ module Recognition
5
+ module Route #:nodoc:
6
+ attr_reader :named_captures
7
+
8
+ def initialize(*args)
9
+ super
10
+
11
+ # TODO: Don't explict check for :path_info condition
12
+ if @conditions.has_key?(:path_info) &&
13
+ !Utils.regexp_anchored?(@conditions[:path_info])
14
+ @app = Prefix.new(@app)
15
+ end
16
+
17
+ @named_captures = {}
18
+ @conditions.map { |method, condition|
19
+ @named_captures[method] = condition.named_captures.inject({}) { |named_captures, (k, v)|
20
+ named_captures[k.to_sym] = v.last - 1
21
+ named_captures
22
+ }.freeze
23
+ }
24
+ @named_captures.freeze
25
+ end
26
+
27
+ def call(req)
28
+ env = req.env
29
+
30
+ routing_args = @defaults.dup
31
+ if @conditions.all? { |method, condition|
32
+ value = req.send(method)
33
+ if m = value.match(condition)
34
+ matches = m.captures
35
+ @named_captures[method].each { |k, i|
36
+ if v = matches[i]
37
+ # TODO: We only want to unescape params from
38
+ # uri related methods
39
+ routing_args[k] = Utils.unescape_uri(v)
40
+ end
41
+ }
42
+ # TODO: Don't explict check for :path_info condition
43
+ if method == :path_info && !Utils.regexp_anchored?(condition)
44
+ env[Prefix::KEY] = m.to_s
45
+ end
46
+ true
47
+ else
48
+ false
49
+ end
50
+ }
51
+ env[@set.parameters_key] = routing_args
52
+ @app.call(env)
53
+ else
54
+ Const::EXPECTATION_FAILED_RESPONSE
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,88 @@
1
+ require 'rack/mount/utils'
2
+
3
+ module Rack::Mount
4
+ module Recognition
5
+ module RouteSet
6
+ attr_reader :parameters_key
7
+
8
+ # Adds recognition related concerns to RouteSet.new.
9
+ def initialize(options = {})
10
+ @parameters_key = options.delete(:parameters_key) || Const::RACK_ROUTING_ARGS
11
+ @parameters_key.freeze
12
+ @recognition_key_analyzer = Analysis::Frequency.new_with_module(Analysis::Splitting)
13
+
14
+ super
15
+ end
16
+
17
+ # Adds recognition aspects to RouteSet#add_route.
18
+ def add_route(*args)
19
+ route = super
20
+ @recognition_key_analyzer << route.conditions
21
+ route
22
+ end
23
+
24
+ # Rack compatible recognition and dispatching method. Routes are
25
+ # tried until one returns a non-catch status code. If no routes
26
+ # match, the catch status code is returned.
27
+ #
28
+ # This method can only be invoked after the RouteSet has been
29
+ # finalized.
30
+ def call(env)
31
+ raise 'route set not finalized' unless @recognition_graph
32
+
33
+ set_expectation = env[Const::EXPECT] != Const::CONTINUE
34
+ env[Const::EXPECT] = Const::CONTINUE if set_expectation
35
+
36
+ env[Const::PATH_INFO] = Utils.normalize_path(env[Const::PATH_INFO])
37
+
38
+ cache = {}
39
+ req = @request_class.new(env)
40
+ keys = @recognition_keys.map { |key|
41
+ if key.is_a?(Array)
42
+ key.call(cache, req)
43
+ else
44
+ req.send(key)
45
+ end
46
+ }
47
+ @recognition_graph[*keys].each do |route|
48
+ result = route.call(req)
49
+ return result unless result[0].to_i == 417
50
+ end
51
+ set_expectation ? Const::NOT_FOUND_RESPONSE : Const::EXPECTATION_FAILED_RESPONSE
52
+ ensure
53
+ env.delete(Const::EXPECT) if set_expectation
54
+ end
55
+
56
+ def rehash #:nodoc:
57
+ @recognition_keys = build_recognition_keys
58
+ @recognition_graph = build_recognition_graph
59
+
60
+ super
61
+ end
62
+
63
+ def valid_conditions #:nodoc:
64
+ @valid_conditions ||= begin
65
+ conditions = @request_class.instance_methods(false)
66
+ conditions.map! { |m| m.to_sym }
67
+ conditions.freeze
68
+ end
69
+ end
70
+
71
+ private
72
+ def expire!
73
+ @recognition_keys = @recognition_graph = nil
74
+ super
75
+ end
76
+
77
+ def build_recognition_graph
78
+ build_nested_route_set(@recognition_keys) { |k, i|
79
+ @recognition_key_analyzer.possible_keys[i][k]
80
+ }
81
+ end
82
+
83
+ def build_recognition_keys
84
+ @recognition_key_analyzer.report
85
+ end
86
+ end
87
+ end
88
+ end