Skip to content

Commit

Permalink
Add method router.
Browse files Browse the repository at this point in the history
  • Loading branch information
CathalMullan committed Dec 19, 2024
1 parent 3e9d6b9 commit 9b2489a
Show file tree
Hide file tree
Showing 57 changed files with 65,617 additions and 9,286 deletions.
3 changes: 3 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Architecture

TODO.
53 changes: 53 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# https://doc.rust-lang.org/cargo/reference/workspaces.html
[workspace]
resolver = "2"
members = [".", "examples/*", "fuzz"]
members = [".", "crates/*", "examples/*", "fuzz"]

[workspace.package]
version = "0.7.0"
Expand Down Expand Up @@ -60,10 +60,12 @@ name = "wayfind"
description = "A speedy, flexible router."
include = [
"/benches",
"/crates",
"/examples",
"/fuzz",
"/src",
"/tests",
"/ARCHITECTURE.md",
"/CHANGELOG.md",
"/LICENSE-APACHE",
"/LICENSE-MIT",
Expand All @@ -87,6 +89,8 @@ workspace = true
smallvec = { version = "1.13", features = ["const_generics", "union"] }

[dev-dependencies]
wayfind-rails-macro = { path = "crates/rails-macro" }

# Testing
# NOTE: Keep in sync with `cargo-insta` Nix package.
insta = "=1.41.1"
Expand All @@ -95,6 +99,13 @@ similar-asserts = "1.6"
# Encoding
percent-encoding = "2.3"

# Serde
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Regex
fancy-regex = "0.14"

# Benchmarking
divan = "0.1"
criterion = { version = "0.5", features = ["html_reports"] }
Expand Down
64 changes: 34 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ fn main() -> Result<(), Box<dyn Error>> {
router.insert(&route, 14)?;

insta::assert_snapshot!(router, @r"
=== Authority
Empty
=== Path
/
├─ user [9]
Expand All @@ -475,21 +477,23 @@ fn main() -> Result<(), Box<dyn Error>> {
│ ╰─ /
│ ╰─ {orderId} [8]
╰─ {*catch_all} [14]
=== Method
Empty
=== Chains
1
2
3
4
5
6
7
8
9
10
11
12
13
14
*-1-*
*-2-*
*-3-*
*-4-*
*-5-*
*-6-*
*-7-*
*-8-*
*-9-*
*-10-*
*-11-*
*-12-*
*-13-*
*-14-*
");

Ok(())
Expand Down Expand Up @@ -524,29 +528,29 @@ In a router of 130 routes, benchmark matching 4 paths.

| Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size |
|:-----------------|----------:|------------:|-----------:|--------------:|-------------:|
| wayfind | 422.40 ns | 5 | 329 B | 5 | 329 B |
| matchit | 571.88 ns | 5 | 480 B | 5 | 512 B |
| path-tree | 584.19 ns | 5 | 480 B | 5 | 512 B |
| xitca-router | 652.85 ns | 8 | 864 B | 8 | 896 B |
| ntex-router | 2.2440 µs | 19 | 1.312 KB | 19 | 1.344 KB |
| route-recognizer | 3.1765 µs | 161 | 8.569 KB | 161 | 8.601 KB |
| routefinder | 6.3295 µs | 68 | 5.088 KB | 68 | 5.12 KB |
| actix-router | 22.663 µs | 215 | 14 KB | 215 | 14.03 KB |
| wayfind | 450.39 ns | 4 | 265 B | 4 | 265 B |
| matchit | 559.16 ns | 4 | 416 B | 4 | 448 B |
| path-tree | 570.10 ns | 4 | 416 B | 4 | 448 B |
| xitca-router | 650.12 ns | 7 | 800 B | 7 | 832 B |
| ntex-router | 2.2439 µs | 18 | 1.248 KB | 18 | 1.28 KB |
| route-recognizer | 3.1662 µs | 160 | 8.505 KB | 160 | 8.537 KB |
| routefinder | 6.2237 µs | 67 | 5.024 KB | 67 | 5.056 KB |
| actix-router | 21.072 µs | 214 | 13.93 KB | 214 | 13.96 KB |

#### `path-tree` inspired benches

In a router of 320 routes, benchmark matching 80 paths.

| Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size |
|:-----------------|----------:|------------:|-----------:|--------------:|-------------:|
| wayfind | 5.7526 µs | 60 | 3.847 KB | 60 | 3.847 KB |
| path-tree | 8.6580 µs | 60 | 8.727 KB | 60 | 8.75 KB |
| matchit | 9.9301 µs | 141 | 19.09 KB | 141 | 19.11 KB |
| xitca-router | 11.894 µs | 210 | 26.79 KB | 210 | 26.81 KB |
| ntex-router | 36.102 µs | 202 | 20.82 KB | 202 | 20.84 KB |
| route-recognizer | 69.253 µs | 2873 | 192.9 KB | 2873 | 206.1 KB |
| routefinder | 87.550 µs | 526 | 49.68 KB | 526 | 49.71 KB |
| actix-router | 187.20 µs | 2202 | 130.1 KB | 2202 | 130.1 KB |
| wayfind | 6.2827 µs | 59 | 2.567 KB | 59 | 2.567 KB |
| path-tree | 8.5983 µs | 59 | 7.447 KB | 59 | 7.47 KB |
| matchit | 9.8800 µs | 140 | 17.81 KB | 140 | 17.83 KB |
| xitca-router | 11.930 µs | 209 | 25.51 KB | 209 | 25.53 KB |
| ntex-router | 35.919 µs | 201 | 19.54 KB | 201 | 19.56 KB |
| route-recognizer | 69.604 µs | 2872 | 191.7 KB | 2872 | 204.8 KB |
| routefinder | 87.659 µs | 525 | 48.40 KB | 525 | 48.43 KB |
| actix-router | 187.49 µs | 2201 | 128.8 KB | 2201 | 128.8 KB |

## License

Expand Down
118 changes: 118 additions & 0 deletions Routers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Routers

We take a modular approach to routing, rather than a typical hierarchial approach.

Each sub router will have an ID.
Sub Router IDs will be joined together to create chain, that will be used in a map.
For example, a chain may look like `*-123-99`.
The map is where the data lives, i.e. `T`.

The interface for each router will look like:
- insert -> RouteId
- find -> RouteId or Vec<RouteId>
- delete -> ()
- search -> RouteData

Search is the most critical function, as a typical router will be search MUCH more often than it is edited.
We optimize for search performance where possible.

The only required sub router is `PathRouter`.
All others are optional, and can be skipped.
Skippable sub router IDs will be `Option<usize>`, displayed as `*`.

Chains are built up over time.
1. `AuthorityID` -> `Option<usize>`
2. `PathId` -> `Option<usize>, usize`
3. `MethodId` -> `Option<usize>, usize, Option<usize>`

With this approach, we can filter as we go.
And shortcuts are simple.
Optionals can easily be converted to `None` when skipping a router.

## At a glance.

### 1. Authority Router

A tree, where `.` is the seperator.
Punycode decoding.
Support for dynamics `{name}.com` and wildcards `{*name}.com`.
Support for constraints `{name:my_constraint}.com`.

Most specific match wins.
If a constraint fails, we keep trying to match.

### 2. Path Router

A tree, where `/` is the seperator.
Percent decoding.
Support for dynamics `/{name}` and wildcards `/{*name}`.
Support for optional groups `/({name})`.
Support for constraints `/{name:my_constraint}`.

Most specific match wins.

### 3. Method Router

A bitset over a `u16`.
Custom methods not allowed.

Most specific match wins.

If a method lookup fails, we need to return a 405 error.
This includes a list of what methods actually ARE allowed. (maybe pre-compute this?)

## Inserts

```rust
struct Route {
authority: Option<String>,
path: String,
methods: Option<Vec<String>>
}
```

### 1. Router

1. Take a `Route` as input.
2. If `authority`, call `AuthorityRouter::insert(...)`

### 2. Authority Router

1. Punycode decode input. (error if already decoded/partial)
2. Parse into parts.
3. Generate Authority ID.
4. Walk radix trie, insert or create nodes as go.
5. Reach final part, insert data with ID or return existing data if already exists.

### 3. Router

1. If we called `AuthorityRouter`, expect a Result<Autho>

### 4. Path Router

1. Percent decode input. (error if already decoded/partial)
2. Expand optional routes.
3. Parse into parts
4. Generate Path ID.
5. Walk radix trie per expanded route, insert or create nodes as go.
6. Reach final part, insert data (composite with authority ID) or return existing data if already exists.

### 5. Router

TODO

### 6. Method Router

TODO

### 7. Router

TODO

## Searches

...

## Deletes

...
Loading

0 comments on commit 9b2489a

Please sign in to comment.