Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router work #33

Merged
merged 10 commits into from
Mar 5, 2012
15 changes: 6 additions & 9 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
GIT
remote: git@github.com:sam/redis_directory.git
revision: ecd2fbc6613bbb0146895f021a86c8de0ea05e1e
specs:
redis_directory (1.0.4)
json
redis

PATH
remote: .
specs:
Expand Down Expand Up @@ -47,13 +39,18 @@ GEM
macaddr (1.5.0)
systemu (>= 2.4.0)
mime-types (1.17.2)
minitest (2.11.3)
rack (1.4.1)
rack-test (0.6.1)
rack (>= 1.0)
rake (0.9.2.2)
rdoc (3.12)
json (~> 1.4)
redis (2.2.2)
redis_directory (1.0.4)
json
minitest
redis
systemu (2.4.2)
testdrive (0.2)
tilt (1.3.3)
Expand All @@ -72,6 +69,6 @@ DEPENDENCIES
rack-test
rake
rdoc (>= 2.4.2)
redis_directory!
redis_directory (>= 1.0.4)
testdrive
uuid
1 change: 1 addition & 0 deletions lib/harbor/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def self.route(method, path, handler)
end

def self.method_name_for_route(http_method, path)
return "GET__root__" if path == "/"
parts = [ http_method.upcase ]

Router::Route::expand(path).each do |part|
Expand Down
3 changes: 1 addition & 2 deletions lib/harbor/controller/normalized_path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def to_s
end
end
end

end
end
end
end
17 changes: 9 additions & 8 deletions lib/harbor/router.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "set"
require_relative "router/tree"
require_relative "router/route"
require_relative "router/wildcard_route"

module Harbor
class Router
Expand All @@ -24,13 +25,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
21 changes: 21 additions & 0 deletions lib/harbor/router/tree.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Harbor
class Router
class Tree
attr_reader :root, :home

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

def search(tokens)
result = tokens.empty?? home : root.search(tokens)
result.action if result
end
end
end
end
46 changes: 46 additions & 0 deletions lib/harbor/router/wildcard_route.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 WildcardRoute
def self.extended(base)
new_node = Route.new.assign_from(base)
if base.wildcard?
base.wildcard_tree = new_node
else
base.trees[base.fragment] = new_node
end
base.reset!
end

attr_accessor :wildcard_tree

def search(tokens)
part = tokens.first

exact_result = if (tree = trees[part])
# The dup is required as the search method will consume tokens
tree.search(tokens.dup)
end
return exact_result if exact_result

# An exact match could be found? Lets give one last shot and try to match
# wildcard routes ;)
@wildcard_tree.search(tokens) if @wildcard_tree
end

def find_or_create_node!(tokens, index = 0)
part = tokens[index]
if part[0] == self.class::WILDCARD_CHAR
(@wildcard_tree ||= Route.new).find_or_create_node!(tokens, index)
else
(trees[part] ||= Route.new).find_or_create_node!(tokens, index)
end
end

def trees
@trees ||= {}
end
end
end
end
9 changes: 8 additions & 1 deletion test/controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ class Foos < Harbor::Controller
:GET
end

# /
get "/" do
:GET__root__
end

# /foos/executive_report
get "executive_report" do
:GET_executive_report
Expand Down Expand Up @@ -46,7 +51,8 @@ def setup
@router = Harbor::Router::instance
end

def test_generated_action_methods_return_exepected_results
def test_generated_action_methods_return_expected_results
assert_equal :GET__root__, @example.GET__root__
assert_equal :GET, @example.GET
assert_equal :GET_executive_report, @example.GET_executive_report
assert_equal :GET__id, @example.GET__id
Expand All @@ -56,6 +62,7 @@ def test_generated_action_methods_return_exepected_results
end

def test_generated_routes_match_actions
assert_controller_route_matches("GET", "/", Controllers::Foos, :GET__root__)
assert_controller_route_matches("GET", "/controller_test/foos", Controllers::Foos, :GET)
assert_controller_route_matches("GET", "/controller_test/foos/executive_report", Controllers::Foos, :GET_executive_report)
assert_controller_route_matches("GET", "/controller_test/foos/42", Controllers::Foos, :GET__id)
Expand Down
Loading