rs_transfer 8.0.0

A simple crate to handle downloads and uploads on multiple providers
Documentation
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());
  // etc..

  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()
  );
}