1use std::borrow::Cow;
2
3use nidus_http::router::RouteMetadata;
4use nidus_http::{StatusCode, error::RoutePathError};
5use serde_json::{Value, json};
6use utoipa::ToSchema;
7
8use crate::path::{openapi_path, openapi_path_parameters, operation_id};
9
10#[derive(Clone, Debug)]
12pub struct OpenApiRoute {
13 method: Cow<'static, str>,
14 path: String,
15 path_parameters: Vec<String>,
16 summary: Option<String>,
17 tags: Vec<String>,
18 response_status: StatusCode,
19 request_schema: Option<String>,
20 response_schema: Option<String>,
21 guards: Vec<String>,
22 pipes: Vec<String>,
23 validates: bool,
24}
25
26impl OpenApiRoute {
27 pub fn get(path: impl Into<String>) -> Self {
29 Self::try_get(path).unwrap_or_else(|error| panic!("{error}"))
30 }
31
32 pub fn try_get(path: impl Into<String>) -> Result<Self, RoutePathError> {
34 Self::try_new("get", path)
35 }
36
37 pub fn post(path: impl Into<String>) -> Self {
39 Self::try_post(path).unwrap_or_else(|error| panic!("{error}"))
40 }
41
42 pub fn try_post(path: impl Into<String>) -> Result<Self, RoutePathError> {
44 Self::try_new("post", path)
45 }
46
47 pub fn put(path: impl Into<String>) -> Self {
49 Self::try_put(path).unwrap_or_else(|error| panic!("{error}"))
50 }
51
52 pub fn try_put(path: impl Into<String>) -> Result<Self, RoutePathError> {
54 Self::try_new("put", path)
55 }
56
57 pub fn patch(path: impl Into<String>) -> Self {
59 Self::try_patch(path).unwrap_or_else(|error| panic!("{error}"))
60 }
61
62 pub fn try_patch(path: impl Into<String>) -> Result<Self, RoutePathError> {
64 Self::try_new("patch", path)
65 }
66
67 pub fn delete(path: impl Into<String>) -> Self {
69 Self::try_delete(path).unwrap_or_else(|error| panic!("{error}"))
70 }
71
72 pub fn try_delete(path: impl Into<String>) -> Result<Self, RoutePathError> {
74 Self::try_new("delete", path)
75 }
76
77 pub fn summary(mut self, summary: impl Into<String>) -> Self {
79 self.summary = Some(summary.into());
80 self
81 }
82
83 pub fn tag(mut self, tag: impl Into<String>) -> Self {
85 self.tags.push(tag.into());
86 self
87 }
88
89 pub fn response_status(mut self, status: StatusCode) -> Self {
91 self.response_status = status;
92 self
93 }
94
95 pub fn request_schema<T>(self) -> Self
97 where
98 T: ToSchema,
99 {
100 self.request_schema_ref(T::name())
101 }
102
103 pub fn response_schema<T>(self) -> Self
105 where
106 T: ToSchema,
107 {
108 self.response_schema_ref(T::name())
109 }
110
111 pub(crate) fn method(&self) -> &str {
112 self.method.as_ref()
113 }
114
115 pub(crate) fn path(&self) -> &str {
116 &self.path
117 }
118
119 pub(crate) fn try_from_route_metadata(
120 metadata: &RouteMetadata,
121 ) -> Result<Self, RoutePathError> {
122 Self::try_from_route_metadata_at_path(metadata, metadata.path())
123 }
124
125 pub(crate) fn try_from_route_metadata_at_path(
126 metadata: &RouteMetadata,
127 path: impl AsRef<str>,
128 ) -> Result<Self, RoutePathError> {
129 let path = openapi_path(path.as_ref())?;
130 let path_parameters = openapi_path_parameters(&path);
131 let mut route = Self::new(openapi_method(metadata.method()), path, path_parameters);
132 if let Some(summary) = metadata.summary() {
133 route = route.summary(summary);
134 }
135 for tag in metadata.tags() {
136 route = route.tag(*tag);
137 }
138 if let Some(status) = metadata.response_status() {
139 route = route.response_status(status);
140 }
141 if let Some(schema) = metadata.request_schema() {
142 route = route.request_schema_ref(schema);
143 }
144 if let Some(schema) = metadata.response_schema() {
145 route = route.response_schema_ref(schema);
146 }
147 route.guards = metadata
148 .guards()
149 .iter()
150 .map(|guard| (*guard).to_owned())
151 .collect();
152 route.pipes = metadata
153 .pipes()
154 .iter()
155 .map(|pipe| (*pipe).to_owned())
156 .collect();
157 route.validates = metadata.validates();
158 Ok(route)
159 }
160
161 pub(crate) fn to_json_value(&self) -> Value {
162 let mut success_response = json!({
163 "description": "Success"
164 });
165 if let Some(schema) = &self.response_schema {
166 success_response["content"] = json!({
167 "application/json": {
168 "schema": {
169 "$ref": format!("#/components/schemas/{schema}")
170 }
171 }
172 });
173 }
174
175 let mut responses = serde_json::Map::new();
176 responses.insert(self.response_status.as_u16().to_string(), success_response);
177
178 if !self.guards.is_empty() {
181 responses.insert("401".to_owned(), json!({ "description": "Unauthorized" }));
182 responses.insert("403".to_owned(), json!({ "description": "Forbidden" }));
183 }
184 if self.validates {
185 responses.insert(
186 "422".to_owned(),
187 json!({ "description": "Validation failed" }),
188 );
189 }
190
191 let mut operation = json!({
192 "operationId": operation_id(&self.method, &self.path),
193 "responses": responses
194 });
195
196 if let Some(summary) = &self.summary {
197 operation["summary"] = json!(summary);
198 }
199 if !self.tags.is_empty() {
200 operation["tags"] = json!(self.tags);
201 }
202 if let Some(schema) = &self.request_schema {
203 operation["requestBody"] = json!({
204 "required": true,
205 "content": {
206 "application/json": {
207 "schema": {
208 "$ref": format!("#/components/schemas/{schema}")
209 }
210 }
211 }
212 });
213 }
214 if !self.path_parameters.is_empty() {
215 operation["parameters"] = json!(
216 self.path_parameters
217 .iter()
218 .map(|name| {
219 json!({
220 "name": name,
221 "in": "path",
222 "required": true,
223 "schema": {
224 "type": "string"
225 }
226 })
227 })
228 .collect::<Vec<_>>()
229 );
230 }
231 if !self.guards.is_empty() {
232 operation["x-nidus-guards"] = json!(self.guards);
233 }
234 if !self.pipes.is_empty() {
235 operation["x-nidus-pipes"] = json!(self.pipes);
236 }
237 if self.validates {
238 operation["x-nidus-validates"] = json!(true);
239 }
240
241 operation
242 }
243
244 fn request_schema_ref(mut self, schema: impl Into<String>) -> Self {
245 self.request_schema = Some(schema.into());
246 self
247 }
248
249 fn response_schema_ref(mut self, schema: impl Into<String>) -> Self {
250 self.response_schema = Some(schema.into());
251 self
252 }
253
254 fn new(
255 method: impl Into<Cow<'static, str>>,
256 path: impl Into<String>,
257 path_parameters: Vec<String>,
258 ) -> Self {
259 Self {
260 method: method.into(),
261 path: path.into(),
262 path_parameters,
263 summary: None,
264 tags: Vec::new(),
265 response_status: StatusCode::OK,
266 request_schema: None,
267 response_schema: None,
268 guards: Vec::new(),
269 pipes: Vec::new(),
270 validates: false,
271 }
272 }
273
274 fn try_new(
275 method: impl Into<Cow<'static, str>>,
276 path: impl Into<String>,
277 ) -> Result<Self, RoutePathError> {
278 let path = path.into();
279 let path = openapi_path(&path)?;
280 let path_parameters = openapi_path_parameters(&path);
281 Ok(Self::new(method, path, path_parameters))
282 }
283}
284
285fn openapi_method(method: &'static str) -> Cow<'static, str> {
286 match method {
287 "GET" | "get" => Cow::Borrowed("get"),
288 "POST" | "post" => Cow::Borrowed("post"),
289 "PUT" | "put" => Cow::Borrowed("put"),
290 "PATCH" | "patch" => Cow::Borrowed("patch"),
291 "DELETE" | "delete" => Cow::Borrowed("delete"),
292 method => Cow::Owned(method.to_ascii_lowercase()),
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn fixed_route_methods_are_borrowed() {
302 let routes = [
303 (OpenApiRoute::get("/"), "get"),
304 (OpenApiRoute::post("/"), "post"),
305 (OpenApiRoute::put("/"), "put"),
306 (OpenApiRoute::patch("/"), "patch"),
307 (OpenApiRoute::delete("/"), "delete"),
308 ];
309
310 for (route, expected) in routes {
311 assert_eq!(route.method(), expected);
312 assert!(matches!(route.method, Cow::Borrowed(_)));
313 }
314 }
315
316 #[test]
317 fn route_metadata_borrows_supported_methods_and_owns_fallbacks() {
318 let supported =
319 OpenApiRoute::try_from_route_metadata(&RouteMetadata::new("GET", "/")).unwrap();
320 let fallback =
321 OpenApiRoute::try_from_route_metadata(&RouteMetadata::new("OPTIONS", "/")).unwrap();
322
323 assert!(matches!(supported.method, Cow::Borrowed("get")));
324 assert!(matches!(fallback.method, Cow::Owned(ref method) if method == "options"));
325 }
326}