Skip to main content

postrust_response/
headers.rs

1//! Response header building.
2
3use http::{HeaderMap, HeaderValue};
4use postrust_core::ApiRequest;
5use std::fmt;
6
7/// Content-Range header value.
8#[derive(Clone, Debug)]
9pub struct ContentRange {
10    /// Start of range (0-based)
11    pub start: i64,
12    /// End of range (inclusive)
13    pub end: i64,
14    /// Total count (or None if unknown)
15    pub total: Option<i64>,
16    /// Unit name
17    pub unit: String,
18}
19
20impl ContentRange {
21    /// Create a new content range.
22    pub fn new(start: i64, end: i64, total: Option<i64>) -> Self {
23        Self {
24            start,
25            end,
26            total,
27            unit: "items".to_string(),
28        }
29    }
30
31    /// Create from offset, limit, and total.
32    pub fn from_pagination(
33        offset: i64,
34        limit: Option<i64>,
35        count: i64,
36        total: Option<i64>,
37    ) -> Self {
38        let end = match limit {
39            Some(l) => (offset + l - 1).min(offset + count - 1).max(offset),
40            None => offset + count - 1,
41        };
42
43        Self::new(offset, end, total)
44    }
45}
46
47impl fmt::Display for ContentRange {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self.total {
50            Some(total) => write!(f, "{} {}-{}/{}", self.unit, self.start, self.end, total),
51            None => write!(f, "{} {}-{}/*", self.unit, self.start, self.end),
52        }
53    }
54}
55
56/// Build response headers based on request and result.
57pub fn build_response_headers(
58    request: &ApiRequest,
59    content_type: &str,
60    content_range: Option<&ContentRange>,
61    location: Option<&str>,
62) -> HeaderMap {
63    let mut headers = HeaderMap::new();
64
65    // Content-Type
66    if let Ok(v) = HeaderValue::from_str(content_type) {
67        headers.insert(http::header::CONTENT_TYPE, v);
68    }
69
70    // Content-Range
71    if let Some(range) = content_range {
72        if let Ok(v) = HeaderValue::from_str(&range.to_string()) {
73            headers.insert(http::header::CONTENT_RANGE, v);
74        }
75    }
76
77    // Location
78    if let Some(loc) = location {
79        if let Ok(v) = HeaderValue::from_str(loc) {
80            headers.insert(http::header::LOCATION, v);
81        }
82    }
83
84    // Content-Profile
85    if request.negotiated_by_profile {
86        if let Ok(v) = HeaderValue::from_str(&request.schema) {
87            headers.insert(http::header::HeaderName::from_static("content-profile"), v);
88        }
89    }
90
91    // Preference-Applied
92    if let Some(applied) =
93        postrust_core::api_request::preferences::preference_applied(&request.preferences)
94    {
95        if let Ok(v) = HeaderValue::from_str(&applied) {
96            headers.insert(
97                http::header::HeaderName::from_static("preference-applied"),
98                v,
99            );
100        }
101    }
102
103    headers
104}
105
106/// Parse GUC headers from database response.
107#[allow(dead_code)] // Reserved for GUC-driven response headers (`response.headers`); not yet wired.
108pub fn parse_guc_headers(guc_headers: &str) -> Vec<(String, String)> {
109    // Format: "header1: value1\nheader2: value2"
110    guc_headers
111        .lines()
112        .filter_map(|line| {
113            let mut parts = line.splitn(2, ':');
114            let key = parts.next()?.trim().to_string();
115            let value = parts.next()?.trim().to_string();
116            Some((key, value))
117        })
118        .collect()
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_content_range_display() {
127        let range = ContentRange::new(0, 9, Some(100));
128        assert_eq!(range.to_string(), "items 0-9/100");
129
130        let range = ContentRange::new(10, 19, None);
131        assert_eq!(range.to_string(), "items 10-19/*");
132    }
133
134    #[test]
135    fn test_content_range_from_pagination() {
136        // First page of 10
137        let range = ContentRange::from_pagination(0, Some(10), 10, Some(100));
138        assert_eq!(range.start, 0);
139        assert_eq!(range.end, 9);
140
141        // Partial last page
142        let range = ContentRange::from_pagination(90, Some(10), 5, Some(95));
143        assert_eq!(range.start, 90);
144        assert_eq!(range.end, 94);
145    }
146
147    #[test]
148    fn test_parse_guc_headers() {
149        let guc = "X-Custom-Header: value1\nX-Another: value2";
150        let headers = parse_guc_headers(guc);
151
152        assert_eq!(headers.len(), 2);
153        assert_eq!(headers[0], ("X-Custom-Header".into(), "value1".into()));
154        assert_eq!(headers[1], ("X-Another".into(), "value2".into()));
155    }
156}