Skip to main content

oxirs_ttl/patch/
patch_types.rs

1//! Data types for the RDF Patch protocol.
2//!
3//! Contains: [`PatchError`], [`PatchResult`], [`PatchHeader`], [`PatchTerm`],
4//! [`PatchTriple`], [`PatchQuad`], [`PatchChange`], [`RdfPatch`], [`PatchStats`], [`Graph`].
5
6use crate::writer::{RdfTerm, TermType};
7use std::collections::{BTreeMap, HashSet};
8use std::fmt;
9
10// ─── Error ───────────────────────────────────────────────────────────────────
11
12/// Error produced when parsing an RDF Patch document
13#[derive(Debug, Clone)]
14pub struct PatchError {
15    /// 1-based line number where the error occurred
16    pub line: usize,
17    /// Human-readable description
18    pub message: String,
19}
20
21impl PatchError {
22    pub(crate) fn new(line: usize, message: impl Into<String>) -> Self {
23        Self {
24            line,
25            message: message.into(),
26        }
27    }
28
29    pub(crate) fn at(line: usize, msg: impl fmt::Display) -> Self {
30        Self::new(line, msg.to_string())
31    }
32}
33
34impl fmt::Display for PatchError {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(f, "patch error at line {}: {}", self.line, self.message)
37    }
38}
39
40impl std::error::Error for PatchError {}
41
42/// Convenience result alias
43pub type PatchResult<T> = Result<T, PatchError>;
44
45// ─── Data model ──────────────────────────────────────────────────────────────
46
47/// A header entry in an RDF Patch document
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum PatchHeader {
50    /// `H version <value>` — patch format version
51    Version(String),
52    /// `H prev <uuid>` — IRI/UUID of the previous patch in the chain
53    Previous(String),
54    /// `H id <uuid>` — IRI/UUID identifying this patch
55    Id(String),
56    /// Any other `H key <value>` header not defined in the spec
57    Unknown {
58        /// The header key
59        key: String,
60        /// The header value
61        value: String,
62    },
63}
64
65impl PatchHeader {
66    /// Return the header key string as it appears in the serialised format
67    pub fn key(&self) -> &str {
68        match self {
69            PatchHeader::Version(_) => "version",
70            PatchHeader::Previous(_) => "prev",
71            PatchHeader::Id(_) => "id",
72            PatchHeader::Unknown { key, .. } => key.as_str(),
73        }
74    }
75
76    /// Return the header value string
77    pub fn value(&self) -> &str {
78        match self {
79            PatchHeader::Version(v) | PatchHeader::Previous(v) | PatchHeader::Id(v) => v.as_str(),
80            PatchHeader::Unknown { value, .. } => value.as_str(),
81        }
82    }
83}
84
85impl fmt::Display for PatchHeader {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "H {} {}", self.key(), self.value())
88    }
89}
90
91/// A subject/object position that accepts either a named node, blank node, or literal.
92/// Used internally for parsed term positions.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct PatchTerm(pub RdfTerm);
95
96impl PatchTerm {
97    /// Create from an IRI (angle-bracket or prefixed form, already resolved)
98    pub fn iri(iri: impl Into<String>) -> Self {
99        Self(RdfTerm::iri(iri))
100    }
101
102    /// Create from a blank node identifier
103    pub fn blank_node(id: impl Into<String>) -> Self {
104        Self(RdfTerm::blank_node(id))
105    }
106
107    /// Create from a plain literal value
108    pub fn literal(value: impl Into<String>) -> Self {
109        Self(RdfTerm::simple_literal(value))
110    }
111
112    /// Create from a language-tagged literal
113    pub fn lang_literal(value: impl Into<String>, lang: impl Into<String>) -> Self {
114        Self(RdfTerm::lang_literal(value, lang))
115    }
116
117    /// Create from a typed literal
118    pub fn typed_literal(value: impl Into<String>, datatype: impl Into<String>) -> Self {
119        Self(RdfTerm::typed_literal(value, datatype))
120    }
121
122    /// Access the underlying [`RdfTerm`]
123    pub fn term(&self) -> &RdfTerm {
124        &self.0
125    }
126
127    /// Return `true` if this term is an IRI
128    pub fn is_iri(&self) -> bool {
129        self.0.term_type == TermType::Iri
130    }
131
132    /// Return `true` if this term is a blank node
133    pub fn is_blank_node(&self) -> bool {
134        self.0.term_type == TermType::BlankNode
135    }
136
137    /// Return the lexical value
138    pub fn value(&self) -> &str {
139        &self.0.value
140    }
141}
142
143impl fmt::Display for PatchTerm {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match &self.0.term_type {
146            TermType::Iri => write!(f, "<{}>", self.0.value),
147            TermType::BlankNode => write!(f, "_:{}", self.0.value),
148            TermType::Literal { datatype, lang } => {
149                // Escape internal quotes
150                let escaped = self.0.value.replace('\\', "\\\\").replace('"', "\\\"");
151                write!(f, "\"{escaped}\"")?;
152                if let Some(l) = lang {
153                    write!(f, "@{l}")?;
154                } else if let Some(dt) = datatype {
155                    write!(f, "^^<{dt}>")?;
156                }
157                Ok(())
158            }
159        }
160    }
161}
162
163/// A triple (subject, predicate, object) using [`PatchTerm`]
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct PatchTriple {
166    /// The subject term
167    pub subject: PatchTerm,
168    /// The predicate term
169    pub predicate: PatchTerm,
170    /// The object term
171    pub object: PatchTerm,
172}
173
174impl PatchTriple {
175    /// Construct a new triple from three terms
176    pub fn new(subject: PatchTerm, predicate: PatchTerm, object: PatchTerm) -> Self {
177        Self {
178            subject,
179            predicate,
180            object,
181        }
182    }
183}
184
185impl fmt::Display for PatchTriple {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        write!(f, "{} {} {} .", self.subject, self.predicate, self.object)
188    }
189}
190
191/// A quad (subject, predicate, object, graph) using [`PatchTerm`]
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct PatchQuad {
194    /// The subject term
195    pub subject: PatchTerm,
196    /// The predicate term
197    pub predicate: PatchTerm,
198    /// The object term
199    pub object: PatchTerm,
200    /// The named graph term
201    pub graph: PatchTerm,
202}
203
204impl PatchQuad {
205    /// Construct a new quad
206    pub fn new(
207        subject: PatchTerm,
208        predicate: PatchTerm,
209        object: PatchTerm,
210        graph: PatchTerm,
211    ) -> Self {
212        Self {
213            subject,
214            predicate,
215            object,
216            graph,
217        }
218    }
219}
220
221impl fmt::Display for PatchQuad {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        write!(
224            f,
225            "{} {} {} {} .",
226            self.subject, self.predicate, self.object, self.graph
227        )
228    }
229}
230
231/// A single change line in an RDF Patch document
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum PatchChange {
234    /// `PA prefix <iri>` — add a prefix declaration
235    AddPrefix {
236        /// Namespace prefix label
237        prefix: String,
238        /// IRI bound to the prefix
239        iri: String,
240    },
241    /// `PD prefix <iri>` — delete a prefix declaration
242    DeletePrefix {
243        /// Namespace prefix label
244        prefix: String,
245        /// IRI bound to the prefix
246        iri: String,
247    },
248    /// `A <s> <p> <o> .` — add a triple
249    AddTriple(PatchTriple),
250    /// `D <s> <p> <o> .` — delete a triple
251    DeleteTriple(PatchTriple),
252    /// `A <s> <p> <o> <g> .` — add a quad
253    AddQuad(PatchQuad),
254    /// `D <s> <p> <o> <g> .` — delete a quad
255    DeleteQuad(PatchQuad),
256    /// `TX` — begin a transaction block
257    TransactionBegin,
258    /// `TC` — commit a transaction block
259    TransactionCommit,
260    /// `TA` — abort a transaction block
261    TransactionAbort,
262}
263
264impl PatchChange {
265    /// Return the line prefix that represents this change in the patch format
266    pub fn line_prefix(&self) -> &'static str {
267        match self {
268            PatchChange::AddPrefix { .. } => "PA",
269            PatchChange::DeletePrefix { .. } => "PD",
270            PatchChange::AddTriple(_) => "A",
271            PatchChange::DeleteTriple(_) => "D",
272            PatchChange::AddQuad(_) => "A",
273            PatchChange::DeleteQuad(_) => "D",
274            PatchChange::TransactionBegin => "TX",
275            PatchChange::TransactionCommit => "TC",
276            PatchChange::TransactionAbort => "TA",
277        }
278    }
279
280    /// Return `true` if this change adds data
281    pub fn is_add(&self) -> bool {
282        matches!(
283            self,
284            PatchChange::AddTriple(_) | PatchChange::AddQuad(_) | PatchChange::AddPrefix { .. }
285        )
286    }
287
288    /// Return `true` if this change deletes data
289    pub fn is_delete(&self) -> bool {
290        matches!(
291            self,
292            PatchChange::DeleteTriple(_)
293                | PatchChange::DeleteQuad(_)
294                | PatchChange::DeletePrefix { .. }
295        )
296    }
297
298    /// Return `true` if this is a transaction control statement
299    pub fn is_transaction_control(&self) -> bool {
300        matches!(
301            self,
302            PatchChange::TransactionBegin
303                | PatchChange::TransactionCommit
304                | PatchChange::TransactionAbort
305        )
306    }
307}
308
309impl fmt::Display for PatchChange {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        match self {
312            PatchChange::AddPrefix { prefix, iri } => {
313                write!(f, "PA {prefix} <{iri}>")
314            }
315            PatchChange::DeletePrefix { prefix, iri } => {
316                write!(f, "PD {prefix} <{iri}>")
317            }
318            PatchChange::AddTriple(t) => write!(f, "A {t}"),
319            PatchChange::DeleteTriple(t) => write!(f, "D {t}"),
320            PatchChange::AddQuad(q) => write!(f, "A {q}"),
321            PatchChange::DeleteQuad(q) => write!(f, "D {q}"),
322            PatchChange::TransactionBegin => write!(f, "TX"),
323            PatchChange::TransactionCommit => write!(f, "TC"),
324            PatchChange::TransactionAbort => write!(f, "TA"),
325        }
326    }
327}
328
329// ─── RdfPatch ────────────────────────────────────────────────────────────────
330
331/// A complete RDF Patch document: a list of headers followed by change lines
332#[derive(Debug, Clone, Default)]
333pub struct RdfPatch {
334    /// Header entries (`H key value`)
335    pub headers: Vec<PatchHeader>,
336    /// Change entries (`TX / TC / TA / PA / PD / A / D`)
337    pub changes: Vec<PatchChange>,
338}
339
340impl RdfPatch {
341    /// Construct an empty patch
342    pub fn new() -> Self {
343        Self::default()
344    }
345
346    /// Construct a patch with headers and changes
347    pub fn with_changes(headers: Vec<PatchHeader>, changes: Vec<PatchChange>) -> Self {
348        Self { headers, changes }
349    }
350
351    /// Return the `id` header value if present
352    pub fn id(&self) -> Option<&str> {
353        self.headers.iter().find_map(|h| {
354            if let PatchHeader::Id(v) = h {
355                Some(v.as_str())
356            } else {
357                None
358            }
359        })
360    }
361
362    /// Return the `prev` header value if present
363    pub fn previous(&self) -> Option<&str> {
364        self.headers.iter().find_map(|h| {
365            if let PatchHeader::Previous(v) = h {
366                Some(v.as_str())
367            } else {
368                None
369            }
370        })
371    }
372
373    /// Count how many triple/quad additions are in the patch
374    pub fn add_count(&self) -> usize {
375        self.changes
376            .iter()
377            .filter(|c| matches!(c, PatchChange::AddTriple(_) | PatchChange::AddQuad(_)))
378            .count()
379    }
380
381    /// Count how many triple/quad deletions are in the patch
382    pub fn delete_count(&self) -> usize {
383        self.changes
384            .iter()
385            .filter(|c| matches!(c, PatchChange::DeleteTriple(_) | PatchChange::DeleteQuad(_)))
386            .count()
387    }
388
389    /// Return `true` if the patch contains no headers and no changes
390    pub fn is_empty(&self) -> bool {
391        self.headers.is_empty() && self.changes.is_empty()
392    }
393}
394
395// ─── Statistics ──────────────────────────────────────────────────────────────
396
397/// Statistics collected when applying a patch to a graph
398#[derive(Debug, Clone, Default, PartialEq, Eq)]
399pub struct PatchStats {
400    /// Number of triples that were actually inserted
401    pub triples_added: usize,
402    /// Number of triples that were actually removed
403    pub triples_deleted: usize,
404    /// Number of prefix declarations that were added
405    pub prefixes_added: usize,
406    /// Number of prefix declarations that were removed
407    pub prefixes_deleted: usize,
408    /// Number of transaction blocks encountered
409    pub transactions: usize,
410    /// Number of transaction aborts encountered
411    pub aborts: usize,
412}
413
414// ─── In-memory graph ─────────────────────────────────────────────────────────
415
416/// A minimal in-memory RDF graph used for patch application and diff generation.
417///
418/// Triples are stored as `(subject, predicate, object)` tuples of [`PatchTerm`].
419/// Prefix mappings are stored separately.
420#[derive(Debug, Clone, Default)]
421pub struct Graph {
422    /// Set of (subject, predicate, object) triples
423    pub triples: HashSet<String>,
424    /// Prefix → IRI mappings
425    pub prefixes: BTreeMap<String, String>,
426    /// Raw triple objects for retrieval (mirrors `triples`)
427    triple_objects: Vec<PatchTriple>,
428}
429
430impl Graph {
431    /// Construct an empty graph
432    pub fn new() -> Self {
433        Self::default()
434    }
435
436    /// Add a triple to the graph; returns `true` if newly inserted
437    pub fn add_triple(&mut self, triple: PatchTriple) -> bool {
438        let key = Self::triple_key(&triple);
439        if self.triples.insert(key) {
440            self.triple_objects.push(triple);
441            true
442        } else {
443            false
444        }
445    }
446
447    /// Remove a triple from the graph; returns `true` if it was present
448    pub fn remove_triple(&mut self, triple: &PatchTriple) -> bool {
449        let key = Self::triple_key(triple);
450        if self.triples.remove(&key) {
451            self.triple_objects.retain(|t| Self::triple_key(t) != key);
452            true
453        } else {
454            false
455        }
456    }
457
458    /// Return `true` if the triple is present in the graph
459    pub fn contains(&self, triple: &PatchTriple) -> bool {
460        self.triples.contains(&Self::triple_key(triple))
461    }
462
463    /// Number of triples in the graph
464    pub fn len(&self) -> usize {
465        self.triples.len()
466    }
467
468    /// Return `true` if the graph has no triples
469    pub fn is_empty(&self) -> bool {
470        self.triples.is_empty()
471    }
472
473    /// Iterate over all triples in the graph
474    pub fn iter(&self) -> impl Iterator<Item = &PatchTriple> {
475        self.triple_objects.iter()
476    }
477
478    pub(crate) fn triple_key(t: &PatchTriple) -> String {
479        format!("{}\x00{}\x00{}", t.subject, t.predicate, t.object)
480    }
481}