use indexmap::IndexMap;
use parse_rust_core::op::OpPath;
use parse_rust_core::{
classify_field, ErrorCode, FieldWrite, Op, ParseError, ParseMap, ParseValue,
};
use parse_rust_storage::{Update, UpdateValue};
use serde_json::Value as Json;
pub type WriteBody = IndexMap<String, FieldWrite>;
pub fn decode_write_body(value: &Json, path: OpPath) -> Result<WriteBody, ParseError> {
let Json::Object(map) = value else {
return Err(ParseError::invalid_json("body must be an object"));
};
let mut out = WriteBody::new();
for (key, value) in map {
out.insert(key.clone(), classify_field(value.clone(), path)?);
}
Ok(out)
}
pub fn as_plain_body(body: &WriteBody) -> ParseMap {
let mut out = ParseMap::new();
for (key, write) in body {
let value = match write {
FieldWrite::Value(v) => v.clone(),
FieldWrite::Op(op) => {
let mut envelope = ParseMap::new();
envelope.insert(
"__op".to_string(),
ParseValue::String(op.name().to_string()),
);
ParseValue::Object(envelope)
}
};
out.insert(key.clone(), value);
}
out
}
pub fn flatten_for_create(body: &WriteBody) -> Result<ParseMap, ParseError> {
let mut out = ParseMap::new();
for (key, write) in body {
match write {
FieldWrite::Value(value) => {
out.insert(key.clone(), value.clone());
}
FieldWrite::Op(op) => {
if let Some(value) = op.flatten_for_create()? {
out.insert(key.clone(), value);
}
}
}
}
Ok(out)
}
pub fn lower_update(body: &WriteBody) -> Result<Update, ParseError> {
let mut out = Update::new();
for (key, write) in body {
let value = match write {
FieldWrite::Value(value) => UpdateValue::Set(value.clone()),
FieldWrite::Op(op) => match op {
Op::Increment(amount) => UpdateValue::Increment(*amount),
Op::SetOnInsert(value) => UpdateValue::SetOnInsert(value.clone()),
Op::Add(objects) => UpdateValue::Add(objects.clone()),
Op::AddUnique(objects) => UpdateValue::AddUnique(objects.clone()),
Op::Remove(objects) => UpdateValue::Remove(objects.clone()),
Op::Delete => UpdateValue::Unset,
Op::AddRelation(_) | Op::RemoveRelation(_) | Op::Batch(_) => {
return Err(ParseError::new(
ErrorCode::CommandUnavailable,
format!("The {} operator is not supported yet.", op.name()),
))
}
},
};
out.insert(key.clone(), value);
}
Ok(out)
}
pub fn enforce_object_id_policy(
body: &WriteBody,
allow_custom_object_id: bool,
) -> Result<(), ParseError> {
let present = |key: &str| match body.get(key) {
Some(FieldWrite::Value(value)) => Some(value),
Some(FieldWrite::Op(_)) => Some(&ParseValue::Bool(true)),
None => None,
};
if allow_custom_object_id {
if let Some(value) = present("objectId") {
if !parse_rust_core::is_js_truthy(value) {
return Err(ParseError::new(
ErrorCode::MissingObjectId,
"objectId must not be empty, null or undefined",
));
}
}
return Ok(());
}
for key in ["objectId", "id"] {
if present(key).is_some_and(parse_rust_core::is_js_truthy) {
return Err(ParseError::new(
ErrorCode::InvalidKeyName,
format!("{key} is an invalid field name."),
));
}
}
Ok(())
}
pub fn echoed_keys(body: &WriteBody) -> Vec<String> {
body.iter()
.filter_map(|(key, write)| match write {
FieldWrite::Op(op) if op.echoes_result() => Some(key.clone()),
_ => None,
})
.collect()
}
pub fn echo_response(body: &WriteBody, row: Option<&ParseMap>) -> ParseMap {
let mut out = ParseMap::new();
let Some(row) = row else { return out };
for key in echoed_keys(body) {
if let Some(value) = row.get(&key) {
out.insert(key, value.clone());
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn body(json: &str, path: OpPath) -> WriteBody {
decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
}
#[test]
fn an_op_is_decoded_rather_than_stored_as_an_object() {
let b = body(
r#"{"views":{"__op":"Increment","amount":2}}"#,
OpPath::Update,
);
assert!(matches!(b.get("views"), Some(FieldWrite::Op(Op::Increment(a))) if *a == 2.0));
let update = lower_update(&b).expect("lower");
assert!(matches!(update.get("views"), Some(UpdateValue::Increment(a)) if *a == 2.0));
}
#[test]
fn create_flattens_every_op_the_way_upstream_does() {
let b = body(
r#"{
"views":{"__op":"Increment","amount":3},
"tags":{"__op":"Add","objects":["a"]},
"unique":{"__op":"AddUnique","objects":["b"]},
"gone":{"__op":"Remove","objects":["c"]},
"dropped":{"__op":"Delete"},
"plain":"x"
}"#,
OpPath::Create,
);
let row = flatten_for_create(&b).expect("flatten");
assert!(matches!(row.get("views"), Some(ParseValue::Number(n)) if *n == 3.0));
assert!(matches!(row.get("tags"), Some(ParseValue::Array(a)) if a.len() == 1));
assert!(matches!(row.get("unique"), Some(ParseValue::Array(a)) if a.len() == 1));
assert!(
matches!(row.get("gone"), Some(ParseValue::Array(a)) if a.is_empty()),
"Remove yields an empty array rather than removing anything"
);
assert!(row.get("dropped").is_none());
assert!(row.get("plain").is_some());
}
#[test]
fn only_the_five_result_bearing_ops_echo_back() {
let b = body(
r#"{
"views":{"__op":"Increment","amount":1},
"tags":{"__op":"Add","objects":["a"]},
"unique":{"__op":"AddUnique","objects":["b"]},
"gone":{"__op":"Remove","objects":["c"]},
"dropped":{"__op":"Delete"},
"plain":"x"
}"#,
OpPath::Update,
);
assert_eq!(echoed_keys(&b), vec!["views", "tags", "unique", "gone"]);
let mut row = ParseMap::new();
row.insert("views".into(), ParseValue::Number(4.0));
row.insert("plain".into(), ParseValue::String("x".into()));
let echoed = echo_response(&b, Some(&row));
assert!(matches!(echoed.get("views"), Some(ParseValue::Number(n)) if *n == 4.0));
assert!(
echoed.get("plain").is_none(),
"a plain set tells the client nothing it did not already know"
);
}
#[test]
fn the_increment_message_differs_by_path() {
let bad = serde_json::from_str(r#"{"n":{"__op":"Increment","amount":"x"}}"#)
.expect("test literal");
assert_eq!(
decode_write_body(&bad, OpPath::Create).unwrap_err().message,
"objects to add must be an array"
);
assert_eq!(
decode_write_body(&bad, OpPath::Update).unwrap_err().message,
"incrementing must provide a number"
);
}
#[test]
fn a_relation_op_reaching_the_update_lowering_is_refused() {
let b = body(
r#"{"users":{"__op":"AddRelation","objects":[]}}"#,
OpPath::Update,
);
let e = lower_update(&b).unwrap_err();
assert_eq!(e.code, ErrorCode::CommandUnavailable);
}
#[test]
fn a_delete_survives_as_its_envelope_in_the_plain_view() {
let b = body(r#"{"ACL":{"__op":"Delete"},"n":1}"#, OpPath::Update);
let plain = as_plain_body(&b);
match plain.get("ACL") {
Some(ParseValue::Object(map)) => {
assert!(matches!(map.get("__op"), Some(ParseValue::String(s)) if s == "Delete"))
}
other => panic!("expected an op envelope, got {other:?}"),
}
assert!(matches!(plain.get("n"), Some(ParseValue::Number(_))));
}
#[test]
fn the_default_refuses_a_client_supplied_object_id_and_id() {
for key in ["objectId", "id"] {
let b = body(
&format!(r#"{{"{key}":"chosen","title":"a"}}"#),
OpPath::Create,
);
let e = enforce_object_id_policy(&b, false).unwrap_err();
assert_eq!(e.code, ErrorCode::InvalidKeyName);
assert_eq!(e.message, format!("{key} is an invalid field name."));
}
let plain = body(r#"{"title":"a"}"#, OpPath::Create);
assert!(enforce_object_id_policy(&plain, false).is_ok());
}
#[test]
fn a_falsy_object_id_is_not_a_custom_one() {
let b = body(r#"{"objectId":""}"#, OpPath::Create);
assert!(enforce_object_id_policy(&b, false).is_ok());
}
#[test]
fn allowing_custom_ids_refuses_only_an_empty_one() {
let chosen = body(r#"{"objectId":"chosen"}"#, OpPath::Create);
assert!(enforce_object_id_policy(&chosen, true).is_ok());
for literal in [r#"{"objectId":""}"#, r#"{"objectId":null}"#] {
let b = body(literal, OpPath::Create);
let e = enforce_object_id_policy(&b, true).unwrap_err();
assert_eq!(e.code, ErrorCode::MissingObjectId);
assert_eq!(e.message, "objectId must not be empty, null or undefined");
}
let absent = body(r#"{"title":"a"}"#, OpPath::Create);
assert!(enforce_object_id_policy(&absent, true).is_ok());
}
}