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