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编程

Download history 14/week @ 2024-06-30 58/week @ 2024-07-28

每月 58 次下载

MIT 许可证

34KB
257

Usher

Build Status Crates.io

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中有两个可用的特质;ParserMatcher 特质。这两个特质可以实施来描述如何匹配传入路径中的特定段。

Matcher 特质用于确定传入路径段是否与配置的路径段匹配。它还负责提取与传入段关联的任何捕获。Parser 特质用于计算在配置的路径段上应使用哪种 Matcher 类型。乍一看,这两者似乎可以合并,但区别在于 Parser 特质在路由器创建时操作,而 Matcher 特质在匹配创建的路由器时执行。

为了演示这些特质,我们可以使用典型的Web框架中的 :id 示例。这种语法的概念是它应该匹配提供给树的任何值。如果我的路由器配置了路径 /:id,它将匹配传入路径 /123/abc(但不匹配 /)。这将提供一个捕获值 id,它包含值 123abc

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))
    }
}

拆分特质的好处之一是你可以轻松地更改语法。尽管 DynamicMatcherDynamicParser 都包含在 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