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