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
152pub fn encode_response_streaming(
159 grid: &HGrid,
160 accept: &str,
161) -> Result<(Vec<u8>, Vec<Vec<u8>>, &'static str), CodecError> {
162 let mime = parse_accept(accept);
163
164 if mime == HBF_MIME {
166 let bytes = haystack_core::codecs::encode_grid_binary(grid).map_err(CodecError::Encode)?;
167 return Ok((bytes, Vec::new(), HBF_MIME));
168 }
169
170 let codec = codec_for(mime)
171 .unwrap_or_else(|| codec_for(DEFAULT_MIME).expect("default codec must exist"));
172
173 let header = codec.encode_grid_header(grid)?;
174
175 const BATCH_SIZE: usize = 500;
177 let mut batches = Vec::with_capacity(grid.rows.len() / BATCH_SIZE + 1);
178 for chunk in grid.rows.chunks(BATCH_SIZE) {
179 let mut buf = Vec::new();
180 for row in chunk {
181 buf.extend_from_slice(&codec.encode_grid_row(&grid.cols, row)?);
182 }
183 batches.push(buf);
184 }
185
186 let ct = SUPPORTED
187 .iter()
188 .find(|&&s| s == mime)
189 .copied()
190 .unwrap_or(DEFAULT_MIME);
191 Ok((header, batches, ct))
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn parse_accept_empty() {
200 assert_eq!(parse_accept(""), "text/zinc");
201 }
202
203 #[test]
204 fn parse_accept_wildcard() {
205 assert_eq!(parse_accept("*/*"), "text/zinc");
206 }
207
208 #[test]
209 fn parse_accept_json() {
210 assert_eq!(parse_accept("application/json"), "application/json");
211 }
212
213 #[test]
214 fn parse_accept_zinc() {
215 assert_eq!(parse_accept("text/zinc"), "text/zinc");
216 }
217
218 #[test]
219 fn parse_accept_trio() {
220 assert_eq!(parse_accept("text/trio"), "text/trio");
221 }
222
223 #[test]
224 fn parse_accept_json_v3() {
225 assert_eq!(parse_accept("application/json;v=3"), "application/json;v=3");
226 }
227
228 #[test]
229 fn parse_accept_unsupported_falls_back() {
230 assert_eq!(parse_accept("text/html"), "text/zinc");
231 }
232
233 #[test]
234 fn parse_accept_multiple_with_quality() {
235 assert_eq!(
236 parse_accept("text/zinc;q=0.5, application/json;q=1.0"),
237 "application/json"
238 );
239 }
240
241 #[test]
242 fn normalize_content_type_empty() {
243 assert_eq!(normalize_content_type(""), "text/zinc");
244 }
245
246 #[test]
247 fn normalize_content_type_json_v3() {
248 assert_eq!(
249 normalize_content_type("application/json; v=3"),
250 "application/json;v=3"
251 );
252 }
253
254 #[test]
255 fn normalize_content_type_with_charset() {
256 assert_eq!(
257 normalize_content_type("text/zinc; charset=utf-8"),
258 "text/zinc"
259 );
260 }
261
262 #[test]
263 fn decode_request_grid_empty_zinc() {
264 let result = decode_request_grid("ver:\"3.0\"\nempty\n", "text/zinc");
266 assert!(result.is_ok());
267 }
268
269 #[test]
270 fn encode_response_grid_default() {
271 let grid = HGrid::new();
272 let result = encode_response_grid(&grid, "");
273 assert!(result.is_ok());
274 let (_, content_type) = result.unwrap();
275 assert_eq!(content_type, "text/zinc");
276 }
277
278 #[test]
279 fn parse_accept_hbf() {
280 assert_eq!(parse_accept(HBF_MIME), HBF_MIME);
281 }
282
283 #[test]
284 fn normalize_content_type_hbf() {
285 assert_eq!(normalize_content_type(HBF_MIME), HBF_MIME);
286 }
287
288 #[test]
289 fn encode_decode_hbf_round_trip() {
290 let grid = HGrid::new();
291 let (bytes, ct) = encode_response_grid(&grid, HBF_MIME).unwrap();
292 assert_eq!(ct, HBF_MIME);
293 let decoded = decode_request_grid_bytes(&bytes, HBF_MIME).unwrap();
294 assert!(decoded.is_empty());
295 }
296
297 #[test]
298 fn decode_request_grid_bytes_text_fallback() {
299 let result = decode_request_grid_bytes(b"ver:\"3.0\"\nempty\n", "text/zinc");
300 assert!(result.is_ok());
301 }
302
303 #[test]
304 fn streaming_zinc_matches_full_encode() {
305 use haystack_core::data::{HCol, HDict};
306 use haystack_core::kinds::{HRef, Kind};
307
308 let mut rows = Vec::new();
309 for i in 0..5 {
310 let mut d = HDict::new();
311 d.set(
312 String::from("id"),
313 Kind::Ref(HRef::from_val(&format!("r{i}"))),
314 );
315 d.set(String::from("dis"), Kind::Str(format!("Row {i}")));
316 rows.push(d);
317 }
318 let cols = vec![
319 HCol::new(String::from("id")),
320 HCol::new(String::from("dis")),
321 ];
322 let grid = HGrid::from_parts(HDict::new(), cols, rows);
323
324 let (full_bytes, ct) = encode_response_grid(&grid, "text/zinc").unwrap();
326 assert_eq!(ct, "text/zinc");
327
328 let (header, row_chunks, ct2) = encode_response_streaming(&grid, "text/zinc").unwrap();
330 assert_eq!(ct2, "text/zinc");
331
332 let mut streamed = header;
333 for chunk in row_chunks {
334 streamed.extend_from_slice(&chunk);
335 }
336 assert_eq!(full_bytes, streamed);
337 }
338}