Skip to main content

ontocore_edit/
transaction.rs

1use crate::adapter::{ApplyTextResult, EditFormat};
2use crate::change::SemanticChange;
3use crate::error::{EditError, Result};
4use crate::invert::invert_change;
5use ontocore_obo::OboPatchOp;
6use ontocore_owl::PatchOp;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10/// Ordered semantic edits applied as one atomic unit.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
12pub struct Transaction {
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub id: Option<String>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub label: Option<String>,
17    pub changes: Vec<SemanticChange>,
18}
19
20impl Transaction {
21    pub fn new(changes: Vec<SemanticChange>) -> Self {
22        Self { id: None, label: None, changes }
23    }
24
25    pub fn from_turtle(changes: Vec<PatchOp>) -> Self {
26        Self::new(changes.into_iter().map(SemanticChange::turtle).collect())
27    }
28
29    pub fn from_obo(changes: Vec<OboPatchOp>) -> Self {
30        Self::new(changes.into_iter().map(SemanticChange::obo).collect())
31    }
32
33    pub fn is_empty(&self) -> bool {
34        self.changes.is_empty()
35    }
36
37    pub fn format(&self) -> Result<EditFormat> {
38        if self.changes.is_empty() {
39            return Err(EditError::Empty);
40        }
41        let mut turtle = false;
42        let mut obo = false;
43        for change in &self.changes {
44            if change.is_turtle() {
45                turtle = true;
46            }
47            if change.is_obo() {
48                obo = true;
49            }
50        }
51        if turtle && obo {
52            return Err(EditError::MixedFormats);
53        }
54        if turtle {
55            Ok(EditFormat::Turtle)
56        } else if obo {
57            Ok(EditFormat::Obo)
58        } else {
59            Err(EditError::Empty)
60        }
61    }
62
63    pub fn compose(mut self, other: Transaction) -> Result<Self> {
64        if !self.changes.is_empty() && !other.changes.is_empty() {
65            let left = self.format()?;
66            let right = other.format()?;
67            if left != right {
68                return Err(EditError::MixedFormats);
69            }
70        }
71        self.changes.extend(other.changes);
72        Ok(self)
73    }
74
75    pub fn invert(&self) -> Result<Self> {
76        if self.is_empty() {
77            return Err(EditError::Empty);
78        }
79        let mut inverted = Vec::with_capacity(self.changes.len());
80        for change in self.changes.iter().rev() {
81            inverted.push(invert_change(change)?);
82        }
83        Ok(Self { id: self.id.clone(), label: self.label.clone(), changes: inverted })
84    }
85
86    pub fn validate(&self) -> Result<()> {
87        if self.is_empty() {
88            return Err(EditError::Validation("transaction has no changes".into()));
89        }
90        let _ = self.format()?;
91        Ok(())
92    }
93
94    pub fn apply_to_text(
95        &self,
96        source: &str,
97        preview_only: bool,
98        namespaces: &BTreeMap<String, String>,
99    ) -> Result<ApplyTextResult> {
100        self.validate()?;
101        crate::adapter::apply_transaction_to_text(self, source, preview_only, namespaces)
102    }
103
104    /// Apply with an explicit document format (RDF/XML / OWL/XML use Horned re-serialize).
105    pub fn apply_to_text_as(
106        &self,
107        source: &str,
108        preview_only: bool,
109        namespaces: &BTreeMap<String, String>,
110        format: EditFormat,
111    ) -> Result<ApplyTextResult> {
112        self.validate()?;
113        crate::adapter::apply_transaction_to_text_as(self, source, preview_only, namespaces, format)
114    }
115
116    pub fn turtle_patches(&self) -> Result<Vec<PatchOp>> {
117        self.changes
118            .iter()
119            .map(|c| match c {
120                SemanticChange::Turtle { change } => Ok(change.clone()),
121                SemanticChange::Obo { .. } => Err(EditError::MixedFormats),
122            })
123            .collect()
124    }
125
126    pub fn obo_patches(&self) -> Result<Vec<OboPatchOp>> {
127        self.changes
128            .iter()
129            .map(|c| match c {
130                SemanticChange::Obo { change } => Ok(change.clone()),
131                SemanticChange::Turtle { .. } => Err(EditError::MixedFormats),
132            })
133            .collect()
134    }
135}
136
137/// Parse legacy patch JSON (array) or `{ "transaction": { ... } }` envelope.
138///
139/// Envelope `changes` may be either raw Turtle ops (documented wire format) or
140/// tagged [`SemanticChange`] values. Rejects envelopes whose changes are not Turtle.
141pub fn parse_turtle_input(value: serde_json::Value) -> Result<Transaction> {
142    if let Some(txn) = value.get("transaction") {
143        if let Some(changes) = txn.get("changes") {
144            if let Ok(patches) = serde_json::from_value::<Vec<PatchOp>>(changes.clone()) {
145                let mut parsed = Transaction::from_turtle(patches);
146                parsed.id = txn.get("id").and_then(|v| v.as_str()).map(str::to_string);
147                parsed.label = txn.get("label").and_then(|v| v.as_str()).map(str::to_string);
148                return Ok(parsed);
149            }
150        }
151        let parsed: Transaction = serde_json::from_value(txn.clone())?;
152        match parsed.format()? {
153            EditFormat::Turtle => Ok(parsed),
154            EditFormat::Obo => Err(EditError::Validation(
155                "transaction contains OBO changes; refuse Turtle apply path".into(),
156            )),
157            EditFormat::RdfXml | EditFormat::OwlXml => Err(EditError::Validation(
158                "unexpected XML edit format on ParseOps from tagged SemanticChange".into(),
159            )),
160        }
161    } else {
162        let patches: Vec<PatchOp> = serde_json::from_value(value)?;
163        Ok(Transaction::from_turtle(patches))
164    }
165}
166
167/// Parse legacy OBO patch JSON (array) or `{ "transaction": { ... } }` envelope.
168pub fn parse_obo_input(value: serde_json::Value) -> Result<Transaction> {
169    if let Some(txn) = value.get("transaction") {
170        if let Some(changes) = txn.get("changes") {
171            if let Ok(patches) = serde_json::from_value::<Vec<OboPatchOp>>(changes.clone()) {
172                let mut parsed = Transaction::from_obo(patches);
173                parsed.id = txn.get("id").and_then(|v| v.as_str()).map(str::to_string);
174                parsed.label = txn.get("label").and_then(|v| v.as_str()).map(str::to_string);
175                return Ok(parsed);
176            }
177        }
178        let parsed: Transaction = serde_json::from_value(txn.clone())?;
179        match parsed.format()? {
180            EditFormat::Obo => Ok(parsed),
181            EditFormat::Turtle | EditFormat::RdfXml | EditFormat::OwlXml => {
182                Err(EditError::Validation(
183                    "transaction contains Turtle changes; refuse OBO apply path".into(),
184                ))
185            }
186        }
187    } else {
188        let patches: Vec<OboPatchOp> = serde_json::from_value(value)?;
189        Ok(Transaction::from_obo(patches))
190    }
191}