use std::collections::HashMap;
use serde::{Deserialize, Deserializer};
use serde_json::Value as JsonValue;
use uuid::Uuid;
use super::cascade::MutationErrorClass;
use crate::error::{FraiseQLError, Result};
const HTTP_STATUS_MIN: i16 = 100;
const HTTP_STATUS_MAX: i16 = 599;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum MutationOutcome {
Success {
entity: JsonValue,
entity_type: Option<String>,
entity_id: Option<String>,
cascade: Option<JsonValue>,
updated_fields: Vec<String>,
},
Error {
error_class: MutationErrorClass,
message: String,
http_status: Option<i16>,
entity_type: Option<String>,
metadata: JsonValue,
},
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct MutationResponse {
pub succeeded: bool,
pub state_changed: bool,
#[serde(default)]
pub error_class: Option<MutationErrorClass>,
#[serde(default)]
pub status_detail: Option<String>,
#[serde(default)]
pub http_status: Option<i16>,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub entity_id: Option<Uuid>,
#[serde(default)]
pub entity_type: Option<String>,
#[serde(default)]
pub entity: JsonValue,
#[serde(default, deserialize_with = "null_as_empty_string_vec")]
pub updated_fields: Vec<String>,
#[serde(default)]
pub cascade: JsonValue,
#[serde(default)]
pub error_detail: JsonValue,
#[serde(default)]
pub metadata: JsonValue,
}
fn null_as_empty_string_vec<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
Ok(Option::<Vec<String>>::deserialize(deserializer)?.unwrap_or_default())
}
pub fn parse_mutation_row<S: ::std::hash::BuildHasher>(
row: &HashMap<String, JsonValue, S>,
) -> Result<MutationOutcome> {
let obj: serde_json::Map<String, JsonValue> =
row.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
let parsed: MutationResponse =
serde_json::from_value(JsonValue::Object(obj)).map_err(|e| FraiseQLError::Validation {
message: format!("mutation_response row failed to deserialize: {e}"),
path: None,
})?;
to_outcome(parsed)
}
fn to_outcome(row: MutationResponse) -> Result<MutationOutcome> {
if let Some(status) = row.http_status {
if !(HTTP_STATUS_MIN..=HTTP_STATUS_MAX).contains(&status) {
return Err(FraiseQLError::Validation {
message: format!(
"mutation_response 'http_status' out of range: {status} \
(expected {HTTP_STATUS_MIN}..={HTTP_STATUS_MAX})"
),
path: None,
});
}
}
if row.succeeded {
if row.error_class.is_some() {
return Err(FraiseQLError::Validation {
message: "mutation_response: succeeded=true but error_class is set".to_string(),
path: None,
});
}
Ok(MutationOutcome::Success {
entity: row.entity,
entity_type: row.entity_type,
entity_id: row.entity_id.map(|u| u.to_string()),
cascade: filter_null(row.cascade),
updated_fields: row.updated_fields,
})
} else {
if row.state_changed {
return Err(FraiseQLError::Validation {
message: "mutation_response: succeeded=false with state_changed=true is illegal \
(partial-failure rows are builder-rejected)"
.to_string(),
path: None,
});
}
let Some(class) = row.error_class else {
return Err(FraiseQLError::Validation {
message: "mutation_response: succeeded=false requires error_class".to_string(),
path: None,
});
};
Ok(MutationOutcome::Error {
error_class: class,
message: row.message.unwrap_or_default(),
http_status: row.http_status,
entity_type: row.entity_type,
metadata: row.error_detail,
})
}
}
fn filter_null(v: JsonValue) -> Option<JsonValue> {
if v.is_null() { None } else { Some(v) }
}