1use std::sync::Arc;
2
3use nest_rs_core::Container;
4use poem::Route;
5
6pub trait Controller: 'static {
10 fn mount(container: &Container, route: Route) -> Route;
11}
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum HttpVerb {
15 Get,
16 Post,
17 Put,
18 Delete,
19 Patch,
20}
21
22impl HttpVerb {
23 pub fn as_str(self) -> &'static str {
24 match self {
25 Self::Get => "GET",
26 Self::Post => "POST",
27 Self::Put => "PUT",
28 Self::Delete => "DELETE",
29 Self::Patch => "PATCH",
30 }
31 }
32}
33
34pub type SchemaFn = fn(&mut schemars::SchemaGenerator) -> schemars::Schema;
39
40pub fn schema_of<T: schemars::JsonSchema>(
43 generator: &mut schemars::SchemaGenerator,
44) -> schemars::Schema {
45 generator.subschema_for::<T>()
46}
47
48#[derive(Clone)]
52pub struct HttpRouteMeta {
53 pub verb: HttpVerb,
54 pub path: &'static str,
55 pub handler: &'static str,
56 pub summary: Option<&'static str>,
57 pub description: Option<&'static str>,
58 pub tags: &'static [&'static str],
61 pub request_body: Option<SchemaFn>,
62 pub response: Option<SchemaFn>,
63}
64
65type MountFn = dyn Fn(&Container, Route) -> Route + Send + Sync;
66
67pub struct HttpControllerMeta {
72 pub path: &'static str,
73 pub version: Option<&'static str>,
74 pub routes: Vec<HttpRouteMeta>,
75 mount: Arc<MountFn>,
76}
77
78impl HttpControllerMeta {
79 pub fn new<F>(
80 path: &'static str,
81 version: Option<&'static str>,
82 routes: Vec<HttpRouteMeta>,
83 mount: F,
84 ) -> Self
85 where
86 F: Fn(&Container, Route) -> Route + Send + Sync + 'static,
87 {
88 Self {
89 path,
90 version,
91 routes,
92 mount: Arc::new(mount),
93 }
94 }
95
96 pub fn effective_prefix(&self) -> String {
100 crate::version_path(self.version, self.path)
101 }
102
103 pub fn mount(&self, container: &Container, route: Route) -> Route {
104 (self.mount)(container, route)
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use std::sync::atomic::{AtomicUsize, Ordering};
112
113 #[test]
114 fn http_verb_as_str_renders_each_method_name() {
115 assert_eq!(HttpVerb::Get.as_str(), "GET");
116 assert_eq!(HttpVerb::Post.as_str(), "POST");
117 assert_eq!(HttpVerb::Put.as_str(), "PUT");
118 assert_eq!(HttpVerb::Delete.as_str(), "DELETE");
119 assert_eq!(HttpVerb::Patch.as_str(), "PATCH");
120 }
121
122 #[test]
123 fn http_verb_is_value_type_for_equality_and_clone() {
124 let a = HttpVerb::Get;
127 let b = a;
128 assert_eq!(a, b);
129 assert_eq!(format!("{a:?}"), "Get");
130 }
131
132 #[test]
133 fn schema_of_records_a_subschema_for_the_payload_type() {
134 let mut generator = schemars::SchemaGenerator::default();
135 let schema = schema_of::<String>(&mut generator);
136 let value: serde_json::Value = serde_json::to_value(&schema).expect("schema serializes");
138 assert!(value.is_object(), "schema serializes to a JSON object");
139 }
140
141 #[test]
142 fn effective_prefix_returns_path_unchanged_without_a_version() {
143 let meta = HttpControllerMeta::new("/users", None, Vec::new(), |_c, r| r);
144 assert_eq!(meta.effective_prefix(), "/users");
145 }
146
147 #[test]
148 fn effective_prefix_prepends_the_uri_version_when_present() {
149 let meta = HttpControllerMeta::new("/users", Some("1"), Vec::new(), |_c, r| r);
152 assert_eq!(meta.effective_prefix(), "/v1/users");
153 }
154
155 #[test]
156 fn new_stores_the_path_version_and_routes_verbatim() {
157 let routes = vec![HttpRouteMeta {
158 verb: HttpVerb::Get,
159 path: "/:id",
160 handler: "show",
161 summary: Some("Fetch one"),
162 description: None,
163 tags: &["Users"],
164 request_body: None,
165 response: None,
166 }];
167 let meta = HttpControllerMeta::new("/users", Some("2"), routes, |_c, r| r);
168 assert_eq!(meta.path, "/users");
169 assert_eq!(meta.version, Some("2"));
170 assert_eq!(meta.routes.len(), 1);
171 assert_eq!(meta.routes[0].handler, "show");
172 assert_eq!(meta.routes[0].tags, &["Users"]);
173 }
174
175 #[test]
176 fn mount_invokes_the_closure_with_the_container_and_route() {
177 static CALLS: AtomicUsize = AtomicUsize::new(0);
180 let meta = HttpControllerMeta::new("/health", None, Vec::new(), |_c, r| {
181 CALLS.fetch_add(1, Ordering::SeqCst);
182 r
183 });
184 let container = Container::builder().build();
185 let route = Route::new();
186
187 let _routed = meta.mount(&container, route);
188 assert_eq!(CALLS.load(Ordering::SeqCst), 1);
189 let _ = meta.mount(&container, Route::new());
190 assert_eq!(CALLS.load(Ordering::SeqCst), 2);
191 }
192}