axum crate の Router (1) Router::route メソッドなど
前回は axum crate を学び直そうということでドキュメントの見出しを確認し、かんたんな例を書きました。
今回は axum crate の Router の概要と route メソッドを見ていこうと思います。
今回も axum crate のバージョンは 0.8.7 です。
Router struct
https://docs.rs/axum/0.8.7/axum/struct.Router.html
Router はリクエストをハンドラーにルーティングするための構造体です。
Router は S という型パラメーターをとります。これはハンドラーに要求されている (設定されず欠けている) State の型です。 State は今後とりあげると思うので、今回の記事では skip します。
Router の実装を見ます。ドキュメントの右端にある Source のリンクから参照できます。
https://docs.rs/axum/0.8.7/axum/struct.Router.html
pub struct Router<S = ()> {
inner: Arc<RouterInner<S>>,
}
内部的には inner: Arc<RouterInner<S>> というフィールドを持ちます。
Router struct のメソッドは layer や *_service や fallback などいろいろあるのですが、今回は前回の例で確認した new と route からはじめようと思います。
前回の例:
let app = axum::Router::new()
.route("/", axum::routing::get(root))
.route("/users", axum::routing::post(create_user))
.route("/users/{user_id}", axum::routing::get(get_user));
Router::new の実装の確認
https://docs.rs/axum/0.8.7/axum/struct.Router.html#method.new
Router::new は Router のコンストラクタです。
ドキュメントによるとルートを追加しないとすべてのリクエストに HTTP 404 を返すらしいです。
pub fn new() -> Self {
Self {
inner: Arc::new(RouterInner {
path_router: Default::default(),
fallback_router: PathRouter::new_fallback(),
default_fallback: true,
catch_all_fallback: Fallback::Default(Route::new(NotFound)),
}),
}
}
実装を見ると、 RouterInner のフィールドが見えます。
path_router 以外のフィールドは fallback のためのものですが、今回の記事では skip します。 Router::fallback メソッドを調べる際に改めて見ようと思います。
RouterInner 構造体の詳細は、後でまた出てくるのでここでは skip します。
Router::route の実装の確認
https://docs.rs/axum/0.8.7/axum/struct.Router.html#method.route
Router::route は Router にルートを追加するものです。
第一引数は path: &str です。 / 区切りのセグメントからなる文字列でキャプチャやワイルドカードを指定できるようです。
第二引数は method_router: MethodRouter<S> です。これは axum::routing::get(handler) や axum::routing::post(handler) などと指定していたものですね。
ドキュメントには static paths, captures, wildcards の例が示されています。
static paths は /, /foo, /users/123 のような静的なパスパターン。
captures は /{key}, /users/{id}, /users/{id}/tweets のようなキャプチャする値を含むもの。このキャプチャされた値は Path エクストラクタで取り出せます。数値や正規表現でのパターンの指定はできず、ハンドラーで処理する必要があるようです。 MatchedPath で実際のパスではなくマッチしたパス (/users/{id} のようなパターンのことですね) を得られるようです。
ワイルドカードは /{*key} のようなワイルドカードで終わるパスを指定できるもののようです。
ぼくは Path などは普段から使っているのですが、ワイルドカードにはあまり使ってきませんでした。 /assets/{*path} という例が示されており、なるほどという感じです。ワイルドカード指定した箇所の値には / を含めてよいので (そうでなければ /{key} で十分なので当然ちゃ当然なのですが)、ネストなどと組み合わせると意外とややこしい挙動になるように思いました。
さて、実装を確認します。
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/mod.rs#L178-L182
pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self {
tap_inner!(self, mut this => {
panic_on_err!(this.path_router.route(path, method_router));
})
}
tap_inner マクロを追います。
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/mod.rs#L125-L136
macro_rules! tap_inner {
( $self_:ident, mut $inner:ident => { $($stmt:stmt)* } ) => {
#[allow(redundant_semicolons)]
{
let mut $inner = $self_.into_inner();
$($stmt)*;
Router {
inner: Arc::new($inner),
}
}
};
}
panic_on_err マクロも追います。
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/mod.rs#L48-L55
macro_rules! panic_on_err {
($expr:expr) => {
match $expr {
Ok(x) => x,
Err(err) => panic!("{err}"),
}
};
}
Router::route メソッドの tap_inner マクロと panic_on_err マクロを展開すると以下のようになります。
pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self {
let mut this = self.into_inner();
match this.path_router.route(path, method_router) {
Ok(x) => x,
Err(err) => panic!("{err}"),
}
Router {
inner: Arc::new(this),
}
}
Router::into_inner の実装の確認
Router::route の実装のうち、次に調べるべきものは……
-
Router::into_innermethod - ↑の戻り値の
path_routerfield のroutemethod
です。
Router::into_inner method
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/mod.rs#L157-L167
fn into_inner(self) -> RouterInner<S> {
match Arc::try_unwrap(self.inner) {
Ok(inner) => inner,
Err(arc) => RouterInner {
path_router: arc.path_router.clone(),
fallback_router: arc.fallback_router.clone(),
default_fallback: arc.default_fallback,
catch_all_fallback: arc.catch_all_fallback.clone(),
},
}
}
RouterInner<S> の S は何か? これは Router の型パラメータ S と同じものです。 Clone + Send + Sync + 'static なトレイト境界を持ちます。
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/mod.rs#L138-L141
impl<S> Router<S>
where
S: Clone + Send + Sync + 'static,
{
self.inner は何か? Arc<RouterInner<S>> なフィールドです。冒頭にも書いているので再掲ですね。
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/mod.rs#L68-L70
pub struct Router<S = ()> {
inner: Arc<RouterInner<S>>,
}
std::sync::Arc::try_unwrap はいつ成功するか? 強い参照を持つときです。
https://doc.rust-lang.org/std/sync/struct.Arc.html#method.try_unwrap
ここまでで axum::Router::into_inner は名前通り Router<S> を RouterInner<S> にするものだと分かりました。
RouterInner::path_router field の route method の実装の確認
Router::route の実装のうち、次に調べるべきものは RouterInner::path_router field の route method です。
RouterInner<S> の path_router field
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/mod.rs#L80-L85
struct RouterInner<S> {
path_router: PathRouter<S, false>,
fallback_router: PathRouter<S, true>,
default_fallback: bool,
catch_all_fallback: Fallback<S>,
}
path_router field の型は PathRouter<S, false> でした。
PathRouter の定義を追うと……。
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/mod.rs#L3
use self::{future::RouteFuture, not_found::NotFound, path_router::PathRouter};
PathRouter
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/path_router.rs#L16-L21
pub(super) struct PathRouter<S, const IS_FALLBACK: bool> {
routes: HashMap<RouteId, Endpoint<S>>,
node: Arc<Node>,
prev_route_id: RouteId,
v7_checks: bool,
}
この PathRouter の route method がたどりたかったところです。
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/path_router.rs#L83-L114
pub(super) fn route(
&mut self,
path: &str,
method_router: MethodRouter<S>,
) -> Result<(), Cow<'static, str>> {
validate_path(self.v7_checks, path)?;
let endpoint = if let Some((route_id, Endpoint::MethodRouter(prev_method_router))) = self
.node
.path_to_route_id
.get(path)
.and_then(|route_id| self.routes.get(route_id).map(|svc| (*route_id, svc)))
{
// if we're adding a new `MethodRouter` to a route that already has one just
// merge them. This makes `.route("/", get(_)).route("/", post(_))` work
let service = Endpoint::MethodRouter(
prev_method_router
.clone()
.merge_for_path(Some(path), method_router)?,
);
self.routes.insert(route_id, service);
return Ok(());
} else {
Endpoint::MethodRouter(method_router)
};
let id = self.next_route_id();
self.set_node(path, id)?;
self.routes.insert(id, endpoint);
Ok(())
}
ここで把握できる、おおまかな処理の流れは次のとおりです。
-
pathの妥当性を検証します -
self.node.path_to_route_idからpathでroute_idとEndpointを得ます- 得られたら重複パスなので既存パスの
Endpointにマージして抜けます - 得られなかったら新規パスの
Endpointにします
- 得られたら重複パスなので既存パスの
-
self.next_route_idで次のRouteIdを得ます -
self.set_nodeでpathとRouteIdを node に set します -
self.routesにRouteIdをキーとしてEndpointを挿入します
validate_path function
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/path_router.rs#L39-L51
fn validate_path(v7_checks: bool, path: &str) -> Result<(), &'static str> {
if path.is_empty() {
return Err("Paths must start with a `/`. Use \"/\" for root routes");
} else if !path.starts_with('/') {
return Err("Paths must start with a `/`");
}
if v7_checks {
validate_v07_paths(path)?;
}
Ok(())
}
互換性のためのチェックである v7_checks は skip して考えると、 path が空のときや、 / からはじまっていないときはエラーにします。
PathRouter::next_route_id の実装の確認
PathRouter::next_route_id method
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/path_router.rs#L434-L443
fn next_route_id(&mut self) -> RouteId {
let next_id = self
.prev_route_id
.0
.checked_add(1)
.expect("Over `u32::MAX` routes created. If you need this, please file an issue.");
self.prev_route_id = RouteId(next_id);
self.prev_route_id
}
pub(super) struct PathRouter<S, const IS_FALLBACK: bool> {
// ...
prev_route_id: RouteId,
// ...
}
PathRouter::prev_route_id は先ほど確認したとおり RouteId です。 RouteId の詳細はここでは追いませんが、 u32 を wrap したものなのが読み取れます。
RouteId をインクリメントして self.prev_route_id フィールドを更新しているのが分かります。
PathRouter::set_node の実装の確認
PathRouter::set_node method
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/path_router.rs#L155-L160
fn set_node(&mut self, path: &str, id: RouteId) -> Result<(), String> {
let node = Arc::make_mut(&mut self.node);
node.insert(path, id)
.map_err(|err| format!("Invalid route {path:?}: {err}"))
}
Node struct と Node::insert method
https://github.com/tokio-rs/axum/blob/axum-v0.8.7/axum/src/routing/path_router.rs#L478-L499
struct Node {
inner: matchit::Router<RouteId>,
route_id_to_path: HashMap<RouteId, Arc<str>>,
path_to_route_id: HashMap<Arc<str>, RouteId>,
}
impl Node {
fn insert(
&mut self,
path: impl Into<String>,
val: RouteId,
) -> Result<(), matchit::InsertError> {
let path = path.into();
self.inner.insert(&path, val)?;
let shared_path: Arc<str> = path.into();
self.route_id_to_path.insert(val, shared_path.clone());
self.path_to_route_id.insert(shared_path, val);
Ok(())
}
Node はパスと RouteId の対応を管理するもの。相互に探索できるように保持しています。
insert は適切に探索できるように挿入するようです。
おわりに
まだ、途中ではありますが、 Router の内部のデータの持ち方が見えてきたように思います。
次回は fallback などを調べていきたいと思います。
Discussion