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 => {
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(payload: &Payload, expected: &HashSet<String>) -> Result<()> {
72    let keys = match payload {
73        Payload::ProcessedJson { keys, .. } => keys,
74        Payload::ProcessedUrlEncoded { keys, .. } => keys,
75        _ => return Ok(()),
76    };
77
78    for key in keys {
79        if !expected.contains(key) {
80            return Err(Error::UnknownColumn(key.clone()));
81        }
82    }
83
84    Ok(())
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_parse_json_object() {
93        let body = Bytes::from(r#"{"name": "John", "age": 30}"#);
94        let payload = parse_payload(body, &MediaType::ApplicationJson)
95            .unwrap()
96            .unwrap();
97
98        match payload {
99            Payload::ProcessedJson { keys, .. } => {
100                assert!(keys.contains("name"));
101                assert!(keys.contains("age"));
102            }
103            _ => panic!("Expected ProcessedJson"),
104        }
105    }
106
107    #[test]
108    fn test_parse_json_array() {
109        let body = Bytes::from(r#"[{"id": 1}, {"id": 2, "name": "test"}]"#);
110        let payload = parse_payload(body, &MediaType::ApplicationJson)
111            .unwrap()
112            .unwrap();
113
114        match payload {
115            Payload::ProcessedJson { keys, .. } => {
116                assert!(keys.contains("id"));
117                assert!(keys.contains("name"));
118            }
119            _ => panic!("Expected ProcessedJson"),
120        }
121    }
122
123    #[test]
124    fn test_parse_urlencoded() {
125        let body = Bytes::from("name=John&age=30");
126        let payload = parse_payload(body, &MediaType::UrlEncoded)
127            .unwrap()
128            .unwrap();
129
130        match payload {
131            Payload::ProcessedUrlEncoded { data, keys } => {
132                assert_eq!(data.len(), 2);
133                assert!(keys.contains("name"));
134                assert!(keys.contains("age"));
135            }
136            _ => panic!("Expected ProcessedUrlEncoded"),
137        }
138    }
139
140    #[test]
141    fn test_parse_empty_body() {
142        let body = Bytes::new();
143        let payload = parse_payload(body, &MediaType::ApplicationJson).unwrap();
144        assert!(payload.is_none());
145    }
146
147    #[test]
148    fn test_parse_octet_stream() {
149        let body = Bytes::from(vec![0u8, 1, 2, 3]);
150        let payload = parse_payload(body.clone(), &MediaType::OctetStream)
151            .unwrap()
152            .unwrap();
153
154        match payload {
155            Payload::RawPayload(data) => {
156                assert_eq!(data, body);
157            }
158            _ => panic!("Expected RawPayload"),
159        }
160    }
161}