Skip to content

Commit

Permalink
* Integrate new routing tree with router (which fixed ControllerTest#…
Browse files Browse the repository at this point in the history
…test_generated_routes_match_actions)

* Replace Route with RouteNode
* Rename WildcardRouteNode to WildcardRoute
* Change Router::Tree#search to return action from found route
* Fix require for test/router/tree_test.rb
  • Loading branch information
fgrehm committed Mar 4, 2012
1 parent e33222c commit fb6186b
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 219 deletions.
19 changes: 9 additions & 10 deletions lib/harbor/router.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
require "set"
require_relative "router/route"
require_relative "router/tree"
require_relative "router/route_node"
require_relative "router/wildcard_route_node"
require_relative "router/route"
require_relative "router/wildcard_route"

module Harbor
class Router
Expand All @@ -27,13 +26,13 @@ def self.instance

def clear!
@methods = {
"GET" => Route.new,
"POST" => Route.new,
"PUT" => Route.new,
"DELETE" => Route.new,
"HEAD" => Route.new,
"OPTIONS" => Route.new,
"PATCH" => Route.new
"GET" => Tree.new,
"POST" => Tree.new,
"PUT" => Tree.new,
"DELETE" => Tree.new,
"HEAD" => Tree.new,
"OPTIONS" => Tree.new,
"PATCH" => Tree.new
}
end
end
Expand Down
158 changes: 89 additions & 69 deletions lib/harbor/router/route.rb
Original file line number Diff line number Diff line change
@@ -1,104 +1,124 @@
module Harbor
class Router
# A Ternary Search tree implementation that can be extended to a n-way search
# tree at insertion time. It also uses the AVL algorithm for self balancing (TODO)
class Route

PATH_SEPARATOR = /[\/;]/
MATCH = 0
RIGHT = 1
LEFT = -1
WILDCARD_FRAGMENT = '*'
WILDCARD_CHAR = ?:
PATH_SEPARATOR = /[\/;]/

def self.expand(path)
path.split(PATH_SEPARATOR).reject { |part| part.empty? }
end

attr_reader :fragment, :action, :tokens, :match, :left , :right

def initialize(fragment = nil, action = nil)
@fragment = fragment
@action = action
@tokens = nil
attr_reader :fragment, :tokens
attr_accessor :action, :left, :right, :match

@match = nil
@left = nil
@right = nil
def initialize(action = nil)
@action = action
end

def node(tokens, index = 0, length = tokens.length - 1)
part = tokens[index]
# Basic ternary search tree algorithm
def search(tokens, current_token = nil)
current_token = tokens.shift unless current_token

if part == @fragment || @fragment[0] == ?: then
return self if index == length
return @match.node(tokens, index + 1, length) if @match
if current_token == @fragment || wildcard?
return self if tokens.empty?
return @match.search(tokens) if @match
end

return @left.node(tokens, index, length) if @left && part < @fragment
return @right.node(tokens, index, length) if @right
return @left.search(tokens, current_token) if @left && current_token < @fragment
return @right.search(tokens, current_token) if @right
end

# Inserts or updates tree nodes
#
# Searches for path and returns action if matched.
# Returns nil if not found.
#
def search(path)
if result = node(path)
result.action
else
nil
end
# @return [ Route ] The inserted node
def insert(action, tokens)
(leaf = find_or_create_node!(tokens)).action = action
leaf
end

# Finds or create nodes for provided tokens, if a node is not found for a
# token, a "blank" node will be created and the search will continue.
#
# Inserts str and value into tree.
#
# str must implement []
#
def insert(tokens, action = nil, index = 0, length = tokens.size)
# @return [ Route ] The node for a set of tokens
def find_or_create_node!(tokens, index = 0)
part = tokens[index]

# This will extend the current node with "complex wildcard behavior" /
# n-way search tree
return replace!(tokens, index) if should_replace?(part)

if @fragment.nil?
assign! part, tokens
elsif @fragment[0] == ?:
replace! part, tokens, index
@fragment = fragment_from_token(part)
# Removes "extra" tokens
@tokens = tokens[0..index]
end

if part == @fragment then
# We have a match!

