bytedocs_rs/core/
apidocs.rs

1use crate::ai::{new_client, Client as LLMClient, ChatRequest, ChatResponse, ensure_ai_initialized};
2use crate::core::types::{
3    Config, Documentation, APIInfo, EndpointSection, Endpoint, Parameter,
4    RequestBody, Response as ApiResponse, Schema, RouteInfo
5};
6use axum::{
7    extract::State,
8    http::StatusCode,
9    response::{Html, Json, IntoResponse, Response},
10    Router,
11};
12use tower_http::{
13    cors::CorsLayer,
14    limit::RequestBodyLimitLayer,
15    timeout::TimeoutLayer,
16};
17use std::time::Duration;
18use askama::Template;
19use serde_json::{json, Value};
20use std::collections::HashMap;
21use std::sync::Arc;
22use regex::Regex;
23
24#[derive(Template)]
25#[template(path = "full_ui.html")]
26struct DocsTemplate {
27    title: String,
28    docs_json: String,
29    config_json: String,
30}
31
32pub struct APIDocs {
33    config: Config,
34    documentation: Documentation,
35    routes: Vec<RouteInfo>,
36    schemas: HashMap<String, Schema>,
37    #[allow(dead_code)]
38    llm_client: Option<Arc<Box<dyn LLMClient>>>,
39}
40
41impl APIDocs {
42    pub fn new(config: Option<Config>) -> Self {
43        let config = config.unwrap_or_default();
44
45        // Initialize AI client factories
46        ensure_ai_initialized();
47
48        let llm_client = if let Some(ai_config) = &config.ai_config {
49            if ai_config.enabled {
50                match new_client(ai_config) {
51                    Ok(client) => {
52                        Some(Arc::new(client))
53                    }
54                    Err(_e) => {
55                        None
56                    }
57                }
58            } else {
59                None
60            }
61        } else {
62            None
63        };
64
65        let documentation = Documentation {
66            info: APIInfo {
67                title: config.title.clone(),
68                version: config.version.clone(),
69                description: config.description.clone(),
70                base_url: config.base_url.clone(),
71            },
72            endpoints: Vec::new(),
73            schemas: Some(HashMap::new()),
74        };
75
76        Self {
77            config,
78            documentation,
79            routes: Vec::new(),
80            schemas: HashMap::new(),
81            llm_client,
82        }
83    }
84
85    pub fn add_route_info(&mut self, route: RouteInfo) {
86        self.routes.push(route);
87    }
88
89    pub fn get_config(&self) -> &Config {
90        &self.config
91    }
92
93    pub fn add_route(
94        &mut self,
95        method: &str,
96        path: &str,
97        handler: Box<dyn std::any::Any + Send + Sync>,
98        summary: Option<String>,
99        description: Option<String>,
100        parameters: Option<Vec<Parameter>>,
101        request_body: Option<RequestBody>,
102        responses: Option<HashMap<String, ApiResponse>>,
103    ) {
104        let route = RouteInfo {
105            method: method.to_uppercase(),
106            path: path.to_string(),
107            handler,
108            middlewares: Vec::new(),
109            summary,
110            description,
111            parameters,
112            request_body,
113            responses,
114        };
115
116        self.routes.push(route);
117    }
118
119    pub fn generate(&mut self) -> anyhow::Result<()> {
120        let mut sections: HashMap<String, EndpointSection> = HashMap::new();
121
122        for route in &self.routes {
123            let endpoint = self.process_route(route);
124            let section_name = self.extract_section(&endpoint.path);
125
126            if !sections.contains_key(&section_name) {
127                sections.insert(section_name.clone(), EndpointSection {
128                    id: section_name.clone(),
129                    name: self.format_section_name(&section_name),
130                    description: format!("{} related endpoints", self.format_section_name(&section_name)),
131                    endpoints: Vec::new(),
132                });
133            }
134
135            if let Some(section) = sections.get_mut(&section_name) {
136                section.endpoints.push(endpoint);
137            }
138        }
139
140        self.documentation.endpoints = sections.into_values().collect();
141        Ok(())
142    }
143
144    fn process_route(&self, route: &RouteInfo) -> Endpoint {
145        let display_path = convert_path_to_openapi(&route.path);
146
147        let summary = route.summary.clone()
148            .unwrap_or_else(|| self.generate_summary(&route.method, &display_path));
149
150        let description = route.description.clone()
151            .unwrap_or_else(|| summary.clone());
152
153        let path_params = self.extract_parameters(&route.path);
154        let all_params = self.merge_parameters(path_params, route.parameters.clone().unwrap_or_default());
155
156        let responses = route.responses.clone()
157            .unwrap_or_else(|| self.generate_responses());
158
159        Endpoint {
160            id: self.generate_id(&route.method, &display_path),
161            method: route.method.clone(),
162            path: display_path,
163            summary,
164            description,
165            parameters: if all_params.is_empty() { None } else { Some(all_params) },
166            request_body: route.request_body.clone(),
167            responses,
168            tags: None,
169            handler: None, // Internal use - not serialized
170        }
171    }
172
173    fn extract_parameters(&self, path: &str) -> Vec<Parameter> {
174        let mut params = Vec::new();
175        let path_params = extract_path_params(path);
176
177        for param in path_params {
178            params.push(Parameter {
179                name: param,
180                r#in: "path".to_string(),
181                r#type: "string".to_string(),
182                required: true,
183                description: String::new(),
184                example: None,
185            });
186        }
187
188        params
189    }
190
191    fn merge_parameters(&self, path_params: Vec<Parameter>, provided_params: Vec<Parameter>) -> Vec<Parameter> {
192        let mut param_map: HashMap<String, Parameter> = HashMap::new();
193
194        for param in path_params {
195            let key = format!("{}:{}", param.name, param.r#in);
196            param_map.insert(key, param);
197        }
198
199        for param in provided_params {
200            let key = format!("{}:{}", param.name, param.r#in);
201            param_map.insert(key, param);
202        }
203
204        param_map.into_values().collect()
205    }
206
207    fn generate_responses(&self) -> HashMap<String, ApiResponse> {
208        let mut responses = HashMap::new();
209
210        responses.insert("200".to_string(), ApiResponse {
211            description: "Success".to_string(),
212            example: Some(json!({"status": "success"})),
213            schema: None,
214            content_type: None,
215        });
216        responses.insert("400".to_string(), ApiResponse {
217            description: "Bad Request".to_string(),
218            example: None,
219            schema: None,
220            content_type: None,
221        });
222        responses.insert("404".to_string(), ApiResponse {
223            description: "Not Found".to_string(),
224            example: None,
225            schema: None,
226            content_type: None,
227        });
228        responses.insert("500".to_string(), ApiResponse {
229            description: "Internal Server Error".to_string(),
230            example: None,
231            schema: None,
232            content_type: None,
233        });
234
235        responses
236    }
237
238    fn extract_section(&self, path: &str) -> String {
239        let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
240
241        for part in parts.iter().rev() {
242            if !part.is_empty() && !part.starts_with(':') && !part.contains('{') {
243                if *part != "api" && !part.starts_with('v') {
244                    return part.to_string();
245                }
246            }
247        }
248
249        if !parts.is_empty() && !parts[0].is_empty() {
250            return parts[0].to_string();
251        }
252
253        "default".to_string()
254    }
255
256    fn format_section_name(&self, section: &str) -> String {
257        let mut chars: Vec<char> = section.chars().collect();
258        if let Some(first_char) = chars.first_mut() {
259            *first_char = first_char.to_uppercase().next().unwrap_or(*first_char);
260        }
261        chars.into_iter().collect()
262    }
263
264    fn generate_id(&self, method: &str, path: &str) -> String {
265        format!("{}-{}",
266            method.to_lowercase(),
267            path.replace('/', "-").replace(':', ""))
268    }
269
270    fn generate_summary(&self, method: &str, path: &str) -> String {
271        let section = self.extract_section(path);
272        let action = self.infer_action(method, path);
273        format!("{} {}", action, section)
274    }
275
276    fn infer_action(&self, method: &str, path: &str) -> String {
277        match method.to_uppercase().as_str() {
278            "GET" => {
279                let has_param = path.contains(':') || path.contains('{');
280                if has_param { "Get" } else { "List" }.to_string()
281            }
282            "POST" => "Create".to_string(),
283            "PUT" | "PATCH" => "Update".to_string(),
284            "DELETE" => "Delete".to_string(),
285            _ => method.to_string(),
286        }
287    }
288
289    pub fn get_documentation(&self) -> &Documentation {
290        &self.documentation
291    }
292
293    pub async fn get_openapi_json(&mut self) -> anyhow::Result<Value> {
294        self.generate()?;
295
296        let mut openapi = json!({
297            "openapi": "3.0.3",
298            "info": {
299                "title": self.documentation.info.title,
300                "version": self.documentation.info.version,
301                "description": self.documentation.info.description
302            },
303            "servers": [],
304            "paths": {},
305            "components": {
306                "schemas": self.documentation.schemas
307            }
308        });
309
310        // Set servers
311        if !self.config.base_url.is_empty() {
312            openapi["servers"] = json!([{"url": self.config.base_url}]);
313        }
314        if !self.config.base_urls.is_empty() {
315            let servers: Vec<Value> = self.config.base_urls.iter()
316                .map(|base_url| json!({
317                    "url": base_url.url,
318                    "description": base_url.name
319                }))
320                .collect();
321            openapi["servers"] = json!(servers);
322        }
323
324        // Build paths
325        let mut paths = json!({});
326        for section in &self.documentation.endpoints {
327            for endpoint in &section.endpoints {
328                let path_key = convert_path_to_openapi(&endpoint.path);
329                if paths[&path_key].is_null() {
330                    paths[&path_key] = json!({});
331                }
332
333                let method_key = endpoint.method.to_lowercase();
334                let mut operation = json!({
335                    "summary": endpoint.summary,
336                    "description": endpoint.description,
337                    "tags": [section.name],
338                    "operationId": endpoint.id,
339                    "parameters": [],
340                    "responses": {}
341                });
342
343                if let Some(ref parameters) = endpoint.parameters {
344                    let params: Vec<Value> = parameters.iter()
345                        .map(|param| json!({
346                            "name": param.name,
347                            "in": param.r#in,
348                            "required": param.required,
349                            "description": param.description,
350                            "schema": {
351                                "type": normalize_openapi_type(&param.r#type)
352                            },
353                            "example": param.example
354                        }))
355                        .collect();
356                    operation["parameters"] = json!(params);
357                }
358
359                if let Some(ref request_body) = endpoint.request_body {
360                    let content_type = if request_body.content_type.is_empty() {
361                        "application/json"
362                    } else {
363                        &request_body.content_type
364                    };
365
366                    operation["requestBody"] = json!({
367                        "required": request_body.required,
368                        "content": {
369                            content_type: {
370                                "schema": request_body.schema,
371                                "example": request_body.example
372                            }
373                        }
374                    });
375                }
376
377                let mut responses = json!({});
378                for (status_code, response) in &endpoint.responses {
379                    let resp_content_type = response.content_type.as_deref()
380                        .unwrap_or("application/json");
381
382                    responses[status_code] = json!({
383                        "description": response.description,
384                        "content": {
385                            resp_content_type: {
386                                "schema": response.schema,
387                                "example": response.example
388                            }
389                        }
390                    });
391                }
392                operation["responses"] = responses;
393
394                paths[&path_key][&method_key] = operation;
395            }
396        }
397
398        openapi["paths"] = paths;
399        Ok(openapi)
400    }
401
402    pub async fn get_openapi_yaml(&mut self) -> anyhow::Result<String> {
403        let openapi_map = self.get_openapi_json().await?;
404        serde_yaml::to_string(&openapi_map)
405            .map_err(|e| anyhow::anyhow!("Failed to serialize to YAML: {}", e))
406    }
407
408    pub async fn get_api_context(&mut self) -> anyhow::Result<String> {
409        let openapi_json = self.get_openapi_json().await?;
410        let json_bytes = serde_json::to_string_pretty(&openapi_json)?;
411
412        let base_urls_str = if self.config.base_urls.is_empty() {
413            self.config.base_url.clone()
414        } else {
415            self.config.base_urls.iter()
416                .map(|url| format!("{}: {}", url.name, url.url))
417                .collect::<Vec<_>>()
418                .join(", ")
419        };
420
421        let context = format!(
422            r#"
423=== API SPECIFICATION FOR YOUR REFERENCE ===
424
425API Title: {}
426Version: {}
427Description: {}
428Base URLs: {}
429
430=== COMPLETE OPENAPI JSON SPECIFICATION ===
431{}
432
433=== STRICT INSTRUCTIONS ===
434- ONLY answer programming or API-related questions about the OpenAPI JSON specification above.
435- DO NOT answer questions outside the context of this API or its OpenAPI spec.
436- DO NOT provide information unrelated to the API, its endpoints, or usage.
437- ONLY use the provided OpenAPI JSON as your source of truth.
438- Give code examples, endpoint usage, and parameter details strictly based on the OpenAPI spec.
439- Be precise about required/optional parameters and show real request/response JSON from the spec.
440- DO NOT speculate or invent endpoints, parameters, or behaviors not present in the OpenAPI JSON.
441"#,
442            self.documentation.info.title,
443            self.documentation.info.version,
444            self.documentation.info.description,
445            base_urls_str,
446            json_bytes
447        );
448
449        Ok(context)
450    }
451
452    pub fn router(&self) -> Router {
453        Router::new()
454            .route("/chat", axum::routing::post(Self::serve_chat_post_handler))
455            .route("/chat", axum::routing::options(Self::serve_chat_options_handler))
456            .route("/api-data.json", axum::routing::get(Self::serve_api_data_handler))
457            .route("/openapi.json", axum::routing::get(Self::serve_openapi_handler))
458            .route("/openapi.yaml", axum::routing::get(Self::serve_openapi_yaml_handler))
459            .route("/openapi.yml", axum::routing::get(Self::serve_openapi_yaml_handler))
460            .fallback(Self::serve_react_app_handler)
461            .with_state(Arc::new(self.clone()))
462            .layer(
463                CorsLayer::new()
464                    .allow_origin(tower_http::cors::Any)
465                    .allow_methods([axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::OPTIONS])
466                    .allow_headers(tower_http::cors::Any)
467            )
468            .layer(RequestBodyLimitLayer::new(1024 * 1024)) // 1MB limit
469            .layer(TimeoutLayer::new(Duration::from_secs(30))) // 30s timeout
470    }
471
472
473    async fn serve_chat_post_handler(
474        State(docs): State<Arc<APIDocs>>,
475        body: axum::body::Bytes,
476    ) -> Response {
477        let body_str = String::from_utf8_lossy(&body).to_string();
478        Self::serve_chat_post(State(docs), body_str).await.into_response()
479    }
480
481    async fn serve_chat_options_handler() -> Response {
482        Self::serve_chat_options().await.into_response()
483    }
484
485    async fn serve_api_data_handler(State(docs): State<Arc<APIDocs>>) -> Response {
486        Self::serve_api_data(State(docs)).await.into_response()
487    }
488
489    async fn serve_openapi_handler(State(docs): State<Arc<APIDocs>>) -> Response {
490        Self::serve_openapi(State(docs)).await.into_response()
491    }
492
493    async fn serve_openapi_yaml_handler(State(docs): State<Arc<APIDocs>>) -> Response {
494        Self::serve_openapi_yaml(State(docs)).await.into_response()
495    }
496
497    async fn serve_react_app_handler(State(docs): State<Arc<APIDocs>>) -> Response {
498        Self::serve_react_app(State(docs)).await.into_response()
499    }
500
501
502    async fn serve_react_app(State(docs): State<Arc<APIDocs>>) -> impl IntoResponse {
503        let docs_json = serde_json::to_string(&docs.documentation).unwrap_or_default();
504        let config_json = serde_json::to_string(&docs.config).unwrap_or_default();
505
506        let template = DocsTemplate {
507            title: docs.config.title.clone(),
508            docs_json,
509            config_json,
510        };
511
512        match template.render() {
513            Ok(html) => {
514                let mut response = Html(html).into_response();
515                response.headers_mut().insert("content-type", "text/html; charset=utf-8".parse().unwrap());
516                response
517            }
518            Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Template error: {}", e)).into_response(),
519        }
520    }
521
522    #[allow(dead_code)]
523    async fn serve_asset(State(_docs): State<Arc<APIDocs>>, _path: String) -> impl IntoResponse {
524        // For now, return 404 for assets (matching Go implementation)
525        StatusCode::NOT_FOUND
526    }
527
528    async fn serve_chat_options() -> impl IntoResponse {
529        (
530            StatusCode::OK,
531            [
532                ("access-control-allow-origin", "*"),
533                ("access-control-allow-methods", "POST, OPTIONS"),
534                ("access-control-allow-headers", "Content-Type"),
535            ],
536        )
537    }
538
539    async fn serve_chat_post(State(docs): State<Arc<APIDocs>>, body: String) -> impl IntoResponse {
540        // Parse the chat request from JSON body
541        let chat_request: ChatRequest = match serde_json::from_str(&body) {
542            Ok(req) => req,
543            Err(e) => {
544                let error_response = ChatResponse {
545                    response: "".to_string(),
546                    error: format!("Invalid JSON: {}", e),
547                    provider: "none".to_string(),
548                    model: "".to_string(),
549                    tokens_used: 0,
550                };
551                return (
552                    StatusCode::BAD_REQUEST,
553                    [
554                        ("content-type", "application/json"),
555                        ("access-control-allow-origin", "*"),
556                    ],
557                    serde_json::to_string(&error_response).unwrap_or_default(),
558                ).into_response();
559            }
560        };
561
562        // Check if AI is enabled
563        if docs.llm_client.is_none() {
564            let error_response = ChatResponse {
565                response: "".to_string(),
566                error: "AI chat is not enabled or configured".to_string(),
567                provider: "none".to_string(),
568                model: "".to_string(),
569                tokens_used: 0,
570            };
571            return (
572                StatusCode::SERVICE_UNAVAILABLE,
573                [
574                    ("content-type", "application/json"),
575                    ("access-control-allow-origin", "*"),
576                ],
577                serde_json::to_string(&error_response).unwrap_or_default(),
578            ).into_response();
579        }
580
581        // Validate request
582        if chat_request.message.is_empty() {
583            let error_response = ChatResponse {
584                response: "".to_string(),
585                error: "Message is required".to_string(),
586                provider: docs.llm_client.as_ref().map(|c| c.as_ref().get_provider()).unwrap_or("unknown").to_string(),
587                model: docs.llm_client.as_ref().map(|c| c.as_ref().get_model()).unwrap_or("").to_string(),
588                tokens_used: 0,
589            };
590            return (
591                StatusCode::BAD_REQUEST,
592                [
593                    ("content-type", "application/json"),
594                    ("access-control-allow-origin", "*"),
595                ],
596                serde_json::to_string(&error_response).unwrap_or_default(),
597            ).into_response();
598        }
599
600        // Prepare request with context
601        let mut request = chat_request;
602        if request.context.is_none() {
603            let mut docs_clone = (*docs).clone();
604            if let Ok(context) = docs_clone.get_api_context().await {
605                request.context = Some(context);
606            }
607        }
608
609        // Call LLM
610        if let Some(ref llm_client_arc) = docs.llm_client {
611            match llm_client_arc.chat(request).await {
612                Ok(response) => {
613                    (
614                        StatusCode::OK,
615                        [
616                            ("content-type", "application/json"),
617                            ("access-control-allow-origin", "*"),
618                        ],
619                        serde_json::to_string(&response).unwrap_or_default(),
620                    ).into_response()
621                }
622                Err(e) => {
623                    let error_response = ChatResponse {
624                        response: "".to_string(),
625                        error: format!("LLM error: {}", e),
626                        provider: llm_client_arc.get_provider().to_string(),
627                        model: llm_client_arc.get_model().to_string(),
628                        tokens_used: 0,
629                    };
630                    (
631                        StatusCode::INTERNAL_SERVER_ERROR,
632                        [
633                            ("content-type", "application/json"),
634                            ("access-control-allow-origin", "*"),
635                        ],
636                        serde_json::to_string(&error_response).unwrap_or_default(),
637                    ).into_response()
638                }
639            }
640        } else {
641            let error_response = ChatResponse {
642                response: "".to_string(),
643                error: "LLM client not available".to_string(),
644                provider: "none".to_string(),
645                model: "".to_string(),
646                tokens_used: 0,
647            };
648            (
649                StatusCode::INTERNAL_SERVER_ERROR,
650                [
651                    ("content-type", "application/json"),
652                    ("access-control-allow-origin", "*"),
653                ],
654                serde_json::to_string(&error_response).unwrap_or_default(),
655            ).into_response()
656        }
657    }
658
659    async fn serve_api_data(State(docs): State<Arc<APIDocs>>) -> impl IntoResponse {
660        let mut response = Json(docs.documentation.clone()).into_response();
661        response.headers_mut().insert("content-type", "application/json".parse().unwrap());
662        response.headers_mut().insert("access-control-allow-origin", "*".parse().unwrap());
663        response
664    }
665
666    #[allow(dead_code)]
667    async fn serve_chat(
668        State(docs): State<Arc<APIDocs>>,
669        Json(chat_request): Json<ChatRequest>,
670    ) -> impl IntoResponse {
671        if let Some(ref llm_client) = docs.llm_client {
672            let mut request = chat_request;
673
674            if request.context.is_none() {
675                // Get API context - this would need to be implemented properly
676                let mut docs_clone = (*docs).clone();
677                if let Ok(context) = docs_clone.get_api_context().await {
678                    request.context = Some(context);
679                }
680            }
681
682            match llm_client.chat(request).await {
683                Ok(response) => Json(response),
684                Err(e) => Json(ChatResponse {
685                    response: String::new(),
686                    provider: llm_client.get_provider().to_string(),
687                    model: llm_client.get_model().to_string(),
688                    tokens_used: 0,
689                    error: e.to_string(),
690                }),
691            }
692        } else {
693            Json(ChatResponse {
694                response: String::new(),
695                provider: "none".to_string(),
696                model: "".to_string(),
697                tokens_used: 0,
698                error: "AI chat is not enabled or configured".to_string(),
699            })
700        }
701    }
702
703    async fn serve_openapi(State(docs): State<Arc<APIDocs>>) -> impl IntoResponse {
704        // Clone the docs to avoid mutability issues
705        let mut docs_clone = (*docs).clone();
706        match docs_clone.get_openapi_json().await {
707            Ok(openapi) => {
708                let mut response = Json(openapi).into_response();
709                response.headers_mut().insert("access-control-allow-origin", "*".parse().unwrap());
710                response.headers_mut().insert("content-type", "application/json".parse().unwrap());
711                response
712            }
713            Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
714        }
715    }
716
717    async fn serve_openapi_yaml(State(docs): State<Arc<APIDocs>>) -> impl IntoResponse {
718        // Clone the docs to avoid mutability issues
719        let mut docs_clone = (*docs).clone();
720        match docs_clone.get_openapi_yaml().await {
721            Ok(yaml) => {
722                (
723                    StatusCode::OK,
724                    [
725                        ("content-type", "application/yaml"),
726                        ("access-control-allow-origin", "*")
727                    ],
728                    yaml
729                ).into_response()
730            }
731            Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
732        }
733    }
734}
735
736impl Clone for APIDocs {
737    fn clone(&self) -> Self {
738        Self {
739            config: self.config.clone(),
740            documentation: self.documentation.clone(),
741            routes: self.routes.clone(),
742            schemas: self.schemas.clone(),
743            llm_client: self.llm_client.clone(), // Arc can be cloned easily
744        }
745    }
746}
747
748fn convert_path_to_openapi(path: &str) -> String {
749    let path = if path.starts_with('/') {
750        path.to_string()
751    } else {
752        format!("/{}", path)
753    };
754
755    let parts: Vec<String> = path.split('/').map(|part| {
756        if part.starts_with(':') {
757            format!("{{{}}}", &part[1..])
758        } else {
759            part.to_string()
760        }
761    }).collect();
762
763    let mut result = parts.join("/");
764
765    // Handle other parameter formats
766    result = result.replace('<', "{").replace('>', "}");
767
768    // Handle Gorilla Mux style parameters {param:regex}
769    let mux_regex = Regex::new(r"\{([^{}:]+):[^{}]+\}").unwrap();
770    result = mux_regex.replace_all(&result, "{$1}").to_string();
771
772    // Clean up empty parameters
773    result = result.replace("{}/", "/");
774    if result.starts_with("{}") {
775        result = result.trim_start_matches("{}").to_string();
776    }
777
778    result
779}
780
781fn normalize_openapi_type(go_type: &str) -> &str {
782    match go_type.to_lowercase().as_str() {
783        "int" | "int8" | "int16" | "int32" | "int64" | "uint" | "uint8" | "uint16" | "uint32" | "uint64" => "integer",
784        "float32" | "float64" => "number",
785        "bool" | "boolean" => "boolean",
786        "string" | "" => "string",
787        "array" | "slice" | "[]string" | "[]int" => "array",
788        "object" | "map" | "interface{}" => "object",
789        _ => "string",
790    }
791}
792
793fn extract_path_params(path: &str) -> Vec<String> {
794    let mut params = Vec::new();
795    let parts: Vec<&str> = path.split('/').collect();
796
797    for part in parts {
798        if part.starts_with(':') {
799            params.push(part[1..].to_string());
800        }
801
802        if part.starts_with('{') && part.ends_with('}') {
803            let param = part.trim_matches(|c| c == '{' || c == '}');
804            let param = if param.contains(':') {
805                param.split(':').next().unwrap()
806            } else {
807                param
808            };
809            params.push(param.to_string());
810        }
811    }
812
813    params
814}