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 => {
20            // CSV is handled as raw JSON for processing
21            Ok(Some(Payload::RawJson(body)))
22        }
23        MediaType::OctetStream | MediaType::TextPlain | MediaType::TextXml => {
24            Ok(Some(Payload::RawPayload(body)))
25        }
26        _ => parse_json_payload(body),
27    }
28}
29
30/// Parse JSON body and extract keys.
31fn parse_json_payload(body: Bytes) -> Result<Option<Payload>> {
32    // Parse to extract keys
33    let value: serde_json::Value =
34        serde_json::from_slice(&body).map_err(|e| Error::InvalidBody(e.to_string()))?;
35
36    let keys = extract_json_keys(&value);
37
38    Ok(Some(Payload::ProcessedJson { raw: body, keys }))
39}
40
41/// Extract top-level keys from JSON value.
42fn extract_json_keys(value: &serde_json::Value) -> HashSet<String> {
43    match value {
44        serde_json::Value::Object(map) => map.keys().cloned().collect(),
45        serde_json::Value::Array(arr) => {
46            // For arrays, collect keys from all objects
47            arr.iter()
48                .filter_map(|v| v.as_object())
49                .flat_map(|map| map.keys().cloned())
50                .collect()
51        }
52        _ => HashSet::new(),
53    }
54}
55
56/// Parse URL-encoded body.
57fn parse_urlencoded_payload(body: Bytes) -> Result<Option<Payload>> {
58    let body_str =
59        std::str::from_utf8(&body).map_err(|_| Error::InvalidBody("Invalid UTF-8".into()))?;
60
61    let data: Vec<(String, String)> = url::form_urlencoded::parse(body_str.as_bytes())
62        .map(|(k, v)| (k.to_string(), v.to_string()))
63        .collect();
64
65    let keys: HashSet<String> = data.iter().map(|(k, _)| k.clone()).collect();
66
67    Ok(Some(Payload::ProcessedUrlEncoded { data, keys }))
68}
69
70/// Check if payload keys match the expected columns.
71pub fn validate_payload_columns(
72    payload: &Payload,
73    expected: &HashSet<String>,
74) -> Result<()> {
75    let keys = match payload {
76        Payload::ProcessedJson { keys, .. } => keys,
77        Payload::ProcessedUrlEncoded { keys, .. } => keys,
78        _ => return Ok(()),
79    };
80
81    for key in keys {
82        if !expected.contains(key) {
83            return Err(Error::UnknownColumn(key.clone()));
84        }
85    }
86
87    Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_parse_json_object() {
96        let body = Bytes::from(r#"{"name": "John", "age": 30}"#);
97        let payload = parse_payload(body, &MediaType::ApplicationJson)
98            .unwrap()
99            .unwrap();
100
101        match payload {
102            Payload::ProcessedJson { keys, .. } => {
103                assert!(keys.contains("name"));
104                assert!(keys.contains("age"));
105            }
106            _ => panic!("Expected ProcessedJson"),
107        }
108    }
109
110    #[test]
111    fn test_parse_json_array() {
112        let body = Bytes::from(r#"[{"id": 1}, {"id": 2, "name": "test"}]"#);
113        let payload = parse_payload(body, &MediaType::ApplicationJson)
114            .unwrap()
115            .unwrap();
116
117        match payload {
118            Payload::ProcessedJson { keys, .. } => {
119                assert!(keys.contains("id"));
120                assert!(keys.contains("name"));
121            }
122            _ => panic!("Expected ProcessedJson"),
123        }
124    }
125
126    #[test]
127    fn test_parse_urlencoded() {
128        let body = Bytes::from("name=John&age=30");
129        let payload = parse_payload(body, &MediaType::UrlEncoded)
130            .unwrap()
131            .unwrap();
132
133        match payload {
134            Payload::ProcessedUrlEncoded { data, keys } => {
135                assert_eq!(data.len(), 2);
136                assert!(keys.contains("name"));
137                assert!(keys.contains("age"));
138            }
139            _ => panic!("Expected ProcessedUrlEncoded"),
140        }
141    }
142
143    #[test]
144    fn test_parse_empty_body() {
145        let body = Bytes::new();
146        let payload = parse_payload(body, &MediaType::ApplicationJson).unwrap();
147        assert!(payload.is_none());
148    }
149
150    #[test]
151    fn test_parse_octet_stream() {
152        let body = Bytes::from(vec![0u8, 1, 2, 3]);
153        let payload = parse_payload(body.clone(), &MediaType::OctetStream)
154            .unwrap()
155            .unwrap();
156
157        match payload {
158            Payload::RawPayload(data) => {
159                assert_eq!(data, body);
160            }
161            _ => panic!("Expected RawPayload"),
162        }
163    }
164}