Skip to main content

tako_rs_core/
router_state.rs

1//! Per-router typed state container.
2//!
3//! Each [`crate::router::Router`] owns one [`RouterState`](crate::router_state::RouterState) (an `Arc<…>`
4//! internally). Values inserted via [`crate::router::Router::with_state`] live
5//! on the router instance — multiple `Router`s in the same process can hold
6//! distinct state values for the same `T`, which the historical process-wide
7//! [`crate::state::set_state`] cannot do.
8//!
9//! The `State` extractor (from `tako-extractors`) reads from the request-scoped
10//! `Arc<RouterState>` first (inserted by [`crate::router::Router::dispatch`])
11//! and falls back to [`crate::state::get_state`] if the per-router slot is
12//! empty. Existing code that uses the global store keeps working unchanged.
13
14use std::any::Any;
15use std::any::TypeId;
16use std::sync::Arc;
17
18use scc::HashMap as SccHashMap;
19
20/// Type-keyed bag of values, lock-free for both reads and writes.
21#[derive(Default)]
22pub struct RouterState {
23  inner: SccHashMap<TypeId, Arc<dyn Any + Send + Sync>>,
24}
25
26impl std::fmt::Debug for RouterState {
27  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28    f.debug_struct("RouterState").finish_non_exhaustive()
29  }
30}
31
32impl RouterState {
33  /// Construct an empty state container.
34  #[must_use]
35  pub fn new() -> Self {
36    Self::default()
37  }
38
39  /// Insert (or replace) the value associated with `T`.
40  pub fn insert<T: Send + Sync + 'static>(&self, value: T) {
41    let _ = self.inner.insert_sync(TypeId::of::<T>(), Arc::new(value));
42  }
43
44  /// Retrieve the value associated with `T`, if any.
45  pub fn get<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
46    self
47      .inner
48      .get_sync(&TypeId::of::<T>())
49      .map(|v| v.clone())
50      .and_then(|v| v.downcast::<T>().ok())
51  }
52
53  /// `true` when at least one value has been inserted.
54  pub fn is_empty(&self) -> bool {
55    self.inner.is_empty()
56  }
57
58  /// Number of distinct types currently stored.
59  pub fn len(&self) -> usize {
60    self.inner.len()
61  }
62}
63
64/// Routing-time path template attached to the request.
65///
66/// `Router::dispatch` inserts a `MatchedPath` into request extensions before
67/// running middleware and the handler so that metrics, logs, and extractors
68/// can label by the route template (e.g. `/users/{id}`) rather than the
69/// concrete URI (`/users/42`).
70///
71/// This is also the public extractor type — `tako-extractors` re-exports it
72/// from this module so the extension key and the user-facing extractor share
73/// one canonical type. Previously the extractor was a separate newtype with
74/// the same name, which made it easy to insert the wrong type into request
75/// extensions and have lookups silently miss.
76#[derive(Debug, Clone)]
77pub struct MatchedPath(pub String);
78
79impl MatchedPath {
80  /// Borrow the matched path template.
81  #[inline]
82  pub fn as_str(&self) -> &str {
83    &self.0
84  }
85}
86
87impl<'a> crate::extractors::FromRequest<'a> for MatchedPath {
88  type Error = MatchedPathMissing;
89
90  fn from_request(
91    req: &'a mut crate::types::Request,
92  ) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
93    futures_util::future::ready(
94      req
95        .extensions()
96        .get::<MatchedPath>()
97        .cloned()
98        .ok_or(MatchedPathMissing),
99    )
100  }
101}
102
103impl<'a> crate::extractors::FromRequestParts<'a> for MatchedPath {
104  type Error = MatchedPathMissing;
105
106  fn from_request_parts(
107    parts: &'a mut http::request::Parts,
108  ) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
109    futures_util::future::ready(
110      parts
111        .extensions
112        .get::<MatchedPath>()
113        .cloned()
114        .ok_or(MatchedPathMissing),
115    )
116  }
117}
118
119/// Rejection when no [`MatchedPath`] extension is on the request.
120#[derive(Debug)]
121pub struct MatchedPathMissing;
122
123impl crate::responder::Responder for MatchedPathMissing {
124  fn into_response(self) -> crate::types::Response {
125    let mut res = crate::types::Response::new(crate::body::TakoBody::from(
126      "matched path is unavailable on this request",
127    ));
128    *res.status_mut() = http::StatusCode::INTERNAL_SERVER_ERROR;
129    res
130  }
131}