haystack_server/
content.rs1use haystack_core::codecs::{CodecError, codec_for};
4use haystack_core::data::HGrid;
5
6const DEFAULT_MIME: &str = "text/zinc";
8
9const SUPPORTED: &[&str] = &[
11 "text/zinc",
12 "application/json",
13 "text/trio",
14 "application/json;v=3",
15];
16
17pub fn parse_accept(accept_header: &str) -> &'static str {
22 let accept = accept_header.trim();
23 if accept.is_empty() || accept == "*/*" {
24 return DEFAULT_MIME;
25 }
26
27 let mut candidates: Vec<(&str, f32)> = Vec::new();
29
30 for part in accept.split(',') {
31 let part = part.trim();
32 let mut segments = part.splitn(2, ';');
33 let mime = segments.next().unwrap_or("").trim();
34
35 let quality = segments
37 .next()
38 .and_then(|params| {
39 for param in params.split(';') {
40 let param = param.trim();
41 if let Some(q_val) = param.strip_prefix("q=") {
42 return q_val.trim().parse::<f32>().ok();
43 }
44 }
45 None
46 })
47 .unwrap_or(1.0);
48
49 if mime == "application/json" && part.contains("v=3") {
51 candidates.push(("application/json;v=3", quality));
52 } else if mime == "*/*" {
53 candidates.push((DEFAULT_MIME, quality));
54 } else {
55 candidates.push((mime, quality));
56 }
57 }
58
59 candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
61
62 for (mime, _) in &candidates {
63 for supported in SUPPORTED {
64 if mime == supported {
65 return supported;
66 }
67 }
68 }
69
70 DEFAULT_MIME
71}
72
73pub fn decode_request_grid(body: &str, content_type: &str) -> Result<HGrid, CodecError> {
77 let mime = normalize_content_type(content_type);
78 let codec = codec_for(mime)
79 .unwrap_or_else(|| codec_for(DEFAULT_MIME).expect("default codec must exist"));
80 codec.decode_grid(body)
81}
82
83pub fn encode_response_grid(
87 grid: &HGrid,
88 accept: &str,
89) -> Result<(Vec<u8>, &'static str), CodecError> {
90 let mime = parse_accept(accept);
91
92 let codec = codec_for(mime)
93 .unwrap_or_else(|| codec_for(DEFAULT_MIME).expect("default codec must exist"));
94 let body = codec.encode_grid(grid)?;
95 for supported in SUPPORTED {
97 if *supported == mime {
98 return Ok((body.into_bytes(), supported));
99 }
100 }
101 Ok((body.into_bytes(), DEFAULT_MIME))
102}
103
104fn normalize_content_type(content_type: &str) -> &str {
106 let ct = content_type.trim();
107 if ct.is_empty() {
108 return DEFAULT_MIME;
109 }
110 if ct.starts_with("application/json") && ct.contains("v=3") {
112 return "application/json;v=3";
113 }
114 let base = ct.split(';').next().unwrap_or(ct).trim();
116 for supported in SUPPORTED {
118 if base == *supported {
119 return supported;
120 }
121 }
122 DEFAULT_MIME
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn parse_accept_empty() {
131 assert_eq!(parse_accept(""), "text/zinc");
132 }
133
134 #[test]
135 fn parse_accept_wildcard() {
136 assert_eq!(parse_accept("*/*"), "text/zinc");
137 }
138
139 #[test]
140 fn parse_accept_json() {
141 assert_eq!(parse_accept("application/json"), "application/json");
142 }
143
144 #[test]
145 fn parse_accept_zinc() {
146 assert_eq!(parse_accept("text/zinc"), "text/zinc");
147 }
148
149 #[test]
150 fn parse_accept_trio() {
151 assert_eq!(parse_accept("text/trio"), "text/trio");
152 }
153
154 #[test]
155 fn parse_accept_json_v3() {
156 assert_eq!(parse_accept("application/json;v=3"), "application/json;v=3");
157 }
158
159 #[test]
160 fn parse_accept_unsupported_falls_back() {
161 assert_eq!(parse_accept("text/html"), "text/zinc");
162 }
163
164 #[test]
165 fn parse_accept_multiple_with_quality() {
166 assert_eq!(
167 parse_accept("text/zinc;q=0.5, application/json;q=1.0"),
168 "application/json"
169 );
170 }
171
172 #[test]
173 fn normalize_content_type_empty() {
174 assert_eq!(normalize_content_type(""), "text/zinc");
175 }
176
177 #[test]
178 fn normalize_content_type_json_v3() {
179 assert_eq!(
180 normalize_content_type("application/json; v=3"),
181 "application/json;v=3"
182 );
183 }
184
185 #[test]
186 fn normalize_content_type_with_charset() {
187 assert_eq!(
188 normalize_content_type("text/zinc; charset=utf-8"),
189 "text/zinc"
190 );
191 }
192
193 #[test]
194 fn decode_request_grid_empty_zinc() {
195 let result = decode_request_grid("ver:\"3.0\"\nempty\n", "text/zinc");
196 assert!(result.is_ok());
197 }
198
199 #[test]
200 fn encode_response_grid_default() {
201 let grid = HGrid::new();
202 let result = encode_response_grid(&grid, "");
203 assert!(result.is_ok());
204 let (_, content_type) = result.unwrap();
205 assert_eq!(content_type, "text/zinc");
206 }
207}