1use crate::{
9 error::{OpenApiError, OpenApiResult},
10 generator::{ParameterInfo, RouteMetadata},
11};
12use regex::Regex;
13use std::collections::HashMap;
14
15#[derive(Debug, Clone)]
17pub struct EndpointMetadata {
18 pub controller: String,
20 pub method: String,
22 pub verb: String,
24 pub path: String,
26 pub documentation: Option<String>,
28 pub parameters: Vec<EndpointParameter>,
30 pub return_type: Option<String>,
32 pub attributes: HashMap<String, String>,
34}
35
36#[derive(Debug, Clone)]
38pub struct EndpointParameter {
39 pub name: String,
41 pub param_type: String,
43 pub source: ParameterSource,
45 pub optional: bool,
47 pub documentation: Option<String>,
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub enum ParameterSource {
54 Path,
55 Query,
56 Body,
57 Header,
58 Cookie,
59}
60
61pub struct EndpointDiscovery {
63 path_param_regex: Regex,
65}
66
67impl EndpointDiscovery {
68 pub fn new() -> OpenApiResult<Self> {
70 Ok(Self {
71 path_param_regex: Regex::new(r"\{([^}]+)\}").map_err(|e| {
72 OpenApiError::route_discovery_error(format!("Failed to compile regex: {}", e))
73 })?,
74 })
75 }
76
77 pub fn discover_endpoints(
79 &self,
80 controllers: &[ControllerInfo],
81 ) -> OpenApiResult<Vec<RouteMetadata>> {
82 let mut routes = Vec::new();
83
84 for controller in controllers {
85 for endpoint in &controller.endpoints {
86 let route = self.convert_endpoint_to_route(controller, endpoint)?;
87 routes.push(route);
88 }
89 }
90
91 Ok(routes)
92 }
93
94 fn convert_endpoint_to_route(
96 &self,
97 controller: &ControllerInfo,
98 endpoint: &EndpointMetadata,
99 ) -> OpenApiResult<RouteMetadata> {
100 let path_params = self.extract_path_parameters(&endpoint.path)?;
102
103 let mut parameters = Vec::new();
105
106 for param_name in &path_params {
108 if let Some(endpoint_param) = endpoint
109 .parameters
110 .iter()
111 .find(|p| &p.name == param_name && p.source == ParameterSource::Path)
112 {
113 parameters.push(ParameterInfo {
114 name: param_name.clone(),
115 location: "path".to_string(),
116 param_type: endpoint_param.param_type.clone(),
117 description: endpoint_param.documentation.clone(),
118 required: true, example: None,
120 });
121 } else {
122 parameters.push(ParameterInfo {
124 name: param_name.clone(),
125 location: "path".to_string(),
126 param_type: "string".to_string(),
127 description: None,
128 required: true,
129 example: None,
130 });
131 }
132 }
133
134 for endpoint_param in &endpoint.parameters {
136 if endpoint_param.source == ParameterSource::Query {
137 parameters.push(ParameterInfo {
138 name: endpoint_param.name.clone(),
139 location: "query".to_string(),
140 param_type: endpoint_param.param_type.clone(),
141 description: endpoint_param.documentation.clone(),
142 required: !endpoint_param.optional,
143 example: None,
144 });
145 }
146 }
147
148 for endpoint_param in &endpoint.parameters {
150 if endpoint_param.source == ParameterSource::Header {
151 parameters.push(ParameterInfo {
152 name: endpoint_param.name.clone(),
153 location: "header".to_string(),
154 param_type: endpoint_param.param_type.clone(),
155 description: endpoint_param.documentation.clone(),
156 required: !endpoint_param.optional,
157 example: None,
158 });
159 }
160 }
161
162 let request_schema = endpoint
164 .parameters
165 .iter()
166 .find(|p| p.source == ParameterSource::Body)
167 .map(|p| p.param_type.clone());
168
169 let mut response_schemas = HashMap::new();
171 if let Some(return_type) = &endpoint.return_type {
172 if return_type != "()" && return_type != "ElifResponse" {
173 response_schemas.insert("200".to_string(), return_type.clone());
174 }
175 }
176
177 let summary = endpoint
179 .attributes
180 .get("summary")
181 .or_else(|| endpoint.attributes.get("description"))
182 .cloned();
183
184 let description = endpoint
185 .documentation
186 .clone()
187 .or_else(|| endpoint.attributes.get("description").cloned());
188
189 let operation_id = Some(format!(
190 "{}{}",
191 controller.name.to_lowercase(),
192 capitalize(&endpoint.method)
193 ));
194
195 let tags = vec![controller.name.clone()];
196
197 let security = if endpoint.attributes.contains_key("requires_auth") {
199 vec!["bearerAuth".to_string()]
200 } else {
201 Vec::new()
202 };
203
204 let deprecated = endpoint
205 .attributes
206 .get("deprecated")
207 .map(|v| v == "true")
208 .unwrap_or(false);
209
210 let full_path = self.join_paths(controller.base_path.as_deref(), &endpoint.path);
212
213 Ok(RouteMetadata {
214 method: endpoint.verb.clone(),
215 path: full_path,
216 summary,
217 description,
218 operation_id,
219 tags,
220 request_schema,
221 response_schemas,
222 parameters,
223 security,
224 deprecated,
225 })
226 }
227
228 fn join_paths(&self, base_path: Option<&str>, endpoint_path: &str) -> String {
230 match base_path {
231 Some(base) => {
232 let base = if base.starts_with('/') {
234 base.to_string()
235 } else {
236 format!("/{}", base)
237 };
238
239 let endpoint = if endpoint_path.starts_with('/') {
241 endpoint_path.to_string()
242 } else {
243 format!("/{}", endpoint_path)
244 };
245
246 let base = base.trim_end_matches('/');
248
249 if endpoint == "/" {
251 base.to_string()
252 } else {
253 format!("{}{}", base, endpoint)
254 }
255 }
256 None => {
257 if endpoint_path.starts_with('/') {
259 endpoint_path.to_string()
260 } else {
261 format!("/{}", endpoint_path)
262 }
263 }
264 }
265 }
266
267 fn extract_path_parameters(&self, path: &str) -> OpenApiResult<Vec<String>> {
269 let mut parameters = Vec::new();
270
271 for caps in self.path_param_regex.captures_iter(path) {
272 if let Some(param) = caps.get(1) {
273 parameters.push(param.as_str().to_string());
274 }
275 }
276
277 Ok(parameters)
278 }
279
280 pub fn extract_from_source(&self, source_code: &str) -> OpenApiResult<Vec<EndpointMetadata>> {
282 let mut endpoints = Vec::new();
285
286 let route_regex = Regex::new(r#"#\[route\((\w+),\s*"([^"]+)"\)\]"#).map_err(|e| {
288 OpenApiError::route_discovery_error(format!("Failed to compile route regex: {}", e))
289 })?;
290
291 let fn_regex = Regex::new(r"pub\s+async\s+fn\s+(\w+)").map_err(|e| {
293 OpenApiError::route_discovery_error(format!("Failed to compile function regex: {}", e))
294 })?;
295
296 for route_match in route_regex.captures_iter(source_code) {
297 if let (Some(verb), Some(path)) = (route_match.get(1), route_match.get(2)) {
298 let route_end = route_match.get(0).unwrap().end();
300 let remaining_code = &source_code[route_end..];
301
302 if let Some(fn_match) = fn_regex.find(remaining_code) {
303 let fn_name = fn_regex
304 .captures(&remaining_code[fn_match.start()..])
305 .and_then(|caps| caps.get(1))
306 .map(|m| m.as_str().to_string())
307 .unwrap_or_else(|| "unknown".to_string());
308
309 endpoints.push(EndpointMetadata {
310 controller: "Unknown".to_string(),
311 method: fn_name,
312 verb: verb.as_str().to_uppercase(),
313 path: path.as_str().to_string(),
314 documentation: None,
315 parameters: Vec::new(),
316 return_type: Some("ElifResponse".to_string()),
317 attributes: HashMap::new(),
318 });
319 }
320 }
321 }
322
323 Ok(endpoints)
324 }
325}
326
327#[derive(Debug, Clone)]
329pub struct ControllerInfo {
330 pub name: String,
332 pub base_path: Option<String>,
334 pub endpoints: Vec<EndpointMetadata>,
336 pub attributes: HashMap<String, String>,
338}
339
340impl ControllerInfo {
341 pub fn new(name: &str) -> Self {
343 Self {
344 name: name.to_string(),
345 base_path: None,
346 endpoints: Vec::new(),
347 attributes: HashMap::new(),
348 }
349 }
350
351 pub fn add_endpoint(mut self, endpoint: EndpointMetadata) -> Self {
353 self.endpoints.push(endpoint);
354 self
355 }
356
357 pub fn with_base_path(mut self, base_path: &str) -> Self {
359 self.base_path = Some(base_path.to_string());
360 self
361 }
362
363 pub fn with_attribute(mut self, key: &str, value: &str) -> Self {
365 self.attributes.insert(key.to_string(), value.to_string());
366 self
367 }
368}
369
370impl EndpointMetadata {
371 pub fn new(method: &str, verb: &str, path: &str) -> Self {
373 Self {
374 controller: "Unknown".to_string(),
375 method: method.to_string(),
376 verb: verb.to_string(),
377 path: path.to_string(),
378 documentation: None,
379 parameters: Vec::new(),
380 return_type: None,
381 attributes: HashMap::new(),
382 }
383 }
384
385 pub fn with_parameter(mut self, parameter: EndpointParameter) -> Self {
387 self.parameters.push(parameter);
388 self
389 }
390
391 pub fn with_return_type(mut self, return_type: &str) -> Self {
393 self.return_type = Some(return_type.to_string());
394 self
395 }
396
397 pub fn with_attribute(mut self, key: &str, value: &str) -> Self {
399 self.attributes.insert(key.to_string(), value.to_string());
400 self
401 }
402
403 pub fn with_documentation(mut self, doc: &str) -> Self {
405 self.documentation = Some(doc.to_string());
406 self
407 }
408}
409
410impl EndpointParameter {
411 pub fn new(name: &str, param_type: &str, source: ParameterSource) -> Self {
413 Self {
414 name: name.to_string(),
415 param_type: param_type.to_string(),
416 source,
417 optional: false,
418 documentation: None,
419 }
420 }
421
422 pub fn optional(mut self) -> Self {
424 self.optional = true;
425 self
426 }
427
428 pub fn with_documentation(mut self, doc: &str) -> Self {
430 self.documentation = Some(doc.to_string());
431 self
432 }
433}
434
435fn capitalize(s: &str) -> String {
437 let mut chars = s.chars();
438 match chars.next() {
439 None => String::new(),
440 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn test_endpoint_discovery_creation() {
450 let discovery = EndpointDiscovery::new().unwrap();
451 assert!(discovery.path_param_regex.is_match("{id}"));
452 }
453
454 #[test]
455 fn test_path_parameter_extraction() {
456 let discovery = EndpointDiscovery::new().unwrap();
457
458 let params = discovery
459 .extract_path_parameters("/users/{id}/posts/{post_id}")
460 .unwrap();
461 assert_eq!(params, vec!["id", "post_id"]);
462
463 let no_params = discovery.extract_path_parameters("/users").unwrap();
464 assert!(no_params.is_empty());
465 }
466
467 #[test]
468 fn test_endpoint_metadata_creation() {
469 let endpoint = EndpointMetadata::new("index", "GET", "/users")
470 .with_return_type("Vec<User>")
471 .with_attribute("summary", "List all users")
472 .with_parameter(
473 EndpointParameter::new("limit", "Option<i32>", ParameterSource::Query).optional(),
474 );
475
476 assert_eq!(endpoint.method, "index");
477 assert_eq!(endpoint.verb, "GET");
478 assert_eq!(endpoint.path, "/users");
479 assert_eq!(endpoint.return_type, Some("Vec<User>".to_string()));
480 assert_eq!(endpoint.parameters.len(), 1);
481 assert_eq!(
482 endpoint.attributes.get("summary"),
483 Some(&"List all users".to_string())
484 );
485 }
486
487 #[test]
488 fn test_controller_info_creation() {
489 let controller = ControllerInfo::new("Users")
490 .with_base_path("/api/v1")
491 .add_endpoint(EndpointMetadata::new("index", "GET", "/users"))
492 .add_endpoint(EndpointMetadata::new("show", "GET", "/users/{id}"));
493
494 assert_eq!(controller.name, "Users");
495 assert_eq!(controller.base_path, Some("/api/v1".to_string()));
496 assert_eq!(controller.endpoints.len(), 2);
497 }
498
499 #[test]
500 fn test_route_metadata_conversion() {
501 let discovery = EndpointDiscovery::new().unwrap();
502
503 let controller = ControllerInfo::new("Users");
504 let endpoint = EndpointMetadata::new("show", "GET", "/users/{id}")
505 .with_return_type("User")
506 .with_parameter(EndpointParameter::new("id", "i32", ParameterSource::Path))
507 .with_attribute("summary", "Get user by ID");
508
509 let route = discovery
510 .convert_endpoint_to_route(&controller, &endpoint)
511 .unwrap();
512
513 assert_eq!(route.method, "GET");
514 assert_eq!(route.path, "/users/{id}");
515 assert_eq!(route.summary, Some("Get user by ID".to_string()));
516 assert_eq!(route.tags, vec!["Users".to_string()]);
517 assert_eq!(route.parameters.len(), 1);
518 assert_eq!(route.parameters[0].name, "id");
519 assert_eq!(route.parameters[0].location, "path");
520 assert!(route.parameters[0].required);
521 }
522
523 #[test]
524 fn test_path_joining_robust_logic() {
525 let discovery = EndpointDiscovery::new().unwrap();
526
527 assert_eq!(
529 discovery.join_paths(Some("/api/v1"), "/users"),
530 "/api/v1/users"
531 );
532
533 assert_eq!(
535 discovery.join_paths(Some("api/v1"), "/users"),
536 "/api/v1/users"
537 );
538
539 assert_eq!(
541 discovery.join_paths(Some("/api/v1"), "users"),
542 "/api/v1/users"
543 );
544
545 assert_eq!(
547 discovery.join_paths(Some("api/v1"), "users"),
548 "/api/v1/users"
549 );
550
551 assert_eq!(
553 discovery.join_paths(Some("/api/v1/"), "/users"),
554 "/api/v1/users"
555 );
556
557 assert_eq!(
559 discovery.join_paths(Some("/api/v1/"), "users"),
560 "/api/v1/users"
561 );
562
563 assert_eq!(discovery.join_paths(Some("/api/v1"), "/"), "/api/v1");
565
566 assert_eq!(discovery.join_paths(None, "/users"), "/users");
568
569 assert_eq!(discovery.join_paths(None, "users"), "/users");
571
572 assert_eq!(discovery.join_paths(Some("/"), "/users"), "/users");
574 assert_eq!(discovery.join_paths(Some("/api"), "/"), "/api");
575
576 assert_eq!(
578 discovery.join_paths(Some("/api/v1"), "/users/{id}/posts"),
579 "/api/v1/users/{id}/posts"
580 );
581 assert_eq!(
582 discovery.join_paths(Some("api/v1/"), "/users/{id}"),
583 "/api/v1/users/{id}"
584 );
585 }
586
587 #[test]
588 fn test_route_metadata_conversion_with_base_path() {
589 let discovery = EndpointDiscovery::new().unwrap();
590
591 let controller = ControllerInfo::new("Users").with_base_path("/api/v1");
592
593 let endpoint = EndpointMetadata::new("show", "GET", "/users/{id}")
594 .with_parameter(EndpointParameter::new("id", "i32", ParameterSource::Path));
595
596 let route = discovery
597 .convert_endpoint_to_route(&controller, &endpoint)
598 .unwrap();
599
600 assert_eq!(route.path, "/api/v1/users/{id}");
602 assert_eq!(route.method, "GET");
603 }
604}