rack-mount 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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.
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 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</tt>:: generates 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
@@ -0,0 +1,51 @@
1
+ module Rack::Mount
2
+ module Analysis
3
+ class Frequency #:nodoc:
4
+ extend Mixover
5
+
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
+ requirements[method] = Utils.extract_static_regexp(requirement)
37
+ else
38
+ requirements[method] = requirement
39
+ end
40
+ end
41
+
42
+ def report
43
+ @report ||= begin
44
+ possible_keys.each { |keys| keys.each_pair { |key, _| @key_frequency << key } }
45
+ return [] if @key_frequency.count <= 1
46
+ @key_frequency.select_upper
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
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
+ end
10
+
11
+ def <<(value)
12
+ @count += 1
13
+ self[value] += 1 if value
14
+ end
15
+
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
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,145 @@
1
+ module Rack::Mount
2
+ module Analysis
3
+ module Splitting
4
+ class Key < Array
5
+ def initialize(method, index, separators)
6
+ replace([method, index, separators])
7
+ end
8
+
9
+ def self.split(value, separator_pattern)
10
+ keys = value.split(separator_pattern)
11
+ keys.shift if keys[0] == Const::EMPTY_STRING
12
+ keys << Const::NULL
13
+ keys
14
+ end
15
+
16
+ def call(cache, obj)
17
+ (cache[self[0]] ||= self.class.split(obj.send(self[0]), self[2]))[self[1]]
18
+ end
19
+
20
+ def call_source(cache, obj)
21
+ "(#{cache}[:#{self[0]}] ||= Analysis::Splitting::Key.split(#{obj}.#{self[0]}, #{self[2].inspect}))[#{self[1]}]"
22
+ end
23
+ end
24
+
25
+ def clear
26
+ @boundaries = {}
27
+ super
28
+ end
29
+
30
+ def <<(key)
31
+ super
32
+ key.each_pair do |k, v|
33
+ analyze_capture_boundaries(v, @boundaries[k] ||= Histogram.new)
34
+ end
35
+ end
36
+
37
+ def separators(key)
38
+ @boundaries[key].select_upper
39
+ end
40
+
41
+ def process_key(requirements, method, requirement)
42
+ separators = separators(method)
43
+ if requirement.is_a?(Regexp) && separators.any?
44
+ generate_split_keys(requirement, separators).each_with_index do |value, index|
45
+ requirements[Key.new(method, index, Regexp.union(*separators))] = value
46
+ end
47
+ else
48
+ super
49
+ end
50
+ end
51
+
52
+ private
53
+ def analyze_capture_boundaries(regexp, boundaries) #:nodoc:
54
+ return boundaries unless regexp.is_a?(Regexp)
55
+
56
+ parts = Utils.extract_regexp_parts(regexp) rescue []
57
+ 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
64
+ end
65
+ boundaries << previous[-1..-1] if previous.is_a?(String)
66
+ end
67
+
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
72
+ end
73
+ if following.is_a?(String) && following != Const::NULL
74
+ boundaries << following[0..0] == '\\' ? following[1..1] : following[0..0]
75
+ end
76
+ end
77
+ end
78
+ boundaries
79
+ end
80
+
81
+ 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
+ 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
103
+ 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)
108
+ 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) }
113
+ end
114
+ 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
+ end
120
+
121
+ while segments.length > 0 && (segments.last.nil? || segments.last == '')
122
+ segments.pop
123
+ end
124
+
125
+ segments
126
+ end
127
+
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
138
+ end
139
+
140
+ static = Utils.extract_static_regexp(s, regexp_options)
141
+ segments << (static.is_a?(String) ? static : static)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,45 @@
1
+ module Rack::Mount
2
+ module Const
3
+ RACK_ROUTING_ARGS = 'rack.routing_args'.freeze
4
+
5
+ begin
6
+ eval('/(?<foo>.*)/').named_captures
7
+ SUPPORTS_NAMED_CAPTURES = true
8
+ REGEXP_NAMED_CAPTURE = '(?<%s>%s)'.freeze
9
+ rescue SyntaxError, NoMethodError
10
+ SUPPORTS_NAMED_CAPTURES = false
11
+ REGEXP_NAMED_CAPTURE = '(?:<%s>%s)'.freeze
12
+ end
13
+
14
+ EMPTY_ARRAY = [].freeze
15
+ EMPTY_HASH = {}.freeze
16
+
17
+ NULL = "\0".freeze
18
+
19
+ CONTENT_TYPE = 'Content-Type'.freeze
20
+ CONTINUE = '100-continue'.freeze
21
+ DELETE = 'PUT'.freeze
22
+ EMPTY_STRING = ''.freeze
23
+ EXPECT = 'Expect'.freeze
24
+ GET = 'GET'.freeze
25
+ HEAD = 'HEAD'.freeze
26
+ PATH_INFO = 'PATH_INFO'.freeze
27
+ POST = 'POST'.freeze
28
+ PUT = 'PUT'.freeze
29
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
30
+ SCRIPT_NAME = 'SCRIPT_NAME'.freeze
31
+ SLASH = '/'.freeze
32
+ TEXT_SLASH_HTML = 'text/html'.freeze
33
+
34
+ DEFAULT_CONTENT_TYPE_HEADERS = {CONTENT_TYPE => TEXT_SLASH_HTML}.freeze
35
+ HTTP_METHODS = [GET, HEAD, POST, PUT, DELETE].freeze
36
+
37
+ OK = 'OK'.freeze
38
+ NOT_FOUND = 'Not Found'.freeze
39
+ EXPECTATION_FAILED = 'Expectation failed'.freeze
40
+
41
+ OK_RESPONSE = [200, DEFAULT_CONTENT_TYPE_HEADERS, [OK].freeze].freeze
42
+ NOT_FOUND_RESPONSE = [404, DEFAULT_CONTENT_TYPE_HEADERS, [NOT_FOUND].freeze].freeze
43
+ EXPECTATION_FAILED_RESPONSE = [417, DEFAULT_CONTENT_TYPE_HEADERS, [EXPECTATION_FAILED].freeze].freeze
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module Rack::Mount
2
+ class RoutingError < StandardError; end
3
+ end
@@ -0,0 +1,163 @@
1
+ require 'rack/mount/utils'
2
+
3
+ module Rack::Mount
4
+ class GeneratableRegexp < Regexp #:nodoc:
5
+ class DynamicSegment #:nodoc:
6
+ attr_reader :name, :requirement
7
+
8
+ def initialize(name, requirement)
9
+ @name, @requirement = name.to_sym, bound_expression(requirement)
10
+ freeze
11
+ end
12
+
13
+ def ==(obj)
14
+ @name == obj.name && @requirement == obj.requirement
15
+ end
16
+
17
+ def =~(str)
18
+ @requirement =~ str
19
+ end
20
+
21
+ def to_hash
22
+ { @name => @requirement }
23
+ end
24
+
25
+ def inspect
26
+ "/(?<#{@name}>#{@requirement.source})/"
27
+ end
28
+
29
+ private
30
+ def bound_expression(regexp)
31
+ source, options = regexp.source, regexp.options
32
+ source = "\\A#{source}\\Z"
33
+ Regexp.compile(source, options).freeze
34
+ end
35
+ end
36
+
37
+ module InstanceMethods
38
+ def self.extended(obj)
39
+ obj.segments
40
+ end
41
+
42
+ def generatable?
43
+ segments.any?
44
+ end
45
+
46
+ def generate(params = {}, recall = {}, defaults = {})
47
+ merged = recall.merge(params)
48
+ generate_from_segments(segments, params, merged, defaults)
49
+ end
50
+
51
+ def segments
52
+ @segments ||= begin
53
+ parse_segments(Utils.extract_regexp_parts(self))
54
+ rescue ArgumentError
55
+ Const::EMPTY_ARRAY
56
+ end
57
+ end
58
+
59
+ def captures
60
+ segments.flatten.find_all { |s| s.is_a?(DynamicSegment) }
61
+ end
62
+
63
+ def required_captures
64
+ segments.find_all { |s| s.is_a?(DynamicSegment) }
65
+ end
66
+
67
+ private
68
+ def parse_segments(segments)
69
+ s = []
70
+ segments.each do |part|
71
+ if part.is_a?(String) && part == Const::NULL
72
+ return s
73
+ elsif part.is_a?(Utils::Capture)
74
+ if part.named?
75
+ source = part.map { |p| p.to_s }.join
76
+ requirement = Regexp.compile(source)
77
+ s << DynamicSegment.new(part.name, requirement)
78
+ else
79
+ s << parse_segments(part)
80
+ end
81
+ else
82
+ part = part.gsub('\\/', '/')
83
+ static = Utils.extract_static_regexp(part)
84
+ if static.is_a?(String)
85
+ s << static.freeze
86
+ else
87
+ raise ArgumentError, "failed to parse #{part.inspect}"
88
+ end
89
+ end
90
+ end
91
+
92
+ s.freeze
93
+ end
94
+
95
+ def generate_from_segments(segments, params, merged, defaults, optional = false)
96
+ if optional
97
+ return Const::EMPTY_STRING if segments.all? { |s| s.is_a?(String) }
98
+ return Const::EMPTY_STRING unless segments.flatten.any? { |s|
99
+ params[s.name] if s.is_a?(DynamicSegment)
100
+ }
101
+ return Const::EMPTY_STRING if segments.any? { |segment|
102
+ if segment.is_a?(DynamicSegment)
103
+ value = merged[segment.name] || defaults[segment.name]
104
+ value = value.to_param if value.respond_to?(:to_param)
105
+
106
+ merged_value = merged[segment.name]
107
+ merged_value = merged_value.to_param if merged_value.respond_to?(:to_param)
108
+
109
+ default_value = defaults[segment.name]
110
+ default_value = default_value.to_param if default_value.respond_to?(:to_param)
111
+
112
+ if value.nil? || segment !~ value
113
+ true
114
+ elsif merged_value == default_value
115
+ # Nasty control flow
116
+ return :clear_remaining_segments
117
+ true
118
+ else
119
+ false
120
+ end
121
+ end
122
+ }
123
+ end
124
+
125
+ generated = segments.map do |segment|
126
+ case segment
127
+ when String
128
+ segment
129
+ when DynamicSegment
130
+ value = params[segment.name] || merged[segment.name] || defaults[segment.name]
131
+ value = value.to_param if value.respond_to?(:to_param)
132
+ if value && segment =~ value.to_s
133
+ value
134
+ else
135
+ return
136
+ end
137
+ when Array
138
+ value = generate_from_segments(segment, params, merged, defaults, true)
139
+ if value == :clear_remaining_segments
140
+ segment.each { |s| params.delete(s.name) if s.is_a?(DynamicSegment) }
141
+ Const::EMPTY_STRING
142
+ elsif value.nil?
143
+ Const::EMPTY_STRING
144
+ else
145
+ value
146
+ end
147
+ end
148
+ end
149
+
150
+ # Delete any used items from the params
151
+ segments.each { |s| params.delete(s.name) if s.is_a?(DynamicSegment) }
152
+
153
+ generated.join
154
+ end
155
+ end
156
+ include InstanceMethods
157
+
158
+ def initialize(regexp)
159
+ super
160
+ segments
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,57 @@
1
+ require 'rack/mount/utils'
2
+
3
+ module Rack::Mount
4
+ module Generation
5
+ module Route #:nodoc:
6
+ attr_reader :generation_keys
7
+
8
+ def initialize(*args)
9
+ super
10
+
11
+ @required_params = {}
12
+ @required_defaults = {}
13
+ @generation_keys = @defaults.dup
14
+
15
+ @conditions.each do |method, condition|
16
+ @required_params[method] = @conditions[method].required_captures.map { |s| s.name }.reject { |s| @defaults.include?(s) }.freeze
17
+ @required_defaults[method] = @defaults.dup
18
+ @conditions[method].captures.inject({}) { |h, s| h.merge!(s.to_hash) }.keys.each { |name|
19
+ @required_defaults[method].delete(name)
20
+ @generation_keys.delete(name) if @defaults.include?(name)
21
+ }
22
+ @required_defaults[method].freeze
23
+ end
24
+
25
+ @required_params.freeze
26
+ @required_defaults.freeze
27
+ @generation_keys.freeze
28
+ end
29
+
30
+ def generate(methods, params = {}, recall = {})
31
+ if methods.is_a?(Array)
32
+ result = methods.map { |m| generate_method(m, params, recall, @defaults) || (return nil) }
33
+ else
34
+ result = generate_method(methods, params, recall, @defaults)
35
+ end
36
+
37
+ if result
38
+ @defaults.each do |key, value|
39
+ params.delete(key) if params[key] == value
40
+ end
41
+ end
42
+
43
+ result
44
+ end
45
+
46
+ private
47
+ def generate_method(method, params, recall, defaults)
48
+ merged = recall.merge(params)
49
+ return nil unless condition = @conditions[method]
50
+ return nil if condition.segments.empty?
51
+ return nil unless @required_params[method].all? { |p| merged.include?(p) }
52
+ return nil unless @required_defaults[method].all? { |k, v| merged[k] == v }
53
+ condition.generate(params, recall, defaults)
54
+ end
55
+ end
56
+ end
57
+ end