use super::types::*;
use crate::error::{Error, Result};
use bytes::Bytes;
use std::collections::HashSet;
pub fn parse_payload(body: Bytes, content_type: &MediaType) -> Result<Option<Payload>> {
if body.is_empty() {
return Ok(None);
}
match content_type {
MediaType::ApplicationJson => parse_json_payload(body),
MediaType::UrlEncoded => parse_urlencoded_payload(body),
MediaType::TextCsv => parse_csv_payload(body),
MediaType::OctetStream | MediaType::TextPlain | MediaType::TextXml => {
Ok(Some(Payload::RawPayload(body)))
}
_ => parse_json_payload(body),
}
}
fn parse_json_payload(body: Bytes) -> Result<Option<Payload>> {
let value: serde_json::Value = serde_json::from_slice(&body)
.map_err(|_| Error::InvalidBody("Empty or invalid json".into()))?;
let keys = extract_json_keys(&value);
Ok(Some(Payload::ProcessedJson { raw: body, keys }))
}
fn parse_csv_payload(body: Bytes) -> Result<Option<Payload>> {
let text = std::str::from_utf8(&body)
.map_err(|_| Error::InvalidBody("Empty or invalid json".into()))?;
let mut records = read_csv(text);
if records.is_empty() {
return Ok(None);
}
let headers: Vec<String> = records
.remove(0)
.into_iter()
.map(|(value, _)| value)
.collect();
let mut rows = Vec::with_capacity(records.len());
for record in records {
if record.len() != headers.len() {
return Err(Error::InvalidBody(
"All lines must have same number of fields".into(),
));
}
let mut row = serde_json::Map::with_capacity(headers.len());
for (name, (value, quoted)) in headers.iter().zip(record) {
let value = match !quoted && (value.is_empty() || value == "NULL") {
true => serde_json::Value::Null,
false => serde_json::Value::String(value),
};
row.insert(name.clone(), value);
}
rows.push(serde_json::Value::Object(row));
}
let raw = serde_json::to_vec(&serde_json::Value::Array(rows))
.map_err(|e| Error::Internal(e.to_string()))?;
Ok(Some(Payload::ProcessedJson {
raw: Bytes::from(raw),
keys: headers.into_iter().collect(),
}))
}
fn read_csv(text: &str) -> Vec<Vec<(String, bool)>> {
let mut records = Vec::new();
let mut record = Vec::new();
let mut field = String::new();
let mut quoted = false;
let mut in_quotes = false;
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if in_quotes {
match ch {
'"' if chars.peek() == Some(&'"') => {
chars.next();
field.push('"');
}
'"' => in_quotes = false,
other => field.push(other),
}
continue;
}
match ch {
'"' => {
in_quotes = true;
quoted = true;
}
',' => record.push((std::mem::take(&mut field), std::mem::take(&mut quoted))),
'\r' => {
if chars.peek() == Some(&'\n') {
chars.next();
}
record.push((std::mem::take(&mut field), std::mem::take(&mut quoted)));
records.push(std::mem::take(&mut record));
}
'\n' => {
record.push((std::mem::take(&mut field), std::mem::take(&mut quoted)));
records.push(std::mem::take(&mut record));
}
other => field.push(other),
}
}
if !field.is_empty() || quoted || !record.is_empty() {
record.push((field, quoted));
records.push(record);
}
records
}
pub fn validate_uniform_keys(payload: &Payload) -> Result<()> {
let Payload::ProcessedJson { raw, .. } = payload else {
return Ok(());
};
let Ok(serde_json::Value::Array(rows)) = serde_json::from_slice::<serde_json::Value>(raw)
else {
return Ok(());
};
let mut expected: Option<HashSet<&String>> = None;
for row in &rows {
let Some(object) = row.as_object() else {
return Err(Error::InvalidBody("All object keys must match".into()));
};
let keys: HashSet<&String> = object.keys().collect();
match &expected {
None => expected = Some(keys),
Some(first) if *first != keys => {
return Err(Error::InvalidBody("All object keys must match".into()))
}
Some(_) => {}
}
}
Ok(())
}
fn extract_json_keys(value: &serde_json::Value) -> HashSet<String> {
match value {
serde_json::Value::Object(map) => map.keys().cloned().collect(),
serde_json::Value::Array(arr) => {
arr.iter()
.filter_map(|v| v.as_object())
.flat_map(|map| map.keys().cloned())
.collect()
}
_ => HashSet::new(),
}
}
fn parse_urlencoded_payload(body: Bytes) -> Result<Option<Payload>> {
let body_str =
std::str::from_utf8(&body).map_err(|_| Error::InvalidBody("Invalid UTF-8".into()))?;
let data: Vec<(String, String)> = url::form_urlencoded::parse(body_str.as_bytes())
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let keys: HashSet<String> = data.iter().map(|(k, _)| k.clone()).collect();
Ok(Some(Payload::ProcessedUrlEncoded { data, keys }))
}
pub fn validate_payload_columns(payload: &Payload, expected: &HashSet<String>) -> Result<()> {
let keys = match payload {
Payload::ProcessedJson { keys, .. } => keys,
Payload::ProcessedUrlEncoded { keys, .. } => keys,
_ => return Ok(()),
};
for key in keys {
if !expected.contains(key) {
return Err(Error::UnknownColumn {
column: key.clone(),
relation: String::new(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_json_object() {
let body = Bytes::from(r#"{"name": "John", "age": 30}"#);
let payload = parse_payload(body, &MediaType::ApplicationJson)
.unwrap()
.unwrap();
match payload {
Payload::ProcessedJson { keys, .. } => {
assert!(keys.contains("name"));
assert!(keys.contains("age"));
}
_ => panic!("Expected ProcessedJson"),
}
}
#[test]
fn test_parse_json_array() {
let body = Bytes::from(r#"[{"id": 1}, {"id": 2, "name": "test"}]"#);
let payload = parse_payload(body, &MediaType::ApplicationJson)
.unwrap()
.unwrap();
match payload {
Payload::ProcessedJson { keys, .. } => {
assert!(keys.contains("id"));
assert!(keys.contains("name"));
}
_ => panic!("Expected ProcessedJson"),
}
}
#[test]
fn test_parse_urlencoded() {
let body = Bytes::from("name=John&age=30");
let payload = parse_payload(body, &MediaType::UrlEncoded)
.unwrap()
.unwrap();
match payload {
Payload::ProcessedUrlEncoded { data, keys } => {
assert_eq!(data.len(), 2);
assert!(keys.contains("name"));
assert!(keys.contains("age"));
}
_ => panic!("Expected ProcessedUrlEncoded"),
}
}
#[test]
fn test_parse_empty_body() {
let body = Bytes::new();
let payload = parse_payload(body, &MediaType::ApplicationJson).unwrap();
assert!(payload.is_none());
}
#[test]
fn test_parse_octet_stream() {
let body = Bytes::from(vec![0u8, 1, 2, 3]);
let payload = parse_payload(body.clone(), &MediaType::OctetStream)
.unwrap()
.unwrap();
match payload {
Payload::RawPayload(data) => {
assert_eq!(data, body);
}
_ => panic!("Expected RawPayload"),
}
}
}