use parse_rust_core::{ParseError, ParseMap, ParseValue};
pub fn reject_reserved_keys(body: &ParseMap) -> Result<(), ParseError> {
reject_reserved_keys_in(body.keys().map(String::as_str))
}
pub fn reject_reserved_keys_in<'a>(
keys: impl IntoIterator<Item = &'a str>,
) -> Result<(), ParseError> {
for key in keys {
if key.starts_with('_') {
return Err(ParseError::invalid_key_name(format!(
"Invalid field name: {key}."
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use parse_rust_core::ParseValue;
fn m(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
let mut map = ParseMap::new();
for (k, v) in pairs {
map.insert(k.to_string(), v);
}
map
}
#[test]
fn ordinary_fields_pass() {
assert!(reject_reserved_keys(&m(vec![
("title", ParseValue::String("x".into())),
("ACL", ParseValue::Null),
]))
.is_ok());
}
#[test]
fn a_client_cannot_supply_a_server_internal_column() {
for key in [
"_hashed_password",
"_rperm",
"_wperm",
"_session_token",
"_perishable_token",
"_anything",
] {
let e =
reject_reserved_keys(&m(vec![(key, ParseValue::String("x".into()))])).unwrap_err();
assert_eq!(e.code, parse_rust_core::ErrorCode::InvalidKeyName, "{key}");
assert!(e.message.contains(key));
}
}
}
pub fn strip_internal_keys(row: &mut ParseMap) {
row.retain(|k, _| !k.starts_with('_'));
}
const BARE_ISO_FIELDS: [&str; 3] = ["createdAt", "updatedAt", "lastUsed"];
pub fn flatten_top_level_dates(row: &mut ParseMap) {
for key in BARE_ISO_FIELDS {
if let Some(ParseValue::Date(d)) = row.get(key) {
let iso = d.to_iso();
row.insert(key.to_string(), ParseValue::String(iso));
}
}
}
pub fn to_response_body(row: &ParseMap) -> ParseMap {
let mut out = row.clone();
strip_internal_keys(&mut out);
flatten_top_level_dates(&mut out);
out
}
#[cfg(test)]
mod strip_tests {
use super::*;
use parse_rust_core::ParseValue;
#[test]
fn every_underscore_key_is_removed() {
let mut row = ParseMap::new();
row.insert("title".into(), ParseValue::String("x".into()));
row.insert("_hashed_password".into(), ParseValue::String("h".into()));
row.insert("_rperm".into(), ParseValue::Array(vec![]));
row.insert("_anything".into(), ParseValue::Null);
strip_internal_keys(&mut row);
assert_eq!(row.len(), 1);
assert!(row.contains_key("title"));
}
#[test]
fn the_acl_field_survives_because_it_is_not_underscore_prefixed() {
let mut row = ParseMap::new();
row.insert("ACL".into(), ParseValue::Object(ParseMap::new()));
strip_internal_keys(&mut row);
assert!(row.contains_key("ACL"), "ACL is a client-visible field");
}
}
#[cfg(test)]
mod response_tests {
use super::*;
use parse_rust_core::ParseDate;
#[test]
fn top_level_timestamps_become_bare_strings() {
let mut row = ParseMap::new();
let d = ParseDate::parse_iso("2026-08-14T13:34:33.581Z").expect("date");
row.insert("createdAt".into(), ParseValue::Date(d));
row.insert("updatedAt".into(), ParseValue::Date(d));
row.insert("dueDate".into(), ParseValue::Date(d));
let out = to_response_body(&row);
assert!(
matches!(out.get("createdAt"), Some(ParseValue::String(s)) if s == "2026-08-14T13:34:33.581Z"),
"createdAt must be a bare ISO string at the top level"
);
assert!(matches!(out.get("updatedAt"), Some(ParseValue::String(_))));
assert!(
matches!(out.get("dueDate"), Some(ParseValue::Date(_))),
"a user-defined Date keeps its __type envelope"
);
}
#[test]
fn the_response_body_is_stripped_and_flattened_together() {
let mut row = ParseMap::new();
row.insert("_hashed_password".into(), ParseValue::String("h".into()));
row.insert(
"createdAt".into(),
ParseValue::Date(ParseDate::parse_iso("2026-01-01T00:00:00.000Z").expect("d")),
);
let out = to_response_body(&row);
assert!(out.get("_hashed_password").is_none());
assert!(matches!(out.get("createdAt"), Some(ParseValue::String(_))));
}
}