Skip to main content

rama_http_headers/util/
flat_csv.rs

1use std::fmt::Display;
2use std::str::FromStr;
3
4use rama_core::error::{BoxError, ErrorContext as _};
5use rama_http_types::HeaderValue;
6use rama_utils::collections::{NonEmptySmallVec, NonEmptyVec};
7
8/// Header value which is either any `*` or
9/// the given values separated by the defined separator.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum ValuesOrAny<T> {
12    /// The specific values as defined in order.
13    Values(NonEmptyVec<T>),
14    /// The any `*` value, also referred to as "wildcard".
15    Any,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
19pub(crate) enum FlatCsvSeparator {
20    #[default]
21    Comma,
22    SemiColon,
23}
24
25impl FlatCsvSeparator {
26    fn as_byte(self) -> u8 {
27        match self {
28            Self::Comma => b',',
29            Self::SemiColon => b';',
30        }
31    }
32
33    fn as_char(self) -> char {
34        match self {
35            Self::Comma => ',',
36            Self::SemiColon => ';',
37        }
38    }
39}
40
41pub(crate) fn try_decode_flat_csv_header_values_as_non_empty_vec<'a, T>(
42    values: impl IntoIterator<Item = &'a HeaderValue>,
43    sep: FlatCsvSeparator,
44) -> Result<NonEmptyVec<T>, BoxError>
45where
46    T: FromStr<Err: Into<BoxError>>,
47{
48    let mut in_quotes = false;
49    let sep_char = sep.as_char();
50    let mut iter = values
51        .into_iter()
52        .flat_map(|v| {
53            let s = v
54                .to_str()
55                .context("header value is not a valid utf-8 str")?;
56            Ok::<_, BoxError>(s.split(move |c| {
57                #[expect(clippy::collapsible_else_if)]
58                if in_quotes {
59                    if c == '"' {
60                        in_quotes = false;
61                    }
62                    false // don't split
63                } else {
64                    if c == sep_char {
65                        true // split
66                    } else {
67                        if c == '"' {
68                            in_quotes = true;
69                        }
70                        false // don't split
71                    }
72                }
73            }))
74        })
75        .flatten()
76        .map(|s| s.trim().parse::<T>());
77
78    let mut vec = NonEmptyVec::new(
79        iter.next()
80            .context("header value is an empty (CSV?)")?
81            .context("parse header value CSV colum from str")?,
82    );
83    for result in iter {
84        vec.push(result.context("parse header value CSV colum from str")?);
85    }
86    Ok(vec)
87}
88
89pub(crate) fn try_encode_non_empty_vec_as_flat_csv_header_value<T>(
90    values: &NonEmptyVec<T>,
91    sep: FlatCsvSeparator,
92) -> Result<HeaderValue, BoxError>
93where
94    T: Display,
95{
96    use std::io::Write as _;
97
98    let mut v = Vec::new();
99
100    let sep_byte = sep.as_byte();
101
102    _ = write!(&mut v, "{}", values.head);
103
104    for value in values.tail.iter() {
105        v.push(sep_byte);
106        v.push(b' ');
107        _ = write!(&mut v, "{value}");
108    }
109
110    HeaderValue::try_from(v).context("turn encoded bytes into HeaderValue")
111}
112
113pub(crate) fn try_encode_non_empty_vec_of_bytes_as_flat_csv_header_value<T>(
114    values: &NonEmptyVec<T>,
115    sep: FlatCsvSeparator,
116) -> Result<HeaderValue, BoxError>
117where
118    T: AsRef<[u8]>,
119{
120    let mut v = Vec::with_capacity(
121        values
122            .iter()
123            .map(|value| value.as_ref().len())
124            .sum::<usize>()
125            + 2 * values.len(),
126    );
127
128    let sep_byte = sep.as_byte();
129
130    v.extend(values.head.as_ref());
131
132    for value in values.tail.iter() {
133        v.push(sep_byte);
134        v.push(b' ');
135        v.extend(value.as_ref());
136    }
137
138    HeaderValue::try_from(v).context("turn encoded bytes into HeaderValue")
139}
140
141pub(crate) fn try_decode_flat_csv_header_values_as_non_empty_smallvec<'a, const N: usize, T>(
142    values: impl IntoIterator<Item = &'a HeaderValue>,
143    sep: FlatCsvSeparator,
144) -> Result<NonEmptySmallVec<N, T>, BoxError>
145where
146    T: FromStr<Err: Into<BoxError>>,
147{
148    let mut in_quotes = false;
149    let sep_char = sep.as_char();
150    let mut iter = values
151        .into_iter()
152        .flat_map(|v| {
153            let s = v
154                .to_str()
155                .context("header value is not a valid utf-8 str")?;
156            Ok::<_, BoxError>(s.split(move |c| {
157                #[expect(clippy::collapsible_else_if)]
158                if in_quotes {
159                    if c == '"' {
160                        in_quotes = false;
161                    }
162                    false // don't split
163                } else {
164                    if c == sep_char {
165                        true // split
166                    } else {
167                        if c == '"' {
168                            in_quotes = true;
169                        }
170                        false // don't split
171                    }
172                }
173            }))
174        })
175        .flatten()
176        .map(|s| s.trim().parse::<T>());
177
178    let mut vec = NonEmptySmallVec::new(
179        iter.next()
180            .context("header value is an empty (CSV?)")?
181            .context("parse header value CSV colum from str")?,
182    );
183    for result in iter {
184        vec.push(result.context("parse header value CSV colum from str")?);
185    }
186    Ok(vec)
187}
188
189pub(crate) fn try_encode_non_empty_smallvec_as_flat_csv_header_value<const N: usize, T>(
190    values: &NonEmptySmallVec<N, T>,
191    sep: FlatCsvSeparator,
192) -> Result<HeaderValue, BoxError>
193where
194    T: Display,
195{
196    use std::io::Write as _;
197
198    let mut v = Vec::new();
199
200    let sep_byte = sep.as_byte();
201
202    _ = write!(&mut v, "{}", values.head);
203
204    for value in values.tail.iter() {
205        v.push(sep_byte);
206        v.push(b' ');
207        _ = write!(&mut v, "{value}");
208    }
209
210    HeaderValue::try_from(v).context("turn encoded bytes into HeaderValue")
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use rama_utils::collections::non_empty_vec;
217
218    #[test]
219    fn decode_flat_csv_into_non_empty_vec() {
220        for (header_values, separator, expected) in [
221            (
222                vec![HeaderValue::from_static("aaa, b; bb, ccc")],
223                FlatCsvSeparator::SemiColon,
224                non_empty_vec![String::from("aaa, b"), String::from("bb, ccc")],
225            ),
226            (
227                vec![HeaderValue::from_static("aaa; b, bb; ccc")],
228                FlatCsvSeparator::Comma,
229                non_empty_vec![String::from("aaa; b"), String::from("bb; ccc")],
230            ),
231            (
232                vec![HeaderValue::from_static("foo=\"bar,baz\", sherlock=holmes")],
233                FlatCsvSeparator::Comma,
234                non_empty_vec![
235                    String::from("foo=\"bar,baz\""),
236                    String::from("sherlock=holmes")
237                ],
238            ),
239            (
240                vec![
241                    HeaderValue::from_static("foo=\"bar,baz\", sherlock=holmes"),
242                    HeaderValue::from_static("answer=42"),
243                ],
244                FlatCsvSeparator::Comma,
245                non_empty_vec![
246                    String::from("foo=\"bar,baz\""),
247                    String::from("sherlock=holmes"),
248                    String::from("answer=42")
249                ],
250            ),
251        ] {
252            let values =
253                try_decode_flat_csv_header_values_as_non_empty_vec(header_values.iter(), separator)
254                    .unwrap();
255            assert_eq!(expected, values);
256        }
257    }
258
259    #[test]
260    fn encode_non_empty_vec_as_flat_csv() {
261        for (values, separator, expected) in [
262            (
263                non_empty_vec![String::from("aaa, b"), String::from("bb, ccc")],
264                FlatCsvSeparator::SemiColon,
265                "aaa, b; bb, ccc",
266            ),
267            (
268                non_empty_vec![String::from("aaa; b"), String::from("bb; ccc")],
269                FlatCsvSeparator::Comma,
270                "aaa; b, bb; ccc",
271            ),
272            (
273                non_empty_vec![
274                    String::from("foo=\"bar,baz\""),
275                    String::from("sherlock=holmes")
276                ],
277                FlatCsvSeparator::Comma,
278                "foo=\"bar,baz\", sherlock=holmes",
279            ),
280        ] {
281            let header_value =
282                try_encode_non_empty_vec_as_flat_csv_header_value(&values, separator).unwrap();
283            assert_eq!(expected, header_value.to_str().unwrap());
284        }
285    }
286}