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}