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