axum crate の Router (4) Router::merge と nest
今回は axum crate の Router::merge と nest について書きます。
今回も axum crate のバージョンは 0.8.6 です。
Router::merge と Router::nest とは
Router::merge と Router::nest とは、 Router を merge したり nest したりするものです。 Router を複数の箇所で定義し、それを統合する際に使用します。
含まれるルートを Router::merge はフラットに統合し、 Router::nest は入れ子の形に統合します。
たとえば Router::merge なら↓をフラットに統合するイメージ。
-
/a1,/b/1を扱う1系 (?)Router -
/a2,/b/2を扱う2系 (?)Router
たとえば Router::nest なら↓を入れ子の形に統合するイメージ。
-
/a1,/a2を扱うa系 (?)Router -
/1,/2を扱うb系 (?)Routerを/bに入れ子の形で- ※
/bの prefix は入れ子にするので持っていない
- ※
そんなイメージです。
どういう単位で Router を定義するかはユーザー判断なので、自由度をもたせているわけですね。
Router::merge
Router::merge の詳細を見ていきます。
https://docs.rs/axum/0.8.6/axum/struct.Router.html#method.merge
pub fn merge<R>(self, other: R) -> Self
where
R: Into<Router<S>>,
ソースコードを見てみます。
https://docs.rs/axum/0.8.6/src/axum/routing/mod.rs.html#252-300
path_router は内部実装である PathRouter::merge にまかせています。
fallback_router は両方に設定されていると panic に、そうでなければよしなにマージしてくれそうです。
両方に fallback router があると panic する仕様はドキュメントにも記載されています。
fallback router はルートにマッチしなかった場合に使用されることから、一方が使用されてしまうと他方が使用されることはないので 2 つあっても困るからですね。
次に PathRouter::merge を見ます。
https://github.com/tokio-rs/axum/blob/axum-v0.8.6/axum/src/routing/path_router.rs#L162-L204
other 側の routes を走査して self.route() で追加していますね。 route については過去に見ていますね。
Router::nest
Router::nest の詳細を見ていきます。
https://docs.rs/axum/0.8.6/axum/struct.Router.html#method.nest
pub fn nest(self, path: &str, router: Router<S>) -> Self
ソースコード。
https://docs.rs/axum/0.8.6/src/axum/routing/mod.rs.html#209-231
Router::nest は path_router も fallback_router も共に nest されています。
Router::nest の fallback router は入れ子にされた側のパスへのリクエストではそちらの fallback が優先されるので、両方にあっても問題ないですね。
次に PathRouter::nest を見ます。
https://github.com/tokio-rs/axum/blob/axum-v0.8.6/axum/src/routing/path_router.rs#L206-L244
Router::merge と同様に self.route() していくのですが、 path_for_nested_route でパスを書き換えています。
https://github.com/tokio-rs/axum/blob/axum-v0.8.6/axum/src/routing/path_router.rs#L535-L546
素朴な書き換えですね。
pub(crate) fn path_for_nested_route<'a>(prefix: &'a str, path: &'a str) -> Cow<'a, str> {
debug_assert!(prefix.starts_with('/'));
debug_assert!(path.starts_with('/'));
if prefix.ends_with('/') {
format!("{prefix}{}", path.trim_start_matches('/')).into()
} else if path == "/" {
prefix.into()
} else {
format!("{prefix}{path}").into()
}
}
余談ですが debug_assert macro 、便利そうですね。実行コストが大したことなければ assert! のほうが安心できそうですが。
細かい点: state の扱い
さて、おおまかには把握したところで細かいところを見ていきます。 state の扱いですね。
Router::merge も Router::nest も state の型が一致しないといけません。
テストコードの全体は https://github.com/bouzuya/rust-examples/blob/8912fd78db7d43348e89371dc270132d3bcafb7b/axum7/src/main.rs に置いています。
#[derive(Clone)]
struct StateA {
a: String,
}
fn router_a() -> axum::Router<StateA> {
axum::Router::new()
.route("/a/1", axum::routing::get(|axum::extract::State(StateA { a }): axum::extract::State<StateA>| async move { a }))
}
#[derive(Clone)]
struct StateB {
b: String,
}
fn router_b() -> axum::Router<StateB> {
axum::Router::new()
.route("/b/1", axum::routing::get(|axum::extract::State(StateB { b }): axum::extract::State<StateB>| async move { b }))
}
fn router() -> axum::Router<()> {
axum::Router::new()
.merge(router_a().with_state(StateA {
a: "a_1".to_owned(),
}))
.merge(router_b().with_state(StateB {
b: "b_1".to_owned(),
}))
}
async fn f(path: &str, status: axum::http::StatusCode, body: &str) -> anyhow::Result<()> {
let router = router();
let request = axum::http::Request::builder()
.uri(path)
.body(axum::body::Body::empty())?;
let response = send_request(router, request).await?;
assert_eq!(response.status(), status);
assert_eq!(response.into_body_string().await?, body);
Ok(())
}
f("/a/1", axum::http::StatusCode::OK, "a_1").await?;
f("/b/1", axum::http::StatusCode::OK, "b_1").await?;
f("/c/1", axum::http::StatusCode::NOT_FOUND, "").await?;
merge の箇所で with_state で Router<StateA> を Router<()> に、 Router<StateB> を Router<()> に変えることで型を合わせています。
トレイトを使用する例
トレイトを使用して一致させるのも手です。ドクターメイト株式会社ではこの方法を採用していたりします。
trait StateTraitA {
fn a(&self) -> String;
}
fn router_a<S: StateTraitA + Clone + Send + Sync + 'static>() -> axum::Router<S> {
axum::Router::new().route(
"/a/1",
axum::routing::get(
|axum::extract::State(s): axum::extract::State<S>| async move { s.a() },
),
)
}
trait StateTraitB {
fn b(&self) -> String;
}
fn router_b<S: StateTraitB + Clone + Send + Sync + 'static>() -> axum::Router<S> {
axum::Router::new().route(
"/b/1",
axum::routing::get(
|axum::extract::State(s): axum::extract::State<S>| async move { s.b() },
),
)
}
#[derive(Clone)]
struct State {
a: String,
b: String,
}
impl StateTraitA for State {
fn a(&self) -> String {
self.a.clone()
}
}
impl StateTraitB for State {
fn b(&self) -> String {
self.b.clone()
}
}
fn router() -> axum::Router<()> {
axum::Router::new()
.merge(router_a())
.merge(router_b())
.with_state(State {
a: "a_1".to_owned(),
b: "b_1".to_owned(),
})
}
async fn f(path: &str, status: axum::http::StatusCode, body: &str) -> anyhow::Result<()> {
let router = router();
let request = axum::http::Request::builder()
.uri(path)
.body(axum::body::Body::empty())?;
let response = send_request(router, request).await?;
assert_eq!(response.status(), status);
assert_eq!(response.into_body_string().await?, body);
Ok(())
}
f("/a/1", axum::http::StatusCode::OK, "a_1").await?;
f("/b/1", axum::http::StatusCode::OK, "b_1").await?;
f("/c/1", axum::http::StatusCode::NOT_FOUND, "").await?;
細かい点: 重複したルートの扱い
Router::merge も Router::nest も重複したルートを定義すると panic します。 Router を分散して定義しているとうっかりがあり得るので注意が必要そうです。
#[test]
#[should_panic(expected = "Overlapping method route. Handler for `GET /a/1` already exists")]
fn test_nest_duplicate() {
fn router_a() -> axum::Router<()> {
axum::Router::new().route("/1", axum::routing::get(|| async { "nest a_1" }))
}
fn router() -> axum::Router<()> {
axum::Router::new()
.route("/a/1", axum::routing::get(|| async { "a_1" }))
.nest("/a", router_a())
}
let _ = router();
}
細かい点: その他
ドキュメントによると、 .nest("/{*wildcard}", ...) や .nest("", ...) もエラーになるようです。
#[test]
#[should_panic(expected = "Invalid route: nested routes cannot contain wildcards (*)")]
fn test_wildcard() {
let _ = axum::Router::<()>::new().nest("/{*wildcard}", axum::Router::new());
}
あとは nest で指定した場合は Path の挙動が配置によって変わることや Uri の prefix が削られるなどの記載もあります。このあたりはあまり細かい検証はできていません。
おわりに
Router::merge と Router::nest について書いてみました。 Router をいくつかに分割して定義しておきたいケースはままあるのでこういう機能は便利ですね。
次回は Router から Handler に進もうかと思います。
Discussion