use crate::error::Error;
use crate::secret::HttpSecret;
use reqwest::{
Method,
header::{HeaderMap, HeaderName, HeaderValue},
};
use serde_json::Value;
use std::{collections::HashMap, str::FromStr};
use url::Url;
pub struct HttpEndpoint {
secret: HttpSecret,
}
impl From<&HttpSecret> for HttpEndpoint {
fn from(secret: &HttpSecret) -> Self {
Self {
secret: secret.clone(),
}
}
}
impl HttpEndpoint {
pub fn body(&self) -> Option<String> {
self.secret.body().clone()
}
pub fn get_method(&self) -> Result<Method, Error> {
if let Some(method) = self.secret.method() {
Self::parse_method(method)
} else {
Ok(Method::GET)
}
}
pub fn get_url(&self, path: &str) -> Result<Url, Error> {
if let Some(endpoint) = self.secret.endpoint() {
let mut url = Self::check_url(endpoint)?;
url.set_path(path);
Ok(url)
} else {
Self::check_url(path)
}
}
pub fn get_headers(&self) -> Result<HeaderMap, Error> {
if let Some(json_headers) = self.secret.headers() {
Self::parse_headers(json_headers)
} else {
Ok(HeaderMap::new())
}
}
pub fn check_url(endpoint: &str) -> Result<Url, Error> {
if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
Url::parse(endpoint).map_err(|parse_error| Error::from((endpoint, parse_error)))
} else {
Err(Error::Other(format!(
"Invalid '{endpoint}' URL, expected HTTP URL base ('http' or 'https')."
)))
}
}
fn parse_method(method: &str) -> Result<Method, Error> {
Method::from_str(&method.to_uppercase())
.map_err(|_| Error::Other(format!("Invalid '{method}' HTTP method.")))
}
fn parse_headers(json_headers: &str) -> Result<HeaderMap, Error> {
let mut headers = HeaderMap::new();
let headers_map: HashMap<String, Value> = serde_json::from_str(json_headers)?;
for (key, value) in headers_map.iter() {
let header_name = HeaderName::from_str(key).unwrap();
let header_value = get_header_value(value)?;
headers.append(header_name, header_value);
}
Ok(headers)
}
}
fn get_header_value(json_value: &Value) -> Result<HeaderValue, Error> {
match json_value {
Value::Bool(boolean) => get_header_value(&Value::String(boolean.to_string())),
Value::Number(number) => {
if number.is_u64() {
Ok(HeaderValue::from(number.as_u64().unwrap()))
} else if number.is_i64() {
Ok(HeaderValue::from(number.as_i64().unwrap()))
} else {
get_header_value(&Value::String(number.as_f64().unwrap().to_string()))
}
}
Value::String(string) => HeaderValue::from_str(string).map_err(|error| {
Error::Other(format!(
"Cannot parse '{json_value}' HTTP header value: {error}"
))
}),
_ => Err(Error::Other(format!(
"Unsupported '{json_value:?}' HTTP header value format."
))),
}
}
#[test]
pub fn test_get_method() {
assert_eq!(Method::GET, HttpEndpoint::parse_method("GET").unwrap());
assert_eq!(Method::GET, HttpEndpoint::parse_method("get").unwrap());
assert_eq!(Method::GET, HttpEndpoint::parse_method("Get").unwrap());
assert_eq!(Method::PUT, HttpEndpoint::parse_method("PUT").unwrap());
assert_eq!(Method::PUT, HttpEndpoint::parse_method("put").unwrap());
assert_eq!(Method::PUT, HttpEndpoint::parse_method("Put").unwrap());
assert_eq!(Method::POST, HttpEndpoint::parse_method("POST").unwrap());
assert_eq!(Method::POST, HttpEndpoint::parse_method("post").unwrap());
assert_eq!(Method::POST, HttpEndpoint::parse_method("Post").unwrap());
let error = HttpEndpoint::parse_method("get ").unwrap_err();
assert_eq!("Invalid 'get ' HTTP method.".to_string(), error.to_string());
}
#[test]
pub fn test_get_url() {
assert_eq!(
"http://www.media-io.com/".to_string(),
HttpEndpoint::check_url("http://www.media-io.com")
.unwrap()
.to_string()
);
assert_eq!(
"https://www.media-io.com/".to_string(),
HttpEndpoint::check_url("https://www.media-io.com")
.unwrap()
.to_string()
);
assert_eq!(
"https://www.media-io.com/resource?param=value".to_string(),
HttpEndpoint::check_url("https://www.media-io.com/resource?param=value")
.unwrap()
.to_string()
);
let error = HttpEndpoint::check_url("http://media-io com").unwrap_err();
assert_eq!(
"Unable to parse 'http://media-io com' URL: invalid international domain name".to_string(),
error.to_string()
);
let error = HttpEndpoint::check_url("ftp://media-io.com").unwrap_err();
assert_eq!(
"Invalid 'ftp://media-io.com' URL, expected HTTP URL base ('http' or 'https').".to_string(),
error.to_string()
);
}
#[test]
pub fn test_get_headers() {
let json = "{}";
let headers = HttpEndpoint::parse_headers(json).unwrap();
assert!(headers.is_empty());
let json = r#"{
"content-length": 12345,
"content-type": "application/json"
}"#;
let headers = HttpEndpoint::parse_headers(json).unwrap();
assert_eq!(headers.len(), 2);
assert_eq!(
Some(&HeaderValue::from_str("12345").unwrap()),
headers.get("content-length")
);
assert_eq!(
Some(&HeaderValue::from_str("application/json").unwrap()),
headers.get("content-type")
);
}
#[test]
pub fn test_get_header_value() {
use serde_json::value::{Map, Number};
let error = get_header_value(&Value::Null).unwrap_err();
assert_eq!(
"Unsupported 'Null' HTTP header value format.".to_string(),
error.to_string()
);
let string = "Hello there!".to_string();
let header_value = get_header_value(&Value::String(string.clone())).unwrap();
assert_eq!(string, header_value);
let boolean = true;
let header_value = get_header_value(&Value::Bool(boolean)).unwrap();
assert_eq!(boolean.to_string(), header_value);
let unsigned_int: u32 = 123;
let header_value = get_header_value(&Value::Number(unsigned_int.into())).unwrap();
assert_eq!(unsigned_int.to_string(), header_value);
let signed_int: i16 = -123;
let header_value = get_header_value(&Value::Number(signed_int.into())).unwrap();
assert_eq!(signed_int.to_string(), header_value);
let float: f64 = 1.23;
let header_value = get_header_value(&Value::Number(Number::from_f64(float).unwrap())).unwrap();
assert_eq!(float.to_string(), header_value);
let invalid_string = "\0\0".to_string();
let error = get_header_value(&Value::String(invalid_string)).unwrap_err();
assert_eq!(
"Cannot parse '\"\\u0000\\u0000\"' HTTP header value: failed to parse header value".to_string(),
error.to_string()
);
let array = vec![];
let error = get_header_value(&Value::Array(array)).unwrap_err();
assert_eq!(
"Unsupported 'Array []' HTTP header value format.".to_string(),
error.to_string()
);
let object = Map::new();
let error = get_header_value(&Value::Object(object)).unwrap_err();
assert_eq!(
"Unsupported 'Object {}' HTTP header value format.".to_string(),
error.to_string()
);
}