Skip to main content

postrust_core/api_request/
payload.rs

1//! Request body payload parsing.
2//!
3//! Handles JSON and URL-encoded request bodies.
4
5use super::types::*;
6use crate::error::{Error, Result};
7use bytes::Bytes;
8use std::collections::HashSet;
9
10/// Parse request body based on content type.
11pub fn parse_payload(body: Bytes, content_type: &MediaType) -> Result<Option<Payload>> {
12    if body.is_empty() {
13        return Ok(None);
14    }
15
16    match content_type {
17        MediaType::ApplicationJson => parse_json_payload(body),
18        MediaType::UrlEncoded => parse_urlencoded_payload(body),
19        MediaType::TextCsv => parse_csv_payload(body),
20        MediaType::OctetStream | MediaType::TextPlain | MediaType::TextXml => {
21            Ok(Some(Payload::RawPayload(body)))
22        }
23        _ => parse_json_payload(body),
24    }
25}
26
27/// Parse JSON body and extract keys.
28fn parse_json_payload(body: Bytes) -> Result<Option<Payload>> {
29    // Parse to extract keys
30    // What went wrong with the JSON is not the client's to act on -- the
31    // parser's offset says nothing it can use -- so this is the one message,
32    // and it is the one PostgREST gives.
33    let value: serde_json::Value = serde_json::from_slice(&body)
34        .map_err(|_| Error::InvalidBody("Empty or invalid json".into()))?;
35
36    let keys = extract_json_keys(&value);
37
38    Ok(Some(Payload::ProcessedJson { raw: body, keys }))
39}
40
41/// Parse a CSV body into the same shape a JSON array body would have taken.
42///
43/// The header row names the columns. Everything after it is a row of text,
44/// except an unquoted empty field, which is null -- that is the only way CSV
45/// can express one, and it is what PostgREST reads it as.
46fn parse_csv_payload(body: Bytes) -> Result<Option<Payload>> {
47    let text = std::str::from_utf8(&body)
48        .map_err(|_| Error::InvalidBody("Empty or invalid json".into()))?;
49
50    let mut records = read_csv(text);
51    if records.is_empty() {
52        return Ok(None);
53    }
54
55    let headers: Vec<String> = records
56        .remove(0)
57        .into_iter()
58        .map(|(value, _)| value)
59        .collect();
60
61    let mut rows = Vec::with_capacity(records.len());
62    for record in records {
63        if record.len() != headers.len() {
64            return Err(Error::InvalidBody(
65                "All lines must have same number of fields".into(),
66            ));
67        }
68        let mut row = serde_json::Map::with_capacity(headers.len());
69        for (name, (value, quoted)) in headers.iter().zip(record) {
70            // CSV has no null of its own, so the two spellings it is given are
71            // an unquoted empty field and the unquoted word NULL. Quoting
72            // either makes it the text it looks like.
73            let value = match !quoted && (value.is_empty() || value == "NULL") {
74                true => serde_json::Value::Null,
75                false => serde_json::Value::String(value),
76            };
77            row.insert(name.clone(), value);
78        }
79        rows.push(serde_json::Value::Object(row));
80    }
81
82    let raw = serde_json::to_vec(&serde_json::Value::Array(rows))
83        .map_err(|e| Error::Internal(e.to_string()))?;
84
85    Ok(Some(Payload::ProcessedJson {
86        raw: Bytes::from(raw),
87        keys: headers.into_iter().collect(),
88    }))
89}
90
91/// Split CSV text into records of `(field, was quoted)`.
92///
93/// A quoted field may contain commas and newlines, and `""` inside one is a
94/// literal quote. Whether a field was quoted is carried out because it is the
95/// only thing separating an empty string from a null.
96fn read_csv(text: &str) -> Vec<Vec<(String, bool)>> {
97    let mut records = Vec::new();
98    let mut record = Vec::new();
99    let mut field = String::new();
100    let mut quoted = false;
101    let mut in_quotes = false;
102    let mut chars = text.chars().peekable();
103
104    while let Some(ch) = chars.next() {
105        if in_quotes {
106            match ch {
107                '"' if chars.peek() == Some(&'"') => {
108                    chars.next();
109                    field.push('"');
110                }
111                '"' => in_quotes = false,
112                other => field.push(other),
113            }
114            continue;
115        }
116        match ch {
117            '"' => {
118                in_quotes = true;
119                quoted = true;
120            }
121            ',' => record.push((std::mem::take(&mut field), std::mem::take(&mut quoted))),
122            '\r' => {
123                if chars.peek() == Some(&'\n') {
124                    chars.next();
125                }
126                record.push((std::mem::take(&mut field), std::mem::take(&mut quoted)));
127                records.push(std::mem::take(&mut record));
128            }
129            '\n' => {
130                record.push((std::mem::take(&mut field), std::mem::take(&mut quoted)));
131                records.push(std::mem::take(&mut record));
132            }
133            other => field.push(other),
134        }
135    }
136
137    if !field.is_empty() || quoted || !record.is_empty() {
138        record.push((field, quoted));
139        records.push(record);
140    }
141
142    records
143}
144
145/// Check that every row of a bulk insert names the same columns.
146///
147/// The insert is one statement with one column list, so a row naming fewer
148/// columns would silently take defaults for the rest. `?columns=` is how a
149/// client says which columns to write when the rows genuinely differ, and this
150/// check is skipped when it does.
151pub fn validate_uniform_keys(payload: &Payload) -> Result<()> {
152    let Payload::ProcessedJson { raw, .. } = payload else {
153        return Ok(());
154    };
155    let Ok(serde_json::Value::Array(rows)) = serde_json::from_slice::<serde_json::Value>(raw)
156    else {
157        return Ok(());
158    };
159
160    let mut expected: Option<HashSet<&String>> = None;
161    for row in &rows {
162        let Some(object) = row.as_object() else {
163            return Err(Error::InvalidBody("All object keys must match".into()));
164        };
165        let keys: HashSet<&String> = object.keys().collect();
166        match &expected {
167            None => expected = Some(keys),
168            Some(first) if *first != keys => {
169                return Err(Error::InvalidBody("All object keys must match".into()))
170            }
171            Some(_) => {}
172        }
173    }
174
175    Ok(())
176}
177
178/// Extract top-level keys from JSON value.
179fn extract_json_keys(value: &serde_json::Value) -> HashSet<String> {
180    match value {
181        serde_json::Value::Object(map) => map.keys().cloned().collect(),
182        serde_json::Value::Array(arr) => {
183            // For arrays, collect keys from all objects
184            arr.iter()
185                .filter_map(|v| v.as_object())
186                .flat_map(|map| map.keys().cloned())
187                .collect()
188        }
189        _ => HashSet::new(),
190    }
191}
192
193/// Parse URL-encoded body.
194fn parse_urlencoded_payload(body: Bytes) -> Result<Option<Payload>> {
195    let body_str =
196        std::str::from_utf8(&body).map_err(|_| Error::InvalidBody("Invalid UTF-8".into()))?;
197
198    let data: Vec<(String, String)> = url::form_urlencoded::parse(body_str.as_bytes())
199        .map(|(k, v)| (k.to_string(), v.to_string()))
200        .collect();
201
202    let keys: HashSet<String> = data.iter().map(|(k, _)| k.clone()).collect();
203
204    Ok(Some(Payload::ProcessedUrlEncoded { data, keys }))
205}
206
207/// Check if payload keys match the expected columns.
208pub fn validate_payload_columns(payload: &Payload, expected: &HashSet<String>) -> Result<()> {
209    let keys = match payload {
210        Payload::ProcessedJson { keys, .. } => keys,
211        Payload::ProcessedUrlEncoded { keys, .. } => keys,
212        _ => return Ok(()),
213    };
214
215    for key in keys {
216        if !expected.contains(key) {
217            return Err(Error::UnknownColumn {
218                column: key.clone(),
219                relation: String::new(),
220            });
221        }
222    }
223
224    Ok(())
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_parse_json_object() {
233        let body = Bytes::from(r#"{"name": "John", "age": 30}"#);
234        let payload = parse_payload(body, &MediaType::ApplicationJson)
235            .unwrap()
236            .unwrap();
237
238        match payload {
239            Payload::ProcessedJson { keys, .. } => {
240                assert!(keys.contains("name"));
241                assert!(keys.contains("age"));
242            }
243            _ => panic!("Expected ProcessedJson"),
244        }
245    }
246
247    #[test]
248    fn test_parse_json_array() {
249        let body = Bytes::from(r#"[{"id": 1}, {"id": 2, "name": "test"}]"#);
250        let payload = parse_payload(body, &MediaType::ApplicationJson)
251            .unwrap()
252            .unwrap();
253
254        match payload {
255            Payload::ProcessedJson { keys, .. } => {
256                assert!(keys.contains("id"));
257                assert!(keys.contains("name"));
258            }
259            _ => panic!("Expected ProcessedJson"),
260        }
261    }
262
263    #[test]
264    fn test_parse_urlencoded() {
265        let body = Bytes::from("name=John&age=30");
266        let payload = parse_payload(body, &MediaType::UrlEncoded)
267            .unwrap()
268            .unwrap();
269
270        match payload {
271            Payload::ProcessedUrlEncoded { data, keys } => {
272                assert_eq!(data.len(), 2);
273                assert!(keys.contains("name"));
274                assert!(keys.contains("age"));
275            }
276            _ => panic!("Expected ProcessedUrlEncoded"),
277        }
278    }
279
280    #[test]
281    fn test_parse_empty_body() {
282        let body = Bytes::new();
283        let payload = parse_payload(body, &MediaType::ApplicationJson).unwrap();
284        assert!(payload.is_none());
285    }
286
287    #[test]
288    fn test_parse_octet_stream() {
289        let body = Bytes::from(vec![0u8, 1, 2, 3]);
290        let payload = parse_payload(body.clone(), &MediaType::OctetStream)
291            .unwrap()
292            .unwrap();
293
294        match payload {
295            Payload::RawPayload(data) => {
296                assert_eq!(data, body);
297            }
298            _ => panic!("Expected RawPayload"),
299        }
300    }
301}