1use http::{HeaderMap, HeaderValue};
4use postrust_core::ApiRequest;
5use std::fmt;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct ContentRange {
14 pub start: i64,
16 pub end: i64,
18 pub total: Option<i64>,
20}
21
22impl ContentRange {
23 pub fn new(start: i64, end: i64, total: Option<i64>) -> Self {
25 Self { start, end, total }
26 }
27
28 pub fn from_pagination(offset: i64, count: i64, total: Option<i64>) -> Self {
35 Self::new(offset, offset + count - 1, total)
36 }
37
38 pub fn status(&self) -> http::StatusCode {
43 match self.total {
44 None => http::StatusCode::OK,
45 Some(total) => {
46 if self.start > total {
47 http::StatusCode::RANGE_NOT_SATISFIABLE
48 } else if (1 + self.end - self.start) < total {
49 http::StatusCode::PARTIAL_CONTENT
50 } else {
51 http::StatusCode::OK
52 }
53 }
54 }
55 }
56}
57
58impl fmt::Display for ContentRange {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 if self.total == Some(0) || self.start > self.end {
62 write!(f, "*")?;
63 } else {
64 write!(f, "{}-{}", self.start, self.end)?;
65 }
66 match self.total {
67 Some(total) => write!(f, "/{total}"),
68 None => write!(f, "/*"),
69 }
70 }
71}
72
73#[deprecated(
88 since = "0.4.0",
89 note = "unused within the workspace and drifted from the live header path; \
90 responses are built by postrust_response::format_response"
91)]
92pub fn build_response_headers(
93 request: &ApiRequest,
94 content_type: &str,
95 content_range: Option<&ContentRange>,
96 location: Option<&str>,
97) -> HeaderMap {
98 let mut headers = HeaderMap::new();
99
100 if let Ok(v) = HeaderValue::from_str(content_type) {
102 headers.insert(http::header::CONTENT_TYPE, v);
103 }
104
105 if let Some(range) = content_range {
107 if let Ok(v) = HeaderValue::from_str(&range.to_string()) {
108 headers.insert(http::header::CONTENT_RANGE, v);
109 }
110 }
111
112 if let Some(loc) = location {
114 if let Ok(v) = HeaderValue::from_str(loc) {
115 headers.insert(http::header::LOCATION, v);
116 }
117 }
118
119 if request.negotiated_by_profile {
121 if let Ok(v) = HeaderValue::from_str(&request.schema) {
122 headers.insert(http::header::HeaderName::from_static("content-profile"), v);
123 }
124 }
125
126 if let Some(applied) = postrust_core::api_request::preferences::preference_applied(
128 &request.preferences,
129 crate::preference_scope(request),
130 ) {
131 if let Ok(v) = HeaderValue::from_str(&applied) {
132 headers.insert(
133 http::header::HeaderName::from_static("preference-applied"),
134 v,
135 );
136 }
137 }
138
139 headers
140}
141
142pub fn parse_guc_headers(guc_headers: &str) -> Option<Vec<(String, String)>> {
152 let Ok(serde_json::Value::Array(entries)) = serde_json::from_str(guc_headers) else {
153 return None;
154 };
155
156 let mut headers = Vec::with_capacity(entries.len());
157 for entry in entries {
158 let serde_json::Value::Object(fields) = entry else {
159 return None;
160 };
161 if fields.len() != 1 {
162 return None;
163 }
164 for (name, value) in fields {
165 let serde_json::Value::String(value) = value else {
166 return None;
167 };
168 headers.push((name, value));
169 }
170 }
171
172 Some(headers)
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn test_content_range_display() {
181 assert_eq!(ContentRange::new(0, 9, Some(100)).to_string(), "0-9/100");
183 assert_eq!(ContentRange::new(10, 19, None).to_string(), "10-19/*");
184 }
185
186 #[test]
187 fn test_content_range_display_empty() {
188 assert_eq!(ContentRange::from_pagination(0, 0, None).to_string(), "*/*");
190 assert_eq!(
191 ContentRange::from_pagination(0, 0, Some(0)).to_string(),
192 "*/0"
193 );
194 }
195
196 #[test]
197 fn test_content_range_from_pagination() {
198 let range = ContentRange::from_pagination(0, 10, Some(100));
200 assert_eq!((range.start, range.end), (0, 9));
201
202 let range = ContentRange::from_pagination(90, 5, Some(95));
204 assert_eq!((range.start, range.end), (90, 94));
205 }
206
207 #[test]
208 fn test_content_range_status() {
209 assert_eq!(
211 ContentRange::from_pagination(0, 10, None).status(),
212 http::StatusCode::OK
213 );
214 assert_eq!(
216 ContentRange::from_pagination(0, 10, Some(100)).status(),
217 http::StatusCode::PARTIAL_CONTENT
218 );
219 assert_eq!(
221 ContentRange::from_pagination(0, 15, Some(15)).status(),
222 http::StatusCode::OK
223 );
224 assert_eq!(
226 ContentRange::from_pagination(50, 0, Some(15)).status(),
227 http::StatusCode::RANGE_NOT_SATISFIABLE
228 );
229 }
230
231 #[test]
232 fn test_parse_guc_headers() {
233 let guc = r#"[{"X-Custom-Header": "value1"}, {"X-Another": "value2"}]"#;
234 let headers = parse_guc_headers(guc).unwrap();
235
236 assert_eq!(headers.len(), 2);
237 assert_eq!(headers[0], ("X-Custom-Header".into(), "value1".into()));
238 assert_eq!(headers[1], ("X-Another".into(), "value2".into()));
239 }
240
241 #[test]
243 fn guc_headers_may_repeat_a_name() {
244 let guc = r#"[{"Set-Cookie": "a=1"}, {"Set-Cookie": "b=2"}]"#;
245 let headers = parse_guc_headers(guc).unwrap();
246
247 assert_eq!(headers.len(), 2);
248 assert!(headers.iter().all(|(name, _)| name == "Set-Cookie"));
249 }
250
251 #[test]
253 fn guc_headers_must_be_single_key_objects_of_strings() {
254 for bad in [
255 r#"{"X": "y"}"#,
256 r#"[{"X": "y", "Z": "w"}]"#,
257 r#"[{"X": 1}]"#,
258 r#"["X: y"]"#,
259 "not json",
260 ] {
261 assert!(parse_guc_headers(bad).is_none(), "accepted {bad}");
262 }
263 }
264}