use std::{collections::BTreeMap, time::Duration}; use crank_core::{HttpMethod, RestTarget}; use reqwest::{ Client, header::{HeaderMap, HeaderName, HeaderValue}, }; use serde_json::Value; use crate::{RestAdapterError, RestRequest, RestResponse}; #[derive(Clone, Debug)] pub struct RestAdapter { client: Client, } impl Default for RestAdapter { fn default() -> Self { Self::new() } } impl RestAdapter { pub fn new() -> Self { Self { client: Client::new(), } } pub async fn execute( &self, target: &RestTarget, request: &RestRequest, ) -> Result { let url = build_url(target, request)?; let headers = build_headers(target, request)?; let mut builder = self .client .request(to_reqwest_method(target.method), url) .headers(headers) .timeout(Duration::from_millis(request.timeout_ms)); if let Some(body) = &request.body { builder = builder.json(body); } let response = builder.send().await?; let status = response.status(); let headers = normalize_headers(response.headers()); let body = decode_body(response).await?; if !status.is_success() { return Err(RestAdapterError::UnexpectedStatus { status: status.as_u16(), body, }); } Ok(RestResponse { status_code: status.as_u16(), headers, body, }) } } fn build_url(target: &RestTarget, request: &RestRequest) -> Result { let base_url = reqwest::Url::parse(&target.base_url).map_err(|_| RestAdapterError::InvalidBaseUrl { url: target.base_url.clone(), })?; let path = substitute_path_params(&target.path_template, &request.path_params); let mut url = base_url.join(path.trim_start_matches('/')).map_err(|_| { RestAdapterError::InvalidBaseUrl { url: target.base_url.clone(), } })?; { let mut query = url.query_pairs_mut(); for (key, value) in &request.query_params { query.append_pair(key, value); } } Ok(url) } fn substitute_path_params(path_template: &str, path_params: &BTreeMap) -> String { let mut rendered = path_template.to_owned(); for (key, value) in path_params { rendered = rendered.replace(&format!("{{{key}}}"), value); } rendered } fn build_headers( target: &RestTarget, request: &RestRequest, ) -> Result { let mut headers = HeaderMap::new(); for (name, value) in &target.static_headers { insert_header(&mut headers, name, value)?; } for (name, value) in &request.headers { insert_header(&mut headers, name, value)?; } Ok(headers) } fn insert_header(headers: &mut HeaderMap, name: &str, value: &str) -> Result<(), RestAdapterError> { let header_name = HeaderName::try_from(name).map_err(|_| RestAdapterError::InvalidHeaderName { header: name.to_owned(), })?; let header_value = HeaderValue::try_from(value).map_err(|_| RestAdapterError::InvalidHeaderValue { header: name.to_owned(), })?; headers.insert(header_name, header_value); Ok(()) } async fn decode_body(response: reqwest::Response) -> Result { let bytes = response.bytes().await?; if bytes.is_empty() { return Ok(Value::Null); } match serde_json::from_slice::(&bytes) { Ok(value) => Ok(value), Err(_) => Ok(Value::String( String::from_utf8_lossy(&bytes).trim().to_owned(), )), } } fn normalize_headers(headers: &HeaderMap) -> BTreeMap { headers .iter() .filter_map(|(name, value)| { value .to_str() .ok() .map(|value| (name.as_str().to_owned(), value.to_owned())) }) .collect() } fn to_reqwest_method(method: HttpMethod) -> reqwest::Method { match method { HttpMethod::Get => reqwest::Method::GET, HttpMethod::Post => reqwest::Method::POST, HttpMethod::Put => reqwest::Method::PUT, HttpMethod::Patch => reqwest::Method::PATCH, HttpMethod::Delete => reqwest::Method::DELETE, } }