Skip to main content

armature_core/
traits.rs

1//! Core traits for the Armature framework
2
3use async_trait::async_trait;
4use std::any::TypeId;
5
6// Re-export DI traits from dependency-injector
7pub use dependency_injector::{Injectable, Provider};
8
9/// Trait for HTTP controllers
10#[async_trait]
11pub trait Controller: Send + Sync + 'static {
12    /// Returns the base path for this controller
13    fn base_path(&self) -> &'static str;
14
15    /// Returns the routes registered on this controller
16    fn routes(&self) -> Vec<RouteDefinition>;
17}
18
19/// Trait for modules that organize components
20pub trait Module: Send + Sync + 'static {
21    /// Returns the `TypeId` of the concrete type implementing this trait.
22    ///
23    /// Do not override this: the default implementation is monomorphized
24    /// per concrete `Self` (the same technique `std::any::Any::type_id`
25    /// uses), so its vtable entry always reports the *concrete* module
26    /// type, even when called through a `&dyn Module` trait object.
27    ///
28    /// This exists so callers that only hold a `&dyn Module` (e.g.
29    /// [`crate::Application`]'s module-tree walk) can still deduplicate
30    /// modules by concrete identity. `std::any::type_name_of_val`/
31    /// `TypeId::of` cannot do this from outside the trait: both resolve
32    /// their type parameter from the *static* type of the reference
33    /// (`dyn Module`), not the concrete type behind the vtable, so every
34    /// module would compare equal to every other module.
35    fn module_type_id(&self) -> std::any::TypeId {
36        std::any::TypeId::of::<Self>()
37    }
38
39    /// Returns the type name of the concrete type implementing this trait.
40    ///
41    /// Like [`Module::module_type_id`], this is monomorphized per concrete
42    /// `Self`, so it reports the real module type name (useful for
43    /// diagnostics/logging) even through a `&dyn Module` reference — unlike
44    /// `std::any::type_name_of_val(&dyn Module)`, which always returns the
45    /// trait object's own type name.
46    fn module_type_name(&self) -> &'static str {
47        std::any::type_name::<Self>()
48    }
49
50    /// Returns the list of provider types to register
51    fn providers(&self) -> Vec<ProviderRegistration>;
52
53    /// Returns the list of controller types to register
54    fn controllers(&self) -> Vec<ControllerRegistration>;
55
56    /// Returns the list of guard types to register
57    fn guards(&self) -> Vec<crate::module::GuardRegistration> {
58        vec![]
59    }
60
61    /// Returns the list of imported modules
62    fn imports(&self) -> Vec<Box<dyn Module>>;
63
64    /// Returns the list of exported provider types
65    fn exports(&self) -> Vec<TypeId>;
66
67    /// Returns the list of re-exported modules
68    ///
69    /// Re-exported modules have their exports forwarded to any module
70    /// that imports this module.
71    fn re_exports(&self) -> Vec<Box<dyn Module>> {
72        vec![]
73    }
74}
75
76/// Trait for request handlers (route methods)
77#[async_trait]
78pub trait RequestHandler: Send + Sync {
79    /// Handle an HTTP request and return a response
80    async fn handle(
81        &self,
82        request: crate::HttpRequest,
83    ) -> Result<crate::HttpResponse, crate::Error>;
84}
85
86/// Trait for validators
87pub trait Validator: Send + Sync {
88    /// Validate a value
89    fn validate(&self, value: &str) -> Result<(), String>;
90}
91
92/// Definition of a route
93#[derive(Clone, Debug)]
94pub struct RouteDefinition {
95    pub method: HttpMethod,
96    pub path: String,
97    pub handler_name: String,
98}
99
100/// HTTP methods
101#[derive(Clone, Debug, PartialEq, Eq)]
102#[non_exhaustive]
103pub enum HttpMethod {
104    GET,
105    POST,
106    PUT,
107    DELETE,
108    PATCH,
109    HEAD,
110    OPTIONS,
111    /// Safe, idempotent query with a request body
112    /// (draft-ietf-httpbis-safe-method-w-body).
113    QUERY,
114}
115
116impl HttpMethod {
117    #[allow(clippy::should_implement_trait)]
118    pub fn from_str(s: &str) -> Option<Self> {
119        match s.to_uppercase().as_str() {
120            "GET" => Some(HttpMethod::GET),
121            "POST" => Some(HttpMethod::POST),
122            "PUT" => Some(HttpMethod::PUT),
123            "DELETE" => Some(HttpMethod::DELETE),
124            "PATCH" => Some(HttpMethod::PATCH),
125            "HEAD" => Some(HttpMethod::HEAD),
126            "OPTIONS" => Some(HttpMethod::OPTIONS),
127            "QUERY" => Some(HttpMethod::QUERY),
128            _ => None,
129        }
130    }
131
132    pub fn as_str(&self) -> &'static str {
133        match self {
134            HttpMethod::GET => "GET",
135            HttpMethod::POST => "POST",
136            HttpMethod::PUT => "PUT",
137            HttpMethod::DELETE => "DELETE",
138            HttpMethod::PATCH => "PATCH",
139            HttpMethod::HEAD => "HEAD",
140            HttpMethod::OPTIONS => "OPTIONS",
141            HttpMethod::QUERY => "QUERY",
142        }
143    }
144}
145
146/// Registration information for a provider
147#[derive(Clone)]
148pub struct ProviderRegistration {
149    pub type_id: TypeId,
150    pub type_name: &'static str,
151    /// Function that registers the provider in the container.
152    /// Uses the Container wrapper type from armature-core.
153    pub register_fn: fn(&crate::container::Container),
154}
155
156impl std::fmt::Debug for ProviderRegistration {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        f.debug_struct("ProviderRegistration")
159            .field("type_id", &self.type_id)
160            .field("type_name", &self.type_name)
161            .finish()
162    }
163}
164
165/// Registration information for a controller
166#[derive(Clone)]
167pub struct ControllerRegistration {
168    pub type_id: TypeId,
169    pub type_name: &'static str,
170    pub base_path: &'static str,
171    pub factory:
172        fn(&crate::Container) -> Result<Box<dyn std::any::Any + Send + Sync>, crate::Error>,
173    #[allow(clippy::type_complexity)]
174    pub route_registrar: fn(
175        &crate::Container,
176        &mut crate::Router,
177        Box<dyn std::any::Any + Send + Sync>,
178    ) -> Result<(), crate::Error>,
179}
180
181impl std::fmt::Debug for ControllerRegistration {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        f.debug_struct("ControllerRegistration")
184            .field("type_id", &self.type_id)
185            .field("type_name", &self.type_name)
186            .field("base_path", &self.base_path)
187            .finish()
188    }
189}
190
191impl From<HttpMethod> for crate::Method {
192    #[inline]
193    fn from(m: HttpMethod) -> Self {
194        match m {
195            HttpMethod::GET => crate::Method::Get,
196            HttpMethod::POST => crate::Method::Post,
197            HttpMethod::PUT => crate::Method::Put,
198            HttpMethod::DELETE => crate::Method::Delete,
199            HttpMethod::PATCH => crate::Method::Patch,
200            HttpMethod::HEAD => crate::Method::Head,
201            HttpMethod::OPTIONS => crate::Method::Options,
202            HttpMethod::QUERY => crate::Method::Query,
203            // Deliberately exhaustive with no catch-all. `HttpMethod` is
204            // #[non_exhaustive] for downstream crates, but this match is inside
205            // the defining crate, so a variant added later fails to compile here
206            // rather than silently mapping onto something wrong.
207        }
208    }
209}
210
211/// The method has no `HttpMethod` counterpart, so it is not routable.
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct UnroutableMethod(pub String);
214
215impl std::fmt::Display for UnroutableMethod {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        write!(f, "method `{}` has no HttpMethod counterpart", self.0)
218    }
219}
220
221impl std::error::Error for UnroutableMethod {}
222
223impl TryFrom<&crate::Method> for HttpMethod {
224    type Error = UnroutableMethod;
225
226    #[inline]
227    fn try_from(m: &crate::Method) -> Result<Self, Self::Error> {
228        match m {
229            crate::Method::Get => Ok(HttpMethod::GET),
230            crate::Method::Post => Ok(HttpMethod::POST),
231            crate::Method::Put => Ok(HttpMethod::PUT),
232            crate::Method::Delete => Ok(HttpMethod::DELETE),
233            crate::Method::Patch => Ok(HttpMethod::PATCH),
234            crate::Method::Head => Ok(HttpMethod::HEAD),
235            crate::Method::Options => Ok(HttpMethod::OPTIONS),
236            crate::Method::Query => Ok(HttpMethod::QUERY),
237            crate::Method::Connect => Err(UnroutableMethod("CONNECT".into())),
238            crate::Method::Trace => Err(UnroutableMethod("TRACE".into())),
239            crate::Method::Other(t) => Err(UnroutableMethod(t.into_owned())),
240        }
241    }
242}
243
244#[cfg(test)]
245mod method_conversion_tests {
246    use super::HttpMethod;
247    use crate::Method;
248
249    #[test]
250    fn every_http_method_round_trips_through_method() {
251        for m in [
252            HttpMethod::GET,
253            HttpMethod::POST,
254            HttpMethod::PUT,
255            HttpMethod::DELETE,
256            HttpMethod::PATCH,
257            HttpMethod::HEAD,
258            HttpMethod::OPTIONS,
259            HttpMethod::QUERY,
260        ] {
261            let converted = Method::from(m.clone());
262            assert_eq!(
263                HttpMethod::try_from(&converted).ok(),
264                Some(m.clone()),
265                "{m:?} did not round-trip"
266            );
267        }
268    }
269
270    #[test]
271    fn methods_with_no_http_method_counterpart_fail_conversion() {
272        // CONNECT, TRACE, and Other exist in armature-h1 because the wire has
273        // them; HttpMethod is the *routable* set, which is deliberately smaller.
274        // A router that silently mapped CONNECT onto GET would be a security bug.
275        assert!(HttpMethod::try_from(&Method::Connect).is_err());
276        assert!(HttpMethod::try_from(&Method::Trace).is_err());
277        assert!(HttpMethod::try_from(&Method::from("PURGE")).is_err());
278    }
279}