use serde_json::Value;
use super::entity_key::EntityKey;
use crate::error::{FraiseQLError, Result};
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct CascadeEntities {
pub updated: Vec<EntityKey>,
pub deleted: Vec<EntityKey>,
}
impl CascadeEntities {
pub const fn new(updated: Vec<EntityKey>, deleted: Vec<EntityKey>) -> Self {
Self { updated, deleted }
}
#[must_use]
pub fn all_affected(&self) -> Vec<EntityKey> {
let mut all = self.updated.clone();
all.extend(self.deleted.clone());
all
}
#[must_use]
pub const fn has_changes(&self) -> bool {
!self.updated.is_empty() || !self.deleted.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct CascadeResponseParser;
impl CascadeResponseParser {
#[must_use]
pub const fn new() -> Self {
Self
}
pub fn parse_cascade_response(&self, response: &Value) -> Result<CascadeEntities> {
let cascade = self.find_cascade_field(response)?;
if cascade.is_null() {
return Ok(CascadeEntities {
updated: Vec::new(),
deleted: Vec::new(),
});
}
let updated = self.extract_entities_list(&cascade, "updated")?;
let deleted = self.extract_entities_list(&cascade, "deleted")?;
Ok(CascadeEntities { updated, deleted })
}
fn find_cascade_field(&self, response: &Value) -> Result<Value> {
if let Some(cascade) = response.get("cascade") {
return Ok(cascade.clone());
}
if let Some(data) = response.get("data") {
if let Some(cascade) = data.get("cascade") {
return Ok(cascade.clone());
}
for (_key, value) in data.as_object().unwrap_or(&serde_json::Map::default()) {
if let Some(cascade) = value.get("cascade") {
return Ok(cascade.clone());
}
}
}
for (_key, value) in response.as_object().unwrap_or(&serde_json::Map::default()) {
if let Some(cascade) = value.get("cascade") {
return Ok(cascade.clone());
}
}
Ok(Value::Null)
}
fn extract_entities_list(&self, cascade: &Value, field_name: &str) -> Result<Vec<EntityKey>> {
let entities_array = match cascade.get(field_name) {
Some(Value::Array(arr)) => arr,
Some(Value::Null) | None => return Ok(Vec::new()),
Some(val) => {
return Err(FraiseQLError::Validation {
message: format!(
"cascade.{} should be array, got {}",
field_name,
match val {
Value::Object(_) => "object",
Value::String(_) => "string",
Value::Number(_) => "number",
Value::Bool(_) => "boolean",
Value::Null => "null",
Value::Array(_) => "unknown",
}
),
path: Some(format!("cascade.{}", field_name)),
});
},
};
let mut entities = Vec::new();
for entity_obj in entities_array {
let entity = self.parse_cascade_entity(entity_obj)?;
entities.push(entity);
}
Ok(entities)
}
fn parse_cascade_entity(&self, entity_obj: &Value) -> Result<EntityKey> {
let obj = entity_obj.as_object().ok_or_else(|| FraiseQLError::Validation {
message: "Cascade entity should be object".to_string(),
path: Some("cascade.updated[*]".to_string()),
})?;
let type_name = obj.get("__typename").and_then(Value::as_str).ok_or_else(|| {
FraiseQLError::Validation {
message: "Cascade entity missing __typename field".to_string(),
path: Some("cascade.updated[*].__typename".to_string()),
}
})?;
let entity_id =
obj.get("id").and_then(Value::as_str).ok_or_else(|| FraiseQLError::Validation {
message: "Cascade entity missing id field".to_string(),
path: Some("cascade.updated[*].id".to_string()),
})?;
EntityKey::new(type_name, entity_id)
}
}
impl Default for CascadeResponseParser {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use serde_json::json;
use super::*;
#[test]
fn test_parse_simple_cascade_response() {
let parser = CascadeResponseParser::new();
let response = json!({
"createPost": {
"cascade": {
"updated": [
{
"__typename": "User",
"id": "550e8400-e29b-41d4-a716-446655440000",
"postCount": 5
}
]
}
}
});
let entities = parser.parse_cascade_response(&response).unwrap();
assert_eq!(entities.updated.len(), 1);
assert_eq!(entities.updated[0].entity_type, "User");
assert_eq!(entities.updated[0].entity_id, "550e8400-e29b-41d4-a716-446655440000");
assert_eq!(entities.deleted.len(), 0);
}
#[test]
fn test_parse_multiple_updated_entities() {
let parser = CascadeResponseParser::new();
let response = json!({
"updateUser": {
"cascade": {
"updated": [
{ "__typename": "User", "id": "uuid-1" },
{ "__typename": "Post", "id": "uuid-2" },
{ "__typename": "Notification", "id": "uuid-3" }
]
}
}
});
let entities = parser.parse_cascade_response(&response).unwrap();
assert_eq!(entities.updated.len(), 3);
assert_eq!(entities.updated[0].entity_type, "User");
assert_eq!(entities.updated[1].entity_type, "Post");
assert_eq!(entities.updated[2].entity_type, "Notification");
}
#[test]
fn test_parse_deleted_entities() {
let parser = CascadeResponseParser::new();
let response = json!({
"deletePost": {
"cascade": {
"deleted": [
{ "__typename": "Post", "id": "post-uuid" },
{ "__typename": "Comment", "id": "comment-uuid" }
]
}
}
});
let entities = parser.parse_cascade_response(&response).unwrap();
assert_eq!(entities.updated.len(), 0);
assert_eq!(entities.deleted.len(), 2);
assert_eq!(entities.deleted[0].entity_type, "Post");
assert_eq!(entities.deleted[1].entity_type, "Comment");
}
#[test]
fn test_parse_both_updated_and_deleted() {
let parser = CascadeResponseParser::new();
let response = json!({
"mutation": {
"cascade": {
"updated": [{ "__typename": "User", "id": "u-1" }],
"deleted": [{ "__typename": "Session", "id": "s-1" }]
}
}
});
let entities = parser.parse_cascade_response(&response).unwrap();
assert_eq!(entities.updated.len(), 1);
assert_eq!(entities.deleted.len(), 1);
assert_eq!(entities.all_affected().len(), 2);
}
#[test]
fn test_parse_empty_cascade() {
let parser = CascadeResponseParser::new();
let response = json!({
"mutation": {
"cascade": {
"updated": [],
"deleted": []
}
}
});
let entities = parser.parse_cascade_response(&response).unwrap();
assert!(!entities.has_changes());
assert_eq!(entities.all_affected().len(), 0);
}
#[test]
fn test_parse_no_cascade_field() {
let parser = CascadeResponseParser::new();
let response = json!({
"createPost": {
"post": { "id": "post-1", "title": "Hello" }
}
});
let entities = parser.parse_cascade_response(&response).unwrap();
assert!(!entities.has_changes());
}
#[test]
fn test_parse_nested_in_data_field() {
let parser = CascadeResponseParser::new();
let response = json!({
"data": {
"createPost": {
"cascade": {
"updated": [{ "__typename": "User", "id": "uuid-1" }]
}
}
}
});
let entities = parser.parse_cascade_response(&response).unwrap();
assert_eq!(entities.updated.len(), 1);
}
#[test]
fn test_parse_missing_typename() {
let parser = CascadeResponseParser::new();
let response = json!({
"mutation": {
"cascade": {
"updated": [{ "id": "uuid-1" }]
}
}
});
let result = parser.parse_cascade_response(&response);
assert!(
matches!(result, Err(FraiseQLError::Validation { .. })),
"expected Validation error for missing __typename, got: {result:?}"
);
}
#[test]
fn test_parse_missing_id() {
let parser = CascadeResponseParser::new();
let response = json!({
"mutation": {
"cascade": {
"updated": [{ "__typename": "User" }]
}
}
});
let result = parser.parse_cascade_response(&response);
assert!(
matches!(result, Err(FraiseQLError::Validation { .. })),
"expected Validation error for missing id, got: {result:?}"
);
}
#[test]
fn test_cascade_entities_all_affected() {
let updated = vec![
EntityKey::new("User", "u-1").unwrap(),
EntityKey::new("User", "u-2").unwrap(),
];
let deleted = vec![EntityKey::new("Post", "p-1").unwrap()];
let cascade = CascadeEntities::new(updated, deleted);
let all = cascade.all_affected();
assert_eq!(all.len(), 3);
}
}