Skip to main content

cratestack_axum/envelope_layer/
resolver_rest.rs

1//! [`RestBindingResolver`]: the generated REST router's ops.
2
3use std::str::FromStr;
4
5use cratestack_core::RouteTransportDescriptor;
6use http::Method;
7
8use super::resolver::{BindingResolver, Resolution, ResolvedRoute, RouteRequest};
9use crate::idempotency::mount_prefix;
10
11/// REST: the `RouteTransportDescriptor` matching the method and the matched
12/// template (mount prefix stripped), bound by its declared path.
13///
14/// Path parameters are every matched parameter in axum's order, including
15/// those of a parameterised mount (`nest("/t/{tenant}", ..)`), which come
16/// first: binding only the template's would let a signed request for tenant
17/// `a` be replayed at tenant `b`. A client of such a mount binds those
18/// values too.
19///
20/// The request's canonical query is bound as well (the layer's own input,
21/// not the resolver's): see `cratestack_core::canonical_query` for its
22/// order semantics.
23#[derive(Debug, Clone)]
24pub struct RestBindingResolver {
25    prefix: String,
26    routes: &'static [RouteTransportDescriptor],
27}
28
29impl RestBindingResolver {
30    /// For a router mounted at `prefix` (`""` at the root; `"/api"` for
31    /// `Router::nest("/api", ..)`), over the generated `ROUTE_TRANSPORTS`.
32    pub fn new(prefix: &str, routes: &'static [RouteTransportDescriptor]) -> Self {
33        Self {
34            prefix: mount_prefix::normalize(prefix),
35            routes,
36        }
37    }
38}
39
40impl BindingResolver for RestBindingResolver {
41    fn resolve(&self, request: &RouteRequest<'_>) -> Resolution {
42        let Some(path) = request
43            .matched_path()
44            .and_then(|matched| mount_prefix::strip(matched, &self.prefix))
45        else {
46            return Resolution::Unresolved;
47        };
48        // axum answers `HEAD` with the `GET` route, which is the only one
49        // the descriptors list; without this a `HEAD` would resolve to
50        // nothing and pass unsigned under `Required` (D3 signs it too). The
51        // binding keeps the real method, `HEAD`.
52        let method = match request.method().as_str() {
53            "HEAD" => "GET",
54            method => method,
55        };
56        let mut on_path = self.routes.iter().filter(|route| route.path == path);
57        if let Some(route) = on_path.clone().find(|route| route.method == method) {
58            let params = request
59                .path_params()
60                .iter()
61                .map(|(_, value)| value.clone())
62                .collect();
63            return Resolution::Op(ResolvedRoute::new(route.path, params).with_payload_types(
64                route.capabilities.request_types,
65                route.capabilities.response_types,
66            ));
67        }
68        // A generated path, another method: the router's `405` (S2 no
69        // longer warns about it as a misconfiguration).
70        let allow: Vec<Method> = on_path
71            .by_ref()
72            .filter_map(|route| Method::from_str(route.method).ok())
73            .collect();
74        if allow.is_empty() {
75            Resolution::Unresolved
76        } else {
77            Resolution::MethodNotAllowed(allow)
78        }
79    }
80}