postrust_core/api_request/
payload.rs1use super::types::*;
6use crate::error::{Error, Result};
7use bytes::Bytes;
8use std::collections::HashSet;
9
10pub 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
27fn parse_json_payload(body: Bytes) -> Result<Option<Payload>> {
29 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
41fn 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 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
91fn 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
145pub 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
178fn 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 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
193fn 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
207pub 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}