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