Skip to content

Commit

Permalink
Merge pull request #19 from luislavena/improve-node-sorting
Browse files Browse the repository at this point in the history
Makes node prioritization insertion-independent
  • Loading branch information
luislavena authored Feb 4, 2017
2 parents 0dc6e17 + 1764332 commit 68e21bc
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 68 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ This project aims to comply with [Semantic Versioning](http://semver.org/),
so please check *Changed* and *Removed* notes before upgrading.

## [Unreleased]
### Fixed
- Correct prioritization of node's children using combination of kind and
priority, allowing partial shared keys to coexist and resolve lookup.

## [0.3.6] - 2017-01-18
### Fixed
Expand Down
76 changes: 59 additions & 17 deletions spec/radix/node_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ require "../spec_helper"

module Radix
describe Node do
describe "#glob?" do
it "returns true when key contains a glob parameter (catch all)" do
node = Node(Nil).new("a")
node.glob?.should be_false

node = Node(Nil).new("*filepath")
node.glob?.should be_true
end
end

describe "#key=" do
it "accepts change of key after initialization" do
node = Node(Nil).new("abc")
Expand All @@ -10,6 +20,38 @@ module Radix
node.key = "xyz"
node.key.should eq("xyz")
end

it "also changes kind when modified" do
node = Node(Nil).new("abc")
node.normal?.should be_true

node.key = ":query"
node.normal?.should be_false
node.named?.should be_true
end
end

describe "#named?" do
it "returns true when key contains a named parameter" do
node = Node(Nil).new("a")
node.named?.should be_false

node = Node(Nil).new(":query")
node.named?.should be_true
end
end

describe "#normal?" do
it "returns true when key does not contain named or glob parameters" do
node = Node(Nil).new("a")
node.normal?.should be_true

node = Node(Nil).new(":query")
node.normal?.should be_false

node = Node(Nil).new("*filepath")
node.normal?.should be_false
end
end

describe "#payload" do
Expand All @@ -36,28 +78,28 @@ module Radix
end

describe "#priority" do
it "calculates it based on key size" do
it "calculates it based on key length" do
node = Node(Nil).new("a")
node.priority.should eq(1)

node = Node(Nil).new("abc")
node.priority.should eq(3)
end

it "returns zero for catch all (globbed) key" do
node = Node(Nil).new("*filepath")
node.priority.should eq(-2)
it "considers key length up until named parameter presence" do
node = Node(Nil).new("/posts/:id")
node.priority.should eq(7)

node = Node(Nil).new("/src/*filepath")
node.priority.should eq(-2)
node = Node(Nil).new("/u/:username")
node.priority.should eq(3)
end

it "returns one for keys with named parameters" do
node = Node(Nil).new(":query")
node.priority.should eq(-1)
it "considers key length up until glob parameter presence" do
node = Node(Nil).new("/search/*query")
node.priority.should eq(8)

node = Node(Nil).new("/search/:query")
node.priority.should eq(-1)
node = Node(Nil).new("/*anything")
node.priority.should eq(1)
end

it "changes when key changes" do
Expand All @@ -67,16 +109,16 @@ module Radix
node.key = "abc"
node.priority.should eq(3)

node.key = "*filepath"
node.priority.should eq(-2)
node.key = "/src/*filepath"
node.priority.should eq(5)

node.key = ":query"
node.priority.should eq(-1)
node.key = "/search/:query"
node.priority.should eq(8)
end
end

describe "#sort!" do
it "orders children by priority" do
it "orders children" do
root = Node(Int32).new("/")
node1 = Node(Int32).new("a", 1)
node2 = Node(Int32).new("bc", 2)
Expand All @@ -90,7 +132,7 @@ module Radix
root.children[2].should eq(node1)
end

it "orders catch all and named parameters lower than others" do
it "orders catch all and named parameters lower than normal nodes" do
root = Node(Int32).new("/")
node1 = Node(Int32).new("*filepath", 1)
node2 = Node(Int32).new("abc", 2)
Expand Down
13 changes: 13 additions & 0 deletions spec/radix/tree_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,19 @@ module Radix
result.payload.should eq(:featured)
end
end

context "dealing with named parameters and shared key" do
it "finds matching path" do
tree = Tree(Symbol).new
tree.add "/one/:id", :one
tree.add "/one-longer/:id", :two

result = tree.find "/one-longer/10"
result.found?.should be_true
result.key.should eq("/one-longer/:id")
result.params["id"].should eq("10")
end
end
end
end
end
163 changes: 112 additions & 51 deletions src/radix/node.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,34 @@ module Radix
# Carries a *payload* and might also contain references to other nodes
# down in the organization inside *children*.
#
# Each node also carries a *priority* number, which might indicate the
# weight of this node depending on characteristics like catch all
# (globbing), named parameters or simply the size of the Node's *key*.
#
# Referenced nodes inside *children* can be sorted by *priority*.
# Each node also carries identification in relation to the kind of key it
# contains, which helps with characteristics of the node like named
# parameters or catch all kind (globbing).
#
# Is not expected direct usage of a node but instead manipulation via
# methods within `Tree`.
#
# ```
# node = Radix::Node.new("/", :root)
# node.children << Radix::Node.new("a", :a)
# node.children << Radix::Node.new("bc", :bc)
# node.children << Radix::Node.new("def", :def)
# node.sort!
#
# node.priority
# # => 1
#
# node.children.map &.priority
# # => [3, 2, 1]
# ```
class Node(T)
include Comparable(self)

# :nodoc:
enum Kind : UInt8
Normal
Named
Glob
end

getter key
getter? placeholder
property children = [] of Node(T)
property! payload : T | Nil
property children

# :nodoc:
protected getter kind = Kind::Normal

# Returns the priority of the Node based on it's *key*
#
# This value will be directly associated to the key size or special
# elements of it.
#
# * A catch all (globbed) key will receive lowest priority (`-2`)
# * A named parameter key will receive priority above catch all (`-1`)
# * Any other type of key will receive priority based on its size.
# This value will be directly associated to the key size up until a
# special elements is found.
#
# ```
# Radix::Node(Nil).new("a").priority
Expand All @@ -48,11 +40,11 @@ module Radix
# Radix::Node(Nil).new("abc").priority
# # => 3
#
# Radix::Node(Nil).new("*filepath").priority
# # => -2
# Radix::Node(Nil).new("/src/*filepath").priority
# # => 5
#
# Radix::Node(Nil).new(":query").priority
# # => -1
# Radix::Node(Nil).new("/search/:query").priority
# # => 8
# ```
getter priority : Int32

Expand All @@ -75,10 +67,62 @@ module Radix
# node = Radix::Node.new("/")
# ```
def initialize(@key : String, @payload : T? = nil, @placeholder = false)
@children = [] of Node(T)
@priority = compute_priority
end

# Compares this node against *other*, returning `-1`, `0` or `1` depending
# on whether this node differentiates from *other*.
#
# Comparison is done combining node's `kind` and `priority`. Nodes of
# same kind are compared by priority. Nodes of different kind are
# ranked.
#
# ### Normal nodes
#
# ```
# node1 = Radix::Node(Nil).new("a") # normal
# node2 = Radix::Node(Nil).new("bc") # normal
# node1 <=> node2 # => 1
# ```
#
# ### Normal vs named or glob nodes
#
# ```
# node1 = Radix::Node(Nil).new("a") # normal
# node2 = Radix::Node(Nil).new(":query") # named
# node3 = Radix::Node(Nil).new("*filepath") # glob
# node1 <=> node2 # => -1
# node1 <=> node3 # => -1
# ```
#
# ### Named vs glob nodes
#
# ```
# node1 = Radix::Node(Nil).new(":query") # named
# node2 = Radix::Node(Nil).new("*filepath") # glob
# node1 <=> node2 # => -1
# ```
def <=>(other : self)
result = kind <=> other.kind
return result if result != 0

other.priority <=> priority
end

# Returns `true` if the node key contains a glob parameter in it
# (catch all)
#
# ```
# node = Radix::Node(Nil).new("*filepath")
# node.glob? # => true
#
# node = Radix::Node(Nil).new("abc")
# node.glob? # => false
# ```
def glob?
kind.glob?
end

# Changes current *key*
#
# ```
Expand All @@ -91,7 +135,7 @@ module Radix
# # => "b"
# ```
#
# This will also result in a new priority for the node.
# This will also result in change of node's `priority`
#
# ```
# node = Radix::Node(Nil).new("a")
Expand All @@ -103,19 +147,50 @@ module Radix
# # => 6
# ```
def key=(@key)
# reset kind on change of key
@kind = Kind::Normal
@priority = compute_priority
end

# Returns `true` if the node key contains a named parameter in it
#
# ```
# node = Radix::Node(Nil).new(":query")
# node.named? # => true
#
# node = Radix::Node(Nil).new("abc")
# node.named? # => false
# ```
def named?
kind.named?
end

# Returns `true` if the node key does not contain an special parameter
# (named or glob)
#
# ```
# node = Radix::Node(Nil).new("a")
# node.normal? # => true
#
# node = Radix::Node(Nil).new(":query")
# node.normal? # => false
# ```
def normal?
kind.normal?
end

# :nodoc:
private def compute_priority
reader = Char::Reader.new(@key)

while reader.has_next?
case reader.current_char
when '*'
return -2
@kind = Kind::Glob
break
when ':'
return -1
@kind = Kind::Named
break
else
reader.next_char
end
Expand All @@ -124,23 +199,9 @@ module Radix
reader.pos
end

# Changes the order of Node's children based on each node priority.
#
# This ensures highest priority nodes are listed before others.
#
# ```
# root = Radix::Node(Nil).new("/")
# root.children << Radix::Node(Nil).new("*filepath") # node.priority => -2
# root.children << Radix::Node(Nil).new(":query") # node.priority => -1
# root.children << Radix::Node(Nil).new("a") # node.priority => 1
# root.children << Radix::Node(Nil).new("bc") # node.priority => 2
# root.sort!
#
# root.children.map &.priority
# # => [2, 1, -1, -2]
# ```
def sort!
@children.sort_by! { |node| -node.priority }
# :nodoc:
protected def sort!
@children.sort!
end
end
end

0 comments on commit 68e21bc

Please sign in to comment.