4个版本
0.2.1 | 2022年6月27日 |
---|---|
0.2.0 | 2020年2月21日 |
0.1.1 | 2019年4月28日 |
0.1.0 | 2019年4月3日 |
#1809 in Web编程
每月 58 次下载
34KB
257 行
Usher
Usher提供了一种简单的方法来在Rust中构建参数化路由树。
这些树的节点自然具有泛型特性,使得Usher适用于各种用例。匹配和参数化规则由开发者通过一组简单的特质定义,允许在路由算法本身中进行定制。这为路由可能被使用的各种环境提供了方便的支持。
这个项目起源于对在不需要整个框架的情况下,在Hyper之上构建小工具的个人需求。随着时间的推移,它变得明显,Usher提供的功能超出了HTTP领域,因此API被调整为更通用。因此,Usher提供了一些基于特定领域的“扩展”,这些扩展实际上提供了典型路由器的糖衣。默认情况下,这些扩展都是关闭的,但可以通过Cargo功能轻松设置为启用。
在v1.0之前,您可以期待API进行一些更改,尽管我会尽力将其保持在最低限度以减少任何震荡。可能发生变化的一个选择是围绕使用非文件系统路径的API。除此之外,除了优化(以及与其相关的API重构)仍需彻底调查外,您还可以期待变化。
入门指南
Usher可在crates.io上获取。使用它最简单的方法是在您的Cargo.toml
中添加一个条目,定义依赖项
[dependencies]
usher = "0.1"
如果您需要任何Usher扩展,可以通过设置依赖项配置中的功能标志来选择它们
usher = { version = "0.1", features = ["web"] }
您可以在文档中找到可用的扩展。
基本用法
树的构建相当简单,具体取决于您希望的结果。为了构建一个非常基本/静态的树,您可以简单地插入您关心的路由
use usher::prelude::*;
fn main() {
// First we construct our `Router` using a set of parsers. Fortunately for
// this example, Usher includes the `StaticParser` which uses basic string
// matching to determine whether a segment in the path matches.
let mut router: Router<String> = Router::new(vec![
Box::new(StaticParser),
]);
// Then we insert our routes; in this case we're going to store the numbers
// 1, 2 and 3, against their equivalent name in typed English (with a "/"
// as a prefix, as Usher expects filesystem-like paths (for now)).
router.insert("/one", "1".to_string());
router.insert("/two", "2".to_string());
router.insert("/three", "3".to_string());
// Finally we'll just do a lookup on each path, as well as the a path which
// doesn't match ("/"), just to demonstrate what the return types look like.
for path in vec!["/", "/one", "/two", "/three"] {
println!("{}: {:?}", path, router.lookup(path));
}
}
这将按照其外观进行路由;将提供的每个静态段与树进行匹配,并检索与路径关联的值。函数 lookup(path)
的返回类型是 Option<(&T, Vec<(&str, (usize, usize)>)>
,其中 &T
指的是提供的泛型值(例如 "1"
等),并且 Vec
包含在路由过程中找到的任何参数集合。如果没有参数,此向量将为空(如上所述)。
有关基于扩展(例如HTTP)的使用,请参阅包含它的模块的文档——或访问示例目录以查看实际用法。
高级用法
当然,对于某些用例,您可能需要能够控制比静态匹配路径段更多。在一个Web框架中,您可能允许某些路径段匹配并且简单地捕获它们的值(即 :id
)。为了允许这种用法,Usher中有两个可用的特质;Parser
和 Matcher
特质。这两个特质可以实施来描述如何匹配传入路径中的特定段。
Matcher
特质用于确定传入路径段是否与配置的路径段匹配。它还负责提取与传入段关联的任何捕获。Parser
特质用于计算在配置的路径段上应使用哪种 Matcher
类型。乍一看,这两者似乎可以合并,但区别在于 Parser
特质在路由器创建时操作,而 Matcher
特质在匹配创建的路由器时执行。
为了演示这些特质,我们可以使用典型的Web框架中的 :id
示例。这种语法的概念是它应该匹配提供给树的任何值。如果我的路由器配置了路径 /:id
,它将匹配传入路径 /123
和 /abc
(但不匹配 /
)。这将提供一个捕获值 id
,它包含值 123
或 abc
。
Matcher
使用我们上面定义的两个特质来实现这个模式相当简单。首先,我们必须构建我们的 Matcher
类型(技术上您可能首先写 Parser
,但按这个顺序解释更容易)。幸运的是,这里的规则非常简单。
/// A `Matcher` type used to match against dynamic segments.
///
/// The internal value here is the name of the path parameter (based on the
/// example talked through above, this would be the _owned_ `String` of `"id"`).
pub struct DynamicMatcher {
inner: String
}
impl Matcher for DynamicMatcher {
/// Determines if there is a capture for the incoming segment.
///
/// In the pattern we described above the entire value becomes the capture,
/// so we return a tuple of `("id", (start, end))` to represent the capture.
fn capture(&self, segment: &str) -> Option<(&str, (usize, usize))> {
Some((&self.inner, (0, segment.len())))
}
/// Determines if this matcher matches the incoming segment.
///
/// Because the segment is dynamic and matches any value, this is able to
/// always return `true` without even looking at the incoming segment.
fn is_match(&self, _segment: &str) -> bool {
true
}
}
这个实现相当简单,应该相当容易理解;匹配器匹配任何东西,所以 is_match/1
总是返回 true。我们总是想捕获段,所以从 capture/1
返回。关于捕获有一些事情要提一下;
- 实现
capture/1
是可选的,因为它将默认为None
。 - 只有在
is_match/1
解析为true
时,才会调用capture/1
的实现。 - 捕获所使用的元组结构是必要的,因为我们需要一种方式在运行时知道捕获的名称。这些名称不能存储在路由器本身中,因为可能存在捕获名称实际上是传入路径段函数的情况(当然,并不是特指这种情况)。
解析器
现在我们已经有了 Matcher
类型,我们需要构造一个 Parser
类型,以便将配置的段与正确的 Matcher
关联起来。在我们的案例中,这非常简单,因为我们几乎只有一条规则,即段必须是模式 :.+
,这可以大致翻译为例如目的的 starts_with(":")
。因此,Parser
类型可能如下所示
/// A `Parser` type used to parse out `DynamicMatcher` values.
pub struct DynamicParser;
impl Parser for DynamicParser {
/// Attempts to parse a segment into a corresponding `Matcher`.
///
/// As a dynamic segment is determined by the pattern `:.+`, we check the first
/// character of the segment. If the segment is not `:` we are unable to parse
/// and so return a `None` value.
///
/// If it does start with a `:`, we construct a `DynamicMatcher` and pass the
/// parameter name through as it's used when capturing values.
fn parse(&self, segment: &str) -> Option<Box<Matcher>> {
if &segment[0..1] != ":" {
return None;
}
let field = &segment[1..];
let matcher = DynamicMatcher {
inner: field.to_owned()
};
Some(Box::new(matcher))
}
}
拆分特质的好处之一是你可以轻松地更改语法。尽管 DynamicMatcher
和 DynamicParser
都包含在 Usher 中,但你可能希望使用不同的语法。参数的另一个语法示例(我认为是在 Java 领域)是 {id}
。为了适应这种情况,你只需要编写一个新的 Parser
实现即可;现有的 Matcher
结构已经工作得很好了!
/// A customer `Parser` type used to parse out `DynamicMatcher` values.
pub struct CustomDynamicParser;
impl Parser for CustomDynamicParser {
/// Attempts to parse a segment into a corresponding `Matcher`.
///
/// This will match segments based on `{id}` syntax, rather than `:id`. We have
/// to check the end characters, and pass back the something in the middle!
fn parse(&self, segment: &str) -> Option<Box<Matcher>> {
// has to start with "{"
if &segment[0..1] != "{" {
return None;
}
// has to end with "}"
if &segment[(len - 1)..] != "}" {
return None;
}
// so 1..(len - 1) trim the brackets
let field = &segment[1..(len - 1)];
let matcher = DynamicMatcher::new(field);
// wrap it up!
Some(Box::new(matcher))
}
}
当然,这也使得匹配上述两种形式中的任何一种变得非常简单。你可以在启动时将两个解析器都附加到树上,这样它将允许使用 :id
和 {id}
。这种灵活性在编写更复杂的框架时,使用 Usher 作为底层路由层时,绝对是有用的。
配置
现在我们有了这些类型,我们必须在实际的路由器中配置它们才能生效。这是在路由器初始化时完成的,你已经在基本示例中看到了一个例子,其中我们提供了基本的 StaticParser
类型。就像这个例子一样,我们直接传递我们的解析器
let mut router: Router<String> = Router::new(vec![
Box::new(DynamicParser),
Box::new(StaticParser),
]);
使用这个定义,我们的新 Parser
将用于确定我们是否可以解析路径中的动态段。下面是一个演示,它使用这两种匹配器类型(S
表示静态段,而 D
表示动态段)
/api/user/:id
^ ^ ^
| | |
S S D
请注意,提供的解析器的顺序非常重要;你应该将“最具体”的解析器放在最前面,因为它们是按顺序测试的。如果你在上面的列表中将 StaticParser
放在第一个位置,那么将永远不会继续到 DynamicParser
,因为每个段都满足 StaticParser
的要求。
依赖关系
~125KB