if (index + 1) < length then
# There are more fragments to consume.
(@match ||= Route.new).insert(tokens, action, index + 1, length)
else
# There are no more fragments to consume.
@action = action
end
elsif part < @fragment then
(@left ||= Route.new).insert(tokens, action, index, length)
else
(@right ||= Route.new).insert(tokens, action, index, length)
is_last_token = index == tokens.size - 1

# Ensures "virtual" wildcard nodes have the right tokens set so
# that we can map parameters back to route handlers
@tokens = tokens[0..index] if wildcard? && is_last_token

# Wildcard routes should always be considered matches
direction = wildcard?? MATCH : part <=> @fragment

# If it is a match and there are no more fragments to consume
return self if is_last_token && direction == MATCH

case direction
when MATCH
(@match ||= Route.new).find_or_create_node!(tokens, index + 1)
when LEFT
(@left ||= Route.new).find_or_create_node!(tokens, index)
when RIGHT
(@right ||= Route.new).find_or_create_node!(tokens, index)
end
end

def assign!(fragment, tokens)
@fragment = fragment
@tokens = tokens
def wildcard?
@fragment == WILDCARD_FRAGMENT
end

def replace!(fragment, tokens, index, length = tokens.size)
@fragment = fragment

# Valid routes are always to the left of Wildcards.
@left = Route.new
@left.insert(@tokens, @action, index, @tokens.size)
def fragment_from_token(token)
(token[0] == WILDCARD_CHAR) ? WILDCARD_FRAGMENT : token
end

@tokens = tokens
def should_replace?(part)
# On a wildcard node with an incoming "non-wildcard" node
(wildcard? && part[0] != WILDCARD_CHAR) ||
# ... or on a "non-wildcard" node with an incoming wildcard node
!@fragment.nil? && !wildcard? && part[0] == WILDCARD_CHAR
end

# If the Wildcard had additional fragments below it...
if @match
@left.match.insert(@left.tokens, @match.action, index + 1, @left.tokens.size)
end
def replace!(tokens, index)
extend WildcardRoute
find_or_create_node!(tokens, index)
end

# Continue insertion of the new path.
@match = Route.new
@match.insert(tokens, action, index + 1, length)
def assign_from(other_node)
@left = other_node.left
@right = other_node.right
@action = other_node.action
@tokens = other_node.tokens
@fragment = other_node.fragment
@match = other_node.match
self
end

end # Route
end # Router
end # Harbor
def reset!
@left = nil
@right = nil
@match = nil
@action = nil
@tokens = nil
@fragment = nil
end
end
end
end
119 changes: 0 additions & 119 deletions lib/harbor/router/route_node.rb

This file was deleted.

7 changes: 4 additions & 3 deletions lib/harbor/router/tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ class Tree

def insert(tokens, action)
if tokens.empty?
@home = RouteNode.new(action)
@home = Route.new(action)
else
(@root ||= RouteNode.new).insert(action, tokens)
(@root ||= Route.new).insert(action, tokens)
end
self
end

def search(tokens)
tokens.empty?? home : root.search(tokens)
result = tokens.empty?? home : root.search(tokens)
result.action if result
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ module Harbor
class Router
# Used to extend a "simple" node with n-way search tree behavior
# TODO: Add inspect information to distinguish from "normal" routes
module WildcardRouteNode
module WildcardRoute
def self.extended(base)
new_node = RouteNode.new.assign_from(base)
new_node = Route.new.assign_from(base)
if base.wildcard?
base.wildcard_tree = new_node
else
Expand Down Expand Up @@ -32,9 +32,9 @@ def search(tokens)
def find_or_create_node!(tokens, index = 0)
part = tokens[index]
if part[0] == self.class::WILDCARD_CHAR
(@wildcard_tree ||= RouteNode.new).find_or_create_node!(tokens, index)
(@wildcard_tree ||= Route.new).find_or_create_node!(tokens, index)
else
(trees[part] ||= RouteNode.new).find_or_create_node!(tokens, index)
(trees[part] ||= Route.new).find_or_create_node!(tokens, index)
end
end

Expand Down
Loading

0 comments on commit fb6186b

Please sign in to comment.