1use reqwest::header::{self, HeaderMap, HeaderValue};
2use reqwest::{Client, Method, RequestBuilder, StatusCode};
3use serde_json::Value;
4use std::collections::HashMap;
5use std::sync::Arc;
6use std::time::Duration;
7use tracing::{debug, error, info, info_span};
8use url::Url;
9
10use crate::error::{
11 Error, NetworkErrorCategory, ToolCallError, ToolCallExecutionError, ToolCallValidationError,
12};
13use crate::tool::ToolMetadata;
14use crate::tool_generator::{ExtractedParameters, QueryParameter, ToolGenerator};
15
16#[derive(Clone)]
18pub struct HttpClient {
19 client: Arc<Client>,
20 base_url: Option<Url>,
21 default_headers: HeaderMap,
22}
23
24impl HttpClient {
25 fn create_user_agent() -> String {
27 format!("rmcp-openapi-server/{}", env!("CARGO_PKG_VERSION"))
28 }
29 #[must_use]
35 pub fn new() -> Self {
36 let user_agent = Self::create_user_agent();
37 let client = Client::builder()
38 .user_agent(&user_agent)
39 .timeout(Duration::from_secs(30))
40 .build()
41 .expect("Failed to create HTTP client");
42
43 Self {
44 client: Arc::new(client),
45 base_url: None,
46 default_headers: HeaderMap::new(),
47 }
48 }
49
50 #[must_use]
56 pub fn with_timeout(timeout_seconds: u64) -> Self {
57 let user_agent = Self::create_user_agent();
58 let client = Client::builder()
59 .user_agent(&user_agent)
60 .timeout(Duration::from_secs(timeout_seconds))
61 .build()
62 .expect("Failed to create HTTP client");
63
64 Self {
65 client: Arc::new(client),
66 base_url: None,
67 default_headers: HeaderMap::new(),
68 }
69 }
70
71 pub fn with_base_url(mut self, base_url: Url) -> Result<Self, Error> {
77 self.base_url = Some(base_url);
78 Ok(self)
79 }
80
81 #[must_use]
83 pub fn with_default_headers(mut self, default_headers: HeaderMap) -> Self {
84 self.default_headers = default_headers;
85 self
86 }
87
88 #[must_use]
93 pub fn with_authorization(&self, auth_value: &str) -> Self {
94 let mut headers = self.default_headers.clone();
95 if let Ok(header_value) = HeaderValue::from_str(auth_value) {
96 headers.insert(header::AUTHORIZATION, header_value);
97 }
98
99 Self {
100 client: self.client.clone(),
101 base_url: self.base_url.clone(),
102 default_headers: headers,
103 }
104 }
105
106 pub async fn execute_tool_call(
112 &self,
113 tool_metadata: &ToolMetadata,
114 arguments: &Value,
115 ) -> Result<HttpResponse, ToolCallError> {
116 let span = info_span!(
117 "http_request",
118 operation_id = %tool_metadata.name,
119 method = %tool_metadata.method,
120 path = %tool_metadata.path
121 );
122 let _enter = span.enter();
123
124 debug!(
125 "Executing tool call: {} {} with arguments: {}",
126 tool_metadata.method,
127 tool_metadata.path,
128 serde_json::to_string_pretty(arguments).unwrap_or_else(|_| "invalid json".to_string())
129 );
130
131 let extracted_params = ToolGenerator::extract_parameters(tool_metadata, arguments)?;
133
134 debug!(
135 "Extracted parameters: path={:?}, query={:?}, headers={:?}, cookies={:?}",
136 extracted_params.path,
137 extracted_params.query,
138 extracted_params.headers,
139 extracted_params.cookies
140 );
141
142 let mut url = self
144 .build_url(tool_metadata, &extracted_params)
145 .map_err(|e| {
146 ToolCallError::Validation(ToolCallValidationError::RequestConstructionError {
147 reason: e.to_string(),
148 })
149 })?;
150
151 if !extracted_params.query.is_empty() {
153 Self::add_query_parameters(&mut url, &extracted_params.query);
154 }
155
156 info!("Final URL: {}", url);
157
158 let mut request = self
160 .create_request(&tool_metadata.method, &url)
161 .map_err(|e| {
162 ToolCallError::Validation(ToolCallValidationError::RequestConstructionError {
163 reason: e.to_string(),
164 })
165 })?;
166
167 if !self.default_headers.is_empty() {
169 request = Self::add_headers_from_map(request, &self.default_headers);
171 }
172
173 if !extracted_params.headers.is_empty() {
175 request = Self::add_headers(request, &extracted_params.headers);
176 }
177
178 if !extracted_params.cookies.is_empty() {
180 request = Self::add_cookies(request, &extracted_params.cookies);
181 }
182
183 if !extracted_params.body.is_empty() {
185 request =
186 Self::add_request_body(request, &extracted_params.body, &extracted_params.config)
187 .map_err(|e| {
188 ToolCallError::Execution(ToolCallExecutionError::ResponseParsingError {
189 reason: format!("Failed to serialize request body: {e}"),
190 raw_response: None,
191 })
192 })?;
193 }
194
195 if extracted_params.config.timeout_seconds != 30 {
197 request = request.timeout(Duration::from_secs(u64::from(
198 extracted_params.config.timeout_seconds,
199 )));
200 }
201
202 let request_body_string = if extracted_params.body.is_empty() {
204 String::new()
205 } else if extracted_params.body.len() == 1
206 && extracted_params.body.contains_key("request_body")
207 {
208 serde_json::to_string(&extracted_params.body["request_body"]).unwrap_or_default()
209 } else {
210 let body_object = Value::Object(
211 extracted_params
212 .body
213 .iter()
214 .map(|(k, v)| (k.clone(), v.clone()))
215 .collect(),
216 );
217 serde_json::to_string(&body_object).unwrap_or_default()
218 };
219
220 let final_url = url.to_string();
222
223 debug!("Sending HTTP request...");
225 let start_time = std::time::Instant::now();
226 let response = request.send().await.map_err(|e| {
227 error!(
228 operation_id = %tool_metadata.name,
229 method = %tool_metadata.method,
230 url = %final_url,
231 error = %e,
232 "HTTP request failed"
233 );
234
235 let (error_msg, category) = if e.is_timeout() {
237 (
238 format!(
239 "Request timeout after {} seconds while calling {} {}",
240 extracted_params.config.timeout_seconds,
241 tool_metadata.method.to_uppercase(),
242 final_url
243 ),
244 NetworkErrorCategory::Timeout,
245 )
246 } else if e.is_connect() {
247 (
248 format!(
249 "Connection failed to {final_url} - Error: {e}. Check if the server is running and the URL is correct."
250 ),
251 NetworkErrorCategory::Connect,
252 )
253 } else if e.is_request() {
254 (
255 format!(
256 "Request error while calling {} {} - Error: {}",
257 tool_metadata.method.to_uppercase(),
258 final_url,
259 e
260 ),
261 NetworkErrorCategory::Request,
262 )
263 } else if e.is_body() {
264 (
265 format!(
266 "Body error while calling {} {} - Error: {}",
267 tool_metadata.method.to_uppercase(),
268 final_url,
269 e
270 ),
271 NetworkErrorCategory::Body,
272 )
273 } else if e.is_decode() {
274 (
275 format!(
276 "Response decode error from {} {} - Error: {}",
277 tool_metadata.method.to_uppercase(),
278 final_url,
279 e
280 ),
281 NetworkErrorCategory::Decode,
282 )
283 } else {
284 (
285 format!(
286 "HTTP request failed: {} (URL: {}, Method: {})",
287 e,
288 final_url,
289 tool_metadata.method.to_uppercase()
290 ),
291 NetworkErrorCategory::Other,
292 )
293 };
294
295 ToolCallError::Execution(ToolCallExecutionError::NetworkError {
296 message: error_msg,
297 category,
298 })
299 })?;
300
301 let elapsed = start_time.elapsed();
302 info!(
303 operation_id = %tool_metadata.name,
304 method = %tool_metadata.method,
305 url = %final_url,
306 status = response.status().as_u16(),
307 elapsed_ms = elapsed.as_millis(),
308 "HTTP request completed"
309 );
310 debug!("Response received with status: {}", response.status());
311
312 self.process_response_with_request(
314 response,
315 &tool_metadata.method,
316 &final_url,
317 &request_body_string,
318 )
319 .await
320 .map_err(|e| {
321 ToolCallError::Execution(ToolCallExecutionError::HttpError {
322 status: 0,
323 message: e.to_string(),
324 details: None,
325 })
326 })
327 }
328
329 fn build_url(
331 &self,
332 tool_metadata: &ToolMetadata,
333 extracted_params: &ExtractedParameters,
334 ) -> Result<Url, Error> {
335 let mut path = tool_metadata.path.clone();
336
337 for (param_name, param_value) in &extracted_params.path {
339 let placeholder = format!("{{{param_name}}}");
340 let value_str = match param_value {
341 Value::String(s) => s.clone(),
342 Value::Number(n) => n.to_string(),
343 Value::Bool(b) => b.to_string(),
344 _ => param_value.to_string(),
345 };
346 path = path.replace(&placeholder, &value_str);
347 }
348
349 if let Some(base_url) = &self.base_url {
351 base_url.join(&path).map_err(|e| {
352 Error::Http(format!(
353 "Failed to join URL '{base_url}' with path '{path}': {e}"
354 ))
355 })
356 } else {
357 if path.starts_with("http") {
359 Url::parse(&path).map_err(|e| Error::Http(format!("Invalid URL '{path}': {e}")))
360 } else {
361 Err(Error::Http(
362 "No base URL configured and path is not a complete URL".to_string(),
363 ))
364 }
365 }
366 }
367
368 fn create_request(&self, method: &str, url: &Url) -> Result<RequestBuilder, Error> {
370 let http_method = method.to_uppercase();
371 let method = match http_method.as_str() {
372 "GET" => Method::GET,
373 "POST" => Method::POST,
374 "PUT" => Method::PUT,
375 "DELETE" => Method::DELETE,
376 "PATCH" => Method::PATCH,
377 "HEAD" => Method::HEAD,
378 "OPTIONS" => Method::OPTIONS,
379 _ => {
380 return Err(Error::Http(format!(
381 "Unsupported HTTP method: {http_method}"
382 )));
383 }
384 };
385
386 Ok(self.client.request(method, url.clone()))
387 }
388
389 fn add_query_parameters(url: &mut Url, query_params: &HashMap<String, QueryParameter>) {
391 {
392 let mut query_pairs = url.query_pairs_mut();
393 for (key, query_param) in query_params {
394 if let Value::Array(arr) = &query_param.value {
395 if query_param.explode {
396 for item in arr {
398 let item_str = match item {
399 Value::String(s) => s.clone(),
400 Value::Number(n) => n.to_string(),
401 Value::Bool(b) => b.to_string(),
402 _ => item.to_string(),
403 };
404 query_pairs.append_pair(key, &item_str);
405 }
406 } else {
407 let array_values: Vec<String> = arr
409 .iter()
410 .map(|item| match item {
411 Value::String(s) => s.clone(),
412 Value::Number(n) => n.to_string(),
413 Value::Bool(b) => b.to_string(),
414 _ => item.to_string(),
415 })
416 .collect();
417 let comma_separated = array_values.join(",");
418 query_pairs.append_pair(key, &comma_separated);
419 }
420 } else {
421 let value_str = match &query_param.value {
422 Value::String(s) => s.clone(),
423 Value::Number(n) => n.to_string(),
424 Value::Bool(b) => b.to_string(),
425 _ => query_param.value.to_string(),
426 };
427 query_pairs.append_pair(key, &value_str);
428 }
429 }
430 }
431 }
432
433 fn add_headers_from_map(mut request: RequestBuilder, headers: &HeaderMap) -> RequestBuilder {
435 for (key, value) in headers {
436 request = request.header(key, value);
438 }
439 request
440 }
441
442 fn add_headers(
444 mut request: RequestBuilder,
445 headers: &HashMap<String, Value>,
446 ) -> RequestBuilder {
447 for (key, value) in headers {
448 let value_str = match value {
449 Value::String(s) => s.clone(),
450 Value::Number(n) => n.to_string(),
451 Value::Bool(b) => b.to_string(),
452 _ => value.to_string(),
453 };
454 request = request.header(key, value_str);
455 }
456 request
457 }
458
459 fn add_cookies(
461 mut request: RequestBuilder,
462 cookies: &HashMap<String, Value>,
463 ) -> RequestBuilder {
464 if !cookies.is_empty() {
465 let cookie_header = cookies
466 .iter()
467 .map(|(key, value)| {
468 let value_str = match value {
469 Value::String(s) => s.clone(),
470 Value::Number(n) => n.to_string(),
471 Value::Bool(b) => b.to_string(),
472 _ => value.to_string(),
473 };
474 format!("{key}={value_str}")
475 })
476 .collect::<Vec<_>>()
477 .join("; ");
478
479 request = request.header(header::COOKIE, cookie_header);
480 }
481 request
482 }
483
484 fn add_request_body(
486 mut request: RequestBuilder,
487 body: &HashMap<String, Value>,
488 config: &crate::tool_generator::RequestConfig,
489 ) -> Result<RequestBuilder, Error> {
490 if body.is_empty() {
491 return Ok(request);
492 }
493
494 request = request.header(header::CONTENT_TYPE, &config.content_type);
496
497 match config.content_type.as_str() {
499 s if s == mime::APPLICATION_JSON.as_ref() => {
500 if body.len() == 1 && body.contains_key("request_body") {
502 let body_value = &body["request_body"];
504 let json_string = serde_json::to_string(body_value).map_err(|e| {
505 Error::Http(format!("Failed to serialize request body: {e}"))
506 })?;
507 request = request.body(json_string);
508 } else {
509 let body_object =
511 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
512 let json_string = serde_json::to_string(&body_object).map_err(|e| {
513 Error::Http(format!("Failed to serialize request body: {e}"))
514 })?;
515 request = request.body(json_string);
516 }
517 }
518 s if s == mime::APPLICATION_WWW_FORM_URLENCODED.as_ref() => {
519 let form_data: Vec<(String, String)> = body
521 .iter()
522 .map(|(key, value)| {
523 let value_str = match value {
524 Value::String(s) => s.clone(),
525 Value::Number(n) => n.to_string(),
526 Value::Bool(b) => b.to_string(),
527 _ => value.to_string(),
528 };
529 (key.clone(), value_str)
530 })
531 .collect();
532 request = request.form(&form_data);
533 }
534 _ => {
535 let body_object =
537 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
538 let json_string = serde_json::to_string(&body_object)
539 .map_err(|e| Error::Http(format!("Failed to serialize request body: {e}")))?;
540 request = request.body(json_string);
541 }
542 }
543
544 Ok(request)
545 }
546
547 async fn process_response_with_request(
549 &self,
550 response: reqwest::Response,
551 method: &str,
552 url: &str,
553 request_body: &str,
554 ) -> Result<HttpResponse, Error> {
555 let status = response.status();
556 let headers = response
557 .headers()
558 .iter()
559 .map(|(name, value)| {
560 (
561 name.to_string(),
562 value.to_str().unwrap_or("<invalid>").to_string(),
563 )
564 })
565 .collect();
566
567 let body = response
568 .text()
569 .await
570 .map_err(|e| Error::Http(format!("Failed to read response body: {e}")))?;
571
572 let is_success = status.is_success();
573 let status_code = status.as_u16();
574 let status_text = status.canonical_reason().unwrap_or("Unknown").to_string();
575
576 let enhanced_status_text = match status {
578 StatusCode::BAD_REQUEST => {
579 format!("{status_text} - Bad Request: Check request parameters")
580 }
581 StatusCode::UNAUTHORIZED => {
582 format!("{status_text} - Unauthorized: Authentication required")
583 }
584 StatusCode::FORBIDDEN => format!("{status_text} - Forbidden: Access denied"),
585 StatusCode::NOT_FOUND => {
586 format!("{status_text} - Not Found: Endpoint or resource does not exist")
587 }
588 StatusCode::METHOD_NOT_ALLOWED => format!(
589 "{} - Method Not Allowed: {} method not supported",
590 status_text,
591 method.to_uppercase()
592 ),
593 StatusCode::UNPROCESSABLE_ENTITY => {
594 format!("{status_text} - Unprocessable Entity: Request validation failed")
595 }
596 StatusCode::TOO_MANY_REQUESTS => {
597 format!("{status_text} - Too Many Requests: Rate limit exceeded")
598 }
599 StatusCode::INTERNAL_SERVER_ERROR => {
600 format!("{status_text} - Internal Server Error: Server encountered an error")
601 }
602 StatusCode::BAD_GATEWAY => {
603 format!("{status_text} - Bad Gateway: Upstream server error")
604 }
605 StatusCode::SERVICE_UNAVAILABLE => {
606 format!("{status_text} - Service Unavailable: Server temporarily unavailable")
607 }
608 StatusCode::GATEWAY_TIMEOUT => {
609 format!("{status_text} - Gateway Timeout: Upstream server timeout")
610 }
611 _ => status_text,
612 };
613
614 Ok(HttpResponse {
615 status_code,
616 status_text: enhanced_status_text,
617 headers,
618 body,
619 is_success,
620 request_method: method.to_string(),
621 request_url: url.to_string(),
622 request_body: request_body.to_string(),
623 })
624 }
625}
626
627impl Default for HttpClient {
628 fn default() -> Self {
629 Self::new()
630 }
631}
632
633#[derive(Debug, Clone)]
635pub struct HttpResponse {
636 pub status_code: u16,
637 pub status_text: String,
638 pub headers: HashMap<String, String>,
639 pub body: String,
640 pub is_success: bool,
641 pub request_method: String,
642 pub request_url: String,
643 pub request_body: String,
644}
645
646impl HttpResponse {
647 pub fn json(&self) -> Result<Value, Error> {
653 serde_json::from_str(&self.body)
654 .map_err(|e| Error::Http(format!("Failed to parse response as JSON: {e}")))
655 }
656
657 #[must_use]
659 pub fn to_mcp_content(&self) -> String {
660 let method = if self.request_method.is_empty() {
661 None
662 } else {
663 Some(self.request_method.as_str())
664 };
665 let url = if self.request_url.is_empty() {
666 None
667 } else {
668 Some(self.request_url.as_str())
669 };
670 let body = if self.request_body.is_empty() {
671 None
672 } else {
673 Some(self.request_body.as_str())
674 };
675 self.to_mcp_content_with_request(method, url, body)
676 }
677
678 pub fn to_mcp_content_with_request(
680 &self,
681 method: Option<&str>,
682 url: Option<&str>,
683 request_body: Option<&str>,
684 ) -> String {
685 let mut result = format!(
686 "HTTP {} {}\n\nStatus: {} {}\n",
687 if self.is_success { "✅" } else { "❌" },
688 if self.is_success { "Success" } else { "Error" },
689 self.status_code,
690 self.status_text
691 );
692
693 if let (Some(method), Some(url)) = (method, url) {
695 result.push_str("\nRequest: ");
696 result.push_str(&method.to_uppercase());
697 result.push(' ');
698 result.push_str(url);
699 result.push('\n');
700
701 if let Some(body) = request_body
702 && !body.is_empty()
703 && body != "{}"
704 {
705 result.push_str("\nRequest Body:\n");
706 if let Ok(parsed) = serde_json::from_str::<Value>(body) {
707 if let Ok(pretty) = serde_json::to_string_pretty(&parsed) {
708 result.push_str(&pretty);
709 } else {
710 result.push_str(body);
711 }
712 } else {
713 result.push_str(body);
714 }
715 result.push('\n');
716 }
717 }
718
719 if !self.headers.is_empty() {
721 result.push_str("\nHeaders:\n");
722 for (key, value) in &self.headers {
723 if [
725 header::CONTENT_TYPE.as_str(),
726 header::CONTENT_LENGTH.as_str(),
727 header::LOCATION.as_str(),
728 header::SET_COOKIE.as_str(),
729 ]
730 .iter()
731 .any(|&h| key.to_lowercase().contains(h))
732 {
733 result.push_str(" ");
734 result.push_str(key);
735 result.push_str(": ");
736 result.push_str(value);
737 result.push('\n');
738 }
739 }
740 }
741
742 result.push_str("\nResponse Body:\n");
744 if self.body.is_empty() {
745 result.push_str("(empty)");
746 } else if let Ok(json_value) = self.json() {
747 match serde_json::to_string_pretty(&json_value) {
749 Ok(pretty) => result.push_str(&pretty),
750 Err(_) => result.push_str(&self.body),
751 }
752 } else {
753 if self.body.len() > 2000 {
755 result.push_str(&self.body[..2000]);
756 result.push_str("\n... (");
757 result.push_str(&(self.body.len() - 2000).to_string());
758 result.push_str(" more characters)");
759 } else {
760 result.push_str(&self.body);
761 }
762 }
763
764 result
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771 use crate::tool_generator::ExtractedParameters;
772 use serde_json::json;
773 use std::collections::HashMap;
774
775 #[test]
776 fn test_with_base_url_validation() {
777 let url = Url::parse("https://api.example.com").unwrap();
779 let client = HttpClient::new().with_base_url(url);
780 assert!(client.is_ok());
781
782 let url = Url::parse("http://localhost:8080").unwrap();
783 let client = HttpClient::new().with_base_url(url);
784 assert!(client.is_ok());
785
786 assert!(Url::parse("not-a-url").is_err());
788 assert!(Url::parse("").is_err());
789
790 let url = Url::parse("ftp://invalid-scheme.com").unwrap();
792 let client = HttpClient::new().with_base_url(url);
793 assert!(client.is_ok()); }
795
796 #[test]
797 fn test_build_url_with_base_url() {
798 let base_url = Url::parse("https://api.example.com").unwrap();
799 let client = HttpClient::new().with_base_url(base_url).unwrap();
800
801 let tool_metadata = crate::ToolMetadata {
802 name: "test".to_string(),
803 title: None,
804 description: "test".to_string(),
805 parameters: json!({}),
806 output_schema: None,
807 method: "GET".to_string(),
808 path: "/pets/{id}".to_string(),
809 security: None,
810 };
811
812 let mut path_params = HashMap::new();
813 path_params.insert("id".to_string(), json!(123));
814
815 let extracted_params = ExtractedParameters {
816 path: path_params,
817 query: HashMap::new(),
818 headers: HashMap::new(),
819 cookies: HashMap::new(),
820 body: HashMap::new(),
821 config: crate::tool_generator::RequestConfig::default(),
822 };
823
824 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
825 assert_eq!(url.to_string(), "https://api.example.com/pets/123");
826 }
827
828 #[test]
829 fn test_build_url_without_base_url() {
830 let client = HttpClient::new();
831
832 let tool_metadata = crate::ToolMetadata {
833 name: "test".to_string(),
834 title: None,
835 description: "test".to_string(),
836 parameters: json!({}),
837 output_schema: None,
838 method: "GET".to_string(),
839 path: "https://api.example.com/pets/123".to_string(),
840 security: None,
841 };
842
843 let extracted_params = ExtractedParameters {
844 path: HashMap::new(),
845 query: HashMap::new(),
846 headers: HashMap::new(),
847 cookies: HashMap::new(),
848 body: HashMap::new(),
849 config: crate::tool_generator::RequestConfig::default(),
850 };
851
852 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
853 assert_eq!(url.to_string(), "https://api.example.com/pets/123");
854
855 let tool_metadata_relative = crate::ToolMetadata {
857 name: "test".to_string(),
858 title: None,
859 description: "test".to_string(),
860 parameters: json!({}),
861 output_schema: None,
862 method: "GET".to_string(),
863 path: "/pets/123".to_string(),
864 security: None,
865 };
866
867 let result = client.build_url(&tool_metadata_relative, &extracted_params);
868 assert!(result.is_err());
869 assert!(
870 result
871 .unwrap_err()
872 .to_string()
873 .contains("No base URL configured")
874 );
875 }
876
877 #[test]
878 fn test_query_parameter_encoding_integration() {
879 let base_url = Url::parse("https://api.example.com").unwrap();
880 let client = HttpClient::new().with_base_url(base_url).unwrap();
881
882 let tool_metadata = crate::ToolMetadata {
883 name: "test".to_string(),
884 title: None,
885 description: "test".to_string(),
886 parameters: json!({}),
887 output_schema: None,
888 method: "GET".to_string(),
889 path: "/search".to_string(),
890 security: None,
891 };
892
893 let mut query_params = HashMap::new();
895 query_params.insert(
896 "q".to_string(),
897 QueryParameter::new(json!("hello world"), true),
898 ); query_params.insert(
900 "category".to_string(),
901 QueryParameter::new(json!("pets&dogs"), true),
902 ); query_params.insert(
904 "special".to_string(),
905 QueryParameter::new(json!("foo=bar"), true),
906 ); query_params.insert(
908 "unicode".to_string(),
909 QueryParameter::new(json!("café"), true),
910 ); query_params.insert(
912 "percent".to_string(),
913 QueryParameter::new(json!("100%"), true),
914 ); let extracted_params = ExtractedParameters {
917 path: HashMap::new(),
918 query: query_params,
919 headers: HashMap::new(),
920 cookies: HashMap::new(),
921 body: HashMap::new(),
922 config: crate::tool_generator::RequestConfig::default(),
923 };
924
925 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
926 HttpClient::add_query_parameters(&mut url, &extracted_params.query);
927
928 let url_string = url.to_string();
929
930 assert!(url_string.contains("q=hello+world")); assert!(url_string.contains("category=pets%26dogs")); assert!(url_string.contains("special=foo%3Dbar")); assert!(url_string.contains("unicode=caf%C3%A9")); assert!(url_string.contains("percent=100%25")); }
938
939 #[test]
940 fn test_array_query_parameters() {
941 let base_url = Url::parse("https://api.example.com").unwrap();
942 let client = HttpClient::new().with_base_url(base_url).unwrap();
943
944 let tool_metadata = crate::ToolMetadata {
945 name: "test".to_string(),
946 title: None,
947 description: "test".to_string(),
948 parameters: json!({}),
949 output_schema: None,
950 method: "GET".to_string(),
951 path: "/search".to_string(),
952 security: None,
953 };
954
955 let mut query_params = HashMap::new();
956 query_params.insert(
957 "status".to_string(),
958 QueryParameter::new(json!(["available", "pending"]), true),
959 );
960 query_params.insert(
961 "tags".to_string(),
962 QueryParameter::new(json!(["red & blue", "fast=car"]), true),
963 );
964
965 let extracted_params = ExtractedParameters {
966 path: HashMap::new(),
967 query: query_params,
968 headers: HashMap::new(),
969 cookies: HashMap::new(),
970 body: HashMap::new(),
971 config: crate::tool_generator::RequestConfig::default(),
972 };
973
974 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
975 HttpClient::add_query_parameters(&mut url, &extracted_params.query);
976
977 let url_string = url.to_string();
978
979 assert!(url_string.contains("status=available"));
981 assert!(url_string.contains("status=pending"));
982 assert!(url_string.contains("tags=red+%26+blue")); assert!(url_string.contains("tags=fast%3Dcar")); }
985
986 #[test]
987 fn test_path_parameter_substitution() {
988 let base_url = Url::parse("https://api.example.com").unwrap();
989 let client = HttpClient::new().with_base_url(base_url).unwrap();
990
991 let tool_metadata = crate::ToolMetadata {
992 name: "test".to_string(),
993 title: None,
994 description: "test".to_string(),
995 parameters: json!({}),
996 output_schema: None,
997 method: "GET".to_string(),
998 path: "/users/{userId}/pets/{petId}".to_string(),
999 security: None,
1000 };
1001
1002 let mut path_params = HashMap::new();
1003 path_params.insert("userId".to_string(), json!(42));
1004 path_params.insert("petId".to_string(), json!("special-pet-123"));
1005
1006 let extracted_params = ExtractedParameters {
1007 path: path_params,
1008 query: HashMap::new(),
1009 headers: HashMap::new(),
1010 cookies: HashMap::new(),
1011 body: HashMap::new(),
1012 config: crate::tool_generator::RequestConfig::default(),
1013 };
1014
1015 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1016 assert_eq!(
1017 url.to_string(),
1018 "https://api.example.com/users/42/pets/special-pet-123"
1019 );
1020 }
1021
1022 #[test]
1023 fn test_url_join_edge_cases() {
1024 let base_url1 = Url::parse("https://api.example.com/").unwrap();
1026 let client1 = HttpClient::new().with_base_url(base_url1).unwrap();
1027
1028 let base_url2 = Url::parse("https://api.example.com").unwrap();
1029 let client2 = HttpClient::new().with_base_url(base_url2).unwrap();
1030
1031 let tool_metadata = crate::ToolMetadata {
1032 name: "test".to_string(),
1033 title: None,
1034 description: "test".to_string(),
1035 parameters: json!({}),
1036 output_schema: None,
1037 method: "GET".to_string(),
1038 path: "/pets".to_string(),
1039 security: None,
1040 };
1041
1042 let extracted_params = ExtractedParameters {
1043 path: HashMap::new(),
1044 query: HashMap::new(),
1045 headers: HashMap::new(),
1046 cookies: HashMap::new(),
1047 body: HashMap::new(),
1048 config: crate::tool_generator::RequestConfig::default(),
1049 };
1050
1051 let url1 = client1
1052 .build_url(&tool_metadata, &extracted_params)
1053 .unwrap();
1054 let url2 = client2
1055 .build_url(&tool_metadata, &extracted_params)
1056 .unwrap();
1057
1058 assert_eq!(url1.to_string(), "https://api.example.com/pets");
1060 assert_eq!(url2.to_string(), "https://api.example.com/pets");
1061 }
1062
1063 #[test]
1064 fn test_explode_array_parameters() {
1065 let base_url = Url::parse("https://api.example.com").unwrap();
1066 let client = HttpClient::new().with_base_url(base_url).unwrap();
1067
1068 let tool_metadata = crate::ToolMetadata {
1069 name: "test".to_string(),
1070 title: None,
1071 description: "test".to_string(),
1072 parameters: json!({}),
1073 output_schema: None,
1074 method: "GET".to_string(),
1075 path: "/search".to_string(),
1076 security: None,
1077 };
1078
1079 let mut query_params_exploded = HashMap::new();
1081 query_params_exploded.insert(
1082 "include".to_string(),
1083 QueryParameter::new(json!(["asset", "scenes"]), true),
1084 );
1085
1086 let extracted_params_exploded = ExtractedParameters {
1087 path: HashMap::new(),
1088 query: query_params_exploded,
1089 headers: HashMap::new(),
1090 cookies: HashMap::new(),
1091 body: HashMap::new(),
1092 config: crate::tool_generator::RequestConfig::default(),
1093 };
1094
1095 let mut url_exploded = client
1096 .build_url(&tool_metadata, &extracted_params_exploded)
1097 .unwrap();
1098 HttpClient::add_query_parameters(&mut url_exploded, &extracted_params_exploded.query);
1099 let url_exploded_string = url_exploded.to_string();
1100
1101 let mut query_params_not_exploded = HashMap::new();
1103 query_params_not_exploded.insert(
1104 "include".to_string(),
1105 QueryParameter::new(json!(["asset", "scenes"]), false),
1106 );
1107
1108 let extracted_params_not_exploded = ExtractedParameters {
1109 path: HashMap::new(),
1110 query: query_params_not_exploded,
1111 headers: HashMap::new(),
1112 cookies: HashMap::new(),
1113 body: HashMap::new(),
1114 config: crate::tool_generator::RequestConfig::default(),
1115 };
1116
1117 let mut url_not_exploded = client
1118 .build_url(&tool_metadata, &extracted_params_not_exploded)
1119 .unwrap();
1120 HttpClient::add_query_parameters(
1121 &mut url_not_exploded,
1122 &extracted_params_not_exploded.query,
1123 );
1124 let url_not_exploded_string = url_not_exploded.to_string();
1125
1126 assert!(url_exploded_string.contains("include=asset"));
1128 assert!(url_exploded_string.contains("include=scenes"));
1129
1130 assert!(url_not_exploded_string.contains("include=asset%2Cscenes")); assert_ne!(url_exploded_string, url_not_exploded_string);
1135
1136 println!("Exploded URL: {url_exploded_string}");
1137 println!("Non-exploded URL: {url_not_exploded_string}");
1138 }
1139}