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, tool_metadata);
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, tool_metadata);
158 }
159
160 if !extracted_params.cookies.is_empty() {
162 request = Self::add_cookies(request, &extracted_params.cookies, tool_metadata);
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(
373 url: &mut Url,
374 query_params: &HashMap<String, QueryParameter>,
375 tool_metadata: &ToolMetadata,
376 ) {
377 {
378 let mut query_pairs = url.query_pairs_mut();
379 for (key, query_param) in query_params {
380 if tool_metadata.should_omit_empty_array_parameter(key, &query_param.value) {
382 continue;
383 }
384 if let Value::Array(arr) = &query_param.value {
385 if arr.is_empty() {
386 query_pairs.append_pair(key, "");
389 } else if query_param.explode {
390 for item in arr {
392 let item_str = 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 query_pairs.append_pair(key, &item_str);
399 }
400 } else {
401 let array_values: Vec<String> = arr
403 .iter()
404 .map(|item| match item {
405 Value::String(s) => s.clone(),
406 Value::Number(n) => n.to_string(),
407 Value::Bool(b) => b.to_string(),
408 _ => item.to_string(),
409 })
410 .collect();
411 let comma_separated = array_values.join(",");
412 query_pairs.append_pair(key, &comma_separated);
413 }
414 } else {
415 let value_str = match &query_param.value {
416 Value::String(s) => s.clone(),
417 Value::Number(n) => n.to_string(),
418 Value::Bool(b) => b.to_string(),
419 _ => query_param.value.to_string(),
420 };
421 query_pairs.append_pair(key, &value_str);
422 }
423 }
424 }
425 }
426
427 fn add_headers_from_map(mut request: RequestBuilder, headers: &HeaderMap) -> RequestBuilder {
429 for (key, value) in headers {
430 request = request.header(key, value);
432 }
433 request
434 }
435
436 fn add_headers(
438 mut request: RequestBuilder,
439 headers: &HashMap<String, Value>,
440 tool_metadata: &ToolMetadata,
441 ) -> RequestBuilder {
442 for (key, value) in headers {
443 if tool_metadata.should_omit_empty_array_parameter(key, value) {
445 continue;
446 }
447 let value_str = match value {
448 Value::String(s) => s.clone(),
449 Value::Number(n) => n.to_string(),
450 Value::Bool(b) => b.to_string(),
451 _ => value.to_string(),
452 };
453 request = request.header(key, value_str);
454 }
455 request
456 }
457
458 fn add_cookies(
460 mut request: RequestBuilder,
461 cookies: &HashMap<String, Value>,
462 tool_metadata: &ToolMetadata,
463 ) -> RequestBuilder {
464 if !cookies.is_empty() {
465 let cookie_header = cookies
466 .iter()
467 .filter(|(key, value)| {
468 !tool_metadata.should_omit_empty_array_parameter(key, value)
470 })
471 .map(|(key, value)| {
472 let value_str = match value {
473 Value::String(s) => s.clone(),
474 Value::Number(n) => n.to_string(),
475 Value::Bool(b) => b.to_string(),
476 _ => value.to_string(),
477 };
478 format!("{key}={value_str}")
479 })
480 .collect::<Vec<_>>()
481 .join("; ");
482
483 if !cookie_header.is_empty() {
484 request = request.header(header::COOKIE, cookie_header);
485 }
486 }
487 request
488 }
489
490 fn add_request_body(
492 mut request: RequestBuilder,
493 body: &HashMap<String, Value>,
494 config: &crate::tool_generator::RequestConfig,
495 ) -> Result<RequestBuilder, Error> {
496 if body.is_empty() {
497 return Ok(request);
498 }
499
500 request = request.header(header::CONTENT_TYPE, &config.content_type);
502
503 match config.content_type.as_str() {
505 s if s == mime::APPLICATION_JSON.as_ref() => {
506 if body.len() == 1 && body.contains_key("request_body") {
508 let body_value = &body["request_body"];
510 let json_string = serde_json::to_string(body_value).map_err(|e| {
511 Error::Http(format!("Failed to serialize request body: {e}"))
512 })?;
513 request = request.body(json_string);
514 } else {
515 let body_object =
517 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
518 let json_string = serde_json::to_string(&body_object).map_err(|e| {
519 Error::Http(format!("Failed to serialize request body: {e}"))
520 })?;
521 request = request.body(json_string);
522 }
523 }
524 s if s == mime::APPLICATION_WWW_FORM_URLENCODED.as_ref() => {
525 let form_data: Vec<(String, String)> = body
527 .iter()
528 .map(|(key, value)| {
529 let value_str = match value {
530 Value::String(s) => s.clone(),
531 Value::Number(n) => n.to_string(),
532 Value::Bool(b) => b.to_string(),
533 _ => value.to_string(),
534 };
535 (key.clone(), value_str)
536 })
537 .collect();
538 request = request.form(&form_data);
539 }
540 _ => {
541 let body_object =
543 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
544 let json_string = serde_json::to_string(&body_object)
545 .map_err(|e| Error::Http(format!("Failed to serialize request body: {e}")))?;
546 request = request.body(json_string);
547 }
548 }
549
550 Ok(request)
551 }
552
553 async fn process_response_with_request(
555 &self,
556 response: reqwest::Response,
557 method: &str,
558 url: &str,
559 request_body: &str,
560 ) -> Result<HttpResponse, Error> {
561 let status = response.status();
562 let headers = response
563 .headers()
564 .iter()
565 .map(|(name, value)| {
566 (
567 name.to_string(),
568 value.to_str().unwrap_or("<invalid>").to_string(),
569 )
570 })
571 .collect();
572
573 let body = response
574 .text()
575 .await
576 .map_err(|e| Error::Http(format!("Failed to read response body: {e}")))?;
577
578 let is_success = status.is_success();
579 let status_code = status.as_u16();
580 let status_text = status.canonical_reason().unwrap_or("Unknown").to_string();
581
582 let enhanced_status_text = match status {
584 StatusCode::BAD_REQUEST => {
585 format!("{status_text} - Bad Request: Check request parameters")
586 }
587 StatusCode::UNAUTHORIZED => {
588 format!("{status_text} - Unauthorized: Authentication required")
589 }
590 StatusCode::FORBIDDEN => format!("{status_text} - Forbidden: Access denied"),
591 StatusCode::NOT_FOUND => {
592 format!("{status_text} - Not Found: Endpoint or resource does not exist")
593 }
594 StatusCode::METHOD_NOT_ALLOWED => format!(
595 "{} - Method Not Allowed: {} method not supported",
596 status_text,
597 method.to_uppercase()
598 ),
599 StatusCode::UNPROCESSABLE_ENTITY => {
600 format!("{status_text} - Unprocessable Entity: Request validation failed")
601 }
602 StatusCode::TOO_MANY_REQUESTS => {
603 format!("{status_text} - Too Many Requests: Rate limit exceeded")
604 }
605 StatusCode::INTERNAL_SERVER_ERROR => {
606 format!("{status_text} - Internal Server Error: Server encountered an error")
607 }
608 StatusCode::BAD_GATEWAY => {
609 format!("{status_text} - Bad Gateway: Upstream server error")
610 }
611 StatusCode::SERVICE_UNAVAILABLE => {
612 format!("{status_text} - Service Unavailable: Server temporarily unavailable")
613 }
614 StatusCode::GATEWAY_TIMEOUT => {
615 format!("{status_text} - Gateway Timeout: Upstream server timeout")
616 }
617 _ => status_text,
618 };
619
620 Ok(HttpResponse {
621 status_code,
622 status_text: enhanced_status_text,
623 headers,
624 body,
625 is_success,
626 request_method: method.to_string(),
627 request_url: url.to_string(),
628 request_body: request_body.to_string(),
629 })
630 }
631}
632
633impl Default for HttpClient {
634 fn default() -> Self {
635 Self::new()
636 }
637}
638
639#[derive(Debug, Clone)]
641pub struct HttpResponse {
642 pub status_code: u16,
643 pub status_text: String,
644 pub headers: HashMap<String, String>,
645 pub body: String,
646 pub is_success: bool,
647 pub request_method: String,
648 pub request_url: String,
649 pub request_body: String,
650}
651
652impl HttpResponse {
653 pub fn json(&self) -> Result<Value, Error> {
659 serde_json::from_str(&self.body)
660 .map_err(|e| Error::Http(format!("Failed to parse response as JSON: {e}")))
661 }
662
663 #[must_use]
665 pub fn to_mcp_content(&self) -> String {
666 let method = if self.request_method.is_empty() {
667 None
668 } else {
669 Some(self.request_method.as_str())
670 };
671 let url = if self.request_url.is_empty() {
672 None
673 } else {
674 Some(self.request_url.as_str())
675 };
676 let body = if self.request_body.is_empty() {
677 None
678 } else {
679 Some(self.request_body.as_str())
680 };
681 self.to_mcp_content_with_request(method, url, body)
682 }
683
684 pub fn to_mcp_content_with_request(
686 &self,
687 method: Option<&str>,
688 url: Option<&str>,
689 request_body: Option<&str>,
690 ) -> String {
691 let mut result = format!(
692 "HTTP {} {}\n\nStatus: {} {}\n",
693 if self.is_success { "✅" } else { "❌" },
694 if self.is_success { "Success" } else { "Error" },
695 self.status_code,
696 self.status_text
697 );
698
699 if let (Some(method), Some(url)) = (method, url) {
701 result.push_str("\nRequest: ");
702 result.push_str(&method.to_uppercase());
703 result.push(' ');
704 result.push_str(url);
705 result.push('\n');
706
707 if let Some(body) = request_body
708 && !body.is_empty()
709 && body != "{}"
710 {
711 result.push_str("\nRequest Body:\n");
712 if let Ok(parsed) = serde_json::from_str::<Value>(body) {
713 if let Ok(pretty) = serde_json::to_string_pretty(&parsed) {
714 result.push_str(&pretty);
715 } else {
716 result.push_str(body);
717 }
718 } else {
719 result.push_str(body);
720 }
721 result.push('\n');
722 }
723 }
724
725 if !self.headers.is_empty() {
727 result.push_str("\nHeaders:\n");
728 for (key, value) in &self.headers {
729 if [
731 header::CONTENT_TYPE.as_str(),
732 header::CONTENT_LENGTH.as_str(),
733 header::LOCATION.as_str(),
734 header::SET_COOKIE.as_str(),
735 ]
736 .iter()
737 .any(|&h| key.to_lowercase().contains(h))
738 {
739 result.push_str(" ");
740 result.push_str(key);
741 result.push_str(": ");
742 result.push_str(value);
743 result.push('\n');
744 }
745 }
746 }
747
748 result.push_str("\nResponse Body:\n");
750 if self.body.is_empty() {
751 result.push_str("(empty)");
752 } else if let Ok(json_value) = self.json() {
753 match serde_json::to_string_pretty(&json_value) {
755 Ok(pretty) => result.push_str(&pretty),
756 Err(_) => result.push_str(&self.body),
757 }
758 } else {
759 if self.body.len() > 2000 {
761 result.push_str(&self.body[..2000]);
762 result.push_str("\n... (");
763 result.push_str(&(self.body.len() - 2000).to_string());
764 result.push_str(" more characters)");
765 } else {
766 result.push_str(&self.body);
767 }
768 }
769
770 result
771 }
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777 use crate::tool_generator::ExtractedParameters;
778 use serde_json::json;
779 use std::collections::HashMap;
780
781 #[test]
782 fn test_with_base_url_validation() {
783 let url = Url::parse("https://api.example.com").unwrap();
785 let client = HttpClient::new().with_base_url(url);
786 assert!(client.is_ok());
787
788 let url = Url::parse("http://localhost:8080").unwrap();
789 let client = HttpClient::new().with_base_url(url);
790 assert!(client.is_ok());
791
792 assert!(Url::parse("not-a-url").is_err());
794 assert!(Url::parse("").is_err());
795
796 let url = Url::parse("ftp://invalid-scheme.com").unwrap();
798 let client = HttpClient::new().with_base_url(url);
799 assert!(client.is_ok()); }
801
802 #[test]
803 fn test_build_url_with_base_url() {
804 let base_url = Url::parse("https://api.example.com").unwrap();
805 let client = HttpClient::new().with_base_url(base_url).unwrap();
806
807 let tool_metadata = crate::ToolMetadata {
808 name: "test".to_string(),
809 title: None,
810 description: "test".to_string(),
811 parameters: json!({}),
812 output_schema: None,
813 method: "GET".to_string(),
814 path: "/pets/{id}".to_string(),
815 };
816
817 let mut path_params = HashMap::new();
818 path_params.insert("id".to_string(), json!(123));
819
820 let extracted_params = ExtractedParameters {
821 path: path_params,
822 query: HashMap::new(),
823 headers: HashMap::new(),
824 cookies: HashMap::new(),
825 body: HashMap::new(),
826 config: crate::tool_generator::RequestConfig::default(),
827 };
828
829 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
830 assert_eq!(url.to_string(), "https://api.example.com/pets/123");
831 }
832
833 #[test]
834 fn test_build_url_without_base_url() {
835 let client = HttpClient::new();
836
837 let tool_metadata = crate::ToolMetadata {
838 name: "test".to_string(),
839 title: None,
840 description: "test".to_string(),
841 parameters: json!({}),
842 output_schema: None,
843 method: "GET".to_string(),
844 path: "https://api.example.com/pets/123".to_string(),
845 };
846
847 let extracted_params = ExtractedParameters {
848 path: HashMap::new(),
849 query: HashMap::new(),
850 headers: HashMap::new(),
851 cookies: HashMap::new(),
852 body: HashMap::new(),
853 config: crate::tool_generator::RequestConfig::default(),
854 };
855
856 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
857 assert_eq!(url.to_string(), "https://api.example.com/pets/123");
858
859 let tool_metadata_relative = crate::ToolMetadata {
861 name: "test".to_string(),
862 title: None,
863 description: "test".to_string(),
864 parameters: json!({}),
865 output_schema: None,
866 method: "GET".to_string(),
867 path: "/pets/123".to_string(),
868 };
869
870 let result = client.build_url(&tool_metadata_relative, &extracted_params);
871 assert!(result.is_err());
872 assert!(
873 result
874 .unwrap_err()
875 .to_string()
876 .contains("No base URL configured")
877 );
878 }
879
880 #[test]
881 fn test_query_parameter_encoding_integration() {
882 let base_url = Url::parse("https://api.example.com").unwrap();
883 let client = HttpClient::new().with_base_url(base_url).unwrap();
884
885 let tool_metadata = crate::ToolMetadata {
886 name: "test".to_string(),
887 title: None,
888 description: "test".to_string(),
889 parameters: json!({}),
890 output_schema: None,
891 method: "GET".to_string(),
892 path: "/search".to_string(),
893 };
894
895 let mut query_params = HashMap::new();
897 query_params.insert(
898 "q".to_string(),
899 QueryParameter::new(json!("hello world"), true),
900 ); query_params.insert(
902 "category".to_string(),
903 QueryParameter::new(json!("pets&dogs"), true),
904 ); query_params.insert(
906 "special".to_string(),
907 QueryParameter::new(json!("foo=bar"), true),
908 ); query_params.insert(
910 "unicode".to_string(),
911 QueryParameter::new(json!("café"), true),
912 ); query_params.insert(
914 "percent".to_string(),
915 QueryParameter::new(json!("100%"), true),
916 ); let extracted_params = ExtractedParameters {
919 path: HashMap::new(),
920 query: query_params,
921 headers: HashMap::new(),
922 cookies: HashMap::new(),
923 body: HashMap::new(),
924 config: crate::tool_generator::RequestConfig::default(),
925 };
926
927 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
928 HttpClient::add_query_parameters(&mut url, &extracted_params.query, &tool_metadata);
929
930 let url_string = url.to_string();
931
932 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")); }
940
941 #[test]
942 fn test_array_query_parameters() {
943 let base_url = Url::parse("https://api.example.com").unwrap();
944 let client = HttpClient::new().with_base_url(base_url).unwrap();
945
946 let tool_metadata = crate::ToolMetadata {
947 name: "test".to_string(),
948 title: None,
949 description: "test".to_string(),
950 parameters: json!({}),
951 output_schema: None,
952 method: "GET".to_string(),
953 path: "/search".to_string(),
954 };
955
956 let mut query_params = HashMap::new();
957 query_params.insert(
958 "status".to_string(),
959 QueryParameter::new(json!(["available", "pending"]), true),
960 );
961 query_params.insert(
962 "tags".to_string(),
963 QueryParameter::new(json!(["red & blue", "fast=car"]), true),
964 );
965
966 let extracted_params = ExtractedParameters {
967 path: HashMap::new(),
968 query: query_params,
969 headers: HashMap::new(),
970 cookies: HashMap::new(),
971 body: HashMap::new(),
972 config: crate::tool_generator::RequestConfig::default(),
973 };
974
975 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
976 HttpClient::add_query_parameters(&mut url, &extracted_params.query, &tool_metadata);
977
978 let url_string = url.to_string();
979
980 assert!(url_string.contains("status=available"));
982 assert!(url_string.contains("status=pending"));
983 assert!(url_string.contains("tags=red+%26+blue")); assert!(url_string.contains("tags=fast%3Dcar")); }
986
987 #[test]
988 fn test_path_parameter_substitution() {
989 let base_url = Url::parse("https://api.example.com").unwrap();
990 let client = HttpClient::new().with_base_url(base_url).unwrap();
991
992 let tool_metadata = crate::ToolMetadata {
993 name: "test".to_string(),
994 title: None,
995 description: "test".to_string(),
996 parameters: json!({}),
997 output_schema: None,
998 method: "GET".to_string(),
999 path: "/users/{userId}/pets/{petId}".to_string(),
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 };
1040
1041 let extracted_params = ExtractedParameters {
1042 path: HashMap::new(),
1043 query: HashMap::new(),
1044 headers: HashMap::new(),
1045 cookies: HashMap::new(),
1046 body: HashMap::new(),
1047 config: crate::tool_generator::RequestConfig::default(),
1048 };
1049
1050 let url1 = client1
1051 .build_url(&tool_metadata, &extracted_params)
1052 .unwrap();
1053 let url2 = client2
1054 .build_url(&tool_metadata, &extracted_params)
1055 .unwrap();
1056
1057 assert_eq!(url1.to_string(), "https://api.example.com/pets");
1059 assert_eq!(url2.to_string(), "https://api.example.com/pets");
1060 }
1061
1062 #[test]
1063 fn test_explode_array_parameters() {
1064 let base_url = Url::parse("https://api.example.com").unwrap();
1065 let client = HttpClient::new().with_base_url(base_url).unwrap();
1066
1067 let tool_metadata = crate::ToolMetadata {
1068 name: "test".to_string(),
1069 title: None,
1070 description: "test".to_string(),
1071 parameters: json!({}),
1072 output_schema: None,
1073 method: "GET".to_string(),
1074 path: "/search".to_string(),
1075 };
1076
1077 let mut query_params_exploded = HashMap::new();
1079 query_params_exploded.insert(
1080 "include".to_string(),
1081 QueryParameter::new(json!(["asset", "scenes"]), true),
1082 );
1083
1084 let extracted_params_exploded = ExtractedParameters {
1085 path: HashMap::new(),
1086 query: query_params_exploded,
1087 headers: HashMap::new(),
1088 cookies: HashMap::new(),
1089 body: HashMap::new(),
1090 config: crate::tool_generator::RequestConfig::default(),
1091 };
1092
1093 let mut url_exploded = client
1094 .build_url(&tool_metadata, &extracted_params_exploded)
1095 .unwrap();
1096 HttpClient::add_query_parameters(
1097 &mut url_exploded,
1098 &extracted_params_exploded.query,
1099 &tool_metadata,
1100 );
1101 let url_exploded_string = url_exploded.to_string();
1102
1103 let mut query_params_not_exploded = HashMap::new();
1105 query_params_not_exploded.insert(
1106 "include".to_string(),
1107 QueryParameter::new(json!(["asset", "scenes"]), false),
1108 );
1109
1110 let extracted_params_not_exploded = ExtractedParameters {
1111 path: HashMap::new(),
1112 query: query_params_not_exploded,
1113 headers: HashMap::new(),
1114 cookies: HashMap::new(),
1115 body: HashMap::new(),
1116 config: crate::tool_generator::RequestConfig::default(),
1117 };
1118
1119 let mut url_not_exploded = client
1120 .build_url(&tool_metadata, &extracted_params_not_exploded)
1121 .unwrap();
1122 HttpClient::add_query_parameters(
1123 &mut url_not_exploded,
1124 &extracted_params_not_exploded.query,
1125 &tool_metadata,
1126 );
1127 let url_not_exploded_string = url_not_exploded.to_string();
1128
1129 assert!(url_exploded_string.contains("include=asset"));
1131 assert!(url_exploded_string.contains("include=scenes"));
1132
1133 assert!(url_not_exploded_string.contains("include=asset%2Cscenes")); assert_ne!(url_exploded_string, url_not_exploded_string);
1138
1139 println!("Exploded URL: {url_exploded_string}");
1140 println!("Non-exploded URL: {url_not_exploded_string}");
1141 }
1142
1143 #[test]
1144 fn test_omit_empty_optional_array_query_parameters() {
1145 let base_url = Url::parse("https://api.example.com").unwrap();
1146 let client = HttpClient::new().with_base_url(base_url).unwrap();
1147
1148 let tool_metadata = crate::ToolMetadata {
1150 name: "test".to_string(),
1151 title: None,
1152 description: "test".to_string(),
1153 parameters: json!({
1154 "type": "object",
1155 "properties": {
1156 "optional_array": {"type": "array", "items": {"type": "string"}},
1157 "required_array": {"type": "array", "items": {"type": "string"}},
1158 "optional_with_default": {
1159 "type": "array",
1160 "items": {"type": "string"},
1161 "default": ["default"]
1162 },
1163 "optional_string": {"type": "string"}
1164 },
1165 "required": ["required_array"]
1166 }),
1167 output_schema: None,
1168 method: "GET".to_string(),
1169 path: "/test".to_string(),
1170 };
1171
1172 let mut query_params = HashMap::new();
1173 query_params.insert(
1174 "optional_array".to_string(),
1175 QueryParameter::new(json!([]), true),
1176 ); query_params.insert(
1178 "required_array".to_string(),
1179 QueryParameter::new(json!([]), true),
1180 ); query_params.insert(
1182 "optional_with_default".to_string(),
1183 QueryParameter::new(json!([]), true),
1184 ); query_params.insert(
1186 "optional_string".to_string(),
1187 QueryParameter::new(json!(""), true),
1188 ); let extracted_params = ExtractedParameters {
1191 path: HashMap::new(),
1192 query: query_params,
1193 headers: HashMap::new(),
1194 cookies: HashMap::new(),
1195 body: HashMap::new(),
1196 config: crate::tool_generator::RequestConfig::default(),
1197 };
1198
1199 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1200 HttpClient::add_query_parameters(&mut url, &extracted_params.query, &tool_metadata);
1201
1202 let url_string = url.to_string();
1203 assert!(!url_string.contains("optional_array"));
1205
1206 assert!(url_string.contains("required_array"));
1208
1209 assert!(url_string.contains("optional_with_default"));
1211
1212 assert!(url_string.contains("optional_string"));
1214 }
1215
1216 #[test]
1217 fn test_omit_empty_optional_array_headers() {
1218 let tool_metadata = crate::ToolMetadata {
1219 name: "test".to_string(),
1220 title: None,
1221 description: "test".to_string(),
1222 parameters: json!({
1223 "type": "object",
1224 "properties": {
1225 "x-optional-array": {"type": "array", "items": {"type": "string"}},
1226 "x-required-array": {"type": "array", "items": {"type": "string"}},
1227 "x-optional-string": {"type": "string"}
1228 },
1229 "required": ["x-required-array"]
1230 }),
1231 output_schema: None,
1232 method: "GET".to_string(),
1233 path: "/test".to_string(),
1234 };
1235
1236 let mut headers = HashMap::new();
1237 headers.insert("x-optional-array".to_string(), json!([])); headers.insert("x-required-array".to_string(), json!([])); headers.insert("x-optional-string".to_string(), json!("value")); let request = HttpClient::new()
1242 .client
1243 .request(reqwest::Method::GET, "https://api.example.com");
1244 let request = HttpClient::add_headers(request, &headers, &tool_metadata);
1245
1246 assert!(request.build().is_ok());
1249 }
1250
1251 #[test]
1252 fn test_omit_empty_optional_array_cookies() {
1253 let tool_metadata = crate::ToolMetadata {
1254 name: "test".to_string(),
1255 title: None,
1256 description: "test".to_string(),
1257 parameters: json!({
1258 "type": "object",
1259 "properties": {
1260 "optional_array_cookie": {"type": "array", "items": {"type": "string"}},
1261 "required_array_cookie": {"type": "array", "items": {"type": "string"}},
1262 "optional_string_cookie": {"type": "string"}
1263 },
1264 "required": ["required_array_cookie"]
1265 }),
1266 output_schema: None,
1267 method: "GET".to_string(),
1268 path: "/test".to_string(),
1269 };
1270
1271 let mut cookies = HashMap::new();
1272 cookies.insert("optional_array_cookie".to_string(), json!([])); cookies.insert("required_array_cookie".to_string(), json!([])); cookies.insert("optional_string_cookie".to_string(), json!("value")); let request = HttpClient::new()
1277 .client
1278 .request(reqwest::Method::GET, "https://api.example.com");
1279 let request = HttpClient::add_cookies(request, &cookies, &tool_metadata);
1280
1281 assert!(request.build().is_ok());
1283 }
1284
1285 #[test]
1286 fn test_non_empty_optional_arrays_are_included() {
1287 let base_url = Url::parse("https://api.example.com").unwrap();
1288 let client = HttpClient::new().with_base_url(base_url).unwrap();
1289
1290 let tool_metadata = crate::ToolMetadata {
1291 name: "test".to_string(),
1292 title: None,
1293 description: "test".to_string(),
1294 parameters: json!({
1295 "type": "object",
1296 "properties": {
1297 "optional_array": {"type": "array", "items": {"type": "string"}}
1298 },
1299 "required": []
1300 }),
1301 output_schema: None,
1302 method: "GET".to_string(),
1303 path: "/test".to_string(),
1304 };
1305
1306 let mut query_params = HashMap::new();
1307 query_params.insert(
1308 "optional_array".to_string(),
1309 QueryParameter::new(json!(["value1", "value2"]), true),
1310 ); let extracted_params = ExtractedParameters {
1313 path: HashMap::new(),
1314 query: query_params,
1315 headers: HashMap::new(),
1316 cookies: HashMap::new(),
1317 body: HashMap::new(),
1318 config: crate::tool_generator::RequestConfig::default(),
1319 };
1320
1321 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1322 HttpClient::add_query_parameters(&mut url, &extracted_params.query, &tool_metadata);
1323
1324 let url_string = url.to_string();
1325
1326 assert!(url_string.contains("optional_array=value1"));
1328 assert!(url_string.contains("optional_array=value2"));
1329 }
1330}