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