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