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<(String, &'static str), CodecError> {
90 let mime = parse_accept(accept);
91 let codec = codec_for(mime)
92 .unwrap_or_else(|| codec_for(DEFAULT_MIME).expect("default codec must exist"));
93 let body = codec.encode_grid(grid)?;
94 for supported in SUPPORTED {
96 if *supported == mime {
97 return Ok((body, supported));
98 }
99 }
100 Ok((body, DEFAULT_MIME))
101}
102
103fn normalize_content_type(content_type: &str) -> &str {
105 let ct = content_type.trim();
106 if ct.is_empty() {
107 return DEFAULT_MIME;
108 }
109 if ct.starts_with("application/json") && ct.contains("v=3") {
111 return "application/json;v=3";
112 }
113 let base = ct.split(';').next().unwrap_or(ct).trim();
115 for supported in SUPPORTED {
117 if base == *supported {
118 return supported;
119 }
120 }
121 DEFAULT_MIME
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn parse_accept_empty() {
130 assert_eq!(parse_accept(""), "text/zinc");
131 }
132
133 #[test]
134 fn parse_accept_wildcard() {
135 assert_eq!(parse_accept("*/*"), "text/zinc");
136 }
137
138 #[test]
139 fn parse_accept_json() {
140 assert_eq!(parse_accept("application/json"), "application/json");
141 }
142
143 #[test]
144 fn parse_accept_zinc() {
145 assert_eq!(parse_accept("text/zinc"), "text/zinc");
146 }
147
148 #[test]
149 fn parse_accept_trio() {
150 assert_eq!(parse_accept("text/trio"), "text/trio");
151 }
152
153 #[test]
154 fn parse_accept_json_v3() {
155 assert_eq!(parse_accept("application/json;v=3"), "application/json;v=3");
156 }
157
158 #[test]
159 fn parse_accept_unsupported_falls_back() {
160 assert_eq!(parse_accept("text/html"), "text/zinc");
161 }
162
163 #[test]
164 fn parse_accept_multiple_with_quality() {
165 assert_eq!(
166 parse_accept("text/zinc;q=0.5, application/json;q=1.0"),
167 "application/json"
168 );
169 }
170
171 #[test]
172 fn normalize_content_type_empty() {
173 assert_eq!(normalize_content_type(""), "text/zinc");
174 }
175
176 #[test]
177 fn normalize_content_type_json_v3() {
178 assert_eq!(
179 normalize_content_type("application/json; v=3"),
180 "application/json;v=3"
181 );
182 }
183
184 #[test]
185 fn normalize_content_type_with_charset() {
186 assert_eq!(
187 normalize_content_type("text/zinc; charset=utf-8"),
188 "text/zinc"
189 );
190 }
191
192 #[test]
193 fn decode_request_grid_empty_zinc() {
194 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}