Skip to content

Commit

Permalink
Improve unit test coverage (#800)
Browse files Browse the repository at this point in the history
* wip

* wip

* wip

* Format Rust code using rustfmt

* wip

* wip

* wip

* wip

* wip

* Format Rust code using rustfmt

* wip

* fix ci

* wip

* wip

* wip

* wip

* wip

* Format Rust code using rustfmt

* wip

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
chrislearn and github-actions[bot] authored Jun 5, 2024
1 parent bf8b99d commit 92e5aec
Show file tree
Hide file tree
Showing 43 changed files with 1,232 additions and 265 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ opentelemetry = { version = "0.23", default-features = false }
parking_lot = "0.12"
path-slash = "0.2"
percent-encoding = "2"
paste = "1"
pin-project = "1"
proc-macro-crate = {version = ">= 2, <= 4"}
proc-macro2-diagnostics = { version = "0.10", default-features = true }
Expand All @@ -108,6 +109,7 @@ serde_json = "1"
serde-xml-rs = "0.6"
serde_urlencoded = "0.7"
serde_yaml = "0.9"
serde_with = "3.0"
sha2 = "0.10"
smallvec = "1"
syn = "2"
Expand Down
244 changes: 244 additions & 0 deletions crates/core/docs/routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
Routing and filters.


- [What is router](#what-is-router)
- [Write in flat way](#write-in-flat-way)
- [Write in tree way](#write-in-tree-way)
- [Get param in routers](#get-param-in-routers)
- [Add middlewares](#add-middlewares)
- [Filters](#filters)
- [Path filter](#path-filter)
- [Method filter](#method-filter)
- [Custom Wisp](#custom-wisp)

# What is router

Router can route http requests to different handlers. This is a basic and key feature in salvo.

The interior of `Router` is actually composed of a series of filters (Filter). When a request comes, the route will test itself and its descendants in order to see if they can match the request in the order they were added, and then execute the middleware on the entire chain formed by the route and its descendants in sequence. If the status of `Response` is set to error (4XX, 5XX) or jump (3XX) during processing, the subsequent middleware and `Handler` will be skipped. You can also manually adjust `ctrl.skip_rest()` to skip subsequent middleware and `Handler`.

# Write in flat way

We can write routers in flat way, like this:

```rust,compile_fail
Router::with_path("writers").get(list_writers).post(create_writer);
Router::with_path("writers/<id>").get(show_writer).patch(edit_writer).delete(delete_writer);
Router::with_path("writers/<id>/articles").get(list_writer_articles);
```

# Write in tree way

We can write router like a tree, this is also the recommended way:

```rust,compile_fail
Router::with_path("writers")
.get(list_writers)
.post(create_writer)
.push(
Router::with_path("<id>")
.get(show_writer)
.patch(edit_writer)
.delete(delete_writer)
.push(Router::with_path("articles").get(list_writer_articles)),
);
```

This form of definition can make the definition of router clear and simple for complex projects.

There are many methods in `Router` that will return to `Self` after being called, so as to write code in a chain. Sometimes, you need to decide how to route according to certain conditions, and the `Router` also provides `then` function, which is also easy to use:

```rust,compile_fail
Router::new()
.push(
Router::with_path("articles")
.get(list_articles)
.push(Router::with_path("<id>").get(show_article))
.then(|router|{
if admin_mode() {
router.post(create_article).push(
Router::with_path("<id>").patch(update_article).delete(delete_writer)
)
} else {
router
}
}),
);
```

This example represents that only when the server is in `admin_mode`, routers such as creating articles, editing and deleting articles will be added.

# Get param in routers

In the previous source code, `<id>` is a param definition. We can access its value via Request instance:

```rust,compile_fail
#[handler]
async fn show_writer(req: &mut Request) {
let id = req.param::<i64>("id").unwrap();
}
```

`<id>` matches a fragment in the path, under normal circumstances, the article `id` is just a number, which we can use regular expressions to restrict `id` matching rules, `r"<id:/\d+/>"`.

For numeric characters there is an easier way to use `<id:num>`, the specific writing is:

- `<id:num>`, matches any number of numeric characters;
- `<id:num[10]>`, only matches a certain number of numeric characters, where 10 means that the match only matches 10 numeric characters;
- `<id:num(..10)>` means matching 1 to 9 numeric characters;
- `<id:num(3..10)>` means matching 3 to 9 numeric characters;
- `<id:num(..=10)>` means matching 1 to 10 numeric characters;
- `<id:num(3..=10)>` means match 3 to 10 numeric characters;
- `<id:num(10..)>` means to match at least 10 numeric characters.

You can also use `<**>`, `<*+*>` or `<*?>` to match all remaining path fragments.
In order to make the code more readable, you can also add appropriate name to make the path semantics more clear, for example: `<**file_path>`.

It is allowed to combine multiple expressions to match the same path segment, such as `/articles/article_<id:num>/`, `/images/<name>.<ext>`.

# Add middlewares

Middleware can be added via `hoop` method.

```rust,compile_fail
Router::new()
.hoop(check_authed)
.path("writers")
.get(list_writers)
.post(create_writer)
.push(
Router::with_path("<id>")
.get(show_writer)
.patch(edit_writer)
.delete(delete_writer)
.push(Router::with_path("articles").get(list_writer_articles)),
);
```

In this example, the root router has a middleware to check current user is authenticated. This middleware will affect the root router and its descendants.

If we don't want to check user is authed when current user view writer informations and articles. We can write router like this:

```rust,compile_fail
Router::new()
.push(
Router::new()
.hoop(check_authed)
.path("writers")
.post(create_writer)
.push(Router::with_path("<id>").patch(edit_writer).delete(delete_writer)),
)
.push(
Router::new().path("writers").get(list_writers).push(
Router::with_path("<id>")
.get(show_writer)
.push(Router::with_path("articles").get(list_writer_articles)),
),
);
```

Although there are two routers have the same `path("writers")`, they can still be added to the same parent route at the same time.

# Filters

Many methods in `Router` return to themselves in order to easily implement chain writing. Sometimes, in some cases, you need to judge based on conditions before you can add routing. Routing also provides some convenience Method, simplify code writing.

`Router` uses the filter to determine whether the route matches. The filter supports logical operations and or. Multiple filters can be added to a route. When all the added filters match, the route is matched successfully.

It should be noted that the URL collection of the website is a tree structure, and this structure is not equivalent to the tree structure of `Router`. A node of the URL may correspond to multiple `Router`. For example, some paths under the `articles/` path require login, and some paths do not require login. Therefore, we can put the same login requirements under a `Router`, and on top of them Add authentication middleware on `Router`. In addition, you can access it without logging in and put it under another route without authentication middleware:

```rust,compile_fail
Router::new()
.push(
Router::new()
.path("articles")
.get(list_articles)
.push(Router::new().path("<id>").get(show_article)),
)
.push(
Router::new()
.path("articles")
.hoop(auth_check)
.post(list_articles)
.push(Router::new().path("<id>").patch(edit_article).delete(delete_article)),
);
```

Router is used to filter requests, and then send the requests to different Handlers for processing.

The most commonly used filtering is `path` and `method`. `path` matches path information; `method` matches the requested Method.

We can use `and`, `or` to connect between filter conditions, for example:

```rust,compile_fail
Router::new().filter(filter::path("hello").and(filter::get()));
```

## Path filter

The filter is based on the request path is the most frequently used. Parameters can be defined in the path filter, such as:

```rust,compile_fail
Router::with_path("articles/<id>").get(show_article);
Router::with_path("files/<**rest_path>").get(serve_file)
```

In `Handler`, it can be obtained through the `get_param` function of the `Request` object:

```rust,compile_fail
#[handler]
pub async fn show_article(req: &mut Request) {
let article_id = req.param::<i64>("id");
}
#[handler]
pub async fn serve_file(req: &mut Request) {
let rest_path = req.param::<i64>("**rest_path");
}
```

## Method filter

Filter requests based on the `HTTP` request's `Method`, for example:

```rust,compile_fail
Router::new().get(show_article).patch(update_article).delete(delete_article);
```

Here `get`, `patch`, `delete` are all Method filters. It is actually equivalent to:

```rust,compile_fail
use salvo::routing::filter;
let show_router = Router::with_filter(filter::get()).handle(show_article);
let update_router = Router::with_filter(filter::patch()).handle(update_article);
let delete_router = Router::with_filter(filter::get()).handle(delete_article);
Router::new().push(show_router).push(update_router).push(delete_router);
```

## Custom Wisp

For some frequently-occurring matching expressions, we can name a short name by `PathFilter::register_wisp_regex` or `PathFilter::register_wisp_builder`. For example, GUID format is often used in paths appears, normally written like this every time a match is required:

```rust,compile_fail
Router::with_path("/articles/<id:/[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}/>");
Router::with_path("/users/<id:/[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}/>");
```

However, writing this complex regular expression every time is prone to errors and hard-coding the regex is not ideal. We could separate the regex into its own Regex variable like so:

```rust,compile_fail
use salvo_core::prelude::*;
use salvo_core::routing::filter::PathFilter;
#[tokio::main]
async fn main() {
let guid = regex::Regex::new("[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}").unwrap();
PathFilter::register_wisp_regex("guid", guid);
Router::new()
.push(Router::with_path("/articles/<id:guid>").get(show_article))
.push(Router::with_path("/users/<id:guid>").get(show_user));
}
```

You only need to register once, and then you can directly match the GUID through the simple writing method as `<id:guid>`, which simplifies the writing of the code.
7 changes: 4 additions & 3 deletions crates/core/src/catcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
//! If the status code of [`Response`] is an error, and the body of [`Response`] is empty, then salvo
//! will try to use `Catcher` to catch the error and display a friendly error page.
//!
//! You can return a system default [`Catcher`] through [`Catcher::default()`], and then add it to [`Service`](crate::Service):
//! You can return a system default [`Catcher`] through [`Catcher::default()`], and then add it to
//! [`Service`](crate::Service):
//!
//! # Example
//!
Expand All @@ -30,8 +31,8 @@
//! You can add a custom error handler to [`Catcher`] by adding `hoop` to the default `Catcher`.
//! The error handler is still [`Handler`].
//!
//! You can add multiple custom error catching handlers to [`Catcher`] through [`Catcher::hoop`]. The custom error handler can call
//! the [`FlowCtrl::skip_rest`] method after handling the error to skip next error handlers and return early.
//! You can add multiple custom error catching handlers to [`Catcher`] through [`Catcher::hoop`]. The custom error
//! handler can call [`FlowCtrl::skip_rest()`] method to skip next error handlers and return early.

use std::borrow::Cow;
use std::sync::Arc;
Expand Down
21 changes: 9 additions & 12 deletions crates/core/src/extract/case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,25 @@ use std::str::FromStr;

use self::RenameRule::*;

/// Rename rule for a field.
/// Rename rule for field.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[non_exhaustive]
pub enum RenameRule {
/// Rename direct children to "lowercase" style.
/// Rename to "lowercase" style.
LowerCase,
/// Rename direct children to "UPPERCASE" style.
/// Rename to "UPPERCASE" style.
UpperCase,
/// Rename direct children to "PascalCase" style, as typically used for
/// enum variants.
/// Rename to "PascalCase" style.
PascalCase,
/// Rename direct children to "camelCase" style.
/// Rename to "camelCase" style.
CamelCase,
/// Rename direct children to "snake_case" style, as commonly used for
/// fields.
/// Rename to "snake_case" style.
SnakeCase,
/// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly
/// used for constants.
/// Rename to "SCREAMING_SNAKE_CASE" style.
ScreamingSnakeCase,
/// Rename direct children to "kebab-case" style.
/// Rename to "kebab-case" style.
KebabCase,
/// Rename direct children to "SCREAMING-KEBAB-CASE" style.
/// Rename to "SCREAMING-KEBAB-CASE" style.
ScreamingKebabCase,
}

Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/extract/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl FromStr for SourceFrom {
}
}

/// Source parser for a source.
/// Parser for a source.
///
/// This parser is used to parse field data, not the request mime type.
/// For example, if request is posted as form, but the field is string as json format, it can be parsed as json.
Expand Down
8 changes: 4 additions & 4 deletions crates/core/src/fs/named_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ pub(crate) enum Flag {
/// # Examples
///
/// ```
/// # use salvo_core::fs::NamedFile;
/// # async fn open() {
/// let file = NamedFile::open("foo.txt").await;
/// # }
/// use salvo_core::fs::NamedFile;
/// async fn open() {
/// let file = NamedFile::open("foo.txt").await;
/// }
///
#[derive(Debug)]
pub struct NamedFile {
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/http/body/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use bytes::Bytes;
use futures_channel::{mpsc, oneshot};
use hyper::HeaderMap;

/// A sender half created through [`ResBody ::Channel`].
/// A sender half created through [`ResBody::Channel`](super::ResBody::Channel).
///
/// Useful when wanting to stream chunks from another thread.
///
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/http/body/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Http body.
//! HTTP body.

pub use hyper::body::{Body, Frame, SizeHint};

Expand Down
3 changes: 1 addition & 2 deletions crates/core/src/http/body/req.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
//! Http body.
use std::fmt::{self, Formatter};
use std::io::{Error as IoError, ErrorKind, Result as IoResult};
use std::pin::Pin;
Expand All @@ -15,7 +14,7 @@ use crate::BoxedError;
pub(crate) type BoxedBody = Pin<Box<dyn Body<Data = Bytes, Error = BoxedError> + Send + Sync + 'static>>;
pub(crate) type PollFrame = Poll<Option<Result<Frame<Bytes>, IoError>>>;

/// Body for request.
/// Body for HTTP request.
#[non_exhaustive]
#[derive(Default)]
pub enum ReqBody {
Expand Down
4 changes: 1 addition & 3 deletions crates/core/src/http/body/res.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
//! Http body.

use std::collections::VecDeque;
use std::fmt::Debug;
use std::future::Future;
Expand All @@ -18,7 +16,7 @@ use crate::error::BoxedError;
use crate::http::body::{BodyReceiver, BodySender, BytesFrame};
use crate::prelude::StatusError;

/// Response body type.
/// Body for HTTP response.
#[allow(clippy::type_complexity)]
#[non_exhaustive]
#[derive(Default)]
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/http/errors/parse_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{async_trait, BoxedError, Depot, Writer};
/// Result type with `ParseError` has it's error type.
pub type ParseResult<T> = Result<T, ParseError>;

/// ParseError, errors happened when read data from http request.
/// Errors happened when read data from http request.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ParseError {
Expand Down
Loading

0 comments on commit 92e5aec

Please sign in to comment.