http_router 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -10,7 +10,7 @@ begin
10
10
  end
11
11
  Jeweler::GemcutterTasks.new
12
12
  rescue LoadError
13
- puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
13
+ puts "Jeweler not available. Install it with: gem install jeweler"
14
14
  end
15
15
 
16
16
  require 'spec'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
@@ -0,0 +1,57 @@
1
+ # Replacement for {Rack::Builder} which using HttpRouter to map requests instead of a simple Hash.
2
+ # As well, add convenience methods for the request methods.
3
+ class Rack::Builder
4
+ def initialize(&block)
5
+ @router = HttpRouter.new
6
+ super
7
+ end
8
+
9
+ # Maps a path to a block.
10
+ # @param path [String] Path to map to.
11
+ # @param options [Hash] Options for added path.
12
+ # @see HttpRouter#add
13
+ def map(path, options = nil, &block)
14
+ @router.add(path).with_options(options).to(&block)
15
+ @ins << @router unless @ins.last == @router
16
+ end
17
+
18
+ # Maps a path with request methods `HEAD` and `GET` to a block.
19
+ # @param path [String] Path to map to.
20
+ # @param options [Hash] Options for added path.
21
+ # @see HttpRouter#add
22
+ def get(path, options = nil, &block)
23
+ @router.get(path).with_options(options).to(&block)
24
+ end
25
+
26
+ # Maps a path with request methods `POST` to a block.
27
+ # @param path [String] Path to map to.
28
+ # @param options [Hash] Options for added path.
29
+ # @see HttpRouter#add
30
+ def post(path, options = nil, &block)
31
+ @router.post(path).with_options(options).to(&block)
32
+ end
33
+
34
+ # Maps a path with request methods `PUT` to a block.
35
+ # @param path [String] Path to map to.
36
+ # @param options [Hash] Options for added path.
37
+ # @see HttpRouter#add
38
+ def put(path, options = nil, &block)
39
+ @router.put(path).with_options(options).to(&block)
40
+ end
41
+
42
+ # Maps a path with request methods `DELETE` to a block.
43
+ # @param path [String] Path to map to.
44
+ # @param options [Hash] Options for added path.
45
+ # @see HttpRouter#add
46
+ def delete(path, options = nil, &block)
47
+ @router.delete(path).with_options(options).to(&block)
48
+ end
49
+
50
+ # Maps a path with request methods `HEAD` to a block.
51
+ # @param path [String] Path to map to.
52
+ # @param options [Hash] Options for added path.
53
+ # @see HttpRouter#add
54
+ def head(path, options = nil, &block)
55
+ @router.head(path).with_options(options).to(&block)
56
+ end
57
+ end
@@ -0,0 +1,26 @@
1
+ module Rack::Utils
2
+ def uri_escape(s)
3
+ s.to_s.gsub(/([^:\/?\[\]\-_~\.!\$&'\(\)\*\+,;=@a-zA-Z0-9]+)/n) {
4
+ '%'<<$1.unpack('H2'*$1.size).join('%').upcase
5
+ }
6
+ end
7
+ module_function :uri_escape
8
+ end unless Rack::Utils.respond_to?(:uri_escape)
9
+
10
+ module Rack::Utils
11
+ def uri_escape!(s)
12
+ s.to_s.gsub!(/([^:\/?\[\]\-_~\.!\$&'\(\)\*\+,;=@a-zA-Z0-9]+)/n) {
13
+ '%'<<$1.unpack('H2'*$1.size).join('%').upcase
14
+ }
15
+ end
16
+ module_function :uri_escape!
17
+ end unless Rack::Utils.respond_to?(:uri_escape!)
18
+
19
+ module Rack::Utils
20
+ def uri_unescape(s)
21
+ gsub(/((?:%[0-9a-fA-F]{2})+)/n){
22
+ [$1.delete('%')].pack('H*')
23
+ }
24
+ end
25
+ module_function :uri_unescape
26
+ end unless Rack::Utils.respond_to?(:uri_unescape)
@@ -1,11 +1,12 @@
1
1
  class HttpRouter
2
2
  class Glob < Variable
3
- def matches(parts, whole_path)
3
+ def matches(env, parts, whole_path)
4
4
  if @matches_with && match = @matches_with.match(parts.first)
5
5
  params = [parts.shift]
6
6
  while !parts.empty? and match = @matches_with.match(parts.first)
7
7
  params << parts.shift
8
8
  end
9
+ return unless additional_matchers(env, params)
9
10
  whole_path.replace(parts.join('/'))
10
11
  params
11
12
  else
@@ -1,4 +1,4 @@
1
- $LOAD_PATH << File.join(File.dirname(__FILE__), '..')
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
2
2
  require 'http_router'
3
3
 
4
4
  class HttpRouter
@@ -26,17 +26,17 @@ class HttpRouter
26
26
  private
27
27
  def route!(base=self.class, pass_block=nil)
28
28
  if base.router and match = base.router.recognize(@request)
29
- if match.is_a?(RoutingError)
30
- route_eval {
31
- match.headers.each{|k,v| response[k] = v}
32
- status match.status
33
- }
34
- else
29
+ if match.matched?
35
30
  @block_params = match.params
36
31
  (@params ||= {}).merge!(match.params_as_hash)
37
32
  pass_block = catch(:pass) do
38
33
  route_eval(&match.route.dest)
39
34
  end
35
+ else
36
+ route_eval {
37
+ match.headers.each{|k,v| response[k] = v}
38
+ status match.status
39
+ }
40
40
  end
41
41
  end
42
42
 
@@ -3,37 +3,41 @@ class HttpRouter
3
3
  attr_accessor :value, :variable, :catchall
4
4
  attr_reader :linear, :lookup, :request_node, :extension_node
5
5
 
6
- def initialize
6
+ def initialize(base)
7
+ @router = base
7
8
  reset!
8
9
  end
9
10
 
10
11
  def reset!
11
- @linear = []
12
- @lookup = {}
12
+ @linear = nil
13
+ @lookup = nil
13
14
  @catchall = nil
14
15
  end
15
16
 
16
17
  def add(val)
17
18
  if val.is_a?(Variable)
18
19
  if val.matches_with
19
- new_node = Node.new
20
+ new_node = router.node
21
+ create_linear
20
22
  @linear << [val, new_node]
21
23
  new_node
22
24
  else
23
- @catchall ||= Node.new
25
+ @catchall ||= router.node
24
26
  @catchall.variable = val
25
27
  @catchall
26
28
  end
27
29
  elsif val.is_a?(Regexp)
28
- @linear << [val, Node.new]
30
+ create_linear
31
+ @linear << [val, router.node]
29
32
  @linear.last.last
30
33
  else
31
- @lookup[val] ||= Node.new
34
+ create_lookup
35
+ @lookup[val] ||= router.node
32
36
  end
33
37
  end
34
38
 
35
39
  def add_extension(ext)
36
- @extension_node ||= Node.new
40
+ @extension_node ||= router.node
37
41
  @extension_node.add(ext)
38
42
  end
39
43
 
@@ -43,7 +47,7 @@ class HttpRouter
43
47
  elsif @request_node
44
48
  current_node = @request_node
45
49
  while current_node.request_method
46
- current_node = (current_node.catchall ||= RequestNode.new)
50
+ current_node = (current_node.catchall ||= router.request_node)
47
51
  end
48
52
  [current_node]
49
53
  else
@@ -52,12 +56,14 @@ class HttpRouter
52
56
  end
53
57
 
54
58
  protected
59
+
60
+ attr_reader :router
55
61
 
56
62
  def transplant_value
57
63
  if @value
58
64
  target_node = @request_node
59
65
  while target_node.request_method
60
- target_node = (target_node.catchall ||= RequestNode.new)
66
+ target_node = (target_node.catchall ||= router.request_node)
61
67
  end
62
68
  target_node.value = @value
63
69
  @value = nil
@@ -65,89 +71,133 @@ class HttpRouter
65
71
  end
66
72
 
67
73
  def generate_request_method_tree(request_options)
68
- raise if (request_options.keys & RequestNode::RequestMethods).size != request_options.size
69
-
70
-
71
- current_nodes = [@request_node ||= RequestNode.new]
74
+ raise(UnsupportedRequestConditionError.new) if (request_options.keys & RequestNode::RequestMethods).size != request_options.size
75
+ current_nodes = [self]
72
76
  RequestNode::RequestMethods.each do |method|
73
- current_nodes.each_with_index do |current_node, current_node_index|
74
- if request_options[method] #we care about the method
75
- if current_node # and we have to pay attention to what currently is there.
77
+ if request_options.key?(method) # so, the request method we care about it ..
78
+ if current_nodes == [self]
79
+ current_nodes = [@request_node ||= router.request_node]
80
+ end
81
+
82
+ for current_node_index in (0...current_nodes.size)
83
+ current_node = current_nodes.at(current_node_index)
84
+ if request_options.key?(method) #we care about the method
76
85
  unless current_node.request_method
77
86
  current_node.request_method = method
78
87
  end
79
-
80
88
  case RequestNode::RequestMethods.index(method) <=> RequestNode::RequestMethods.index(current_node.request_method)
81
89
  when 0 #use this node
82
90
  if request_options[method].is_a?(Regexp)
83
- current_node = RequestNode.new
84
- current_node.linear << [request_options[method], current_node]
91
+ new_node = router.request_node
92
+ current_nodes[current_node_index] = new_node
93
+ current_node.create_linear
94
+ current_node.linear << [request_options[method], new_node]
85
95
  elsif request_options[method].is_a?(Array)
86
- current_nodes[current_node_index] = request_options[method].map{|val| current_node.lookup[val] ||= RequestNode.new}
96
+ current_node.create_lookup
97
+ current_nodes[current_node_index] = request_options[method].map{|val| current_node.lookup[val] ||= router.request_node}
87
98
  else
88
- current_nodes[current_node_index] = (current_node.lookup[request_options[method]] ||= RequestNode.new)
99
+ current_node.create_lookup
100
+ current_nodes[current_node_index] = (current_node.lookup[request_options[method]] ||= router.request_node)
89
101
  end
90
102
  when 1 #this node is farther ahead
91
- current_nodes[current_node_index] = (current_node.catchall ||= RequestNode.new)
92
- redo
103
+ current_nodes[current_node_index] = (current_node.catchall ||= router.request_node)
93
104
  when -1 #this method is more important than the current node
94
- new_node = RequestNode.new
95
- new_node.request_method = method
96
- new_node.catchall = current_node
97
- current_nodes[current_node_index] = new_node
105
+ next_node = current_node.dup
106
+ current_node.reset!
107
+ current_node.request_method = method
98
108
  redo
99
109
  end
100
110
  else
101
- current_nodes[current_node_index] = RequestNode.new
102
- redo
111
+ current_node.catchall ||= router.request_node
103
112
  end
104
- elsif !current_node
105
- @request_node = RequestNode.new
106
- current_nodes[current_node_index] = @request_node
107
- redo
108
- else
109
- current_node.catchall ||= RequestNode.new
110
113
  end
114
+ current_nodes.flatten!
115
+ elsif current_nodes.first.is_a?(RequestNode) && !current_nodes.first.request_method.nil?
116
+ current_nodes.map!{|n| n.catchall ||= router.request_node}
111
117
  end
112
- current_nodes.flatten!
113
118
  end
114
119
  transplant_value
115
120
  current_nodes
116
121
  end
117
122
 
118
- def find_on_request_methods(request)
119
- current_node = self
120
- if current_node && current_node.request_node
121
- current_node = current_node.request_node
122
- while current_node
123
- previous_node = current_node
124
- break if current_node.nil? || current_node.is_a?(RoutingError) || current_node.value
125
- request_value = request.send(current_node.request_method)
126
- unless current_node.linear.empty?
127
- next_node = current_node.linear.find do |(regexp, node)|
128
- regexp === request_value
129
- end
130
- if next_node
131
- current_node = next_node.last
132
- next
123
+ def find_on_parts(request, parts, extension, params)
124
+ if parts.empty? && extension_node && extension
125
+ parts << extension
126
+ extension_node.find_on_parts(request, parts, extension, params)
127
+ else
128
+ if @linear && !@linear.empty?
129
+ whole_path = parts.join('/')
130
+ next_node = @linear.find do |(tester, node)|
131
+ if tester.is_a?(Regexp) and match = whole_path.match(tester) #and match.index == 0 TODO
132
+ whole_path.slice!(0,match[0].size)
133
+ parts.replace(router.split(whole_path))
134
+ node
135
+ elsif new_params = tester.matches(request.env, parts, whole_path)
136
+ params << new_params
137
+ node
138
+ else
139
+ nil
133
140
  end
134
141
  end
135
- current_node = current_node.lookup[request_value] || current_node.catchall
136
- if current_node.nil?
137
- current_node = previous_node.request_method == :request_method ? RoutingError.new(405, {"Allow" => previous_node.lookup.keys.join(", ")}) : nil
138
- else
139
- current_node
140
- end
142
+ return next_node.last.find_on_parts(request, parts, extension, params) if next_node
143
+ end
144
+ if match = @lookup && @lookup[parts.first]
145
+ parts.shift
146
+ match.find_on_parts(request, parts, extension, params)
147
+ elsif @catchall
148
+ params << @catchall.variable.matches(request.env, parts, whole_path)
149
+ parts.shift
150
+ @catchall.find_on_parts(request, parts, extension, params)
151
+ elsif parts.size == 1 && parts.first == '' && (value && value.route.trailing_slash_ignore?)
152
+ parts.shift
153
+ find_on_parts(request, parts, extension, params)
154
+ elsif request_node
155
+ request_node.find_on_request_methods(request)
156
+ elsif @value
157
+ self
158
+ else
159
+ nil
141
160
  end
142
161
  end
143
- current_node
144
162
  end
145
163
 
164
+ def create_linear
165
+ @linear ||= []
166
+ end
167
+
168
+ def create_lookup
169
+ @lookup ||= {}
170
+ end
146
171
  end
147
172
 
148
173
  class RequestNode < Node
149
174
  RequestMethods = [:request_method, :host, :port, :scheme]
150
175
  attr_accessor :request_method
176
+
177
+ def find_on_request_methods(request)
178
+ if @request_method
179
+ request_value = request.send(request_method)
180
+ if @linear && !@linear.empty?
181
+ next_node = @linear.find do |(regexp, node)|
182
+ regexp === request_value
183
+ end
184
+ next_node &&= next_node.find_on_request_methods(request)
185
+ return next_node if next_node
186
+ end
187
+ if @lookup and next_node = (@lookup[request_value] && @lookup[request_value].find_on_request_methods(request))
188
+ return next_node
189
+ elsif next_node = (@catchall && @catchall.find_on_request_methods(request))
190
+ return next_node
191
+ end
192
+ end
193
+
194
+ if @value
195
+ self
196
+ else
197
+ current_node = request_method == :request_method ? Response.unmatched(405, {"Allow" => @lookup.keys.join(", ")}) : nil
198
+ end
199
+ end
200
+
151
201
  end
152
202
 
153
203
  end
@@ -5,14 +5,35 @@ class HttpRouter
5
5
  attr_accessor :route
6
6
  def initialize(path, parts, extension)
7
7
  @path, @parts, @extension = path, parts, extension
8
- @eval_path = path.gsub(/[:\*]([a-zA-Z0-9_]+)/) {"\#{args.shift || (options && options.delete(:#{$1})) || raise(MissingParameterException.new(\"missing parameter #{$1}\"))}" }
8
+ if duplicate_variable_names = variable_names.dup.uniq!
9
+ raise AmbiguousVariableException.new("You have duplicate variable name present: #{duplicate_variable_names.join(', ')}")
10
+ end
11
+
12
+ eval_path = path.gsub(/[:\*]([a-zA-Z0-9_]+)/) {"\#{args.shift || (options && options.delete(:#{$1})) || raise(MissingParameterException.new(\"missing parameter #{$1}\"))}" }
9
13
  instance_eval "
10
14
  def raw_url(args,options)
11
- \"#{@eval_path}\"
15
+ \"#{eval_path}\"
12
16
  end
13
17
  "
14
18
  end
15
19
 
20
+ def ===(other_path)
21
+ return false if @parts.size != other_path.parts.size
22
+ @parts.each_with_index {|p,i|
23
+ return unless compare_parts(p, other_path.parts[i])
24
+ }
25
+ compare_parts(@extension, other_path.extension)
26
+ end
27
+
28
+ def compare_parts(p1, p2)
29
+ case p1
30
+ when Glob then p2.is_a?(Glob)
31
+ when Variable then p2.is_a?(Variable)
32
+ else
33
+ p1 == p2
34
+ end
35
+ end
36
+
16
37
  def url(args, options)
17
38
  path = raw_url(args, options)
18
39
  raise TooManyParametersException.new unless args.empty?
@@ -1,25 +1,46 @@
1
1
  class HttpRouter
2
- class Response < Struct.new(:path, :params, :extension, :matched_path, :remaining_path)
3
- attr_reader :params_as_hash, :route
2
+ module Response
3
+ def self.matched(*args)
4
+ Matched.new(*args)
5
+ end
4
6
 
5
- def initialize(path, params, extension, matched_path, remaining_path)
6
- raise if matched_path.nil?
7
- super
8
- @params_as_hash = path.variable_names.zip(params).inject({}) {|h, (k,v)| h[k] = v; h }
9
- @params_as_hash[path.extension.name] = extension if path.extension && path.extension.is_a?(Variable)
7
+ def self.unmatched(*args)
8
+ Unmatched.new(*args)
10
9
  end
11
10
 
12
- def route
13
- path.route
11
+ private
12
+ class Unmatched < Struct.new(:status, :headers)
13
+ def matched?
14
+ false
15
+ end
14
16
  end
17
+
18
+ class Matched < Struct.new(:path, :params, :extension, :matched_path, :remaining_path)
19
+ attr_reader :params_as_hash, :route
20
+
21
+ def initialize(path, params, extension, matched_path, remaining_path)
22
+ raise if matched_path.nil?
23
+ super
24
+ @params_as_hash = path.variable_names.zip(params).inject({}) {|h, (k,v)| h[k] = v; h }
25
+ @params_as_hash[path.extension.name] = extension if path.extension && path.extension.is_a?(Variable)
26
+ end
27
+
28
+ def matched?
29
+ true
30
+ end
31
+
32
+ def route
33
+ path.route
34
+ end
15
35
 
16
- def dest
17
- route.dest
18
- end
19
- alias_method :destination, :dest
36
+ def dest
37
+ route.dest
38
+ end
39
+ alias_method :destination, :dest
20
40
 
21
- def partial_match?
22
- remaining_path
41
+ def partial_match?
42
+ remaining_path
43
+ end
23
44
  end
24
45
  end
25
46
  end
@@ -1,10 +1,5 @@
1
1
  class HttpRouter
2
2
  class Root < Node
3
- def initialize(base)
4
- @base = base
5
- reset!
6
- end
7
-
8
3
  def add_path(path)
9
4
  node = path.parts.inject(self) { |node, part| node.add(part) }
10
5
  if path.extension
@@ -15,69 +10,23 @@ class HttpRouter
15
10
 
16
11
  def find(request)
17
12
  path = request.path_info.dup
18
- path.slice!(-1) if @base.ignore_trailing_slash? && path[-1] == ?/
13
+ path.slice!(-1) if router.ignore_trailing_slash? && path[-1] == ?/
19
14
  extension = extract_extension(path)
20
- parts = @base.split(path)
15
+ parts = router.split(path)
21
16
  parts << '' if path[path.size - 1] == ?/
22
-
23
17
  params = []
24
- if current_node = process_parts(parts, extension, params)
25
- current_node = current_node.find_on_request_methods(request)
26
- end
27
-
28
- process_response(current_node, parts, extension, params, request)
18
+ process_response(
19
+ find_on_parts(request, parts, extension, params),
20
+ parts,
21
+ extension,
22
+ params,
23
+ request
24
+ )
29
25
  end
30
26
 
31
27
  private
32
-
33
- def process_parts(parts, extension, params)
34
- current_node = self
35
- loop do
36
- if parts.empty? && current_node.extension_node && extension
37
- parts << extension
38
- current_node = current_node.extension_node
39
- end
40
- break if current_node.nil? || (current_node.value && current_node.value.route.partially_match?) || parts.empty?
41
- unless current_node.linear.empty?
42
- whole_path = parts.join('/')
43
- next_node = current_node.linear.find do |(tester, node)|
44
- if tester.is_a?(Regexp) and match = whole_path.match(tester)
45
- whole_path.slice!(0,match[0].size)
46
- parts.replace(@base.split(whole_path))
47
- node
48
- elsif new_params = tester.matches(parts, whole_path)
49
- params << new_params
50
- node
51
- else
52
- nil
53
- end
54
- end
55
- if next_node
56
- current_node = next_node.last
57
- next
58
- end
59
- end
60
- if match = current_node.lookup[parts.first]
61
- parts.shift
62
- current_node = match
63
- elsif current_node.catchall
64
- params << current_node.catchall.variable.matches(parts, whole_path)
65
- parts.shift
66
- current_node = current_node.catchall
67
- elsif parts.size == 1 && parts.first == '' && current_node && (current_node.value && current_node.value.route.trailing_slash_ignore?)
68
- parts.shift
69
- elsif current_node.request_node
70
- break
71
- else
72
- current_node = nil
73
- break
74
- end
75
- end
76
- current_node
77
- end
78
-
79
28
  def process_response(node, parts, extension, params, request)
80
- if node.is_a?(RoutingError)
29
+ if node.respond_to?(:matched?) && !node.matched?
81
30
  node
82
31
  elsif node && node.value
83
32
  if parts.empty?
@@ -103,10 +52,10 @@ class HttpRouter
103
52
 
104
53
  def post_match(path, params, extension, matched_path, remaining_path = nil)
105
54
  if path.route.partially_match? || path.matches_extension?(extension)
106
- Response.new(path, params, extension, matched_path, remaining_path)
55
+ Response.matched(path, params, extension, matched_path, remaining_path)
107
56
  else
108
57
  nil
109
58
  end
110
59
  end
111
60
  end
112
- end
61
+ end