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 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(§ion_name) {
127 sections.insert(section_name.clone(), EndpointSection {
128 id: section_name.clone(),
129 name: self.format_section_name(§ion_name),
130 description: format!("{} related endpoints", self.format_section_name(§ion_name)),
131 endpoints: Vec::new(),
132 });
133 }
134
135 if let Some(section) = sections.get_mut(§ion_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, }
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 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 let mut paths = json!({});
326 for section in &self.documentation.endpoints {
327 for endpoint in §ion.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(¶m.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)) .layer(TimeoutLayer::new(Duration::from_secs(30))) }
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 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 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 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 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 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 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 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 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 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(), }
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 result = result.replace('<', "{").replace('>', "}");
767
768 let mux_regex = Regex::new(r"\{([^{}:]+):[^{}]+\}").unwrap();
770 result = mux_regex.replace_all(&result, "{$1}").to_string();
771
772 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}