josh-rack-mount 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-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.
data/README.rdoc ADDED
@@ -0,0 +1,28 @@
1
+ = Rack::Mount
2
+
3
+ A stackable dynamic tree based Rack router.
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 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#url_for</tt>:: generatess path from identifiers or significant keys
16
+
17
+ === Example
18
+
19
+ require 'rack/mount'
20
+ Routes = Rack::Mount::RouteSet.new do |set|
21
+ # add_route takes a rack application and conditions to match with
22
+ # conditions may be strings or regexps
23
+ # See Rack::Mount::RouteSet#add_route for more options.
24
+ set.add_route FooApp, :method => 'get' :path => %{/foo}
25
+ end
26
+
27
+ # The route set itself is a simple rack app you mount
28
+ run Routes
data/lib/rack/mount.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rack/utils'
2
+ require 'rack/mount/exceptions'
3
+
4
+ module Rack #:nodoc:
5
+ module Mount #:nodoc:
6
+ autoload :Const, 'rack/mount/const'
7
+ autoload :Generation, 'rack/mount/generation'
8
+ autoload :NestedSet, 'rack/mount/nested_set'
9
+ autoload :NestedSetExt, 'rack/mount/nested_set_ext'
10
+ autoload :PathPrefix, 'rack/mount/path_prefix'
11
+ autoload :Recognition, 'rack/mount/recognition'
12
+ autoload :RegexpWithNamedGroups, 'rack/mount/regexp_with_named_groups'
13
+ autoload :Request, 'rack/mount/request'
14
+ autoload :Route, 'rack/mount/route'
15
+ autoload :RouteSet, 'rack/mount/route_set'
16
+ autoload :Utils, 'rack/mount/utils'
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ module Rack
2
+ module Mount
3
+ module Const #:nodoc:
4
+ RACK_ROUTING_ARGS = 'rack.routing_args'.freeze
5
+ RACK_MOUNT_DEBUG = 'RACKMOUNT_DEBUG'.freeze
6
+
7
+ begin
8
+ eval('/(?<foo>.*)/').named_captures
9
+ SUPPORTS_NAMED_CAPTURES = true
10
+ REGEXP_NAMED_CAPTURE = '(?<%s>%s)'.freeze
11
+ rescue SyntaxError, NoMethodError
12
+ SUPPORTS_NAMED_CAPTURES = false
13
+ REGEXP_NAMED_CAPTURE = '(?:<%s>%s)'.freeze
14
+ end
15
+
16
+ EOS_KEY = '$'.freeze
17
+
18
+ CONTENT_TYPE = 'Content-Type'.freeze
19
+ DELETE = 'PUT'.freeze
20
+ EMPTY_STRING = ''.freeze
21
+ GET = 'GET'.freeze
22
+ HEAD = 'HEAD'.freeze
23
+ PATH_INFO = 'PATH_INFO'.freeze
24
+ POST = 'POST'.freeze
25
+ PUT = 'PUT'.freeze
26
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
27
+ SLASH = '/'.freeze
28
+ TEXT_SLASH_HTML = 'text/html'.freeze
29
+
30
+ DEFAULT_CONTENT_TYPE_HEADERS = {CONTENT_TYPE => TEXT_SLASH_HTML}.freeze
31
+ HTTP_METHODS = [GET, HEAD, POST, PUT, DELETE].freeze
32
+
33
+ OK = 'OK'.freeze
34
+ NOT_FOUND = 'Not Found'.freeze
35
+ OK_RESPONSE = [200, DEFAULT_CONTENT_TYPE_HEADERS, [OK].freeze].freeze
36
+ NOT_FOUND_RESPONSE = [404, DEFAULT_CONTENT_TYPE_HEADERS, [NOT_FOUND].freeze].freeze
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ module Mount
3
+ class RoutingError < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Rack
2
+ module Mount
3
+ module Generation #:nodoc:
4
+ autoload :Optimizations, 'rack/mount/generation/optimizations'
5
+ autoload :Route, 'rack/mount/generation/route'
6
+ autoload :RouteSet, 'rack/mount/generation/route_set'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,83 @@
1
+ module Rack
2
+ module Mount
3
+ module Generation
4
+ module Optimizations #:nodoc:
5
+ def freeze
6
+ optimize_call! unless frozen?
7
+ super
8
+ end
9
+
10
+ if ENV[Const::RACK_MOUNT_DEBUG]
11
+ def instance_eval(*args)
12
+ puts
13
+ puts "#{args[1]}##{args[2]}"
14
+ puts args[0]
15
+ puts
16
+
17
+ super
18
+ end
19
+ end
20
+
21
+ private
22
+ def optimize_call!
23
+ @recognition_graph.lists.each do |list|
24
+ body = (0...list.length).zip(list).map { |i, route|
25
+ assign_index_params = assign_index_params(route)
26
+ <<-EOS
27
+ if #{route.method ? "method == #{route.method.inspect} && " : ''}path =~ #{route.path.inspect}
28
+ route = self[#{i}]
29
+ #{if assign_index_params.any?
30
+ 'routing_args, param_matches = route.defaults.dup, $~.captures'
31
+ else
32
+ 'routing_args = route.defaults.dup'
33
+ end}
34
+ #{assign_index_params.join("\n ")}
35
+ env[Const::RACK_ROUTING_ARGS] = routing_args
36
+ result = route.app.call(env)
37
+ return result unless result[0] == #{@catch}
38
+ end
39
+ EOS
40
+ }.join
41
+
42
+ method = <<-EOS, __FILE__, __LINE__
43
+ def optimized_each(env)
44
+ method = env[Const::REQUEST_METHOD]
45
+ path = env[Const::PATH_INFO]
46
+ #{body}
47
+ nil
48
+ end
49
+ EOS
50
+
51
+ puts method if ENV[Const::RACK_MOUNT_DEBUG]
52
+ list.instance_eval(*method)
53
+ end
54
+
55
+ instance_eval(<<-EOS, __FILE__, __LINE__)
56
+ def call(env)
57
+ req = Request.new(env)
58
+ keys = [#{convert_keys_to_method_calls}]
59
+ @recognition_graph[*keys].optimized_each(env) || @throw
60
+ end
61
+ EOS
62
+ end
63
+
64
+ def convert_keys_to_method_calls
65
+ @recognition_keys.map { |key|
66
+ if key.is_a?(Array)
67
+ key = key.dup
68
+ "req.#{key.shift}(#{key.join(',')})"
69
+ else
70
+ "req.#{key}"
71
+ end
72
+ }.join(', ')
73
+ end
74
+
75
+ def assign_index_params(route)
76
+ route.instance_variable_get("@named_captures").map { |k, index|
77
+ "routing_args[#{k.inspect}] = param_matches[#{index}] if param_matches[#{index}]"
78
+ }
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,108 @@
1
+ module Rack
2
+ module Mount
3
+ module Generation
4
+ module Route #:nodoc:
5
+ class DynamicSegment #:nodoc:
6
+ attr_reader :name, :requirement
7
+
8
+ def initialize(name, requirement)
9
+ @name, @requirement = name.to_sym, requirement
10
+ end
11
+
12
+ def ==(obj)
13
+ @name == obj.name && @requirement == obj.requirement
14
+ end
15
+ end
16
+
17
+ def initialize(*args)
18
+ super
19
+
20
+ @segments = segments(@path).freeze
21
+ @required_params = @segments.find_all { |s|
22
+ s.is_a?(DynamicSegment)
23
+ }.map { |s| s.name }.freeze
24
+ end
25
+
26
+ def url_for(params = {})
27
+ params = (params || {}).dup
28
+
29
+ return nil if @segments.empty?
30
+ return nil unless @required_params.all? { |p| params.include?(p) }
31
+
32
+ path = generate_from_segments(@segments, params, @defaults)
33
+
34
+ @defaults.each do |key, value|
35
+ params.delete(key)
36
+ end
37
+
38
+ if params.any?
39
+ path << "?#{Rack::Utils.build_query(params)}"
40
+ end
41
+
42
+ path
43
+ end
44
+
45
+ private
46
+ # Segment data structure used for generations
47
+ # => ['/people', ['.', :format]]
48
+ def segments(regexp)
49
+ parse_segments(Utils.extract_regexp_parts(regexp))
50
+ rescue ArgumentError
51
+ []
52
+ end
53
+
54
+ def parse_segments(segments)
55
+ s = []
56
+ segments.each do |part|
57
+ if part.is_a?(Utils::Capture)
58
+ if part.named?
59
+ source = part.map { |p| p.is_a?(Array) ? "(#{p.join})?" : p }.join
60
+ requirement = Regexp.compile(source)
61
+ s << DynamicSegment.new(part.name, requirement)
62
+ else
63
+ s << parse_segments(part)
64
+ end
65
+ else
66
+ source = part.gsub('\\.', '.').gsub('\\/', '/')
67
+ if Regexp.compile("^(#{part})$") =~ source
68
+ s << source
69
+ else
70
+ raise ArgumentError, "failed to parse #{part.inspect}"
71
+ end
72
+ end
73
+ end
74
+ s
75
+ end
76
+
77
+ def generate_from_segments(segments, params, defaults, optional = false)
78
+ if optional
79
+ return Const::EMPTY_STRING if segments.all? { |s| s.is_a?(String) }
80
+ return Const::EMPTY_STRING if segments.flatten.all? { |s|
81
+ if s.is_a?(DynamicSegment) && params[s.name]
82
+ params[s.name].to_s !~ s.requirement
83
+ else
84
+ true
85
+ end
86
+ }
87
+ end
88
+
89
+ generated = segments.map do |segment|
90
+ case segment
91
+ when String
92
+ segment
93
+ when DynamicSegment
94
+ params[segment.name] || defaults[segment.name]
95
+ when Array
96
+ generate_from_segments(segment, params, defaults, true) || Const::EMPTY_STRING
97
+ end
98
+ end
99
+
100
+ # Delete any used items from the params
101
+ segments.each { |s| params.delete(s.name) if s.is_a?(DynamicSegment) }
102
+
103
+ generated.join
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,60 @@
1
+ module Rack
2
+ module Mount
3
+ module Generation
4
+ module RouteSet
5
+ DEFAULT_KEYS = [] # [:controller, :action].freeze
6
+
7
+ def initialize(options = {})
8
+ @named_routes = {}
9
+ @generation_keys = DEFAULT_KEYS
10
+ @generation_graph = NestedSet.new
11
+ super
12
+ end
13
+
14
+ def add_route(*args)
15
+ route = super
16
+
17
+ @named_routes[route.name] = route if route.name
18
+
19
+ keys = @generation_keys.map { |key| route.defaults[key] }
20
+ @generation_graph[*keys] = route
21
+
22
+ route
23
+ end
24
+
25
+ def url_for(*args)
26
+ params = args.last.is_a?(Hash) ? args.pop : {}
27
+ named_route = args.shift
28
+ route = nil
29
+
30
+ if named_route
31
+ unless route = @named_routes[named_route.to_sym]
32
+ raise RoutingError, "#{named_route} failed to generate from #{params.inspect}"
33
+ end
34
+ else
35
+ keys = @generation_keys.map { |key| params[key] }
36
+ @generation_graph[*keys].each do |r|
37
+ if r.defaults.all? { |k, v| params[k] == v }
38
+ route = r
39
+ break
40
+ end
41
+ end
42
+
43
+ unless route
44
+ raise RoutingError, "No route matches #{params.inspect}"
45
+ end
46
+ end
47
+
48
+ route.url_for(params)
49
+ end
50
+
51
+ def freeze
52
+ @named_routes.freeze
53
+ @generation_keys.freeze
54
+ @generation_graph.freeze
55
+ super
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,143 @@
1
+ require 'active_support/inflector'
2
+ require 'merb-core/dispatch/router'
3
+ require 'rack/request'
4
+
5
+ module Rack
6
+ module Mount
7
+ class RouteSet
8
+ def prepare(*args, &block)
9
+ Mappers::Merb.new(self).prepare(*args, &block)
10
+ freeze
11
+ end
12
+ end
13
+
14
+ module Mappers
15
+ class Merb
16
+ class ::Merb::Router::Behavior
17
+ def to_route
18
+ raise Error, 'The route has already been committed.' if @route
19
+
20
+ controller = @params[:controller]
21
+
22
+ if prefixes = @options[:controller_prefix]
23
+ controller ||= ':controller'
24
+
25
+ prefixes.reverse_each do |prefix|
26
+ break if controller =~ %r{^/(.*)} && controller = $1
27
+ controller = "#{prefix}/#{controller}"
28
+ end
29
+ end
30
+
31
+ @params.merge!(:controller => controller.to_s.gsub(%r{^/}, '')) if controller
32
+
33
+ identifiers = @identifiers.sort { |(first,_),(sec,_)| first <=> sec || 1 }
34
+
35
+ Thread.current[:merb_routes] << [
36
+ @conditions.dup,
37
+ @params,
38
+ @blocks,
39
+ { :defaults => @defaults.dup, :identifiers => identifiers }
40
+ ]
41
+
42
+ self
43
+ end
44
+ end
45
+
46
+ class DeferredProc
47
+ def initialize(app, deferred_procs)
48
+ @app, @proc = app, deferred_procs.cache
49
+ end
50
+
51
+ def call(env)
52
+ # TODO: Change this to a Merb request
53
+ request = Rack::Request.new(env)
54
+ params = env[Const::RACK_ROUTING_ARGS]
55
+ result = @proc.call(request, params)
56
+
57
+ if result
58
+ @app.call(env)
59
+ else
60
+ Const::NOT_FOUND_RESPONSE
61
+ end
62
+ end
63
+ end
64
+
65
+ class RequestConditions
66
+ def initialize(app, conditions)
67
+ @app, @conditions = app, conditions
68
+ end
69
+
70
+ def call(env)
71
+ # TODO: Change this to a Merb request
72
+ request = Rack::Request.new(env)
73
+
74
+ @conditions.each do |method, expected|
75
+ unless request.send(method) == expected
76
+ return Const::NOT_FOUND_RESPONSE
77
+ end
78
+ end
79
+
80
+ @app.call(env)
81
+ end
82
+ end
83
+
84
+ DynamicController = lambda { |env|
85
+ app = ActiveSupport::Inflector.camelize("#{env[Const::RACK_ROUTING_ARGS][:controller]}Controller")
86
+ app = ActiveSupport::Inflector.constantize(app)
87
+ app.call(env)
88
+ }
89
+
90
+ attr_accessor :root_behavior
91
+
92
+ def initialize(set)
93
+ @set = set
94
+ @root_behavior = ::Merb::Router::Behavior.new.defaults(:action => 'index')
95
+ end
96
+
97
+ def prepare(first = [], last = [], &block)
98
+ Thread.current[:merb_routes] = []
99
+ begin
100
+ root_behavior._with_proxy(&block)
101
+ routes = Thread.current[:merb_routes]
102
+ routes.each { |route| add_route(*route) }
103
+ self
104
+ ensure
105
+ Thread.current[:merb_routes] = nil
106
+ end
107
+ end
108
+
109
+ def add_route(conditions, params, deferred_procs, options = {})
110
+ new_conditions = {}
111
+ new_conditions[:path] = conditions.delete(:path)[0]
112
+ new_conditions[:method] = conditions.delete(:method)
113
+
114
+ requirements = {}
115
+ conditions.each do |k, v|
116
+ if v.is_a?(Regexp)
117
+ requirements[k.to_sym] = conditions.delete(k)
118
+ end
119
+ end
120
+
121
+ if new_conditions[:path].is_a?(String)
122
+ new_conditions[:path] = Utils.convert_segment_string_to_regexp(
123
+ new_conditions[:path], requirements, %w( / . ? ))
124
+ end
125
+
126
+ app = params.has_key?(:controller) ?
127
+ ActiveSupport::Inflector.constantize(ActiveSupport::Inflector.camelize("#{params[:controller]}Controller")) :
128
+ DynamicController
129
+
130
+ if deferred_procs.any?
131
+ app = DeferredProc.new(app, deferred_procs.first)
132
+ end
133
+
134
+ if conditions.any?
135
+ app = RequestConditions.new(app, conditions)
136
+ end
137
+
138
+ @set.add_route(app, new_conditions, params)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end