1use crate::error::ParseError;
11use crate::value::ParseValue;
12use serde_json::Value as Json;
13
14#[derive(Debug, Clone)]
20pub enum Op {
21 Increment(f64),
23 Add(Vec<ParseValue>),
25 AddUnique(Vec<ParseValue>),
27 Remove(Vec<ParseValue>),
29 Delete,
31 AddRelation(Vec<ParseValue>),
33 RemoveRelation(Vec<ParseValue>),
35 Batch(Vec<Op>),
38}
39
40impl Op {
41 pub fn classify(value: &Json) -> Result<Option<Op>, ParseError> {
46 let map = match value {
47 Json::Object(m) => m,
48 _ => return Ok(None),
49 };
50 let name = match map.get("__op") {
51 Some(Json::String(s)) => s.as_str(),
52 _ => return Ok(None),
53 };
54
55 let objects = |key: &str| -> Result<Vec<ParseValue>, ParseError> {
56 match map.get(key) {
57 Some(Json::Array(a)) => a
58 .iter()
59 .cloned()
60 .map(crate::decode::classify_nested)
61 .collect::<Result<Vec<_>, _>>(),
62 _ => Err(ParseError::invalid_json(
65 "objects to add must be an array".to_string(),
66 )),
67 }
68 };
69
70 let op = match name {
71 "Increment" => {
72 let amount = map.get("amount").and_then(|v| v.as_f64()).ok_or_else(|| {
73 ParseError::invalid_json("incrementing must provide a number".to_string())
79 })?;
80 Op::Increment(amount)
81 }
82 "Add" => Op::Add(objects("objects")?),
83 "AddUnique" => Op::AddUnique(objects("objects")?),
84 "Remove" => Op::Remove(objects("objects")?),
85 "AddRelation" => Op::AddRelation(objects("objects")?),
86 "RemoveRelation" => Op::RemoveRelation(objects("objects")?),
87 "Delete" => Op::Delete,
88 "Batch" => {
89 let ops = match map.get("ops") {
90 Some(Json::Array(a)) => a,
91 _ => {
92 return Err(ParseError::invalid_json(
93 "Batch requires an ops array".to_string(),
94 ))
95 }
96 };
97 let mut out = Vec::with_capacity(ops.len());
98 for o in ops {
99 match Op::classify(o)? {
100 Some(inner) => out.push(inner),
101 None => {
102 return Err(ParseError::invalid_json(
103 "Batch ops must all be operations".to_string(),
104 ))
105 }
106 }
107 }
108 Op::Batch(out)
109 }
110 other => {
111 return Err(ParseError::invalid_json(format!(
112 "Unknown operation: {other}"
113 )))
114 }
115 };
116 Ok(Some(op))
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::error::ErrorCode;
124
125 fn j(s: &str) -> Json {
126 serde_json::from_str(s).expect("test literal must be valid JSON")
127 }
128
129 #[test]
130 fn decodes_every_op() {
131 assert!(matches!(
132 Op::classify(&j(r#"{"__op":"Increment","amount":3}"#)).unwrap(),
133 Some(Op::Increment(a)) if a == 3.0
134 ));
135 assert!(matches!(
137 Op::classify(&j(r#"{"__op":"Increment","amount":-2}"#)).unwrap(),
138 Some(Op::Increment(a)) if a == -2.0
139 ));
140 assert!(matches!(
141 Op::classify(&j(r#"{"__op":"Delete"}"#)).unwrap(),
142 Some(Op::Delete)
143 ));
144 assert!(matches!(
145 Op::classify(&j(r#"{"__op":"Add","objects":[1,2]}"#)).unwrap(),
146 Some(Op::Add(v)) if v.len() == 2
147 ));
148 assert!(matches!(
149 Op::classify(&j(r#"{"__op":"AddUnique","objects":[]}"#)).unwrap(),
150 Some(Op::AddUnique(v)) if v.is_empty()
151 ));
152 assert!(matches!(
153 Op::classify(&j(r#"{"__op":"Remove","objects":[1]}"#)).unwrap(),
154 Some(Op::Remove(_))
155 ));
156 assert!(matches!(
157 Op::classify(&j(r#"{"__op":"AddRelation","objects":[]}"#)).unwrap(),
158 Some(Op::AddRelation(_))
159 ));
160 assert!(matches!(
161 Op::classify(&j(r#"{"__op":"RemoveRelation","objects":[]}"#)).unwrap(),
162 Some(Op::RemoveRelation(_))
163 ));
164 }
165
166 #[test]
167 fn batch_nests() {
168 let src = r#"{"__op":"Batch","ops":[
169 {"__op":"AddRelation","objects":[]},
170 {"__op":"RemoveRelation","objects":[]}
171 ]}"#;
172 match Op::classify(&j(src)).unwrap() {
173 Some(Op::Batch(ops)) => assert_eq!(ops.len(), 2),
174 other => panic!("expected Batch, got {other:?}"),
175 }
176 }
177
178 #[test]
179 fn non_ops_are_not_errors() {
180 assert!(Op::classify(&j("42")).unwrap().is_none());
182 assert!(Op::classify(&j(r#""text""#)).unwrap().is_none());
183 assert!(Op::classify(&j(r#"{"a":1}"#)).unwrap().is_none());
184 assert!(Op::classify(&j(r#"{"__op":7}"#)).unwrap().is_none());
186 }
187
188 #[test]
189 fn malformed_ops_are_errors() {
190 assert_eq!(
191 Op::classify(&j(r#"{"__op":"Nope"}"#)).unwrap_err().code,
192 ErrorCode::InvalidJson
193 );
194 assert_eq!(
195 Op::classify(&j(r#"{"__op":"Add","objects":3}"#))
196 .unwrap_err()
197 .code,
198 ErrorCode::InvalidJson
199 );
200 assert_eq!(
201 Op::classify(&j(r#"{"__op":"Increment","amount":"x"}"#))
202 .unwrap_err()
203 .code,
204 ErrorCode::InvalidJson
205 );
206 assert_eq!(
207 Op::classify(&j(r#"{"__op":"Batch","ops":[{"a":1}]}"#))
208 .unwrap_err()
209 .code,
210 ErrorCode::InvalidJson
211 );
212 }
213
214 #[test]
215 fn op_objects_may_contain_tagged_values() {
216 let src = r#"{"__op":"AddRelation","objects":[
217 {"__type":"Pointer","className":"Post","objectId":"abc"}
218 ]}"#;
219 match Op::classify(&j(src)).unwrap() {
220 Some(Op::AddRelation(v)) => {
221 assert!(matches!(v[0], ParseValue::Pointer { .. }))
222 }
223 other => panic!("expected AddRelation, got {other:?}"),
224 }
225 }
226}