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#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum ValuesOrAny<T> {
12 Values(NonEmptyVec<T>),
14 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 } else {
64 if c == sep_char {
65 true } else {
67 if c == '"' {
68 in_quotes = true;
69 }
70 false }
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 } else {
164 if c == sep_char {
165 true } else {
167 if c == '"' {
168 in_quotes = true;
169 }
170 false }
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}