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 #[serde(default)]
83 pub is_async: bool,
84 pub cors: Option<CorsConfig>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub body_param_name: Option<String>,
88 #[cfg(feature = "di")]
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub handler_dependencies: Option<Vec<String>>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub jsonrpc_method: Option<Value>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct CompressionConfig {
100 #[serde(default = "default_true")]
102 pub gzip: bool,
103 #[serde(default = "default_true")]
105 pub brotli: bool,
106 #[serde(default = "default_compression_min_size")]
108 pub min_size: usize,
109 #[serde(default = "default_compression_quality")]
111 pub quality: u32,
112}
113
114const fn default_true() -> bool {
115 true
116}
117
118const fn default_compression_min_size() -> usize {
119 1024
120}
121
122const fn default_compression_quality() -> u32 {
123 6
124}
125
126impl Default for CompressionConfig {
127 fn default() -> Self {
128 Self {
129 gzip: true,
130 brotli: true,
131 min_size: default_compression_min_size(),
132 quality: default_compression_quality(),
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct RateLimitConfig {
140 pub per_second: u64,
142 pub burst: u32,
144 #[serde(default = "default_true")]
146 pub ip_based: bool,
147}
148
149impl Default for RateLimitConfig {
150 fn default() -> Self {
151 Self {
152 per_second: 100,
153 burst: 200,
154 ip_based: true,
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use std::str::FromStr;
163
164 #[test]
165 fn test_method_as_str_get() {
166 assert_eq!(Method::Get.as_str(), "GET");
167 }
168
169 #[test]
170 fn test_method_as_str_post() {
171 assert_eq!(Method::Post.as_str(), "POST");
172 }
173
174 #[test]
175 fn test_method_as_str_put() {
176 assert_eq!(Method::Put.as_str(), "PUT");
177 }
178
179 #[test]
180 fn test_method_as_str_patch() {
181 assert_eq!(Method::Patch.as_str(), "PATCH");
182 }
183
184 #[test]
185 fn test_method_as_str_delete() {
186 assert_eq!(Method::Delete.as_str(), "DELETE");
187 }
188
189 #[test]
190 fn test_method_as_str_head() {
191 assert_eq!(Method::Head.as_str(), "HEAD");
192 }
193
194 #[test]
195 fn test_method_as_str_options() {
196 assert_eq!(Method::Options.as_str(), "OPTIONS");
197 }
198
199 #[test]
200 fn test_method_as_str_trace() {
201 assert_eq!(Method::Trace.as_str(), "TRACE");
202 }
203
204 #[test]
205 fn test_method_display_get() {
206 assert_eq!(Method::Get.to_string(), "GET");
207 }
208
209 #[test]
210 fn test_method_display_post() {
211 assert_eq!(Method::Post.to_string(), "POST");
212 }
213
214 #[test]
215 fn test_method_display_put() {
216 assert_eq!(Method::Put.to_string(), "PUT");
217 }
218
219 #[test]
220 fn test_method_display_patch() {
221 assert_eq!(Method::Patch.to_string(), "PATCH");
222 }
223
224 #[test]
225 fn test_method_display_delete() {
226 assert_eq!(Method::Delete.to_string(), "DELETE");
227 }
228
229 #[test]
230 fn test_method_display_head() {
231 assert_eq!(Method::Head.to_string(), "HEAD");
232 }
233
234 #[test]
235 fn test_method_display_options() {
236 assert_eq!(Method::Options.to_string(), "OPTIONS");
237 }
238
239 #[test]
240 fn test_method_display_trace() {
241 assert_eq!(Method::Trace.to_string(), "TRACE");
242 }
243
244 #[test]
245 fn test_from_str_get() {
246 assert_eq!(Method::from_str("GET"), Ok(Method::Get));
247 }
248
249 #[test]
250 fn test_from_str_post() {
251 assert_eq!(Method::from_str("POST"), Ok(Method::Post));
252 }
253
254 #[test]
255 fn test_from_str_put() {
256 assert_eq!(Method::from_str("PUT"), Ok(Method::Put));
257 }
258
259 #[test]
260 fn test_from_str_patch() {
261 assert_eq!(Method::from_str("PATCH"), Ok(Method::Patch));
262 }
263
264 #[test]
265 fn test_from_str_delete() {
266 assert_eq!(Method::from_str("DELETE"), Ok(Method::Delete));
267 }
268
269 #[test]
270 fn test_from_str_head() {
271 assert_eq!(Method::from_str("HEAD"), Ok(Method::Head));
272 }
273
274 #[test]
275 fn test_from_str_options() {
276 assert_eq!(Method::from_str("OPTIONS"), Ok(Method::Options));
277 }
278
279 #[test]
280 fn test_from_str_trace() {
281 assert_eq!(Method::from_str("TRACE"), Ok(Method::Trace));
282 }
283
284 #[test]
285 fn test_from_str_lowercase() {
286 assert_eq!(Method::from_str("get"), Ok(Method::Get));
287 }
288
289 #[test]
290 fn test_from_str_mixed_case() {
291 assert_eq!(Method::from_str("PoSt"), Ok(Method::Post));
292 }
293
294 #[test]
295 fn test_from_str_invalid_method() {
296 let result = Method::from_str("INVALID");
297 assert!(result.is_err());
298 assert_eq!(result.unwrap_err(), "Unknown HTTP method: INVALID");
299 }
300
301 #[test]
302 fn test_from_str_empty_string() {
303 let result = Method::from_str("");
304 assert!(result.is_err());
305 assert_eq!(result.unwrap_err(), "Unknown HTTP method: ");
306 }
307
308 #[test]
309 fn test_compression_config_default() {
310 let config = CompressionConfig::default();
311 assert!(config.gzip);
312 assert!(config.brotli);
313 assert_eq!(config.min_size, 1024);
314 assert_eq!(config.quality, 6);
315 }
316
317 #[test]
318 fn test_default_true() {
319 assert!(default_true());
320 }
321
322 #[test]
323 fn test_default_compression_min_size() {
324 assert_eq!(default_compression_min_size(), 1024);
325 }
326
327 #[test]
328 fn test_default_compression_quality() {
329 assert_eq!(default_compression_quality(), 6);
330 }
331
332 #[test]
333 fn test_rate_limit_config_default() {
334 let config = RateLimitConfig::default();
335 assert_eq!(config.per_second, 100);
336 assert_eq!(config.burst, 200);
337 assert!(config.ip_based);
338 }
339
340 #[test]
341 fn test_method_equality() {
342 assert_eq!(Method::Get, Method::Get);
343 assert_ne!(Method::Get, Method::Post);
344 }
345
346 #[test]
347 fn test_method_clone() {
348 let method = Method::Post;
349 let cloned = method.clone();
350 assert_eq!(method, cloned);
351 }
352
353 #[test]
354 fn test_compression_config_custom_values() {
355 let config = CompressionConfig {
356 gzip: false,
357 brotli: false,
358 min_size: 2048,
359 quality: 11,
360 };
361 assert!(!config.gzip);
362 assert!(!config.brotli);
363 assert_eq!(config.min_size, 2048);
364 assert_eq!(config.quality, 11);
365 }
366
367 #[test]
368 fn test_rate_limit_config_custom_values() {
369 let config = RateLimitConfig {
370 per_second: 50,
371 burst: 100,
372 ip_based: false,
373 };
374 assert_eq!(config.per_second, 50);
375 assert_eq!(config.burst, 100);
376 assert!(!config.ip_based);
377 }
378
379 #[test]
380 fn test_cors_config_construction() {
381 let cors = CorsConfig {
382 allowed_origins: vec!["http://localhost:3000".to_string()],
383 allowed_methods: vec!["GET".to_string(), "POST".to_string()],
384 allowed_headers: vec![],
385 expose_headers: None,
386 max_age: None,
387 allow_credentials: None,
388 };
389 assert_eq!(cors.allowed_origins.len(), 1);
390 assert_eq!(cors.allowed_methods.len(), 2);
391 assert_eq!(cors.allowed_headers.len(), 0);
392 }
393
394 #[test]
395 fn test_route_metadata_construction() {
396 let metadata = RouteMetadata {
397 method: "GET".to_string(),
398 path: "/api/users".to_string(),
399 handler_name: "get_users".to_string(),
400 request_schema: None,
401 response_schema: None,
402 parameter_schema: None,
403 file_params: None,
404 is_async: true,
405 cors: None,
406 body_param_name: None,
407 #[cfg(feature = "di")]
408 handler_dependencies: None,
409 jsonrpc_method: None,
410 };
411 assert_eq!(metadata.method, "GET");
412 assert_eq!(metadata.path, "/api/users");
413 assert_eq!(metadata.handler_name, "get_users");
414 assert!(metadata.is_async);
415 }
416}