http_router 0.1.0 → 0.1.1

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.
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