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 はリクエストをハンドラーにルーティングするための構造体です。

RouterS という型パラメーターをとります。これはハンドラーに要求されている (設定されず欠けている) 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*_servicefallback などいろいろあるのですが、今回は前回の例で確認した newroute からはじめようと思います。

前回の例:

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::newRouter のコンストラクタです。

ドキュメントによるとルートを追加しないとすべてのリクエストに 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::routeRouter にルートを追加するものです。

第一引数は 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_inner method
  • ↑の戻り値の path_router field の route method

です。

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

この PathRouterroute 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(())
    }

ここで把握できる、おおまかな処理の流れは次のとおりです。

  1. path の妥当性を検証します
  2. self.node.path_to_route_id から pathroute_idEndpoint を得ます
    • 得られたら重複パスなので既存パスの Endpoint にマージして抜けます
    • 得られなかったら新規パスの Endpoint にします
  3. self.next_route_id で次の RouteId を得ます
  4. self.set_nodepathRouteId を node に set します
  5. self.routesRouteId をキーとして 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 などを調べていきたいと思います。

GitHubで編集を提案
ドクターメイト

Discussion