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