Skip to main content

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 headers;
6mod json;
7
8pub use headers::{build_response_headers, ContentRange};
9pub use json::format_json_response;
10
11use http::{HeaderMap, HeaderValue, StatusCode};
12use postrust_core::{ApiRequest, MediaType};
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 = if result.singular {
89                format_singular_or_null(&result.rows)?
90            } else {
91                format_json_response(&result.rows)?
92            };
93            let mut response = Response::new(result.status, body);
94            response.set_content_type("application/json; charset=utf-8");
95            add_common_headers(&mut response, request, result);
96            Ok(response)
97        }
98        MediaType::TextCsv => {
99            // CSV formatting would go here
100            let body = format_csv_response(&result.rows)?;
101            let mut response = Response::new(result.status, body);
102            response.set_content_type("text/csv; charset=utf-8");
103            add_common_headers(&mut response, request, result);
104            Ok(response)
105        }
106        MediaType::SingularJson { nullable } => {
107            let body = format_singular_json(&result.rows, *nullable)?;
108            let mut response = Response::new(result.status, body);
109            response.set_content_type("application/vnd.pgrst.object+json; charset=utf-8");
110            add_common_headers(&mut response, request, result);
111            Ok(response)
112        }
113        _ => {
114            // Default to JSON (covers `*/*`, e.g. a default curl request).
115            let body = if result.singular {
116                format_singular_or_null(&result.rows)?
117            } else {
118                format_json_response(&result.rows)?
119            };
120            let mut response = Response::new(result.status, body);
121            response.set_content_type("application/json; charset=utf-8");
122            add_common_headers(&mut response, request, result);
123            Ok(response)
124        }
125    }
126}
127
128/// Add common response headers.
129fn add_common_headers(response: &mut Response, request: &ApiRequest, result: &QueryResult) {
130    // Content-Range
131    if let Some(range) = &result.content_range {
132        response.set_content_range(range);
133    }
134
135    // Location (for POST)
136    if let Some(location) = &result.location {
137        response.set_location(location);
138    }
139
140    // Preference-Applied
141    if let Some(applied) =
142        postrust_core::api_request::preferences::preference_applied(&request.preferences)
143    {
144        response.set_header("preference-applied", &applied);
145    }
146
147    // Content-Profile
148    if request.negotiated_by_profile {
149        response.set_header("content-profile", &request.schema);
150    }
151}
152
153/// Format a result as a single value (PostgREST-compatibility RPC responses).
154///
155/// A single row is emitted bare (not array-wrapped), no rows becomes `null`,
156/// and the (unexpected) multi-row case falls back to a JSON array so no data
157/// is silently dropped.
158fn format_singular_or_null(rows: &[serde_json::Value]) -> Result<bytes::Bytes, FormatError> {
159    match rows.len() {
160        0 => Ok(bytes::Bytes::from_static(b"null")),
161        1 => Ok(bytes::Bytes::from(serde_json::to_vec(&rows[0])?)),
162        _ => format_json_response(rows),
163    }
164}
165
166/// Format singular JSON (single object or null).
167fn format_singular_json(
168    rows: &[serde_json::Value],
169    nullable: bool,
170) -> Result<bytes::Bytes, FormatError> {
171    match rows.len() {
172        0 if nullable => Ok(bytes::Bytes::from_static(b"null")),
173        0 => Err(FormatError::NotFound),
174        1 => Ok(bytes::Bytes::from(serde_json::to_vec(&rows[0])?)),
175        _ => Err(FormatError::MultipleRows),
176    }
177}
178
179/// Format CSV response.
180fn format_csv_response(rows: &[serde_json::Value]) -> Result<bytes::Bytes, FormatError> {
181    if rows.is_empty() {
182        return Ok(bytes::Bytes::new());
183    }
184
185    let mut output = Vec::new();
186
187    // Get headers from first row
188    if let Some(serde_json::Value::Object(map)) = rows.first() {
189        let headers: Vec<&str> = map.keys().map(|s| s.as_str()).collect();
190        output.extend_from_slice(headers.join(",").as_bytes());
191        output.push(b'\n');
192
193        // Write rows
194        for row in rows {
195            if let serde_json::Value::Object(row_map) = row {
196                let values: Vec<String> = headers
197                    .iter()
198                    .map(|h| row_map.get(*h).map(csv_escape).unwrap_or_default())
199                    .collect();
200                output.extend_from_slice(values.join(",").as_bytes());
201                output.push(b'\n');
202            }
203        }
204    }
205
206    Ok(bytes::Bytes::from(output))
207}
208
209/// Escape a value for CSV.
210fn csv_escape(value: &serde_json::Value) -> String {
211    match value {
212        serde_json::Value::String(s) => {
213            if s.contains(',') || s.contains('"') || s.contains('\n') {
214                format!("\"{}\"", s.replace('"', "\"\""))
215            } else {
216                s.clone()
217            }
218        }
219        serde_json::Value::Null => String::new(),
220        other => other.to_string(),
221    }
222}
223
224/// Query result for response formatting.
225#[derive(Clone, Debug, Default)]
226pub struct QueryResult {
227    /// HTTP status code
228    pub status: StatusCode,
229    /// Result rows
230    pub rows: Vec<serde_json::Value>,
231    /// Total row count (for pagination)
232    pub total_count: Option<i64>,
233    /// Content range
234    pub content_range: Option<ContentRange>,
235    /// Location header (for POST)
236    pub location: Option<String>,
237    /// Custom headers from GUC
238    pub guc_headers: Option<String>,
239    /// Custom status from GUC
240    pub guc_status: Option<String>,
241    /// Whether the result should be rendered as a single (un-arrayed) value.
242    ///
243    /// Set for PostgREST-compatibility RPC responses where the underlying
244    /// function is not set-returning: the bare object/scalar is returned
245    /// instead of a one-element array.
246    pub singular: bool,
247}
248
249/// Response formatting error.
250#[derive(Debug, thiserror::Error)]
251pub enum FormatError {
252    #[error("JSON serialization error: {0}")]
253    Json(#[from] serde_json::Error),
254
255    #[error("Resource not found")]
256    NotFound,
257
258    #[error("Multiple rows returned for singular response")]
259    MultipleRows,
260}
261
262impl FormatError {
263    pub fn status_code(&self) -> StatusCode {
264        match self {
265            Self::Json(_) => StatusCode::INTERNAL_SERVER_ERROR,
266            Self::NotFound => StatusCode::NOT_FOUND,
267            Self::MultipleRows => StatusCode::NOT_ACCEPTABLE,
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use serde_json::json;
276
277    fn result(rows: Vec<serde_json::Value>, singular: bool) -> QueryResult {
278        QueryResult {
279            status: StatusCode::OK,
280            rows,
281            singular,
282            ..Default::default()
283        }
284    }
285
286    #[test]
287    fn singular_result_renders_bare_object() {
288        let req = ApiRequest::default();
289        let resp = format_response(&req, &result(vec![json!({"ok": true})], true)).unwrap();
290        assert_eq!(&resp.body[..], br#"{"ok":true}"#);
291    }
292
293    #[test]
294    fn singular_empty_result_renders_null() {
295        let req = ApiRequest::default();
296        let resp = format_response(&req, &result(vec![], true)).unwrap();
297        assert_eq!(&resp.body[..], b"null");
298    }
299
300    #[test]
301    fn non_singular_result_renders_array() {
302        let req = ApiRequest::default();
303        let resp = format_response(&req, &result(vec![json!(1), json!(2)], false)).unwrap();
304        assert_eq!(&resp.body[..], b"[1,2]");
305    }
306}