use std::{
marker::PhantomData,
ops::{Deref, DerefMut},
};
use super::{
flat_csv::{SemiColon, Separator},
parameter::{FieldParameter, InvalidEncodedFieldParameter},
parameter_name::FieldParameterName,
parameter_value::FieldParameterValue,
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FieldParameters<Sep = SemiColon> {
pub params: Vec<FieldParameter>,
_phantom: PhantomData<fn(Sep)>,
}
impl<Sep> FieldParameters<Sep> {
#[inline]
pub fn new(params: Vec<FieldParameter>) -> Self {
Self {
params,
_phantom: PhantomData,
}
}
}
impl<Sep> Deref for FieldParameters<Sep> {
type Target = Vec<FieldParameter>;
fn deref(&self) -> &Self::Target {
&self.params
}
}
impl DerefMut for FieldParameters {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.params
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum InvalidEncodedFieldParameters {
#[error("Invalid parameter")]
InvalidParameter(#[from] InvalidEncodedFieldParameter),
}
impl<Sep: Separator> FieldParameters<Sep> {
pub fn decode(
param_strs: impl Iterator<Item = impl AsRef<str>>,
allow_implicit_param_value: bool,
) -> Result<Self, InvalidEncodedFieldParameters> {
Ok(Self::new(
param_strs
.filter(|s| !s.as_ref().is_empty())
.map(|s| FieldParameter::decode(s.as_ref(), allow_implicit_param_value))
.collect::<Result<Vec<_>, _>>()?,
))
}
pub fn push_encoded_str(&self, buffer: &mut String) {
for (i, param) in self.params.iter().enumerate() {
if i > 0 {
buffer.push(Sep::CHAR);
buffer.push(' ');
}
buffer.push_str(param.name.as_ref());
buffer.push('=');
param.value.push_encoded_str(buffer);
}
}
#[inline]
pub fn encode(&self) -> String {
let mut encoded = String::new();
self.push_encoded_str(&mut encoded);
encoded
}
pub fn get_value(&self, name: &FieldParameterName) -> Option<&FieldParameterValue> {
self.iter().find_map(|p| {
if &p.name == name {
Some(&p.value)
} else {
None
}
})
}
}
#[cfg(test)]
pub(crate) mod tests_parse {
use claims::{assert_err, assert_ok};
use rstest::rstest;
use super::*;
#[rstest]
#[case(
&[r#"rel="previous""#, r#"title="previous chapter""#],
&[("rel", "previous"), ("title", "previous chapter")]
)]
#[case(
&[r#"rel="http://example.net/foo""#],
&[("rel", "http://example.net/foo")]
)]
#[case(
&[r#"rel=copyright"#, r##"anchor="#foo""##],
&[("rel", "copyright"), ("anchor", "#foo")]
)]
#[case(
&[r#"rel="previous""#, r#"title*=UTF-8'de'letztes%20Kapitel"#],
&[("rel", "previous"), ("title*", "UTF-8'de'letztes%20Kapitel")]
)]
#[case(
&[r#"rel="start http://example.net/relation/other""#, ],
&[("rel", "start http://example.net/relation/other")]
)]
fn valid_params_will_be_parsed_correctly(
#[case] params_list: &[&str],
#[case] expected_param_records: &[(&str, &str)],
) {
let params = assert_ok!(FieldParameters::decode(params_list.iter(), false));
assert_matches_param_records(¶ms, expected_param_records);
}
pub fn assert_matches_param_records(params: &FieldParameters, param_records: &[(&str, &str)]) {
assert_eq!(params.len(), param_records.len());
assert!(
params
.iter()
.zip(param_records.iter())
.all(|(param, (n, v))| param.name.eq_ignore_ascii_case(n)
&& param.value.as_ref().eq(*v)),
"Params parsed incorrectly"
);
}
#[rstest]
#[case(&["abc", "pqr"])]
#[case(&["abc=def", "pqr"])]
#[case(&["abc=", "pqr"])]
#[case(&["abc=rd", "pqr=c a b"])]
#[case(&[r#"rel="previous""#, r#"title*=UTF-8'de'letztes%20Kapitel""#],)]
fn invalid_params_will_be_rejected(#[case] params_list: &[&str]) {
assert_err!(FieldParameters::<SemiColon>::decode(
params_list.iter(),
false
));
}
}
#[cfg(test)]
mod tests_encode {
use claims::assert_ok;
use rstest::rstest;
use super::{tests_parse::*, *};
use crate::field::rules::flat_csv::{FlatCsv, SemiColon};
#[rstest]
#[case(
&[r#"rel="previous""#, r#"title="previous chapter""#],
&[("rel", "previous"), ("title", "previous chapter")]
)]
#[case(
&[r#"rel="http://example.net/foo""#],
&[("rel", "http://example.net/foo")]
)]
#[case(
&[r#"rel=copyright"#, r##"anchor="#foo""##],
&[("rel", "copyright"), ("anchor", "#foo")]
)]
#[case(
&[r#"rel="previous""#, r#"title*=UTF-8'de'letztes%20Kapitel"#],
&[("rel", "previous"), ("title*", "UTF-8'de'letztes%20Kapitel")]
)]
#[case(
&[r#"rel="start http://example.net/relation/other""#, ],
&[("rel", "start http://example.net/relation/other")]
)]
fn round_trip_works_correctly(
#[case] params_list: &[&str],
#[case] expected_param_records: &[(&str, &str)],
) {
let params = assert_ok!(FieldParameters::<SemiColon>::decode(
params_list.iter(),
false
));
let params_encoded = params.encode();
let params_round_tripped = assert_ok!(FieldParameters::decode(
FlatCsv::<SemiColon>::from(&assert_ok!(headers::HeaderValue::from_str(
¶ms_encoded
)))
.iter(),
false
));
assert_matches_param_records(¶ms_round_tripped, expected_param_records);
}
}