postrust_response/
lib.rs

1//! Response formatting for Postrust.
2//!
3//! Handles content negotiation and response formatting for JSON, CSV, and other formats.
4
5mod json;
6mod headers;
7
8pub use json::format_json_response;
9pub use headers::{build_response_headers, ContentRange};
10
11use http::{HeaderMap, HeaderValue, StatusCode};
12use postrust_core::{ActionPlan, ApiRequest, MediaType, PreferRepresentation};
13use serde::Serialize;
14
15/// A formatted HTTP response.
16#[derive(Clone, Debug)]
17pub struct Response {
18    /// HTTP status code
19    pub status: StatusCode,
20    /// Response headers
21    pub headers: HeaderMap,
22    /// Response body
23    pub body: bytes::Bytes,
24}
25
26impl Response {
27    /// Create a new response.
28    pub fn new(status: StatusCode, body: impl Into<bytes::Bytes>) -> Self {
29        Self {
30            status,
31            headers: HeaderMap::new(),
32            body: body.into(),
33        }
34    }
35
36    /// Create a JSON response.
37    pub fn json<T: Serialize>(status: StatusCode, value: &T) -> Result<Self, serde_json::Error> {
38        let body = serde_json::to_vec(value)?;
39        let mut response = Self::new(status, body);
40        response.set_content_type("application/json; charset=utf-8");
41        Ok(response)
42    }
43
44    /// Create an empty response.
45    pub fn empty(status: StatusCode) -> Self {
46        Self::new(status, bytes::Bytes::new())
47    }
48
49    /// Set a header.
50    pub fn set_header(&mut self, name: &str, value: &str) {
51        if let Ok(v) = HeaderValue::from_str(value) {
52            self.headers.insert(
53                http::header::HeaderName::from_bytes(name.as_bytes()).unwrap(),
54                v,
55            );
56        }
57    }
58
59    /// Set Content-Type header.
60    pub fn set_content_type(&mut self, content_type: &str) {
61        self.set_header("content-type", content_type);
62    }
63
64    /// Set Content-Range header.
65    pub fn set_content_range(&mut self, range: &ContentRange) {
66        self.set_header("content-range", &range.to_string());
67    }
68
69    /// Set Location header.
70    pub fn set_location(&mut self, location: &str) {
71        self.set_header("location", location);
72    }
73}
74
75/// Format a query result as a response.
76pub fn format_response(
77    request: &ApiRequest,
78    result: &QueryResult,
79) -> Result<Response, FormatError> {
80    let media_type = request
81        .accept_media_types
82        .first()
83        .cloned()
84        .unwrap_or(MediaType::ApplicationJson);
85
86    match &media_type {
87        MediaType::ApplicationJson => {
88            let body = format_json_response(&result.rows)?;
89            let mut response = Response::new(result.status, body);
90            response.set_content_type("application/json; charset=utf-8");
91            add_common_headers(&mut response, request, result);
92            Ok(response)
93        }
94        MediaType::TextCsv => {
95            // CSV formatting would go here
96            let body = format_csv_response(&result.rows)?;
97            let mut response = Response::new(result.status, body);
98            response.set_content_type("text/csv; charset=utf-8");
99            add_common_headers(&mut response, request, result);
100            Ok(response)
101        }
102        MediaType::SingularJson { nullable } => {
103            let body = format_singular_json(&result.rows, *nullable)?;
104            let mut response = Response::new(result.status, body);
105            response.set_content_type("application/vnd.pgrst.object+json; charset=utf-8");
106            add_common_headers(&mut response, request, result);
107            Ok(response)
108        }
109        _ => {
110            // Default to JSON
111            let body = format_json_response(&result.rows)?;
112            let mut response = Response::new(result.status, body);
113            response.set_content_type("application/json; charset=utf-8");
114            add_common_headers(&mut response, request, result);
115            Ok(response)
116        }
117    }
118}
119
120/// Add common response headers.
121fn add_common_headers(response: &mut Response, request: &ApiRequest, result: &QueryResult) {
122    // Content-Range
123    if let Some(range) = &result.content_range {
124        response.set_content_range(range);
125    }
126
127    // Location (for POST)
128    if let Some(location) = &result.location {
129        response.set_location(location);
130    }
131
132    // Preference-Applied
133    if let Some(applied) = postrust_core::api_request::preferences::preference_applied(&request.preferences) {
134        response.set_header("preference-applied", &applied);
135    }
136
137    // Content-Profile
138    if request.negotiated_by_profile {
139        response.set_header("content-profile", &request.schema);
140    }
141}
142
143/// Format singular JSON (single object or null).
144fn format_singular_json(rows: &[serde_json::Value], nullable: bool) -> Result<bytes::Bytes, FormatError> {
145    match rows.len() {
146        0 if nullable => Ok(bytes::Bytes::from_static(b"null")),
147        0 => Err(FormatError::NotFound),
148        1 => Ok(bytes::Bytes::from(serde_json::to_vec(&rows[0])?)),
149        _ => Err(FormatError::MultipleRows),
150    }
151}
152
153/// Format CSV response.
154fn format_csv_response(rows: &[serde_json::Value]) -> Result<bytes::Bytes, FormatError> {
155    if rows.is_empty() {
156        return Ok(bytes::Bytes::new());
157    }
158
159    let mut output = Vec::new();
160
161    // Get headers from first row
162    if let Some(first) = rows.first() {
163        if let serde_json::Value::Object(map) = first {
164            let headers: Vec<&str> = map.keys().map(|s| s.as_str()).collect();
165            output.extend_from_slice(headers.join(",").as_bytes());
166            output.push(b'\n');
167
168            // Write rows
169            for row in rows {
170                if let serde_json::Value::Object(row_map) = row {
171                    let values: Vec<String> = headers
172                        .iter()
173                        .map(|h| {
174                            row_map
175                                .get(*h)
176                                .map(|v| csv_escape(v))
177                                .unwrap_or_default()
178                        })
179                        .collect();
180                    output.extend_from_slice(values.join(",").as_bytes());
181                    output.push(b'\n');
182                }
183            }
184        }
185    }
186
187    Ok(bytes::Bytes::from(output))
188}
189
190/// Escape a value for CSV.
191fn csv_escape(value: &serde_json::Value) -> String {
192    match value {
193        serde_json::Value::String(s) => {
194            if s.contains(',') || s.contains('"') || s.contains('\n') {
195                format!("\"{}\"", s.replace('"', "\"\""))
196            } else {
197                s.clone()
198            }
199        }
200        serde_json::Value::Null => String::new(),
201        other => other.to_string(),
202    }
203}
204
205/// Query result for response formatting.
206#[derive(Clone, Debug, Default)]
207pub struct QueryResult {
208    /// HTTP status code
209    pub status: StatusCode,
210    /// Result rows
211    pub rows: Vec<serde_json::Value>,
212    /// Total row count (for pagination)
213    pub total_count: Option<i64>,
214    /// Content range
215    pub content_range: Option<ContentRange>,
216    /// Location header (for POST)
217    pub location: Option<String>,
218    /// Custom headers from GUC
219    pub guc_headers: Option<String>,
220    /// Custom status from GUC
221    pub guc_status: Option<String>,
222}
223
224/// Response formatting error.
225#[derive(Debug, thiserror::Error)]
226pub enum FormatError {
227    #[error("JSON serialization error: {0}")]
228    Json(#[from] serde_json::Error),
229
230    #[error("Resource not found")]
231    NotFound,
232
233    #[error("Multiple rows returned for singular response")]
234    MultipleRows,
235}
236
237impl FormatError {
238    pub fn status_code(&self) -> StatusCode {
239        match self {
240            Self::Json(_) => StatusCode::INTERNAL_SERVER_ERROR,
241            Self::NotFound => StatusCode::NOT_FOUND,
242            Self::MultipleRows => StatusCode::NOT_ACCEPTABLE,
243        }
244    }
245}