use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use uuid::Uuid;
use crate::boundary::envelope::{Diagnostic, DiagnosticCode, Envelope, Severity};
use crate::boundary::flat::FlatBundle;
use crate::boundary::lifecycle::{
check_manifest_version, compute_stats, now_iso8601_utc, parse_flat, EMBEDDED_SCHEMA,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntityKind {
Person,
Family,
Event,
Link,
Occupation,
Source,
Place,
Document,
}
impl EntityKind {
pub fn collection(self) -> &'static str {
match self {
EntityKind::Person => "persons",
EntityKind::Family => "families",
EntityKind::Event => "events",
EntityKind::Link => "links",
EntityKind::Occupation => "occupations",
EntityKind::Source => "sources",
EntityKind::Place => "places",
EntityKind::Document => "documents",
}
}
pub fn singular(self) -> &'static str {
match self {
EntityKind::Person => "person",
EntityKind::Family => "family",
EntityKind::Event => "event",
EntityKind::Link => "link",
EntityKind::Occupation => "occupation",
EntityKind::Source => "source",
EntityKind::Place => "place",
EntityKind::Document => "document",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeletePolicy {
Reject,
Cascade,
Orphan,
}
pub fn add_entity(flat_json: &str, kind: EntityKind, entity_json: &str) -> Envelope {
let mut bundle = match parse_flat(flat_json) {
Ok(b) => b,
Err(env) => return env,
};
if let Err(env) = check_manifest_version(&bundle.manifest) {
return env;
}
let mut entity: Value = match serde_json::from_str(entity_json) {
Ok(v) => v,
Err(e) => {
return Envelope::error(
DiagnosticCode::InvalidJson,
format!("cannot parse entity JSON: {e}"),
);
}
};
let Some(obj) = entity.as_object_mut() else {
return Envelope::error(
DiagnosticCode::InvalidBundleStructure,
"entity JSON must be a JSON object",
);
};
if !obj.contains_key("id") {
obj.insert("id".into(), Value::String(Uuid::new_v4().to_string()));
}
if !obj.contains_key("type") {
obj.insert("type".into(), Value::String(kind.singular().into()));
}
if !obj.contains_key("axgf_version") {
obj.insert("axgf_version".into(), Value::String("1.0".into()));
}
let id = match obj.get("id").and_then(Value::as_str) {
Some(s) => s.to_string(),
None => {
return Envelope::error(
DiagnosticCode::InvalidBundleStructure,
"entity.id is not a string",
);
}
};
let map = collection_mut(&mut bundle, kind);
if map.contains_key(&id) {
return Envelope::error(
DiagnosticCode::EntityAlreadyExists,
format!("{} already contains id {id}", kind.collection()),
);
}
let diags = validate_entity_in_isolation(kind, &entity, &id);
map.insert(id.clone(), entity);
refresh_manifest(&mut bundle);
let data = serde_json::to_value(&bundle).unwrap_or(Value::Null);
Envelope::ok_with(json!({"id": id, "bundle": data}), diags)
}
pub fn update_entity(flat_json: &str, kind: EntityKind, entity_json: &str) -> Envelope {
let mut bundle = match parse_flat(flat_json) {
Ok(b) => b,
Err(env) => return env,
};
if let Err(env) = check_manifest_version(&bundle.manifest) {
return env;
}
let entity: Value = match serde_json::from_str(entity_json) {
Ok(v) => v,
Err(e) => {
return Envelope::error(
DiagnosticCode::InvalidJson,
format!("cannot parse entity JSON: {e}"),
);
}
};
let id = match entity.get("id").and_then(Value::as_str) {
Some(s) => s.to_string(),
None => {
return Envelope::error(
DiagnosticCode::InvalidBundleStructure,
"update_entity requires entity.id to be present and a string",
);
}
};
let map = collection_mut(&mut bundle, kind);
if !map.contains_key(&id) {
return Envelope::error(
DiagnosticCode::EntityNotFound,
format!("{} does not contain id {id}", kind.collection()),
);
}
let diags = validate_entity_in_isolation(kind, &entity, &id);
map.insert(id.clone(), entity);
refresh_manifest(&mut bundle);
let data = serde_json::to_value(&bundle).unwrap_or(Value::Null);
Envelope::ok_with(json!({"id": id, "bundle": data}), diags)
}
pub fn delete_entity(
flat_json: &str,
kind: EntityKind,
id: &str,
policy: DeletePolicy,
) -> Envelope {
let mut bundle = match parse_flat(flat_json) {
Ok(b) => b,
Err(env) => return env,
};
if let Err(env) = check_manifest_version(&bundle.manifest) {
return env;
}
{
let map = collection_mut(&mut bundle, kind);
if !map.contains_key(id) {
return Envelope::error(
DiagnosticCode::EntityNotFound,
format!("{} does not contain id {id}", kind.collection()),
);
}
}
if matches!(policy, DeletePolicy::Reject) {
let referrers = find_referrers(&bundle, id, kind);
if !referrers.is_empty() {
return Envelope::error_many(vec![Diagnostic {
code: DiagnosticCode::DeleteBlockedByReference,
severity: Severity::Error,
message: format!(
"cannot delete {}/{id} under Reject: still referenced by {} entities: {:?}",
kind.collection(),
referrers.len(),
referrers
),
entity_ref: Some(format!("{}/{id}", kind.collection())),
}]);
}
}
if matches!(policy, DeletePolicy::Cascade | DeletePolicy::Orphan) {
scrub_bundle(&mut bundle, id, policy);
}
let map = collection_mut(&mut bundle, kind);
map.remove(id);
refresh_manifest(&mut bundle);
let data = serde_json::to_value(&bundle).unwrap_or(Value::Null);
Envelope::ok_with(json!({"id": id, "bundle": data}), Vec::new())
}
fn collection_mut(b: &mut FlatBundle, kind: EntityKind) -> &mut BTreeMap<String, Value> {
match kind {
EntityKind::Person => &mut b.persons,
EntityKind::Family => &mut b.families,
EntityKind::Event => &mut b.events,
EntityKind::Link => &mut b.links,
EntityKind::Occupation => &mut b.occupations,
EntityKind::Source => &mut b.sources,
EntityKind::Place => &mut b.places,
EntityKind::Document => &mut b.documents,
}
}
fn refresh_manifest(b: &mut FlatBundle) {
let stats = compute_stats(b);
let now = now_iso8601_utc();
if let Value::Object(ref mut m) = b.manifest {
m.insert("stats".into(), stats);
m.insert("updated_at".into(), Value::String(now));
}
}
fn find_referrers(b: &FlatBundle, target: &str, target_kind: EntityKind) -> Vec<String> {
let mut hits: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for (kind, coll, map) in entity_collections(b) {
for (id, value) in map {
if kind == target_kind && id == target {
continue;
}
if entity_references_target(value, target) {
hits.insert(format!("{coll}/{id}"));
}
}
}
hits.into_iter().collect()
}
fn entity_references_target(v: &Value, target: &str) -> bool {
match v {
Value::Object(m) => {
for (k, val) in m {
if k == "id" {
continue;
}
if k.ends_with("_id") && val.as_str() == Some(target) {
return true;
}
if entity_references_target(val, target) {
return true;
}
}
false
}
Value::Array(a) => a.iter().any(|item| entity_references_target(item, target)),
_ => false,
}
}
fn scrub_bundle(b: &mut FlatBundle, target: &str, policy: DeletePolicy) {
for map in [
&mut b.persons,
&mut b.families,
&mut b.events,
&mut b.links,
&mut b.occupations,
&mut b.sources,
&mut b.places,
&mut b.documents,
] {
for value in map.values_mut() {
scrub_value(value, target, policy);
}
}
}
fn scrub_value(v: &mut Value, target: &str, policy: DeletePolicy) {
match v {
Value::Object(m) => scrub_object(m, target, policy),
Value::Array(a) => {
if matches!(policy, DeletePolicy::Cascade) {
a.retain(|item| !object_holds_ref(item, target));
}
for item in a.iter_mut() {
scrub_value(item, target, policy);
}
}
_ => {}
}
}
fn scrub_object(m: &mut Map<String, Value>, target: &str, policy: DeletePolicy) {
let matching_keys: Vec<String> = m
.iter()
.filter_map(|(k, v)| {
if k == "id" || !k.ends_with("_id") {
return None;
}
if v.as_str() == Some(target) {
Some(k.clone())
} else {
None
}
})
.collect();
for k in matching_keys {
match policy {
DeletePolicy::Cascade => {
m.remove(&k);
}
DeletePolicy::Orphan => {
m.insert(k, Value::Null);
}
DeletePolicy::Reject => {}
}
}
for val in m.values_mut() {
scrub_value(val, target, policy);
}
}
fn object_holds_ref(v: &Value, target: &str) -> bool {
match v {
Value::Object(m) => m
.iter()
.any(|(k, val)| k != "id" && k.ends_with("_id") && val.as_str() == Some(target)),
_ => false,
}
}
fn validate_entity_in_isolation(kind: EntityKind, entity: &Value, id: &str) -> Vec<Diagnostic> {
use jsonschema::JSONSchema;
let root: Value = match serde_json::from_str(EMBEDDED_SCHEMA) {
Ok(v) => v,
Err(_) => return Vec::new(),
};
let defs = root.get("$defs").cloned().unwrap_or(Value::Null);
if defs.is_null() {
return Vec::new();
}
let wrapper = json!({
"$defs": defs,
"$ref": format!("#/$defs/{}", kind.singular()),
});
let compiled = match JSONSchema::compile(&wrapper) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let mut out = Vec::new();
if let Err(errors) = compiled.validate(entity) {
for e in errors {
out.push(Diagnostic {
code: DiagnosticCode::SchemaValidationFailed,
severity: Severity::Warning,
message: format!("{}: {e}", kind.singular()),
entity_ref: Some(format!("{}/{id}", kind.collection())),
});
}
}
out
}
fn entity_collections(b: &FlatBundle) -> [(EntityKind, &'static str, &BTreeMap<String, Value>); 8] {
[
(EntityKind::Person, "persons", &b.persons),
(EntityKind::Family, "families", &b.families),
(EntityKind::Event, "events", &b.events),
(EntityKind::Link, "links", &b.links),
(EntityKind::Occupation, "occupations", &b.occupations),
(EntityKind::Source, "sources", &b.sources),
(EntityKind::Place, "places", &b.places),
(EntityKind::Document, "documents", &b.documents),
]
}