1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
6pub enum Method {
7 Get,
8 Post,
9 Put,
10 Patch,
11 Delete,
12 Head,
13 Options,
14 Trace,
15}
16
17impl Method {
18 pub fn as_str(&self) -> &'static str {
19 match self {
20 Method::Get => "GET",
21 Method::Post => "POST",
22 Method::Put => "PUT",
23 Method::Patch => "PATCH",
24 Method::Delete => "DELETE",
25 Method::Head => "HEAD",
26 Method::Options => "OPTIONS",
27 Method::Trace => "TRACE",
28 }
29 }
30}
31
32impl std::fmt::Display for Method {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 write!(f, "{}", self.as_str())
35 }
36}
37
38impl std::str::FromStr for Method {
39 type Err = String;
40
41 fn from_str(s: &str) -> Result<Self, Self::Err> {
42 match s.to_uppercase().as_str() {
43 "GET" => Ok(Method::Get),
44 "POST" => Ok(Method::Post),
45 "PUT" => Ok(Method::Put),
46 "PATCH" => Ok(Method::Patch),
47 "DELETE" => Ok(Method::Delete),
48 "HEAD" => Ok(Method::Head),
49 "OPTIONS" => Ok(Method::Options),
50 "TRACE" => Ok(Method::Trace),
51 _ => Err(format!("Unknown HTTP method: {}", s)),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CorsConfig {
59 pub allowed_origins: Vec<String>,
60 pub allowed_methods: Vec<String>,
61 #[serde(default)]
62 pub allowed_headers: Vec<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub expose_headers: Option<Vec<String>>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub max_age: Option<u32>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub allow_credentials: Option<bool>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct RouteMetadata {
74 pub method: String,
75 pub path: String,
76 pub handler_name: String,
77 pub request_schema: Option<Value>,
78 pub response_schema: Option<Value>,
79 pub parameter_schema: Option<Value>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub file_params: Option<Value>,
82 pub is_async: bool,
83 pub cors: Option<CorsConfig>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub body_param_name: Option<String>,
87 #[cfg(feature = "di")]
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub handler_dependencies: Option<Vec<String>>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub jsonrpc_method: Option<Value>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct CompressionConfig {
99 #[serde(default = "default_true")]
101 pub gzip: bool,
102 #[serde(default = "default_true")]
104 pub brotli: bool,
105 #[serde(default = "default_compression_min_size")]
107 pub min_size: usize,
108 #[serde(default = "default_compression_quality")]
110 pub quality: u32,
111}
112
113const fn default_true() -> bool {
114 true
115}
116
117const fn default_compression_min_size() -> usize {
118 1024
119}
120
121const fn default_compression_quality() -> u32 {
122 6
123}
124
125impl Default for CompressionConfig {
126 fn default() -> Self {
127 Self {
128 gzip: true,
129 brotli: true,
130 min_size: default_compression_min_size(),
131 quality: default_compression_quality(),
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct RateLimitConfig {
139 pub per_second: u64,
141 pub burst: u32,
143 #[serde(default = "default_true")]
145 pub ip_based: bool,
146}
147
148impl Default for RateLimitConfig {
149 fn default() -> Self {
150 Self {
151 per_second: 100,
152 burst: 200,
153 ip_based: true,
154 }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use std::str::FromStr;
162
163 #[test]
164 fn test_method_as_str_get() {
165 assert_eq!(Method::Get.as_str(), "GET");
166 }
167
168 #[test]
169 fn test_method_as_str_post() {
170 assert_eq!(Method::Post.as_str(), "POST");
171 }
172
173 #[test]
174 fn test_method_as_str_put() {
175 assert_eq!(Method::Put.as_str(), "PUT");
176 }
177
178 #[test]
179 fn test_method_as_str_patch() {
180 assert_eq!(Method::Patch.as_str(), "PATCH");
181 }
182
183 #[test]
184 fn test_method_as_str_delete() {
185 assert_eq!(Method::Delete.as_str(), "DELETE");
186 }
187
188 #[test]
189 fn test_method_as_str_head() {
190 assert_eq!(Method::Head.as_str(), "HEAD");
191 }
192
193 #[test]
194 fn test_method_as_str_options() {
195 assert_eq!(Method::Options.as_str(), "OPTIONS");
196 }
197
198 #[test]
199 fn test_method_as_str_trace() {
200 assert_eq!(Method::Trace.as_str(), "TRACE");
201 }
202
203 #[test]
204 fn test_method_display_get() {
205 assert_eq!(Method::Get.to_string(), "GET");
206 }
207
208 #[test]
209 fn test_method_display_post() {
210 assert_eq!(Method::Post.to_string(), "POST");
211 }
212
213 #[test]
214 fn test_method_display_put() {
215 assert_eq!(Method::Put.to_string(), "PUT");
216 }
217
218 #[test]
219 fn test_method_display_patch() {
220 assert_eq!(Method::Patch.to_string(), "PATCH");
221 }
222
223 #[test]
224 fn test_method_display_delete() {
225 assert_eq!(Method::Delete.to_string(), "DELETE");
226 }
227
228 #[test]
229 fn test_method_display_head() {
230 assert_eq!(Method::Head.to_string(), "HEAD");
231 }
232
233 #[test]
234 fn test_method_display_options() {
235 assert_eq!(Method::Options.to_string(), "OPTIONS");
236 }
237
238 #[test]
239 fn test_method_display_trace() {
240 assert_eq!(Method::Trace.to_string(), "TRACE");
241 }
242
243 #[test]
244 fn test_from_str_get() {
245 assert_eq!(Method::from_str("GET"), Ok(Method::Get));
246 }
247
248 #[test]
249 fn test_from_str_post() {
250 assert_eq!(Method::from_str("POST"), Ok(Method::Post));
251 }
252
253 #[test]
254 fn test_from_str_put() {
255 assert_eq!(Method::from_str("PUT"), Ok(Method::Put));
256 }
257
258 #[test]
259 fn test_from_str_patch() {
260 assert_eq!(Method::from_str("PATCH"), Ok(Method::Patch));
261 }
262
263 #[test]
264 fn test_from_str_delete() {
265 assert_eq!(Method::from_str("DELETE"), Ok(Method::Delete));
266 }
267
268 #[test]
269 fn test_from_str_head() {
270 assert_eq!(Method::from_str("HEAD"), Ok(Method::Head));
271 }
272
273 #[test]
274 fn test_from_str_options() {
275 assert_eq!(Method::from_str("OPTIONS"), Ok(Method::Options));
276 }
277
278 #[test]
279 fn test_from_str_trace() {
280 assert_eq!(Method::from_str("TRACE"), Ok(Method::Trace));
281 }
282
283 #[test]
284 fn test_from_str_lowercase() {
285 assert_eq!(Method::from_str("get"), Ok(Method::Get));
286 }
287
288 #[test]
289 fn test_from_str_mixed_case() {
290 assert_eq!(Method::from_str("PoSt"), Ok(Method::Post));
291 }
292
293 #[test]
294 fn test_from_str_invalid_method() {
295 let result = Method::from_str("INVALID");
296 assert!(result.is_err());
297 assert_eq!(result.unwrap_err(), "Unknown HTTP method: INVALID");
298 }
299
300 #[test]
301 fn test_from_str_empty_string() {
302 let result = Method::from_str("");
303 assert!(result.is_err());
304 assert_eq!(result.unwrap_err(), "Unknown HTTP method: ");
305 }
306
307 #[test]
308 fn test_compression_config_default() {
309 let config = CompressionConfig::default();
310 assert!(config.gzip);
311 assert!(config.brotli);
312 assert_eq!(config.min_size, 1024);
313 assert_eq!(config.quality, 6);
314 }
315
316 #[test]
317 fn test_default_true() {
318 assert!(default_true());
319 }
320
321 #[test]
322 fn test_default_compression_min_size() {
323 assert_eq!(default_compression_min_size(), 1024);
324 }
325
326 #[test]
327 fn test_default_compression_quality() {
328 assert_eq!(default_compression_quality(), 6);
329 }
330
331 #[test]
332 fn test_rate_limit_config_default() {
333 let config = RateLimitConfig::default();
334 assert_eq!(config.per_second, 100);
335 assert_eq!(config.burst, 200);
336 assert!(config.ip_based);
337 }
338
339 #[test]
340 fn test_method_equality() {
341 assert_eq!(Method::Get, Method::Get);
342 assert_ne!(Method::Get, Method::Post);
343 }
344
345 #[test]
346 fn test_method_clone() {
347 let method = Method::Post;
348 let cloned = method.clone();
349 assert_eq!(method, cloned);
350 }
351
352 #[test]
353 fn test_compression_config_custom_values() {
354 let config = CompressionConfig {
355 gzip: false,
356 brotli: false,
357 min_size: 2048,
358 quality: 11,
359 };
360 assert!(!config.gzip);
361 assert!(!config.brotli);
362 assert_eq!(config.min_size, 2048);
363 assert_eq!(config.quality, 11);
364 }
365
366 #[test]
367 fn test_rate_limit_config_custom_values() {
368 let config = RateLimitConfig {
369 per_second: 50,
370 burst: 100,
371 ip_based: false,
372 };
373 assert_eq!(config.per_second, 50);
374 assert_eq!(config.burst, 100);
375 assert!(!config.ip_based);
376 }
377
378 #[test]
379 fn test_cors_config_construction() {
380 let cors = CorsConfig {
381 allowed_origins: vec!["http://localhost:3000".to_string()],
382 allowed_methods: vec!["GET".to_string(), "POST".to_string()],
383 allowed_headers: vec![],
384 expose_headers: None,
385 max_age: None,
386 allow_credentials: None,
387 };
388 assert_eq!(cors.allowed_origins.len(), 1);
389 assert_eq!(cors.allowed_methods.len(), 2);
390 assert_eq!(cors.allowed_headers.len(), 0);
391 }
392
393 #[test]
394 fn test_route_metadata_construction() {
395 let metadata = RouteMetadata {
396 method: "GET".to_string(),
397 path: "/api/users".to_string(),
398 handler_name: "get_users".to_string(),
399 request_schema: None,
400 response_schema: None,
401 parameter_schema: None,
402 file_params: None,
403 is_async: true,
404 cors: None,
405 body_param_name: None,
406 #[cfg(feature = "di")]
407 handler_dependencies: None,
408 jsonrpc_method: None,
409 };
410 assert_eq!(metadata.method, "GET");
411 assert_eq!(metadata.path, "/api/users");
412 assert_eq!(metadata.handler_name, "get_users");
413 assert!(metadata.is_async);
414 }
415}