ontocore_edit/
transaction.rs1use 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#[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 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
137pub 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
167pub 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}