1use serde_json::Value;
67
68use super::entity_key::EntityKey;
69use crate::error::{FraiseQLError, Result};
70
71#[derive(Debug, Clone, Eq, PartialEq)]
76pub struct CascadeEntities {
77 pub updated: Vec<EntityKey>,
79
80 pub deleted: Vec<EntityKey>,
82}
83
84impl CascadeEntities {
85 pub const fn new(updated: Vec<EntityKey>, deleted: Vec<EntityKey>) -> Self {
87 Self { updated, deleted }
88 }
89
90 #[must_use]
92 pub fn all_affected(&self) -> Vec<EntityKey> {
93 let mut all = self.updated.clone();
94 all.extend(self.deleted.clone());
95 all
96 }
97
98 #[must_use]
100 pub const fn has_changes(&self) -> bool {
101 !self.updated.is_empty() || !self.deleted.is_empty()
102 }
103}
104
105#[derive(Debug, Clone)]
110pub struct CascadeResponseParser;
111
112impl CascadeResponseParser {
113 #[must_use]
115 pub const fn new() -> Self {
116 Self
117 }
118
119 pub fn parse_cascade_response(&self, response: &Value) -> Result<CascadeEntities> {
159 let cascade = self.find_cascade_field(response)?;
161
162 if cascade.is_null() {
163 return Ok(CascadeEntities {
165 updated: Vec::new(),
166 deleted: Vec::new(),
167 });
168 }
169
170 let updated = self.extract_entities_list(&cascade, "updated")?;
172
173 let deleted = self.extract_entities_list(&cascade, "deleted")?;
175
176 Ok(CascadeEntities { updated, deleted })
177 }
178
179 fn find_cascade_field(&self, response: &Value) -> Result<Value> {
186 if let Some(cascade) = response.get("cascade") {
188 return Ok(cascade.clone());
189 }
190
191 if let Some(data) = response.get("data") {
193 if let Some(cascade) = data.get("cascade") {
194 return Ok(cascade.clone());
195 }
196
197 for (_key, value) in data.as_object().unwrap_or(&serde_json::Map::default()) {
199 if let Some(cascade) = value.get("cascade") {
200 return Ok(cascade.clone());
201 }
202 }
203 }
204
205 for (_key, value) in response.as_object().unwrap_or(&serde_json::Map::default()) {
207 if let Some(cascade) = value.get("cascade") {
208 return Ok(cascade.clone());
209 }
210 }
211
212 Ok(Value::Null)
214 }
215
216 fn extract_entities_list(&self, cascade: &Value, field_name: &str) -> Result<Vec<EntityKey>> {
218 let entities_array = match cascade.get(field_name) {
219 Some(Value::Array(arr)) => arr,
220 Some(Value::Null) | None => return Ok(Vec::new()),
221 Some(val) => {
222 return Err(FraiseQLError::Validation {
223 message: format!(
224 "cascade.{} should be array, got {}",
225 field_name,
226 match val {
227 Value::Object(_) => "object",
228 Value::String(_) => "string",
229 Value::Number(_) => "number",
230 Value::Bool(_) => "boolean",
231 Value::Null => "null",
232 Value::Array(_) => "unknown",
233 }
234 ),
235 path: Some(format!("cascade.{}", field_name)),
236 });
237 },
238 };
239
240 let mut entities = Vec::new();
241
242 for entity_obj in entities_array {
243 let entity = self.parse_cascade_entity(entity_obj)?;
244 entities.push(entity);
245 }
246
247 Ok(entities)
248 }
249
250 fn parse_cascade_entity(&self, entity_obj: &Value) -> Result<EntityKey> {
254 let obj = entity_obj.as_object().ok_or_else(|| FraiseQLError::Validation {
255 message: "Cascade entity should be object".to_string(),
256 path: Some("cascade.updated[*]".to_string()),
257 })?;
258
259 let type_name = obj.get("__typename").and_then(Value::as_str).ok_or_else(|| {
261 FraiseQLError::Validation {
262 message: "Cascade entity missing __typename field".to_string(),
263 path: Some("cascade.updated[*].__typename".to_string()),
264 }
265 })?;
266
267 let entity_id =
269 obj.get("id").and_then(Value::as_str).ok_or_else(|| FraiseQLError::Validation {
270 message: "Cascade entity missing id field".to_string(),
271 path: Some("cascade.updated[*].id".to_string()),
272 })?;
273
274 EntityKey::new(type_name, entity_id)
275 }
276}
277
278impl Default for CascadeResponseParser {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 #![allow(clippy::unwrap_used)] use serde_json::json;
289
290 use super::*;
291
292 #[test]
293 fn test_parse_simple_cascade_response() {
294 let parser = CascadeResponseParser::new();
295 let response = json!({
296 "createPost": {
297 "cascade": {
298 "updated": [
299 {
300 "__typename": "User",
301 "id": "550e8400-e29b-41d4-a716-446655440000",
302 "postCount": 5
303 }
304 ]
305 }
306 }
307 });
308
309 let entities = parser.parse_cascade_response(&response).unwrap();
310 assert_eq!(entities.updated.len(), 1);
311 assert_eq!(entities.updated[0].entity_type, "User");
312 assert_eq!(entities.updated[0].entity_id, "550e8400-e29b-41d4-a716-446655440000");
313 assert_eq!(entities.deleted.len(), 0);
314 }
315
316 #[test]
317 fn test_parse_multiple_updated_entities() {
318 let parser = CascadeResponseParser::new();
319 let response = json!({
320 "updateUser": {
321 "cascade": {
322 "updated": [
323 { "__typename": "User", "id": "uuid-1" },
324 { "__typename": "Post", "id": "uuid-2" },
325 { "__typename": "Notification", "id": "uuid-3" }
326 ]
327 }
328 }
329 });
330
331 let entities = parser.parse_cascade_response(&response).unwrap();
332 assert_eq!(entities.updated.len(), 3);
333 assert_eq!(entities.updated[0].entity_type, "User");
334 assert_eq!(entities.updated[1].entity_type, "Post");
335 assert_eq!(entities.updated[2].entity_type, "Notification");
336 }
337
338 #[test]
339 fn test_parse_deleted_entities() {
340 let parser = CascadeResponseParser::new();
341 let response = json!({
342 "deletePost": {
343 "cascade": {
344 "deleted": [
345 { "__typename": "Post", "id": "post-uuid" },
346 { "__typename": "Comment", "id": "comment-uuid" }
347 ]
348 }
349 }
350 });
351
352 let entities = parser.parse_cascade_response(&response).unwrap();
353 assert_eq!(entities.updated.len(), 0);
354 assert_eq!(entities.deleted.len(), 2);
355 assert_eq!(entities.deleted[0].entity_type, "Post");
356 assert_eq!(entities.deleted[1].entity_type, "Comment");
357 }
358
359 #[test]
360 fn test_parse_both_updated_and_deleted() {
361 let parser = CascadeResponseParser::new();
362 let response = json!({
363 "mutation": {
364 "cascade": {
365 "updated": [{ "__typename": "User", "id": "u-1" }],
366 "deleted": [{ "__typename": "Session", "id": "s-1" }]
367 }
368 }
369 });
370
371 let entities = parser.parse_cascade_response(&response).unwrap();
372 assert_eq!(entities.updated.len(), 1);
373 assert_eq!(entities.deleted.len(), 1);
374 assert_eq!(entities.all_affected().len(), 2);
375 }
376
377 #[test]
378 fn test_parse_empty_cascade() {
379 let parser = CascadeResponseParser::new();
380 let response = json!({
381 "mutation": {
382 "cascade": {
383 "updated": [],
384 "deleted": []
385 }
386 }
387 });
388
389 let entities = parser.parse_cascade_response(&response).unwrap();
390 assert!(!entities.has_changes());
391 assert_eq!(entities.all_affected().len(), 0);
392 }
393
394 #[test]
395 fn test_parse_no_cascade_field() {
396 let parser = CascadeResponseParser::new();
397 let response = json!({
398 "createPost": {
399 "post": { "id": "post-1", "title": "Hello" }
400 }
401 });
402
403 let entities = parser.parse_cascade_response(&response).unwrap();
404 assert!(!entities.has_changes());
405 }
406
407 #[test]
408 fn test_parse_nested_in_data_field() {
409 let parser = CascadeResponseParser::new();
410 let response = json!({
411 "data": {
412 "createPost": {
413 "cascade": {
414 "updated": [{ "__typename": "User", "id": "uuid-1" }]
415 }
416 }
417 }
418 });
419
420 let entities = parser.parse_cascade_response(&response).unwrap();
421 assert_eq!(entities.updated.len(), 1);
422 }
423
424 #[test]
425 fn test_parse_missing_typename() {
426 let parser = CascadeResponseParser::new();
427 let response = json!({
428 "mutation": {
429 "cascade": {
430 "updated": [{ "id": "uuid-1" }]
431 }
432 }
433 });
434
435 let result = parser.parse_cascade_response(&response);
436 assert!(
437 matches!(result, Err(FraiseQLError::Validation { .. })),
438 "expected Validation error for missing __typename, got: {result:?}"
439 );
440 }
441
442 #[test]
443 fn test_parse_missing_id() {
444 let parser = CascadeResponseParser::new();
445 let response = json!({
446 "mutation": {
447 "cascade": {
448 "updated": [{ "__typename": "User" }]
449 }
450 }
451 });
452
453 let result = parser.parse_cascade_response(&response);
454 assert!(
455 matches!(result, Err(FraiseQLError::Validation { .. })),
456 "expected Validation error for missing id, got: {result:?}"
457 );
458 }
459
460 #[test]
461 fn test_cascade_entities_all_affected() {
462 let updated = vec![
463 EntityKey::new("User", "u-1").unwrap(),
464 EntityKey::new("User", "u-2").unwrap(),
465 ];
466 let deleted = vec![EntityKey::new("Post", "p-1").unwrap()];
467
468 let cascade = CascadeEntities::new(updated, deleted);
469 let all = cascade.all_affected();
470 assert_eq!(all.len(), 3);
471 }
472}