1use crate::writer::{RdfTerm, TermType};
33use std::collections::{BTreeMap, HashSet};
34use std::fmt;
35use std::io::{BufRead, BufReader, Read};
36
37#[derive(Debug, Clone)]
41pub struct PatchError {
42 pub line: usize,
44 pub message: String,
46}
47
48impl PatchError {
49 fn new(line: usize, message: impl Into<String>) -> Self {
50 Self {
51 line,
52 message: message.into(),
53 }
54 }
55
56 fn at(line: usize, msg: impl fmt::Display) -> Self {
57 Self::new(line, msg.to_string())
58 }
59}
60
61impl fmt::Display for PatchError {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 write!(f, "patch error at line {}: {}", self.line, self.message)
64 }
65}
66
67impl std::error::Error for PatchError {}
68
69pub type PatchResult<T> = Result<T, PatchError>;
71
72#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum PatchHeader {
77 Version(String),
79 Previous(String),
81 Id(String),
83 Unknown {
85 key: String,
87 value: String,
89 },
90}
91
92impl PatchHeader {
93 pub fn key(&self) -> &str {
95 match self {
96 PatchHeader::Version(_) => "version",
97 PatchHeader::Previous(_) => "prev",
98 PatchHeader::Id(_) => "id",
99 PatchHeader::Unknown { key, .. } => key.as_str(),
100 }
101 }
102
103 pub fn value(&self) -> &str {
105 match self {
106 PatchHeader::Version(v) | PatchHeader::Previous(v) | PatchHeader::Id(v) => v.as_str(),
107 PatchHeader::Unknown { value, .. } => value.as_str(),
108 }
109 }
110}
111
112impl fmt::Display for PatchHeader {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 write!(f, "H {} {}", self.key(), self.value())
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct PatchTerm(pub RdfTerm);
122
123impl PatchTerm {
124 pub fn iri(iri: impl Into<String>) -> Self {
126 Self(RdfTerm::iri(iri))
127 }
128
129 pub fn blank_node(id: impl Into<String>) -> Self {
131 Self(RdfTerm::blank_node(id))
132 }
133
134 pub fn literal(value: impl Into<String>) -> Self {
136 Self(RdfTerm::simple_literal(value))
137 }
138
139 pub fn lang_literal(value: impl Into<String>, lang: impl Into<String>) -> Self {
141 Self(RdfTerm::lang_literal(value, lang))
142 }
143
144 pub fn typed_literal(value: impl Into<String>, datatype: impl Into<String>) -> Self {
146 Self(RdfTerm::typed_literal(value, datatype))
147 }
148
149 pub fn term(&self) -> &RdfTerm {
151 &self.0
152 }
153
154 pub fn is_iri(&self) -> bool {
156 self.0.term_type == TermType::Iri
157 }
158
159 pub fn is_blank_node(&self) -> bool {
161 self.0.term_type == TermType::BlankNode
162 }
163
164 pub fn value(&self) -> &str {
166 &self.0.value
167 }
168}
169
170impl fmt::Display for PatchTerm {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 match &self.0.term_type {
173 TermType::Iri => write!(f, "<{}>", self.0.value),
174 TermType::BlankNode => write!(f, "_:{}", self.0.value),
175 TermType::Literal { datatype, lang } => {
176 let escaped = self.0.value.replace('\\', "\\\\").replace('"', "\\\"");
178 write!(f, "\"{escaped}\"")?;
179 if let Some(l) = lang {
180 write!(f, "@{l}")?;
181 } else if let Some(dt) = datatype {
182 write!(f, "^^<{dt}>")?;
183 }
184 Ok(())
185 }
186 }
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct PatchTriple {
193 pub subject: PatchTerm,
195 pub predicate: PatchTerm,
197 pub object: PatchTerm,
199}
200
201impl PatchTriple {
202 pub fn new(subject: PatchTerm, predicate: PatchTerm, object: PatchTerm) -> Self {
204 Self {
205 subject,
206 predicate,
207 object,
208 }
209 }
210}
211
212impl fmt::Display for PatchTriple {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 write!(f, "{} {} {} .", self.subject, self.predicate, self.object)
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct PatchQuad {
221 pub subject: PatchTerm,
223 pub predicate: PatchTerm,
225 pub object: PatchTerm,
227 pub graph: PatchTerm,
229}
230
231impl PatchQuad {
232 pub fn new(
234 subject: PatchTerm,
235 predicate: PatchTerm,
236 object: PatchTerm,
237 graph: PatchTerm,
238 ) -> Self {
239 Self {
240 subject,
241 predicate,
242 object,
243 graph,
244 }
245 }
246}
247
248impl fmt::Display for PatchQuad {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 write!(
251 f,
252 "{} {} {} {} .",
253 self.subject, self.predicate, self.object, self.graph
254 )
255 }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
260pub enum PatchChange {
261 AddPrefix {
263 prefix: String,
265 iri: String,
267 },
268 DeletePrefix {
270 prefix: String,
272 iri: String,
274 },
275 AddTriple(PatchTriple),
277 DeleteTriple(PatchTriple),
279 AddQuad(PatchQuad),
281 DeleteQuad(PatchQuad),
283 TransactionBegin,
285 TransactionCommit,
287 TransactionAbort,
289}
290
291impl PatchChange {
292 pub fn line_prefix(&self) -> &'static str {
294 match self {
295 PatchChange::AddPrefix { .. } => "PA",
296 PatchChange::DeletePrefix { .. } => "PD",
297 PatchChange::AddTriple(_) => "A",
298 PatchChange::DeleteTriple(_) => "D",
299 PatchChange::AddQuad(_) => "A",
300 PatchChange::DeleteQuad(_) => "D",
301 PatchChange::TransactionBegin => "TX",
302 PatchChange::TransactionCommit => "TC",
303 PatchChange::TransactionAbort => "TA",
304 }
305 }
306
307 pub fn is_add(&self) -> bool {
309 matches!(
310 self,
311 PatchChange::AddTriple(_) | PatchChange::AddQuad(_) | PatchChange::AddPrefix { .. }
312 )
313 }
314
315 pub fn is_delete(&self) -> bool {
317 matches!(
318 self,
319 PatchChange::DeleteTriple(_)
320 | PatchChange::DeleteQuad(_)
321 | PatchChange::DeletePrefix { .. }
322 )
323 }
324
325 pub fn is_transaction_control(&self) -> bool {
327 matches!(
328 self,
329 PatchChange::TransactionBegin
330 | PatchChange::TransactionCommit
331 | PatchChange::TransactionAbort
332 )
333 }
334}
335
336impl fmt::Display for PatchChange {
337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338 match self {
339 PatchChange::AddPrefix { prefix, iri } => {
340 write!(f, "PA {prefix} <{iri}>")
341 }
342 PatchChange::DeletePrefix { prefix, iri } => {
343 write!(f, "PD {prefix} <{iri}>")
344 }
345 PatchChange::AddTriple(t) => write!(f, "A {t}"),
346 PatchChange::DeleteTriple(t) => write!(f, "D {t}"),
347 PatchChange::AddQuad(q) => write!(f, "A {q}"),
348 PatchChange::DeleteQuad(q) => write!(f, "D {q}"),
349 PatchChange::TransactionBegin => write!(f, "TX"),
350 PatchChange::TransactionCommit => write!(f, "TC"),
351 PatchChange::TransactionAbort => write!(f, "TA"),
352 }
353 }
354}
355
356#[derive(Debug, Clone, Default)]
360pub struct RdfPatch {
361 pub headers: Vec<PatchHeader>,
363 pub changes: Vec<PatchChange>,
365}
366
367impl RdfPatch {
368 pub fn new() -> Self {
370 Self::default()
371 }
372
373 pub fn with_changes(headers: Vec<PatchHeader>, changes: Vec<PatchChange>) -> Self {
375 Self { headers, changes }
376 }
377
378 pub fn id(&self) -> Option<&str> {
380 self.headers.iter().find_map(|h| {
381 if let PatchHeader::Id(v) = h {
382 Some(v.as_str())
383 } else {
384 None
385 }
386 })
387 }
388
389 pub fn previous(&self) -> Option<&str> {
391 self.headers.iter().find_map(|h| {
392 if let PatchHeader::Previous(v) = h {
393 Some(v.as_str())
394 } else {
395 None
396 }
397 })
398 }
399
400 pub fn add_count(&self) -> usize {
402 self.changes
403 .iter()
404 .filter(|c| matches!(c, PatchChange::AddTriple(_) | PatchChange::AddQuad(_)))
405 .count()
406 }
407
408 pub fn delete_count(&self) -> usize {
410 self.changes
411 .iter()
412 .filter(|c| matches!(c, PatchChange::DeleteTriple(_) | PatchChange::DeleteQuad(_)))
413 .count()
414 }
415
416 pub fn is_empty(&self) -> bool {
418 self.headers.is_empty() && self.changes.is_empty()
419 }
420}
421
422#[derive(Debug, Clone, Default, PartialEq, Eq)]
426pub struct PatchStats {
427 pub triples_added: usize,
429 pub triples_deleted: usize,
431 pub prefixes_added: usize,
433 pub prefixes_deleted: usize,
435 pub transactions: usize,
437 pub aborts: usize,
439}
440
441#[derive(Debug, Clone, Default)]
448pub struct Graph {
449 pub triples: HashSet<String>,
451 pub prefixes: BTreeMap<String, String>,
453 triple_objects: Vec<PatchTriple>,
455}
456
457impl Graph {
458 pub fn new() -> Self {
460 Self::default()
461 }
462
463 pub fn add_triple(&mut self, triple: PatchTriple) -> bool {
465 let key = Self::triple_key(&triple);
466 if self.triples.insert(key) {
467 self.triple_objects.push(triple);
468 true
469 } else {
470 false
471 }
472 }
473
474 pub fn remove_triple(&mut self, triple: &PatchTriple) -> bool {
476 let key = Self::triple_key(triple);
477 if self.triples.remove(&key) {
478 self.triple_objects.retain(|t| Self::triple_key(t) != key);
479 true
480 } else {
481 false
482 }
483 }
484
485 pub fn contains(&self, triple: &PatchTriple) -> bool {
487 self.triples.contains(&Self::triple_key(triple))
488 }
489
490 pub fn len(&self) -> usize {
492 self.triples.len()
493 }
494
495 pub fn is_empty(&self) -> bool {
497 self.triples.is_empty()
498 }
499
500 pub fn iter(&self) -> impl Iterator<Item = &PatchTriple> {
502 self.triple_objects.iter()
503 }
504
505 fn triple_key(t: &PatchTriple) -> String {
506 format!("{}\x00{}\x00{}", t.subject, t.predicate, t.object)
507 }
508}
509
510pub struct PatchParser;
514
515impl PatchParser {
516 pub fn parse(input: &str) -> PatchResult<RdfPatch> {
518 let mut headers = Vec::new();
519 let mut changes = Vec::new();
520 let mut prefixes: BTreeMap<String, String> = BTreeMap::new();
521
522 for (idx, raw_line) in input.lines().enumerate() {
523 let line_no = idx + 1;
524 let line = raw_line.trim();
525
526 if line.is_empty() || line.starts_with('#') {
528 continue;
529 }
530
531 if let Some(rest) = line.strip_prefix("H ") {
532 let header = Self::parse_header(rest.trim(), line_no)?;
533 headers.push(header);
534 } else if line == "TX" {
535 changes.push(PatchChange::TransactionBegin);
536 } else if line == "TC" {
537 changes.push(PatchChange::TransactionCommit);
538 } else if line == "TA" {
539 changes.push(PatchChange::TransactionAbort);
540 } else if let Some(rest) = line.strip_prefix("PA ") {
541 let (prefix, iri) = Self::parse_prefix_decl(rest.trim(), line_no)?;
542 prefixes.insert(prefix.clone(), iri.clone());
543 changes.push(PatchChange::AddPrefix { prefix, iri });
544 } else if let Some(rest) = line.strip_prefix("PD ") {
545 let (prefix, iri) = Self::parse_prefix_decl(rest.trim(), line_no)?;
546 changes.push(PatchChange::DeletePrefix { prefix, iri });
547 } else if let Some(rest) = line.strip_prefix("A ") {
548 let change = Self::parse_triple_or_quad("A", rest.trim(), &prefixes, line_no)?;
549 changes.push(change);
550 } else if let Some(rest) = line.strip_prefix("D ") {
551 let change = Self::parse_triple_or_quad("D", rest.trim(), &prefixes, line_no)?;
552 changes.push(change);
553 } else {
554 return Err(PatchError::at(
555 line_no,
556 format!("unrecognised line: {line:?}"),
557 ));
558 }
559 }
560
561 Ok(RdfPatch { headers, changes })
562 }
563
564 pub fn parse_streaming(reader: impl Read) -> impl Iterator<Item = PatchResult<PatchChange>> {
567 StreamingPatchParser::new(reader)
568 }
569
570 fn parse_header(rest: &str, line_no: usize) -> PatchResult<PatchHeader> {
573 let mut parts = rest.splitn(2, ' ');
575 let key = parts
576 .next()
577 .ok_or_else(|| PatchError::at(line_no, "missing header key"))?
578 .trim();
579 let value_raw = parts.next().unwrap_or("").trim();
580 let value = strip_angle_brackets(value_raw);
581 match key {
582 "version" => Ok(PatchHeader::Version(value.to_string())),
583 "prev" => Ok(PatchHeader::Previous(value.to_string())),
584 "id" => Ok(PatchHeader::Id(value.to_string())),
585 other => Ok(PatchHeader::Unknown {
586 key: other.to_string(),
587 value: value.to_string(),
588 }),
589 }
590 }
591
592 fn parse_prefix_decl(rest: &str, line_no: usize) -> PatchResult<(String, String)> {
593 let mut parts = rest.splitn(2, ' ');
595 let prefix_raw = parts
596 .next()
597 .ok_or_else(|| PatchError::at(line_no, "missing prefix name"))?
598 .trim_end_matches(':');
599 let iri_raw = parts
600 .next()
601 .ok_or_else(|| PatchError::at(line_no, "missing prefix IRI"))?
602 .trim();
603 let iri = strip_angle_brackets(iri_raw);
604 Ok((prefix_raw.to_string(), iri.to_string()))
605 }
606
607 fn parse_triple_or_quad(
608 op: &str,
609 rest: &str,
610 prefixes: &BTreeMap<String, String>,
611 line_no: usize,
612 ) -> PatchResult<PatchChange> {
613 let rest = rest.trim_end_matches('.').trim();
615 let terms = tokenise_terms(rest, prefixes, line_no)?;
616 match terms.len() {
617 3 => {
618 let triple = PatchTriple::new(terms[0].clone(), terms[1].clone(), terms[2].clone());
619 if op == "A" {
620 Ok(PatchChange::AddTriple(triple))
621 } else {
622 Ok(PatchChange::DeleteTriple(triple))
623 }
624 }
625 4 => {
626 let quad = PatchQuad::new(
627 terms[0].clone(),
628 terms[1].clone(),
629 terms[2].clone(),
630 terms[3].clone(),
631 );
632 if op == "A" {
633 Ok(PatchChange::AddQuad(quad))
634 } else {
635 Ok(PatchChange::DeleteQuad(quad))
636 }
637 }
638 n => Err(PatchError::at(
639 line_no,
640 format!("expected 3 or 4 terms, got {n}"),
641 )),
642 }
643 }
644}
645
646struct StreamingPatchParser<R: Read> {
649 reader: BufReader<R>,
650 line_no: usize,
651 prefixes: BTreeMap<String, String>,
652 done: bool,
653}
654
655impl<R: Read> StreamingPatchParser<R> {
656 fn new(reader: R) -> Self {
657 Self {
658 reader: BufReader::new(reader),
659 line_no: 0,
660 prefixes: BTreeMap::new(),
661 done: false,
662 }
663 }
664}
665
666impl<R: Read> Iterator for StreamingPatchParser<R> {
667 type Item = PatchResult<PatchChange>;
668
669 fn next(&mut self) -> Option<Self::Item> {
670 if self.done {
671 return None;
672 }
673 loop {
674 let mut raw = String::new();
675 match self.reader.read_line(&mut raw) {
676 Ok(0) => {
677 self.done = true;
678 return None;
679 }
680 Err(e) => {
681 self.done = true;
682 return Some(Err(PatchError::at(self.line_no, e.to_string())));
683 }
684 Ok(_) => {}
685 }
686 self.line_no += 1;
687 let line = raw.trim();
688
689 if line.is_empty() || line.starts_with('#') {
690 continue;
691 }
692
693 if line.starts_with("H ") {
695 continue;
696 }
697
698 let result = if line == "TX" {
699 Ok(PatchChange::TransactionBegin)
700 } else if line == "TC" {
701 Ok(PatchChange::TransactionCommit)
702 } else if line == "TA" {
703 Ok(PatchChange::TransactionAbort)
704 } else if let Some(rest) = line.strip_prefix("PA ") {
705 match parse_prefix_decl_inline(rest.trim(), self.line_no) {
706 Ok((prefix, iri)) => {
707 self.prefixes.insert(prefix.clone(), iri.clone());
708 Ok(PatchChange::AddPrefix { prefix, iri })
709 }
710 Err(e) => Err(e),
711 }
712 } else if let Some(rest) = line.strip_prefix("PD ") {
713 match parse_prefix_decl_inline(rest.trim(), self.line_no) {
714 Ok((prefix, iri)) => Ok(PatchChange::DeletePrefix { prefix, iri }),
715 Err(e) => Err(e),
716 }
717 } else if let Some(rest) = line.strip_prefix("A ") {
718 PatchParser::parse_triple_or_quad("A", rest.trim(), &self.prefixes, self.line_no)
719 } else if let Some(rest) = line.strip_prefix("D ") {
720 PatchParser::parse_triple_or_quad("D", rest.trim(), &self.prefixes, self.line_no)
721 } else {
722 Err(PatchError::at(
723 self.line_no,
724 format!("unrecognised line: {line:?}"),
725 ))
726 };
727
728 return Some(result);
729 }
730 }
731}
732
733pub struct PatchSerializer;
737
738impl PatchSerializer {
739 pub fn serialize(patch: &RdfPatch) -> String {
741 let mut out = String::new();
742 for header in &patch.headers {
743 out.push_str(&header.to_string());
744 out.push('\n');
745 }
746 if !patch.headers.is_empty() && !patch.changes.is_empty() {
747 out.push('\n');
748 }
749 for change in &patch.changes {
750 out.push_str(&change.to_string());
751 out.push('\n');
752 }
753 out
754 }
755
756 pub fn serialize_change(change: &PatchChange) -> String {
758 change.to_string()
759 }
760}
761
762pub fn apply_patch(graph: &mut Graph, patch: &RdfPatch) -> PatchResult<PatchStats> {
769 let mut stats = PatchStats::default();
770 let mut in_tx = false;
771 let mut tx_adds: Vec<PatchTriple> = Vec::new();
773 let mut tx_deletes: Vec<PatchTriple> = Vec::new();
774 let mut tx_prefix_adds: Vec<(String, String)> = Vec::new();
775
776 for change in &patch.changes {
777 match change {
778 PatchChange::TransactionBegin => {
779 in_tx = true;
780 tx_adds.clear();
781 tx_deletes.clear();
782 tx_prefix_adds.clear();
783 stats.transactions += 1;
784 }
785 PatchChange::TransactionCommit => {
786 for t in tx_adds.drain(..) {
788 if graph.add_triple(t) {
789 stats.triples_added += 1;
790 }
791 }
792 for t in &tx_deletes {
793 if graph.remove_triple(t) {
794 stats.triples_deleted += 1;
795 }
796 }
797 tx_deletes.clear();
798 for (p, i) in tx_prefix_adds.drain(..) {
799 graph.prefixes.insert(p, i);
800 stats.prefixes_added += 1;
801 }
802 in_tx = false;
803 }
804 PatchChange::TransactionAbort => {
805 tx_adds.clear();
807 tx_deletes.clear();
808 tx_prefix_adds.clear();
809 in_tx = false;
810 stats.aborts += 1;
811 }
812 PatchChange::AddPrefix { prefix, iri } => {
813 if in_tx {
814 tx_prefix_adds.push((prefix.clone(), iri.clone()));
815 } else {
816 graph.prefixes.insert(prefix.clone(), iri.clone());
817 stats.prefixes_added += 1;
818 }
819 }
820 PatchChange::DeletePrefix { prefix, .. } => {
821 graph.prefixes.remove(prefix.as_str());
822 stats.prefixes_deleted += 1;
823 }
824 PatchChange::AddTriple(t) => {
825 if in_tx {
826 tx_adds.push(t.clone());
827 } else if graph.add_triple(t.clone()) {
828 stats.triples_added += 1;
829 }
830 }
831 PatchChange::DeleteTriple(t) => {
832 if in_tx {
833 tx_deletes.push(t.clone());
834 } else if graph.remove_triple(t) {
835 stats.triples_deleted += 1;
836 }
837 }
838 PatchChange::AddQuad(q) => {
840 let t = PatchTriple::new(q.subject.clone(), q.predicate.clone(), q.object.clone());
841 if in_tx {
842 tx_adds.push(t);
843 } else if graph.add_triple(t) {
844 stats.triples_added += 1;
845 }
846 }
847 PatchChange::DeleteQuad(q) => {
848 let t = PatchTriple::new(q.subject.clone(), q.predicate.clone(), q.object.clone());
849 if in_tx {
850 tx_deletes.push(t.clone());
851 } else if graph.remove_triple(&t) {
852 stats.triples_deleted += 1;
853 }
854 }
855 }
856 }
857
858 Ok(stats)
859}
860
861pub fn diff_to_patch(old: &Graph, new: &Graph) -> RdfPatch {
868 let mut changes = Vec::new();
869
870 for triple in old.iter() {
872 if !new.contains(triple) {
873 changes.push(PatchChange::DeleteTriple(triple.clone()));
874 }
875 }
876
877 for triple in new.iter() {
879 if !old.contains(triple) {
880 changes.push(PatchChange::AddTriple(triple.clone()));
881 }
882 }
883
884 for (prefix, iri) in &new.prefixes {
886 if old.prefixes.get(prefix) != Some(iri) {
887 changes.push(PatchChange::AddPrefix {
888 prefix: prefix.clone(),
889 iri: iri.clone(),
890 });
891 }
892 }
893
894 for (prefix, iri) in &old.prefixes {
896 if !new.prefixes.contains_key(prefix.as_str()) {
897 changes.push(PatchChange::DeletePrefix {
898 prefix: prefix.clone(),
899 iri: iri.clone(),
900 });
901 }
902 }
903
904 RdfPatch {
905 headers: Vec::new(),
906 changes,
907 }
908}
909
910fn tokenise_terms(
916 input: &str,
917 prefixes: &BTreeMap<String, String>,
918 line_no: usize,
919) -> PatchResult<Vec<PatchTerm>> {
920 let mut terms = Vec::new();
921 let chars: Vec<char> = input.chars().collect();
922 let mut pos = 0;
923
924 while pos < chars.len() {
925 while pos < chars.len() && chars[pos].is_whitespace() {
927 pos += 1;
928 }
929 if pos >= chars.len() {
930 break;
931 }
932
933 if chars[pos] == '<' {
934 pos += 1;
936 let start = pos;
937 while pos < chars.len() && chars[pos] != '>' {
938 pos += 1;
939 }
940 if pos >= chars.len() {
941 return Err(PatchError::at(line_no, "unterminated IRI"));
942 }
943 let iri: String = chars[start..pos].iter().collect();
944 pos += 1; terms.push(PatchTerm::iri(iri));
946 } else if chars[pos] == '"' {
947 pos += 1;
949 let mut value = String::new();
950 while pos < chars.len() {
951 if chars[pos] == '\\' && pos + 1 < chars.len() {
952 pos += 1;
953 match chars[pos] {
954 '"' => value.push('"'),
955 '\\' => value.push('\\'),
956 'n' => value.push('\n'),
957 'r' => value.push('\r'),
958 't' => value.push('\t'),
959 c => {
960 value.push('\\');
961 value.push(c);
962 }
963 }
964 pos += 1;
965 } else if chars[pos] == '"' {
966 break;
967 } else {
968 value.push(chars[pos]);
969 pos += 1;
970 }
971 }
972 if pos >= chars.len() {
973 return Err(PatchError::at(line_no, "unterminated literal"));
974 }
975 pos += 1; if pos < chars.len() && chars[pos] == '@' {
979 pos += 1;
980 let start = pos;
981 while pos < chars.len() && !chars[pos].is_whitespace() {
982 pos += 1;
983 }
984 let lang: String = chars[start..pos].iter().collect();
985 terms.push(PatchTerm::lang_literal(value, lang));
986 } else if pos + 1 < chars.len() && chars[pos] == '^' && chars[pos + 1] == '^' {
987 pos += 2;
988 if pos >= chars.len() || chars[pos] != '<' {
989 return Err(PatchError::at(line_no, "expected '<' after '^^'"));
990 }
991 pos += 1;
992 let start = pos;
993 while pos < chars.len() && chars[pos] != '>' {
994 pos += 1;
995 }
996 if pos >= chars.len() {
997 return Err(PatchError::at(line_no, "unterminated datatype IRI"));
998 }
999 let dt: String = chars[start..pos].iter().collect();
1000 pos += 1;
1001 terms.push(PatchTerm::typed_literal(value, dt));
1002 } else {
1003 terms.push(PatchTerm::literal(value));
1004 }
1005 } else if pos + 1 < chars.len() && chars[pos] == '_' && chars[pos + 1] == ':' {
1006 pos += 2;
1008 let start = pos;
1009 while pos < chars.len() && !chars[pos].is_whitespace() && chars[pos] != '.' {
1010 pos += 1;
1011 }
1012 let id: String = chars[start..pos].iter().collect();
1013 terms.push(PatchTerm::blank_node(id));
1014 } else if chars[pos] == '.' {
1015 pos += 1;
1017 } else {
1018 let start = pos;
1020 while pos < chars.len() && !chars[pos].is_whitespace() && chars[pos] != '.' {
1021 pos += 1;
1022 }
1023 let token: String = chars[start..pos].iter().collect();
1024 if let Some(colon_pos) = token.find(':') {
1025 let ns = &token[..colon_pos];
1026 let local = &token[colon_pos + 1..];
1027 match prefixes.get(ns) {
1028 Some(base) => {
1029 let full = format!("{base}{local}");
1030 terms.push(PatchTerm::iri(full));
1031 }
1032 None => {
1033 return Err(PatchError::at(
1034 line_no,
1035 format!("unknown prefix '{ns}' in '{token}'"),
1036 ))
1037 }
1038 }
1039 } else if token.is_empty() || token == "." {
1040 } else {
1042 return Err(PatchError::at(
1043 line_no,
1044 format!("unexpected token '{token}'"),
1045 ));
1046 }
1047 }
1048 }
1049
1050 Ok(terms)
1051}
1052
1053fn strip_angle_brackets(s: &str) -> &str {
1055 if s.starts_with('<') && s.ends_with('>') {
1056 &s[1..s.len() - 1]
1057 } else {
1058 s
1059 }
1060}
1061
1062fn parse_prefix_decl_inline(rest: &str, line_no: usize) -> PatchResult<(String, String)> {
1064 let mut parts = rest.splitn(2, ' ');
1065 let prefix_raw = parts
1066 .next()
1067 .ok_or_else(|| PatchError::at(line_no, "missing prefix name"))?
1068 .trim_end_matches(':');
1069 let iri_raw = parts
1070 .next()
1071 .ok_or_else(|| PatchError::at(line_no, "missing prefix IRI"))?
1072 .trim();
1073 let iri = strip_angle_brackets(iri_raw);
1074 Ok((prefix_raw.to_string(), iri.to_string()))
1075}
1076
1077#[cfg(test)]
1080mod tests {
1081 use super::*;
1082
1083 fn triple(s: &str, p: &str, o: &str) -> PatchTriple {
1085 PatchTriple::new(PatchTerm::iri(s), PatchTerm::iri(p), PatchTerm::iri(o))
1086 }
1087
1088 fn triple_lit(s: &str, p: &str, o: &str) -> PatchTriple {
1089 PatchTriple::new(PatchTerm::iri(s), PatchTerm::iri(p), PatchTerm::literal(o))
1090 }
1091
1092 #[test]
1095 fn test_parse_header_id() {
1096 let patch = PatchParser::parse("H id <urn:uuid:1234>\n").expect("should succeed");
1097 assert_eq!(patch.headers.len(), 1);
1098 assert_eq!(patch.id(), Some("urn:uuid:1234"));
1099 }
1100
1101 #[test]
1102 fn test_parse_header_prev() {
1103 let patch = PatchParser::parse("H prev <urn:uuid:abcd>\n").expect("should succeed");
1104 assert_eq!(patch.previous(), Some("urn:uuid:abcd"));
1105 }
1106
1107 #[test]
1108 fn test_parse_header_version() {
1109 let patch = PatchParser::parse("H version 1\n").expect("should succeed");
1110 matches!(&patch.headers[0], PatchHeader::Version(v) if v == "1");
1111 }
1112
1113 #[test]
1114 fn test_parse_header_unknown() {
1115 let patch = PatchParser::parse("H custom myval\n").expect("should succeed");
1116 assert!(matches!(&patch.headers[0], PatchHeader::Unknown { key, .. } if key == "custom"));
1117 }
1118
1119 #[test]
1120 fn test_parse_multiple_headers() {
1121 let input = "H id <urn:1>\nH prev <urn:0>\nH version 2\n";
1122 let patch = PatchParser::parse(input).expect("should succeed");
1123 assert_eq!(patch.headers.len(), 3);
1124 }
1125
1126 #[test]
1129 fn test_parse_tx_tc() {
1130 let patch = PatchParser::parse("TX\nTC\n").expect("should succeed");
1131 assert_eq!(patch.changes.len(), 2);
1132 assert!(matches!(patch.changes[0], PatchChange::TransactionBegin));
1133 assert!(matches!(patch.changes[1], PatchChange::TransactionCommit));
1134 }
1135
1136 #[test]
1137 fn test_parse_ta() {
1138 let patch = PatchParser::parse("TX\nTA\n").expect("should succeed");
1139 assert!(matches!(patch.changes[1], PatchChange::TransactionAbort));
1140 }
1141
1142 #[test]
1143 fn test_transaction_control_predicates() {
1144 assert!(PatchChange::TransactionBegin.is_transaction_control());
1145 assert!(PatchChange::TransactionCommit.is_transaction_control());
1146 assert!(PatchChange::TransactionAbort.is_transaction_control());
1147 }
1148
1149 #[test]
1152 fn test_parse_prefix_add() {
1153 let patch = PatchParser::parse("PA ex <http://example.org/>\n").expect("should succeed");
1154 assert_eq!(patch.changes.len(), 1);
1155 match &patch.changes[0] {
1156 PatchChange::AddPrefix { prefix, iri } => {
1157 assert_eq!(prefix, "ex");
1158 assert_eq!(iri, "http://example.org/");
1159 }
1160 _ => panic!("unexpected change type"),
1161 }
1162 }
1163
1164 #[test]
1165 fn test_parse_prefix_delete() {
1166 let patch = PatchParser::parse("PD ex <http://example.org/>\n").expect("should succeed");
1167 assert!(
1168 matches!(&patch.changes[0], PatchChange::DeletePrefix { prefix, .. } if prefix == "ex")
1169 );
1170 }
1171
1172 #[test]
1173 fn test_prefix_resolution_in_triple() {
1174 let input = "PA ex <http://example.org/>\nA ex:s ex:p ex:o .\n";
1175 let patch = PatchParser::parse(input).expect("should succeed");
1176 assert_eq!(patch.changes.len(), 2);
1177 if let PatchChange::AddTriple(t) = &patch.changes[1] {
1178 assert_eq!(t.subject.value(), "http://example.org/s");
1179 } else {
1180 panic!("expected AddTriple");
1181 }
1182 }
1183
1184 #[test]
1187 fn test_parse_add_triple() {
1188 let input = "A <http://s> <http://p> <http://o> .\n";
1189 let patch = PatchParser::parse(input).expect("should succeed");
1190 assert!(matches!(&patch.changes[0], PatchChange::AddTriple(_)));
1191 }
1192
1193 #[test]
1194 fn test_parse_delete_triple() {
1195 let input = "D <http://s> <http://p> <http://o> .\n";
1196 let patch = PatchParser::parse(input).expect("should succeed");
1197 assert!(matches!(&patch.changes[0], PatchChange::DeleteTriple(_)));
1198 }
1199
1200 #[test]
1201 fn test_parse_triple_with_literal() {
1202 let input = "A <http://s> <http://p> \"hello\" .\n";
1203 let patch = PatchParser::parse(input).expect("should succeed");
1204 if let PatchChange::AddTriple(t) = &patch.changes[0] {
1205 assert!(
1206 t.object.0.term_type
1207 == TermType::Literal {
1208 datatype: None,
1209 lang: None
1210 }
1211 );
1212 assert_eq!(t.object.value(), "hello");
1213 } else {
1214 panic!("expected AddTriple");
1215 }
1216 }
1217
1218 #[test]
1219 fn test_parse_literal_with_language() {
1220 let input = "A <http://s> <http://p> \"hello\"@en .\n";
1221 let patch = PatchParser::parse(input).expect("should succeed");
1222 if let PatchChange::AddTriple(t) = &patch.changes[0] {
1223 assert!(matches!(
1224 &t.object.0.term_type,
1225 TermType::Literal { lang: Some(l), .. } if l == "en"
1226 ));
1227 } else {
1228 panic!("expected AddTriple");
1229 }
1230 }
1231
1232 #[test]
1233 fn test_parse_literal_with_datatype() {
1234 let input =
1235 "A <http://s> <http://p> \"42\"^^<http://www.w3.org/2001/XMLSchema#integer> .\n";
1236 let patch = PatchParser::parse(input).expect("should succeed");
1237 if let PatchChange::AddTriple(t) = &patch.changes[0] {
1238 assert!(matches!(
1239 &t.object.0.term_type,
1240 TermType::Literal { datatype: Some(dt), .. }
1241 if dt == "http://www.w3.org/2001/XMLSchema#integer"
1242 ));
1243 } else {
1244 panic!("expected AddTriple");
1245 }
1246 }
1247
1248 #[test]
1249 fn test_parse_triple_blank_node() {
1250 let input = "A _:b0 <http://p> <http://o> .\n";
1251 let patch = PatchParser::parse(input).expect("should succeed");
1252 if let PatchChange::AddTriple(t) = &patch.changes[0] {
1253 assert!(t.subject.is_blank_node());
1254 assert_eq!(t.subject.value(), "b0");
1255 } else {
1256 panic!("expected AddTriple");
1257 }
1258 }
1259
1260 #[test]
1263 fn test_parse_add_quad() {
1264 let input = "A <http://s> <http://p> <http://o> <http://g> .\n";
1265 let patch = PatchParser::parse(input).expect("should succeed");
1266 assert!(matches!(&patch.changes[0], PatchChange::AddQuad(_)));
1267 }
1268
1269 #[test]
1270 fn test_parse_delete_quad() {
1271 let input = "D <http://s> <http://p> <http://o> <http://g> .\n";
1272 let patch = PatchParser::parse(input).expect("should succeed");
1273 assert!(matches!(&patch.changes[0], PatchChange::DeleteQuad(_)));
1274 }
1275
1276 #[test]
1277 fn test_quad_graph_term() {
1278 let input = "A <http://s> <http://p> <http://o> <http://graph1> .\n";
1279 let patch = PatchParser::parse(input).expect("should succeed");
1280 if let PatchChange::AddQuad(q) = &patch.changes[0] {
1281 assert_eq!(q.graph.value(), "http://graph1");
1282 } else {
1283 panic!("expected AddQuad");
1284 }
1285 }
1286
1287 #[test]
1290 fn test_serialize_header() {
1291 let patch = RdfPatch {
1292 headers: vec![PatchHeader::Id("urn:1".to_string())],
1293 changes: vec![],
1294 };
1295 let s = PatchSerializer::serialize(&patch);
1296 assert!(s.contains("H id urn:1"));
1297 }
1298
1299 #[test]
1300 fn test_serialize_add_triple() {
1301 let patch = RdfPatch {
1302 headers: vec![],
1303 changes: vec![PatchChange::AddTriple(triple(
1304 "http://s", "http://p", "http://o",
1305 ))],
1306 };
1307 let s = PatchSerializer::serialize(&patch);
1308 assert!(s.contains("A <http://s> <http://p> <http://o>"));
1309 }
1310
1311 #[test]
1312 fn test_serialize_delete_triple() {
1313 let patch = RdfPatch {
1314 headers: vec![],
1315 changes: vec![PatchChange::DeleteTriple(triple(
1316 "http://s", "http://p", "http://o",
1317 ))],
1318 };
1319 let s = PatchSerializer::serialize(&patch);
1320 assert!(s.starts_with("D "));
1321 }
1322
1323 #[test]
1324 fn test_serialize_prefix_add() {
1325 let change = PatchChange::AddPrefix {
1326 prefix: "ex".to_string(),
1327 iri: "http://example.org/".to_string(),
1328 };
1329 let s = PatchSerializer::serialize_change(&change);
1330 assert_eq!(s, "PA ex <http://example.org/>");
1331 }
1332
1333 #[test]
1334 fn test_serialize_transaction_control() {
1335 let patch = RdfPatch {
1336 headers: vec![],
1337 changes: vec![
1338 PatchChange::TransactionBegin,
1339 PatchChange::TransactionCommit,
1340 ],
1341 };
1342 let s = PatchSerializer::serialize(&patch);
1343 assert!(s.contains("TX"));
1344 assert!(s.contains("TC"));
1345 }
1346
1347 #[test]
1348 fn test_serialize_literal() {
1349 let patch = RdfPatch {
1350 headers: vec![],
1351 changes: vec![PatchChange::AddTriple(triple_lit(
1352 "http://s", "http://p", "hello",
1353 ))],
1354 };
1355 let s = PatchSerializer::serialize(&patch);
1356 assert!(s.contains("\"hello\""));
1357 }
1358
1359 #[test]
1362 fn test_apply_add_triple() {
1363 let mut graph = Graph::new();
1364 let patch = RdfPatch {
1365 headers: vec![],
1366 changes: vec![PatchChange::AddTriple(triple(
1367 "http://s", "http://p", "http://o",
1368 ))],
1369 };
1370 let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1371 assert_eq!(stats.triples_added, 1);
1372 assert_eq!(graph.len(), 1);
1373 }
1374
1375 #[test]
1376 fn test_apply_delete_triple() {
1377 let mut graph = Graph::new();
1378 let t = triple("http://s", "http://p", "http://o");
1379 graph.add_triple(t.clone());
1380 let patch = RdfPatch {
1381 headers: vec![],
1382 changes: vec![PatchChange::DeleteTriple(t)],
1383 };
1384 let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1385 assert_eq!(stats.triples_deleted, 1);
1386 assert_eq!(graph.len(), 0);
1387 }
1388
1389 #[test]
1390 fn test_apply_idempotent_add() {
1391 let mut graph = Graph::new();
1392 let t = triple("http://s", "http://p", "http://o");
1393 graph.add_triple(t.clone());
1394 let patch = RdfPatch {
1395 headers: vec![],
1396 changes: vec![PatchChange::AddTriple(t)],
1397 };
1398 let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1399 assert_eq!(stats.triples_added, 0);
1401 assert_eq!(graph.len(), 1);
1402 }
1403
1404 #[test]
1405 fn test_apply_prefix_add() {
1406 let mut graph = Graph::new();
1407 let patch = RdfPatch {
1408 headers: vec![],
1409 changes: vec![PatchChange::AddPrefix {
1410 prefix: "ex".to_string(),
1411 iri: "http://example.org/".to_string(),
1412 }],
1413 };
1414 let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1415 assert_eq!(stats.prefixes_added, 1);
1416 assert_eq!(
1417 graph.prefixes.get("ex").map(String::as_str),
1418 Some("http://example.org/")
1419 );
1420 }
1421
1422 #[test]
1423 fn test_apply_transaction_commit() {
1424 let mut graph = Graph::new();
1425 let patch = RdfPatch {
1426 headers: vec![],
1427 changes: vec![
1428 PatchChange::TransactionBegin,
1429 PatchChange::AddTriple(triple("http://s", "http://p", "http://o")),
1430 PatchChange::TransactionCommit,
1431 ],
1432 };
1433 let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1434 assert_eq!(stats.triples_added, 1);
1435 assert_eq!(stats.transactions, 1);
1436 assert_eq!(graph.len(), 1);
1437 }
1438
1439 #[test]
1440 fn test_apply_transaction_abort() {
1441 let mut graph = Graph::new();
1442 let patch = RdfPatch {
1443 headers: vec![],
1444 changes: vec![
1445 PatchChange::TransactionBegin,
1446 PatchChange::AddTriple(triple("http://s", "http://p", "http://o")),
1447 PatchChange::TransactionAbort,
1448 ],
1449 };
1450 let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1451 assert_eq!(stats.aborts, 1);
1452 assert_eq!(graph.len(), 0);
1454 }
1455
1456 #[test]
1457 fn test_apply_multiple_changes() {
1458 let mut graph = Graph::new();
1459 let t1 = triple("http://a", "http://p", "http://x");
1460 let t2 = triple("http://b", "http://p", "http://y");
1461 let patch = RdfPatch {
1462 headers: vec![],
1463 changes: vec![
1464 PatchChange::AddTriple(t1.clone()),
1465 PatchChange::AddTriple(t2.clone()),
1466 PatchChange::DeleteTriple(t1),
1467 ],
1468 };
1469 let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1470 assert_eq!(stats.triples_added, 2);
1471 assert_eq!(stats.triples_deleted, 1);
1472 assert_eq!(graph.len(), 1);
1473 }
1474
1475 #[test]
1478 fn test_diff_to_patch_add() {
1479 let old = Graph::new();
1480 let mut new_graph = Graph::new();
1481 new_graph.add_triple(triple("http://s", "http://p", "http://o"));
1482 let patch = diff_to_patch(&old, &new_graph);
1483 assert_eq!(patch.add_count(), 1);
1484 assert_eq!(patch.delete_count(), 0);
1485 }
1486
1487 #[test]
1488 fn test_diff_to_patch_delete() {
1489 let mut old = Graph::new();
1490 old.add_triple(triple("http://s", "http://p", "http://o"));
1491 let new_graph = Graph::new();
1492 let patch = diff_to_patch(&old, &new_graph);
1493 assert_eq!(patch.add_count(), 0);
1494 assert_eq!(patch.delete_count(), 1);
1495 }
1496
1497 #[test]
1498 fn test_diff_to_patch_no_change() {
1499 let mut old = Graph::new();
1500 old.add_triple(triple("http://s", "http://p", "http://o"));
1501 let new_graph = old.clone();
1502 let patch = diff_to_patch(&old, &new_graph);
1503 assert!(patch.changes.is_empty());
1504 }
1505
1506 #[test]
1507 fn test_diff_to_patch_prefix_added() {
1508 let old = Graph::new();
1509 let mut new_graph = Graph::new();
1510 new_graph
1511 .prefixes
1512 .insert("ex".to_string(), "http://example.org/".to_string());
1513 let patch = diff_to_patch(&old, &new_graph);
1514 assert!(patch
1515 .changes
1516 .iter()
1517 .any(|c| matches!(c, PatchChange::AddPrefix { .. })));
1518 }
1519
1520 #[test]
1521 fn test_diff_to_patch_prefix_removed() {
1522 let mut old = Graph::new();
1523 old.prefixes
1524 .insert("ex".to_string(), "http://example.org/".to_string());
1525 let new_graph = Graph::new();
1526 let patch = diff_to_patch(&old, &new_graph);
1527 assert!(patch
1528 .changes
1529 .iter()
1530 .any(|c| matches!(c, PatchChange::DeletePrefix { .. })));
1531 }
1532
1533 #[test]
1536 fn test_round_trip_simple() {
1537 let input = "H id <urn:1>\nA <http://s> <http://p> <http://o> .\nD <http://s> <http://p> <http://old> .\n";
1538 let patch = PatchParser::parse(input).expect("should succeed");
1539 let serialized = PatchSerializer::serialize(&patch);
1540 let reparsed = PatchParser::parse(&serialized).expect("should succeed");
1541 assert_eq!(reparsed.headers.len(), patch.headers.len());
1542 assert_eq!(reparsed.changes.len(), patch.changes.len());
1543 }
1544
1545 #[test]
1546 fn test_round_trip_with_prefixes() {
1547 let input = "PA ex <http://example.org/>\nA ex:s ex:p ex:o .\n";
1548 let patch = PatchParser::parse(input).expect("should succeed");
1549 let serialized = PatchSerializer::serialize(&patch);
1550 assert!(serialized.contains("<http://example.org/s>"));
1552 let reparsed = PatchParser::parse(&serialized).expect("should succeed");
1554 assert_eq!(reparsed.changes.len(), 2);
1555 }
1556
1557 #[test]
1558 fn test_round_trip_transaction() {
1559 let input = "TX\nA <http://s> <http://p> <http://o> .\nTC\n";
1560 let patch = PatchParser::parse(input).expect("should succeed");
1561 let serialized = PatchSerializer::serialize(&patch);
1562 let reparsed = PatchParser::parse(&serialized).expect("should succeed");
1563 assert_eq!(reparsed.changes.len(), 3);
1564 assert!(matches!(reparsed.changes[0], PatchChange::TransactionBegin));
1565 assert!(matches!(
1566 reparsed.changes[2],
1567 PatchChange::TransactionCommit
1568 ));
1569 }
1570
1571 #[test]
1572 fn test_round_trip_with_blank_nodes() {
1573 let input = "A _:b0 <http://p> <http://o> .\n";
1574 let patch = PatchParser::parse(input).expect("should succeed");
1575 let s = PatchSerializer::serialize(&patch);
1576 let reparsed = PatchParser::parse(&s).expect("should succeed");
1577 if let PatchChange::AddTriple(t) = &reparsed.changes[0] {
1578 assert!(t.subject.is_blank_node());
1579 } else {
1580 panic!("expected AddTriple");
1581 }
1582 }
1583
1584 #[test]
1585 fn test_round_trip_literal_with_lang() {
1586 let input = "A <http://s> <http://p> \"bonjour\"@fr .\n";
1587 let patch = PatchParser::parse(input).expect("should succeed");
1588 let s = PatchSerializer::serialize(&patch);
1589 let reparsed = PatchParser::parse(&s).expect("should succeed");
1590 if let PatchChange::AddTriple(t) = &reparsed.changes[0] {
1591 assert!(matches!(
1592 &t.object.0.term_type,
1593 TermType::Literal { lang: Some(l), .. } if l == "fr"
1594 ));
1595 } else {
1596 panic!("expected AddTriple");
1597 }
1598 }
1599
1600 #[test]
1601 fn test_round_trip_literal_with_datatype() {
1602 let dt = "http://www.w3.org/2001/XMLSchema#integer";
1603 let input = format!("A <http://s> <http://p> \"42\"^^<{dt}> .\n");
1604 let patch = PatchParser::parse(&input).expect("should succeed");
1605 let s = PatchSerializer::serialize(&patch);
1606 let reparsed = PatchParser::parse(&s).expect("should succeed");
1607 if let PatchChange::AddTriple(t) = &reparsed.changes[0] {
1608 assert!(matches!(
1609 &t.object.0.term_type,
1610 TermType::Literal { datatype: Some(d), .. } if d == dt
1611 ));
1612 } else {
1613 panic!("expected AddTriple");
1614 }
1615 }
1616
1617 #[test]
1620 fn test_streaming_parser_basic() {
1621 let input = "TX\nA <http://s> <http://p> <http://o> .\nTC\n";
1622 let changes: Vec<_> = PatchParser::parse_streaming(input.as_bytes()).collect();
1623 assert_eq!(changes.len(), 3);
1624 assert!(changes[0]
1625 .as_ref()
1626 .map(|c| matches!(c, PatchChange::TransactionBegin))
1627 .unwrap_or(false));
1628 }
1629
1630 #[test]
1631 fn test_streaming_skips_headers() {
1632 let input = "H id <urn:1>\nA <http://s> <http://p> <http://o> .\n";
1633 let changes: Vec<_> = PatchParser::parse_streaming(input.as_bytes()).collect();
1634 assert_eq!(changes.len(), 1);
1636 }
1637
1638 #[test]
1639 fn test_streaming_parser_prefixes() {
1640 let input = "PA ex <http://example.org/>\nA ex:s ex:p ex:o .\n";
1641 let changes: Vec<_> = PatchParser::parse_streaming(input.as_bytes())
1642 .collect::<Result<Vec<_>, _>>()
1643 .expect("should succeed");
1644 assert_eq!(changes.len(), 2);
1645 }
1646
1647 #[test]
1648 fn test_streaming_parser_multiple_batches() {
1649 let input = "A <http://s1> <http://p> <http://o1> .\nA <http://s2> <http://p> <http://o2> .\nD <http://s1> <http://p> <http://o1> .\n";
1650 let changes: Vec<_> = PatchParser::parse_streaming(input.as_bytes())
1651 .collect::<Result<Vec<_>, _>>()
1652 .expect("should succeed");
1653 assert_eq!(changes.len(), 3);
1654 }
1655
1656 #[test]
1659 fn test_empty_patch() {
1660 let patch = PatchParser::parse("").expect("should succeed");
1661 assert!(patch.is_empty());
1662 }
1663
1664 #[test]
1665 fn test_comments_ignored() {
1666 let input =
1667 "# This is a comment\nA <http://s> <http://p> <http://o> .\n# Another comment\n";
1668 let patch = PatchParser::parse(input).expect("should succeed");
1669 assert_eq!(patch.changes.len(), 1);
1670 }
1671
1672 #[test]
1673 fn test_blank_lines_ignored() {
1674 let input = "\n\nA <http://s> <http://p> <http://o> .\n\n";
1675 let patch = PatchParser::parse(input).expect("should succeed");
1676 assert_eq!(patch.changes.len(), 1);
1677 }
1678
1679 #[test]
1680 fn test_error_unknown_line() {
1681 let result = PatchParser::parse("UNKNOWN_CMD <http://x>\n");
1682 assert!(result.is_err());
1683 }
1684
1685 #[test]
1686 fn test_error_unterminated_iri() {
1687 let result = PatchParser::parse("A <http://s <http://p> <http://o> .\n");
1688 assert!(result.is_err());
1689 }
1690
1691 #[test]
1692 fn test_patch_change_predicates() {
1693 assert!(PatchChange::AddTriple(triple("h://s", "h://p", "h://o")).is_add());
1694 assert!(PatchChange::DeleteTriple(triple("h://s", "h://p", "h://o")).is_delete());
1695 assert!(!PatchChange::AddTriple(triple("h://s", "h://p", "h://o")).is_delete());
1696 assert!(!PatchChange::DeleteTriple(triple("h://s", "h://p", "h://o")).is_add());
1697 }
1698
1699 #[test]
1700 fn test_graph_contains() {
1701 let mut g = Graph::new();
1702 let t = triple("http://s", "http://p", "http://o");
1703 assert!(!g.contains(&t));
1704 g.add_triple(t.clone());
1705 assert!(g.contains(&t));
1706 g.remove_triple(&t);
1707 assert!(!g.contains(&t));
1708 }
1709
1710 #[test]
1711 fn test_graph_len() {
1712 let mut g = Graph::new();
1713 assert_eq!(g.len(), 0);
1714 assert!(g.is_empty());
1715 g.add_triple(triple("http://s", "http://p", "http://o"));
1716 assert_eq!(g.len(), 1);
1717 assert!(!g.is_empty());
1718 }
1719
1720 #[test]
1721 fn test_apply_patch_from_parsed_text() {
1722 let input =
1723 "PA ex <http://example.org/>\nTX\nA ex:alice <http://type> <http://Person> .\nTC\n";
1724 let patch = PatchParser::parse(input).expect("should succeed");
1725 let mut graph = Graph::new();
1726 let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1727 assert_eq!(stats.triples_added, 1);
1728 assert_eq!(stats.transactions, 1);
1729 }
1730
1731 #[test]
1732 fn test_patch_stats_default() {
1733 let stats = PatchStats::default();
1734 assert_eq!(stats.triples_added, 0);
1735 assert_eq!(stats.triples_deleted, 0);
1736 assert_eq!(stats.prefixes_added, 0);
1737 assert_eq!(stats.prefixes_deleted, 0);
1738 assert_eq!(stats.transactions, 0);
1739 assert_eq!(stats.aborts, 0);
1740 }
1741
1742 #[test]
1743 fn test_patch_header_key_value() {
1744 let h = PatchHeader::Id("urn:test".to_string());
1745 assert_eq!(h.key(), "id");
1746 assert_eq!(h.value(), "urn:test");
1747 }
1748
1749 #[test]
1750 fn test_diff_then_apply_round_trip() {
1751 let mut old = Graph::new();
1752 old.add_triple(triple("http://s", "http://p", "http://o1"));
1753 old.add_triple(triple("http://s", "http://p", "http://o2"));
1754
1755 let mut new_graph = Graph::new();
1756 new_graph.add_triple(triple("http://s", "http://p", "http://o2"));
1757 new_graph.add_triple(triple("http://s", "http://p", "http://o3"));
1758
1759 let patch = diff_to_patch(&old, &new_graph);
1760 let mut result = old.clone();
1762 apply_patch(&mut result, &patch).expect("should succeed");
1763
1764 assert_eq!(result.len(), new_graph.len());
1765 for t in new_graph.iter() {
1766 assert!(result.contains(t), "missing triple: {t}");
1767 }
1768 }
1769
1770 #[test]
1771 fn test_serialize_then_parse_complete_patch() {
1772 let mut patch = RdfPatch::new();
1773 patch
1774 .headers
1775 .push(PatchHeader::Id("urn:test:42".to_string()));
1776 patch
1777 .headers
1778 .push(PatchHeader::Previous("urn:test:41".to_string()));
1779 patch.changes.push(PatchChange::AddPrefix {
1780 prefix: "foaf".to_string(),
1781 iri: "http://xmlns.com/foaf/0.1/".to_string(),
1782 });
1783 patch.changes.push(PatchChange::TransactionBegin);
1784 patch.changes.push(PatchChange::AddTriple(triple(
1785 "http://example.org/alice",
1786 "http://xmlns.com/foaf/0.1/name",
1787 "http://example.org/literal_placeholder",
1788 )));
1789 patch.changes.push(PatchChange::TransactionCommit);
1790
1791 let serialized = PatchSerializer::serialize(&patch);
1792 let reparsed = PatchParser::parse(&serialized).expect("should succeed");
1793
1794 assert_eq!(reparsed.id(), Some("urn:test:42"));
1795 assert_eq!(reparsed.previous(), Some("urn:test:41"));
1796 assert_eq!(reparsed.changes.len(), 4);
1797 }
1798
1799 #[test]
1800 fn test_quad_apply_to_simple_graph() {
1801 let mut graph = Graph::new();
1802 let q = PatchQuad::new(
1803 PatchTerm::iri("http://s"),
1804 PatchTerm::iri("http://p"),
1805 PatchTerm::iri("http://o"),
1806 PatchTerm::iri("http://graph1"),
1807 );
1808 let patch = RdfPatch {
1809 headers: vec![],
1810 changes: vec![PatchChange::AddQuad(q)],
1811 };
1812 let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1813 assert_eq!(stats.triples_added, 1);
1814 }
1815
1816 #[test]
1817 fn test_escaped_literal() {
1818 let input = "A <http://s> <http://p> \"say \\\"hello\\\"\" .\n";
1819 let patch = PatchParser::parse(input).expect("should succeed");
1820 if let PatchChange::AddTriple(t) = &patch.changes[0] {
1821 assert_eq!(t.object.value(), "say \"hello\"");
1822 } else {
1823 panic!("expected AddTriple");
1824 }
1825 }
1826
1827 #[test]
1828 fn test_newline_in_literal_escape() {
1829 let input = "A <http://s> <http://p> \"line1\\nline2\" .\n";
1830 let patch = PatchParser::parse(input).expect("should succeed");
1831 if let PatchChange::AddTriple(t) = &patch.changes[0] {
1832 assert!(t.object.value().contains('\n'));
1833 } else {
1834 panic!("expected AddTriple");
1835 }
1836 }
1837
1838 #[test]
1839 fn test_patch_change_line_prefix() {
1840 assert_eq!(PatchChange::TransactionBegin.line_prefix(), "TX");
1841 assert_eq!(PatchChange::TransactionCommit.line_prefix(), "TC");
1842 assert_eq!(PatchChange::TransactionAbort.line_prefix(), "TA");
1843 assert_eq!(
1844 PatchChange::AddTriple(triple("http://s", "http://p", "http://o")).line_prefix(),
1845 "A"
1846 );
1847 assert_eq!(
1848 PatchChange::DeleteTriple(triple("http://s", "http://p", "http://o")).line_prefix(),
1849 "D"
1850 );
1851 }
1852}