haystack_server/
content.rs1use haystack_core::codecs::{CodecError, codec_for};
4use haystack_core::data::HGrid;
5
6const DEFAULT_MIME: &str = "text/zinc";
8
9const HBF_MIME: &str = "application/x-haystack-binary";
11
12const SUPPORTED: &[&str] = &[
14 "text/zinc",
15 "application/json",
16 "text/trio",
17 "application/json;v=3",
18 HBF_MIME,
19];
20
21pub fn parse_accept(accept_header: &str) -> &'static str {
26 let accept = accept_header.trim();
27 if accept.is_empty() || accept == "*/*" {
28 return DEFAULT_MIME;
29 }
30
31 let mut candidates: Vec<(&str, f32)> = Vec::new();
33
34 for part in accept.split(',') {
35 let part = part.trim();
36 let mut segments = part.splitn(2, ';');
37 let mime = segments.next().unwrap_or("").trim();
38
39 let quality = segments
41 .next()
42 .and_then(|params| {
43 for param in params.split(';') {
44 let param = param.trim();
45 if let Some(q_val) = param.strip_prefix("q=") {
46 return q_val.trim().parse::<f32>().ok();
47 }
48 }
49 None
50 })
51 .unwrap_or(1.0);
52
53 if mime == "application/json" && part.contains("v=3") {
55 candidates.push(("application/json;v=3", quality));
56 } else if mime == "*/*" {
57 candidates.push((DEFAULT_MIME, quality));
58 } else {
59 candidates.push((mime, quality));
60 }
61 }
62
63 candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
65
66 for (mime, _) in &candidates {
67 for supported in SUPPORTED {
68 if mime == supported {
69 return supported;
70 }
71 }
72 }
73
74 DEFAULT_MIME
75}
76
77pub fn decode_request_grid(body: &str, content_type: &str) -> Result<HGrid, CodecError> {
81 let mime = normalize_content_type(content_type);
82 let codec = codec_for(mime)
83 .unwrap_or_else(|| codec_for(DEFAULT_MIME).expect("default codec must exist"));
84 codec.decode_grid(body)
85}
86
87pub fn encode_response_grid(
92 grid: &HGrid,
93 accept: &str,
94) -> Result<(Vec<u8>, &'static str), CodecError> {
95 let mime = parse_accept(accept);
96
97 if mime == HBF_MIME {
98 let bytes = haystack_core::codecs::encode_grid_binary(grid).map_err(CodecError::Encode)?;
99 return Ok((bytes, HBF_MIME));
100 }
101
102 let codec = codec_for(mime)
103 .unwrap_or_else(|| codec_for(DEFAULT_MIME).expect("default codec must exist"));
104 let body = codec.encode_grid(grid)?;
105 for supported in SUPPORTED {
107 if *supported == mime {
108 return Ok((body.into_bytes(), supported));
109 }
110 }
111 Ok((body.into_bytes(), DEFAULT_MIME))
112}
113
114fn normalize_content_type(content_type: &str) -> &str {
116 let ct = content_type.trim();
117 if ct.is_empty() {
118 return DEFAULT_MIME;
119 }
120 if ct.starts_with(HBF_MIME) {
122 return HBF_MIME;
123 }
124 if ct.starts_with("application/json") && ct.contains("v=3") {
126 return "application/json;v=3";
127 }
128 let base = ct.split(';').next().unwrap_or(ct).trim();
130 for supported in SUPPORTED {
132 if base == *supported {
133 return supported;
134 }
135 }
136 DEFAULT_MIME
137}
138
139pub fn decode_request_grid_bytes(body: &[u8], content_type: &str) -> Result<HGrid, CodecError> {
144 let ct = normalize_content_type(content_type);
145 if ct == HBF_MIME {
146 return haystack_core::codecs::decode_grid_binary(body).map_err(CodecError::Encode);
147 }
148 let text = std::str::from_utf8(body).map_err(|e| CodecError::Encode(e.to_string()))?;
149 decode_request_grid(text, content_type)
150}
151
152#[allow(clippy::type_complexity)]
159pub fn encode_response_streaming(
160 grid: &HGrid,
161 accept: &str,
162) -> Result<(Vec<u8>, Vec<Vec<u8>>, &'static str), CodecError> {
163 let mime = parse_accept(accept);
164
165 if mime == HBF_MIME {
167 let bytes = haystack_core::codecs::encode_grid_binary(grid).map_err(CodecError::Encode)?;
168 return Ok((bytes, Vec::new(), HBF_MIME));
169 }
170
171 let codec = codec_for(mime)
172 .unwrap_or_else(|| codec_for(DEFAULT_MIME).expect("default codec must exist"));
173
174 let header = codec.encode_grid_header(grid)?;
175
176 const BATCH_SIZE: usize = 500;
178 let mut batches = Vec::with_capacity(grid.rows.len() / BATCH_SIZE + 1);
179 for chunk in grid.rows.chunks(BATCH_SIZE) {
180 let mut buf = Vec::new();
181 for row in chunk {
182 buf.extend_from_slice(&codec.encode_grid_row(&grid.cols, row)?);
183 }
184 batches.push(buf);
185 }
186
187 let ct = SUPPORTED
188 .iter()
189 .find(|&&s| s == mime)
190 .copied()
191 .unwrap_or(DEFAULT_MIME);
192 Ok((header, batches, ct))
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn parse_accept_empty() {
201 assert_eq!(parse_accept(""), "text/zinc");
202 }
203
204 #[test]
205 fn parse_accept_wildcard() {
206 assert_eq!(parse_accept("*/*"), "text/zinc");
207 }
208
209 #[test]
210 fn parse_accept_json() {
211 assert_eq!(parse_accept("application/json"), "application/json");
212 }
213
214 #[test]
215 fn parse_accept_zinc() {
216 assert_eq!(parse_accept("text/zinc"), "text/zinc");
217 }
218
219 #[test]
220 fn parse_accept_trio() {
221 assert_eq!(parse_accept("text/trio"), "text/trio");
222 }
223
224 #[test]
225 fn parse_accept_json_v3() {
226 assert_eq!(parse_accept("application/json;v=3"), "application/json;v=3");
227 }
228
229 #[test]
230 fn parse_accept_unsupported_falls_back() {
231 assert_eq!(parse_accept("text/html"), "text/zinc");
232 }
233
234 #[test]
235 fn parse_accept_multiple_with_quality() {
236 assert_eq!(
237 parse_accept("text/zinc;q=0.5, application/json;q=1.0"),
238 "application/json"
239 );
240 }
241
242 #[test]
243 fn normalize_content_type_empty() {
244 assert_eq!(normalize_content_type(""), "text/zinc");
245 }
246
247 #[test]
248 fn normalize_content_type_json_v3() {
249 assert_eq!(
250 normalize_content_type("application/json; v=3"),
251 "application/json;v=3"
252 );
253 }
254
255 #[test]
256 fn normalize_content_type_with_charset() {
257 assert_eq!(
258 normalize_content_type("text/zinc; charset=utf-8"),
259 "text/zinc"
260 );
261 }
262
263 #[test]
264 fn decode_request_grid_empty_zinc() {
265 let result = decode_request_grid("ver:\"3.0\"\nempty\n", "text/zinc");
267 assert!(result.is_ok());
268 }
269
270 #[test]
271 fn encode_response_grid_default() {
272 let grid = HGrid::new();
273 let result = encode_response_grid(&grid, "");
274 assert!(result.is_ok());
275 let (_, content_type) = result.unwrap();
276 assert_eq!(content_type, "text/zinc");
277 }
278
279 #[test]
280 fn parse_accept_hbf() {
281 assert_eq!(parse_accept(HBF_MIME), HBF_MIME);
282 }
283
284 #[test]
285 fn normalize_content_type_hbf() {
286 assert_eq!(normalize_content_type(HBF_MIME), HBF_MIME);
287 }
288
289 #[test]
290 fn encode_decode_hbf_round_trip() {
291 let grid = HGrid::new();
292 let (bytes, ct) = encode_response_grid(&grid, HBF_MIME).unwrap();
293 assert_eq!(ct, HBF_MIME);
294 let decoded = decode_request_grid_bytes(&bytes, HBF_MIME).unwrap();
295 assert!(decoded.is_empty());
296 }
297
298 #[test]
299 fn decode_request_grid_bytes_text_fallback() {
300 let result = decode_request_grid_bytes(b"ver:\"3.0\"\nempty\n", "text/zinc");
301 assert!(result.is_ok());
302 }
303
304 #[test]
305 fn streaming_zinc_matches_full_encode() {
306 use haystack_core::data::{HCol, HDict};
307 use haystack_core::kinds::{HRef, Kind};
308
309 let mut rows = Vec::new();
310 for i in 0..5 {
311 let mut d = HDict::new();
312 d.set(
313 String::from("id"),
314 Kind::Ref(HRef::from_val(format!("r{i}"))),
315 );
316 d.set(String::from("dis"), Kind::Str(format!("Row {i}")));
317 rows.push(d);
318 }
319 let cols = vec![
320 HCol::new(String::from("id")),
321 HCol::new(String::from("dis")),
322 ];
323 let grid = HGrid::from_parts(HDict::new(), cols, rows);
324
325 let (full_bytes, ct) = encode_response_grid(&grid, "text/zinc").unwrap();
327 assert_eq!(ct, "text/zinc");
328
329 let (header, row_chunks, ct2) = encode_response_streaming(&grid, "text/zinc").unwrap();
331 assert_eq!(ct2, "text/zinc");
332
333 let mut streamed = header;
334 for chunk in row_chunks {
335 streamed.extend_from_slice(&chunk);
336 }
337 assert_eq!(full_bytes, streamed);
338 }
339}