Skip to content

Commit

Permalink
Merge pull request #12 from Blacksmoke16/route-requirements
Browse files Browse the repository at this point in the history
Param requirements
  • Loading branch information
robacarp committed Jan 4, 2019
2 parents 0142b0c + 766a4e6 commit 15e4a93
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 63 deletions.
54 changes: 30 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Amber/Router

[![Build Status](https://travis-ci.org/amberframework/amber-router.svg?branch=master)](https://travis-ci.org/amberframework/amber-router) A tree based url router with a similar API interface to [radix](luislavena/radix).
[![Build Status](https://travis-ci.org/amberframework/amber-router.svg?branch=master)](https://travis-ci.org/amberframework/amber-router) A tree based url router with a similar API interface to [radix](https://github.com/luislavena/radix).

## Usage

Expand Down Expand Up @@ -31,6 +31,15 @@ route_set.add "/get/posts/*post_name/comments", :wordpress_style
# Supports match-all globs.
route_set.add "/get/*", :catch_all
# Supports `Regex` based argument constraints
route_set.add "/get/posts/:page", :user_path, {"page" => /\d+/}
route_set.add "/get/test/:id", :user_path, {"id" => /foo_\d/}
router.find("/get/posts/1").found? # => true
router.find("/get/posts/foo").found? # => false
router.find("/get/test/foo_7").found? # => true
router.find("/get/test/foo_").found? # => false
# Finding routes from a payload:
Expand All @@ -49,45 +58,42 @@ result.params #=> { "post_name" => "my_trip_to_kansas" }

`crystal run src/benchmark.cr --release` produces a comparison of this router and [radix](https://github.com/luislavena/radix). As of now, this is the comparison:

```
```Text
> crystal run src/benchmark.cr --release
/get/
router: root 1.68M (596.67ns) (± 5.64%) 1.37× slower
radix: root 2.3M (435.57ns) (± 5.08%) fastest
router: root 3.63M (275.23ns) (± 5.58%) 546 B/op 1.44× slower
radix: root 5.25M (190.49ns) (± 4.07%) 320 B/op fastest
/get/books/23/chapters
router: deep 986.19k ( 1.01µs) (± 4.84%) fastest
radix: deep 907.61k ( 1.1µs) (± 7.06%) 1.09× slower
router: deep 1.73M (578.29ns) (± 4.25%) 1040 B/op fastest
radix: deep 1.54M ( 647.9ns) (± 5.46%) 592 B/op 1.12× slower
/get/books/23/pages
router: wrong 1.42M (702.46ns) (± 3.84%) fastest
radix: wrong 1.01M (991.22ns) (± 1.30%) 1.41× slower
router: wrong 2.46M (406.07ns) (± 1.50%) 768 B/op fastest
radix: wrong 1.85M ( 541.1ns) (± 2.09%) 513 B/op 1.33× slower
/get/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z
router: many segments 225.66k ( 4.43µs) (± 3.87%) 5.38× slower
radix: many segments 1.21M ( 824.2ns) (± 1.36%) fastest
router: many segments 475.73k ( 2.1µs) (± 3.78%) 4498 B/op 4.31× slower
radix: many segments 2.05M (488.09ns) (± 1.28%) 448 B/op fastest
/get/var/2/3/4/5/6/7/8/9/0/1/2/3/4/5/6/7/8/9/0/1/2/3/4/5/6
router: many variables 143.56k ( 6.97µs) (± 4.55%) 1.50× slower
radix: many variables 215.03k ( 4.65µs) (± 1.70%) fastest
router: many variables 276.63k ( 3.61µs) (± 4.65%) 6517 B/op 1.44× slower
radix: many variables 397.75k ( 2.51µs) (± 1.67%) 2853 B/op fastest
/get/foobarbizfoobarbizfoobarbizfoobarbizfoobarbizbat/3
router: long_segments 1.13M (885.26ns) (± 2.95%) fastest
radix: long_segments 737.03k ( 1.36µs) (± 3.71%) 1.53× slower
router: long_segments 1.83M (546.36ns) (± 2.03%) 912 B/op fastest
radix: long_segments 1.19M ( 842.9ns) (± 1.46%) 624 B/op 1.54× slower
/post/products/23/reviews/
router: catchall route 1.21M (828.65ns) (± 4.67%) 1.52× slower
radix: catchall route 1.84M (544.43ns) (± 3.36%) fastest
router: catchall route 2.2M (455.48ns) (± 1.13%) 896 B/op 1.66× slower
radix: catchall route 3.65M (274.33ns) (± 4.71%) 449 B/op fastest
/put/products/Winter-Windproof-Trapper-Hat/dp/B01J7DAMCQ
globs with suffix match 667.91k ( 1.5µs) (± 3.89%) fastest
globs with suffix match 1.18M (845.37ns) (± 1.27%) 1489 B/op fastest
Route Constraints
route with a valid constraint 1.84M (544.55ns) (± 1.11%) 912 B/op 1.31× slower
route with an invalid constraint 2.41M (414.72ns) (± 1.25%) 672 B/op fastest
```

## Contributing
Expand Down
3 changes: 2 additions & 1 deletion shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ version: 0.2.0
authors:
- Robert L Carpenter <robert@robacarp.com>

crystal: 0.26.0
crystal: 0.27.0

license: MIT

targets:
benchmark:
main: src/benchmark.cr

44 changes: 33 additions & 11 deletions spec/amber_router/route_set/matching/routes_with_variables_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,42 @@ describe "routes with variables" do

it "correctly selects routes" do
router = build do
add "/get/users/:id", :users
add "/get/users/:id/books", :users_books
add "/get/books/:id", :books
add "/get/users/:id", :users
add "/get/users/:id/books", :users_books
add "/get/books/:id", :books
add "/get/books/:id/chapters", :book_chapters
add "/get/books/:id/authors", :book_authors
add "/get/books/:id/authors", :book_authors
add "/get/books/:id/pictures", :book_pictures
end

router.find("/get/") .payload?.should eq :root
router.find("/get/users/3") .payload?.should eq :users
router.find("/get/users/3/books") .payload?.should eq :users_books
router.find("/get/books/3") .payload?.should eq :books
router.find("/get/books/3/chapters") .payload?.should eq :book_chapters
router.find("/get/books/3/authors") .payload?.should eq :book_authors
router.find("/get/books/3/pictures") .payload?.should eq :book_pictures
router.find("/get/").payload?.should eq :root
router.find("/get/users/3").payload?.should eq :users
router.find("/get/users/3/books").payload?.should eq :users_books
router.find("/get/books/3").payload?.should eq :books
router.find("/get/books/3/chapters").payload?.should eq :book_chapters
router.find("/get/books/3/authors").payload?.should eq :book_authors
router.find("/get/books/3/pictures").payload?.should eq :book_pictures
end

it "routes with constraints" do
router = build do
# With symbol hash
add "/get/posts/:page", :user_path, {:page => /\d+/}

# With string hash
add "/get/test/:id", :user_path, {"id" => /foo_\d/}

# with named tuple
add "/get/time/:id", :user_path, {id: /\d:\d:\d/}
end

router.find("/get/posts/1").found?.should be_true
router.find("/get/posts/foo").found?.should be_false

router.find("/get/time/foo").found?.should be_false
router.find("/get/time/1:2:3").found?.should be_true

router.find("/get/test/foo_7").found?.should be_true
router.find("/get/test/foo_").found?.should be_false
end
end
60 changes: 36 additions & 24 deletions src/amber/router/route_set.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ module Amber::Router
# route_set.add "/get/users/:id/books", :users_books
# route_set.add "/get/*/slug", :slug
# route_set.add "/get/*", :catch_all
# route_set.add "/get/posts/:page", :pages, {"page" => /\d+/}
#
# p route_set.formatted_s # => a textual representation of the routing tree
# route_set.formatted_s # => a textual representation of the routing tree
#
# route_set.find("/get/users/3").payload # => :users
# route_set.find("/get/users/3/books").payload # => :users_books
# route_set.find("/get/coffee_maker/slug").payload # => :slug
# route_set.find("/get/made/up/url").payload # => :catch_all
#
# route_set.find("/get/posts/123").found? # => true
# route_set.find("/get/posts/one").found? # => false
# ```
class RouteSet(T)
@trunk : RouteSet(T)?
Expand All @@ -29,13 +33,13 @@ module Amber::Router
end

# Look for or create a subtree matching a given segment.
private def find_subtree!(segment : String) : Segment(T)
private def find_subtree!(segment : String, constraints : Hash(String, Regex)) : Segment(T)
if subtree = find_subtree segment
subtree
else
case
when segment.starts_with? ':'
new_segment = VariableSegment(T).new(segment)
new_segment = VariableSegment(T).new(segment, constraints[segment.lchop(':')]?)
when segment.starts_with? '*'
new_segment = GlobSegment(T).new(segment)
else
Expand All @@ -60,19 +64,18 @@ module Amber::Router
end

# Add a route to the tree.
def add(path, payload : T) : Nil
if path.includes?("(") || path.includes?(")")
paths = parse_subpaths path
else
paths = [path]
end
def add(path, payload : T, constraints : Hash(String, Regex) = {} of String => Regex) : Nil
add_route path, payload, constraints
end

paths.each do |path|
segments = split_path path
terminal_segment = add(segments, payload, path)
terminal_segment.priority = @insert_count
@insert_count += 1
end
# Add a route to the tree.
def add(path, payload : T, constraints : Hash(Symbol, Regex)) : Nil
add_route path, payload, constraints.transform_keys { |k| k.to_s }
end

# Add a route to the tree.
def add(path, payload : T, constraints : NamedTuple) : Nil
add_route path, payload, constraints.to_h.transform_keys { |k| k.to_s }
end

def parse_subpaths(path : String) : Array(String)
Expand All @@ -81,15 +84,15 @@ module Amber::Router

# Recursively find or create subtrees matching a given path, and store the
# application route at the leaf.
protected def add(url_segments : Array(String), route : T, full_path : String) : TerminalSegment(T)
protected def add(url_segments : Array(String), route : T, full_path : String, constraints : Hash(String, Regex)) : TerminalSegment(T)
unless url_segments.any?
segment = TerminalSegment(T).new(route, full_path)
@segments.push segment
return segment
end

segment = find_subtree! url_segments.shift
segment.route_set.add(url_segments, route, full_path)
segment = find_subtree! url_segments.shift, constraints
segment.route_set.add(url_segments, route, full_path, constraints)
end

def routes? : Bool
Expand All @@ -107,7 +110,6 @@ module Amber::Router
case segment
when TerminalSegment(T)
matches << RoutedResult(T).new segment if accepting_terminal_segments

when FixedSegment(T), VariableSegment(T)
next unless can_recurse
next unless segment.match? path[path_offset]
Expand All @@ -117,7 +119,6 @@ module Amber::Router
matched_route[segment.parameter] = path[path_offset] if segment.parametric?
matches << matched_route
end

when GlobSegment(T)
glob_matches = segment.route_set.reverse_select_routes(path)

Expand All @@ -144,15 +145,13 @@ module Amber::Router
# { array of potential matches, position in path array : Int32)
#
protected def reverse_select_routes(path : Array(String)) : Array(GlobMatch(T))
no_matches = [] of T
matches = [] of GlobMatch(T)

@segments.each do |segment|
case segment
when TerminalSegment
match = GlobMatch(T).new segment, path
matches << match

when FixedSegment, VariableSegment
glob_matches = segment.route_set.reverse_select_routes path

Expand All @@ -166,7 +165,6 @@ module Amber::Router
matches << glob_match
end
end

end
end

Expand Down Expand Up @@ -194,6 +192,21 @@ module Amber::Router
end
end

private def add_route(path, payload : T, constraints : Hash(String, Regex)) : Nil
if path.includes?("(") || path.includes?(")")
paths = parse_subpaths path
else
paths = [path]
end

paths.each do |p|
segments = split_path p
terminal_segment = add(segments, payload, p, constraints)
terminal_segment.priority = @insert_count
@insert_count += 1
end
end

# Split a path by slashes, remove blanks, and compact the path array.
# E.g. split_path("/a/b/c/d") => ["a", "b", "c", "d"]
private def split_path(path : String) : Array(String)
Expand All @@ -202,6 +215,5 @@ module Amber::Router
segment
end.compact
end

end
end
10 changes: 9 additions & 1 deletion src/amber/router/segments/variable_segment.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
module Amber::Router
class VariableSegment(T) < Segment(T)
def initialize(segment, @pattern : Regex? = nil)
super segment
end

def match?(segment : String) : Bool
true
if p = @pattern
!(segment =~ p).nil?
else
true
end
end

def parametric? : Bool
Expand Down
14 changes: 12 additions & 2 deletions src/benchmark.cr
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Benchmarker
}

@amber_routes = {
"/put/products/*slug/dp/:id" => :amazon_style_url
"/put/products/*slug/dp/:id" => :amazon_style_url,
}

@amber_router = Amber::Router::RouteSet(Symbol).new
Expand All @@ -39,6 +39,9 @@ class Benchmarker
@amber_routes.each do |k, v|
amber_router.add k, v
end

# Add a route with a requirement
amber_router.add "/get/test/:id", :requirement_path, {"id" => /foo_\d/}
end

def run_check(router, check, expected_result)
Expand All @@ -65,7 +68,6 @@ class Benchmarker
end

puts
puts
end

def compare_to_radix
Expand All @@ -83,6 +85,14 @@ class Benchmarker
Benchmark.ips do |x|
x.report("globs with suffix match") { run_check(amber_router, "/put/products/Winter-Windproof-Trapper-Hat/dp/B01J7DAMCQ", :amazon_style_url) }
end

puts

puts "Route Constraints"
Benchmark.ips do |x|
x.report("route with a valid constraint") { run_check(amber_router, "/get/test/foo_99", :requirement_path) }
x.report("route with an invalid constraint") { run_check(amber_router, "/get/test/foo_bar", nil) }
end
end
end

Expand Down

0 comments on commit 15e4a93

Please sign in to comment.