use std::collections::{BTreeMap, HashMap};
use std::fmt::Debug;
use std::str::FromStr;
use std::time::Duration;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use url::Url;
use crate::secrets::Sensitive;
pub fn deserialize_milliseconds_to_duration<'de, D>(de: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let millis: u64 = Deserialize::deserialize(de)?;
Ok(Duration::from_millis(millis))
}
pub fn serialize_duration_as_milliseconds<S>(duration: &Duration, se: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
se.serialize_u64(u64::try_from(duration.as_millis()).unwrap_or(u64::MAX))
}
pub fn deserialize_seconds_to_duration<'de, D>(de: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let secs: u64 = Deserialize::deserialize(de)?;
Ok(Duration::from_secs(secs))
}
pub fn deserialize_float_seconds_to_duration<'de, D>(de: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let secs: f64 = Deserialize::deserialize(de)?;
Ok(Duration::from_secs_f64(secs))
}
pub fn serialize_optional_map(optional_map: &Option<HashMap<String, String>>) -> String {
match optional_map {
None => "".to_owned(),
Some(map) => map.iter().map(|(k, v)| format!("{k}:{v}")).collect::<Vec<String>>().join(" "),
}
}
pub fn deserialize_optional_map<'de, D>(de: D) -> Result<Option<HashMap<String, String>>, D::Error>
where
D: Deserializer<'de>,
{
let raw_str: String = Deserialize::deserialize(de)?;
if raw_str.is_empty() {
return Ok(None);
}
let mut map = HashMap::new();
for raw_pair in raw_str.split(' ') {
let split: Vec<&str> = raw_pair.split(':').collect();
if split.len() != 2 {
return Err(D::Error::custom(format!(
"pair \"{raw_pair}\" is not valid. The Expected format is name:value"
)));
}
map.insert(split[0].to_string(), split[1].to_string());
}
Ok(Some(map))
}
pub fn deserialize_optional_sensitive_map<'de, D>(
de: D,
) -> Result<Option<Sensitive<HashMap<String, String>>>, D::Error>
where
D: Deserializer<'de>,
{
let optional_map = deserialize_optional_map(de)?;
Ok(optional_map.map(Sensitive::new))
}
#[derive(Clone, Deserialize, Serialize, PartialEq)]
pub struct UrlAndHeaders {
pub url: Url,
pub headers: BTreeMap<String, String>,
}
impl UrlAndHeaders {
const RESERVED_CHARS: [char; 2] = ['^', ','];
pub fn to_custom_string(&self) -> Result<String, String> {
for (k, v) in &self.headers {
Self::validate_component(k, "key")?;
Self::validate_component(v, "value")?;
}
let mut output = self.url.as_str().to_string();
for (key, value) in &self.headers {
output.push(',');
output.push_str(key);
output.push('^');
output.push_str(value);
}
Ok(output)
}
pub fn from_custom_string(s: &str) -> Result<Self, String> {
let mut parts = s.splitn(2, ',');
let url_str = parts.next().ok_or("Missing URL")?;
let rest = parts.next().unwrap_or("");
let url = Url::parse(url_str).map_err(|e| format!("Invalid URL: {e}"))?;
let mut headers = BTreeMap::new();
if !rest.is_empty() {
for pair in rest.split(',') {
let mut kv = pair.splitn(2, '^');
let k = kv.next().ok_or("Missing header key")?;
let v = kv.next().ok_or("Missing header value")?;
Self::validate_component(k, "key")?;
Self::validate_component(v, "value")?;
headers.insert(k.to_string(), v.to_string());
}
}
Ok(UrlAndHeaders { url, headers })
}
fn validate_component(value: &str, label: &str) -> Result<(), String> {
if let Some(c) = value.chars().find(|c| Self::RESERVED_CHARS.contains(c)) {
return Err(format!("Invalid character '{c}' in header {label}: '{value}'"));
}
Ok(())
}
}
impl Debug for UrlAndHeaders {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[cfg(not(test))]
write!(f, "{}", self.url)?;
#[cfg(test)]
write!(f, "url: {}, headers: {:?}", self.url, self.headers)?;
Ok(())
}
}
pub fn serialize_optional_list_with_url_and_headers(list: &Option<Vec<UrlAndHeaders>>) -> String {
match list {
None => "".to_owned(),
Some(list) => list
.iter()
.map(|item| {
UrlAndHeaders::to_custom_string(item).expect("Failed to serialize UrlAndHeader")
})
.collect::<Vec<String>>()
.join("|"),
}
}
pub fn deserialize_optional_list_with_url_and_headers<'de, D>(
de: D,
) -> Result<Option<Vec<UrlAndHeaders>>, D::Error>
where
D: Deserializer<'de>,
{
let raw: String = Deserialize::deserialize(de)?;
if raw.trim().is_empty() {
return Ok(None);
}
let items = raw.split('|');
let number_of_items = items.clone().count();
let mut output = Vec::with_capacity(number_of_items);
for item in items {
let value: UrlAndHeaders = UrlAndHeaders::from_custom_string(item).map_err(|e| {
D::Error::custom(format!("Invalid UrlAndHeaders formatting '{item}': {e}"))
})?;
output.push(value);
}
Ok(Some(output))
}
pub fn deserialize_optional_sensitive_list_with_url_and_headers<'de, D>(
de: D,
) -> Result<Option<Vec<Sensitive<UrlAndHeaders>>>, D::Error>
where
D: Deserializer<'de>,
{
let optional_list = deserialize_optional_list_with_url_and_headers(de)?;
Ok(optional_list.map(|list| list.into_iter().map(Sensitive::new).collect()))
}
pub fn serialize_optional_vec_u8(optional_vector: &Option<Vec<u8>>) -> String {
match optional_vector {
None => "".to_owned(),
Some(vector) => {
format!(
"0x{}",
vector.iter().map(|num| format!("{num:02x}")).collect::<Vec<String>>().join("")
)
}
}
}
pub fn deserialize_optional_vec_u8<'de, D>(de: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: Deserializer<'de>,
{
let raw_str: String = Deserialize::deserialize(de)?;
if raw_str.is_empty() {
return Ok(None);
}
if !raw_str.starts_with("0x") {
return Err(D::Error::custom(
"Couldn't deserialize vector. Expected hex string starting with \"0x\"",
));
}
let hex_str = &raw_str[2..];
let mut vector = Vec::new();
for i in (0..hex_str.len()).step_by(2) {
let byte_str = &hex_str[i..i + 2];
let byte = u8::from_str_radix(byte_str, 16).map_err(|e| {
D::Error::custom(format!(
"Couldn't deserialize vector. Failed to parse byte: {byte_str} {e}"
))
})?;
vector.push(byte);
}
Ok(Some(vector))
}
pub fn serialize_slice<T: AsRef<str>>(vector: &[T]) -> String {
vector.iter().map(|item| item.as_ref()).collect::<Vec<_>>().join(" ")
}
pub fn deserialize_vec<'de, D, T>(de: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: std::fmt::Display,
{
let raw = String::deserialize(de)?;
if raw.trim().is_empty() {
return Ok(Vec::new());
}
raw.split_whitespace()
.map(|s| T::from_str(s).map_err(|e| D::Error::custom(format!("Invalid value '{s}': {e}"))))
.collect()
}
pub fn serialize_optional_comma_separated<T>(list: &Option<Vec<T>>) -> Option<String>
where
T: ToString,
{
match list {
None => None,
Some(list) => Some(list.iter().map(|item| item.to_string()).collect::<Vec<_>>().join(",")),
}
}
pub fn deserialize_comma_separated_str<'de, D, T>(de: D) -> Result<Option<Vec<T>>, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
<T as FromStr>::Err: std::fmt::Display,
{
let raw = String::deserialize(de).unwrap_or_default();
if raw.trim().is_empty() {
return Ok(None);
}
let mut output: Vec<T> = Vec::new();
for part in raw.split(',').filter(|s| !s.is_empty()) {
let value = T::from_str(part)
.map_err(|e| D::Error::custom(format!("Invalid value '{part}': {e}")))?;
output.push(value);
}
if output.is_empty() {
return Ok(None);
}
Ok(Some(output))
}
pub fn deserialize_optional_sensitive_vec_u8<'de, D>(
de: D,
) -> Result<Option<Sensitive<Vec<u8>>>, D::Error>
where
D: Deserializer<'de>,
{
let optional_vec = deserialize_optional_vec_u8(de)?;
Ok(optional_vec.map(Sensitive::new))
}