use std::sync::Arc;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EditProvenance {
pub source_edit_desc: String,
pub rules_applied: Vec<Arc<str>>,
pub policy_consulted: Option<Arc<str>>,
pub was_total: bool,
}
impl EditProvenance {
#[must_use]
pub const fn new(source_edit_desc: String) -> Self {
Self {
source_edit_desc,
rules_applied: Vec::new(),
policy_consulted: None,
was_total: true,
}
}
pub fn record_rule(&mut self, rule: Arc<str>) {
self.rules_applied.push(rule);
}
pub fn record_policy(&mut self, policy: Arc<str>) {
self.policy_consulted = Some(policy);
}
pub const fn mark_partial(&mut self) {
self.was_total = false;
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::sync::Arc;
use panproto_gat::Name;
use panproto_inst::TreeEdit;
use panproto_schema::Protocol;
use crate::edit_lens::EditLens;
use crate::tests::{identity_lens, three_node_instance, three_node_schema};
use super::EditProvenance;
fn test_protocol() -> Protocol {
Protocol {
name: "test".into(),
schema_theory: "ThTest".into(),
instance_theory: "ThWType".into(),
schema_composition: None,
instance_composition: None,
edge_rules: vec![],
obj_kinds: vec![],
constraint_sorts: vec![],
has_order: false,
has_coproducts: false,
has_recursion: false,
has_causal: false,
nominal_identity: false,
has_defaults: false,
has_coercions: false,
has_mergers: false,
has_policies: false,
}
}
#[test]
fn provenance_records_structural_remap() {
let schema = three_node_schema();
let lens = identity_lens(&schema);
let instance = three_node_instance();
let mut edit_lens = EditLens::from_lens(lens, test_protocol());
edit_lens.initialize(&instance).unwrap();
let edit = TreeEdit::SetField {
node_id: 1,
field: Name::from("text"),
value: panproto_inst::Value::Str("updated".into()),
};
let (_translated, provenance) = edit_lens.get_edit_with_provenance(edit).unwrap();
assert!(
provenance
.rules_applied
.iter()
.any(|r| r.as_ref() == "structural_remap"),
"provenance should record structural_remap rule"
);
}
#[test]
fn provenance_identity_is_total() {
let schema = three_node_schema();
let lens = identity_lens(&schema);
let instance = three_node_instance();
let mut edit_lens = EditLens::from_lens(lens, test_protocol());
edit_lens.initialize(&instance).unwrap();
let edit = TreeEdit::SetField {
node_id: 1,
field: Name::from("text"),
value: panproto_inst::Value::Str("hello".into()),
};
let (_translated, provenance) = edit_lens.get_edit_with_provenance(edit).unwrap();
assert!(
provenance.was_total,
"identity lens translation should be total"
);
}
#[test]
fn provenance_serialization_round_trip() {
let prov = EditProvenance {
source_edit_desc: "SetField(1, text)".into(),
rules_applied: vec![Arc::from("structural_remap"), Arc::from("field_text")],
policy_consulted: Some(Arc::from("last_writer_wins")),
was_total: true,
};
let json = serde_json::to_string(&prov).unwrap();
let back: EditProvenance = serde_json::from_str(&json).unwrap();
assert_eq!(back.source_edit_desc, prov.source_edit_desc);
assert_eq!(back.rules_applied.len(), 2);
assert_eq!(back.rules_applied[0].as_ref(), "structural_remap");
assert_eq!(back.rules_applied[1].as_ref(), "field_text");
assert_eq!(back.policy_consulted.as_deref(), Some("last_writer_wins"));
assert!(back.was_total);
}
}