strixonomy-edit 0.27.0

Semantic transaction model for Strixonomy ontology editing
Documentation
use strixonomy_edit::{SemanticChange, Transaction};
use strixonomy_owl::PatchOp;

const TURTLE_FIXTURE: &str = r#"@prefix ex: <http://example.org/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .

ex:A a owl:Class .
"#;

#[test]
fn empty_transaction_fails_validation() {
    let txn = Transaction::default();
    assert!(txn.validate().is_err());
}

#[test]
fn compose_same_format() {
    let a = Transaction::from_turtle(vec![PatchOp::AddLabel {
        entity_iri: "http://example.org/A".into(),
        value: "One".into(),
    }]);
    let b = Transaction::from_turtle(vec![PatchOp::AddComment {
        entity_iri: "http://example.org/A".into(),
        value: "note".into(),
    }]);
    let merged = a.compose(b).expect("compose");
    assert_eq!(merged.changes.len(), 2);
}

#[test]
fn compose_mixed_format_fails() {
    let turtle = Transaction::from_turtle(vec![PatchOp::SetLabel {
        entity_iri: "http://example.org/A".into(),
        value: "A".into(),
    }]);
    let obo = Transaction::from_obo(vec![strixonomy_obo::OboPatchOp::SetName {
        term_id: "EX:0000001".into(),
        value: "term".into(),
    }]);
    assert!(turtle.compose(obo).is_err());
}

#[test]
fn invert_add_remove_label_roundtrip_text() {
    let txn = Transaction::from_turtle(vec![PatchOp::AddLabel {
        entity_iri: "http://example.org/A".into(),
        value: "Example".into(),
    }]);
    let namespaces = std::collections::BTreeMap::new();
    let applied = txn.apply_to_text(TURTLE_FIXTURE, false, &namespaces).expect("apply");
    let text = applied.preview_text.expect("preview");
    assert!(text.contains("rdfs:label"));

    let undo = txn.invert().expect("invert");
    let restored = undo.apply_to_text(&text, false, &namespaces).expect("undo apply");
    let restored_text = restored.preview_text.expect("restored");
    assert!(!restored_text.contains("rdfs:label"));
}

#[test]
fn transaction_json_roundtrip() {
    let txn = Transaction::from_turtle(vec![PatchOp::SetDeprecated {
        entity_iri: "http://example.org/A".into(),
        value: true,
    }]);
    let json = serde_json::to_string(&txn).expect("serialize");
    let parsed: Transaction = serde_json::from_str(&json).expect("deserialize");
    assert_eq!(txn, parsed);
}

#[test]
fn legacy_patch_array_parses_as_transaction() {
    let value = serde_json::json!([{
        "op": "add_label",
        "entity_iri": "http://example.org/A",
        "value": "X"
    }]);
    let txn = strixonomy_edit::parse_turtle_input(value).expect("parse");
    assert_eq!(txn.changes.len(), 1);
    assert!(matches!(txn.changes[0], SemanticChange::Turtle { .. }));
}

#[test]
fn turtle_envelope_with_raw_ops_parses() {
    let value = serde_json::json!({
        "transaction": {
            "changes": [{
                "op": "add_label",
                "entity_iri": "http://example.org/A",
                "value": "X"
            }]
        }
    });
    let txn = strixonomy_edit::parse_turtle_input(value).expect("parse");
    assert_eq!(txn.changes.len(), 1);
    assert!(matches!(txn.changes[0], SemanticChange::Turtle { .. }));
}

#[test]
fn turtle_path_rejects_obo_envelope() {
    let value = serde_json::json!({
        "transaction": {
            "changes": [{
                "format": "obo",
                "change": {
                    "op": "set_name",
                    "term_id": "EX:1",
                    "value": "x"
                }
            }]
        }
    });
    let err =
        strixonomy_edit::parse_turtle_input(value).expect_err("must reject OBO on turtle path");
    assert!(err.to_string().contains("OBO") || err.to_string().contains("Turtle"));
}

#[test]
fn obo_path_rejects_turtle_envelope() {
    let value = serde_json::json!({
        "transaction": {
            "changes": [{
                "format": "turtle",
                "change": {
                    "op": "add_label",
                    "entity_iri": "http://example.org/A",
                    "value": "X"
                }
            }]
        }
    });
    let err = strixonomy_edit::parse_obo_input(value).expect_err("must reject Turtle on obo path");
    assert!(err.to_string().contains("Turtle") || err.to_string().contains("OBO"));
}

#[test]
fn adapter_matches_direct_patch_apply() {
    let patches = vec![PatchOp::AddComment {
        entity_iri: "http://example.org/A".into(),
        value: "hello".into(),
    }];
    let namespaces = std::collections::BTreeMap::new();
    let direct =
        strixonomy_owl::apply_patches_to_text(TURTLE_FIXTURE, &patches, true, &namespaces).unwrap();
    let via_txn =
        Transaction::from_turtle(patches).apply_to_text(TURTLE_FIXTURE, true, &namespaces).unwrap();
    assert_eq!(direct.preview_text, via_txn.preview_text);
    assert_eq!(direct.applied, via_txn.applied);
}