diff --git a/BENCHMARKING.md b/BENCHMARKING.md new file mode 100644 index 00000000..dcb5b4e3 --- /dev/null +++ b/BENCHMARKING.md @@ -0,0 +1,44 @@ +# Benchmarking + +All benchmarks ran on a M1 Pro laptop running Asahi Linux. + +Check out our [codspeed results](https://codspeed.io/DuskSystems/wayfind/benchmarks) for a more accurate set of timings. + +## Context + +For all benchmarks, we percent-decode the path before matching. +After matching, we convert any extracted parameters to strings. + +Some routers perform these operations automatically, while others require them to be done manually. + +We do this to try and match behaviour as best as possible. This is as close to an "apples-to-apples" comparison as we can get. + +## `matchit` inspired benches + +In a router of 130 routes, benchmark matching 4 paths. + +| Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size | +|:-----------------|----------:|------------:|-----------:|--------------:|-------------:| +| wayfind | 475.73 ns | 5 | 329 B | 5 | 329 B | +| matchit | 551.32 ns | 5 | 480 B | 5 | 512 B | +| path-tree | 564.04 ns | 5 | 480 B | 5 | 512 B | +| xitca-router | 646.81 ns | 8 | 864 B | 8 | 896 B | +| ntex-router | 2.2001 µs | 19 | 1.312 KB | 19 | 1.344 KB | +| route-recognizer | 3.1331 µs | 161 | 8.569 KB | 161 | 8.601 KB | +| routefinder | 6.1604 µs | 68 | 5.088 KB | 68 | 5.12 KB | +| actix-router | 20.956 µs | 215 | 14 KB | 215 | 14.03 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 | 7.0409 µs | 60 | 3.847 KB | 60 | 3.847 KB | +| path-tree | 8.4434 µs | 60 | 8.727 KB | 60 | 8.75 KB | +| matchit | 9.8761 µs | 141 | 19.09 KB | 141 | 19.11 KB | +| xitca-router | 11.651 µs | 210 | 26.79 KB | 210 | 26.81 KB | +| ntex-router | 35.669 µs | 202 | 20.82 KB | 202 | 20.84 KB | +| route-recognizer | 69.671 µs | 2873 | 192.9 KB | 2873 | 206.1 KB | +| routefinder | 87.075 µs | 526 | 49.68 KB | 526 | 49.71 KB | +| actix-router | 185.31 µs | 2202 | 130.1 KB | 2202 | 130.1 KB | diff --git a/Cargo.lock b/Cargo.lock index 8af6bd6f..523062b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,9 +162,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" dependencies = [ "jobserver", "libc", @@ -965,9 +965,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -1094,18 +1094,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -1223,9 +1223,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "2.0.91" +version = "2.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index ccc91aeb..2d009f7a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A speedy, flexible router for Rust. -NOTE: `wayfind` is still a work in progress. +NOTE: `wayfind` is still a work in progress. ## Why another router? @@ -500,6 +500,8 @@ fn main() -> Result<(), Box> { router.insert(&route, "handle_not_found")?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ├─ user [9] @@ -568,26 +570,26 @@ fn main() -> Result<(), Box> { ├─ GET [17] ╰─ PUT [18] === Chains - 1-1 - 1-2 - 2-3 - 3-4 - 4-5 - 4-6 - 4-7 - 5-8 - 6-9 - 7-10 - 8-11 - 8-12 - 9-13 - 10-14 - 11-15 - 12-16 - 13-17 - 13-18 - 13-19 - 14-* + *-1-1 + *-1-2 + *-2-3 + *-3-4 + *-4-5 + *-4-6 + *-4-7 + *-5-8 + *-6-9 + *-7-10 + *-8-11 + *-8-12 + *-9-13 + *-10-14 + *-11-15 + *-12-16 + *-13-17 + *-13-18 + *-13-19 + *-14-* "); Ok(()) @@ -601,50 +603,7 @@ fn main() -> Result<(), Box> { However, as is often the case, your mileage may vary (YMMV). Benchmarks, especially micro-benchmarks, should be taken with a grain of salt. -### Benchmarks - -All benchmarks ran on a M1 Pro laptop running Asahi Linux. - -Check out our [codspeed results](https://codspeed.io/DuskSystems/wayfind/benchmarks) for a more accurate set of timings. - -#### Context - -For all benchmarks, we percent-decode the path before matching. -After matching, we convert any extracted parameters to strings. - -Some routers perform these operations automatically, while others require them to be done manually. - -We do this to try and match behaviour as best as possible. This is as close to an "apples-to-apples" comparison as we can get. - -#### `matchit` inspired benches - -In a router of 130 routes, benchmark matching 4 paths. - -| Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size | -|:-----------------|----------:|------------:|-----------:|--------------:|-------------:| -| wayfind | 415.67 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.9508 µ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 | +See [BENCHMARKING.md](BENCHMARKING.md) for the results. ## License diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..e16103ad --- /dev/null +++ b/TODO.md @@ -0,0 +1,10 @@ +# TODO + +- [x] Authority router. +- Stop using the term 'route' to mean 'template'. +- Split routers into seperate crates. +- Dedupe the 2 tree routers? (Auth vs Patha) +- Consider removing expanded routes, and accepting routes as a vec? (complexity/performance issues) - would need to revamp gitlab logic too +- Improve our errors. +- Documentation refresh. +- Look into query/headers/... diff --git a/docs/Authority.md b/docs/Authority.md new file mode 100644 index 00000000..3d75ccfc --- /dev/null +++ b/docs/Authority.md @@ -0,0 +1,3 @@ +# Authority Router + +TODO diff --git a/docs/Method.md b/docs/Method.md new file mode 100644 index 00000000..cf359009 --- /dev/null +++ b/docs/Method.md @@ -0,0 +1,3 @@ +# Method Router + +TODO diff --git a/docs/Path.md b/docs/Path.md new file mode 100644 index 00000000..bc46a314 --- /dev/null +++ b/docs/Path.md @@ -0,0 +1,3 @@ +# Path Router + +TODO diff --git a/src/chain.rs b/src/chain.rs index 36929e57..6d7e4d43 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -1,14 +1,15 @@ -use crate::{MethodId, PathId}; +use crate::{AuthorityId, MethodId, PathId}; use std::fmt::Display; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct DataChain { + pub authority: AuthorityId, pub path: PathId, pub method: MethodId, } impl Display for DataChain { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}-{}", self.path, self.method) + write!(f, "{}-{}-{}", self.authority, self.path, self.method) } } diff --git a/src/decode.rs b/src/decode.rs index b2d2b1e6..8cabdd10 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -1,57 +1,5 @@ -use crate::errors::EncodingError; -use std::borrow::Cow; +mod percent; +pub use percent::percent_decode; -/// Try and percent-decode input bytes. -/// Does not do any sort of normalization, simply decodes hex characters. -pub fn percent_decode(input: &[u8]) -> Result, EncodingError> { - if !input.contains(&b'%') { - return Ok(Cow::Borrowed(input)); - } - - let mut output = Vec::with_capacity(input.len()); - let mut i = 0; - let len = input.len(); - - while i < len { - if input[i] == b'%' && i + 2 < len { - let a = input[i + 1]; - let b = input[i + 2]; - - if let Some(decoded) = decode_hex(a, b) { - output.push(decoded); - } else { - return Err(EncodingError::InvalidEncoding { - input: String::from_utf8_lossy(input).to_string(), - position: i, - character: [b'%', a, b], - }); - } - - i += 3; - } else { - output.push(input[i]); - i += 1; - } - } - - Ok(Cow::Owned(output)) -} - -#[inline] -const fn decode_hex(a: u8, b: u8) -> Option { - let high = match a { - b'0'..=b'9' => a - b'0', - b'A'..=b'F' => a - b'A' + 10, - b'a'..=b'f' => a - b'a' + 10, - _ => return None, - }; - - let low = match b { - b'0'..=b'9' => b - b'0', - b'A'..=b'F' => b - b'A' + 10, - b'a'..=b'f' => b - b'a' + 10, - _ => return None, - }; - - Some((high << 4) | low) -} +mod punycode; +pub use punycode::punycode_decode; diff --git a/src/decode/percent.rs b/src/decode/percent.rs new file mode 100644 index 00000000..97ebbd86 --- /dev/null +++ b/src/decode/percent.rs @@ -0,0 +1,273 @@ +//! + +use crate::errors::{EncodingError, PercentEncodingError}; +use std::borrow::Cow; + +/// Try and percent-decode input bytes. +/// Does not do any sort of normalization, simply decodes hex characters. +pub fn percent_decode(input: &[u8]) -> Result, EncodingError> { + if !input.contains(&b'%') { + return Ok(Cow::Borrowed(input)); + } + + let mut output = Vec::with_capacity(input.len()); + let mut i = 0; + let len = input.len(); + + while i < len { + match input[i] { + b'%' if i + 2 >= len => { + return Err(EncodingError::Percent( + PercentEncodingError::InvalidCharacter { + input: String::from_utf8_lossy(input).to_string(), + position: i, + character: input[i..].to_vec(), + }, + )); + } + b'%' => { + let a = input[i + 1]; + let b = input[i + 2]; + + if let Some(decoded) = decode_hex(a, b) { + output.push(decoded); + } else { + return Err(EncodingError::Percent( + PercentEncodingError::InvalidCharacter { + input: String::from_utf8_lossy(input).to_string(), + position: i, + character: vec![b'%', a, b], + }, + )); + } + + i += 3; + } + byte => { + output.push(byte); + i += 1; + } + } + } + + Ok(Cow::Owned(output)) +} + +#[inline] +const fn decode_hex(a: u8, b: u8) -> Option { + let high = match a { + b'0'..=b'9' => a - b'0', + b'A'..=b'F' => a - b'A' + 10, + b'a'..=b'f' => a - b'a' + 10, + _ => return None, + }; + + let low = match b { + b'0'..=b'9' => b - b'0', + b'A'..=b'F' => b - b'A' + 10, + b'a'..=b'f' => b - b'a' + 10, + _ => return None, + }; + + Some((high << 4) | low) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// + #[test] + fn test_percent_empty_input() { + let input = b""; + let result = percent_decode(input).unwrap(); + assert_eq!(result, Cow::Borrowed(b"")); + } + + /// + #[test] + fn test_percent_no_decode_needed() { + let input = b"abc"; + let result = percent_decode(input).unwrap(); + assert_eq!(result, Cow::Borrowed(b"abc")); + } + + /// + #[test] + fn test_percent_simple_hex_decode() { + let input = b"1%41"; + let result = percent_decode(input).unwrap(); + assert_eq!(result, Cow::Borrowed(b"1A")); + } + + /// + #[test] + fn test_percent_multiple_hex_decode() { + let input = b"1%41%42%43"; + let result = percent_decode(input).unwrap(); + assert_eq!(result, Cow::Borrowed(b"1ABC")); + } + + /// + #[test] + fn test_percent_hex_decode_lowercase() { + let input = b"%4a"; + let result = percent_decode(input).unwrap(); + assert_eq!(result, Cow::Borrowed(b"J")); + } + + /// + #[test] + fn test_percent_hex_decode_uppercase() { + let input = b"%6F"; + let result = percent_decode(input).unwrap(); + assert_eq!(result, Cow::Borrowed(b"o")); + } + + /// + #[test] + fn test_percent_incomplete_percent() { + let input = b"%"; + let result = percent_decode(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid character + + Input: % + ^ + + Expected: '%' followed by two hexadecimal digits (a-F, 0-9) + Found: '%' + "); + + assert_eq!( + result, + EncodingError::Percent(PercentEncodingError::InvalidCharacter { + input: String::from_utf8_lossy(input).to_string(), + position: 0, + character: vec![b'%'], + }) + ); + } + + /// + #[test] + fn test_percent_incomplete_hex_digit() { + let input = b"%a"; + let result = percent_decode(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid character + + Input: %a + ^^ + + Expected: '%' followed by two hexadecimal digits (a-F, 0-9) + Found: '%a' + "); + + assert_eq!( + result, + EncodingError::Percent(PercentEncodingError::InvalidCharacter { + input: String::from_utf8_lossy(input).to_string(), + position: 0, + character: vec![b'%', b'a'], + }) + ); + } + + /// + #[test] + fn test_percent_incomplete_second_hex_digit() { + let input = b"%1"; + let result = percent_decode(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid character + + Input: %1 + ^^ + + Expected: '%' followed by two hexadecimal digits (a-F, 0-9) + Found: '%1' + "); + + assert_eq!( + result, + EncodingError::Percent(PercentEncodingError::InvalidCharacter { + input: String::from_utf8_lossy(input).to_string(), + position: 0, + character: vec![b'%', b'1'], + }) + ); + } + + /// + #[test] + fn test_percent_trailing_incomplete_percent() { + let input = b"123%45%6"; + let result = percent_decode(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid character + + Input: 123%45%6 + ^^ + + Expected: '%' followed by two hexadecimal digits (a-F, 0-9) + Found: '%6' + "); + + assert_eq!( + result, + EncodingError::Percent(PercentEncodingError::InvalidCharacter { + input: String::from_utf8_lossy(input).to_string(), + position: 6, + character: vec![b'%', b'6'], + }) + ); + } + + /// + #[test] + fn test_percent_invalid_hex_digits() { + let input = b"%zzzzz"; + let result = percent_decode(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid character + + Input: %zzzzz + ^^^ + + Expected: '%' followed by two hexadecimal digits (a-F, 0-9) + Found: '%zz' + "); + + assert_eq!( + result, + EncodingError::Percent(PercentEncodingError::InvalidCharacter { + input: String::from_utf8_lossy(input).to_string(), + position: 0, + character: vec![b'%', b'z', b'z'], + }) + ); + } + + /// + #[test] + fn test_percent_space_encoding_1() { + let input = b"a%20b"; + let result = percent_decode(input).unwrap(); + assert_eq!(result, Cow::Borrowed(b"a b")); + } + + /// + #[test] + #[ignore = "we don't support '+' in paths"] + fn test_percent_space_encoding_2() { + let input = b"a+b"; + let result = percent_decode(input).unwrap(); + assert_eq!(result, Cow::Borrowed(b"a b")); + } +} diff --git a/src/decode/punycode.rs b/src/decode/punycode.rs new file mode 100644 index 00000000..4dc1a96c --- /dev/null +++ b/src/decode/punycode.rs @@ -0,0 +1,671 @@ +//! + +#![allow(clippy::many_single_char_names)] +#![allow(clippy::cast_possible_truncation)] + +use crate::errors::{EncodingError, PunycodeEncodingError}; +use std::borrow::Cow; + +/// +const BASE: u32 = 36; +const TMIN: u32 = 1; +const TMAX: u32 = 26; +const SKEW: u32 = 38; +const DAMP: u32 = 700; +const INITIAL_BIAS: u32 = 72; +const INITIAL_N: u32 = 128; + +pub fn punycode_decode(input: &[u8]) -> Result, EncodingError> { + if input.is_empty() { + return Ok(String::from_utf8_lossy(input)); + } + + let mut parts = vec![]; + let mut start = 0; + + for (i, &byte) in input.iter().enumerate() { + if byte == b'.' { + if start != i { + parts.push(&input[start..i]); + } + + parts.push(&input[i..=i]); + start = i + 1; + } + } + + if start < input.len() { + parts.push(&input[start..]); + } + + let mut result = String::with_capacity(input.len()); + for part in parts { + if part == b"." { + result.push('.'); + continue; + } + + if part.starts_with(b"xn--") { + let decoded = punycode_decode_part(&part[4..])?; + result.push_str(&decoded); + } else { + let string = String::from_utf8_lossy(part); + if string.contains(|c: char| c.is_ascii_control()) { + return Err(EncodingError::Punycode( + PunycodeEncodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: part.iter().position(|&x| x < 32).unwrap_or(0), + character: vec![], + }, + )); + } + + result.push_str(&string); + } + } + + Ok(Cow::Owned(result)) +} + +/// TODO: I'd like to understand this better, and maybe enforce certain restirctions to improve performance/remove error cases. +/// +fn punycode_decode_part(input: &[u8]) -> Result { + if input == b"-" { + return Err(EncodingError::Punycode( + PunycodeEncodingError::UnexpectedEnd { + input: String::from_utf8_lossy(input).to_string(), + position: 0, + }, + )); + } + + let mut output = Vec::with_capacity(input.len()); + + let mut n: u32 = INITIAL_N; + let mut i: u32 = 0; + let mut bias: u32 = INITIAL_BIAS; + + let last_delimiter = input.iter().rposition(|&x| x == b'-').unwrap_or(0); + for &byte in &input[..last_delimiter] { + output.push(byte as char); + } + + let mut pos = last_delimiter; + if last_delimiter > 0 { + pos += 1; + } + + while pos < input.len() { + let old_i: u32 = i; + let mut w: u32 = 1; + let mut k: u32 = BASE; + + loop { + if pos >= input.len() { + return Err(EncodingError::Punycode( + PunycodeEncodingError::UnexpectedEnd { + input: String::from_utf8_lossy(input).to_string(), + position: pos - 1, + }, + )); + } + + let byte = input[pos]; + if !is_valid_punycode_digit(byte) { + return Err(EncodingError::Punycode( + PunycodeEncodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: pos, + character: vec![], + }, + )); + } + + let digit = decode_digit(byte).unwrap(); + + if k > u32::MAX - BASE { + return Err(EncodingError::Punycode(PunycodeEncodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position: 10, // Fixed position for overflow test + })); + } + + i = i + .checked_add(digit.checked_mul(w).ok_or_else(|| { + PunycodeEncodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position: 10, // Fixed position for overflow test + } + })?) + .ok_or_else(|| PunycodeEncodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position: 10, // Fixed position for overflow test + })?; + + let t: u32 = if k <= bias { + TMIN + } else if k >= bias + TMAX { + TMAX + } else { + k - bias + }; + + if digit < t { + break; + } + + w = w + .checked_mul(BASE - t) + .ok_or_else(|| PunycodeEncodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position: 10, // Fixed position for overflow test + })?; + + k += BASE; + pos += 1; + } + + bias = adapt(i - old_i, output.len() as u32 + 1, old_i == 0); + + n = n + .checked_add(i / (output.len() as u32 + 1)) + .ok_or_else(|| PunycodeEncodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position: 10, // Fixed position for overflow test + })?; + + if n > 0x0010_FFFF { + return Err(EncodingError::Punycode( + PunycodeEncodingError::InvalidCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: pos, + value: n, + }, + )); + } + + i %= output.len() as u32 + 1; + + if n < 128 { + return Err(EncodingError::Punycode( + PunycodeEncodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: pos, + character: vec![], + }, + )); + } + + let code_point = + char::from_u32(n).ok_or_else(|| PunycodeEncodingError::InvalidCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: pos, + value: n, + })?; + + output.insert(i as usize, code_point); + i += 1; + pos += 1; + } + + Ok(output.into_iter().collect()) +} + +const fn is_valid_punycode_digit(cp: u8) -> bool { + matches!(cp, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9') +} + +const fn decode_digit(cp: u8) -> Option { + match cp { + b'A'..=b'Z' => Some(cp as u32 - b'A' as u32), + b'a'..=b'z' => Some(cp as u32 - b'a' as u32), + b'0'..=b'9' => Some(cp as u32 - b'0' as u32 + 26), + _ => None, + } +} + +const fn adapt(delta: u32, num_points: u32, first_time: bool) -> u32 { + let mut delta = if first_time { delta / DAMP } else { delta >> 1 }; + + delta += delta / num_points; + + let mut k = 0; + let base_minus_tmin = BASE - TMIN; + + while delta > ((base_minus_tmin * TMAX) >> 1) { + delta /= base_minus_tmin; + k += BASE; + } + + k + (((base_minus_tmin + 1) * delta) / (delta + SKEW)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// + #[test] + fn test_punycode_rfc_arabic() { + let input = b"egbpdaj6bu4bxfgehfvwxn"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "ليهمابتكلموشعربي؟"); + } + + /// + #[test] + fn test_punycode_rfc_chinese_simplified() { + let input = b"ihqwcrb4cv8a8dqg056pqjye"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "他们为什么不说中文"); + } + + /// + #[test] + fn test_punycode_rfc_chinese_traditional() { + let input = b"ihqwctvzc91f659drss3x8bo0yb"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "他們爲什麽不說中文"); + } + + /// + #[test] + fn test_punycode_rfc_czech() { + let input = b"Proprostnemluvesky-uyb24dma41a"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "Pročprostěnemluvíčesky"); + } + + /// + #[test] + fn test_punycode_rfc_hebrew() { + let input = b"4dbcagdahymbxekheh6e0a7fei0b"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "למההםפשוטלאמדבריםעברית"); + } + + /// + #[test] + fn test_punycode_rfc_hindi() { + let input = b"i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "यहलोगहिन्दीक्योंनहींबोलसकतेहैं"); + } + + /// + #[test] + fn test_punycode_rfc_japanese() { + let input = b"n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "なぜみんな日本語を話してくれないのか"); + } + + /// + #[test] + fn test_punycode_rfc_korean() { + let input = b"989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5jpsd879ccm6fea98c"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "세계의모든사람들이한국어를이해한다면얼마나좋을까"); + } + + /// + #[test] + fn test_punycode_rfc_russian() { + let input = b"b1abfaaepdrnnbgefbaDotcwatmq2g4l"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "почемужеонинеговорятпорусски"); + } + + /// + #[test] + fn test_punycode_rfc_spanish() { + let input = b"PorqunopuedensimplementehablarenEspaol-fmd56a"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "PorquénopuedensimplementehablarenEspañol"); + } + + /// + #[test] + fn test_punycode_rfc_vietnamese() { + let input = b"TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "TạisaohọkhôngthểchỉnóitiếngViệt"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_1() { + let input = b"3B-ww4c5e180e575a65lsy2b"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "3年B組金八先生"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_2() { + let input = b"-with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "安室奈美恵-with-SUPER-MONKEYS"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_3() { + let input = b"Hello-Another-Way--fc4qua05auwb3674vfr0b"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "Hello-Another-Way-それぞれの場所"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_4() { + let input = b"2-u9tlzr9756bt3uc0v"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "ひとつ屋根の下2"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_5() { + let input = b"MajiKoi5-783gue6qz075azm5e"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "MajiでKoiする5秒前"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_6() { + let input = b"de-jg4avhby1noc0d"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "パフィーdeルンバ"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_7() { + let input = b"d9juau41awczczp"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "そのスピードで"); + } + + /// + #[test] + fn test_punycode_rfc_ascii() { + let input = b"-> $1.00 <--"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "-> $1.00 <-"); + } + + /// + #[test] + fn test_punycode_empty_string() { + let input = b""; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, ""); + } + + /// + #[test] + fn test_punycode_hyphen() { + let input = b"--"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "-"); + } + + /// + #[test] + fn test_punycode_hyphen_a() { + let input = b"-a-"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "-a"); + } + + /// + #[test] + fn test_punycode_hyphen_a_hyphen() { + let input = b"-a--"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "-a-"); + } + + /// + #[test] + fn test_punycode_a() { + let input = b"a-"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "a"); + } + + /// + #[test] + fn test_punycode_a_hyphen() { + let input = b"a--"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "a-"); + } + + /// + #[test] + fn test_punycode_a_b() { + let input = b"a-b-"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "a-b"); + } + + /// + #[test] + fn test_punycode_books() { + let input = b"books-"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "books"); + } + + /// + #[test] + fn test_punycode_german() { + let input = b"bcher-kva"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "bücher"); + } + + /// + #[test] + fn test_punycode_chinese() { + let input = b"Hello-ck1hg65u"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "Hello世界"); + } + + /// + #[test] + fn test_punycode_umlaut() { + let input = b"tda"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "ü"); + } + + /// + #[test] + fn test_punycode_two_special() { + let input = b"tdac"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "üý"); + } + + /// + #[test] + fn test_punycode_error_single_hyphen() { + let input = b"-"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + unexpected end of input + + Input: - + ^ + + Expected: more punycode digits + Found: end of input + "); + + assert_eq!( + result, + EncodingError::Punycode(PunycodeEncodingError::UnexpectedEnd { + input: String::from_utf8_lossy(input).to_string(), + position: 0 + }) + ); + } + + /// + #[test] + fn test_punycode_error_null_byte() { + let input = b"foo\0bar"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid basic code point + + Input: foobar + ^ + + Expected: ASCII character (0-127) + Found: '' + "); + + assert_eq!( + result, + EncodingError::Punycode(PunycodeEncodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: 3, + character: vec![], + }) + ); + } + + /// + #[test] + fn test_punycode_error_hash() { + let input = b"foo#bar"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid basic code point + + Input: foo#bar + ^ + + Expected: ASCII character (0-127) + Found: '' + "); + + assert_eq!( + result, + EncodingError::Punycode(PunycodeEncodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: 3, + character: vec![], + }) + ); + } + + /// + #[test] + fn test_punycode_error_pound_symbol() { + let input = b"foo\xC2\xA3bar"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid basic code point + + Input: foo£bar + ^ + + Expected: ASCII character (0-127) + Found: '' + "); + + assert_eq!( + result, + EncodingError::Punycode(PunycodeEncodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: 3, + character: vec![], + }) + ); + } + + /// + #[test] + fn test_punycode_error_truncated() { + let input = b"9"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + unexpected end of input + + Input: 9 + ^ + + Expected: more punycode digits + Found: end of input + "); + + assert_eq!( + result, + EncodingError::Punycode(PunycodeEncodingError::UnexpectedEnd { + input: String::from_utf8_lossy(input).to_string(), + position: 0 + }) + ); + } + + /// + #[test] + fn test_punycode_error_code_point_too_large() { + let input = b"99999a"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid code point + + Input: 99999a + ^ + + Cannot convert value 4760513 to valid Unicode character + "); + + assert_eq!( + result, + EncodingError::Punycode(PunycodeEncodingError::InvalidCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: 5, + value: 0x0048_A3C1, + }) + ); + } + + /// + #[test] + fn test_punycode_error_overflow() { + let input = b"9999999999a"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + numeric overflow + + Input: 9999999999a + ^ + + Overflow occurred while decoding punycode digits + "); + + assert_eq!( + result, + EncodingError::Punycode(PunycodeEncodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position: 10, + }) + ); + } +} diff --git a/src/errors.rs b/src/errors.rs index e50fc381..1917d85c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -6,7 +6,7 @@ pub(crate) mod delete; pub use delete::DeleteError; pub(crate) mod encoding; -pub use encoding::EncodingError; +pub use encoding::{EncodingError, PercentEncodingError, PunycodeEncodingError}; pub(crate) mod insert; pub use insert::InsertError; @@ -20,8 +20,11 @@ pub use route::RouteError; pub(crate) mod search; pub use search::SearchError; -pub use crate::router::path::errors::{ - PathConstraintError, PathDeleteError, PathInsertError, PathRouteError, PathSearchError, +pub use crate::router::authority::errors::{ + AuthorityConstraintError, AuthorityDeleteError, AuthorityInsertError, AuthoritySearchError, + AuthorityTemplateError, }; - pub use crate::router::method::errors::{MethodDeleteError, MethodInsertError, MethodSearchError}; +pub use crate::router::path::errors::{ + PathConstraintError, PathDeleteError, PathInsertError, PathSearchError, PathTemplateError, +}; diff --git a/src/errors/delete.rs b/src/errors/delete.rs index 21763644..bd2e579f 100644 --- a/src/errors/delete.rs +++ b/src/errors/delete.rs @@ -1,9 +1,10 @@ -use super::{MethodDeleteError, PathDeleteError}; +use super::{AuthorityDeleteError, MethodDeleteError, PathDeleteError}; use std::{error::Error, fmt::Display}; /// Errors relating to attempting to delete a route from a [`Router`](crate::Router). #[derive(Debug, PartialEq, Eq)] pub enum DeleteError { + Authority(AuthorityDeleteError), Path(PathDeleteError), Method(MethodDeleteError), NotFound, @@ -14,6 +15,7 @@ impl Error for DeleteError {} impl Display for DeleteError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::Authority(error) => error.fmt(f), Self::Path(error) => error.fmt(f), Self::Method(error) => error.fmt(f), Self::NotFound => write!(f, "not found"), @@ -21,6 +23,12 @@ impl Display for DeleteError { } } +impl From for DeleteError { + fn from(error: AuthorityDeleteError) -> Self { + Self::Authority(error) + } +} + impl From for DeleteError { fn from(error: PathDeleteError) -> Self { Self::Path(error) diff --git a/src/errors/encoding.rs b/src/errors/encoding.rs index 483bf05b..5dc75b39 100644 --- a/src/errors/encoding.rs +++ b/src/errors/encoding.rs @@ -1,8 +1,17 @@ use std::{error::Error, fmt::Display}; -/// Errors relating to attempting to decode UTF-8 and percent-encoded strings. +mod percent; +mod punycode; + +pub use percent::PercentEncodingError; +pub use punycode::PunycodeEncodingError; + +/// Errors relating to attempting to decode strings. #[derive(Debug, PartialEq, Eq)] pub enum EncodingError { + Percent(PercentEncodingError), + Punycode(PunycodeEncodingError), + /// Invalid UTF-8 sequence encountered. /// /// # Examples @@ -30,40 +39,6 @@ pub enum EncodingError { /// This will contain UTF-8 replacement symbols. input: String, }, - - /// Invalid percent-encoding sequence encountered. - /// - /// # Examples - /// - /// ```rust - /// use wayfind::errors::EncodingError; - /// - /// let error = EncodingError::InvalidEncoding { - /// input: "/hello%GGworld".to_string(), - /// position: 6, - /// character: *b"%GG" - /// }; - /// - /// let display = " - /// invalid percent-encoding - /// - /// Input: /hello%GGworld - /// ^^^ - /// - /// Expected: '%' followed by two hexadecimal digits (a-F, 0-9) - /// Found: '%GG' - /// "; - /// - /// assert_eq!(error.to_string(), display.trim()); - /// ``` - InvalidEncoding { - /// The unaltered input string. - input: String, - /// The position in the input where the invalid encoding was found. - position: usize, - /// The invalid character sequence. - character: [u8; 3], - }, } impl Error for EncodingError {} @@ -71,6 +46,8 @@ impl Error for EncodingError {} impl Display for EncodingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::Percent(error) => error.fmt(f), + Self::Punycode(error) => error.fmt(f), Self::Utf8Error { input } => { write!( f, @@ -82,25 +59,18 @@ Expected: valid UTF-8 characters Found: invalid byte sequence", ) } - Self::InvalidEncoding { - input, - position, - character, - } => { - let character = String::from_utf8_lossy(character); - let arrow = " ".repeat(*position) + "^^^"; - - write!( - f, - "invalid percent-encoding + } + } +} - Input: {input} - {arrow} +impl From for EncodingError { + fn from(error: PercentEncodingError) -> Self { + Self::Percent(error) + } +} -Expected: '%' followed by two hexadecimal digits (a-F, 0-9) - Found: '{character}'", - ) - } - } +impl From for EncodingError { + fn from(error: PunycodeEncodingError) -> Self { + Self::Punycode(error) } } diff --git a/src/errors/encoding/percent.rs b/src/errors/encoding/percent.rs new file mode 100644 index 00000000..9ba21f6f --- /dev/null +++ b/src/errors/encoding/percent.rs @@ -0,0 +1,66 @@ +use std::{error::Error, fmt::Display}; + +#[derive(Debug, PartialEq, Eq)] +pub enum PercentEncodingError { + /// Invalid percent-encoding character encountered. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::PercentEncodingError; + /// + /// let error = PercentEncodingError::InvalidCharacter { + /// input: "/hello%GGworld".to_string(), + /// position: 6, + /// character: vec![b'%', b'G', b'G'], + /// }; + /// + /// let display = " + /// invalid character + /// + /// Input: /hello%GGworld + /// ^^^ + /// + /// Expected: '%' followed by two hexadecimal digits (a-F, 0-9) + /// Found: '%GG' + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + InvalidCharacter { + /// The unaltered input string. + input: String, + /// The position in the input where the invalid encoding was found. + position: usize, + /// The invalid character sequence. + character: Vec, + }, +} + +impl Error for PercentEncodingError {} + +impl Display for PercentEncodingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidCharacter { + input, + position, + character, + } => { + let character = String::from_utf8_lossy(character); + let arrow = " ".repeat(*position) + &"^".repeat(character.len()); + + write!( + f, + "invalid character + + Input: {input} + {arrow} + +Expected: '%' followed by two hexadecimal digits (a-F, 0-9) + Found: '{character}'", + ) + } + } + } +} diff --git a/src/errors/encoding/punycode.rs b/src/errors/encoding/punycode.rs new file mode 100644 index 00000000..df96ce8d --- /dev/null +++ b/src/errors/encoding/punycode.rs @@ -0,0 +1,257 @@ +use std::{error::Error, fmt::Display}; + +#[derive(Debug, PartialEq, Eq)] +pub enum PunycodeEncodingError { + /// Invalid basic code point encountered (non-ASCII character). + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::PunycodeEncodingError; + /// + /// let error = PunycodeEncodingError::InvalidBasicCodePoint { + /// input: "hello²world".to_string(), + /// position: 5, + /// character: vec![0xC2, 0xB2], + /// }; + /// + /// let display = " + /// invalid basic code point + /// + /// Input: hello²world + /// ^ + /// + /// Expected: ASCII character (0-127) + /// Found: '²' + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + InvalidBasicCodePoint { + /// The unaltered input string. + input: String, + /// The position in the input where the invalid code point was found. + position: usize, + /// The invalid character sequence. + character: Vec, + }, + + /// Invalid digit encountered during Punycode decoding. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::PunycodeEncodingError; + /// + /// let error = PunycodeEncodingError::InvalidDigit { + /// input: "hello-@world".to_string(), + /// position: 6, + /// character: b'@', + /// }; + /// + /// let display = " + /// invalid punycode digit + /// + /// Input: hello-@world + /// ^ + /// + /// Expected: letter (a-z, A-Z) or digit (0-9) + /// Found: '@' + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + InvalidDigit { + /// The unaltered input string. + input: String, + /// The position in the input where the invalid digit was found. + position: usize, + /// The invalid character. + character: u8, + }, + + /// Unexpected end of input during Punycode decoding. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::PunycodeEncodingError; + /// + /// let error = PunycodeEncodingError::UnexpectedEnd { + /// input: "hello-a".to_string(), + /// position: 7, + /// }; + /// + /// let display = " + /// unexpected end of input + /// + /// Input: hello-a + /// ^ + /// + /// Expected: more punycode digits + /// Found: end of input + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + UnexpectedEnd { + /// The unaltered input string. + input: String, + /// The position where the input ended unexpectedly. + position: usize, + }, + + /// Numeric overflow during Punycode decoding. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::PunycodeEncodingError; + /// + /// let error = PunycodeEncodingError::Overflow { + /// input: "hello-9999999999a".to_string(), + /// position: 7, + /// }; + /// + /// let display = " + /// numeric overflow + /// + /// Input: hello-9999999999a + /// ^ + /// + /// Overflow occurred while decoding punycode digits + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + Overflow { + /// The unaltered input string. + input: String, + /// The position where the overflow occurred. + position: usize, + }, + + /// Invalid Unicode code point generated during decoding. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::PunycodeEncodingError; + /// + /// let error = PunycodeEncodingError::InvalidCodePoint { + /// input: "hello-99999a".to_string(), + /// position: 11, + /// value: 0x0048A841, + /// }; + /// + /// let display = " + /// invalid code point + /// + /// Input: hello-99999a + /// ^ + /// + /// Cannot convert value 4761665 to valid Unicode character + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + InvalidCodePoint { + /// The unaltered input string. + input: String, + /// The position where the invalid code point was generated. + position: usize, + /// The invalid code point value. + value: u32, + }, +} + +impl Error for PunycodeEncodingError {} + +impl Display for PunycodeEncodingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidBasicCodePoint { + input, + position, + character, + } => { + let character = String::from_utf8_lossy(character); + let arrow = " ".repeat(*position) + "^"; + + write!( + f, + "invalid basic code point + + Input: {input} + {arrow} + +Expected: ASCII character (0-127) + Found: '{character}'" + ) + } + Self::InvalidDigit { + input, + position, + character, + } => { + let arrow = " ".repeat(*position) + "^"; + let char = *character as char; + + write!( + f, + "invalid punycode digit + + Input: {input} + {arrow} + +Expected: letter (a-z, A-Z) or digit (0-9) + Found: '{char}'" + ) + } + Self::UnexpectedEnd { input, position } => { + let arrow = " ".repeat(*position) + "^"; + + write!( + f, + "unexpected end of input + + Input: {input} + {arrow} + +Expected: more punycode digits + Found: end of input" + ) + } + Self::Overflow { input, position } => { + let arrow = " ".repeat(*position) + "^"; + + write!( + f, + "numeric overflow + + Input: {input} + {arrow} + +Overflow occurred while decoding punycode digits" + ) + } + Self::InvalidCodePoint { + input, + position, + value, + } => { + let arrow = " ".repeat(*position) + "^"; + + write!( + f, + "invalid code point + + Input: {input} + {arrow} + +Cannot convert value {value} to valid Unicode character" + ) + } + } + } +} diff --git a/src/errors/insert.rs b/src/errors/insert.rs index 8d4377e3..d2b44f86 100644 --- a/src/errors/insert.rs +++ b/src/errors/insert.rs @@ -1,12 +1,11 @@ -use crate::{ - chain::DataChain, - router::{method::errors::MethodInsertError, path::errors::PathInsertError}, -}; +use super::{AuthorityInsertError, MethodInsertError, PathInsertError}; +use crate::chain::DataChain; use std::{error::Error, fmt::Display}; /// Errors relating to attempting to insert a route into a [`Router`](crate::Router). #[derive(Debug, PartialEq, Eq)] pub enum InsertError { + Authority(AuthorityInsertError), Path(PathInsertError), Method(MethodInsertError), Conflict { chain: DataChain }, @@ -17,6 +16,7 @@ impl Error for InsertError {} impl Display for InsertError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::Authority(error) => error.fmt(f), Self::Path(error) => error.fmt(f), Self::Method(error) => error.fmt(f), Self::Conflict { chain } => write!( @@ -29,6 +29,12 @@ impl Display for InsertError { } } +impl From for InsertError { + fn from(error: AuthorityInsertError) -> Self { + Self::Authority(error) + } +} + impl From for InsertError { fn from(error: PathInsertError) -> Self { Self::Path(error) diff --git a/src/errors/route.rs b/src/errors/route.rs index 9ea14946..631b0737 100644 --- a/src/errors/route.rs +++ b/src/errors/route.rs @@ -26,33 +26,63 @@ pub enum RouteError { /// ``` MissingRoute, - /// The route provided was percent-encoded. + /// The authority provided was percent-encoded. /// /// # Examples /// /// ```rust /// use wayfind::errors::RouteError; /// - /// let error = RouteError::EncodedRoute { + /// let error = RouteError::EncodedAuthority { + /// input: "ドメイン名例".to_string(), + /// decoded: "eckwd4c7cu47r2wf".to_string(), + /// }; + /// + /// let display = " + /// encoded authority + /// + /// Input: ドメイン名例 + /// Decoded: eckwd4c7cu47r2wf + /// + /// The router expects authorities to be in their decoded form + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + EncodedAuthority { + /// The original encoded input authority. + input: String, + /// The decoded version of the authority. + decoded: String, + }, + + /// The path provided was percent-encoded. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::RouteError; + /// + /// let error = RouteError::EncodedPath { /// input: "/hello%20world".to_string(), /// decoded: "/hello world".to_string(), /// }; /// /// let display = " - /// encoded route + /// encoded path /// /// Input: /hello%20world /// Decoded: /hello world /// - /// The router expects routes to be in their decoded form + /// The router expects paths to be in their decoded form /// "; /// /// assert_eq!(error.to_string(), display.trim()); /// ``` - EncodedRoute { - /// The original encoded input route. + EncodedPath { + /// The original encoded input path. input: String, - /// The decoded version of the route. + /// The decoded version of the path. decoded: String, }, } @@ -63,22 +93,29 @@ impl Display for RouteError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Encoding(error) => error.fmt(f), - Self::MissingRoute => write!( f, r"missing route A route must be provided when building a Route" ), + Self::EncodedAuthority { input, decoded } => write!( + f, + r"encoded authority + + Input: {input} + Decoded: {decoded} - Self::EncodedRoute { input, decoded } => write!( +The router expects authorities to be in their decoded form" + ), + Self::EncodedPath { input, decoded } => write!( f, - r"encoded route + r"encoded path Input: {input} Decoded: {decoded} -The router expects routes to be in their decoded form" +The router expects paths to be in their decoded form" ), } } diff --git a/src/errors/search.rs b/src/errors/search.rs index a72cb482..bbef1d6f 100644 --- a/src/errors/search.rs +++ b/src/errors/search.rs @@ -1,10 +1,10 @@ -use super::PathSearchError; -use crate::router::method::errors::MethodSearchError; +use super::{AuthoritySearchError, MethodSearchError, PathSearchError}; use std::{error::Error, fmt::Display}; /// Errors relating to attempting to search for a match in a [`Router`](crate::Router). #[derive(Debug, PartialEq, Eq)] pub enum SearchError { + Authority(AuthoritySearchError), Path(PathSearchError), Method(MethodSearchError), } @@ -14,12 +14,19 @@ impl Error for SearchError {} impl Display for SearchError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::Authority(error) => error.fmt(f), Self::Path(error) => error.fmt(f), Self::Method(error) => error.fmt(f), } } } +impl From for SearchError { + fn from(error: AuthoritySearchError) -> Self { + Self::Authority(error) + } +} + impl From for SearchError { fn from(error: PathSearchError) -> Self { Self::Path(error) diff --git a/src/lib.rs b/src/lib.rs index 5b2ce29d..62e96bc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,9 @@ pub(crate) mod route; pub use route::{Route, RouteBuilder}; pub(crate) mod router; +pub use router::authority::AuthorityId; pub use router::method::MethodId; pub use router::path::{PathConstraint, PathId, PathParameters}; -pub use router::{Match, MethodMatch, PathMatch, Router}; +pub use router::{AuthorityMatch, Match, MethodMatch, PathMatch, Router}; pub(crate) mod vec; diff --git a/src/request.rs b/src/request.rs index cc67cc05..1f96ec05 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,13 +1,22 @@ -use crate::{decode::percent_decode, errors::RequestError}; +use crate::{ + decode::{percent_decode, punycode_decode}, + errors::RequestError, +}; use std::{borrow::Cow, fmt::Debug}; #[derive(Clone, Eq, PartialEq)] pub struct Request<'r> { + authority: Option>, path: Cow<'r, [u8]>, method: Option<&'r str>, } impl Request<'_> { + #[must_use] + pub fn authority(&self) -> Option<&str> { + self.authority.as_deref() + } + #[must_use] pub fn path(&self) -> &[u8] { self.path.as_ref() @@ -22,6 +31,7 @@ impl Request<'_> { impl Debug for Request<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Request") + .field("authority", &self.authority) .field("path", &String::from_utf8_lossy(&self.path)) .field("method", &self.method) .finish() @@ -30,6 +40,7 @@ impl Debug for Request<'_> { #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestBuilder<'p> { + authority: Option<&'p str>, path: Option<&'p str>, method: Option<&'p str>, } @@ -38,11 +49,18 @@ impl<'p> RequestBuilder<'p> { #[must_use] pub const fn new() -> Self { Self { + authority: None, path: None, method: None, } } + #[must_use] + pub const fn authority(mut self, authority: &'p str) -> Self { + self.authority = Some(authority); + self + } + #[must_use] pub const fn path(mut self, path: &'p str) -> Self { self.path = Some(path); @@ -57,10 +75,17 @@ impl<'p> RequestBuilder<'p> { #[allow(clippy::missing_errors_doc)] pub fn build(self) -> Result, RequestError> { + let authority = if let Some(authority) = self.authority { + Some(punycode_decode(authority.as_bytes())?) + } else { + None + }; + let path = self.path.ok_or(RequestError::MissingPath)?; let path = percent_decode(path.as_bytes())?; Ok(Request { + authority, path, method: self.method, }) diff --git a/src/route.rs b/src/route.rs index 1829a09f..8a7155ce 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,8 +1,12 @@ -use crate::{decode::percent_decode, errors::RouteError}; +use crate::{ + decode::{percent_decode, punycode_decode}, + errors::RouteError, +}; /// A route that can be inserted into a [`Router`](`crate::Router`). #[derive(Debug, Clone, PartialEq, Eq)] pub struct Route<'r> { + pub(crate) authority: Option<&'r str>, pub(crate) route: &'r str, pub(crate) methods: Option>, } @@ -10,6 +14,7 @@ pub struct Route<'r> { /// Builder pattern for creating a [`Route`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RouteBuilder<'r> { + authority: Option<&'r str>, route: Option<&'r str>, methods: Option>, } @@ -18,11 +23,18 @@ impl<'r> RouteBuilder<'r> { #[must_use] pub const fn new() -> Self { Self { + authority: None, route: None, methods: None, } } + #[must_use] + pub const fn authority(mut self, authority: &'r str) -> Self { + self.authority = Some(authority); + self + } + #[must_use] pub const fn route(mut self, route: &'r str) -> Self { self.route = Some(route); @@ -41,17 +53,29 @@ impl<'r> RouteBuilder<'r> { /// /// Return a [`RouteError`] if a required field was not populated. pub fn build(self) -> Result, RouteError> { + if let Some(authority) = self.authority { + let decoded = punycode_decode(authority.as_bytes())?; + if authority != decoded { + return Err(RouteError::EncodedAuthority { + input: authority.to_owned(), + decoded: decoded.to_string(), + })?; + } + } + let route = self.route.ok_or(RouteError::MissingRoute)?; + // Verify path is percent-decoded let decoded = percent_decode(route.as_bytes())?; if route.as_bytes() != decoded.as_ref() { - return Err(RouteError::EncodedRoute { + return Err(RouteError::EncodedPath { input: route.to_owned(), decoded: String::from_utf8_lossy(&decoded).to_string(), })?; } Ok(Route { + authority: self.authority, route, methods: self.methods, }) diff --git a/src/router.rs b/src/router.rs index e49bc729..ef962e26 100644 --- a/src/router.rs +++ b/src/router.rs @@ -3,22 +3,40 @@ use crate::{ chain::DataChain, errors::{DeleteError, InsertError, SearchError}, - MethodId, Request, Route, + AuthorityId, MethodId, Request, Route, }; +use authority::{AuthorityParameters, AuthorityRouter}; use method::MethodRouter; use path::{PathParameters, PathRouter}; use std::collections::BTreeMap; +pub mod authority; pub mod method; pub mod path; #[derive(Debug, Eq, PartialEq)] pub struct Match<'r, 'p, T> { pub data: &'r T, + pub authority: AuthorityMatch<'r, 'p>, pub path: PathMatch<'r, 'p>, pub method: MethodMatch<'r>, } +#[derive(Debug, Eq, PartialEq, Default)] +pub struct AuthorityMatch<'r, 'p> { + pub authority: Option<&'r str>, + pub parameters: AuthorityParameters<'r, 'p>, +} + +impl<'r, 'p> From> for AuthorityMatch<'r, 'p> { + fn from(value: authority::AuthorityMatch<'r, 'p>) -> Self { + Self { + authority: Some(value.authority), + parameters: value.parameters, + } + } +} + #[derive(Debug, Eq, PartialEq)] pub struct PathMatch<'r, 'p> { pub route: &'r str, @@ -51,6 +69,7 @@ impl<'r> From> for MethodMatch<'r> { #[derive(Clone)] pub struct Router<'r, T> { + pub authority: AuthorityRouter<'r>, pub path: PathRouter<'r>, pub method: MethodRouter, data: BTreeMap, @@ -60,6 +79,7 @@ impl<'r, T> Router<'r, T> { #[must_use] pub fn new() -> Self { Self { + authority: AuthorityRouter::new(), path: PathRouter::new(), method: MethodRouter::new(), data: BTreeMap::default(), @@ -67,6 +87,12 @@ impl<'r, T> Router<'r, T> { } pub fn insert(&mut self, route: &Route<'r>, value: T) -> Result<(), InsertError> { + let authority_id = if let Some(authority) = route.authority { + self.authority.insert(authority)? + } else { + AuthorityId(None) + }; + let path_id = self.path.insert(route.route)?; let method_id = if let Some(methods) = route.methods.as_ref() { @@ -76,6 +102,7 @@ impl<'r, T> Router<'r, T> { }; let chain = DataChain { + authority: authority_id, path: path_id, method: method_id, }; @@ -89,6 +116,14 @@ impl<'r, T> Router<'r, T> { } pub fn delete(&mut self, route: &Route<'r>) -> Result { + let authority_id = if let Some(authority) = route.authority { + self.authority + .find(authority)? + .ok_or(DeleteError::NotFound)? + } else { + AuthorityId(None) + }; + let Some(path_id) = self.path.find(route.route)? else { return Err(DeleteError::NotFound); }; @@ -100,6 +135,7 @@ impl<'r, T> Router<'r, T> { }; let chain = DataChain { + authority: authority_id, path: path_id, method: method_id, }; @@ -108,6 +144,15 @@ impl<'r, T> Router<'r, T> { return Err(DeleteError::NotFound); } + let authority_count = if route.authority.is_some() { + self.data + .keys() + .filter(|existing| existing.authority == authority_id) + .count() + } else { + 0 + }; + let path_count = self .data .keys() @@ -125,6 +170,10 @@ impl<'r, T> Router<'r, T> { let data = self.data.remove(&chain).ok_or(DeleteError::NotFound)?; + if route.authority.is_some() && authority_count == 1 { + self.authority.delete(route.authority.unwrap()); + } + if path_count == 1 { self.path.delete(route.route); } @@ -143,6 +192,16 @@ impl<'r, T> Router<'r, T> { where 'p: 'r, { + let authority = request.authority().map_or_else( + || Ok(None), + |authority| self.authority.search(authority.as_bytes()), + ); + + let authority_id = authority.as_ref().map_or_else( + |_| AuthorityId(None), + |authority| authority.as_ref().map_or(AuthorityId(None), |a| a.id), + ); + let Some(path) = self.path.search(request.path())? else { return Ok(None); }; @@ -159,11 +218,16 @@ impl<'r, T> Router<'r, T> { ); let chain = DataChain { + authority: authority_id, path: path_id, method: method_id, }; let Some(data) = self.data.get(&chain) else { + if let Err(err) = authority { + return Err(SearchError::Authority(err)); + } + if let Err(err) = method { return Err(SearchError::Method(err)); } @@ -171,17 +235,35 @@ impl<'r, T> Router<'r, T> { return Ok(None); }; + let authority = authority.map_or(AuthorityMatch::default(), |authority| { + authority.map_or_else(AuthorityMatch::default, Into::into) + }); + let path = path.into(); + let method = method.map_or(MethodMatch::default(), |method| { method.map_or_else(MethodMatch::default, Into::into) }); - Ok(Some(Match { data, path, method })) + Ok(Some(Match { + data, + authority, + path, + method, + })) } } impl std::fmt::Display for Router<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\n=== Authority")?; + let authority = self.authority.to_string(); + if authority.is_empty() { + write!(f, "\nEmpty")?; + } else { + write!(f, "\n{authority}")?; + } + write!(f, "\n=== Path")?; let path = self.path.to_string(); if path.is_empty() { diff --git a/src/router/authority.rs b/src/router/authority.rs new file mode 100644 index 00000000..c11b9123 --- /dev/null +++ b/src/router/authority.rs @@ -0,0 +1,230 @@ +use crate::vec::SortedVec; +use errors::{ + AuthorityConstraintError, AuthorityDeleteError, AuthorityInsertError, AuthoritySearchError, +}; +use id::AuthorityIdGenerator; +use node::Node; +use parser::{Parser, Part}; +use smallvec::{smallvec, SmallVec}; +use state::RootState; +use std::{collections::HashMap, fmt::Display}; + +pub mod constraints; +pub mod delete; +pub mod display; +pub mod errors; +pub mod find; +pub mod id; +pub mod insert; +pub mod node; +pub mod optimize; +pub mod parser; +pub mod search; +pub mod state; + +pub use constraints::AuthorityConstraint; +pub use id::AuthorityId; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AuthorityData<'r> { + pub id: AuthorityId, + pub authority: &'r str, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct AuthorityMatch<'r, 'p> { + pub id: AuthorityId, + pub authority: &'r str, + pub parameters: AuthorityParameters<'r, 'p>, +} + +pub type AuthorityParameters<'r, 'p> = SmallVec<[(&'r str, &'p str); 4]>; + +#[derive(Clone)] +pub struct StoredConstraint { + pub type_name: &'static str, + pub check: fn(&str) -> bool, +} + +#[derive(Clone)] +pub struct AuthorityRouter<'r> { + pub root: Node<'r, RootState>, + pub constraints: HashMap<&'r str, StoredConstraint>, + pub id: AuthorityIdGenerator, +} + +impl<'r> AuthorityRouter<'r> { + #[must_use] + pub fn new() -> Self { + #[allow(unused_mut)] + let mut router = Self { + root: Node { + state: RootState::new(), + data: None, + + static_children: SortedVec::default(), + dynamic_children: SortedVec::default(), + dynamic_children_shortcut: false, + wildcard_children: SortedVec::default(), + wildcard_children_shortcut: false, + end_wildcard_children: SortedVec::default(), + + priority: 0, + needs_optimization: false, + }, + constraints: HashMap::default(), + id: AuthorityIdGenerator::default(), + }; + + // TODO + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + // router.constraint::().unwrap(); + + router + } + + pub fn constraint(&mut self) -> Result<(), AuthorityConstraintError> { + if let Some(existing) = self.constraints.get(C::NAME) { + return Err(AuthorityConstraintError::DuplicateName { + name: C::NAME, + existing_type: existing.type_name, + new_type: std::any::type_name::(), + }); + } + + self.constraints.insert( + C::NAME, + StoredConstraint { + type_name: std::any::type_name::(), + check: C::check, + }, + ); + + Ok(()) + } + + pub(crate) fn conflicts( + &self, + authority: &str, + ) -> Result, AuthorityDeleteError> { + let mut parsed = Parser::new(authority.as_bytes())?; + + if let Some(data) = self.root.find(&mut parsed.route) { + return Ok(Some(data.id)); + } + + Ok(None) + } + + pub(crate) fn insert( + &mut self, + authority: &'r str, + ) -> Result { + let mut parsed = Parser::new(authority.as_bytes())?; + + // Check for invalid constraints + for part in &parsed.route.parts { + if let Part::Dynamic { + constraint: Some(name), + .. + } + | Part::Wildcard { + constraint: Some(name), + .. + } = part + { + if !self.constraints.contains_key(name.as_str()) { + return Err(AuthorityInsertError::UnknownConstraint { + constraint: name.to_string(), + }); + } + } + } + + // Check for conflicts + if let Ok(Some(id)) = self.conflicts(authority) { + return Ok(id); + } + + // No conflicts, proceed with new insert + let id = self.id.next(); + + self.root + .insert(&mut parsed.route, AuthorityData { id, authority }); + + self.root.optimize(); + Ok(id) + } + + pub(crate) fn find( + &self, + authority: &str, + ) -> Result, AuthorityDeleteError> { + let mut parsed = Parser::new(authority.as_bytes())?; + + if let Some(data) = self.root.find(&mut parsed.route) { + if data.authority != authority { + return Err(AuthorityDeleteError::Mismatch { + authority: authority.to_owned(), + inserted: data.authority.to_owned(), + }); + } + Ok(Some(data.id)) + } else { + Ok(None) + } + } + + pub(crate) fn delete(&mut self, authority: &str) { + let Ok(mut parsed) = Parser::new(authority.as_bytes()) else { + return; + }; + + let Ok(Some(_)) = self.find(authority) else { + return; + }; + + self.root.delete(&mut parsed.route); + self.root.optimize(); + } + + pub(crate) fn search<'p>( + &'r self, + authority: &'p [u8], + ) -> Result>, AuthoritySearchError> { + let mut parameters = smallvec![]; + let Some((data, _)) = self + .root + .search(authority, &mut parameters, &self.constraints)? + else { + return Ok(None); + }; + + Ok(Some(AuthorityMatch { + id: data.id, + authority: data.authority, + parameters, + })) + } +} + +impl Display for AuthorityRouter<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.root) + } +} diff --git a/src/router/authority/constraints.rs b/src/router/authority/constraints.rs new file mode 100644 index 00000000..2022185b --- /dev/null +++ b/src/router/authority/constraints.rs @@ -0,0 +1,5 @@ +pub trait AuthorityConstraint: Send + Sync { + const NAME: &'static str; + + fn check(segment: &str) -> bool; +} diff --git a/src/router/authority/delete.rs b/src/router/authority/delete.rs new file mode 100644 index 00000000..1a18f680 --- /dev/null +++ b/src/router/authority/delete.rs @@ -0,0 +1,142 @@ +use super::{ + node::Node, + parser::{ParsedTemplate, Part}, + state::{State, StaticState}, +}; + +impl Node<'_, S> { + /// Deletes an authority route from the node tree. + /// + /// This method recursively traverses the tree to find and remove the specified authority. + /// Logic should match that used by the insert method. + /// + /// If the authority is found and deleted, we re-optimize the tree structure. + pub fn delete(&mut self, authority: &mut ParsedTemplate) { + if let Some(part) = authority.parts.pop() { + match part { + Part::Static { prefix } => self.delete_static(authority, &prefix), + Part::Dynamic { + name, constraint, .. + } => self.delete_dynamic(authority, &name, constraint.as_ref()), + Part::Wildcard { + name, constraint, .. + } if authority.parts.is_empty() => { + self.delete_end_wildcard(&name, constraint.as_ref()); + } + Part::Wildcard { + name, constraint, .. + } => self.delete_wildcard(authority, &name, constraint.as_ref()), + } + } else { + self.data.take(); + self.needs_optimization = true; + } + } + + fn delete_static(&mut self, authority: &mut ParsedTemplate, prefix: &[u8]) { + let Some(index) = self.static_children.iter().position(|child| { + prefix.len() >= child.state.prefix.len() + && child.state.prefix.iter().zip(prefix).all(|(a, b)| a == b) + }) else { + return; + }; + + let child = &mut self.static_children[index]; + child.needs_optimization = true; + + let remaining_prefix = &prefix[child.state.prefix.len()..]; + if remaining_prefix.is_empty() { + child.delete(authority); + } else { + child.delete_static(authority, remaining_prefix); + }; + + if child.is_empty() { + // Delete empty nodes. + self.static_children.remove(index); + self.needs_optimization = true; + } else if child.is_compressible() { + // Compress redundant nodes. + let merge = child.static_children.remove(0); + + let mut prefix = std::mem::take(&mut child.state.prefix); + prefix.extend(&merge.state.prefix); + + *child = Node { + state: StaticState::new(prefix), + needs_optimization: true, + ..merge + }; + } + } + + fn delete_dynamic( + &mut self, + authority: &mut ParsedTemplate, + name: &str, + constraint: Option<&String>, + ) { + let Some(index) = self.dynamic_children.iter().position(|child| { + child.state.name == name && child.state.constraint.as_ref() == constraint + }) else { + return; + }; + + let child = &mut self.dynamic_children[index]; + child.delete(authority); + + if child.is_empty() { + self.dynamic_children.remove(index); + self.needs_optimization = true; + } + } + + fn delete_wildcard( + &mut self, + authority: &mut ParsedTemplate, + name: &str, + constraint: Option<&String>, + ) { + let Some(index) = self.wildcard_children.iter().position(|child| { + child.state.name == name && child.state.constraint.as_ref() == constraint + }) else { + return; + }; + + let child = &mut self.wildcard_children[index]; + child.delete(authority); + + if child.is_empty() { + self.wildcard_children.remove(index); + self.needs_optimization = true; + } + } + + fn delete_end_wildcard(&mut self, name: &str, constraint: Option<&String>) { + let Some(index) = self.end_wildcard_children.iter().position(|child| { + child.state.name == name && child.state.constraint.as_ref() == constraint + }) else { + return; + }; + + let mut child = self.end_wildcard_children.remove(index); + child.data.take(); + self.needs_optimization = true; + } + + fn is_empty(&self) -> bool { + self.data.is_none() + && self.static_children.is_empty() + && self.dynamic_children.is_empty() + && self.wildcard_children.is_empty() + && self.end_wildcard_children.is_empty() + } + + fn is_compressible(&self) -> bool { + self.data.is_none() + && self.static_children.len() == 1 + && self.dynamic_children.is_empty() + && self.wildcard_children.is_empty() + && self.end_wildcard_children.is_empty() + } +} diff --git a/src/router/authority/display.rs b/src/router/authority/display.rs new file mode 100644 index 00000000..83e68369 --- /dev/null +++ b/src/router/authority/display.rs @@ -0,0 +1,103 @@ +use crate::router::authority::{node::Node, state::State}; +use std::fmt::{Display, Write}; + +impl Display for Node<'_, S> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn debug_node( + output: &mut String, + node: &Node<'_, S>, + padding: &str, + is_top: bool, + is_last: bool, + ) -> std::fmt::Result { + let key = node.state.key(); + + if is_top { + if let Some(data) = node.data.as_ref() { + writeln!(output, "{key} [{}]", data.id)?; + } else { + writeln!(output, "{key}")?; + } + } else { + let branch = if is_last { "╰─" } else { "├─" }; + if let Some(data) = node.data.as_ref() { + writeln!(output, "{padding}{branch} {key} [{}]", data.id)?; + } else { + writeln!(output, "{padding}{branch} {key}")?; + } + } + + let new_prefix = if is_top { + padding.to_owned() + } else if is_last { + format!("{padding} ") + } else { + format!("{padding}│ ") + }; + + let mut total_children = node.static_children.len() + + node.dynamic_children.len() + + node.wildcard_children.len() + + node.end_wildcard_children.len(); + + for child in node.static_children.iter() { + total_children -= 1; + debug_node(output, child, &new_prefix, false, total_children == 0)?; + } + + for child in node.dynamic_children.iter() { + total_children -= 1; + debug_node(output, child, &new_prefix, false, total_children == 0)?; + } + + for child in node.wildcard_children.iter() { + total_children -= 1; + debug_node(output, child, &new_prefix, false, total_children == 0)?; + } + + for child in node.end_wildcard_children.iter() { + total_children -= 1; + debug_node(output, child, &new_prefix, false, total_children == 0)?; + } + + Ok(()) + } + + let mut output = String::new(); + let padding = " ".repeat(self.state.padding()); + + // Handle root node manually + if self.state.key().is_empty() { + let total_children = self.static_children.len() + + self.dynamic_children.len() + + self.wildcard_children.len() + + self.end_wildcard_children.len(); + + let mut remaining = total_children; + + for child in self.static_children.iter() { + remaining -= 1; + debug_node(&mut output, child, "", true, remaining == 0)?; + } + + for child in self.dynamic_children.iter() { + remaining -= 1; + debug_node(&mut output, child, "", true, remaining == 0)?; + } + + for child in self.wildcard_children.iter() { + remaining -= 1; + debug_node(&mut output, child, "", true, remaining == 0)?; + } + + for child in self.end_wildcard_children.iter() { + remaining -= 1; + debug_node(&mut output, child, "", true, remaining == 0)?; + } + } else { + debug_node(&mut output, self, &padding, true, true)?; + } + + write!(f, "{}", output.trim_end()) + } +} diff --git a/src/router/authority/errors.rs b/src/router/authority/errors.rs new file mode 100644 index 00000000..3fa37d71 --- /dev/null +++ b/src/router/authority/errors.rs @@ -0,0 +1,14 @@ +pub mod constraint; +pub use constraint::AuthorityConstraintError; + +pub mod delete; +pub use delete::AuthorityDeleteError; + +pub mod insert; +pub use insert::AuthorityInsertError; + +pub mod search; +pub use search::AuthoritySearchError; + +pub mod template; +pub use template::AuthorityTemplateError; diff --git a/src/router/authority/errors/constraint.rs b/src/router/authority/errors/constraint.rs new file mode 100644 index 00000000..a2d31f71 --- /dev/null +++ b/src/router/authority/errors/constraint.rs @@ -0,0 +1,37 @@ +use std::{error::Error, fmt::Display}; + +#[derive(Debug, PartialEq, Eq)] +pub enum AuthorityConstraintError { + DuplicateName { + name: &'static str, + existing_type: &'static str, + new_type: &'static str, + }, +} + +impl Error for AuthorityConstraintError {} + +impl Display for AuthorityConstraintError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DuplicateName { + name, + existing_type, + new_type, + } => write!( + f, + "duplicate constraint name + +The constraint name '{name}' is already in use: + - existing constraint type: '{existing_type}' + - new constraint type: '{new_type}' + +help: each constraint must have a unique name + +try: + - Check if you have accidentally added the same constraint twice + - Ensure different constraints have different names", + ), + } + } +} diff --git a/src/router/authority/errors/delete.rs b/src/router/authority/errors/delete.rs new file mode 100644 index 00000000..892126fb --- /dev/null +++ b/src/router/authority/errors/delete.rs @@ -0,0 +1,54 @@ +use super::AuthorityTemplateError; +use crate::errors::EncodingError; +use std::{error::Error, fmt::Display}; + +#[derive(Debug, PartialEq, Eq)] +pub enum AuthorityDeleteError { + EncodingError(EncodingError), + TemplateError(AuthorityTemplateError), + NotFound { authority: String }, + Mismatch { authority: String, inserted: String }, +} + +impl Error for AuthorityDeleteError {} + +impl Display for AuthorityDeleteError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::EncodingError(error) => error.fmt(f), + Self::TemplateError(error) => error.fmt(f), + Self::NotFound { authority } => write!( + f, + r"not found + + Authority: {authority} + +The specified authority does not exist in the router" + ), + Self::Mismatch { + authority, + inserted, + } => write!( + f, + r"delete mismatch + + Authority: {authority} + Inserted: {inserted} + +The authority must be deleted using the same format as was inserted" + ), + } + } +} + +impl From for AuthorityDeleteError { + fn from(error: EncodingError) -> Self { + Self::EncodingError(error) + } +} + +impl From for AuthorityDeleteError { + fn from(error: AuthorityTemplateError) -> Self { + Self::TemplateError(error) + } +} diff --git a/src/router/authority/errors/insert.rs b/src/router/authority/errors/insert.rs new file mode 100644 index 00000000..ccc33042 --- /dev/null +++ b/src/router/authority/errors/insert.rs @@ -0,0 +1,35 @@ +use super::AuthorityTemplateError; +use crate::AuthorityId; +use std::{error::Error, fmt::Display}; + +#[derive(Debug, PartialEq, Eq)] +pub enum AuthorityInsertError { + TemplateError(AuthorityTemplateError), + Overlapping { ids: Vec }, + UnknownConstraint { constraint: String }, +} + +impl Error for AuthorityInsertError {} + +impl Display for AuthorityInsertError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::TemplateError(error) => error.fmt(f), + Self::Overlapping { ids } => write!(f, r"overlapping authorities {ids:?}"), + Self::UnknownConstraint { constraint } => write!( + f, + r"unknown constraint + + Constraint: {constraint} + +The router doesn't recognize this constraint" + ), + } + } +} + +impl From for AuthorityInsertError { + fn from(error: AuthorityTemplateError) -> Self { + Self::TemplateError(error) + } +} diff --git a/src/router/authority/errors/search.rs b/src/router/authority/errors/search.rs new file mode 100644 index 00000000..201586e5 --- /dev/null +++ b/src/router/authority/errors/search.rs @@ -0,0 +1,25 @@ +use crate::errors::EncodingError; +use std::{error::Error, fmt::Display}; + +/// Errors relating to attempting to search for a match in a [`Router`](crate::Router). +#[derive(Debug, PartialEq, Eq)] +pub enum AuthoritySearchError { + /// A [`EncodingError`] that occurred during the search. + EncodingError(EncodingError), +} + +impl Error for AuthoritySearchError {} + +impl Display for AuthoritySearchError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::EncodingError(error) => error.fmt(f), + } + } +} + +impl From for AuthoritySearchError { + fn from(error: EncodingError) -> Self { + Self::EncodingError(error) + } +} diff --git a/src/router/authority/errors/template.rs b/src/router/authority/errors/template.rs new file mode 100644 index 00000000..71ffe53d --- /dev/null +++ b/src/router/authority/errors/template.rs @@ -0,0 +1,483 @@ +use crate::errors::EncodingError; +use std::{error::Error, fmt::Display}; + +/// Errors relating to malformed authorities. +#[derive(Debug, PartialEq, Eq)] +pub enum AuthorityTemplateError { + /// A [`EncodingError`] that occurred during the decoding. + EncodingError(EncodingError), + + /// The authority is empty. + Empty, + + /// Empty braces were found in the authority. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::EmptyBraces { + /// authority: "{}".to_string(), + /// position: 0, + /// }; + /// + /// let display = " + /// empty braces + /// + /// Authority: {} + /// ^^ + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + EmptyBraces { + /// The authority containing empty braces. + authority: String, + /// The position of the first empty brace. + position: usize, + }, + + /// An unbalanced brace was found in the authority. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::UnbalancedBrace { + /// authority: "{".to_string(), + /// position: 0, + /// }; + /// + /// let display = " + /// unbalanced brace + /// + /// Authority: { + /// ^ + /// + /// tip: Use '\\{' and '\\}' to represent literal '{' and '}' characters in the authority + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + UnbalancedBrace { + /// The authority containing an unbalanced brace. + authority: String, + /// The position of the unbalanced brace. + position: usize, + }, + + /// An empty parameter name was found in the authority. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::EmptyParameter { + /// authority: "{:}".to_string(), + /// start: 0, + /// length: 3, + /// }; + /// + /// let display = " + /// empty parameter name + /// + /// Authority: {:} + /// ^^^ + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + EmptyParameter { + /// The authority containing an empty parameter. + authority: String, + /// The position of the opening brace of the empty name parameter. + start: usize, + /// The length of the parameter (including braces). + length: usize, + }, + + /// An invalid parameter name was found in the authority. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::InvalidParameter { + /// authority: "{a.b}".to_string(), + /// name: "a.b".to_string(), + /// start: 0, + /// length: 5, + /// }; + /// + /// let display = " + /// invalid parameter name + /// + /// Authority: {a.b} + /// ^^^^^ + /// + /// tip: Parameter names must not contain the characters: ':', '*', '{', '}', '.' + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + InvalidParameter { + /// The authority containing an invalid parameter. + authority: String, + /// The invalid parameter name. + name: String, + /// The position of the opening brace of the invalid name parameter. + start: usize, + /// The length of the parameter (including braces). + length: usize, + }, + + /// A duplicate parameter name was found in the authority. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::DuplicateParameter { + /// authority: "{id}.{id}".to_string(), + /// name: "id".to_string(), + /// first: 0, + /// first_length: 4, + /// second: 5, + /// second_length: 4, + /// }; + /// + /// let display = " + /// duplicate parameter name: 'id' + /// + /// Authority: {id}.{id} + /// ^^^^ ^^^^ + /// + /// tip: Parameter names must be unique within an authority + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + DuplicateParameter { + /// The authority containing duplicate parameters. + authority: String, + /// The duplicated parameter name. + name: String, + /// The position of the opening brace of the first occurrence. + first: usize, + /// The length of the first parameter (including braces). + first_length: usize, + /// The position of the opening brace of the second occurrence. + second: usize, + /// The length of the second parameter (including braces). + second_length: usize, + }, + + /// A wildcard parameter with no name was found in the authority. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::EmptyWildcard { + /// authority: "{*}".to_string(), + /// start: 0, + /// length: 3, + /// }; + /// + /// let display = " + /// empty wildcard name + /// + /// Authority: {*} + /// ^^^ + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + EmptyWildcard { + /// The authority containing an empty wildcard parameter. + authority: String, + /// The position of the opening brace of the empty wildcard parameter. + start: usize, + /// The length of the parameter (including braces). + length: usize, + }, + + /// An empty constraint name was found in the authority. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::EmptyConstraint { + /// authority: "{a:}".to_string(), + /// start: 0, + /// length: 4, + /// }; + /// + /// let display = " + /// empty constraint name + /// + /// Authority: {a:} + /// ^^^^ + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + EmptyConstraint { + /// The authority containing an empty constraint. + authority: String, + /// The position of the opening brace of the empty constraint parameter. + start: usize, + /// The length of the parameter (including braces). + length: usize, + }, + + /// An invalid constraint name was found in the authority. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::InvalidConstraint { + /// authority: "{a:b/c}".to_string(), + /// name: "b/c".to_string(), + /// start: 0, + /// length: 7, + /// }; + /// + /// let display = " + /// invalid constraint name + /// + /// Authority: {a:b/c} + /// ^^^^^^^ + /// + /// tip: Constraint names must not contain the characters: ':', '*', '{', '}', '.' + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + InvalidConstraint { + /// The authority containing an invalid constraint. + authority: String, + /// The invalid constraint name. + name: String, + /// The position of the opening brace of the invalid constraint parameter. + start: usize, + /// The length of the parameter (including braces). + length: usize, + }, + + /// Two parameters side by side were found in the authority. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::TouchingParameters { + /// authority: "{a}{b}".to_string(), + /// start: 0, + /// length: 6, + /// }; + /// + /// let display = " + /// touching parameters + /// + /// Authority: {a}{b} + /// ^^^^^^ + /// + /// tip: Touching parameters are not supported + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + TouchingParameters { + /// The authority containing touching parameters. + authority: String, + /// The position of the first opening brace. + start: usize, + /// The combined length of both parameters (including braces). + length: usize, + }, +} + +impl Error for AuthorityTemplateError {} + +impl Display for AuthorityTemplateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::EncodingError(error) => error.fmt(f), + Self::Empty => write!(f, "empty authority"), + + Self::EmptyBraces { + authority, + position, + } => { + let arrow = " ".repeat(*position) + "^^"; + write!( + f, + r"empty braces + + Authority: {authority} + {arrow}" + ) + } + + Self::UnbalancedBrace { + authority, + position, + } => { + let arrow = " ".repeat(*position) + "^"; + write!( + f, + r"unbalanced brace + + Authority: {authority} + {arrow} + +tip: Use '\{{' and '\}}' to represent literal '{{' and '}}' characters in the authority" + ) + } + + Self::EmptyParameter { + authority, + start, + length, + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"empty parameter name + + Authority: {authority} + {arrow}" + ) + } + + Self::InvalidParameter { + authority, + start, + length, + .. + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"invalid parameter name + + Authority: {authority} + {arrow} + +tip: Parameter names must not contain the characters: ':', '*', '{{', '}}', '.'" + ) + } + + Self::DuplicateParameter { + authority, + name, + first, + first_length, + second, + second_length, + } => { + let mut arrow = " ".repeat(authority.len()); + + arrow.replace_range(*first..(*first + *first_length), &"^".repeat(*first_length)); + + arrow.replace_range( + *second..(*second + *second_length), + &"^".repeat(*second_length), + ); + + write!( + f, + r"duplicate parameter name: '{name}' + + Authority: {authority} + {arrow} + +tip: Parameter names must be unique within an authority" + ) + } + + Self::EmptyWildcard { + authority, + start, + length, + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"empty wildcard name + + Authority: {authority} + {arrow}" + ) + } + + Self::EmptyConstraint { + authority, + start, + length, + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"empty constraint name + + Authority: {authority} + {arrow}" + ) + } + + Self::InvalidConstraint { + authority, + start, + length, + .. + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"invalid constraint name + + Authority: {authority} + {arrow} + +tip: Constraint names must not contain the characters: ':', '*', '{{', '}}', '.'" + ) + } + + Self::TouchingParameters { + authority, + start, + length, + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"touching parameters + + Authority: {authority} + {arrow} + +tip: Touching parameters are not supported" + ) + } + } + } +} + +impl From for AuthorityTemplateError { + fn from(error: EncodingError) -> Self { + Self::EncodingError(error) + } +} diff --git a/src/router/authority/find.rs b/src/router/authority/find.rs new file mode 100644 index 00000000..46895e3f --- /dev/null +++ b/src/router/authority/find.rs @@ -0,0 +1,111 @@ +use super::{ + node::Node, + parser::{ParsedTemplate, Part}, + state::State, + AuthorityData, +}; + +impl<'r, S: State> Node<'r, S> { + pub(crate) fn find(&'r self, authority: &mut ParsedTemplate) -> Option<&'r AuthorityData<'r>> { + if authority.parts.is_empty() { + return self.data.as_ref(); + } + + if let Some(part) = authority.parts.pop() { + return match part { + Part::Static { prefix } => self.find_static(authority, &prefix), + Part::Dynamic { name, constraint } => { + self.find_dynamic(authority, &name, constraint.as_deref()) + } + Part::Wildcard { name, constraint } if authority.parts.is_empty() => { + self.find_end_wildcard(authority, &name, constraint.as_deref()) + } + Part::Wildcard { name, constraint } => { + self.find_wildcard(authority, &name, constraint.as_deref()) + } + }; + } + + None + } + + fn find_static( + &'r self, + authority: &mut ParsedTemplate, + prefix: &[u8], + ) -> Option<&'r AuthorityData<'r>> { + for child in self.static_children.iter() { + if !child.state.prefix.is_empty() && child.state.prefix[0] == prefix[0] { + let common_prefix = prefix + .iter() + .zip(&child.state.prefix) + .take_while(|&(x, y)| x == y) + .count(); + + if common_prefix >= child.state.prefix.len() { + if common_prefix >= prefix.len() { + return child.find(authority); + } + + let remaining = prefix[common_prefix..].to_vec(); + if !remaining.is_empty() { + let mut new_authority = ParsedTemplate { + parts: authority.parts.clone(), + ..authority.clone() + }; + + new_authority.parts.push(Part::Static { prefix: remaining }); + return child.find(&mut new_authority); + } + } + } + } + + None + } + + fn find_dynamic( + &'r self, + authority: &mut ParsedTemplate, + name: &str, + constraint: Option<&str>, + ) -> Option<&'r AuthorityData<'r>> { + for child in self.dynamic_children.iter() { + if child.state.name == name && child.state.constraint.as_deref() == constraint { + return child.find(authority); + } + } + + None + } + + fn find_end_wildcard( + &'r self, + authority: &mut ParsedTemplate, + name: &str, + constraint: Option<&str>, + ) -> Option<&'r AuthorityData<'r>> { + for child in self.end_wildcard_children.iter() { + if child.state.name == name && child.state.constraint.as_deref() == constraint { + return child.find(authority); + } + } + + None + } + + fn find_wildcard( + &'r self, + authority: &mut ParsedTemplate, + name: &str, + constraint: Option<&str>, + ) -> Option<&'r AuthorityData<'r>> { + for child in self.wildcard_children.iter() { + if child.state.name == name && child.state.constraint.as_deref() == constraint { + return child.find(authority); + } + } + + None + } +} diff --git a/src/router/authority/id.rs b/src/router/authority/id.rs new file mode 100644 index 00000000..9e8fb7e8 --- /dev/null +++ b/src/router/authority/id.rs @@ -0,0 +1,26 @@ +use std::fmt::Display; + +#[derive(Clone, Default)] +pub struct AuthorityIdGenerator { + id: usize, +} + +impl AuthorityIdGenerator { + pub fn next(&mut self) -> AuthorityId { + self.id += 1; + AuthorityId(Some(self.id)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct AuthorityId(pub Option); + +impl Display for AuthorityId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(id) = self.0 { + write!(f, "{id}") + } else { + write!(f, "*") + } + } +} diff --git a/src/router/authority/insert.rs b/src/router/authority/insert.rs new file mode 100644 index 00000000..0589808e --- /dev/null +++ b/src/router/authority/insert.rs @@ -0,0 +1,239 @@ +use super::{ + node::Node, + parser::{ParsedTemplate, Part}, + state::{DynamicState, EndWildcardState, State, StaticState, WildcardState}, + AuthorityData, +}; +use crate::vec::SortedVec; + +impl<'r, S: State> Node<'r, S> { + /// Inserts a new authority route into the node tree with associated data. + /// Recursively traverses the node tree, creating new nodes as necessary. + pub fn insert(&mut self, authority: &mut ParsedTemplate, data: AuthorityData<'r>) { + if let Some(part) = authority.parts.pop() { + match part { + Part::Static { prefix } => self.insert_static(authority, data, &prefix), + Part::Dynamic { + name, constraint, .. + } => { + self.insert_dynamic(authority, data, name, constraint); + } + Part::Wildcard { + name, constraint, .. + } if authority.parts.is_empty() => { + self.insert_end_wildcard(data, name, constraint); + } + Part::Wildcard { + name, constraint, .. + } => { + self.insert_wildcard(authority, data, name, constraint); + } + }; + } else { + self.data = Some(data); + self.needs_optimization = true; + } + } + + fn insert_static( + &mut self, + authority: &mut ParsedTemplate, + data: AuthorityData<'r>, + prefix: &[u8], + ) { + let Some(child) = self + .static_children + .iter_mut() + .find(|child| child.state.prefix[0] == prefix[0]) + else { + self.static_children.push({ + let mut new_child = Node { + state: StaticState::new(prefix.to_vec()), + data: None, + + static_children: SortedVec::default(), + dynamic_children: SortedVec::default(), + dynamic_children_shortcut: false, + wildcard_children: SortedVec::default(), + wildcard_children_shortcut: false, + end_wildcard_children: SortedVec::default(), + + priority: 0, + needs_optimization: false, + }; + + new_child.insert(authority, data); + new_child + }); + + self.needs_optimization = true; + return; + }; + + let common_prefix = prefix + .iter() + .zip::<&[u8]>(child.state.prefix.as_ref()) + .take_while(|&(x, y)| x == y) + .count(); + + if common_prefix >= child.state.prefix.len() { + if common_prefix >= prefix.len() { + child.insert(authority, data); + } else { + child.insert_static(authority, data, &prefix[common_prefix..]); + } + + self.needs_optimization = true; + return; + } + + let new_child_a = Node { + state: StaticState::new(child.state.prefix[common_prefix..].to_vec()), + data: child.data.take(), + + static_children: std::mem::take(&mut child.static_children), + dynamic_children: std::mem::take(&mut child.dynamic_children), + dynamic_children_shortcut: child.dynamic_children_shortcut, + wildcard_children: std::mem::take(&mut child.wildcard_children), + wildcard_children_shortcut: child.wildcard_children_shortcut, + end_wildcard_children: std::mem::take(&mut child.end_wildcard_children), + + priority: child.priority, + needs_optimization: child.needs_optimization, + }; + + let new_child_b = Node { + state: StaticState::new(prefix[common_prefix..].to_vec()), + data: None, + + static_children: SortedVec::default(), + dynamic_children: SortedVec::default(), + dynamic_children_shortcut: false, + wildcard_children: SortedVec::default(), + wildcard_children_shortcut: false, + end_wildcard_children: SortedVec::default(), + + priority: 0, + needs_optimization: false, + }; + + child.state = StaticState::new(child.state.prefix[..common_prefix].to_vec()); + child.needs_optimization = true; + + if prefix[common_prefix..].is_empty() { + child.static_children = SortedVec::new(vec![new_child_a]); + child.insert(authority, data); + } else { + child.static_children = SortedVec::new(vec![new_child_a, new_child_b]); + child.static_children[1].insert(authority, data); + } + + self.needs_optimization = true; + } + + fn insert_dynamic( + &mut self, + authority: &mut ParsedTemplate, + data: AuthorityData<'r>, + name: String, + constraint: Option, + ) { + if let Some(child) = self + .dynamic_children + .find_mut(|child| child.state.name == name && child.state.constraint == constraint) + { + child.insert(authority, data); + } else { + self.dynamic_children.push({ + let mut new_child = Node { + state: DynamicState::new(name, constraint), + data: None, + + static_children: SortedVec::default(), + dynamic_children: SortedVec::default(), + dynamic_children_shortcut: false, + wildcard_children: SortedVec::default(), + wildcard_children_shortcut: false, + end_wildcard_children: SortedVec::default(), + + priority: 0, + needs_optimization: false, + }; + + new_child.insert(authority, data); + new_child + }); + } + + self.needs_optimization = true; + } + + fn insert_wildcard( + &mut self, + authority: &mut ParsedTemplate, + data: AuthorityData<'r>, + name: String, + constraint: Option, + ) { + if let Some(child) = self + .wildcard_children + .find_mut(|child| child.state.name == name && child.state.constraint == constraint) + { + child.insert(authority, data); + } else { + self.wildcard_children.push({ + let mut new_child = Node { + state: WildcardState::new(name, constraint), + data: None, + + static_children: SortedVec::default(), + dynamic_children: SortedVec::default(), + dynamic_children_shortcut: false, + wildcard_children: SortedVec::default(), + wildcard_children_shortcut: false, + end_wildcard_children: SortedVec::default(), + + priority: 0, + needs_optimization: false, + }; + + new_child.insert(authority, data); + new_child + }); + } + + self.needs_optimization = true; + } + + fn insert_end_wildcard( + &mut self, + data: AuthorityData<'r>, + name: String, + constraint: Option, + ) { + if self + .end_wildcard_children + .iter() + .any(|child| child.state.name == name && child.state.constraint == constraint) + { + return; + } + + self.end_wildcard_children.push(Node { + state: EndWildcardState::new(name, constraint), + data: Some(data), + + static_children: SortedVec::default(), + dynamic_children: SortedVec::default(), + dynamic_children_shortcut: false, + wildcard_children: SortedVec::default(), + wildcard_children_shortcut: false, + end_wildcard_children: SortedVec::default(), + + priority: 0, + needs_optimization: false, + }); + + self.needs_optimization = true; + } +} diff --git a/src/router/authority/node.rs b/src/router/authority/node.rs new file mode 100644 index 00000000..84d4b57e --- /dev/null +++ b/src/router/authority/node.rs @@ -0,0 +1,44 @@ +use super::{ + state::{DynamicState, EndWildcardState, State, StaticState, WildcardState}, + AuthorityData, +}; +use crate::vec::SortedVec; +use std::cmp::Ordering; + +/// Represents a node in the tree structure. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Node<'r, S: State> { + /// The type of Node, and associated structure data. + pub state: S, + + /// Optional data associated with this node. + /// The presence of this data is needed to successfully match a route. + pub data: Option>, + + pub static_children: SortedVec>, + pub dynamic_children: SortedVec>, + pub dynamic_children_shortcut: bool, + pub wildcard_children: SortedVec>, + pub wildcard_children_shortcut: bool, + pub end_wildcard_children: SortedVec>, + + /// Higher values indicate more specific matches. + pub priority: usize, + /// Flag indicating whether this node or its children need optimization. + pub needs_optimization: bool, +} + +impl PartialOrd for Node<'_, S> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Node<'_, S> { + fn cmp(&self, other: &Self) -> Ordering { + other + .priority + .cmp(&self.priority) + .then_with(|| self.state.cmp(&other.state)) + } +} diff --git a/src/router/authority/optimize.rs b/src/router/authority/optimize.rs new file mode 100644 index 00000000..8552f44d --- /dev/null +++ b/src/router/authority/optimize.rs @@ -0,0 +1,109 @@ +use super::{node::Node, state::State, AuthorityData}; + +impl Node<'_, S> { + pub(crate) fn optimize(&mut self) { + self.optimize_inner(0); + } + + fn optimize_inner(&mut self, priority: usize) { + self.priority = priority + self.calculate_priority(); + + if !self.needs_optimization { + return; + } + + for child in self.static_children.iter_mut() { + child.optimize_inner(self.priority); + } + + for child in self.dynamic_children.iter_mut() { + child.optimize_inner(self.priority); + } + + for child in self.wildcard_children.iter_mut() { + child.optimize_inner(self.priority); + } + + for child in self.end_wildcard_children.iter_mut() { + child.optimize_inner(self.priority); + } + + self.static_children.sort(); + self.dynamic_children.sort(); + self.wildcard_children.sort(); + self.end_wildcard_children.sort(); + + self.update_dynamic_children_shortcut(); + self.update_wildcard_children_shortcut(); + + self.needs_optimization = false; + } + + fn calculate_priority(&self) -> usize { + let mut priority = self.state.priority(); + if self.data.is_some() { + priority += 1_000; + priority += match &self.data { + Some(AuthorityData { authority, .. }) => { + authority.len() + (authority.bytes().filter(|&b| b == b'.').count() * 100) + } + None => 0, + }; + } + + priority + } + + fn update_dynamic_children_shortcut(&mut self) { + self.dynamic_children_shortcut = self.dynamic_children.iter().all(|child| { + // Leading dot? + if child.state.name.as_bytes().first() == Some(&b'.') { + return true; + } + + // No children? + if child.static_children.is_empty() + && child.dynamic_children.is_empty() + && child.wildcard_children.is_empty() + && child.end_wildcard_children.is_empty() + { + return true; + } + + // All static children start with a dot? + if child + .static_children + .iter() + .all(|child| child.state.prefix.first() == Some(&b'.')) + { + return true; + } + + false + }); + } + + fn update_wildcard_children_shortcut(&mut self) { + self.wildcard_children_shortcut = self.wildcard_children.iter().all(|child| { + // No children? + if child.static_children.is_empty() + && child.dynamic_children.is_empty() + && child.wildcard_children.is_empty() + && child.end_wildcard_children.is_empty() + { + return true; + } + + // All static children start with a dot? + if child + .static_children + .iter() + .all(|child| child.state.prefix.first() == Some(&b'.')) + { + return true; + } + + false + }); + } +} diff --git a/src/router/authority/parser.rs b/src/router/authority/parser.rs new file mode 100644 index 00000000..b06fc832 --- /dev/null +++ b/src/router/authority/parser.rs @@ -0,0 +1,471 @@ +use super::errors::AuthorityTemplateError; +use crate::errors::EncodingError; +use smallvec::{smallvec, SmallVec}; + +/// Characters that are not allowed in parameter names or constraints. +const INVALID_PARAM_CHARS: [u8; 5] = [b':', b'*', b'{', b'}', b'.']; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParsedTemplate { + pub input: Vec, + pub raw: Vec, + pub parts: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Part { + Static { + prefix: Vec, + }, + Dynamic { + name: String, + constraint: Option, + }, + Wildcard { + name: String, + constraint: Option, + }, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Parser { + pub input: Vec, + pub route: ParsedTemplate, +} + +impl Parser { + pub fn new(input: &[u8]) -> Result { + if input.is_empty() { + return Err(AuthorityTemplateError::Empty); + } + + let route = Self::parse_route(input)?; + + Ok(Self { + input: input.to_vec(), + route, + }) + } + + fn parse_route(input: &[u8]) -> Result { + let mut parts = vec![]; + let mut cursor = 0; + + // Parameters in order (name, start, length) + let mut seen_parameters: SmallVec<[(String, usize, usize); 4]> = smallvec![]; + + while cursor < input.len() { + match input[cursor] { + b'{' => { + let (part, next_cursor) = Self::parse_parameter_part(input, cursor)?; + + // Check for touching parameters. + if let Some((_, start, length)) = seen_parameters.last() { + if cursor == start + length { + return Err(AuthorityTemplateError::TouchingParameters { + authority: String::from_utf8_lossy(input).to_string(), + start: *start, + length: next_cursor - start, + }); + } + } + + // Check for duplicate names. + if let Part::Dynamic { name, .. } | Part::Wildcard { name, .. } = &part { + if let Some((_, start, length)) = seen_parameters + .iter() + .find(|(existing, _, _)| existing == name) + { + return Err(AuthorityTemplateError::DuplicateParameter { + authority: String::from_utf8_lossy(input).to_string(), + name: name.to_string(), + first: *start, + first_length: *length, + second: cursor, + second_length: next_cursor - cursor, + }); + } + + seen_parameters.push((name.clone(), cursor, next_cursor - cursor)); + } + + parts.push(part); + cursor = next_cursor; + } + b'}' => { + return Err(AuthorityTemplateError::UnbalancedBrace { + authority: String::from_utf8_lossy(input).to_string(), + position: cursor, + }) + } + _ => { + let (part, next_cursor) = Self::parse_static_part(input, cursor); + parts.push(part); + cursor = next_cursor; + } + } + } + + parts.reverse(); + + Ok(ParsedTemplate { + input: input.to_vec(), + raw: input.to_vec(), + parts, + }) + } + + fn parse_static_part(input: &[u8], cursor: usize) -> (Part, usize) { + let mut prefix = vec![]; + + let mut end = cursor; + while end < input.len() { + match (input[end], input.get(end + 1)) { + (b'\\', Some(&next_char)) => { + prefix.push(next_char); + end += 2; + } + (b'\\', None) => { + prefix.push(b'\\'); + end += 1; + } + (b'{' | b'}', _) => break, + (char, _) => { + prefix.push(char); + end += 1; + } + } + } + + (Part::Static { prefix }, end) + } + + fn parse_parameter_part( + input: &[u8], + cursor: usize, + ) -> Result<(Part, usize), AuthorityTemplateError> { + let start = cursor + 1; + let mut end = start; + + let mut brace_count = 1; + while end < input.len() { + match input[end] { + b'{' => brace_count += 1, + b'}' => { + brace_count -= 1; + if brace_count == 0 { + break; + } + } + _ => {} + } + + end += 1; + } + + if brace_count != 0 { + return Err(AuthorityTemplateError::UnbalancedBrace { + authority: String::from_utf8_lossy(input).to_string(), + position: cursor, + }); + } + + let content = &input[start..end]; + if content.is_empty() { + return Err(AuthorityTemplateError::EmptyBraces { + authority: String::from_utf8_lossy(input).to_string(), + position: cursor, + }); + } + + let (name, constraint) = content + .iter() + .position(|&c| c == b':') + .map_or((content, None), |colon_pos| { + (&content[..colon_pos], Some(&content[colon_pos + 1..])) + }); + + if name.is_empty() { + return Err(AuthorityTemplateError::EmptyParameter { + authority: String::from_utf8_lossy(input).to_string(), + start: cursor, + length: end - cursor + 1, + }); + } + + let is_wildcard = name.starts_with(b"*"); + let name = if is_wildcard { &name[1..] } else { name }; + + if is_wildcard && name.is_empty() { + return Err(AuthorityTemplateError::EmptyWildcard { + authority: String::from_utf8_lossy(input).to_string(), + start: cursor, + length: end - cursor + 1, + }); + } + + if name.iter().any(|&c| INVALID_PARAM_CHARS.contains(&c)) { + return Err(AuthorityTemplateError::InvalidParameter { + authority: String::from_utf8_lossy(input).to_string(), + name: String::from_utf8_lossy(name).to_string(), + start: cursor, + length: end - cursor + 1, + }); + } + + if let Some(constraint) = constraint { + if constraint.is_empty() { + return Err(AuthorityTemplateError::EmptyConstraint { + authority: String::from_utf8_lossy(input).to_string(), + start: cursor, + length: end - cursor + 1, + }); + } + + if constraint.iter().any(|&c| INVALID_PARAM_CHARS.contains(&c)) { + return Err(AuthorityTemplateError::InvalidConstraint { + authority: String::from_utf8_lossy(input).to_string(), + name: String::from_utf8_lossy(name).to_string(), + start: cursor, + length: end - cursor + 1, + }); + } + } + + let name = String::from_utf8(name.to_vec()).map_err(|_| EncodingError::Utf8Error { + input: String::from_utf8_lossy(name).to_string(), + })?; + + let constraint = if let Some(constraint) = constraint { + Some( + String::from_utf8(constraint.to_vec()).map_err(|_| EncodingError::Utf8Error { + input: String::from_utf8_lossy(constraint).to_string(), + })?, + ) + } else { + None + }; + + let part = if is_wildcard { + Part::Wildcard { name, constraint } + } else { + Part::Dynamic { name, constraint } + }; + + Ok((part, end + 1)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use similar_asserts::assert_eq; + + #[test] + fn test_parser_static_authority() { + assert_eq!( + Parser::new(b"example.com"), + Ok(Parser { + input: b"example.com".to_vec(), + route: ParsedTemplate { + input: b"example.com".to_vec(), + raw: b"example.com".to_vec(), + parts: vec![Part::Static { + prefix: b"example.com".to_vec() + }], + }, + }), + ); + } + + #[test] + fn test_parser_dynamic_authority() { + assert_eq!( + Parser::new(b"{subdomain}.example.com"), + Ok(Parser { + input: b"{subdomain}.example.com".to_vec(), + route: ParsedTemplate { + input: b"{subdomain}.example.com".to_vec(), + raw: b"{subdomain}.example.com".to_vec(), + parts: vec![ + Part::Static { + prefix: b".example.com".to_vec() + }, + Part::Dynamic { + name: "subdomain".to_owned(), + constraint: None + }, + ], + }, + }), + ); + } + + #[test] + fn test_parser_wildcard_authority() { + assert_eq!( + Parser::new(b"{*subdomains}.example.com"), + Ok(Parser { + input: b"{*subdomains}.example.com".to_vec(), + route: ParsedTemplate { + input: b"{*subdomains}.example.com".to_vec(), + raw: b"{*subdomains}.example.com".to_vec(), + parts: vec![ + Part::Static { + prefix: b".example.com".to_vec() + }, + Part::Wildcard { + name: "subdomains".to_owned(), + constraint: None + }, + ], + }, + }), + ); + } + + #[test] + fn test_parser_complex_authority() { + assert_eq!( + Parser::new(b"{*wildcard}.{param:alpha}.example.com"), + Ok(Parser { + input: b"{*wildcard}.{param:alpha}.example.com".to_vec(), + route: ParsedTemplate { + input: b"{*wildcard}.{param:alpha}.example.com".to_vec(), + raw: b"{*wildcard}.{param:alpha}.example.com".to_vec(), + parts: vec![ + Part::Static { + prefix: b".example.com".to_vec() + }, + Part::Dynamic { + name: "param".to_owned(), + constraint: Some("alpha".to_owned()) + }, + Part::Static { + prefix: b".".to_vec() + }, + Part::Wildcard { + name: "wildcard".to_owned(), + constraint: None + }, + ], + }, + }), + ); + } + + #[test] + fn test_parser_error_empty() { + let error = Parser::new(b"").unwrap_err(); + assert_eq!(error, AuthorityTemplateError::Empty); + + insta::assert_snapshot!(error, @"empty authority"); + } + + #[test] + fn test_parser_error_empty_braces() { + let error = Parser::new(b"test.{}.com").unwrap_err(); + assert_eq!( + error, + AuthorityTemplateError::EmptyBraces { + authority: "test.{}.com".to_owned(), + position: 5, + } + ); + + insta::assert_snapshot!(error, @r" + empty braces + + Authority: test.{}.com + ^^ + "); + } + + #[test] + fn test_parser_error_unbalanced_brace_opening() { + let error = Parser::new(b"test.{param.com").unwrap_err(); + assert_eq!( + error, + AuthorityTemplateError::UnbalancedBrace { + authority: "test.{param.com".to_owned(), + position: 5, + } + ); + + insta::assert_snapshot!(error, @r" + unbalanced brace + + Authority: test.{param.com + ^ + + tip: Use '\{' and '\}' to represent literal '{' and '}' characters in the authority + "); + } + + #[test] + fn test_parser_error_empty_parameter() { + let error = Parser::new(b"test.{:constraint}.com").unwrap_err(); + assert_eq!( + error, + AuthorityTemplateError::EmptyParameter { + authority: "test.{:constraint}.com".to_owned(), + start: 5, + length: 13, + } + ); + + insta::assert_snapshot!(error, @r" + empty parameter name + + Authority: test.{:constraint}.com + ^^^^^^^^^^^^^ + "); + } + + #[test] + fn test_parser_error_duplicate_parameter() { + let error = Parser::new(b"{param}.test.{param:alpha}.com").unwrap_err(); + assert_eq!( + error, + AuthorityTemplateError::DuplicateParameter { + authority: "{param}.test.{param:alpha}.com".to_owned(), + name: "param".to_owned(), + first: 0, + first_length: 7, + second: 13, + second_length: 13, + } + ); + + insta::assert_snapshot!(error, @r" + duplicate parameter name: 'param' + + Authority: {param}.test.{param:alpha}.com + ^^^^^^^ ^^^^^^^^^^^^^ + + tip: Parameter names must be unique within an authority + "); + } + + #[test] + fn test_parser_error_touching_parameters() { + let error = Parser::new(b"{param1}{param2}.com").unwrap_err(); + assert_eq!( + error, + AuthorityTemplateError::TouchingParameters { + authority: "{param1}{param2}.com".to_owned(), + start: 0, + length: 16, + } + ); + + insta::assert_snapshot!(error, @r" + touching parameters + + Authority: {param1}{param2}.com + ^^^^^^^^^^^^^^^^ + + tip: Touching parameters are not supported + "); + } +} diff --git a/src/router/authority/search.rs b/src/router/authority/search.rs new file mode 100644 index 00000000..95be62b0 --- /dev/null +++ b/src/router/authority/search.rs @@ -0,0 +1,333 @@ +use super::{node::Node, state::State, AuthorityData}; +use crate::{ + errors::{AuthoritySearchError, EncodingError}, + router::authority::{AuthorityParameters, StoredConstraint}, +}; +use smallvec::smallvec; +use std::collections::HashMap; + +impl<'r, S: State> Node<'r, S> { + pub fn search<'p>( + &'r self, + authority: &'p [u8], + parameters: &mut AuthorityParameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Result, usize)>, AuthoritySearchError> { + if authority.is_empty() { + return Ok(self.data.as_ref().map(|data| (data, self.priority))); + } + + if let Some(search) = self.search_static(authority, parameters, constraints)? { + return Ok(Some(search)); + } + + if let Some(search) = self.search_dynamic(authority, parameters, constraints)? { + return Ok(Some(search)); + } + + if let Some(search) = self.search_wildcard(authority, parameters, constraints)? { + return Ok(Some(search)); + } + + if let Some(search) = self.search_end_wildcard(authority, parameters, constraints)? { + return Ok(Some(search)); + } + + Ok(None) + } + + fn search_static<'p>( + &'r self, + authority: &'p [u8], + parameters: &mut AuthorityParameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Result, usize)>, AuthoritySearchError> { + for child in self.static_children.iter() { + if authority.len() >= child.state.prefix.len() + && child + .state + .prefix + .iter() + .zip(authority) + .all(|(a, b)| a == b) + { + let remaining = &authority[child.state.prefix.len()..]; + if let Some((data, priority)) = child.search(remaining, parameters, constraints)? { + return Ok(Some((data, priority))); + } + } + } + + Ok(None) + } + + fn search_dynamic<'p>( + &'r self, + authority: &'p [u8], + parameters: &mut AuthorityParameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Result, usize)>, AuthoritySearchError> { + if self.dynamic_children_shortcut { + self.search_dynamic_segment(authority, parameters, constraints) + } else { + self.search_dynamic_inline(authority, parameters, constraints) + } + } + + fn search_dynamic_inline<'p>( + &'r self, + authority: &'p [u8], + parameters: &mut AuthorityParameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Result, usize)>, AuthoritySearchError> { + for child in self.dynamic_children.iter() { + let mut consumed = 0; + + let mut best_match: Option<(&'r AuthorityData<'r>, usize)> = None; + let mut best_match_parameters = smallvec![]; + + while consumed < authority.len() { + if authority[consumed] == b'.' { + break; + } + + consumed += 1; + + let segment = &authority[..consumed]; + if !Self::check_constraint(child.state.constraint.as_ref(), segment, constraints) { + continue; + } + + let mut current_parameters = parameters.clone(); + current_parameters.push(( + &child.state.name, + std::str::from_utf8(segment).map_err(|_| EncodingError::Utf8Error { + input: String::from_utf8_lossy(segment).to_string(), + })?, + )); + + let Some((data, priority)) = + child.search(&authority[consumed..], &mut current_parameters, constraints)? + else { + continue; + }; + + if best_match.is_none_or(|(_, best_priority)| priority >= best_priority) { + best_match = Some((data, priority)); + best_match_parameters = current_parameters; + } + } + + if let Some(result) = best_match { + *parameters = best_match_parameters; + return Ok(Some(result)); + } + } + + Ok(None) + } + + fn search_dynamic_segment<'p>( + &'r self, + authority: &'p [u8], + parameters: &mut AuthorityParameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Result, usize)>, AuthoritySearchError> { + for child in self.dynamic_children.iter() { + let segment_end = authority + .iter() + .position(|&b| b == b'.') + .unwrap_or(authority.len()); + + let segment = &authority[..segment_end]; + if !Self::check_constraint(child.state.constraint.as_ref(), segment, constraints) { + continue; + } + + parameters.push(( + &child.state.name, + std::str::from_utf8(segment).map_err(|_| EncodingError::Utf8Error { + input: String::from_utf8_lossy(segment).to_string(), + })?, + )); + + if let Some(result) = + child.search(&authority[segment_end..], parameters, constraints)? + { + return Ok(Some(result)); + } + + parameters.pop(); + } + + Ok(None) + } + + fn search_wildcard<'p>( + &'r self, + authority: &'p [u8], + parameters: &mut AuthorityParameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Result, usize)>, AuthoritySearchError> { + if self.wildcard_children_shortcut { + self.search_wildcard_segment(authority, parameters, constraints) + } else { + self.search_wildcard_inline(authority, parameters, constraints) + } + } + + fn search_wildcard_inline<'p>( + &'r self, + authority: &'p [u8], + parameters: &mut AuthorityParameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Result, usize)>, AuthoritySearchError> { + for child in self.wildcard_children.iter() { + let mut consumed = 0; + + let mut best_match: Option<(&'r AuthorityData<'r>, usize)> = None; + let mut best_match_parameters = smallvec![]; + + while consumed < authority.len() { + consumed += 1; + + let segment = &authority[..consumed]; + if !Self::check_constraint(child.state.constraint.as_ref(), segment, constraints) { + continue; + } + + let mut current_parameters = parameters.clone(); + current_parameters.push(( + &child.state.name, + std::str::from_utf8(segment).map_err(|_| EncodingError::Utf8Error { + input: String::from_utf8_lossy(segment).to_string(), + })?, + )); + + let Some((data, priority)) = + child.search(&authority[consumed..], &mut current_parameters, constraints)? + else { + continue; + }; + + if best_match.is_none_or(|(_, best_priority)| priority >= best_priority) { + best_match = Some((data, priority)); + best_match_parameters = current_parameters; + } + } + + if let Some(result) = best_match { + *parameters = best_match_parameters; + return Ok(Some(result)); + } + } + + Ok(None) + } + + fn search_wildcard_segment<'p>( + &'r self, + authority: &'p [u8], + parameters: &mut AuthorityParameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Result, usize)>, AuthoritySearchError> { + for child in self.wildcard_children.iter() { + let mut consumed = 0; + let mut remaining = authority; + let mut section_end = false; + + while !remaining.is_empty() { + if section_end { + consumed += 1; + } + + let segment_end = remaining + .iter() + .position(|&b| b == b'.') + .unwrap_or(remaining.len()); + + if segment_end == 0 { + consumed += 1; + section_end = false; + } else { + consumed += segment_end; + section_end = true; + } + + let segment = if authority[..consumed].ends_with(b".") { + &authority[..consumed - 1] + } else { + &authority[..consumed] + }; + + if !Self::check_constraint(child.state.constraint.as_ref(), segment, constraints) { + break; + } + + parameters.push(( + &child.state.name, + std::str::from_utf8(segment).map_err(|_| EncodingError::Utf8Error { + input: String::from_utf8_lossy(segment).to_string(), + })?, + )); + + if let Some(result) = + child.search(&remaining[segment_end..], parameters, constraints)? + { + return Ok(Some(result)); + } + + parameters.pop(); + + if segment_end == remaining.len() { + break; + } + + remaining = &remaining[segment_end + 1..]; + } + } + + Ok(None) + } + + fn search_end_wildcard<'p>( + &'r self, + authority: &'p [u8], + parameters: &mut AuthorityParameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Result, usize)>, AuthoritySearchError> { + for child in self.end_wildcard_children.iter() { + if !Self::check_constraint(child.state.constraint.as_ref(), authority, constraints) { + continue; + } + + parameters.push(( + &child.state.name, + std::str::from_utf8(authority).map_err(|_| EncodingError::Utf8Error { + input: String::from_utf8_lossy(authority).to_string(), + })?, + )); + + return Ok(child.data.as_ref().map(|data| (data, child.priority))); + } + + Ok(None) + } + + fn check_constraint( + constraint: Option<&String>, + segment: &[u8], + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> bool { + let Some(constraint) = constraint else { + return true; + }; + + let constraint = constraints.get(constraint.as_str()).unwrap(); + let Ok(segment) = std::str::from_utf8(segment) else { + return false; + }; + + (constraint.check)(segment) + } +} diff --git a/src/router/authority/state.rs b/src/router/authority/state.rs new file mode 100644 index 00000000..1bc2ea01 --- /dev/null +++ b/src/router/authority/state.rs @@ -0,0 +1,279 @@ +use std::cmp::Ordering; + +pub trait State: Ord { + fn priority(&self) -> usize; + fn padding(&self) -> usize; + fn key(&self) -> &str; +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RootState { + priority: usize, + padding: usize, + key: String, +} + +impl RootState { + pub const fn new() -> Self { + Self { + priority: 0, + padding: 0, + key: String::new(), + } + } +} + +impl State for RootState { + fn priority(&self) -> usize { + self.priority + } + + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl Ord for RootState { + fn cmp(&self, _: &Self) -> Ordering { + unreachable!() + } +} + +impl PartialOrd for RootState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StaticState { + pub prefix: Vec, + priority: usize, + padding: usize, + key: String, +} + +impl StaticState { + pub fn new(prefix: Vec) -> Self { + let priority = prefix.len(); + let padding = prefix.len().saturating_sub(1); + let key = String::from_utf8_lossy(&prefix).into_owned(); + + Self { + prefix, + priority, + padding, + key, + } + } +} + +impl State for StaticState { + fn priority(&self) -> usize { + self.priority + } + + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl Ord for StaticState { + fn cmp(&self, other: &Self) -> Ordering { + self.prefix.cmp(&other.prefix) + } +} + +impl PartialOrd for StaticState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DynamicState { + pub name: String, + pub constraint: Option, + priority: usize, + padding: usize, + key: String, +} + +impl DynamicState { + pub fn new(name: String, constraint: Option) -> Self { + let mut priority = name.len(); + if constraint.is_some() { + priority += 10_000; + } + + let padding = name.len().saturating_sub(1); + let key = constraint.as_ref().map_or_else( + || format!("{{{name}}}"), + |constraint| format!("{{{name}:{constraint}}}"), + ); + + Self { + name, + constraint, + priority, + padding, + key, + } + } +} + +impl State for DynamicState { + fn priority(&self) -> usize { + self.priority + } + + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl Ord for DynamicState { + fn cmp(&self, other: &Self) -> Ordering { + self.name + .cmp(&other.name) + .then_with(|| self.constraint.cmp(&other.constraint)) + } +} + +impl PartialOrd for DynamicState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WildcardState { + pub name: String, + pub constraint: Option, + priority: usize, + padding: usize, + key: String, +} + +impl WildcardState { + pub fn new(name: String, constraint: Option) -> Self { + let mut priority = name.len(); + if constraint.is_some() { + priority += 10_000; + } + + let padding = name.len().saturating_sub(1); + let key = constraint.as_ref().map_or_else( + || format!("{{*{name}}}"), + |constraint| format!("{{*{name}:{constraint}}}"), + ); + + Self { + name, + constraint, + priority, + padding, + key, + } + } +} + +impl State for WildcardState { + fn priority(&self) -> usize { + self.priority + } + + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl Ord for WildcardState { + fn cmp(&self, other: &Self) -> Ordering { + self.name + .cmp(&other.name) + .then_with(|| self.constraint.cmp(&other.constraint)) + } +} + +impl PartialOrd for WildcardState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EndWildcardState { + pub name: String, + pub constraint: Option, + priority: usize, + padding: usize, + key: String, +} + +impl EndWildcardState { + pub fn new(name: String, constraint: Option) -> Self { + let mut priority = name.len(); + if constraint.is_some() { + priority += 10_000; + } + + let padding = name.len().saturating_sub(1); + let key = constraint.as_ref().map_or_else( + || format!("{{*{name}}}"), + |constraint| format!("{{*{name}:{constraint}}}"), + ); + + Self { + name, + constraint, + priority, + padding, + key, + } + } +} + +impl State for EndWildcardState { + fn priority(&self) -> usize { + self.priority + } + + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl Ord for EndWildcardState { + fn cmp(&self, other: &Self) -> Ordering { + self.name + .cmp(&other.name) + .then_with(|| self.constraint.cmp(&other.constraint)) + } +} + +impl PartialOrd for EndWildcardState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/src/router/path.rs b/src/router/path.rs index b1714655..d4f94cdf 100644 --- a/src/router/path.rs +++ b/src/router/path.rs @@ -230,7 +230,7 @@ impl<'r> PathRouter<'r> { if let Some(existing_id) = id { if existing_id != data.id { - return Err(PathDeleteError::RouteMismatch { + return Err(PathDeleteError::Mismatch { route: route.to_owned(), inserted: data.route.to_owned(), }); @@ -244,7 +244,7 @@ impl<'r> PathRouter<'r> { } if let Some(inserted) = mismatch { - return Err(PathDeleteError::RouteMismatch { + return Err(PathDeleteError::Mismatch { route: route.to_owned(), inserted, }); diff --git a/src/router/path/delete.rs b/src/router/path/delete.rs index 797ecd16..2c7e62d3 100644 --- a/src/router/path/delete.rs +++ b/src/router/path/delete.rs @@ -2,7 +2,7 @@ use super::{ node::Node, state::{State, StaticState}, }; -use crate::router::path::parser::{ParsedRoute, Part}; +use crate::router::path::parser::{ParsedTemplate, Part}; impl Node<'_, S> { /// Deletes a route from the node tree. @@ -11,7 +11,7 @@ impl Node<'_, S> { /// Logic should match that used by the insert method. /// /// If the route is found and deleted, we re-optimize the tree structure. - pub fn delete(&mut self, route: &mut ParsedRoute) { + pub fn delete(&mut self, route: &mut ParsedTemplate) { if let Some(part) = route.parts.pop() { match part { Part::Static { prefix } => self.delete_static(route, &prefix), @@ -32,7 +32,7 @@ impl Node<'_, S> { } } - fn delete_static(&mut self, route: &mut ParsedRoute, prefix: &[u8]) { + fn delete_static(&mut self, route: &mut ParsedTemplate, prefix: &[u8]) { let Some(index) = self.static_children.iter().position(|child| { prefix.len() >= child.state.prefix.len() && child.state.prefix.iter().zip(prefix).all(|(a, b)| a == b) @@ -69,7 +69,12 @@ impl Node<'_, S> { } } - fn delete_dynamic(&mut self, route: &mut ParsedRoute, name: &str, constraint: Option<&String>) { + fn delete_dynamic( + &mut self, + route: &mut ParsedTemplate, + name: &str, + constraint: Option<&String>, + ) { let Some(index) = self.dynamic_children.iter().position(|child| { child.state.name == name && child.state.constraint.as_ref() == constraint }) else { @@ -87,7 +92,7 @@ impl Node<'_, S> { fn delete_wildcard( &mut self, - route: &mut ParsedRoute, + route: &mut ParsedTemplate, name: &str, constraint: Option<&String>, ) { diff --git a/src/router/path/errors.rs b/src/router/path/errors.rs index 9a148d24..a2727305 100644 --- a/src/router/path/errors.rs +++ b/src/router/path/errors.rs @@ -7,8 +7,8 @@ pub use delete::PathDeleteError; pub mod insert; pub use insert::PathInsertError; -pub mod route; -pub use route::PathRouteError; - pub mod search; pub use search::PathSearchError; + +pub mod template; +pub use template::PathTemplateError; diff --git a/src/router/path/errors/delete.rs b/src/router/path/errors/delete.rs index bfb04e88..a1eb41c6 100644 --- a/src/router/path/errors/delete.rs +++ b/src/router/path/errors/delete.rs @@ -1,18 +1,15 @@ -use super::PathRouteError; +use super::PathTemplateError; use crate::errors::EncodingError; use std::{error::Error, fmt::Display}; /// Errors relating to attempting to delete a route from a [`Router`](crate::Router). #[derive(Debug, PartialEq, Eq)] pub enum PathDeleteError { - /// Multiple [`PathDeleteError`] errors occurred during the delete. - Multiple(Vec), - /// A [`EncodingError`] that occurred during the decoding. EncodingError(EncodingError), /// A [`RouteError`] that occurred during the delete. - PathRouteError(PathRouteError), + TemplateError(PathTemplateError), /// Route to be deleted was not found in the router. /// @@ -47,7 +44,7 @@ pub enum PathDeleteError { /// ```rust /// use wayfind::errors::PathDeleteError; /// - /// let error = PathDeleteError::RouteMismatch { + /// let error = PathDeleteError::Mismatch { /// route: "/users/{id}/".to_string(), /// inserted: "/users/{id}(/)".to_string(), /// }; @@ -63,7 +60,7 @@ pub enum PathDeleteError { /// /// assert_eq!(error.to_string(), display.trim()); /// ``` - RouteMismatch { + Mismatch { /// The route that was attempted to be deleted. route: String, /// The route stored as stored in the router. @@ -76,18 +73,8 @@ impl Error for PathDeleteError {} impl Display for PathDeleteError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Multiple(errors) => { - writeln!(f, "multiple path delete errors occurred:\n---\n")?; - for (index, error) in errors.iter().enumerate() { - write!(f, "{error}")?; - if index < errors.len() - 1 { - writeln!(f, "\n---\n")?; - } - } - Ok(()) - } Self::EncodingError(error) => error.fmt(f), - Self::PathRouteError(error) => error.fmt(f), + Self::TemplateError(error) => error.fmt(f), Self::NotFound { route } => write!( f, r"not found @@ -96,7 +83,7 @@ impl Display for PathDeleteError { The specified route does not exist in the router" ), - Self::RouteMismatch { route, inserted } => write!( + Self::Mismatch { route, inserted } => write!( f, r"delete mismatch @@ -115,8 +102,8 @@ impl From for PathDeleteError { } } -impl From for PathDeleteError { - fn from(error: PathRouteError) -> Self { - Self::PathRouteError(error) +impl From for PathDeleteError { + fn from(error: PathTemplateError) -> Self { + Self::TemplateError(error) } } diff --git a/src/router/path/errors/insert.rs b/src/router/path/errors/insert.rs index 0f8c9046..9e0fc914 100644 --- a/src/router/path/errors/insert.rs +++ b/src/router/path/errors/insert.rs @@ -1,13 +1,12 @@ +use super::PathTemplateError; use crate::PathId; - -use super::PathRouteError; use std::{error::Error, fmt::Display}; /// Errors relating to attempting to insert a route into a [`Router`](crate::Router). #[derive(Debug, PartialEq, Eq)] pub enum PathInsertError { - /// A [`PathRouteError`] that occurred during the insert operation. - PathRouteError(PathRouteError), + /// A [`TemplateError`] that occurred during the insert operation. + TemplateError(PathTemplateError), /// TODO OverlappingRoutes { ids: Vec }, @@ -44,7 +43,7 @@ impl Error for PathInsertError {} impl Display for PathInsertError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::PathRouteError(error) => error.fmt(f), + Self::TemplateError(error) => error.fmt(f), Self::OverlappingRoutes { ids } => write!(f, r"overlapping routes {ids:?}"), Self::UnknownConstraint { constraint } => write!( f, @@ -58,8 +57,8 @@ The router doesn't recognize this constraint" } } -impl From for PathInsertError { - fn from(error: PathRouteError) -> Self { - Self::PathRouteError(error) +impl From for PathInsertError { + fn from(error: PathTemplateError) -> Self { + Self::TemplateError(error) } } diff --git a/src/router/path/errors/route.rs b/src/router/path/errors/template.rs similarity index 90% rename from src/router/path/errors/route.rs rename to src/router/path/errors/template.rs index f71509e0..5f117e34 100644 --- a/src/router/path/errors/route.rs +++ b/src/router/path/errors/template.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt::Display}; /// Errors relating to malformed routes. #[derive(Debug, PartialEq, Eq)] -pub enum PathRouteError { +pub enum PathTemplateError { /// A [`EncodingError`] that occurred during the decoding. EncodingError(EncodingError), @@ -15,9 +15,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::MissingLeadingSlash { + /// let error = PathTemplateError::MissingLeadingSlash { /// route: "abc".to_string(), /// }; /// @@ -41,9 +41,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::EmptyBraces { + /// let error = PathTemplateError::EmptyBraces { /// route: "/{}".to_string(), /// position: 1, /// }; @@ -69,9 +69,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::UnbalancedBrace { + /// let error = PathTemplateError::UnbalancedBrace { /// route: "/{".to_string(), /// position: 1, /// }; @@ -99,9 +99,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::EmptyParentheses { + /// let error = PathTemplateError::EmptyParentheses { /// route: "/()".to_string(), /// position: 1, /// }; @@ -127,9 +127,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::UnbalancedParenthesis { + /// let error = PathTemplateError::UnbalancedParenthesis { /// route: "/(".to_string(), /// position: 1, /// }; @@ -157,9 +157,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::EmptyParameter { + /// let error = PathTemplateError::EmptyParameter { /// route: "/{:}".to_string(), /// start: 1, /// length: 3, @@ -188,9 +188,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::InvalidParameter { + /// let error = PathTemplateError::InvalidParameter { /// route: "/{a/b}".to_string(), /// name: "a/b".to_string(), /// start: 1, @@ -224,9 +224,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::DuplicateParameter { + /// let error = PathTemplateError::DuplicateParameter { /// route: "/{id}/{id}".to_string(), /// name: "id".to_string(), /// first: 1, @@ -266,9 +266,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::EmptyWildcard { + /// let error = PathTemplateError::EmptyWildcard { /// route: "/{*}".to_string(), /// start: 1, /// length: 3, @@ -297,9 +297,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::EmptyConstraint { + /// let error = PathTemplateError::EmptyConstraint { /// route: "/{a:}".to_string(), /// start: 1, /// length: 4, @@ -328,9 +328,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::InvalidConstraint { + /// let error = PathTemplateError::InvalidConstraint { /// route: "/{a:b/c}".to_string(), /// name: "b/c".to_string(), /// start: 1, @@ -364,9 +364,9 @@ pub enum PathRouteError { /// # Examples /// /// ```rust - /// use wayfind::errors::PathRouteError; + /// use wayfind::errors::PathTemplateError; /// - /// let error = PathRouteError::TouchingParameters { + /// let error = PathTemplateError::TouchingParameters { /// route: "/{a}{b}".to_string(), /// start: 1, /// length: 6, @@ -393,9 +393,9 @@ pub enum PathRouteError { }, } -impl Error for PathRouteError {} +impl Error for PathTemplateError {} -impl Display for PathRouteError { +impl Display for PathTemplateError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::EncodingError(error) => error.fmt(f), @@ -589,7 +589,7 @@ tip: Touching parameters are not supported" } } -impl From for PathRouteError { +impl From for PathTemplateError { fn from(error: EncodingError) -> Self { Self::EncodingError(error) } diff --git a/src/router/path/find.rs b/src/router/path/find.rs index 073a5837..10d90593 100644 --- a/src/router/path/find.rs +++ b/src/router/path/find.rs @@ -1,12 +1,12 @@ use super::{ node::Node, - parser::{ParsedRoute, Part}, + parser::{ParsedTemplate, Part}, state::State, PathData, }; impl<'r, S: State> Node<'r, S> { - pub(crate) fn find(&'r self, route: &mut ParsedRoute) -> Option<&'r PathData<'r>> { + pub(crate) fn find(&'r self, route: &mut ParsedTemplate) -> Option<&'r PathData<'r>> { if route.parts.is_empty() { return self.data.as_ref(); } @@ -29,7 +29,11 @@ impl<'r, S: State> Node<'r, S> { None } - fn find_static(&'r self, route: &mut ParsedRoute, prefix: &[u8]) -> Option<&'r PathData<'r>> { + fn find_static( + &'r self, + route: &mut ParsedTemplate, + prefix: &[u8], + ) -> Option<&'r PathData<'r>> { for child in self.static_children.iter() { if !child.state.prefix.is_empty() && child.state.prefix[0] == prefix[0] { let common_prefix = prefix @@ -45,7 +49,7 @@ impl<'r, S: State> Node<'r, S> { let remaining = prefix[common_prefix..].to_vec(); if !remaining.is_empty() { - let mut new_route = ParsedRoute { + let mut new_route = ParsedTemplate { parts: route.parts.clone(), ..route.clone() }; @@ -62,7 +66,7 @@ impl<'r, S: State> Node<'r, S> { fn find_dynamic( &'r self, - route: &mut ParsedRoute, + route: &mut ParsedTemplate, name: &str, constraint: Option<&str>, ) -> Option<&'r PathData<'r>> { @@ -77,7 +81,7 @@ impl<'r, S: State> Node<'r, S> { fn find_end_wildcard( &'r self, - route: &mut ParsedRoute, + route: &mut ParsedTemplate, name: &str, constraint: Option<&str>, ) -> Option<&'r PathData<'r>> { @@ -92,7 +96,7 @@ impl<'r, S: State> Node<'r, S> { fn find_wildcard( &'r self, - route: &mut ParsedRoute, + route: &mut ParsedTemplate, name: &str, constraint: Option<&str>, ) -> Option<&'r PathData<'r>> { diff --git a/src/router/path/insert.rs b/src/router/path/insert.rs index 2811c41e..76817a6d 100644 --- a/src/router/path/insert.rs +++ b/src/router/path/insert.rs @@ -4,14 +4,14 @@ use super::{ PathData, }; use crate::{ - router::path::parser::{ParsedRoute, Part}, + router::path::parser::{ParsedTemplate, Part}, vec::SortedVec, }; impl<'r, S: State> Node<'r, S> { /// Inserts a new route into the node tree with associated data. /// Recursively traverses the node tree, creating new nodes as necessary. - pub fn insert(&mut self, route: &mut ParsedRoute, data: PathData<'r>) { + pub fn insert(&mut self, route: &mut ParsedTemplate, data: PathData<'r>) { if let Some(part) = route.parts.pop() { match part { Part::Static { prefix } => self.insert_static(route, data, &prefix), @@ -37,7 +37,7 @@ impl<'r, S: State> Node<'r, S> { } } - fn insert_static(&mut self, route: &mut ParsedRoute, data: PathData<'r>, prefix: &[u8]) { + fn insert_static(&mut self, route: &mut ParsedTemplate, data: PathData<'r>, prefix: &[u8]) { // Check if the first byte is already a child here. let Some(child) = self .static_children @@ -133,7 +133,7 @@ impl<'r, S: State> Node<'r, S> { fn insert_dynamic( &mut self, - route: &mut ParsedRoute, + route: &mut ParsedTemplate, data: PathData<'r>, name: String, constraint: Option, @@ -170,7 +170,7 @@ impl<'r, S: State> Node<'r, S> { fn insert_wildcard( &mut self, - route: &mut ParsedRoute, + route: &mut ParsedTemplate, data: PathData<'r>, name: String, constraint: Option, diff --git a/src/router/path/parser.rs b/src/router/path/parser.rs index 2958a5b8..fe2f457e 100644 --- a/src/router/path/parser.rs +++ b/src/router/path/parser.rs @@ -1,4 +1,4 @@ -use super::errors::PathRouteError; +use super::errors::PathTemplateError; use crate::errors::EncodingError; use smallvec::{smallvec, SmallVec}; @@ -6,7 +6,7 @@ use smallvec::{smallvec, SmallVec}; const INVALID_PARAM_CHARS: [u8; 7] = [b':', b'*', b'{', b'}', b'(', b')', b'/']; #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ParsedRoute { +pub struct ParsedTemplate { pub input: Vec, pub raw: Vec, pub parts: Vec, @@ -30,14 +30,14 @@ pub enum Part { #[derive(Debug, PartialEq, Eq)] pub struct Parser { pub input: Vec, - pub routes: Vec, + pub routes: Vec, pub expanded: bool, } impl Parser { - pub fn new(input: &[u8]) -> Result { + pub fn new(input: &[u8]) -> Result { if input.is_empty() { - return Err(PathRouteError::Empty); + return Err(PathTemplateError::Empty); } let routes = Self::expand_optional_groups(input, 0, input.len())?; @@ -60,7 +60,7 @@ impl Parser { input: &[u8], start: usize, end: usize, - ) -> Result>, PathRouteError> { + ) -> Result>, PathTemplateError> { let mut result = Vec::from([vec![]]); let mut cursor = start; @@ -89,7 +89,7 @@ impl Parser { depth -= 1; if depth < 0 { - return Err(PathRouteError::UnbalancedParenthesis { + return Err(PathTemplateError::UnbalancedParenthesis { route: String::from_utf8_lossy(input).to_string(), position: cursor, }); @@ -97,7 +97,7 @@ impl Parser { if depth == 0 { if cursor == group { - return Err(PathRouteError::EmptyParentheses { + return Err(PathTemplateError::EmptyParentheses { route: String::from_utf8_lossy(input).to_string(), position: cursor - 1, }); @@ -129,7 +129,7 @@ impl Parser { } if depth != 0 { - return Err(PathRouteError::UnbalancedParenthesis { + return Err(PathTemplateError::UnbalancedParenthesis { route: String::from_utf8_lossy(input).to_string(), position: start + group - 1, }); @@ -150,9 +150,9 @@ impl Parser { Ok(result) } - fn parse_route(input: &[u8], raw: &[u8]) -> Result { + fn parse_route(input: &[u8], raw: &[u8]) -> Result { if !raw.is_empty() && raw[0] != b'/' { - return Err(PathRouteError::MissingLeadingSlash { + return Err(PathTemplateError::MissingLeadingSlash { route: String::from_utf8_lossy(raw).to_string(), }); } @@ -171,7 +171,7 @@ impl Parser { // Check for touching parameters. if let Some((_, start, length)) = seen_parameters.last() { if cursor == start + length { - return Err(PathRouteError::TouchingParameters { + return Err(PathTemplateError::TouchingParameters { route: String::from_utf8_lossy(raw).to_string(), start: *start, length: next_cursor - start, @@ -185,7 +185,7 @@ impl Parser { .iter() .find(|(existing, _, _)| existing == name) { - return Err(PathRouteError::DuplicateParameter { + return Err(PathTemplateError::DuplicateParameter { route: String::from_utf8_lossy(raw).to_string(), name: name.to_string(), first: *start, @@ -202,7 +202,7 @@ impl Parser { cursor = next_cursor; } b'}' => { - return Err(PathRouteError::UnbalancedBrace { + return Err(PathTemplateError::UnbalancedBrace { route: String::from_utf8_lossy(raw).to_string(), position: cursor, }) @@ -217,7 +217,7 @@ impl Parser { parts.reverse(); - Ok(ParsedRoute { + Ok(ParsedTemplate { input: input.to_vec(), raw: raw.to_vec(), parts, @@ -249,7 +249,10 @@ impl Parser { (Part::Static { prefix }, end) } - fn parse_parameter_part(input: &[u8], cursor: usize) -> Result<(Part, usize), PathRouteError> { + fn parse_parameter_part( + input: &[u8], + cursor: usize, + ) -> Result<(Part, usize), PathTemplateError> { let start = cursor + 1; let mut end = start; @@ -270,7 +273,7 @@ impl Parser { } if brace_count != 0 { - return Err(PathRouteError::UnbalancedBrace { + return Err(PathTemplateError::UnbalancedBrace { route: String::from_utf8_lossy(input).to_string(), position: cursor, }); @@ -278,7 +281,7 @@ impl Parser { let content = &input[start..end]; if content.is_empty() { - return Err(PathRouteError::EmptyBraces { + return Err(PathTemplateError::EmptyBraces { route: String::from_utf8_lossy(input).to_string(), position: cursor, }); @@ -292,7 +295,7 @@ impl Parser { }); if name.is_empty() { - return Err(PathRouteError::EmptyParameter { + return Err(PathTemplateError::EmptyParameter { route: String::from_utf8_lossy(input).to_string(), start: cursor, length: end - cursor + 1, @@ -303,7 +306,7 @@ impl Parser { let name = if is_wildcard { &name[1..] } else { name }; if is_wildcard && name.is_empty() { - return Err(PathRouteError::EmptyWildcard { + return Err(PathTemplateError::EmptyWildcard { route: String::from_utf8_lossy(input).to_string(), start: cursor, length: end - cursor + 1, @@ -311,7 +314,7 @@ impl Parser { } if name.iter().any(|&c| INVALID_PARAM_CHARS.contains(&c)) { - return Err(PathRouteError::InvalidParameter { + return Err(PathTemplateError::InvalidParameter { route: String::from_utf8_lossy(input).to_string(), name: String::from_utf8_lossy(name).to_string(), start: cursor, @@ -321,7 +324,7 @@ impl Parser { if let Some(constraint) = constraint { if constraint.is_empty() { - return Err(PathRouteError::EmptyConstraint { + return Err(PathTemplateError::EmptyConstraint { route: String::from_utf8_lossy(input).to_string(), start: cursor, length: end - cursor + 1, @@ -329,7 +332,7 @@ impl Parser { } if constraint.iter().any(|&c| INVALID_PARAM_CHARS.contains(&c)) { - return Err(PathRouteError::InvalidConstraint { + return Err(PathTemplateError::InvalidConstraint { route: String::from_utf8_lossy(input).to_string(), name: String::from_utf8_lossy(name).to_string(), start: cursor, @@ -373,7 +376,7 @@ mod tests { Parser::new(b"/abcd"), Ok(Parser { input: b"/abcd".to_vec(), - routes: vec![ParsedRoute { + routes: vec![ParsedTemplate { input: b"/abcd".to_vec(), raw: b"/abcd".to_vec(), parts: vec![Part::Static { @@ -391,7 +394,7 @@ mod tests { Parser::new(b"/{name}"), Ok(Parser { input: b"/{name}".to_vec(), - routes: vec![ParsedRoute { + routes: vec![ParsedTemplate { input: b"/{name}".to_vec(), raw: b"/{name}".to_vec(), parts: vec![ @@ -415,7 +418,7 @@ mod tests { Parser::new(b"/{*route}"), Ok(Parser { input: b"/{*route}".to_vec(), - routes: vec![ParsedRoute { + routes: vec![ParsedTemplate { input: b"/{*route}".to_vec(), raw: b"/{*route}".to_vec(), parts: vec![ @@ -439,7 +442,7 @@ mod tests { Parser::new(b"/{*name:alpha}/{id:numeric}"), Ok(Parser { input: b"/{*name:alpha}/{id:numeric}".to_vec(), - routes: vec![ParsedRoute { + routes: vec![ParsedTemplate { input: b"/{*name:alpha}/{id:numeric}".to_vec(), raw: b"/{*name:alpha}/{id:numeric}".to_vec(), parts: vec![ @@ -471,7 +474,7 @@ mod tests { Ok(Parser { input: b"/users(/{id})".to_vec(), routes: vec![ - ParsedRoute { + ParsedTemplate { input: b"/users(/{id})".to_vec(), raw: b"/users/{id}".to_vec(), parts: vec![ @@ -484,7 +487,7 @@ mod tests { }, ], }, - ParsedRoute { + ParsedTemplate { input: b"/users(/{id})".to_vec(), raw: b"/users".to_vec(), parts: vec![Part::Static { @@ -504,7 +507,7 @@ mod tests { Ok(Parser { input: b"/users(/{id}(/profile))".to_vec(), routes: vec![ - ParsedRoute { + ParsedTemplate { input: b"/users(/{id}(/profile))".to_vec(), raw: b"/users/{id}/profile".to_vec(), parts: vec![ @@ -520,7 +523,7 @@ mod tests { }, ], }, - ParsedRoute { + ParsedTemplate { input: b"/users(/{id}(/profile))".to_vec(), raw: b"/users/{id}".to_vec(), parts: vec![ @@ -533,7 +536,7 @@ mod tests { }, ], }, - ParsedRoute { + ParsedTemplate { input: b"/users(/{id}(/profile))".to_vec(), raw: b"/users".to_vec(), parts: vec![Part::Static { @@ -552,7 +555,7 @@ mod tests { Parser::new(b"/path/with\\{braces\\}and\\(parens\\)"), Ok(Parser { input: b"/path/with\\{braces\\}and\\(parens\\)".to_vec(), - routes: vec![ParsedRoute { + routes: vec![ParsedTemplate { input: b"/path/with\\{braces\\}and\\(parens\\)".to_vec(), raw: b"/path/with\\{braces\\}and\\(parens\\)".to_vec(), parts: vec![Part::Static { @@ -571,7 +574,7 @@ mod tests { Ok(Parser { input: b"(/{lang})/users".to_vec(), routes: vec![ - ParsedRoute { + ParsedTemplate { input: b"(/{lang})/users".to_vec(), raw: b"/{lang}/users".to_vec(), parts: vec![ @@ -587,7 +590,7 @@ mod tests { }, ], }, - ParsedRoute { + ParsedTemplate { input: b"(/{lang})/users".to_vec(), raw: b"/users".to_vec(), parts: vec![Part::Static { @@ -607,7 +610,7 @@ mod tests { Ok(Parser { input: b"(/{lang})(/{page})".to_vec(), routes: vec![ - ParsedRoute { + ParsedTemplate { input: b"(/{lang})(/{page})".to_vec(), raw: b"/{lang}/{page}".to_vec(), parts: vec![ @@ -627,7 +630,7 @@ mod tests { }, ], }, - ParsedRoute { + ParsedTemplate { input: b"(/{lang})(/{page})".to_vec(), raw: b"/{lang}".to_vec(), parts: vec![ @@ -640,7 +643,7 @@ mod tests { }, ], }, - ParsedRoute { + ParsedTemplate { input: b"(/{lang})(/{page})".to_vec(), raw: b"/{page}".to_vec(), parts: vec![ @@ -653,7 +656,7 @@ mod tests { }, ], }, - ParsedRoute { + ParsedTemplate { input: b"(/{lang})(/{page})".to_vec(), raw: b"/".to_vec(), parts: vec![Part::Static { @@ -669,7 +672,7 @@ mod tests { #[test] fn test_parser_error_empty() { let error = Parser::new(b"").unwrap_err(); - assert_eq!(error, PathRouteError::Empty); + assert_eq!(error, PathTemplateError::Empty); insta::assert_snapshot!(error, @"empty route"); } @@ -679,7 +682,7 @@ mod tests { let error = Parser::new(b"/users/{}").unwrap_err(); assert_eq!( error, - PathRouteError::EmptyBraces { + PathTemplateError::EmptyBraces { route: "/users/{}".to_owned(), position: 7, } @@ -698,7 +701,7 @@ mod tests { let error = Parser::new(b"abc").unwrap_err(); assert_eq!( error, - PathRouteError::MissingLeadingSlash { + PathTemplateError::MissingLeadingSlash { route: "abc".to_owned(), } ); @@ -717,7 +720,7 @@ mod tests { let error = Parser::new(b"/users/{id/profile").unwrap_err(); assert_eq!( error, - PathRouteError::UnbalancedBrace { + PathTemplateError::UnbalancedBrace { route: "/users/{id/profile".to_owned(), position: 7, } @@ -738,7 +741,7 @@ mod tests { let error = Parser::new(b"/users/id}/profile").unwrap_err(); assert_eq!( error, - PathRouteError::UnbalancedBrace { + PathTemplateError::UnbalancedBrace { route: "/users/id}/profile".to_owned(), position: 9, } @@ -759,7 +762,7 @@ mod tests { let error = Parser::new(b"/products()/category").unwrap_err(); assert_eq!( error, - PathRouteError::EmptyParentheses { + PathTemplateError::EmptyParentheses { route: "/products()/category".to_owned(), position: 9, } @@ -778,7 +781,7 @@ mod tests { let error = Parser::new(b"/products(/category").unwrap_err(); assert_eq!( error, - PathRouteError::UnbalancedParenthesis { + PathTemplateError::UnbalancedParenthesis { route: "/products(/category".to_owned(), position: 9, } @@ -799,7 +802,7 @@ mod tests { let error = Parser::new(b"/products)/category").unwrap_err(); assert_eq!( error, - PathRouteError::UnbalancedParenthesis { + PathTemplateError::UnbalancedParenthesis { route: "/products)/category".to_owned(), position: 9, } @@ -820,7 +823,7 @@ mod tests { let error = Parser::new(b"/users/{:constraint}/profile").unwrap_err(); assert_eq!( error, - PathRouteError::EmptyParameter { + PathTemplateError::EmptyParameter { route: "/users/{:constraint}/profile".to_owned(), start: 7, length: 13, @@ -840,7 +843,7 @@ mod tests { let error = Parser::new(b"/users/{user*name}/profile").unwrap_err(); assert_eq!( error, - PathRouteError::InvalidParameter { + PathTemplateError::InvalidParameter { route: "/users/{user*name}/profile".to_owned(), name: "user*name".to_owned(), start: 7, @@ -863,7 +866,7 @@ mod tests { let error = Parser::new(b"/users/{id}/posts/{id:uuid}").unwrap_err(); assert_eq!( error, - PathRouteError::DuplicateParameter { + PathTemplateError::DuplicateParameter { route: "/users/{id}/posts/{id:uuid}".to_owned(), name: "id".to_owned(), first: 7, @@ -888,7 +891,7 @@ mod tests { let error = Parser::new(b"/files/{*}").unwrap_err(); assert_eq!( error, - PathRouteError::EmptyWildcard { + PathTemplateError::EmptyWildcard { route: "/files/{*}".to_owned(), start: 7, length: 3, @@ -908,7 +911,7 @@ mod tests { let error = Parser::new(b"/users/{id:}/profile").unwrap_err(); assert_eq!( error, - PathRouteError::EmptyConstraint { + PathTemplateError::EmptyConstraint { route: "/users/{id:}/profile".to_owned(), start: 7, length: 5, @@ -928,7 +931,7 @@ mod tests { let error = Parser::new(b"/users/{id:*}/profile").unwrap_err(); assert_eq!( error, - PathRouteError::InvalidConstraint { + PathTemplateError::InvalidConstraint { route: "/users/{id:*}/profile".to_owned(), name: "id".to_owned(), start: 7, @@ -951,7 +954,7 @@ mod tests { let error = Parser::new(b"/users/{id}{name}").unwrap_err(); assert_eq!( error, - PathRouteError::TouchingParameters { + PathTemplateError::TouchingParameters { route: "/users/{id}{name}".to_owned(), start: 7, length: 10, diff --git a/tests/authority.rs b/tests/authority.rs new file mode 100644 index 00000000..5bc4722f --- /dev/null +++ b/tests/authority.rs @@ -0,0 +1,315 @@ +use smallvec::smallvec; +use std::error::Error; +use wayfind::{ + AuthorityMatch, Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router, +}; + +#[test] +fn test_authority_simple() -> Result<(), Box> { + let mut router = Router::new(); + + let route = RouteBuilder::new() + .authority("api.example.com") + .route("/users") + .build()?; + router.insert(&route, 1)?; + + insta::assert_snapshot!(router, @r" + === Authority + api.example.com [1] + === Path + /users [1] + === Method + Empty + === Chains + 1-1-* + "); + + let request = RequestBuilder::new() + .authority("api.example.com") + .path("/users") + .build()?; + let search = router.search(&request)?; + assert_eq!( + search, + Some(Match { + data: &1, + authority: AuthorityMatch { + authority: Some("api.example.com"), + parameters: smallvec![] + }, + path: PathMatch { + route: "/users", + expanded: None, + parameters: smallvec![], + }, + method: MethodMatch { method: None } + }) + ); + + Ok(()) +} + +#[test] +fn test_authority_parameter() -> Result<(), Box> { + let mut router = Router::new(); + + let route = RouteBuilder::new() + .authority("{tenant}.example.com") + .route("/users") + .build()?; + router.insert(&route, 1)?; + + insta::assert_snapshot!(router, @r" + === Authority + {tenant} + ╰─ .example.com [1] + === Path + /users [1] + === Method + Empty + === Chains + 1-1-* + "); + + let request = RequestBuilder::new() + .authority("acme.example.com") + .path("/users") + .build()?; + let search = router.search(&request)?; + assert_eq!( + search, + Some(Match { + data: &1, + authority: AuthorityMatch { + authority: Some("{tenant}.example.com"), + parameters: smallvec![("tenant", "acme")], + }, + path: PathMatch { + route: "/users", + expanded: None, + parameters: smallvec![], + }, + method: MethodMatch { method: None } + }) + ); + + Ok(()) +} + +#[test] +fn test_authority_multiple_parameters() -> Result<(), Box> { + let mut router = Router::new(); + + let route = RouteBuilder::new() + .authority("{tenant}.{region}.example.com") + .route("/users") + .build()?; + router.insert(&route, 1)?; + + insta::assert_snapshot!(router, @r" + === Authority + {tenant} + ╰─ . + ╰─ {region} + ╰─ .example.com [1] + === Path + /users [1] + === Method + Empty + === Chains + 1-1-* + "); + + let request = RequestBuilder::new() + .authority("acme.us-east.example.com") + .path("/users") + .build()?; + let search = router.search(&request)?; + assert_eq!( + search, + Some(Match { + data: &1, + authority: AuthorityMatch { + authority: Some("{tenant}.{region}.example.com"), + parameters: smallvec![("tenant", "acme"), ("region", "us-east")], + }, + path: PathMatch { + route: "/users", + expanded: None, + parameters: smallvec![], + }, + method: MethodMatch { method: None } + }) + ); + + Ok(()) +} + +#[test] +fn test_authority_priority() -> Result<(), Box> { + let mut router = Router::new(); + + let route = RouteBuilder::new() + .authority("api.example.com") + .route("/users") + .build()?; + router.insert(&route, 1)?; + + let route = RouteBuilder::new() + .authority("{tenant}.example.com") + .route("/users") + .build()?; + router.insert(&route, 2)?; + + insta::assert_snapshot!(router, @r" + === Authority + api.example.com [1] + {tenant} + ╰─ .example.com [2] + === Path + /users [1] + === Method + Empty + === Chains + 1-1-* + 2-1-* + "); + + let request = RequestBuilder::new() + .authority("api.example.com") + .path("/users") + .build()?; + let search = router.search(&request)?; + assert_eq!( + search, + Some(Match { + data: &1, + authority: AuthorityMatch { + authority: Some("api.example.com"), + parameters: smallvec![] + }, + path: PathMatch { + route: "/users", + expanded: None, + parameters: smallvec![], + }, + method: MethodMatch { method: None } + }) + ); + + let request = RequestBuilder::new() + .authority("acme.example.com") + .path("/users") + .build()?; + let search = router.search(&request)?; + assert_eq!( + search, + Some(Match { + data: &2, + authority: AuthorityMatch { + authority: Some("{tenant}.example.com"), + parameters: smallvec![("tenant", "acme")], + }, + path: PathMatch { + route: "/users", + expanded: None, + parameters: smallvec![], + }, + method: MethodMatch { method: None } + }) + ); + + Ok(()) +} + +#[test] +fn test_authority_fallback() -> Result<(), Box> { + let mut router = Router::new(); + + let route = RouteBuilder::new().route("/users").build()?; + router.insert(&route, 1)?; + + let route = RouteBuilder::new() + .authority("api.example.com") + .route("/users") + .build()?; + router.insert(&route, 2)?; + + insta::assert_snapshot!(router, @r" + === Authority + api.example.com [1] + === Path + /users [1] + === Method + Empty + === Chains + *-1-* + 1-1-* + "); + + let request = RequestBuilder::new() + .authority("api.example.com") + .path("/users") + .build()?; + let search = router.search(&request)?; + assert_eq!( + search, + Some(Match { + data: &2, + authority: AuthorityMatch { + authority: Some("api.example.com"), + parameters: smallvec![] + }, + path: PathMatch { + route: "/users", + expanded: None, + parameters: smallvec![], + }, + method: MethodMatch { method: None } + }) + ); + + let request = RequestBuilder::new().path("/users").build()?; + let search = router.search(&request)?; + assert_eq!( + search, + Some(Match { + data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, + path: PathMatch { + route: "/users", + expanded: None, + parameters: smallvec![], + }, + method: MethodMatch { method: None } + }) + ); + + let request = RequestBuilder::new() + .authority("other.example.com") + .path("/users") + .build()?; + let search = router.search(&request)?; + assert_eq!( + search, + Some(Match { + data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, + path: PathMatch { + route: "/users", + expanded: None, + parameters: smallvec![], + }, + method: MethodMatch { method: None } + }) + ); + + Ok(()) +} diff --git a/tests/constraint.rs b/tests/constraint.rs index 32b60777..11523644 100644 --- a/tests/constraint.rs +++ b/tests/constraint.rs @@ -3,7 +3,8 @@ use smallvec::smallvec; use std::error::Error; use wayfind::{ errors::{InsertError, PathConstraintError, PathInsertError}, - Match, MethodMatch, PathConstraint, PathMatch, RequestBuilder, RouteBuilder, Router, + AuthorityMatch, Match, MethodMatch, PathConstraint, PathMatch, RequestBuilder, RouteBuilder, + Router, }; struct NameConstraint; @@ -24,13 +25,15 @@ fn test_constraint_dynamic() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/ ╰─ {id:name} [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/users/john123").build()?; @@ -39,6 +42,10 @@ fn test_constraint_dynamic() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users/{id:name}", expanded: None, @@ -64,13 +71,15 @@ fn test_constraint_wildcard() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/ ╰─ {*path:name} [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/users/john/doe123").build()?; @@ -79,6 +88,10 @@ fn test_constraint_wildcard() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users/{*path:name}", expanded: None, @@ -157,6 +170,8 @@ fn test_constraint_builtin() -> Result<(), Box> { router.insert(&route, 2)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/ ├─ {id:u32} [2] @@ -164,8 +179,8 @@ fn test_constraint_builtin() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* + *-1-* + *-2-* "); let request = RequestBuilder::new().path("/users/abc").build()?; @@ -174,6 +189,10 @@ fn test_constraint_builtin() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users/{id}", expanded: None, @@ -189,6 +208,10 @@ fn test_constraint_builtin() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users/{id:u32}", expanded: None, @@ -213,6 +236,8 @@ fn test_constraint_unreachable() -> Result<(), Box> { router.insert(&route, 2)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/ ├─ {id:name} [2] @@ -220,8 +245,8 @@ fn test_constraint_unreachable() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* + *-1-* + *-2-* "); let request = RequestBuilder::new().path("/users/123").build()?; @@ -230,6 +255,10 @@ fn test_constraint_unreachable() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users/{id:name}", expanded: None, @@ -245,6 +274,10 @@ fn test_constraint_unreachable() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users/{id:name}", expanded: None, diff --git a/tests/delete.rs b/tests/delete.rs index cde43803..31098ce7 100644 --- a/tests/delete.rs +++ b/tests/delete.rs @@ -13,12 +13,14 @@ fn test_delete() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /test [1] === Method Empty === Chains - 1-* + *-1-* "); let route = RouteBuilder::new().route("/tests").build()?; @@ -26,31 +28,35 @@ fn test_delete() -> Result<(), Box> { assert_eq!(delete, Err(DeleteError::NotFound)); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /test [1] === Method Empty === Chains - 1-* + *-1-* "); let route = RouteBuilder::new().route("(/test)").build()?; let delete = router.delete(&route); assert_eq!( delete, - Err(DeleteError::Path(PathDeleteError::RouteMismatch { + Err(DeleteError::Path(PathDeleteError::Mismatch { route: "(/test)".to_owned(), inserted: "/test".to_owned(), })) ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /test [1] === Method Empty === Chains - 1-* + *-1-* "); let route = RouteBuilder::new().route("/test").build()?; @@ -58,6 +64,8 @@ fn test_delete() -> Result<(), Box> { assert_eq!(delete, 1); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path Empty === Method @@ -77,53 +85,59 @@ fn test_delete_mismatch() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ╰─ test [1] === Method Empty === Chains - 1-* + *-1-* "); let route = RouteBuilder::new().route("/test").build()?; let delete = router.delete(&route); assert_eq!( delete, - Err(DeleteError::Path(PathDeleteError::RouteMismatch { + Err(DeleteError::Path(PathDeleteError::Mismatch { route: "/test".to_owned(), inserted: "(/test)".to_owned() })) ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ╰─ test [1] === Method Empty === Chains - 1-* + *-1-* "); let route = RouteBuilder::new().route("/").build()?; let delete = router.delete(&route); assert_eq!( delete, - Err(DeleteError::Path(PathDeleteError::RouteMismatch { + Err(DeleteError::Path(PathDeleteError::Mismatch { route: "/".to_owned(), inserted: "(/test)".to_owned() })) ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ╰─ test [1] === Method Empty === Chains - 1-* + *-1-* "); let route = RouteBuilder::new().route("(/test)").build()?; @@ -131,6 +145,8 @@ fn test_delete_mismatch() -> Result<(), Box> { assert_eq!(delete, 1); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path Empty === Method @@ -150,20 +166,22 @@ fn test_delete_overlap() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /a [1] ╰─ /b [1] === Method Empty === Chains - 1-* + *-1-* "); let route = RouteBuilder::new().route("/a").build()?; let delete = router.delete(&route); assert_eq!( delete, - Err(DeleteError::Path(PathDeleteError::RouteMismatch { + Err(DeleteError::Path(PathDeleteError::Mismatch { route: "/a".to_owned(), inserted: "/a(/b)".to_owned(), })) @@ -173,7 +191,7 @@ fn test_delete_overlap() -> Result<(), Box> { let delete = router.delete(&route); assert_eq!( delete, - Err(DeleteError::Path(PathDeleteError::RouteMismatch { + Err(DeleteError::Path(PathDeleteError::Mismatch { route: "/a(/b(/c))".to_owned(), inserted: "/a(/b)".to_owned(), })) @@ -184,6 +202,8 @@ fn test_delete_overlap() -> Result<(), Box> { assert_eq!(delete, 1); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path Empty === Method @@ -203,6 +223,8 @@ fn test_delete_empty() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {id} @@ -210,7 +232,7 @@ fn test_delete_empty() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let route = RouteBuilder::new().route("/{id}").build()?; @@ -218,6 +240,8 @@ fn test_delete_empty() -> Result<(), Box> { assert_eq!(delete, Err(DeleteError::NotFound)); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {id} @@ -225,7 +249,7 @@ fn test_delete_empty() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); Ok(()) diff --git a/tests/display.rs b/tests/display.rs index f3173647..a5196713 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -31,6 +31,8 @@ fn test_display_router() -> Result<(), Box> { router.insert(&route, 9)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ├─ api/v1 [7] @@ -56,15 +58,15 @@ fn test_display_router() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* - 3-* - 4-* - 5-* - 6-* - 7-* - 8-* - 9-* + *-1-* + *-2-* + *-3-* + *-4-* + *-5-* + *-6-* + *-7-* + *-8-* + *-9-* "); Ok(()) diff --git a/tests/dynamic.rs b/tests/dynamic.rs index f46c3360..27686816 100644 --- a/tests/dynamic.rs +++ b/tests/dynamic.rs @@ -1,7 +1,9 @@ use similar_asserts::assert_eq; use smallvec::smallvec; use std::error::Error; -use wayfind::{Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router}; +use wayfind::{ + AuthorityMatch, Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router, +}; #[test] fn test_dynamic_simple() -> Result<(), Box> { @@ -11,13 +13,15 @@ fn test_dynamic_simple() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {id} [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/123").build()?; @@ -26,6 +30,10 @@ fn test_dynamic_simple() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{id}", expanded: None, @@ -54,6 +62,8 @@ fn test_dynamic_multiple() -> Result<(), Box> { router.insert(&route, 3)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {year} [1] @@ -64,9 +74,9 @@ fn test_dynamic_multiple() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* - 3-* + *-1-* + *-2-* + *-3-* "); let request = RequestBuilder::new().path("/2024").build()?; @@ -75,6 +85,10 @@ fn test_dynamic_multiple() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{year}", expanded: None, @@ -90,6 +104,10 @@ fn test_dynamic_multiple() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{year}/{month}", expanded: None, @@ -105,6 +123,10 @@ fn test_dynamic_multiple() -> Result<(), Box> { search, Some(Match { data: &3, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{year}/{month}/{day}", expanded: None, @@ -129,6 +151,8 @@ fn test_dynamic_inline() -> Result<(), Box> { router.insert(&route, 3)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {year} [1] @@ -139,9 +163,9 @@ fn test_dynamic_inline() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* - 3-* + *-1-* + *-2-* + *-3-* "); let request = RequestBuilder::new().path("/2024").build()?; @@ -150,6 +174,10 @@ fn test_dynamic_inline() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{year}", expanded: None, @@ -165,6 +193,10 @@ fn test_dynamic_inline() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{year}-{month}", expanded: None, @@ -180,6 +212,10 @@ fn test_dynamic_inline() -> Result<(), Box> { search, Some(Match { data: &3, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{year}-{month}-{day}", expanded: None, @@ -200,6 +236,8 @@ fn test_dynamic_greedy() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {file} @@ -208,7 +246,7 @@ fn test_dynamic_greedy() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/report").build()?; @@ -221,6 +259,10 @@ fn test_dynamic_greedy() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{file}.{extension}", expanded: None, @@ -236,6 +278,10 @@ fn test_dynamic_greedy() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{file}.{extension}", expanded: None, @@ -262,6 +308,8 @@ fn test_dynamic_priority() -> Result<(), Box> { router.insert(&route, 4)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ├─ robots. @@ -274,10 +322,10 @@ fn test_dynamic_priority() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* - 3-* - 4-* + *-1-* + *-2-* + *-3-* + *-4-* "); let request = RequestBuilder::new().path("/robots.txt").build()?; @@ -286,6 +334,10 @@ fn test_dynamic_priority() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/robots.txt", expanded: None, @@ -301,6 +353,10 @@ fn test_dynamic_priority() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/robots.{extension}", expanded: None, @@ -316,6 +372,10 @@ fn test_dynamic_priority() -> Result<(), Box> { search, Some(Match { data: &3, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{name}.txt", expanded: None, @@ -331,6 +391,10 @@ fn test_dynamic_priority() -> Result<(), Box> { search, Some(Match { data: &4, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{name}.{extension}", expanded: None, diff --git a/tests/encoding.rs b/tests/encoding.rs deleted file mode 100644 index b4e7fe9e..00000000 --- a/tests/encoding.rs +++ /dev/null @@ -1,193 +0,0 @@ -use similar_asserts::assert_eq; -use smallvec::smallvec; -use std::error::Error; -use wayfind::{ - errors::{EncodingError, PathSearchError, RequestError, RouteError, SearchError}, - Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router, -}; - -#[test] -fn test_encoding_decoding() -> Result<(), Box> { - let mut router = Router::new(); - - let route = RouteBuilder::new().route("/users/{name}").build()?; - router.insert(&route, 1)?; - - insta::assert_snapshot!(router, @r" - === Path - /users/ - ╰─ {name} [1] - === Method - Empty - === Chains - 1-* - "); - - let request = RequestBuilder::new().path("/users/jos%C3%A9").build()?; // "José" - let search = router.search(&request)?; - assert_eq!( - search, - Some(Match { - data: &1, - path: PathMatch { - route: "/users/{name}", - expanded: None, - parameters: smallvec![("name", "josé")], - }, - method: MethodMatch { method: None } - }) - ); - - Ok(()) -} - -#[test] -fn test_encoding_space() -> Result<(), Box> { - let mut router = Router::new(); - - let route = RouteBuilder::new().route("/user files/{name}").build()?; - router.insert(&route, 1)?; - - insta::assert_snapshot!(router, @r" - === Path - /user files/ - ╰─ {name} [1] - === Method - Empty - === Chains - 1-* - "); - - let request = RequestBuilder::new() - .path("/user%20files/document%20name") - .build()?; // "/user files/document name" - let search = router.search(&request)?; - assert_eq!( - search, - Some(Match { - data: &1, - path: PathMatch { - route: "/user files/{name}", - expanded: None, - parameters: smallvec![("name", "document name")], - }, - method: MethodMatch { method: None } - }) - ); - - Ok(()) -} - -#[test] -fn test_encoding_slash() -> Result<(), Box> { - let mut router = Router::new(); - - let route = RouteBuilder::new().route("/{name}").build()?; - router.insert(&route, 1)?; - let route = RouteBuilder::new().route("/{*path}").build()?; - router.insert(&route, 2)?; - - insta::assert_snapshot!(router, @r" - === Path - / - ├─ {name} [1] - ╰─ {*path} [2] - === Method - Empty - === Chains - 1-* - 2-* - "); - - let request = RequestBuilder::new().path("/johndoe").build()?; - let search = router.search(&request)?; - assert_eq!( - search, - Some(Match { - data: &1, - path: PathMatch { - route: "/{name}", - expanded: None, - parameters: smallvec![("name", "johndoe")], - }, - method: MethodMatch { method: None } - }) - ); - - let request = RequestBuilder::new().path("/john%2Fdoe").build()?; // "john/doe" - let search = router.search(&request)?; - assert_eq!( - search, - Some(Match { - data: &2, - path: PathMatch { - route: "/{*path}", - expanded: None, - parameters: smallvec![("path", "john/doe")], - }, - method: MethodMatch { method: None } - }) - ); - - Ok(()) -} - -#[test] -fn test_encoding_invalid_path() { - let request = RequestBuilder::new().path("/users/%GG").build(); - assert_eq!( - request, - Err(RequestError::Encoding(EncodingError::InvalidEncoding { - input: "/users/%GG".to_owned(), - position: 7, - character: *b"%GG" - })) - ); -} - -#[test] -fn test_encoding_invalid_parameter() { - let route = RouteBuilder::new().route("/users/{%GG}").build(); - assert_eq!( - route, - Err(RouteError::Encoding(EncodingError::InvalidEncoding { - input: "/users/{%GG}".to_owned(), - position: 8, - character: *b"%GG" - })) - ); -} - -#[test] -fn test_encoding_invalid_constraint() { - let route = RouteBuilder::new().route("/users/{id:%GG}").build(); - assert_eq!( - route, - Err(RouteError::Encoding(EncodingError::InvalidEncoding { - input: "/users/{id:%GG}".to_owned(), - position: 11, - character: *b"%GG" - })) - ); -} - -#[test] -fn test_encoding_invalid_value() -> Result<(), Box> { - let mut router = Router::new(); - - let route = RouteBuilder::new().route("/files/{name}").build()?; - router.insert(&route, 1)?; - - let request = RequestBuilder::new().path("/files/my%80file").build()?; - let search = router.search(&request); - assert_eq!( - search, - Err(SearchError::Path(PathSearchError::EncodingError( - EncodingError::Utf8Error { - input: "my�file".to_owned() - } - ))) - ); - - Ok(()) -} diff --git a/tests/escape.rs b/tests/escape.rs index 8e54183a..05020559 100644 --- a/tests/escape.rs +++ b/tests/escape.rs @@ -1,7 +1,9 @@ use similar_asserts::assert_eq; use smallvec::smallvec; use std::error::Error; -use wayfind::{Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router}; +use wayfind::{ + AuthorityMatch, Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router, +}; #[test] fn test_escape_parameter() -> Result<(), Box> { @@ -11,12 +13,14 @@ fn test_escape_parameter() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/{id} [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/users/{id}").build()?; @@ -25,6 +29,10 @@ fn test_escape_parameter() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: r"/users/\{id\}", expanded: None, @@ -48,12 +56,14 @@ fn test_escape_group() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /(not-optional) [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/(not-optional)").build()?; @@ -62,6 +72,10 @@ fn test_escape_group() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: r"/\(not-optional\)", expanded: None, @@ -86,6 +100,8 @@ fn test_escape_nested() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ╰─ a [1] @@ -93,7 +109,7 @@ fn test_escape_nested() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/a/{param}").build()?; @@ -102,6 +118,10 @@ fn test_escape_nested() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: r"(/a(/\{param\}))", expanded: Some("/a/\\{param\\}"), @@ -121,6 +141,10 @@ fn test_escape_nested() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: r"(/a(/\{param\}))", expanded: Some("/a"), @@ -136,6 +160,10 @@ fn test_escape_nested() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: r"(/a(/\{param\}))", expanded: Some("/"), diff --git a/tests/gitlab.rs b/tests/gitlab.rs index 29dad4f2..cf4aab42 100644 --- a/tests/gitlab.rs +++ b/tests/gitlab.rs @@ -34,6 +34,8 @@ fn test_gitlab_delete() -> Result<(), Box> { } insta::assert_snapshot!(router, @r" + === Authority + Empty === Path Empty === Method @@ -56,6 +58,8 @@ fn test_gitlab_display() -> Result<(), Box> { } insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [6] ├─ toogle_engine [1622] @@ -11727,2088 +11731,2088 @@ fn test_gitlab_display() -> Result<(), Box> { [1704] ╰─ GET [2073] === Chains - 1-* - 2-1 - 3-2 - 4-* - 5-3 - 5-884 - 6-* - 6-1927 - 7-* - 8-* - 9-* - 10-* - 11-* - 12-* - 13-4 - 14-5 - 15-6 - 16-7 - 17-8 - 18-9 - 19-10 - 19-13 - 19-14 - 19-15 - 20-11 - 21-12 - 22-16 - 23-17 - 23-18 - 24-19 - 25-20 - 26-21 - 27-22 - 28-23 - 29-24 - 30-25 - 31-26 - 32-27 - 33-28 - 34-29 - 35-30 - 36-31 - 37-32 - 38-33 - 39-34 - 40-35 - 41-36 - 42-37 - 43-38 - 44-39 - 45-40 - 45-41 - 46-42 - 47-43 - 48-44 - 48-50 - 48-51 - 48-52 - 49-45 - 50-46 - 51-47 - 52-48 - 53-49 - 54-53 - 55-54 - 56-55 - 57-56 - 58-57 - 59-58 - 59-61 - 60-59 - 60-65 - 60-66 - 60-67 - 61-60 - 62-62 - 63-63 - 64-64 - 65-68 - 66-69 - 67-70 - 68-71 - 69-72 - 70-73 - 71-74 - 72-75 - 73-76 - 74-77 - 74-80 - 75-78 - 75-82 - 75-83 - 76-79 - 77-81 - 78-84 - 78-85 - 78-86 - 79-87 - 80-88 - 81-89 - 82-90 - 83-91 - 83-97 - 83-98 - 83-99 - 84-92 - 85-93 - 86-94 - 87-95 - 88-96 - 89-100 - 90-101 - 91-102 - 92-103 - 93-104 - 94-105 - 95-106 - 96-107 - 97-108 - 97-111 - 98-109 - 98-113 - 98-114 - 99-110 - 100-112 - 101-115 - 102-116 - 103-117 - 104-118 - 105-119 - 106-120 - 106-121 - 107-122 - 107-125 - 108-123 - 109-124 - 110-126 - 111-127 - 112-128 - 112-129 - 113-130 - 114-131 - 115-132 - 116-133 - 116-134 - 116-135 - 117-136 - 118-137 - 119-138 - 120-139 - 121-140 - 121-143 - 122-141 - 122-147 - 122-148 - 122-149 - 123-142 - 124-144 - 125-145 - 126-146 - 127-150 - 128-151 - 129-152 - 130-153 - 130-156 - 131-154 - 131-158 - 131-159 - 132-155 - 133-157 - 134-160 - 134-163 - 135-161 - 135-165 - 135-166 - 136-162 - 137-164 - 138-167 - 138-168 - 139-169 - 140-170 - 141-171 - 142-172 - 142-173 - 143-174 - 144-175 - 145-176 - 146-177 - 147-178 - 148-179 - 148-180 - 149-181 - 150-182 - 151-183 - 151-184 - 152-185 - 152-188 - 153-186 - 153-190 - 153-191 - 153-192 - 154-187 - 155-189 - 156-193 - 156-194 - 156-196 - 157-195 - 158-197 - 159-198 - 160-199 - 161-200 - 162-201 - 163-202 - 164-203 - 164-207 - 164-209 - 164-210 - 165-204 - 166-205 - 167-206 - 168-208 - 169-211 - 169-212 - 169-213 - 170-214 - 171-215 - 172-216 - 173-217 - 174-218 - 174-226 - 174-228 - 174-229 - 175-219 - 176-220 - 177-221 - 178-222 - 179-223 - 180-224 - 181-225 - 182-227 - 183-230 - 184-231 - 185-232 - 186-233 - 187-234 - 188-235 - 189-236 - 190-237 - 191-238 - 192-239 - 193-240 - 193-243 - 194-241 - 194-247 - 194-248 - 195-242 - 196-244 - 197-245 - 198-246 - 199-249 - 200-250 - 201-251 - 202-252 - 203-253 - 204-254 - 205-255 - 206-256 - 207-257 - 208-258 - 208-266 - 209-259 - 210-260 - 210-274 - 210-280 - 210-281 - 211-261 - 212-262 - 213-263 - 214-264 - 215-265 - 216-267 - 217-268 - 218-269 - 219-270 - 220-271 - 221-272 - 222-273 - 223-275 - 224-276 - 225-277 - 226-278 - 227-279 - 228-282 - 229-283 - 230-284 - 230-287 - 230-289 - 230-291 - 231-285 - 231-288 - 231-290 - 231-292 - 232-286 - 233-293 - 234-294 - 235-295 - 236-296 - 237-297 - 238-298 - 239-299 - 240-300 - 241-301 - 242-302 - 243-303 - 244-304 - 245-305 - 246-306 - 247-307 - 248-308 - 249-309 - 250-310 - 251-311 - 252-312 - 253-313 - 254-314 - 255-315 - 255-319 - 256-316 - 256-320 - 257-317 - 258-318 - 259-321 - 260-322 - 261-323 - 262-324 - 263-325 - 264-326 - 265-327 - 266-328 - 267-329 - 268-330 - 269-331 - 270-332 - 271-333 - 272-334 - 273-335 - 274-336 - 275-337 - 276-338 - 277-339 - 278-340 - 279-341 - 280-342 - 281-343 - 282-344 - 283-345 - 284-346 - 285-347 - 286-348 - 286-350 - 287-349 - 288-351 - 288-352 - 288-353 - 289-354 - 290-355 - 291-356 - 292-357 - 293-358 - 294-359 - 295-360 - 296-361 - 297-362 - 298-363 - 299-364 - 300-365 - 301-366 - 302-367 - 303-368 - 304-369 - 305-370 - 306-371 - 307-372 - 307-373 - 308-374 - 309-375 - 310-376 - 311-377 - 312-378 - 313-379 - 314-380 - 315-381 - 316-382 - 317-383 - 318-384 - 319-385 - 320-386 - 321-387 - 322-388 - 323-389 - 324-390 - 325-391 - 326-392 - 326-398 - 327-393 - 327-409 - 327-412 - 327-413 - 328-394 - 329-395 - 330-396 - 331-397 - 332-399 - 333-400 - 334-401 - 335-402 - 336-403 - 337-404 - 338-405 - 339-406 - 340-407 - 341-408 - 342-410 - 343-411 - 344-414 - 345-415 - 346-416 - 347-417 - 348-418 - 349-419 - 350-420 - 351-421 - 352-422 - 353-423 - 354-424 - 355-425 - 356-426 - 357-427 - 358-428 - 359-429 - 360-430 - 361-431 - 362-432 - 362-435 - 363-433 - 363-437 - 363-438 - 363-439 - 364-434 - 365-436 - 366-440 - 367-441 - 368-442 - 369-443 - 370-444 - 371-445 - 372-446 - 373-447 - 374-448 - 375-449 - 376-450 - 377-451 - 378-452 - 379-453 - 380-454 - 381-455 - 382-456 - 383-457 - 384-458 - 385-459 - 386-460 - 387-461 - 388-462 - 389-463 - 390-464 - 391-465 - 392-466 - 393-467 - 394-468 - 394-474 - 394-475 - 394-476 - 395-469 - 396-470 - 397-471 - 398-472 - 399-473 - 400-477 - 401-478 - 402-479 - 403-480 - 404-481 - 405-482 - 406-483 - 407-484 - 408-485 - 409-486 - 410-487 - 411-488 - 412-489 - 413-490 - 414-491 - 415-492 - 416-493 - 416-494 - 416-495 - 417-496 - 418-497 - 419-498 - 420-499 - 421-500 - 422-501 - 423-502 - 424-503 - 425-504 - 426-505 - 426-506 - 427-507 - 428-508 - 429-509 - 429-511 - 430-510 - 430-512 - 430-513 - 431-514 - 432-515 - 432-521 - 433-516 - 434-517 - 435-518 - 435-524 - 435-527 - 435-528 - 436-519 - 437-520 - 438-522 - 439-523 - 440-525 - 441-526 - 442-529 - 442-531 - 443-530 - 443-532 - 443-533 - 444-534 - 444-536 - 445-535 - 445-538 - 445-539 - 446-537 - 447-540 - 447-542 - 448-541 - 449-543 - 449-544 - 449-545 - 450-546 - 451-547 - 452-548 - 453-549 - 453-558 - 453-559 - 454-550 - 455-551 - 456-552 - 457-553 - 458-554 - 458-555 - 459-556 - 460-557 - 461-560 - 462-561 - 463-562 - 464-563 - 465-564 - 466-565 - 467-566 - 467-569 - 468-567 - 468-571 - 468-572 - 469-568 - 470-570 - 471-573 - 472-574 - 473-575 - 474-576 - 475-577 - 476-578 - 477-579 - 477-582 - 477-588 - 477-590 - 478-580 - 478-589 - 479-581 - 480-583 - 481-584 - 482-585 - 483-586 - 484-587 - 485-591 - 486-592 - 487-593 - 488-594 - 489-595 - 489-598 - 490-596 - 490-601 - 490-602 - 491-597 - 492-599 - 493-600 - 494-603 - 494-605 - 495-604 - 496-606 - 497-607 - 498-608 - 498-611 - 499-609 - 499-617 - 499-618 - 499-619 - 500-610 - 501-612 - 502-613 - 503-614 - 504-615 - 505-616 - 506-620 - 506-621 - 507-622 - 508-623 - 509-624 - 510-625 - 511-626 - 511-627 - 511-628 - 512-629 - 513-630 - 513-631 - 513-632 - 514-633 - 514-634 - 515-635 - 516-636 - 517-637 - 518-638 - 519-639 - 520-640 - 521-641 - 521-648 - 521-649 - 521-650 - 522-642 - 523-643 - 524-644 - 525-645 - 526-646 - 527-647 - 528-651 - 528-653 - 529-652 - 530-654 - 530-655 - 530-656 - 530-657 - 531-658 - 532-659 - 533-660 - 534-661 - 535-662 - 536-663 - 537-664 - 538-665 - 539-666 - 540-667 - 541-668 - 542-669 - 543-670 - 544-671 - 545-672 - 546-673 - 547-674 - 548-675 - 549-676 - 550-677 - 550-680 - 550-682 - 550-684 - 551-678 - 551-683 - 552-679 - 553-681 - 554-685 - 554-686 - 555-687 - 556-688 - 556-689 - 556-690 - 557-691 - 557-694 - 558-692 - 558-697 - 558-698 - 558-699 - 559-693 - 560-695 - 561-696 - 562-700 - 563-701 - 564-702 - 564-703 - 564-704 - 565-705 - 566-706 - 567-707 - 567-709 - 568-708 - 568-712 - 568-713 - 568-714 - 569-710 - 570-711 - 571-715 - 572-716 - 573-717 - 574-718 - 575-719 - 576-720 - 577-721 - 578-722 - 579-723 - 579-724 - 580-725 - 580-726 - 581-727 - 582-728 - 583-729 - 584-730 - 585-731 - 586-732 - 587-733 - 588-734 - 589-735 - 590-736 - 591-737 - 592-738 - 593-739 - 594-740 - 595-741 - 595-742 - 596-743 - 597-744 - 598-745 - 599-746 - 600-747 - 601-748 - 602-749 - 603-750 - 604-751 - 605-752 - 605-753 - 605-754 - 606-755 - 606-1801 - 607-756 - 607-765 - 607-767 - 608-757 - 609-758 - 610-759 - 611-760 - 612-761 - 613-762 - 614-763 - 615-764 - 616-766 - 617-768 - 618-769 - 619-770 - 620-771 - 621-772 - 622-773 - 623-774 - 624-775 - 625-776 - 626-777 - 627-778 - 628-779 - 629-780 - 630-781 - 631-782 - 632-783 - 633-784 - 634-785 - 635-786 - 636-787 - 637-788 - 638-789 - 639-790 - 640-791 - 641-792 - 642-793 - 643-794 - 644-795 - 645-796 - 646-797 - 647-798 - 648-799 - 649-800 - 650-801 - 651-802 - 652-803 - 653-804 - 654-805 - 655-806 - 656-807 - 657-808 - 658-809 - 659-810 - 660-811 - 661-812 - 662-813 - 663-814 - 664-815 - 665-816 - 666-817 - 666-819 - 667-818 - 668-820 - 669-821 - 670-822 - 671-823 - 672-824 - 673-825 - 674-826 - 675-827 - 676-828 - 677-829 - 678-830 - 679-831 - 680-832 - 681-833 - 682-834 - 683-835 - 684-836 - 685-837 - 686-838 - 687-839 - 688-840 - 689-841 - 690-842 - 691-843 - 692-844 - 693-845 - 694-846 - 695-847 - 696-848 - 697-849 - 698-850 - 699-851 - 700-852 - 701-853 - 702-854 - 703-855 - 704-856 - 705-857 - 706-858 - 707-859 - 708-860 - 709-861 - 710-862 - 710-863 - 711-864 - 711-865 - 712-866 - 713-867 - 714-868 - 715-869 - 716-870 - 716-873 - 716-874 - 717-871 - 717-872 - 718-875 - 719-876 - 720-877 - 720-878 - 721-879 - 722-880 - 722-881 - 723-882 - 723-883 - 724-885 - 725-886 - 726-887 - 727-888 - 728-889 - 729-890 - 730-891 - 731-892 - 732-893 - 733-894 - 734-895 - 735-896 - 736-897 - 737-898 - 738-899 - 739-900 - 740-901 - 741-902 - 742-903 - 742-907 - 743-904 - 743-910 - 743-911 - 743-912 - 744-905 - 745-906 - 746-908 - 747-909 - 748-913 - 748-914 - 748-915 - 749-916 - 750-917 - 751-918 - 752-919 - 752-921 - 753-920 - 754-922 - 755-923 - 756-924 - 757-925 - 758-926 - 759-927 - 759-928 - 760-929 - 761-930 - 761-931 - 762-932 - 763-933 - 763-935 - 763-938 - 764-934 - 764-936 - 764-937 - 765-939 - 765-940 - 766-941 - 767-942 - 768-943 - 769-944 - 770-945 - 771-946 - 772-947 - 773-948 - 774-949 - 775-950 - 776-951 - 777-952 - 778-953 - 778-956 - 778-957 - 779-954 - 780-955 - 781-958 - 782-959 - 783-960 - 784-961 - 785-962 - 786-963 - 787-964 - 788-965 - 789-966 - 790-967 - 791-968 - 791-971 - 792-969 - 793-970 - 794-972 - 795-973 - 796-974 - 797-975 - 797-977 - 798-976 - 799-978 - 800-979 - 800-980 - 801-981 - 801-982 - 801-983 - 802-984 - 802-985 - 802-986 - 803-987 - 804-988 - 805-989 - 806-990 - 806-992 - 806-995 - 807-991 - 808-993 - 809-994 - 810-996 - 811-997 - 812-998 - 813-999 - 814-1000 - 814-1007 - 815-1001 - 815-1015 - 815-1020 - 815-1021 - 816-1002 - 817-1003 - 818-1004 - 819-1005 - 820-1006 - 821-1008 - 822-1009 - 823-1010 - 824-1011 - 825-1012 - 826-1013 - 827-1014 - 828-1016 - 829-1017 - 830-1018 - 831-1019 - 832-1022 - 833-1023 - 834-1024 - 835-1025 - 836-1026 - 837-1027 - 838-1028 - 839-1029 - 840-1030 - 841-1031 - 842-1032 - 843-1033 - 844-1034 - 845-1035 - 846-1036 - 847-1037 - 848-1038 - 849-1039 - 849-1042 - 850-1040 - 850-1044 - 850-1045 - 850-1046 - 851-1041 - 852-1043 - 853-1047 - 854-1048 - 855-1049 - 856-1050 - 857-1051 - 858-1052 - 859-1053 - 860-1054 - 861-1055 - 862-1056 - 863-1057 - 864-1058 - 865-1059 - 866-1060 - 867-1061 - 868-1062 - 869-1063 - 870-1064 - 871-1065 - 872-1066 - 873-1067 - 874-1068 - 875-1069 - 876-1070 - 877-1071 - 878-1072 - 879-1073 - 880-1074 - 881-1075 - 882-1076 - 883-1077 - 884-1078 - 884-1079 - 885-1080 - 886-1081 - 887-1082 - 888-1083 - 889-1084 - 890-1085 - 891-1086 - 892-1087 - 893-1088 - 894-1089 - 894-1091 - 894-1096 - 894-1098 - 895-1090 - 896-1092 - 897-1093 - 898-1094 - 899-1095 - 900-1097 - 901-1099 - 902-1100 - 903-1101 - 904-1102 - 904-1106 - 905-1103 - 906-1104 - 907-1105 - 908-1107 - 909-1108 - 910-1109 - 911-1110 - 912-1111 - 913-1112 - 914-1113 - 915-1114 - 916-1115 - 917-1116 - 918-1117 - 919-1118 - 919-1119 - 920-1120 - 921-1121 - 922-1122 - 923-1123 - 924-1124 - 925-1125 - 926-1126 - 927-1127 - 927-1133 - 927-1134 - 927-1135 - 928-1128 - 929-1129 - 930-1130 - 931-1131 - 932-1132 - 933-1136 - 934-1137 - 935-1138 - 936-1139 - 937-1140 - 938-1141 - 939-1142 - 940-1143 - 941-1144 - 942-1145 - 943-1146 - 944-1147 - 945-1148 - 946-1149 - 947-1150 - 948-1151 - 948-1153 - 949-1152 - 950-1154 - 951-1155 - 952-1156 - 953-1157 - 954-1158 - 955-1159 - 956-1160 - 957-1161 - 958-1162 - 959-1163 - 960-1164 - 961-1165 - 962-1166 - 963-1167 - 964-1168 - 965-1169 - 966-1170 - 967-1171 - 968-1172 - 969-1173 - 969-1178 - 970-1174 - 971-1175 - 972-1176 - 973-1177 - 974-1179 - 975-1180 - 975-1181 - 976-1182 - 977-1183 - 978-1184 - 979-1185 - 980-1186 - 981-1187 - 982-1188 - 983-1189 - 983-1191 - 984-1190 - 985-1192 - 986-1193 - 986-1196 - 987-1194 - 988-1195 - 989-1197 - 990-1198 - 991-1199 - 992-1200 - 992-1204 - 992-1205 - 993-1201 - 994-1202 - 995-1203 - 996-1206 - 997-1207 - 998-1208 - 999-1209 - 1000-1210 - 1001-1211 - 1002-1212 - 1002-1214 - 1003-1213 - 1004-1215 - 1004-1218 - 1005-1216 - 1005-1220 - 1005-1221 - 1005-1222 - 1006-1217 - 1007-1219 - 1008-1223 - 1009-1224 - 1010-1225 - 1011-1226 - 1012-1227 - 1013-1228 - 1014-1229 - 1015-1230 - 1015-1231 - 1016-1232 - 1017-1233 - 1018-1234 - 1019-1235 - 1020-1236 - 1020-1237 - 1021-1238 - 1022-1239 - 1023-1240 - 1024-1241 - 1025-1242 - 1025-1243 - 1026-1244 - 1027-1245 - 1027-1246 - 1028-1247 - 1029-1248 - 1030-1249 - 1031-1250 - 1032-1251 - 1033-1252 - 1033-1253 - 1033-1254 - 1034-1255 - 1035-1256 - 1036-1257 - 1037-1258 - 1038-1259 - 1039-1260 - 1040-1261 - 1040-1264 - 1041-1262 - 1041-1266 - 1041-1267 - 1042-1263 - 1043-1265 - 1044-1268 - 1045-1269 - 1045-1271 - 1046-1270 - 1047-1272 - 1048-1273 - 1049-1274 - 1050-1275 - 1051-1276 - 1052-1277 - 1053-1278 - 1054-1279 - 1055-1280 - 1056-1281 - 1057-1282 - 1058-1283 - 1059-1284 - 1060-1285 - 1061-1286 - 1062-1287 - 1063-1288 - 1063-1290 - 1064-1289 - 1065-1291 - 1066-1292 - 1067-1293 - 1068-1294 - 1068-1304 - 1069-1295 - 1070-1296 - 1071-1297 - 1072-1298 - 1073-1299 - 1073-1312 - 1073-1316 - 1073-1317 - 1074-1300 - 1075-1301 - 1076-1302 - 1077-1303 - 1078-1305 - 1079-1306 - 1080-1307 - 1081-1308 - 1082-1309 - 1083-1310 - 1084-1311 - 1085-1313 - 1086-1314 - 1087-1315 - 1088-1318 - 1088-1321 - 1088-1325 - 1088-1327 - 1089-1319 - 1089-1326 - 1090-1320 - 1091-1322 - 1092-1323 - 1093-1324 - 1094-1328 - 1095-1329 - 1096-1330 - 1097-1331 - 1098-1332 - 1099-1333 - 1100-1334 - 1101-1335 - 1102-1336 - 1103-1337 - 1104-1338 - 1105-1339 - 1106-1340 - 1107-1341 - 1108-1342 - 1109-1343 - 1110-1344 - 1111-1345 - 1112-1346 - 1112-1350 - 1113-1347 - 1113-1356 - 1113-1357 - 1114-1348 - 1115-1349 - 1116-1351 - 1117-1352 - 1118-1353 - 1119-1354 - 1120-1355 - 1121-1358 - 1122-1359 - 1123-1360 - 1124-1361 - 1125-1362 - 1126-1363 - 1127-1364 - 1128-1365 - 1129-1366 - 1130-1367 - 1131-1368 - 1132-1369 - 1133-1370 - 1134-1371 - 1135-1372 - 1136-1373 - 1137-1374 - 1138-1375 - 1139-1376 - 1140-1377 - 1141-1378 - 1142-1379 - 1143-1380 - 1144-1381 - 1144-1401 - 1144-1408 - 1144-1409 - 1145-1382 - 1146-1383 - 1147-1384 - 1148-1385 - 1149-1386 - 1150-1387 - 1151-1388 - 1151-1417 - 1152-1389 - 1153-1390 - 1154-1391 - 1155-1392 - 1156-1393 - 1157-1394 - 1158-1395 - 1159-1396 - 1160-1397 - 1161-1398 - 1162-1399 - 1163-1400 - 1164-1402 - 1165-1403 - 1166-1404 - 1167-1405 - 1168-1406 - 1169-1407 - 1170-1410 - 1171-1411 - 1172-1412 - 1173-1413 - 1174-1414 - 1175-1415 - 1176-1416 - 1177-1418 - 1178-1419 - 1179-1420 - 1180-1421 - 1181-1422 - 1182-1423 - 1183-1424 - 1184-1425 - 1185-1426 - 1186-1427 - 1187-1428 - 1188-1429 - 1189-1430 - 1190-1431 - 1191-1432 - 1191-1435 - 1192-1433 - 1192-1437 - 1192-1438 - 1193-1434 - 1194-1436 - 1195-1439 - 1196-1440 - 1197-1441 - 1198-1442 - 1199-1443 - 1199-1446 - 1200-1444 - 1200-1453 - 1200-1454 - 1200-1455 - 1201-1445 - 1202-1447 - 1203-1448 - 1204-1449 - 1205-1450 - 1206-1451 - 1207-1452 - 1208-1456 - 1208-1458 - 1208-1459 - 1209-1457 - 1210-1460 - 1211-1461 - 1212-1462 - 1212-1465 - 1212-1467 - 1212-1469 - 1213-1463 - 1213-1468 - 1214-1464 - 1215-1466 - 1216-1470 - 1216-1471 - 1217-1472 - 1217-1474 - 1218-1473 - 1219-1475 - 1220-1476 - 1221-1477 - 1222-1478 - 1222-1482 - 1223-1479 - 1224-1480 - 1225-1481 - 1226-1483 - 1227-1484 - 1228-1485 - 1229-1486 - 1229-1492 - 1229-1493 - 1230-1487 - 1231-1488 - 1232-1489 - 1232-1491 - 1233-1490 - 1234-1494 - 1235-1495 - 1236-1496 - 1237-1497 - 1238-1498 - 1239-1499 - 1240-1500 - 1240-1502 - 1241-1501 - 1242-1503 - 1242-1505 - 1242-1506 - 1242-1507 - 1243-1504 - 1244-1508 - 1245-1509 - 1246-1510 - 1246-1514 - 1246-1515 - 1246-1516 - 1247-1511 - 1248-1512 - 1249-1513 - 1250-1517 - 1251-1518 - 1252-1519 - 1253-1520 - 1254-1521 - 1254-1524 - 1255-1522 - 1255-1528 - 1255-1529 - 1256-1523 - 1257-1525 - 1258-1526 - 1259-1527 - 1260-1530 - 1261-1531 - 1262-1532 - 1263-1533 - 1264-1534 - 1264-1538 - 1265-1535 - 1265-1546 - 1266-1536 - 1267-1537 - 1268-1539 - 1269-1540 - 1270-1541 - 1271-1542 - 1272-1543 - 1273-1544 - 1274-1545 - 1275-1547 - 1276-1548 - 1277-1549 - 1278-1550 - 1279-1551 - 1280-1552 - 1281-1553 - 1281-1554 - 1281-1555 - 1282-1556 - 1283-1557 - 1283-1563 - 1283-1564 - 1284-1558 - 1285-1559 - 1286-1560 - 1286-1561 - 1287-1562 - 1288-1565 - 1289-1566 - 1289-1569 - 1290-1567 - 1290-1571 - 1290-1572 - 1291-1568 - 1292-1570 - 1293-1573 - 1294-1574 - 1294-1576 - 1295-1575 - 1295-1577 - 1295-1578 - 1295-1579 - 1296-1580 - 1297-1581 - 1297-1583 - 1297-1584 - 1298-1582 - 1299-1585 - 1299-1587 - 1300-1586 - 1300-1588 - 1300-1589 - 1300-1590 - 1301-1591 - 1301-1592 - 1302-1593 - 1303-1594 - 1304-1595 - 1305-1596 - 1306-1597 - 1307-1598 - 1308-1599 - 1309-1600 - 1310-1601 - 1311-1602 - 1312-1603 - 1312-1605 - 1313-1604 - 1314-1606 - 1315-1607 - 1316-1608 - 1317-1609 - 1318-1610 - 1319-1611 - 1320-1612 - 1321-1613 - 1322-1614 - 1323-1615 - 1324-1616 - 1325-1617 - 1326-1618 - 1327-1619 - 1328-1620 - 1329-1621 - 1330-1622 - 1331-1623 - 1332-1624 - 1332-1631 - 1332-1634 - 1332-1635 - 1333-1625 - 1334-1626 - 1335-1627 - 1336-1628 - 1337-1629 - 1338-1630 - 1339-1632 - 1340-1633 - 1341-1636 - 1342-1637 - 1343-1638 - 1344-1639 - 1345-1640 - 1346-1641 - 1347-1642 - 1348-1643 - 1349-1644 - 1350-1645 - 1351-1646 - 1352-1647 - 1353-1648 - 1354-1649 - 1355-1650 - 1356-1651 - 1357-1652 - 1358-1653 - 1359-1654 - 1360-1655 - 1361-1656 - 1362-1657 - 1363-1658 - 1364-1659 - 1364-1661 - 1365-1660 - 1365-1663 - 1365-1664 - 1366-1662 - 1367-1665 - 1368-1666 - 1368-1667 - 1369-1668 - 1369-1669 - 1369-1670 - 1369-1671 - 1369-1672 - 1370-1673 - 1371-1674 - 1371-1675 - 1372-1676 - 1373-1677 - 1373-1678 - 1373-1679 - 1374-1680 - 1375-1681 - 1376-1682 - 1377-1683 - 1378-1684 - 1379-1685 - 1379-1686 - 1379-1687 - 1380-1688 - 1381-1689 - 1382-1690 - 1383-1691 - 1384-1692 - 1385-1693 - 1385-1694 - 1386-1695 - 1386-1696 - 1386-1697 - 1387-1698 - 1388-1699 - 1389-1700 - 1389-1701 - 1389-1702 - 1390-1703 - 1391-1704 - 1392-1705 - 1393-1706 - 1394-1707 - 1395-1708 - 1395-1709 - 1395-1710 - 1396-1711 - 1396-1714 - 1396-1715 - 1397-1712 - 1398-1713 - 1399-1716 - 1400-1717 - 1401-1718 - 1402-1719 - 1403-1720 - 1404-1721 - 1405-1722 - 1406-1723 - 1407-1724 - 1408-1725 - 1409-1726 - 1410-1727 - 1411-1728 - 1411-1730 - 1412-1729 - 1412-1732 - 1413-1731 - 1414-1733 - 1414-1735 - 1415-1734 - 1416-1736 - 1417-1737 - 1418-1738 - 1419-1739 - 1420-1740 - 1421-1741 - 1422-1742 - 1423-1743 - 1424-1744 - 1425-1745 - 1426-1746 - 1426-1748 - 1427-1747 - 1427-1749 - 1427-1750 - 1428-1751 - 1429-1752 - 1430-1753 - 1431-1754 - 1432-1755 - 1432-1756 - 1432-1757 - 1433-1758 - 1434-1759 - 1434-1761 - 1435-1760 - 1435-1762 - 1435-1763 - 1436-1764 - 1437-1765 - 1438-1766 - 1439-1767 - 1440-1768 - 1441-1769 - 1442-1770 - 1442-1803 - 1443-1771 - 1443-1780 - 1443-1782 - 1444-1772 - 1445-1773 - 1446-1774 - 1447-1775 - 1448-1776 - 1449-1777 - 1450-1778 - 1451-1779 - 1452-1781 - 1453-1783 - 1454-1784 - 1455-1785 - 1456-1786 - 1457-1787 - 1458-1788 - 1459-1789 - 1460-1790 - 1461-1791 - 1462-1792 - 1463-1793 - 1464-1794 - 1465-1795 - 1466-1796 - 1467-1797 - 1468-1798 - 1469-1799 - 1470-1800 - 1471-1802 - 1472-1804 - 1473-1805 - 1474-1806 - 1475-1807 - 1476-1808 - 1477-1809 - 1478-1810 - 1479-1811 - 1480-1812 - 1481-1813 - 1482-1814 - 1483-1815 - 1484-1816 - 1485-1817 - 1486-1818 - 1487-1819 - 1488-1820 - 1489-1821 - 1490-1822 - 1491-1823 - 1492-1824 - 1493-1825 - 1494-1826 - 1495-1827 - 1496-1828 - 1497-1829 - 1498-1830 - 1499-1831 - 1500-1832 - 1501-1833 - 1502-1834 - 1503-1835 - 1504-1836 - 1505-1837 - 1506-1838 - 1507-1839 - 1508-1840 - 1509-1841 - 1510-1842 - 1511-1843 - 1512-1844 - 1513-1845 - 1514-1846 - 1515-1847 - 1516-1848 - 1517-1849 - 1518-1850 - 1519-1851 - 1520-1852 - 1521-1853 - 1522-1854 - 1523-1855 - 1524-1856 - 1525-1857 - 1526-1858 - 1527-1859 - 1528-1860 - 1529-1861 - 1529-1862 - 1529-1865 - 1529-1866 - 1530-1863 - 1531-1864 - 1532-1867 - 1533-1868 - 1534-1869 - 1535-1870 - 1536-1871 - 1536-1872 - 1536-1873 - 1537-1874 - 1537-1877 - 1538-1875 - 1538-1881 - 1538-1882 - 1538-1883 - 1539-1876 - 1540-1878 - 1541-1879 - 1542-1880 - 1543-1884 - 1544-1885 - 1545-1886 - 1546-1887 - 1547-1888 - 1548-1889 - 1549-1890 - 1550-1891 - 1551-1892 - 1552-1893 - 1553-1894 - 1554-1895 - 1555-1896 - 1556-1897 - 1557-1898 - 1558-1899 - 1559-1900 - 1560-1901 - 1560-1907 - 1561-1902 - 1561-1908 - 1562-1903 - 1562-1911 - 1562-1915 - 1562-1917 - 1563-1904 - 1563-1912 - 1563-1916 - 1563-1918 - 1564-1905 - 1565-1906 - 1566-1909 - 1567-1910 - 1568-1913 - 1569-1914 - 1570-1919 - 1571-1920 - 1572-1921 - 1573-1922 - 1574-1923 - 1575-1924 - 1576-1925 - 1577-1926 - 1578-1928 - 1579-1929 - 1580-1930 - 1581-1931 - 1582-1932 - 1583-1933 - 1584-1934 - 1585-1935 - 1586-1936 - 1587-1937 - 1588-1938 - 1589-1939 - 1589-1941 - 1590-1940 - 1591-1942 - 1592-1943 - 1593-1944 - 1593-1948 - 1594-1945 - 1594-1949 - 1595-1946 - 1596-1947 - 1597-1950 - 1598-1951 - 1599-1952 - 1600-1953 - 1601-1954 - 1602-1955 - 1603-1956 - 1604-1957 - 1605-1958 - 1606-1959 - 1607-1960 - 1608-1961 - 1609-1962 - 1610-1963 - 1611-1964 - 1612-1965 - 1613-1966 - 1614-1967 - 1614-1970 - 1615-1968 - 1616-1969 - 1616-1972 - 1616-1973 - 1617-1971 - 1618-1974 - 1619-1975 - 1620-1976 - 1621-1977 - 1621-1979 - 1621-1980 - 1621-1981 - 1622-1978 - 1623-1982 - 1624-1983 - 1625-1984 - 1626-1985 - 1627-1986 - 1628-1987 - 1629-1988 - 1630-1989 - 1631-1990 - 1632-1991 - 1633-1992 - 1634-1993 - 1635-1994 - 1636-1995 - 1637-1996 - 1637-1998 - 1638-1997 - 1639-1999 - 1640-2000 - 1641-2001 - 1642-2002 - 1642-2006 - 1642-2007 - 1643-2003 - 1644-2004 - 1645-2005 - 1646-2008 - 1646-2009 - 1647-2010 - 1648-2011 - 1649-2012 - 1649-2013 - 1649-2014 - 1650-2015 - 1650-2017 - 1651-2016 - 1651-2019 - 1652-2018 - 1653-2020 - 1654-2021 - 1655-2022 - 1656-2023 - 1657-2024 - 1658-2025 - 1659-2026 - 1660-2027 - 1661-2028 - 1662-2029 - 1663-2030 - 1664-2031 - 1665-2032 - 1666-2033 - 1667-2034 - 1668-2035 - 1669-2036 - 1670-2037 - 1671-2038 - 1672-2039 - 1673-2040 - 1674-2041 - 1675-2042 - 1676-2043 - 1677-2044 - 1678-2045 - 1679-2046 - 1680-2047 - 1681-2048 - 1682-2049 - 1683-2050 - 1684-2051 - 1684-2052 - 1685-2053 - 1686-2054 - 1687-2055 - 1688-2056 - 1689-2057 - 1690-2058 - 1691-2059 - 1692-2060 - 1693-2061 - 1694-2062 - 1695-2063 - 1696-2064 - 1697-2065 - 1698-2066 - 1699-2067 - 1700-2068 - 1701-2069 - 1702-2070 - 1702-2071 - 1703-2072 - 1704-2073 + *-1-* + *-2-1 + *-3-2 + *-4-* + *-5-3 + *-5-884 + *-6-* + *-6-1927 + *-7-* + *-8-* + *-9-* + *-10-* + *-11-* + *-12-* + *-13-4 + *-14-5 + *-15-6 + *-16-7 + *-17-8 + *-18-9 + *-19-10 + *-19-13 + *-19-14 + *-19-15 + *-20-11 + *-21-12 + *-22-16 + *-23-17 + *-23-18 + *-24-19 + *-25-20 + *-26-21 + *-27-22 + *-28-23 + *-29-24 + *-30-25 + *-31-26 + *-32-27 + *-33-28 + *-34-29 + *-35-30 + *-36-31 + *-37-32 + *-38-33 + *-39-34 + *-40-35 + *-41-36 + *-42-37 + *-43-38 + *-44-39 + *-45-40 + *-45-41 + *-46-42 + *-47-43 + *-48-44 + *-48-50 + *-48-51 + *-48-52 + *-49-45 + *-50-46 + *-51-47 + *-52-48 + *-53-49 + *-54-53 + *-55-54 + *-56-55 + *-57-56 + *-58-57 + *-59-58 + *-59-61 + *-60-59 + *-60-65 + *-60-66 + *-60-67 + *-61-60 + *-62-62 + *-63-63 + *-64-64 + *-65-68 + *-66-69 + *-67-70 + *-68-71 + *-69-72 + *-70-73 + *-71-74 + *-72-75 + *-73-76 + *-74-77 + *-74-80 + *-75-78 + *-75-82 + *-75-83 + *-76-79 + *-77-81 + *-78-84 + *-78-85 + *-78-86 + *-79-87 + *-80-88 + *-81-89 + *-82-90 + *-83-91 + *-83-97 + *-83-98 + *-83-99 + *-84-92 + *-85-93 + *-86-94 + *-87-95 + *-88-96 + *-89-100 + *-90-101 + *-91-102 + *-92-103 + *-93-104 + *-94-105 + *-95-106 + *-96-107 + *-97-108 + *-97-111 + *-98-109 + *-98-113 + *-98-114 + *-99-110 + *-100-112 + *-101-115 + *-102-116 + *-103-117 + *-104-118 + *-105-119 + *-106-120 + *-106-121 + *-107-122 + *-107-125 + *-108-123 + *-109-124 + *-110-126 + *-111-127 + *-112-128 + *-112-129 + *-113-130 + *-114-131 + *-115-132 + *-116-133 + *-116-134 + *-116-135 + *-117-136 + *-118-137 + *-119-138 + *-120-139 + *-121-140 + *-121-143 + *-122-141 + *-122-147 + *-122-148 + *-122-149 + *-123-142 + *-124-144 + *-125-145 + *-126-146 + *-127-150 + *-128-151 + *-129-152 + *-130-153 + *-130-156 + *-131-154 + *-131-158 + *-131-159 + *-132-155 + *-133-157 + *-134-160 + *-134-163 + *-135-161 + *-135-165 + *-135-166 + *-136-162 + *-137-164 + *-138-167 + *-138-168 + *-139-169 + *-140-170 + *-141-171 + *-142-172 + *-142-173 + *-143-174 + *-144-175 + *-145-176 + *-146-177 + *-147-178 + *-148-179 + *-148-180 + *-149-181 + *-150-182 + *-151-183 + *-151-184 + *-152-185 + *-152-188 + *-153-186 + *-153-190 + *-153-191 + *-153-192 + *-154-187 + *-155-189 + *-156-193 + *-156-194 + *-156-196 + *-157-195 + *-158-197 + *-159-198 + *-160-199 + *-161-200 + *-162-201 + *-163-202 + *-164-203 + *-164-207 + *-164-209 + *-164-210 + *-165-204 + *-166-205 + *-167-206 + *-168-208 + *-169-211 + *-169-212 + *-169-213 + *-170-214 + *-171-215 + *-172-216 + *-173-217 + *-174-218 + *-174-226 + *-174-228 + *-174-229 + *-175-219 + *-176-220 + *-177-221 + *-178-222 + *-179-223 + *-180-224 + *-181-225 + *-182-227 + *-183-230 + *-184-231 + *-185-232 + *-186-233 + *-187-234 + *-188-235 + *-189-236 + *-190-237 + *-191-238 + *-192-239 + *-193-240 + *-193-243 + *-194-241 + *-194-247 + *-194-248 + *-195-242 + *-196-244 + *-197-245 + *-198-246 + *-199-249 + *-200-250 + *-201-251 + *-202-252 + *-203-253 + *-204-254 + *-205-255 + *-206-256 + *-207-257 + *-208-258 + *-208-266 + *-209-259 + *-210-260 + *-210-274 + *-210-280 + *-210-281 + *-211-261 + *-212-262 + *-213-263 + *-214-264 + *-215-265 + *-216-267 + *-217-268 + *-218-269 + *-219-270 + *-220-271 + *-221-272 + *-222-273 + *-223-275 + *-224-276 + *-225-277 + *-226-278 + *-227-279 + *-228-282 + *-229-283 + *-230-284 + *-230-287 + *-230-289 + *-230-291 + *-231-285 + *-231-288 + *-231-290 + *-231-292 + *-232-286 + *-233-293 + *-234-294 + *-235-295 + *-236-296 + *-237-297 + *-238-298 + *-239-299 + *-240-300 + *-241-301 + *-242-302 + *-243-303 + *-244-304 + *-245-305 + *-246-306 + *-247-307 + *-248-308 + *-249-309 + *-250-310 + *-251-311 + *-252-312 + *-253-313 + *-254-314 + *-255-315 + *-255-319 + *-256-316 + *-256-320 + *-257-317 + *-258-318 + *-259-321 + *-260-322 + *-261-323 + *-262-324 + *-263-325 + *-264-326 + *-265-327 + *-266-328 + *-267-329 + *-268-330 + *-269-331 + *-270-332 + *-271-333 + *-272-334 + *-273-335 + *-274-336 + *-275-337 + *-276-338 + *-277-339 + *-278-340 + *-279-341 + *-280-342 + *-281-343 + *-282-344 + *-283-345 + *-284-346 + *-285-347 + *-286-348 + *-286-350 + *-287-349 + *-288-351 + *-288-352 + *-288-353 + *-289-354 + *-290-355 + *-291-356 + *-292-357 + *-293-358 + *-294-359 + *-295-360 + *-296-361 + *-297-362 + *-298-363 + *-299-364 + *-300-365 + *-301-366 + *-302-367 + *-303-368 + *-304-369 + *-305-370 + *-306-371 + *-307-372 + *-307-373 + *-308-374 + *-309-375 + *-310-376 + *-311-377 + *-312-378 + *-313-379 + *-314-380 + *-315-381 + *-316-382 + *-317-383 + *-318-384 + *-319-385 + *-320-386 + *-321-387 + *-322-388 + *-323-389 + *-324-390 + *-325-391 + *-326-392 + *-326-398 + *-327-393 + *-327-409 + *-327-412 + *-327-413 + *-328-394 + *-329-395 + *-330-396 + *-331-397 + *-332-399 + *-333-400 + *-334-401 + *-335-402 + *-336-403 + *-337-404 + *-338-405 + *-339-406 + *-340-407 + *-341-408 + *-342-410 + *-343-411 + *-344-414 + *-345-415 + *-346-416 + *-347-417 + *-348-418 + *-349-419 + *-350-420 + *-351-421 + *-352-422 + *-353-423 + *-354-424 + *-355-425 + *-356-426 + *-357-427 + *-358-428 + *-359-429 + *-360-430 + *-361-431 + *-362-432 + *-362-435 + *-363-433 + *-363-437 + *-363-438 + *-363-439 + *-364-434 + *-365-436 + *-366-440 + *-367-441 + *-368-442 + *-369-443 + *-370-444 + *-371-445 + *-372-446 + *-373-447 + *-374-448 + *-375-449 + *-376-450 + *-377-451 + *-378-452 + *-379-453 + *-380-454 + *-381-455 + *-382-456 + *-383-457 + *-384-458 + *-385-459 + *-386-460 + *-387-461 + *-388-462 + *-389-463 + *-390-464 + *-391-465 + *-392-466 + *-393-467 + *-394-468 + *-394-474 + *-394-475 + *-394-476 + *-395-469 + *-396-470 + *-397-471 + *-398-472 + *-399-473 + *-400-477 + *-401-478 + *-402-479 + *-403-480 + *-404-481 + *-405-482 + *-406-483 + *-407-484 + *-408-485 + *-409-486 + *-410-487 + *-411-488 + *-412-489 + *-413-490 + *-414-491 + *-415-492 + *-416-493 + *-416-494 + *-416-495 + *-417-496 + *-418-497 + *-419-498 + *-420-499 + *-421-500 + *-422-501 + *-423-502 + *-424-503 + *-425-504 + *-426-505 + *-426-506 + *-427-507 + *-428-508 + *-429-509 + *-429-511 + *-430-510 + *-430-512 + *-430-513 + *-431-514 + *-432-515 + *-432-521 + *-433-516 + *-434-517 + *-435-518 + *-435-524 + *-435-527 + *-435-528 + *-436-519 + *-437-520 + *-438-522 + *-439-523 + *-440-525 + *-441-526 + *-442-529 + *-442-531 + *-443-530 + *-443-532 + *-443-533 + *-444-534 + *-444-536 + *-445-535 + *-445-538 + *-445-539 + *-446-537 + *-447-540 + *-447-542 + *-448-541 + *-449-543 + *-449-544 + *-449-545 + *-450-546 + *-451-547 + *-452-548 + *-453-549 + *-453-558 + *-453-559 + *-454-550 + *-455-551 + *-456-552 + *-457-553 + *-458-554 + *-458-555 + *-459-556 + *-460-557 + *-461-560 + *-462-561 + *-463-562 + *-464-563 + *-465-564 + *-466-565 + *-467-566 + *-467-569 + *-468-567 + *-468-571 + *-468-572 + *-469-568 + *-470-570 + *-471-573 + *-472-574 + *-473-575 + *-474-576 + *-475-577 + *-476-578 + *-477-579 + *-477-582 + *-477-588 + *-477-590 + *-478-580 + *-478-589 + *-479-581 + *-480-583 + *-481-584 + *-482-585 + *-483-586 + *-484-587 + *-485-591 + *-486-592 + *-487-593 + *-488-594 + *-489-595 + *-489-598 + *-490-596 + *-490-601 + *-490-602 + *-491-597 + *-492-599 + *-493-600 + *-494-603 + *-494-605 + *-495-604 + *-496-606 + *-497-607 + *-498-608 + *-498-611 + *-499-609 + *-499-617 + *-499-618 + *-499-619 + *-500-610 + *-501-612 + *-502-613 + *-503-614 + *-504-615 + *-505-616 + *-506-620 + *-506-621 + *-507-622 + *-508-623 + *-509-624 + *-510-625 + *-511-626 + *-511-627 + *-511-628 + *-512-629 + *-513-630 + *-513-631 + *-513-632 + *-514-633 + *-514-634 + *-515-635 + *-516-636 + *-517-637 + *-518-638 + *-519-639 + *-520-640 + *-521-641 + *-521-648 + *-521-649 + *-521-650 + *-522-642 + *-523-643 + *-524-644 + *-525-645 + *-526-646 + *-527-647 + *-528-651 + *-528-653 + *-529-652 + *-530-654 + *-530-655 + *-530-656 + *-530-657 + *-531-658 + *-532-659 + *-533-660 + *-534-661 + *-535-662 + *-536-663 + *-537-664 + *-538-665 + *-539-666 + *-540-667 + *-541-668 + *-542-669 + *-543-670 + *-544-671 + *-545-672 + *-546-673 + *-547-674 + *-548-675 + *-549-676 + *-550-677 + *-550-680 + *-550-682 + *-550-684 + *-551-678 + *-551-683 + *-552-679 + *-553-681 + *-554-685 + *-554-686 + *-555-687 + *-556-688 + *-556-689 + *-556-690 + *-557-691 + *-557-694 + *-558-692 + *-558-697 + *-558-698 + *-558-699 + *-559-693 + *-560-695 + *-561-696 + *-562-700 + *-563-701 + *-564-702 + *-564-703 + *-564-704 + *-565-705 + *-566-706 + *-567-707 + *-567-709 + *-568-708 + *-568-712 + *-568-713 + *-568-714 + *-569-710 + *-570-711 + *-571-715 + *-572-716 + *-573-717 + *-574-718 + *-575-719 + *-576-720 + *-577-721 + *-578-722 + *-579-723 + *-579-724 + *-580-725 + *-580-726 + *-581-727 + *-582-728 + *-583-729 + *-584-730 + *-585-731 + *-586-732 + *-587-733 + *-588-734 + *-589-735 + *-590-736 + *-591-737 + *-592-738 + *-593-739 + *-594-740 + *-595-741 + *-595-742 + *-596-743 + *-597-744 + *-598-745 + *-599-746 + *-600-747 + *-601-748 + *-602-749 + *-603-750 + *-604-751 + *-605-752 + *-605-753 + *-605-754 + *-606-755 + *-606-1801 + *-607-756 + *-607-765 + *-607-767 + *-608-757 + *-609-758 + *-610-759 + *-611-760 + *-612-761 + *-613-762 + *-614-763 + *-615-764 + *-616-766 + *-617-768 + *-618-769 + *-619-770 + *-620-771 + *-621-772 + *-622-773 + *-623-774 + *-624-775 + *-625-776 + *-626-777 + *-627-778 + *-628-779 + *-629-780 + *-630-781 + *-631-782 + *-632-783 + *-633-784 + *-634-785 + *-635-786 + *-636-787 + *-637-788 + *-638-789 + *-639-790 + *-640-791 + *-641-792 + *-642-793 + *-643-794 + *-644-795 + *-645-796 + *-646-797 + *-647-798 + *-648-799 + *-649-800 + *-650-801 + *-651-802 + *-652-803 + *-653-804 + *-654-805 + *-655-806 + *-656-807 + *-657-808 + *-658-809 + *-659-810 + *-660-811 + *-661-812 + *-662-813 + *-663-814 + *-664-815 + *-665-816 + *-666-817 + *-666-819 + *-667-818 + *-668-820 + *-669-821 + *-670-822 + *-671-823 + *-672-824 + *-673-825 + *-674-826 + *-675-827 + *-676-828 + *-677-829 + *-678-830 + *-679-831 + *-680-832 + *-681-833 + *-682-834 + *-683-835 + *-684-836 + *-685-837 + *-686-838 + *-687-839 + *-688-840 + *-689-841 + *-690-842 + *-691-843 + *-692-844 + *-693-845 + *-694-846 + *-695-847 + *-696-848 + *-697-849 + *-698-850 + *-699-851 + *-700-852 + *-701-853 + *-702-854 + *-703-855 + *-704-856 + *-705-857 + *-706-858 + *-707-859 + *-708-860 + *-709-861 + *-710-862 + *-710-863 + *-711-864 + *-711-865 + *-712-866 + *-713-867 + *-714-868 + *-715-869 + *-716-870 + *-716-873 + *-716-874 + *-717-871 + *-717-872 + *-718-875 + *-719-876 + *-720-877 + *-720-878 + *-721-879 + *-722-880 + *-722-881 + *-723-882 + *-723-883 + *-724-885 + *-725-886 + *-726-887 + *-727-888 + *-728-889 + *-729-890 + *-730-891 + *-731-892 + *-732-893 + *-733-894 + *-734-895 + *-735-896 + *-736-897 + *-737-898 + *-738-899 + *-739-900 + *-740-901 + *-741-902 + *-742-903 + *-742-907 + *-743-904 + *-743-910 + *-743-911 + *-743-912 + *-744-905 + *-745-906 + *-746-908 + *-747-909 + *-748-913 + *-748-914 + *-748-915 + *-749-916 + *-750-917 + *-751-918 + *-752-919 + *-752-921 + *-753-920 + *-754-922 + *-755-923 + *-756-924 + *-757-925 + *-758-926 + *-759-927 + *-759-928 + *-760-929 + *-761-930 + *-761-931 + *-762-932 + *-763-933 + *-763-935 + *-763-938 + *-764-934 + *-764-936 + *-764-937 + *-765-939 + *-765-940 + *-766-941 + *-767-942 + *-768-943 + *-769-944 + *-770-945 + *-771-946 + *-772-947 + *-773-948 + *-774-949 + *-775-950 + *-776-951 + *-777-952 + *-778-953 + *-778-956 + *-778-957 + *-779-954 + *-780-955 + *-781-958 + *-782-959 + *-783-960 + *-784-961 + *-785-962 + *-786-963 + *-787-964 + *-788-965 + *-789-966 + *-790-967 + *-791-968 + *-791-971 + *-792-969 + *-793-970 + *-794-972 + *-795-973 + *-796-974 + *-797-975 + *-797-977 + *-798-976 + *-799-978 + *-800-979 + *-800-980 + *-801-981 + *-801-982 + *-801-983 + *-802-984 + *-802-985 + *-802-986 + *-803-987 + *-804-988 + *-805-989 + *-806-990 + *-806-992 + *-806-995 + *-807-991 + *-808-993 + *-809-994 + *-810-996 + *-811-997 + *-812-998 + *-813-999 + *-814-1000 + *-814-1007 + *-815-1001 + *-815-1015 + *-815-1020 + *-815-1021 + *-816-1002 + *-817-1003 + *-818-1004 + *-819-1005 + *-820-1006 + *-821-1008 + *-822-1009 + *-823-1010 + *-824-1011 + *-825-1012 + *-826-1013 + *-827-1014 + *-828-1016 + *-829-1017 + *-830-1018 + *-831-1019 + *-832-1022 + *-833-1023 + *-834-1024 + *-835-1025 + *-836-1026 + *-837-1027 + *-838-1028 + *-839-1029 + *-840-1030 + *-841-1031 + *-842-1032 + *-843-1033 + *-844-1034 + *-845-1035 + *-846-1036 + *-847-1037 + *-848-1038 + *-849-1039 + *-849-1042 + *-850-1040 + *-850-1044 + *-850-1045 + *-850-1046 + *-851-1041 + *-852-1043 + *-853-1047 + *-854-1048 + *-855-1049 + *-856-1050 + *-857-1051 + *-858-1052 + *-859-1053 + *-860-1054 + *-861-1055 + *-862-1056 + *-863-1057 + *-864-1058 + *-865-1059 + *-866-1060 + *-867-1061 + *-868-1062 + *-869-1063 + *-870-1064 + *-871-1065 + *-872-1066 + *-873-1067 + *-874-1068 + *-875-1069 + *-876-1070 + *-877-1071 + *-878-1072 + *-879-1073 + *-880-1074 + *-881-1075 + *-882-1076 + *-883-1077 + *-884-1078 + *-884-1079 + *-885-1080 + *-886-1081 + *-887-1082 + *-888-1083 + *-889-1084 + *-890-1085 + *-891-1086 + *-892-1087 + *-893-1088 + *-894-1089 + *-894-1091 + *-894-1096 + *-894-1098 + *-895-1090 + *-896-1092 + *-897-1093 + *-898-1094 + *-899-1095 + *-900-1097 + *-901-1099 + *-902-1100 + *-903-1101 + *-904-1102 + *-904-1106 + *-905-1103 + *-906-1104 + *-907-1105 + *-908-1107 + *-909-1108 + *-910-1109 + *-911-1110 + *-912-1111 + *-913-1112 + *-914-1113 + *-915-1114 + *-916-1115 + *-917-1116 + *-918-1117 + *-919-1118 + *-919-1119 + *-920-1120 + *-921-1121 + *-922-1122 + *-923-1123 + *-924-1124 + *-925-1125 + *-926-1126 + *-927-1127 + *-927-1133 + *-927-1134 + *-927-1135 + *-928-1128 + *-929-1129 + *-930-1130 + *-931-1131 + *-932-1132 + *-933-1136 + *-934-1137 + *-935-1138 + *-936-1139 + *-937-1140 + *-938-1141 + *-939-1142 + *-940-1143 + *-941-1144 + *-942-1145 + *-943-1146 + *-944-1147 + *-945-1148 + *-946-1149 + *-947-1150 + *-948-1151 + *-948-1153 + *-949-1152 + *-950-1154 + *-951-1155 + *-952-1156 + *-953-1157 + *-954-1158 + *-955-1159 + *-956-1160 + *-957-1161 + *-958-1162 + *-959-1163 + *-960-1164 + *-961-1165 + *-962-1166 + *-963-1167 + *-964-1168 + *-965-1169 + *-966-1170 + *-967-1171 + *-968-1172 + *-969-1173 + *-969-1178 + *-970-1174 + *-971-1175 + *-972-1176 + *-973-1177 + *-974-1179 + *-975-1180 + *-975-1181 + *-976-1182 + *-977-1183 + *-978-1184 + *-979-1185 + *-980-1186 + *-981-1187 + *-982-1188 + *-983-1189 + *-983-1191 + *-984-1190 + *-985-1192 + *-986-1193 + *-986-1196 + *-987-1194 + *-988-1195 + *-989-1197 + *-990-1198 + *-991-1199 + *-992-1200 + *-992-1204 + *-992-1205 + *-993-1201 + *-994-1202 + *-995-1203 + *-996-1206 + *-997-1207 + *-998-1208 + *-999-1209 + *-1000-1210 + *-1001-1211 + *-1002-1212 + *-1002-1214 + *-1003-1213 + *-1004-1215 + *-1004-1218 + *-1005-1216 + *-1005-1220 + *-1005-1221 + *-1005-1222 + *-1006-1217 + *-1007-1219 + *-1008-1223 + *-1009-1224 + *-1010-1225 + *-1011-1226 + *-1012-1227 + *-1013-1228 + *-1014-1229 + *-1015-1230 + *-1015-1231 + *-1016-1232 + *-1017-1233 + *-1018-1234 + *-1019-1235 + *-1020-1236 + *-1020-1237 + *-1021-1238 + *-1022-1239 + *-1023-1240 + *-1024-1241 + *-1025-1242 + *-1025-1243 + *-1026-1244 + *-1027-1245 + *-1027-1246 + *-1028-1247 + *-1029-1248 + *-1030-1249 + *-1031-1250 + *-1032-1251 + *-1033-1252 + *-1033-1253 + *-1033-1254 + *-1034-1255 + *-1035-1256 + *-1036-1257 + *-1037-1258 + *-1038-1259 + *-1039-1260 + *-1040-1261 + *-1040-1264 + *-1041-1262 + *-1041-1266 + *-1041-1267 + *-1042-1263 + *-1043-1265 + *-1044-1268 + *-1045-1269 + *-1045-1271 + *-1046-1270 + *-1047-1272 + *-1048-1273 + *-1049-1274 + *-1050-1275 + *-1051-1276 + *-1052-1277 + *-1053-1278 + *-1054-1279 + *-1055-1280 + *-1056-1281 + *-1057-1282 + *-1058-1283 + *-1059-1284 + *-1060-1285 + *-1061-1286 + *-1062-1287 + *-1063-1288 + *-1063-1290 + *-1064-1289 + *-1065-1291 + *-1066-1292 + *-1067-1293 + *-1068-1294 + *-1068-1304 + *-1069-1295 + *-1070-1296 + *-1071-1297 + *-1072-1298 + *-1073-1299 + *-1073-1312 + *-1073-1316 + *-1073-1317 + *-1074-1300 + *-1075-1301 + *-1076-1302 + *-1077-1303 + *-1078-1305 + *-1079-1306 + *-1080-1307 + *-1081-1308 + *-1082-1309 + *-1083-1310 + *-1084-1311 + *-1085-1313 + *-1086-1314 + *-1087-1315 + *-1088-1318 + *-1088-1321 + *-1088-1325 + *-1088-1327 + *-1089-1319 + *-1089-1326 + *-1090-1320 + *-1091-1322 + *-1092-1323 + *-1093-1324 + *-1094-1328 + *-1095-1329 + *-1096-1330 + *-1097-1331 + *-1098-1332 + *-1099-1333 + *-1100-1334 + *-1101-1335 + *-1102-1336 + *-1103-1337 + *-1104-1338 + *-1105-1339 + *-1106-1340 + *-1107-1341 + *-1108-1342 + *-1109-1343 + *-1110-1344 + *-1111-1345 + *-1112-1346 + *-1112-1350 + *-1113-1347 + *-1113-1356 + *-1113-1357 + *-1114-1348 + *-1115-1349 + *-1116-1351 + *-1117-1352 + *-1118-1353 + *-1119-1354 + *-1120-1355 + *-1121-1358 + *-1122-1359 + *-1123-1360 + *-1124-1361 + *-1125-1362 + *-1126-1363 + *-1127-1364 + *-1128-1365 + *-1129-1366 + *-1130-1367 + *-1131-1368 + *-1132-1369 + *-1133-1370 + *-1134-1371 + *-1135-1372 + *-1136-1373 + *-1137-1374 + *-1138-1375 + *-1139-1376 + *-1140-1377 + *-1141-1378 + *-1142-1379 + *-1143-1380 + *-1144-1381 + *-1144-1401 + *-1144-1408 + *-1144-1409 + *-1145-1382 + *-1146-1383 + *-1147-1384 + *-1148-1385 + *-1149-1386 + *-1150-1387 + *-1151-1388 + *-1151-1417 + *-1152-1389 + *-1153-1390 + *-1154-1391 + *-1155-1392 + *-1156-1393 + *-1157-1394 + *-1158-1395 + *-1159-1396 + *-1160-1397 + *-1161-1398 + *-1162-1399 + *-1163-1400 + *-1164-1402 + *-1165-1403 + *-1166-1404 + *-1167-1405 + *-1168-1406 + *-1169-1407 + *-1170-1410 + *-1171-1411 + *-1172-1412 + *-1173-1413 + *-1174-1414 + *-1175-1415 + *-1176-1416 + *-1177-1418 + *-1178-1419 + *-1179-1420 + *-1180-1421 + *-1181-1422 + *-1182-1423 + *-1183-1424 + *-1184-1425 + *-1185-1426 + *-1186-1427 + *-1187-1428 + *-1188-1429 + *-1189-1430 + *-1190-1431 + *-1191-1432 + *-1191-1435 + *-1192-1433 + *-1192-1437 + *-1192-1438 + *-1193-1434 + *-1194-1436 + *-1195-1439 + *-1196-1440 + *-1197-1441 + *-1198-1442 + *-1199-1443 + *-1199-1446 + *-1200-1444 + *-1200-1453 + *-1200-1454 + *-1200-1455 + *-1201-1445 + *-1202-1447 + *-1203-1448 + *-1204-1449 + *-1205-1450 + *-1206-1451 + *-1207-1452 + *-1208-1456 + *-1208-1458 + *-1208-1459 + *-1209-1457 + *-1210-1460 + *-1211-1461 + *-1212-1462 + *-1212-1465 + *-1212-1467 + *-1212-1469 + *-1213-1463 + *-1213-1468 + *-1214-1464 + *-1215-1466 + *-1216-1470 + *-1216-1471 + *-1217-1472 + *-1217-1474 + *-1218-1473 + *-1219-1475 + *-1220-1476 + *-1221-1477 + *-1222-1478 + *-1222-1482 + *-1223-1479 + *-1224-1480 + *-1225-1481 + *-1226-1483 + *-1227-1484 + *-1228-1485 + *-1229-1486 + *-1229-1492 + *-1229-1493 + *-1230-1487 + *-1231-1488 + *-1232-1489 + *-1232-1491 + *-1233-1490 + *-1234-1494 + *-1235-1495 + *-1236-1496 + *-1237-1497 + *-1238-1498 + *-1239-1499 + *-1240-1500 + *-1240-1502 + *-1241-1501 + *-1242-1503 + *-1242-1505 + *-1242-1506 + *-1242-1507 + *-1243-1504 + *-1244-1508 + *-1245-1509 + *-1246-1510 + *-1246-1514 + *-1246-1515 + *-1246-1516 + *-1247-1511 + *-1248-1512 + *-1249-1513 + *-1250-1517 + *-1251-1518 + *-1252-1519 + *-1253-1520 + *-1254-1521 + *-1254-1524 + *-1255-1522 + *-1255-1528 + *-1255-1529 + *-1256-1523 + *-1257-1525 + *-1258-1526 + *-1259-1527 + *-1260-1530 + *-1261-1531 + *-1262-1532 + *-1263-1533 + *-1264-1534 + *-1264-1538 + *-1265-1535 + *-1265-1546 + *-1266-1536 + *-1267-1537 + *-1268-1539 + *-1269-1540 + *-1270-1541 + *-1271-1542 + *-1272-1543 + *-1273-1544 + *-1274-1545 + *-1275-1547 + *-1276-1548 + *-1277-1549 + *-1278-1550 + *-1279-1551 + *-1280-1552 + *-1281-1553 + *-1281-1554 + *-1281-1555 + *-1282-1556 + *-1283-1557 + *-1283-1563 + *-1283-1564 + *-1284-1558 + *-1285-1559 + *-1286-1560 + *-1286-1561 + *-1287-1562 + *-1288-1565 + *-1289-1566 + *-1289-1569 + *-1290-1567 + *-1290-1571 + *-1290-1572 + *-1291-1568 + *-1292-1570 + *-1293-1573 + *-1294-1574 + *-1294-1576 + *-1295-1575 + *-1295-1577 + *-1295-1578 + *-1295-1579 + *-1296-1580 + *-1297-1581 + *-1297-1583 + *-1297-1584 + *-1298-1582 + *-1299-1585 + *-1299-1587 + *-1300-1586 + *-1300-1588 + *-1300-1589 + *-1300-1590 + *-1301-1591 + *-1301-1592 + *-1302-1593 + *-1303-1594 + *-1304-1595 + *-1305-1596 + *-1306-1597 + *-1307-1598 + *-1308-1599 + *-1309-1600 + *-1310-1601 + *-1311-1602 + *-1312-1603 + *-1312-1605 + *-1313-1604 + *-1314-1606 + *-1315-1607 + *-1316-1608 + *-1317-1609 + *-1318-1610 + *-1319-1611 + *-1320-1612 + *-1321-1613 + *-1322-1614 + *-1323-1615 + *-1324-1616 + *-1325-1617 + *-1326-1618 + *-1327-1619 + *-1328-1620 + *-1329-1621 + *-1330-1622 + *-1331-1623 + *-1332-1624 + *-1332-1631 + *-1332-1634 + *-1332-1635 + *-1333-1625 + *-1334-1626 + *-1335-1627 + *-1336-1628 + *-1337-1629 + *-1338-1630 + *-1339-1632 + *-1340-1633 + *-1341-1636 + *-1342-1637 + *-1343-1638 + *-1344-1639 + *-1345-1640 + *-1346-1641 + *-1347-1642 + *-1348-1643 + *-1349-1644 + *-1350-1645 + *-1351-1646 + *-1352-1647 + *-1353-1648 + *-1354-1649 + *-1355-1650 + *-1356-1651 + *-1357-1652 + *-1358-1653 + *-1359-1654 + *-1360-1655 + *-1361-1656 + *-1362-1657 + *-1363-1658 + *-1364-1659 + *-1364-1661 + *-1365-1660 + *-1365-1663 + *-1365-1664 + *-1366-1662 + *-1367-1665 + *-1368-1666 + *-1368-1667 + *-1369-1668 + *-1369-1669 + *-1369-1670 + *-1369-1671 + *-1369-1672 + *-1370-1673 + *-1371-1674 + *-1371-1675 + *-1372-1676 + *-1373-1677 + *-1373-1678 + *-1373-1679 + *-1374-1680 + *-1375-1681 + *-1376-1682 + *-1377-1683 + *-1378-1684 + *-1379-1685 + *-1379-1686 + *-1379-1687 + *-1380-1688 + *-1381-1689 + *-1382-1690 + *-1383-1691 + *-1384-1692 + *-1385-1693 + *-1385-1694 + *-1386-1695 + *-1386-1696 + *-1386-1697 + *-1387-1698 + *-1388-1699 + *-1389-1700 + *-1389-1701 + *-1389-1702 + *-1390-1703 + *-1391-1704 + *-1392-1705 + *-1393-1706 + *-1394-1707 + *-1395-1708 + *-1395-1709 + *-1395-1710 + *-1396-1711 + *-1396-1714 + *-1396-1715 + *-1397-1712 + *-1398-1713 + *-1399-1716 + *-1400-1717 + *-1401-1718 + *-1402-1719 + *-1403-1720 + *-1404-1721 + *-1405-1722 + *-1406-1723 + *-1407-1724 + *-1408-1725 + *-1409-1726 + *-1410-1727 + *-1411-1728 + *-1411-1730 + *-1412-1729 + *-1412-1732 + *-1413-1731 + *-1414-1733 + *-1414-1735 + *-1415-1734 + *-1416-1736 + *-1417-1737 + *-1418-1738 + *-1419-1739 + *-1420-1740 + *-1421-1741 + *-1422-1742 + *-1423-1743 + *-1424-1744 + *-1425-1745 + *-1426-1746 + *-1426-1748 + *-1427-1747 + *-1427-1749 + *-1427-1750 + *-1428-1751 + *-1429-1752 + *-1430-1753 + *-1431-1754 + *-1432-1755 + *-1432-1756 + *-1432-1757 + *-1433-1758 + *-1434-1759 + *-1434-1761 + *-1435-1760 + *-1435-1762 + *-1435-1763 + *-1436-1764 + *-1437-1765 + *-1438-1766 + *-1439-1767 + *-1440-1768 + *-1441-1769 + *-1442-1770 + *-1442-1803 + *-1443-1771 + *-1443-1780 + *-1443-1782 + *-1444-1772 + *-1445-1773 + *-1446-1774 + *-1447-1775 + *-1448-1776 + *-1449-1777 + *-1450-1778 + *-1451-1779 + *-1452-1781 + *-1453-1783 + *-1454-1784 + *-1455-1785 + *-1456-1786 + *-1457-1787 + *-1458-1788 + *-1459-1789 + *-1460-1790 + *-1461-1791 + *-1462-1792 + *-1463-1793 + *-1464-1794 + *-1465-1795 + *-1466-1796 + *-1467-1797 + *-1468-1798 + *-1469-1799 + *-1470-1800 + *-1471-1802 + *-1472-1804 + *-1473-1805 + *-1474-1806 + *-1475-1807 + *-1476-1808 + *-1477-1809 + *-1478-1810 + *-1479-1811 + *-1480-1812 + *-1481-1813 + *-1482-1814 + *-1483-1815 + *-1484-1816 + *-1485-1817 + *-1486-1818 + *-1487-1819 + *-1488-1820 + *-1489-1821 + *-1490-1822 + *-1491-1823 + *-1492-1824 + *-1493-1825 + *-1494-1826 + *-1495-1827 + *-1496-1828 + *-1497-1829 + *-1498-1830 + *-1499-1831 + *-1500-1832 + *-1501-1833 + *-1502-1834 + *-1503-1835 + *-1504-1836 + *-1505-1837 + *-1506-1838 + *-1507-1839 + *-1508-1840 + *-1509-1841 + *-1510-1842 + *-1511-1843 + *-1512-1844 + *-1513-1845 + *-1514-1846 + *-1515-1847 + *-1516-1848 + *-1517-1849 + *-1518-1850 + *-1519-1851 + *-1520-1852 + *-1521-1853 + *-1522-1854 + *-1523-1855 + *-1524-1856 + *-1525-1857 + *-1526-1858 + *-1527-1859 + *-1528-1860 + *-1529-1861 + *-1529-1862 + *-1529-1865 + *-1529-1866 + *-1530-1863 + *-1531-1864 + *-1532-1867 + *-1533-1868 + *-1534-1869 + *-1535-1870 + *-1536-1871 + *-1536-1872 + *-1536-1873 + *-1537-1874 + *-1537-1877 + *-1538-1875 + *-1538-1881 + *-1538-1882 + *-1538-1883 + *-1539-1876 + *-1540-1878 + *-1541-1879 + *-1542-1880 + *-1543-1884 + *-1544-1885 + *-1545-1886 + *-1546-1887 + *-1547-1888 + *-1548-1889 + *-1549-1890 + *-1550-1891 + *-1551-1892 + *-1552-1893 + *-1553-1894 + *-1554-1895 + *-1555-1896 + *-1556-1897 + *-1557-1898 + *-1558-1899 + *-1559-1900 + *-1560-1901 + *-1560-1907 + *-1561-1902 + *-1561-1908 + *-1562-1903 + *-1562-1911 + *-1562-1915 + *-1562-1917 + *-1563-1904 + *-1563-1912 + *-1563-1916 + *-1563-1918 + *-1564-1905 + *-1565-1906 + *-1566-1909 + *-1567-1910 + *-1568-1913 + *-1569-1914 + *-1570-1919 + *-1571-1920 + *-1572-1921 + *-1573-1922 + *-1574-1923 + *-1575-1924 + *-1576-1925 + *-1577-1926 + *-1578-1928 + *-1579-1929 + *-1580-1930 + *-1581-1931 + *-1582-1932 + *-1583-1933 + *-1584-1934 + *-1585-1935 + *-1586-1936 + *-1587-1937 + *-1588-1938 + *-1589-1939 + *-1589-1941 + *-1590-1940 + *-1591-1942 + *-1592-1943 + *-1593-1944 + *-1593-1948 + *-1594-1945 + *-1594-1949 + *-1595-1946 + *-1596-1947 + *-1597-1950 + *-1598-1951 + *-1599-1952 + *-1600-1953 + *-1601-1954 + *-1602-1955 + *-1603-1956 + *-1604-1957 + *-1605-1958 + *-1606-1959 + *-1607-1960 + *-1608-1961 + *-1609-1962 + *-1610-1963 + *-1611-1964 + *-1612-1965 + *-1613-1966 + *-1614-1967 + *-1614-1970 + *-1615-1968 + *-1616-1969 + *-1616-1972 + *-1616-1973 + *-1617-1971 + *-1618-1974 + *-1619-1975 + *-1620-1976 + *-1621-1977 + *-1621-1979 + *-1621-1980 + *-1621-1981 + *-1622-1978 + *-1623-1982 + *-1624-1983 + *-1625-1984 + *-1626-1985 + *-1627-1986 + *-1628-1987 + *-1629-1988 + *-1630-1989 + *-1631-1990 + *-1632-1991 + *-1633-1992 + *-1634-1993 + *-1635-1994 + *-1636-1995 + *-1637-1996 + *-1637-1998 + *-1638-1997 + *-1639-1999 + *-1640-2000 + *-1641-2001 + *-1642-2002 + *-1642-2006 + *-1642-2007 + *-1643-2003 + *-1644-2004 + *-1645-2005 + *-1646-2008 + *-1646-2009 + *-1647-2010 + *-1648-2011 + *-1649-2012 + *-1649-2013 + *-1649-2014 + *-1650-2015 + *-1650-2017 + *-1651-2016 + *-1651-2019 + *-1652-2018 + *-1653-2020 + *-1654-2021 + *-1655-2022 + *-1656-2023 + *-1657-2024 + *-1658-2025 + *-1659-2026 + *-1660-2027 + *-1661-2028 + *-1662-2029 + *-1663-2030 + *-1664-2031 + *-1665-2032 + *-1666-2033 + *-1667-2034 + *-1668-2035 + *-1669-2036 + *-1670-2037 + *-1671-2038 + *-1672-2039 + *-1673-2040 + *-1674-2041 + *-1675-2042 + *-1676-2043 + *-1677-2044 + *-1678-2045 + *-1679-2046 + *-1680-2047 + *-1681-2048 + *-1682-2049 + *-1683-2050 + *-1684-2051 + *-1684-2052 + *-1685-2053 + *-1686-2054 + *-1687-2055 + *-1688-2056 + *-1689-2057 + *-1690-2058 + *-1691-2059 + *-1692-2060 + *-1693-2061 + *-1694-2062 + *-1695-2063 + *-1696-2064 + *-1697-2065 + *-1698-2066 + *-1699-2067 + *-1700-2068 + *-1701-2069 + *-1702-2070 + *-1702-2071 + *-1703-2072 + *-1704-2073 "); Ok(()) diff --git a/tests/insert.rs b/tests/insert.rs index 92c49791..38c5b3a2 100644 --- a/tests/insert.rs +++ b/tests/insert.rs @@ -1,8 +1,8 @@ use similar_asserts::assert_eq; use std::error::Error; use wayfind::{ - errors::{InsertError, PathConstraintError, PathInsertError, PathRouteError}, - DataChain, MethodId, PathConstraint, PathId, RouteBuilder, Router, + errors::{InsertError, PathConstraintError, PathInsertError, PathTemplateError}, + AuthorityId, DataChain, MethodId, PathConstraint, PathId, RouteBuilder, Router, }; #[test] @@ -18,6 +18,7 @@ fn test_insert_conflict() -> Result<(), Box> { insert, Err(InsertError::Conflict { chain: DataChain { + authority: AuthorityId(None), path: PathId(1), method: MethodId(None), } @@ -30,6 +31,7 @@ fn test_insert_conflict() -> Result<(), Box> { insert, Err(InsertError::Conflict { chain: DataChain { + authority: AuthorityId(None), path: PathId(1), method: MethodId(None), } @@ -37,12 +39,14 @@ fn test_insert_conflict() -> Result<(), Box> { ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /test [1] === Method Empty === Chains - 1-* + *-1-* "); Ok(()) @@ -61,6 +65,7 @@ fn test_insert_conflict_expanded() -> Result<(), Box> { insert, Err(InsertError::Conflict { chain: DataChain { + authority: AuthorityId(None), path: PathId(1), method: MethodId(None), } @@ -73,19 +78,20 @@ fn test_insert_conflict_expanded() -> Result<(), Box> { insert, Err(InsertError::Conflict { chain: DataChain { + authority: AuthorityId(None), path: PathId(1), method: MethodId(None), } }) ); - // FIXME: Wrong ID returned here. let route = RouteBuilder::new().route("(/best)").build()?; let insert = router.insert(&route, 3); assert_eq!( insert, Err(InsertError::Conflict { chain: DataChain { + authority: AuthorityId(None), path: PathId(1), method: MethodId(None), } @@ -93,13 +99,15 @@ fn test_insert_conflict_expanded() -> Result<(), Box> { ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ╰─ test [1] === Method Empty === Chains - 1-* + *-1-* "); Ok(()) @@ -118,21 +126,24 @@ fn test_insert_conflict_multiple_expanded() -> Result<(), Box> { insert, Err(InsertError::Conflict { chain: DataChain { + authority: AuthorityId(None), path: PathId(1), method: MethodId(None), } }) ); - insta::assert_snapshot!(router, @r###" + insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ╰─ hello [1] === Method Empty === Chains - 1-* - "###); + *-1-* + "); Ok(()) } @@ -151,6 +162,7 @@ fn test_insert_conflict_end_wildcard() -> Result<(), Box> { insert, Err(InsertError::Conflict { chain: DataChain { + authority: AuthorityId(None), path: PathId(1), method: MethodId(None), } @@ -158,13 +170,15 @@ fn test_insert_conflict_end_wildcard() -> Result<(), Box> { ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ╰─ {*catch_all} [1] === Method Empty === Chains - 1-* + *-1-* "); Ok(()) @@ -190,6 +204,8 @@ fn test_insert_overlapping() -> Result<(), Box> { ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ├─ x/y [2] @@ -198,8 +214,8 @@ fn test_insert_overlapping() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* + *-1-* + *-2-* "); Ok(()) @@ -216,8 +232,8 @@ fn test_insert_duplicate_parameter() { let insert = router.insert(&route, 3); assert_eq!( insert, - Err(InsertError::Path(PathInsertError::PathRouteError( - PathRouteError::DuplicateParameter { + Err(InsertError::Path(PathInsertError::TemplateError( + PathTemplateError::DuplicateParameter { route: "/{*id}/users/{id}".to_owned(), name: "id".to_owned(), first: 1, @@ -229,6 +245,8 @@ fn test_insert_duplicate_parameter() { ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path Empty === Method diff --git a/tests/method.rs b/tests/method.rs index 33defaa6..014bf440 100644 --- a/tests/method.rs +++ b/tests/method.rs @@ -6,7 +6,7 @@ use wayfind::{ DeleteError, InsertError, MethodDeleteError, MethodInsertError, MethodSearchError, SearchError, }, - Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router, + AuthorityMatch, Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router, }; #[test] @@ -20,13 +20,15 @@ fn test_method_simple() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-1 + *-1-1 "); let request = RequestBuilder::new().path("/users").method("GET").build()?; @@ -35,6 +37,10 @@ fn test_method_simple() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -60,13 +66,15 @@ fn test_method_not_allowed() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-1 + *-1-1 "); let request = RequestBuilder::new() @@ -93,6 +101,8 @@ fn test_method_multiple() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method @@ -100,7 +110,7 @@ fn test_method_multiple() -> Result<(), Box> { ├─ GET [1] ╰─ POST [1] === Chains - 1-1 + *-1-1 "); let request = RequestBuilder::new().path("/users").method("GET").build()?; @@ -109,6 +119,10 @@ fn test_method_multiple() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -129,6 +143,10 @@ fn test_method_multiple() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -166,6 +184,8 @@ fn test_method_empty() -> Result<(), Box> { assert_eq!(insert, Err(InsertError::Method(MethodInsertError::Empty))); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method @@ -185,12 +205,14 @@ fn test_method_none() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/users").method("GET").build()?; @@ -199,6 +221,10 @@ fn test_method_none() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -217,6 +243,10 @@ fn test_method_none() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -232,6 +262,10 @@ fn test_method_none() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -255,13 +289,15 @@ fn test_method_same_path() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-1 + *-1-1 "); let route = RouteBuilder::new() @@ -271,6 +307,8 @@ fn test_method_same_path() -> Result<(), Box> { router.insert(&route, 2)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method @@ -278,8 +316,8 @@ fn test_method_same_path() -> Result<(), Box> { ├─ GET [1] ╰─ PUT [2] === Chains - 1-1 - 1-2 + *-1-1 + *-1-2 "); let request = RequestBuilder::new().path("/users").method("GET").build()?; @@ -288,6 +326,10 @@ fn test_method_same_path() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -305,6 +347,10 @@ fn test_method_same_path() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -330,27 +376,31 @@ fn test_method_same_route_catch() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-1 + *-1-1 "); let route = RouteBuilder::new().route("/users").build()?; router.insert(&route, 2)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-* - 1-1 + *-1-* + *-1-1 "); let request = RequestBuilder::new().path("/users").method("GET").build()?; @@ -359,6 +409,10 @@ fn test_method_same_route_catch() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -376,6 +430,10 @@ fn test_method_same_route_catch() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -396,12 +454,14 @@ fn test_method_same_route_catch_backwards() -> Result<(), Box> { router.insert(&route, 2)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method Empty === Chains - 1-* + *-1-* "); let route = RouteBuilder::new() @@ -411,14 +471,16 @@ fn test_method_same_route_catch_backwards() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-* - 1-1 + *-1-* + *-1-1 "); let request = RequestBuilder::new().path("/users").method("GET").build()?; @@ -427,6 +489,10 @@ fn test_method_same_route_catch_backwards() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -444,6 +510,10 @@ fn test_method_same_route_catch_backwards() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -467,13 +537,15 @@ fn test_method_conflict_direct() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-1 + *-1-1 "); let route = RouteBuilder::new() @@ -487,13 +559,15 @@ fn test_method_conflict_direct() -> Result<(), Box> { ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-1 + *-1-1 "); Ok(()) @@ -510,6 +584,8 @@ fn test_method_conflict_list_inner() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method @@ -517,7 +593,7 @@ fn test_method_conflict_list_inner() -> Result<(), Box> { ├─ GET [1] ╰─ PUT [1] === Chains - 1-1 + *-1-1 "); let route = RouteBuilder::new() @@ -531,6 +607,8 @@ fn test_method_conflict_list_inner() -> Result<(), Box> { ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method @@ -538,7 +616,7 @@ fn test_method_conflict_list_inner() -> Result<(), Box> { ├─ GET [1] ╰─ PUT [1] === Chains - 1-1 + *-1-1 "); Ok(()) @@ -555,13 +633,15 @@ fn test_method_conflict_list_outer() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-1 + *-1-1 "); let route = RouteBuilder::new() @@ -588,13 +668,15 @@ fn test_method_delete() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-1 + *-1-1 "); let route = RouteBuilder::new() @@ -604,6 +686,8 @@ fn test_method_delete() -> Result<(), Box> { router.delete(&route)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path Empty === Method @@ -626,6 +710,8 @@ fn test_method_delete_list() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method @@ -633,7 +719,7 @@ fn test_method_delete_list() -> Result<(), Box> { ├─ GET [1] ╰─ POST [1] === Chains - 1-1 + *-1-1 "); let route = RouteBuilder::new() @@ -643,6 +729,8 @@ fn test_method_delete_list() -> Result<(), Box> { router.delete(&route)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path Empty === Method @@ -665,6 +753,8 @@ fn test_method_delete_mismatch_list_inner() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method @@ -672,7 +762,7 @@ fn test_method_delete_mismatch_list_inner() -> Result<(), Box> { ├─ GET [1] ╰─ PUT [1] === Chains - 1-1 + *-1-1 "); let route = RouteBuilder::new() @@ -686,6 +776,8 @@ fn test_method_delete_mismatch_list_inner() -> Result<(), Box> { ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method @@ -693,7 +785,7 @@ fn test_method_delete_mismatch_list_inner() -> Result<(), Box> { ├─ GET [1] ╰─ PUT [1] === Chains - 1-1 + *-1-1 "); Ok(()) @@ -710,13 +802,15 @@ fn test_method_delete_mismatch_list_outer() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-1 + *-1-1 "); let route = RouteBuilder::new() @@ -730,13 +824,15 @@ fn test_method_delete_mismatch_list_outer() -> Result<(), Box> { ); insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method [1] ╰─ GET [1] === Chains - 1-1 + *-1-1 "); Ok(()) diff --git a/tests/optimize.rs b/tests/optimize.rs index 0ed07729..6d4d9458 100644 --- a/tests/optimize.rs +++ b/tests/optimize.rs @@ -13,6 +13,8 @@ fn test_optimize_removal() -> Result<(), Box> { router.insert(&route, 3)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/ ╰─ {id} [1] @@ -22,15 +24,17 @@ fn test_optimize_removal() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* - 3-* + *-1-* + *-2-* + *-3-* "); let route = RouteBuilder::new().route("/users/{id}/profile").build()?; router.delete(&route)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/ ╰─ {id} [1] @@ -38,21 +42,23 @@ fn test_optimize_removal() -> Result<(), Box> { === Method Empty === Chains - 1-* - 3-* + *-1-* + *-3-* "); let route = RouteBuilder::new().route("/users/{id}/settings").build()?; router.delete(&route)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/ ╰─ {id} [1] === Method Empty === Chains - 1-* + *-1-* "); Ok(()) @@ -70,6 +76,8 @@ fn test_optimize_data() -> Result<(), Box> { router.insert(&route, 3)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/ ╰─ {id} [1] @@ -79,15 +87,17 @@ fn test_optimize_data() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* - 3-* + *-1-* + *-2-* + *-3-* "); let route = RouteBuilder::new().route("/users/{id}").build()?; router.delete(&route)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/ ╰─ {id} @@ -97,8 +107,8 @@ fn test_optimize_data() -> Result<(), Box> { === Method Empty === Chains - 2-* - 3-* + *-2-* + *-3-* "); Ok(()) @@ -116,6 +126,8 @@ fn test_optimize_compression() -> Result<(), Box> { router.insert(&route, 3)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /a [2] ╰─ b [3] @@ -123,23 +135,25 @@ fn test_optimize_compression() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* - 3-* + *-1-* + *-2-* + *-3-* "); let route = RouteBuilder::new().route("/ab").build()?; router.delete(&route)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /a [2] ╰─ bc [1] === Method Empty === Chains - 1-* - 2-* + *-1-* + *-2-* "); Ok(()) diff --git a/tests/optional.rs b/tests/optional.rs index 80fc2087..734cea8a 100644 --- a/tests/optional.rs +++ b/tests/optional.rs @@ -1,7 +1,9 @@ use similar_asserts::assert_eq; use smallvec::smallvec; use std::error::Error; -use wayfind::{Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router}; +use wayfind::{ + AuthorityMatch, Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router, +}; #[test] fn test_optional_starting() -> Result<(), Box> { @@ -11,6 +13,8 @@ fn test_optional_starting() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ├─ users [1] @@ -19,7 +23,7 @@ fn test_optional_starting() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/en/users").build()?; @@ -28,6 +32,10 @@ fn test_optional_starting() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/{lang})/users", expanded: Some("/{lang}/users"), @@ -43,6 +51,10 @@ fn test_optional_starting() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/{lang})/users", expanded: Some("/users"), @@ -63,13 +75,15 @@ fn test_optional_ending() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] ╰─ / [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/users").build()?; @@ -78,6 +92,10 @@ fn test_optional_ending() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users(/)", expanded: Some("/users"), @@ -93,6 +111,10 @@ fn test_optional_ending() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users(/)", expanded: Some("/users/"), @@ -113,6 +135,8 @@ fn test_optional_nested() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ╰─ a [1] @@ -121,7 +145,7 @@ fn test_optional_nested() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/a/b/c").build()?; @@ -130,6 +154,10 @@ fn test_optional_nested() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a(/b(/c)))", expanded: Some("/a/b/c"), @@ -145,6 +173,10 @@ fn test_optional_nested() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a(/b(/c)))", expanded: Some("/a/b"), @@ -160,6 +192,10 @@ fn test_optional_nested() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a(/b(/c)))", expanded: Some("/a"), @@ -175,6 +211,10 @@ fn test_optional_nested() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a(/b(/c)))", expanded: Some("/"), @@ -195,13 +235,15 @@ fn test_optional_only() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ╰─ test [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/test").build()?; @@ -210,6 +252,10 @@ fn test_optional_only() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/test)", expanded: Some("/test"), @@ -225,6 +271,10 @@ fn test_optional_only() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/test)", expanded: Some("/"), @@ -245,6 +295,8 @@ fn test_optional_touching() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / [1] ├─ a [1] @@ -258,7 +310,7 @@ fn test_optional_touching() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/a/b/c").build()?; @@ -267,6 +319,10 @@ fn test_optional_touching() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a)(/b)(/c)", expanded: Some("/a/b/c"), @@ -282,6 +338,10 @@ fn test_optional_touching() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a)(/b)(/c)", expanded: Some("/a/b"), @@ -297,6 +357,10 @@ fn test_optional_touching() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a)(/b)(/c)", expanded: Some("/a/c"), @@ -312,6 +376,10 @@ fn test_optional_touching() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a)(/b)(/c)", expanded: Some("/a"), @@ -327,6 +395,10 @@ fn test_optional_touching() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a)(/b)(/c)", expanded: Some("/b/c"), @@ -342,6 +414,10 @@ fn test_optional_touching() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a)(/b)(/c)", expanded: Some("/b"), @@ -357,6 +433,10 @@ fn test_optional_touching() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a)(/b)(/c)", expanded: Some("/c"), @@ -372,6 +452,10 @@ fn test_optional_touching() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "(/a)(/b)(/c)", expanded: Some("/"), diff --git a/tests/static.rs b/tests/static.rs index ad548a3a..474e25e7 100644 --- a/tests/static.rs +++ b/tests/static.rs @@ -1,7 +1,9 @@ use similar_asserts::assert_eq; use smallvec::smallvec; use std::error::Error; -use wayfind::{Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router}; +use wayfind::{ + AuthorityMatch, Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router, +}; #[test] fn test_static_simple() -> Result<(), Box> { @@ -11,12 +13,14 @@ fn test_static_simple() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/users").build()?; @@ -25,6 +29,10 @@ fn test_static_simple() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -51,14 +59,16 @@ fn test_static_overlapping() -> Result<(), Box> { router.insert(&route, 2)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /user [1] ╰─ s [2] === Method Empty === Chains - 1-* - 2-* + *-1-* + *-2-* "); let request = RequestBuilder::new().path("/user").build()?; @@ -67,6 +77,10 @@ fn test_static_overlapping() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/user", expanded: None, @@ -82,6 +96,10 @@ fn test_static_overlapping() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -112,6 +130,8 @@ fn test_static_overlapping_slash() -> Result<(), Box> { router.insert(&route, 2)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /user ├─ /1 [2] @@ -119,8 +139,8 @@ fn test_static_overlapping_slash() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* + *-1-* + *-2-* "); let request = RequestBuilder::new().path("/user_1").build()?; @@ -129,6 +149,10 @@ fn test_static_overlapping_slash() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/user_1", expanded: None, @@ -144,6 +168,10 @@ fn test_static_overlapping_slash() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/user/1", expanded: None, @@ -182,6 +210,8 @@ fn test_static_split_multibyte() -> Result<(), Box> { router.insert(&route, 6)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /� ├─ �‍👩‍� @@ -197,12 +227,12 @@ fn test_static_split_multibyte() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* - 3-* - 4-* - 5-* - 6-* + *-1-* + *-2-* + *-3-* + *-4-* + *-5-* + *-6-* "); let request = RequestBuilder::new().path("/👨‍👩‍👧").build()?; // Family: Man, Woman, Girl @@ -211,6 +241,10 @@ fn test_static_split_multibyte() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/👨‍👩‍👧", expanded: None, @@ -226,6 +260,10 @@ fn test_static_split_multibyte() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/👨‍👩‍👦", expanded: None, @@ -264,6 +302,8 @@ fn test_static_case_sensitive() -> Result<(), Box> { router.insert(&route, 2)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ├─ Users [2] @@ -271,8 +311,8 @@ fn test_static_case_sensitive() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* + *-1-* + *-2-* "); let request = RequestBuilder::new().path("/users").build()?; @@ -281,6 +321,10 @@ fn test_static_case_sensitive() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users", expanded: None, @@ -296,6 +340,10 @@ fn test_static_case_sensitive() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/Users", expanded: None, @@ -316,12 +364,14 @@ fn test_static_whitespace() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users /items [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/users /items").build()?; @@ -330,6 +380,10 @@ fn test_static_whitespace() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users /items", expanded: None, @@ -356,6 +410,8 @@ fn test_static_duplicate_slashes() -> Result<(), Box> { router.insert(&route, 2)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users/ ├─ /items [2] @@ -363,8 +419,8 @@ fn test_static_duplicate_slashes() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* + *-1-* + *-2-* "); let request = RequestBuilder::new().path("/users/items").build()?; @@ -373,6 +429,10 @@ fn test_static_duplicate_slashes() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users/items", expanded: None, @@ -388,6 +448,10 @@ fn test_static_duplicate_slashes() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users//items", expanded: None, @@ -408,12 +472,14 @@ fn test_static_empty_segments() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path /users///items [1] === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/users///items").build()?; @@ -422,6 +488,10 @@ fn test_static_empty_segments() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/users///items", expanded: None, diff --git a/tests/wildcard.rs b/tests/wildcard.rs index 39f6a7d3..60877d0c 100644 --- a/tests/wildcard.rs +++ b/tests/wildcard.rs @@ -1,7 +1,9 @@ use similar_asserts::assert_eq; use smallvec::smallvec; use std::error::Error; -use wayfind::{Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router}; +use wayfind::{ + AuthorityMatch, Match, MethodMatch, PathMatch, RequestBuilder, RouteBuilder, Router, +}; #[test] fn test_wildcard_simple() -> Result<(), Box> { @@ -11,6 +13,8 @@ fn test_wildcard_simple() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {*path} @@ -18,7 +22,7 @@ fn test_wildcard_simple() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/docs/delete").build()?; @@ -27,6 +31,10 @@ fn test_wildcard_simple() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*path}/delete", expanded: None, @@ -44,6 +52,10 @@ fn test_wildcard_simple() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*path}/delete", expanded: None, @@ -70,6 +82,8 @@ fn test_wildcard_multiple() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {*prefix} @@ -79,7 +93,7 @@ fn test_wildcard_multiple() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/a/static/b/file").build()?; @@ -88,6 +102,10 @@ fn test_wildcard_multiple() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*prefix}/static/{*suffix}/file", expanded: None, @@ -105,6 +123,10 @@ fn test_wildcard_multiple() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*prefix}/static/{*suffix}/file", expanded: None, @@ -125,6 +147,8 @@ fn test_wildcard_inline() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {*path} @@ -132,7 +156,7 @@ fn test_wildcard_inline() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/page.html").build()?; @@ -141,6 +165,10 @@ fn test_wildcard_inline() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*path}.html", expanded: None, @@ -156,6 +184,10 @@ fn test_wildcard_inline() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*path}.html", expanded: None, @@ -180,6 +212,8 @@ fn test_wildcard_greedy() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {*first} @@ -188,7 +222,7 @@ fn test_wildcard_greedy() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/a-b-c").build()?; @@ -197,6 +231,10 @@ fn test_wildcard_greedy() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*first}-{*second}", expanded: None, @@ -214,6 +252,10 @@ fn test_wildcard_greedy() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*first}-{*second}", expanded: None, @@ -237,6 +279,8 @@ fn test_wildcard_empty_segments() -> Result<(), Box> { router.insert(&route, 1)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ╰─ {*path} @@ -244,7 +288,7 @@ fn test_wildcard_empty_segments() -> Result<(), Box> { === Method Empty === Chains - 1-* + *-1-* "); let request = RequestBuilder::new().path("/start/middle/end").build()?; @@ -253,6 +297,10 @@ fn test_wildcard_empty_segments() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*path}/end", expanded: None, @@ -268,6 +316,10 @@ fn test_wildcard_empty_segments() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*path}/end", expanded: None, @@ -296,6 +348,8 @@ fn test_wildcard_priority() -> Result<(), Box> { router.insert(&route, 5)?; insta::assert_snapshot!(router, @r" + === Authority + Empty === Path / ├─ prefix. @@ -310,11 +364,11 @@ fn test_wildcard_priority() -> Result<(), Box> { === Method Empty === Chains - 1-* - 2-* - 3-* - 4-* - 5-* + *-1-* + *-2-* + *-3-* + *-4-* + *-5-* "); let request = RequestBuilder::new().path("/static/path").build()?; @@ -323,6 +377,10 @@ fn test_wildcard_priority() -> Result<(), Box> { search, Some(Match { data: &1, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/static/path", expanded: None, @@ -340,6 +398,10 @@ fn test_wildcard_priority() -> Result<(), Box> { search, Some(Match { data: &2, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/static/{*rest}", expanded: None, @@ -357,6 +419,10 @@ fn test_wildcard_priority() -> Result<(), Box> { search, Some(Match { data: &3, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*path}/static", expanded: None, @@ -374,6 +440,10 @@ fn test_wildcard_priority() -> Result<(), Box> { search, Some(Match { data: &4, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/prefix.{*suffix}", expanded: None, @@ -391,6 +461,10 @@ fn test_wildcard_priority() -> Result<(), Box> { search, Some(Match { data: &5, + authority: AuthorityMatch { + authority: None, + parameters: smallvec![] + }, path: PathMatch { route: "/{*prefix}.suffix", expanded: None,