use http::{HeaderMap, HeaderValue};
use postrust_core::ApiRequest;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContentRange {
pub start: i64,
pub end: i64,
pub total: Option<i64>,
}
impl ContentRange {
pub fn new(start: i64, end: i64, total: Option<i64>) -> Self {
Self { start, end, total }
}
pub fn from_pagination(offset: i64, count: i64, total: Option<i64>) -> Self {
Self::new(offset, offset + count - 1, total)
}
pub fn status(&self) -> http::StatusCode {
match self.total {
None => http::StatusCode::OK,
Some(total) => {
if self.start > total {
http::StatusCode::RANGE_NOT_SATISFIABLE
} else if (1 + self.end - self.start) < total {
http::StatusCode::PARTIAL_CONTENT
} else {
http::StatusCode::OK
}
}
}
}
}
impl fmt::Display for ContentRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.total == Some(0) || self.start > self.end {
write!(f, "*")?;
} else {
write!(f, "{}-{}", self.start, self.end)?;
}
match self.total {
Some(total) => write!(f, "/{total}"),
None => write!(f, "/*"),
}
}
}
#[deprecated(
since = "0.4.0",
note = "unused within the workspace and drifted from the live header path; \
responses are built by postrust_response::format_response"
)]
pub fn build_response_headers(
request: &ApiRequest,
content_type: &str,
content_range: Option<&ContentRange>,
location: Option<&str>,
) -> HeaderMap {
let mut headers = HeaderMap::new();
if let Ok(v) = HeaderValue::from_str(content_type) {
headers.insert(http::header::CONTENT_TYPE, v);
}
if let Some(range) = content_range {
if let Ok(v) = HeaderValue::from_str(&range.to_string()) {
headers.insert(http::header::CONTENT_RANGE, v);
}
}
if let Some(loc) = location {
if let Ok(v) = HeaderValue::from_str(loc) {
headers.insert(http::header::LOCATION, v);
}
}
if request.negotiated_by_profile {
if let Ok(v) = HeaderValue::from_str(&request.schema) {
headers.insert(http::header::HeaderName::from_static("content-profile"), v);
}
}
if let Some(applied) = postrust_core::api_request::preferences::preference_applied(
&request.preferences,
crate::preference_scope(request),
) {
if let Ok(v) = HeaderValue::from_str(&applied) {
headers.insert(
http::header::HeaderName::from_static("preference-applied"),
v,
);
}
}
headers
}
pub fn parse_guc_headers(guc_headers: &str) -> Option<Vec<(String, String)>> {
let Ok(serde_json::Value::Array(entries)) = serde_json::from_str(guc_headers) else {
return None;
};
let mut headers = Vec::with_capacity(entries.len());
for entry in entries {
let serde_json::Value::Object(fields) = entry else {
return None;
};
if fields.len() != 1 {
return None;
}
for (name, value) in fields {
let serde_json::Value::String(value) = value else {
return None;
};
headers.push((name, value));
}
}
Some(headers)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_range_display() {
assert_eq!(ContentRange::new(0, 9, Some(100)).to_string(), "0-9/100");
assert_eq!(ContentRange::new(10, 19, None).to_string(), "10-19/*");
}
#[test]
fn test_content_range_display_empty() {
assert_eq!(ContentRange::from_pagination(0, 0, None).to_string(), "*/*");
assert_eq!(
ContentRange::from_pagination(0, 0, Some(0)).to_string(),
"*/0"
);
}
#[test]
fn test_content_range_from_pagination() {
let range = ContentRange::from_pagination(0, 10, Some(100));
assert_eq!((range.start, range.end), (0, 9));
let range = ContentRange::from_pagination(90, 5, Some(95));
assert_eq!((range.start, range.end), (90, 94));
}
#[test]
fn test_content_range_status() {
assert_eq!(
ContentRange::from_pagination(0, 10, None).status(),
http::StatusCode::OK
);
assert_eq!(
ContentRange::from_pagination(0, 10, Some(100)).status(),
http::StatusCode::PARTIAL_CONTENT
);
assert_eq!(
ContentRange::from_pagination(0, 15, Some(15)).status(),
http::StatusCode::OK
);
assert_eq!(
ContentRange::from_pagination(50, 0, Some(15)).status(),
http::StatusCode::RANGE_NOT_SATISFIABLE
);
}
#[test]
fn test_parse_guc_headers() {
let guc = r#"[{"X-Custom-Header": "value1"}, {"X-Another": "value2"}]"#;
let headers = parse_guc_headers(guc).unwrap();
assert_eq!(headers.len(), 2);
assert_eq!(headers[0], ("X-Custom-Header".into(), "value1".into()));
assert_eq!(headers[1], ("X-Another".into(), "value2".into()));
}
#[test]
fn guc_headers_may_repeat_a_name() {
let guc = r#"[{"Set-Cookie": "a=1"}, {"Set-Cookie": "b=2"}]"#;
let headers = parse_guc_headers(guc).unwrap();
assert_eq!(headers.len(), 2);
assert!(headers.iter().all(|(name, _)| name == "Set-Cookie"));
}
#[test]
fn guc_headers_must_be_single_key_objects_of_strings() {
for bad in [
r#"{"X": "y"}"#,
r#"[{"X": "y", "Z": "w"}]"#,
r#"[{"X": 1}]"#,
r#"["X: y"]"#,
"not json",
] {
assert!(parse_guc_headers(bad).is_none(), "accepted {bad}");
}
}
}