use crate::error::ParseError;
use crate::value::ParseValue;
use serde_json::Value as Json;
#[derive(Debug, Clone)]
pub enum Op {
Increment(f64),
Add(Vec<ParseValue>),
AddUnique(Vec<ParseValue>),
Remove(Vec<ParseValue>),
Delete,
AddRelation(Vec<ParseValue>),
RemoveRelation(Vec<ParseValue>),
Batch(Vec<Op>),
}
impl Op {
pub fn classify(value: &Json) -> Result<Option<Op>, ParseError> {
let map = match value {
Json::Object(m) => m,
_ => return Ok(None),
};
let name = match map.get("__op") {
Some(Json::String(s)) => s.as_str(),
_ => return Ok(None),
};
let objects = |key: &str| -> Result<Vec<ParseValue>, ParseError> {
match map.get(key) {
Some(Json::Array(a)) => a
.iter()
.cloned()
.map(crate::decode::classify_nested)
.collect::<Result<Vec<_>, _>>(),
_ => Err(ParseError::invalid_json(
"objects to add must be an array".to_string(),
)),
}
};
let op = match name {
"Increment" => {
let amount = map.get("amount").and_then(|v| v.as_f64()).ok_or_else(|| {
ParseError::invalid_json("incrementing must provide a number".to_string())
})?;
Op::Increment(amount)
}
"Add" => Op::Add(objects("objects")?),
"AddUnique" => Op::AddUnique(objects("objects")?),
"Remove" => Op::Remove(objects("objects")?),
"AddRelation" => Op::AddRelation(objects("objects")?),
"RemoveRelation" => Op::RemoveRelation(objects("objects")?),
"Delete" => Op::Delete,
"Batch" => {
let ops = match map.get("ops") {
Some(Json::Array(a)) => a,
_ => {
return Err(ParseError::invalid_json(
"Batch requires an ops array".to_string(),
))
}
};
let mut out = Vec::with_capacity(ops.len());
for o in ops {
match Op::classify(o)? {
Some(inner) => out.push(inner),
None => {
return Err(ParseError::invalid_json(
"Batch ops must all be operations".to_string(),
))
}
}
}
Op::Batch(out)
}
other => {
return Err(ParseError::invalid_json(format!(
"Unknown operation: {other}"
)))
}
};
Ok(Some(op))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorCode;
fn j(s: &str) -> Json {
serde_json::from_str(s).expect("test literal must be valid JSON")
}
#[test]
fn decodes_every_op() {
assert!(matches!(
Op::classify(&j(r#"{"__op":"Increment","amount":3}"#)).unwrap(),
Some(Op::Increment(a)) if a == 3.0
));
assert!(matches!(
Op::classify(&j(r#"{"__op":"Increment","amount":-2}"#)).unwrap(),
Some(Op::Increment(a)) if a == -2.0
));
assert!(matches!(
Op::classify(&j(r#"{"__op":"Delete"}"#)).unwrap(),
Some(Op::Delete)
));
assert!(matches!(
Op::classify(&j(r#"{"__op":"Add","objects":[1,2]}"#)).unwrap(),
Some(Op::Add(v)) if v.len() == 2
));
assert!(matches!(
Op::classify(&j(r#"{"__op":"AddUnique","objects":[]}"#)).unwrap(),
Some(Op::AddUnique(v)) if v.is_empty()
));
assert!(matches!(
Op::classify(&j(r#"{"__op":"Remove","objects":[1]}"#)).unwrap(),
Some(Op::Remove(_))
));
assert!(matches!(
Op::classify(&j(r#"{"__op":"AddRelation","objects":[]}"#)).unwrap(),
Some(Op::AddRelation(_))
));
assert!(matches!(
Op::classify(&j(r#"{"__op":"RemoveRelation","objects":[]}"#)).unwrap(),
Some(Op::RemoveRelation(_))
));
}
#[test]
fn batch_nests() {
let src = r#"{"__op":"Batch","ops":[
{"__op":"AddRelation","objects":[]},
{"__op":"RemoveRelation","objects":[]}
]}"#;
match Op::classify(&j(src)).unwrap() {
Some(Op::Batch(ops)) => assert_eq!(ops.len(), 2),
other => panic!("expected Batch, got {other:?}"),
}
}
#[test]
fn non_ops_are_not_errors() {
assert!(Op::classify(&j("42")).unwrap().is_none());
assert!(Op::classify(&j(r#""text""#)).unwrap().is_none());
assert!(Op::classify(&j(r#"{"a":1}"#)).unwrap().is_none());
assert!(Op::classify(&j(r#"{"__op":7}"#)).unwrap().is_none());
}
#[test]
fn malformed_ops_are_errors() {
assert_eq!(
Op::classify(&j(r#"{"__op":"Nope"}"#)).unwrap_err().code,
ErrorCode::InvalidJson
);
assert_eq!(
Op::classify(&j(r#"{"__op":"Add","objects":3}"#))
.unwrap_err()
.code,
ErrorCode::InvalidJson
);
assert_eq!(
Op::classify(&j(r#"{"__op":"Increment","amount":"x"}"#))
.unwrap_err()
.code,
ErrorCode::InvalidJson
);
assert_eq!(
Op::classify(&j(r#"{"__op":"Batch","ops":[{"a":1}]}"#))
.unwrap_err()
.code,
ErrorCode::InvalidJson
);
}
#[test]
fn op_objects_may_contain_tagged_values() {
let src = r#"{"__op":"AddRelation","objects":[
{"__type":"Pointer","className":"Post","objectId":"abc"}
]}"#;
match Op::classify(&j(src)).unwrap() {
Some(Op::AddRelation(v)) => {
assert!(matches!(v[0], ParseValue::Pointer { .. }))
}
other => panic!("expected AddRelation, got {other:?}"),
}
}
}