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