manas_http/header/prefer/
preference.rs

1//! I define [`Preference] struct
2//! corresponding to `preference` production.
3//!
4use std::str::FromStr;
5
6use once_cell::sync::Lazy;
7
8use crate::field::{
9    pvalue::{InvalidEncodedPFieldValue, PFieldValue},
10    rules::{
11        parameter::{FieldParameter, InvalidEncodedFieldParameter},
12        parameter_name::FieldParameterName,
13        parameter_value::FieldParameterValue,
14        parameters::FieldParameters,
15    },
16};
17
18/// A preference in a `Prefer` header as defined by rfc7240
19///
20/// It follows ABNF of `preference` production as in rfc:
21/// ```txt
22/// preference = token [ BWS "=" BWS word ]
23///             *( OWS ";" [ OWS parameter ] )
24/// parameter  = token [ BWS "=" BWS word ]
25/// ```
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Preference {
28    /// Token name.
29    pub token_name: FieldParameterName,
30
31    /// Token value.
32    pub token_value: FieldParameterValue,
33
34    /// parametets.
35    pub params: FieldParameters,
36}
37
38/// Static for "return" token name.
39pub static TOKEN_NAME_RETURN: Lazy<FieldParameterName> =
40    Lazy::new(|| "return".parse().expect("Must be valid."));
41
42/// Static for "include" param name.
43pub static PARAM_NAME_INCLUDE: Lazy<FieldParameterName> =
44    Lazy::new(|| "include".parse().expect("Must be valid."));
45
46/// Static for "representation" token value.
47pub static TOKEN_VALUE_REPRESENTATION: Lazy<FieldParameterValue> =
48    Lazy::new(|| "representation".try_into().expect("Must be valid."));
49
50#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
51/// Error of invalid `Preference`.
52pub enum InvalidEncodedPreference {
53    /// Invalid parameterized field value.
54    #[error("Given header value is not a valid parameterized header value")]
55    InvalidPFieldValue(#[from] InvalidEncodedPFieldValue),
56
57    /// Invalid preference token.
58    #[error("Given preference has invalid preference token")]
59    InvalidPreferenceToken(#[from] InvalidEncodedFieldParameter),
60}
61
62impl FromStr for Preference {
63    type Err = InvalidEncodedPreference;
64
65    fn from_str(value: &str) -> Result<Self, Self::Err> {
66        let PFieldValue {
67            base_value: pref_token,
68            params,
69        } = PFieldValue::decode(value, true)?;
70
71        let FieldParameter {
72            name: token_name,
73            value: token_value,
74        } = FieldParameter::decode(&pref_token, true)?;
75
76        Ok(Self {
77            token_name,
78            token_value,
79            params,
80        })
81    }
82}
83
84impl Preference {
85    /// Push encoded value to buffer
86    #[inline]
87    pub(crate) fn push_encoded_str(&self, buffer: &mut String) {
88        self.token_name.push_encoded_str(buffer);
89        buffer.push('=');
90        self.token_value.push_encoded_str(buffer);
91
92        if self.params.len() > 0 {
93            buffer.push_str("; ");
94            self.params.push_encoded_str(buffer);
95        }
96    }
97
98    /// Get encoded value as per rfc
99    #[inline]
100    pub fn str_encode(&self) -> String {
101        let mut encoded = String::new();
102        self.push_encoded_str(&mut encoded);
103        encoded
104    }
105}
106
107#[cfg(test)]
108pub(crate) mod tests_decode {
109    use claims::*;
110    use rstest::*;
111
112    use super::*;
113    use crate::field::rules::parameters::tests_parse::assert_matches_param_records;
114
115    #[rstest]
116    #[case::invalid_param_key("abc; def/ghi = 123")]
117    #[case::invalid_param_value("abc; def=a b")]
118    fn preference_with_invalid_params_will_be_rejected(#[case] preference_str: &str) {
119        assert_matches!(
120            assert_err!(Preference::from_str(preference_str)),
121            InvalidEncodedPreference::InvalidPFieldValue(..)
122        );
123    }
124
125    #[rstest]
126    #[case("abc/def; a=1")]
127    #[case("abc def; b=2")]
128    #[case("abc = def/pqr; b=2")]
129    #[case("abc = def pqr; b=2")]
130    fn preference_with_invalid_preference_token_will_be_rejected(#[case] preference_str: &str) {
131        assert_matches!(
132            assert_err!(Preference::from_str(preference_str)),
133            InvalidEncodedPreference::InvalidPreferenceToken(..)
134        );
135    }
136
137    pub fn assert_valid_preference(preference_str: &str) -> Preference {
138        assert_ok!(Preference::from_str(preference_str))
139    }
140
141    pub fn assert_preference_match(
142        preference: &Preference,
143        expected_token_name: &str,
144        expected_token_value: &str,
145        expected_params: &[(&str, &str)],
146    ) {
147        assert_eq!(preference.token_name, expected_token_name.parse().unwrap());
148        assert_eq!(preference.token_value.as_ref(), expected_token_value);
149        assert_matches_param_records(&preference.params, expected_params);
150    }
151
152    #[rstest]
153    #[case("foo; bar", "foo", "", &[("bar","")])]
154    #[case("foo; bar=\"\"", "foo", "", &[("bar","")])]
155    #[case("foo=\"\"; bar", "foo", "", &[("bar","")])]
156    #[case("respond-async; wait=100", "respond-async", "", &[("wait","100")])]
157    #[case("return=minimal; foo=\"some parameter\"", "return", "minimal", &[("foo", "some parameter")])]
158    #[case(
159        r#"return=representation; include="http://www.w3.org/ns/ldp#PreferMembership http://www.w3.org/ns/ldp#PreferMinimalContainer""#,
160        "return",
161        "representation",
162        &[("include", "http://www.w3.org/ns/ldp#PreferMembership http://www.w3.org/ns/ldp#PreferMinimalContainer")]
163    )]
164    fn valid_preference_will_be_round_tripped_correctly(
165        #[case] preference_str: &str,
166        #[case] expected_token_name: &str,
167        #[case] expected_token_value: &str,
168        #[case] expected_params: &[(&str, &str)],
169    ) {
170        let preference = assert_valid_preference(preference_str);
171        assert_preference_match(
172            &preference,
173            expected_token_name,
174            expected_token_value,
175            expected_params,
176        );
177
178        let encoded_preference_str = preference.str_encode();
179        let round_tripped_preference = assert_valid_preference(&encoded_preference_str);
180        assert_preference_match(
181            &round_tripped_preference,
182            expected_token_name,
183            expected_token_value,
184            expected_params,
185        );
186    }
187}