headers/util/
flat_csv.rs

1use std::fmt;
2use std::iter::FromIterator;
3use std::marker::PhantomData;
4
5use bytes::BytesMut;
6use http::HeaderValue;
7
8use crate::util::TryFromValues;
9use crate::Error;
10
11// A single `HeaderValue` that can flatten multiple values with commas.
12#[derive(Clone, PartialEq, Eq, Hash)]
13pub(crate) struct FlatCsv<Sep = Comma> {
14    pub(crate) value: HeaderValue,
15    _marker: PhantomData<Sep>,
16}
17
18pub(crate) trait Separator {
19    const BYTE: u8;
20    const CHAR: char;
21}
22
23#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24pub(crate) enum Comma {}
25
26impl Separator for Comma {
27    const BYTE: u8 = b',';
28    const CHAR: char = ',';
29}
30
31#[derive(Clone, Debug, PartialEq, Eq, Hash)]
32pub(crate) enum SemiColon {}
33
34impl Separator for SemiColon {
35    const BYTE: u8 = b';';
36    const CHAR: char = ';';
37}
38
39impl<Sep: Separator> FlatCsv<Sep> {
40    pub(crate) fn iter(&self) -> impl Iterator<Item = &str> {
41        self.value.to_str().ok().into_iter().flat_map(|value_str| {
42            let mut in_quotes = false;
43            value_str
44                .split(move |c| {
45                    #[allow(clippy::collapsible_else_if)]
46                    if in_quotes {
47                        if c == '"' {
48                            in_quotes = false;
49                        }
50                        false // dont split
51                    } else {
52                        if c == Sep::CHAR {
53                            true // split
54                        } else {
55                            if c == '"' {
56                                in_quotes = true;
57                            }
58                            false // dont split
59                        }
60                    }
61                })
62                .map(|item| item.trim())
63        })
64    }
65}
66
67impl<Sep: Separator> TryFromValues for FlatCsv<Sep> {
68    fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
69    where
70        I: Iterator<Item = &'i HeaderValue>,
71    {
72        let flat = values.collect();
73        Ok(flat)
74    }
75}
76
77impl<Sep> From<HeaderValue> for FlatCsv<Sep> {
78    fn from(value: HeaderValue) -> Self {
79        FlatCsv {
80            value,
81            _marker: PhantomData,
82        }
83    }
84}
85
86impl<'a, Sep> From<&'a FlatCsv<Sep>> for HeaderValue {
87    fn from(flat: &'a FlatCsv<Sep>) -> HeaderValue {
88        flat.value.clone()
89    }
90}
91
92impl<Sep> fmt::Debug for FlatCsv<Sep> {
93    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94        fmt::Debug::fmt(&self.value, f)
95    }
96}
97
98impl<'a, Sep: Separator> FromIterator<&'a HeaderValue> for FlatCsv<Sep> {
99    fn from_iter<I>(iter: I) -> Self
100    where
101        I: IntoIterator<Item = &'a HeaderValue>,
102    {
103        let mut values = iter.into_iter();
104
105        // Common case is there is only 1 value, optimize for that
106        if let (1, Some(1)) = values.size_hint() {
107            return values
108                .next()
109                .expect("size_hint claimed 1 item")
110                .clone()
111                .into();
112        }
113
114        // Otherwise, there are multiple, so this should merge them into 1.
115        let mut buf = values
116            .next()
117            .cloned()
118            .map(|val| BytesMut::from(val.as_bytes()))
119            .unwrap_or_default();
120
121        for val in values {
122            buf.extend_from_slice(&[Sep::BYTE, b' ']);
123            buf.extend_from_slice(val.as_bytes());
124        }
125
126        let val = HeaderValue::from_maybe_shared(buf.freeze())
127            .expect("comma separated HeaderValues are valid");
128
129        val.into()
130    }
131}
132
133// TODO: would be great if there was a way to de-dupe these with above
134impl<Sep: Separator> FromIterator<HeaderValue> for FlatCsv<Sep> {
135    fn from_iter<I>(iter: I) -> Self
136    where
137        I: IntoIterator<Item = HeaderValue>,
138    {
139        let mut values = iter.into_iter();
140
141        // Common case is there is only 1 value, optimize for that
142        if let (1, Some(1)) = values.size_hint() {
143            return values.next().expect("size_hint claimed 1 item").into();
144        }
145
146        // Otherwise, there are multiple, so this should merge them into 1.
147        let mut buf = values
148            .next()
149            .map(|val| BytesMut::from(val.as_bytes()))
150            .unwrap_or_default();
151
152        for val in values {
153            buf.extend_from_slice(&[Sep::BYTE, b' ']);
154            buf.extend_from_slice(val.as_bytes());
155        }
156
157        let val = HeaderValue::from_maybe_shared(buf.freeze())
158            .expect("comma separated HeaderValues are valid");
159
160        val.into()
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn comma() {
170        let val = HeaderValue::from_static("aaa, b; bb, ccc");
171        let csv = FlatCsv::<Comma>::from(val);
172
173        let mut values = csv.iter();
174        assert_eq!(values.next(), Some("aaa"));
175        assert_eq!(values.next(), Some("b; bb"));
176        assert_eq!(values.next(), Some("ccc"));
177        assert_eq!(values.next(), None);
178    }
179
180    #[test]
181    fn semicolon() {
182        let val = HeaderValue::from_static("aaa; b, bb; ccc");
183        let csv = FlatCsv::<SemiColon>::from(val);
184
185        let mut values = csv.iter();
186        assert_eq!(values.next(), Some("aaa"));
187        assert_eq!(values.next(), Some("b, bb"));
188        assert_eq!(values.next(), Some("ccc"));
189        assert_eq!(values.next(), None);
190    }
191
192    #[test]
193    fn quoted_text() {
194        let val = HeaderValue::from_static("foo=\"bar,baz\", sherlock=holmes");
195        let csv = FlatCsv::<Comma>::from(val);
196
197        let mut values = csv.iter();
198        assert_eq!(values.next(), Some("foo=\"bar,baz\""));
199        assert_eq!(values.next(), Some("sherlock=holmes"));
200        assert_eq!(values.next(), None);
201    }
202}