Skip to main content

vespera_core/
route.rs

1//! Route-related structure definitions
2
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashMap};
5
6use crate::SchemaRef;
7
8/// HTTP method
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "UPPERCASE")]
11pub enum HttpMethod {
12    Get,
13    Post,
14    Put,
15    Patch,
16    Delete,
17    Head,
18    Options,
19    Trace,
20}
21
22impl std::fmt::Display for HttpMethod {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            Self::Get => write!(f, "GET"),
26            Self::Post => write!(f, "POST"),
27            Self::Put => write!(f, "PUT"),
28            Self::Patch => write!(f, "PATCH"),
29            Self::Delete => write!(f, "DELETE"),
30            Self::Head => write!(f, "HEAD"),
31            Self::Options => write!(f, "OPTIONS"),
32            Self::Trace => write!(f, "TRACE"),
33        }
34    }
35}
36
37impl TryFrom<&str> for HttpMethod {
38    type Error = String;
39
40    fn try_from(value: &str) -> Result<Self, Self::Error> {
41        match value.to_uppercase().as_str() {
42            "GET" => Ok(Self::Get),
43            "POST" => Ok(Self::Post),
44            "PUT" => Ok(Self::Put),
45            "PATCH" => Ok(Self::Patch),
46            "DELETE" => Ok(Self::Delete),
47            "HEAD" => Ok(Self::Head),
48            "OPTIONS" => Ok(Self::Options),
49            "TRACE" => Ok(Self::Trace),
50            other => Err(format!("unknown HTTP method: {other}")),
51        }
52    }
53}
54
55/// Parameter location in the request
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum ParameterLocation {
59    Query,
60    Header,
61    Path,
62    Cookie,
63}
64
65/// Parameter definition
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct Parameter {
69    /// Parameter name
70    pub name: String,
71    /// Parameter location
72    pub r#in: ParameterLocation,
73    /// Parameter description
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub description: Option<String>,
76    /// Whether the parameter is required
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub required: Option<bool>,
79    /// Schema reference or inline schema
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub schema: Option<SchemaRef>,
82    /// Example value
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub example: Option<serde_json::Value>,
85}
86
87/// Request body definition
88#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct RequestBody {
91    /// Request body description
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub description: Option<String>,
94    /// Whether the request body is required
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub required: Option<bool>,
97    /// Schema per Content-Type
98    pub content: BTreeMap<String, MediaType>,
99}
100
101/// Media type definition
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct MediaType {
105    /// Schema reference or inline schema
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub schema: Option<SchemaRef>,
108    /// Example
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub example: Option<serde_json::Value>,
111    /// Examples
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub examples: Option<HashMap<String, Example>>,
114}
115
116/// Example definition
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct Example {
120    /// Example summary
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub summary: Option<String>,
123    /// Example description
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub description: Option<String>,
126    /// Example value
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub value: Option<serde_json::Value>,
129}
130
131/// Response definition
132#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct Response {
135    /// Response description
136    pub description: String,
137    /// Header definitions
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub headers: Option<HashMap<String, Header>>,
140    /// Schema per Content-Type
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub content: Option<BTreeMap<String, MediaType>>,
143}
144
145/// Header definition
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct Header {
149    /// Header description
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub description: Option<String>,
152    /// Schema reference or inline schema
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub schema: Option<SchemaRef>,
155}
156
157/// `OpenAPI` Operation definition
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct Operation {
161    /// Operation ID (unique identifier)
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub operation_id: Option<String>,
164    /// List of tags
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub tags: Option<Vec<String>>,
167    /// Summary
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub summary: Option<String>,
170    /// Description
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub description: Option<String>,
173    /// List of parameters
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub parameters: Option<Vec<Parameter>>,
176    /// Request body
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub request_body: Option<RequestBody>,
179    /// Response definitions (status code -> Response)
180    pub responses: BTreeMap<String, Response>,
181    /// Security requirements
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub security: Option<Vec<HashMap<String, Vec<String>>>>,
184}
185
186/// Path Item definition (all HTTP methods for a specific path)
187#[derive(Debug, Clone, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct PathItem {
190    /// GET method
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub get: Option<Operation>,
193    /// POST method
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub post: Option<Operation>,
196    /// PUT method
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub put: Option<Operation>,
199    /// PATCH method
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub patch: Option<Operation>,
202    /// DELETE method
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub delete: Option<Operation>,
205    /// HEAD method
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub head: Option<Operation>,
208    /// OPTIONS method
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub options: Option<Operation>,
211    /// TRACE method
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub trace: Option<Operation>,
214    /// Path parameters
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub parameters: Option<Vec<Parameter>>,
217    /// Summary
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub summary: Option<String>,
220    /// Description
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub description: Option<String>,
223}
224
225impl PathItem {
226    /// Set an operation for a specific HTTP method
227    pub fn set_operation(&mut self, method: HttpMethod, operation: Operation) {
228        match method {
229            HttpMethod::Get => self.get = Some(operation),
230            HttpMethod::Post => self.post = Some(operation),
231            HttpMethod::Put => self.put = Some(operation),
232            HttpMethod::Patch => self.patch = Some(operation),
233            HttpMethod::Delete => self.delete = Some(operation),
234            HttpMethod::Head => self.head = Some(operation),
235            HttpMethod::Options => self.options = Some(operation),
236            HttpMethod::Trace => self.trace = Some(operation),
237        }
238    }
239
240    /// Get an operation for a specific HTTP method
241    #[must_use]
242    pub const fn get_operation(&self, method: &HttpMethod) -> Option<&Operation> {
243        match method {
244            HttpMethod::Get => self.get.as_ref(),
245            HttpMethod::Post => self.post.as_ref(),
246            HttpMethod::Put => self.put.as_ref(),
247            HttpMethod::Patch => self.patch.as_ref(),
248            HttpMethod::Delete => self.delete.as_ref(),
249            HttpMethod::Head => self.head.as_ref(),
250            HttpMethod::Options => self.options.as_ref(),
251            HttpMethod::Trace => self.trace.as_ref(),
252        }
253    }
254}
255
256/// Route information (for internal use)
257#[derive(Debug, Clone)]
258pub struct RouteInfo {
259    /// HTTP method
260    pub method: HttpMethod,
261    /// Path
262    pub path: String,
263    /// Operation information
264    pub operation: Operation,
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use rstest::rstest;
271
272    #[rstest]
273    #[case("GET", HttpMethod::Get)]
274    #[case("get", HttpMethod::Get)]
275    #[case("Get", HttpMethod::Get)]
276    #[case("POST", HttpMethod::Post)]
277    #[case("post", HttpMethod::Post)]
278    #[case("Post", HttpMethod::Post)]
279    #[case("PUT", HttpMethod::Put)]
280    #[case("put", HttpMethod::Put)]
281    #[case("Put", HttpMethod::Put)]
282    #[case("PATCH", HttpMethod::Patch)]
283    #[case("patch", HttpMethod::Patch)]
284    #[case("Patch", HttpMethod::Patch)]
285    #[case("DELETE", HttpMethod::Delete)]
286    #[case("delete", HttpMethod::Delete)]
287    #[case("Delete", HttpMethod::Delete)]
288    #[case("HEAD", HttpMethod::Head)]
289    #[case("head", HttpMethod::Head)]
290    #[case("Head", HttpMethod::Head)]
291    #[case("OPTIONS", HttpMethod::Options)]
292    #[case("options", HttpMethod::Options)]
293    #[case("Options", HttpMethod::Options)]
294    #[case("TRACE", HttpMethod::Trace)]
295    #[case("trace", HttpMethod::Trace)]
296    #[case("Trace", HttpMethod::Trace)]
297    fn test_http_method_from_str(#[case] input: &str, #[case] expected: HttpMethod) {
298        let result = HttpMethod::try_from(input).unwrap();
299        assert_eq!(result, expected);
300    }
301
302    #[test]
303    fn test_http_method_from_invalid_str() {
304        let result = HttpMethod::try_from("INVALID");
305        assert!(result.is_err());
306    }
307
308    #[test]
309    fn test_http_method_serialization() {
310        // Test serde serialization (should be UPPERCASE)
311        let method = HttpMethod::Get;
312        let serialized = serde_json::to_string(&method).unwrap();
313        assert_eq!(serialized, "\"GET\"");
314
315        let method = HttpMethod::Post;
316        let serialized = serde_json::to_string(&method).unwrap();
317        assert_eq!(serialized, "\"POST\"");
318
319        let method = HttpMethod::Delete;
320        let serialized = serde_json::to_string(&method).unwrap();
321        assert_eq!(serialized, "\"DELETE\"");
322    }
323
324    #[test]
325    fn test_http_method_deserialization() {
326        // Test serde deserialization
327        let method: HttpMethod = serde_json::from_str("\"GET\"").unwrap();
328        assert_eq!(method, HttpMethod::Get);
329
330        let method: HttpMethod = serde_json::from_str("\"POST\"").unwrap();
331        assert_eq!(method, HttpMethod::Post);
332
333        let method: HttpMethod = serde_json::from_str("\"DELETE\"").unwrap();
334        assert_eq!(method, HttpMethod::Delete);
335    }
336
337    #[test]
338    fn test_path_item_set_operation() {
339        let mut path_item = PathItem {
340            get: None,
341            post: None,
342            put: None,
343            patch: None,
344            delete: None,
345            head: None,
346            options: None,
347            trace: None,
348            parameters: None,
349            summary: None,
350            description: None,
351        };
352
353        let operation = Operation {
354            operation_id: Some("test_operation".to_string()),
355            tags: None,
356            summary: None,
357            description: None,
358            parameters: None,
359            request_body: None,
360            responses: BTreeMap::new(),
361            security: None,
362        };
363
364        // Test setting GET operation
365        path_item.set_operation(HttpMethod::Get, operation.clone());
366        assert!(path_item.get.is_some());
367        assert_eq!(
368            path_item.get.as_ref().unwrap().operation_id,
369            Some("test_operation".to_string())
370        );
371
372        // Test setting POST operation
373        let mut operation_post = operation.clone();
374        operation_post.operation_id = Some("post_operation".to_string());
375        path_item.set_operation(HttpMethod::Post, operation_post);
376        assert!(path_item.post.is_some());
377        assert_eq!(
378            path_item.post.as_ref().unwrap().operation_id,
379            Some("post_operation".to_string())
380        );
381
382        // Test setting PUT operation
383        let mut operation_put = operation.clone();
384        operation_put.operation_id = Some("put_operation".to_string());
385        path_item.set_operation(HttpMethod::Put, operation_put);
386        assert!(path_item.put.is_some());
387
388        // Test setting PATCH operation
389        let mut operation_patch = operation.clone();
390        operation_patch.operation_id = Some("patch_operation".to_string());
391        path_item.set_operation(HttpMethod::Patch, operation_patch);
392        assert!(path_item.patch.is_some());
393
394        // Test setting DELETE operation
395        let mut operation_delete = operation.clone();
396        operation_delete.operation_id = Some("delete_operation".to_string());
397        path_item.set_operation(HttpMethod::Delete, operation_delete);
398        assert!(path_item.delete.is_some());
399
400        // Test setting HEAD operation
401        let mut operation_head = operation.clone();
402        operation_head.operation_id = Some("head_operation".to_string());
403        path_item.set_operation(HttpMethod::Head, operation_head);
404        assert!(path_item.head.is_some());
405
406        // Test setting OPTIONS operation
407        let mut operation_options = operation.clone();
408        operation_options.operation_id = Some("options_operation".to_string());
409        path_item.set_operation(HttpMethod::Options, operation_options);
410        assert!(path_item.options.is_some());
411
412        // Test setting TRACE operation
413        let mut operation_trace = operation;
414        operation_trace.operation_id = Some("trace_operation".to_string());
415        path_item.set_operation(HttpMethod::Trace, operation_trace);
416        assert!(path_item.trace.is_some());
417    }
418
419    #[test]
420    fn test_path_item_get_operation() {
421        let mut path_item = PathItem {
422            get: None,
423            post: None,
424            put: None,
425            patch: None,
426            delete: None,
427            head: None,
428            options: None,
429            trace: None,
430            parameters: None,
431            summary: None,
432            description: None,
433        };
434
435        let operation = Operation {
436            operation_id: Some("test_operation".to_string()),
437            tags: None,
438            summary: None,
439            description: None,
440            parameters: None,
441            request_body: None,
442            responses: BTreeMap::new(),
443            security: None,
444        };
445
446        // Initially, all operations should be None
447        assert!(path_item.get_operation(&HttpMethod::Get).is_none());
448        assert!(path_item.get_operation(&HttpMethod::Post).is_none());
449
450        // Set GET operation
451        path_item.set_operation(HttpMethod::Get, operation.clone());
452        let retrieved = path_item.get_operation(&HttpMethod::Get);
453        assert!(retrieved.is_some());
454        assert_eq!(
455            retrieved.unwrap().operation_id,
456            Some("test_operation".to_string())
457        );
458
459        // Set POST operation
460        let mut operation_post = operation.clone();
461        operation_post.operation_id = Some("post_operation".to_string());
462        path_item.set_operation(HttpMethod::Post, operation_post);
463        let retrieved = path_item.get_operation(&HttpMethod::Post);
464        assert!(retrieved.is_some());
465        assert_eq!(
466            retrieved.unwrap().operation_id,
467            Some("post_operation".to_string())
468        );
469
470        // Test all methods
471        path_item.set_operation(HttpMethod::Put, operation.clone());
472        assert!(path_item.get_operation(&HttpMethod::Put).is_some());
473
474        path_item.set_operation(HttpMethod::Patch, operation.clone());
475        assert!(path_item.get_operation(&HttpMethod::Patch).is_some());
476
477        path_item.set_operation(HttpMethod::Delete, operation.clone());
478        assert!(path_item.get_operation(&HttpMethod::Delete).is_some());
479
480        path_item.set_operation(HttpMethod::Head, operation.clone());
481        assert!(path_item.get_operation(&HttpMethod::Head).is_some());
482
483        path_item.set_operation(HttpMethod::Options, operation.clone());
484        assert!(path_item.get_operation(&HttpMethod::Options).is_some());
485
486        path_item.set_operation(HttpMethod::Trace, operation);
487        assert!(path_item.get_operation(&HttpMethod::Trace).is_some());
488    }
489
490    #[test]
491    fn test_path_item_set_operation_overwrites() {
492        let mut path_item = PathItem {
493            get: None,
494            post: None,
495            put: None,
496            patch: None,
497            delete: None,
498            head: None,
499            options: None,
500            trace: None,
501            parameters: None,
502            summary: None,
503            description: None,
504        };
505
506        let operation1 = Operation {
507            operation_id: Some("first".to_string()),
508            tags: None,
509            summary: None,
510            description: None,
511            parameters: None,
512            request_body: None,
513            responses: BTreeMap::new(),
514            security: None,
515        };
516
517        let operation2 = Operation {
518            operation_id: Some("second".to_string()),
519            tags: None,
520            summary: None,
521            description: None,
522            parameters: None,
523            request_body: None,
524            responses: BTreeMap::new(),
525            security: None,
526        };
527
528        // Set first operation
529        path_item.set_operation(HttpMethod::Get, operation1);
530        assert_eq!(
531            path_item.get.as_ref().unwrap().operation_id,
532            Some("first".to_string())
533        );
534
535        // Overwrite with second operation
536        path_item.set_operation(HttpMethod::Get, operation2);
537        assert_eq!(
538            path_item.get.as_ref().unwrap().operation_id,
539            Some("second".to_string())
540        );
541    }
542
543    #[rstest]
544    #[case(HttpMethod::Get, "GET")]
545    #[case(HttpMethod::Post, "POST")]
546    #[case(HttpMethod::Put, "PUT")]
547    #[case(HttpMethod::Patch, "PATCH")]
548    #[case(HttpMethod::Delete, "DELETE")]
549    #[case(HttpMethod::Head, "HEAD")]
550    #[case(HttpMethod::Options, "OPTIONS")]
551    #[case(HttpMethod::Trace, "TRACE")]
552    fn test_http_method_display(#[case] method: HttpMethod, #[case] expected: &str) {
553        assert_eq!(method.to_string(), expected);
554    }
555
556    #[test]
557    fn test_http_method_equality() {
558        let method1 = HttpMethod::Get;
559        let method2 = HttpMethod::Get;
560        let method3 = HttpMethod::Post;
561
562        assert_eq!(method1, method2);
563        assert_ne!(method1, method3);
564    }
565
566    #[test]
567    fn test_http_method_clone() {
568        let method = HttpMethod::Get;
569        let cloned = method;
570        assert_eq!(method, cloned);
571    }
572
573    #[test]
574    fn test_http_method_hash() {
575        use std::collections::HashMap;
576
577        let mut map = HashMap::new();
578        map.insert(HttpMethod::Get, "GET method");
579        map.insert(HttpMethod::Post, "POST method");
580
581        assert_eq!(map.get(&HttpMethod::Get), Some(&"GET method"));
582        assert_eq!(map.get(&HttpMethod::Post), Some(&"POST method"));
583        assert_eq!(map.get(&HttpMethod::Put), None);
584    }
585}