Skip to main content

sova_core/
route_value.rs

1//! Typed route/router/app attributes: [`RouteValue`] + [`MetaMap`].
2
3use crate::state::StateMap;
4use http::Method;
5use rustc_hash::FxHashMap;
6use std::any::{Any, TypeId};
7use std::borrow::Cow;
8use std::collections::HashSet;
9use std::sync::Arc;
10
11/// Value attached via [`crate::Router::with`] (route, router, or app scope).
12pub trait RouteValue: Any + Send + Sync + 'static {
13    /// Startup validation; default is a no-op.
14    fn check(&self, _ctx: &BuildCtx<'_>) -> Result<(), String> {
15        Ok(())
16    }
17
18    /// Label for `explain` / CLI `check`.
19    fn label(&self) -> Cow<'static, str> {
20        Cow::Borrowed(std::any::type_name::<Self>())
21    }
22}
23
24/// Context passed to [`RouteValue::check`] during [`crate::App::build`].
25pub struct BuildCtx<'a> {
26    pub state: &'a StateMap,
27    pub installed_plugins: &'a HashSet<&'static str>,
28    pub route_path: &'a str,
29    pub route_method: Option<&'a Method>,
30}
31
32type CheckFn = Arc<dyn Fn(&BuildCtx<'_>) -> Result<(), String> + Send + Sync>;
33
34/// Typed metadata bag for routes/routers (one value per `TypeId`; last insert wins).
35#[derive(Default, Clone)]
36pub struct MetaMap {
37    map: FxHashMap<TypeId, Arc<dyn Any + Send + Sync>>,
38    checkers: FxHashMap<TypeId, CheckFn>,
39    labels: FxHashMap<TypeId, String>,
40}
41
42impl MetaMap {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    pub fn insert<T: RouteValue>(&mut self, value: T) {
48        let id = TypeId::of::<T>();
49        let label = value.label().into_owned();
50        let arc = Arc::new(value);
51        let for_check = Arc::clone(&arc);
52        self.map.insert(id, arc);
53        self.checkers.insert(
54            id,
55            Arc::new(move |ctx| RouteValue::check(for_check.as_ref(), ctx)),
56        );
57        self.labels.insert(id, label);
58    }
59
60    pub fn get<T: RouteValue>(&self) -> Option<Arc<T>> {
61        self.map
62            .get(&TypeId::of::<T>())
63            .and_then(|v| v.clone().downcast::<T>().ok())
64    }
65
66    /// Merge another map; later values overwrite the same `TypeId`.
67    pub fn extend(&mut self, other: MetaMap) {
68        for (id, v) in other.map {
69            self.map.insert(id, v);
70        }
71        for (id, c) in other.checkers {
72            self.checkers.insert(id, c);
73        }
74        for (id, l) in other.labels {
75            self.labels.insert(id, l);
76        }
77    }
78
79    pub fn is_empty(&self) -> bool {
80        self.map.is_empty()
81    }
82
83    pub fn check_all(&self, ctx: &BuildCtx<'_>) -> Result<(), String> {
84        for check in self.checkers.values() {
85            check(ctx)?;
86        }
87        Ok(())
88    }
89
90    /// Labels for introspection (stable order by label string).
91    pub fn labels(&self) -> Vec<&str> {
92        let mut v: Vec<_> = self.labels.values().map(|s| s.as_str()).collect();
93        v.sort_unstable();
94        v
95    }
96}
97
98/// Declares that application state must contain `T` before serving.
99pub struct Needs<T>(std::marker::PhantomData<fn() -> T>);
100
101impl<T: Send + Sync + 'static> Needs<T> {
102    pub fn new() -> Self {
103        Self(std::marker::PhantomData)
104    }
105}
106
107impl<T: Send + Sync + 'static> Default for Needs<T> {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113impl<T: Send + Sync + 'static> RouteValue for Needs<T> {
114    fn check(&self, ctx: &BuildCtx<'_>) -> Result<(), String> {
115        if ctx.state.get::<T>().is_some() {
116            Ok(())
117        } else {
118            Err(format!(
119                "route {} needs state `{}` (missing in App::state)",
120                ctx.route_path,
121                std::any::type_name::<T>()
122            ))
123        }
124    }
125
126    fn label(&self) -> Cow<'static, str> {
127        Cow::Owned(format!("Needs<{}>", std::any::type_name::<T>()))
128    }
129}