1use crate::model::{
7 NamedNode, Object, ObjectTerm, Predicate, RdfTerm, Subject, SubjectTerm, Triple,
8};
9use crate::query::algebra::{AlgebraTriplePattern, TermPattern};
10use crate::OxirsError;
11use std::fmt;
12use std::sync::Arc;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
16pub struct QuotedTriple {
17 inner: Arc<Triple>,
19}
20
21impl QuotedTriple {
22 pub fn new(triple: Triple) -> Self {
24 QuotedTriple {
25 inner: Arc::new(triple),
26 }
27 }
28
29 pub fn from_arc(triple: Arc<Triple>) -> Self {
31 QuotedTriple { inner: triple }
32 }
33
34 pub fn inner(&self) -> &Triple {
36 &self.inner
37 }
38
39 pub fn subject(&self) -> &Subject {
41 self.inner.subject()
42 }
43
44 pub fn predicate(&self) -> &Predicate {
46 self.inner.predicate()
47 }
48
49 pub fn object(&self) -> &Object {
51 self.inner.object()
52 }
53
54 pub fn as_ref(&self) -> QuotedTripleRef<'_> {
56 QuotedTripleRef { inner: &self.inner }
57 }
58}
59
60impl fmt::Display for QuotedTriple {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 write!(
68 f,
69 "<< {} {} {} >>",
70 self.inner.subject(),
71 self.inner.predicate(),
72 self.inner.object()
73 )
74 }
75}
76
77impl RdfTerm for QuotedTriple {
78 fn as_str(&self) -> &str {
79 "<<quoted-triple>>"
81 }
82
83 fn is_quoted_triple(&self) -> bool {
84 true
85 }
86}
87
88impl SubjectTerm for QuotedTriple {}
89impl ObjectTerm for QuotedTriple {}
90
91#[cfg(feature = "serde")]
93impl serde::Serialize for QuotedTriple {
94 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
95 where
96 S: serde::Serializer,
97 {
98 self.inner.as_ref().serialize(serializer)
100 }
101}
102
103#[cfg(feature = "serde")]
104impl<'de> serde::Deserialize<'de> for QuotedTriple {
105 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106 where
107 D: serde::Deserializer<'de>,
108 {
109 let triple = Triple::deserialize(deserializer)?;
110 Ok(QuotedTriple::new(triple))
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
116pub struct QuotedTripleRef<'a> {
117 inner: &'a Triple,
118}
119
120impl<'a> QuotedTripleRef<'a> {
121 pub fn new(triple: &'a Triple) -> Self {
123 QuotedTripleRef { inner: triple }
124 }
125
126 pub fn inner(&self) -> &'a Triple {
128 self.inner
129 }
130
131 pub fn to_owned(&self) -> QuotedTriple {
133 QuotedTriple::new(self.inner.clone())
134 }
135}
136
137impl<'a> fmt::Display for QuotedTripleRef<'a> {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 write!(
142 f,
143 "<< {} {} {} >>",
144 self.inner.subject(),
145 self.inner.predicate(),
146 self.inner.object()
147 )
148 }
149}
150
151impl<'a> RdfTerm for QuotedTripleRef<'a> {
152 fn as_str(&self) -> &str {
153 "<<quoted-triple>>"
154 }
155
156 fn is_quoted_triple(&self) -> bool {
157 true
158 }
159}
160
161pub struct Annotation {
163 pub statement: QuotedTriple,
165 pub property: NamedNode,
167 pub value: Object,
169}
170
171impl Annotation {
172 pub fn new(statement: Triple, property: NamedNode, value: Object) -> Self {
174 Annotation {
175 statement: QuotedTriple::new(statement),
176 property,
177 value,
178 }
179 }
180
181 pub fn to_triple(&self) -> Triple {
183 Triple::new(
184 Subject::QuotedTriple(Box::new(self.statement.clone())),
185 self.property.clone(),
186 self.value.clone(),
187 )
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Hash)]
193pub enum StarPattern {
194 Triple(AlgebraTriplePattern),
196 QuotedTriple {
198 subject: Box<StarPattern>,
200 predicate: TermPattern,
202 object: Box<StarPattern>,
204 },
205 Annotation {
207 statement: Box<StarPattern>,
209 property: TermPattern,
211 value: TermPattern,
213 },
214}
215
216impl StarPattern {
217 pub fn has_variables(&self) -> bool {
219 match self {
220 StarPattern::Triple(pattern) => {
221 matches!(pattern.subject, TermPattern::Variable(_))
222 || matches!(pattern.predicate, TermPattern::Variable(_))
223 || matches!(pattern.object, TermPattern::Variable(_))
224 }
225 StarPattern::QuotedTriple {
226 subject,
227 predicate: _,
228 object,
229 } => subject.has_variables() || object.has_variables(),
230 StarPattern::Annotation {
231 statement,
232 property: _,
233 value: _,
234 } => statement.has_variables(),
235 }
236 }
237
238 pub fn variables(&self) -> Vec<crate::model::Variable> {
240 let mut vars = Vec::new();
241 self.collect_variables(&mut vars);
242 vars
243 }
244
245 fn collect_variables(&self, _vars: &mut Vec<crate::model::Variable>) {
246 match self {
247 StarPattern::Triple(pattern) => {
248 if let TermPattern::Variable(ref v) = pattern.subject {
249 _vars.push(v.clone());
250 }
251 if let TermPattern::Variable(ref v) = pattern.predicate {
252 _vars.push(v.clone());
253 }
254 if let TermPattern::Variable(ref v) = pattern.object {
255 _vars.push(v.clone());
256 }
257 }
258 StarPattern::QuotedTriple {
259 subject,
260 predicate: _,
261 object,
262 } => {
263 subject.collect_variables(_vars);
264 object.collect_variables(_vars);
265 }
266 StarPattern::Annotation {
267 statement,
268 property: _,
269 value: _,
270 } => {
271 statement.collect_variables(_vars);
272 }
273 }
274 }
275}
276
277pub mod serialization {
279 use super::*;
280
281 pub mod turtle_star {
283 use super::*;
284
285 pub fn serialize_quoted_triple(qt: &QuotedTriple) -> String {
287 format!("<< {} {} {} >>", qt.subject(), qt.predicate(), qt.object())
288 }
289
290 pub fn parse_quoted_triple(input: &str) -> Result<QuotedTriple, OxirsError> {
300 let trimmed = input.trim();
301 if !trimmed.starts_with("<<") || !trimmed.ends_with(">>") || trimmed.len() < 4 {
302 return Err(OxirsError::Parse(
303 "Invalid quoted triple syntax: expected '<< subject predicate object >>'"
304 .to_string(),
305 ));
306 }
307
308 let inner = trimmed[2..trimmed.len() - 2].trim();
309 let terms = split_star_terms(inner)?;
310 if terms.len() != 3 {
311 return Err(OxirsError::Parse(format!(
312 "Invalid quoted triple: expected exactly 3 terms (subject, predicate, object), found {}",
313 terms.len()
314 )));
315 }
316
317 let subject = parse_star_subject(terms[0])?;
318 let predicate = parse_star_predicate(terms[1])?;
319 let object = parse_star_object(terms[2])?;
320
321 Ok(QuotedTriple::new(Triple::new(subject, predicate, object)))
322 }
323
324 fn split_star_terms(input: &str) -> Result<Vec<&str>, OxirsError> {
329 let bytes = input.as_bytes();
330 let mut terms = Vec::new();
331 let mut i = 0usize;
332 let mut depth = 0i32;
333 let mut in_string = false;
334 let mut escape = false;
335 let mut term_start: Option<usize> = None;
336
337 while i < bytes.len() {
338 let c = bytes[i] as char;
339 if in_string {
340 if escape {
341 escape = false;
342 } else if c == '\\' {
343 escape = true;
344 } else if c == '"' {
345 in_string = false;
346 }
347 i += 1;
348 continue;
349 }
350
351 match c {
352 '"' => {
353 in_string = true;
354 if term_start.is_none() {
355 term_start = Some(i);
356 }
357 }
358 '<' if input[i..].starts_with("<<") => {
359 depth += 1;
360 if term_start.is_none() {
361 term_start = Some(i);
362 }
363 i += 1; }
365 '>' if input[i..].starts_with(">>") => {
366 depth -= 1;
367 if depth < 0 {
368 return Err(OxirsError::Parse(
369 "Unbalanced '>>' in quoted triple term".to_string(),
370 ));
371 }
372 i += 1; }
374 '<' => {
375 if term_start.is_none() {
376 term_start = Some(i);
377 }
378 }
379 c if c.is_whitespace() && depth == 0 => {
380 if let Some(start) = term_start.take() {
381 terms.push(input[start..i].trim());
382 }
383 }
384 _ => {
385 if term_start.is_none() {
386 term_start = Some(i);
387 }
388 }
389 }
390 i += 1;
391 }
392 if in_string {
393 return Err(OxirsError::Parse(
394 "Unterminated string literal in quoted triple".to_string(),
395 ));
396 }
397 if depth != 0 {
398 return Err(OxirsError::Parse(
399 "Unbalanced '<<'/'>>' nesting in quoted triple".to_string(),
400 ));
401 }
402 if let Some(start) = term_start {
403 terms.push(input[start..].trim());
404 }
405 Ok(terms.into_iter().filter(|t| !t.is_empty()).collect())
406 }
407
408 fn parse_star_iri(term: &str) -> Result<NamedNode, OxirsError> {
409 let inner = term
410 .strip_prefix('<')
411 .and_then(|s| s.strip_suffix('>'))
412 .ok_or_else(|| OxirsError::Parse(format!("Invalid IRI term: {term}")))?;
413 NamedNode::new(inner)
414 }
415
416 fn parse_star_literal(term: &str) -> Result<crate::model::Literal, OxirsError> {
417 use crate::model::Literal;
418
419 let bytes = term.as_bytes();
421 if bytes.first() != Some(&b'"') {
422 return Err(OxirsError::Parse(format!("Invalid literal term: {term}")));
423 }
424 let mut end = None;
425 let mut escape = false;
426 for (idx, b) in bytes.iter().enumerate().skip(1) {
427 if escape {
428 escape = false;
429 } else if *b == b'\\' {
430 escape = true;
431 } else if *b == b'"' {
432 end = Some(idx);
433 break;
434 }
435 }
436 let end =
437 end.ok_or_else(|| OxirsError::Parse(format!("Unterminated literal: {term}")))?;
438 let raw_value = &term[1..end];
439 let value = unescape_star_string(raw_value)?;
440 let suffix = &term[end + 1..];
441
442 if let Some(lang) = suffix.strip_prefix('@') {
443 Literal::new_language_tagged_literal(value, lang)
444 .map_err(|e| OxirsError::Parse(format!("Invalid language tag: {e}")))
445 } else if let Some(datatype) = suffix.strip_prefix("^^") {
446 let datatype = parse_star_iri(datatype)?;
447 Ok(Literal::new_typed(value, datatype))
448 } else if suffix.is_empty() {
449 Ok(Literal::new(value))
450 } else {
451 Err(OxirsError::Parse(format!(
452 "Invalid literal suffix in term: {term}"
453 )))
454 }
455 }
456
457 fn read_hex_escape(
461 chars: &mut std::str::Chars<'_>,
462 digits: usize,
463 kind: char,
464 ) -> Result<u32, OxirsError> {
465 let mut value: u32 = 0;
466 for _ in 0..digits {
467 let c = chars.next().ok_or_else(|| {
468 OxirsError::Parse(format!(
469 "Truncated \\{kind} escape in quoted triple literal: expected {digits} hex digits"
470 ))
471 })?;
472 let digit = c.to_digit(16).ok_or_else(|| {
473 OxirsError::Parse(format!(
474 "Invalid hex digit '{c}' in \\{kind} escape in quoted triple literal"
475 ))
476 })?;
477 value = (value << 4) | digit;
478 }
479 Ok(value)
480 }
481
482 fn unescape_star_string(raw: &str) -> Result<String, OxirsError> {
492 let mut result = String::with_capacity(raw.len());
493 let mut chars = raw.chars();
494 while let Some(c) = chars.next() {
495 if c != '\\' {
496 result.push(c);
497 continue;
498 }
499 match chars.next() {
500 Some('n') => result.push('\n'),
501 Some('t') => result.push('\t'),
502 Some('r') => result.push('\r'),
503 Some('b') => result.push('\u{08}'),
504 Some('f') => result.push('\u{0C}'),
505 Some('"') => result.push('"'),
506 Some('\'') => result.push('\''),
507 Some('\\') => result.push('\\'),
508 Some('u') => {
509 let codepoint = read_hex_escape(&mut chars, 4, 'u')?;
510 let c = char::from_u32(codepoint).ok_or_else(|| {
511 OxirsError::Parse(format!(
512 "Invalid Unicode codepoint U+{codepoint:04X} in \\u escape in quoted triple literal"
513 ))
514 })?;
515 result.push(c);
516 }
517 Some('U') => {
518 let codepoint = read_hex_escape(&mut chars, 8, 'U')?;
519 let c = char::from_u32(codepoint).ok_or_else(|| {
520 OxirsError::Parse(format!(
521 "Invalid Unicode codepoint U+{codepoint:08X} in \\U escape in quoted triple literal"
522 ))
523 })?;
524 result.push(c);
525 }
526 Some(other) => {
527 return Err(OxirsError::Parse(format!(
528 "Unsupported escape sequence '\\{other}' in quoted triple literal"
529 )))
530 }
531 None => {
532 return Err(OxirsError::Parse(
533 "Trailing backslash in quoted triple literal".to_string(),
534 ))
535 }
536 }
537 }
538 Ok(result)
539 }
540
541 fn parse_star_subject(term: &str) -> Result<Subject, OxirsError> {
542 if term.starts_with("<<") {
543 Ok(Subject::QuotedTriple(Box::new(parse_quoted_triple(term)?)))
544 } else if let Some(id) = term.strip_prefix("_:") {
545 Ok(Subject::BlankNode(crate::model::BlankNode::new(id)?))
546 } else if term.starts_with('<') {
547 Ok(Subject::NamedNode(parse_star_iri(term)?))
548 } else {
549 Err(OxirsError::Parse(format!(
550 "Unsupported subject term in quoted triple: {term}"
551 )))
552 }
553 }
554
555 fn parse_star_predicate(term: &str) -> Result<Predicate, OxirsError> {
556 if term.starts_with('<') {
557 Ok(Predicate::NamedNode(parse_star_iri(term)?))
558 } else {
559 Err(OxirsError::Parse(format!(
560 "Unsupported predicate term in quoted triple: {term}"
561 )))
562 }
563 }
564
565 fn parse_star_object(term: &str) -> Result<Object, OxirsError> {
566 if term.starts_with("<<") {
567 Ok(Object::QuotedTriple(Box::new(parse_quoted_triple(term)?)))
568 } else if let Some(id) = term.strip_prefix("_:") {
569 Ok(Object::BlankNode(crate::model::BlankNode::new(id)?))
570 } else if term.starts_with('<') {
571 Ok(Object::NamedNode(parse_star_iri(term)?))
572 } else if term.starts_with('"') {
573 Ok(Object::Literal(parse_star_literal(term)?))
574 } else {
575 Err(OxirsError::Parse(format!(
576 "Unsupported object term in quoted triple: {term}"
577 )))
578 }
579 }
580 }
581
582 pub mod sparql_star {
584 use super::*;
585
586 pub fn format_star_pattern(pattern: &StarPattern) -> String {
588 match pattern {
589 StarPattern::Triple(pattern) => pattern.to_string(),
590 StarPattern::QuotedTriple {
591 subject,
592 predicate: _,
593 object,
594 } => {
595 format!(
596 "<< {} {} {} >>",
597 format_star_pattern(subject),
598 "PREDICATE",
599 format_star_pattern(object)
600 )
601 }
602 StarPattern::Annotation {
603 statement,
604 property: _,
605 value: _,
606 } => {
607 format!(
608 "{} {} {}",
609 format_star_pattern(statement),
610 "PROPERTY",
611 "VALUE"
612 )
613 }
614 }
615 }
616 }
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622 use crate::model::{Literal, NamedNode};
623
624 #[test]
625 fn test_quoted_triple() {
626 let subject = NamedNode::new("http://example.org/alice").expect("valid IRI");
627 let predicate = NamedNode::new("http://example.org/says").expect("valid IRI");
628 let object = Object::Literal(Literal::new("Hello"));
629
630 let triple = Triple::new(subject, predicate, object);
631 let quoted = QuotedTriple::new(triple.clone());
632
633 assert_eq!(quoted.inner(), &triple);
634 assert_eq!(
638 format!("{quoted}"),
639 "<< <http://example.org/alice> <http://example.org/says> \"Hello\" >>"
640 );
641 }
642
643 #[test]
648 fn regression_quoted_triple_display_round_trips() {
649 use serialization::turtle_star::{parse_quoted_triple, serialize_quoted_triple};
650
651 let subject = NamedNode::new("http://example.org/alice").expect("valid IRI");
652 let predicate = NamedNode::new("http://example.org/says").expect("valid IRI");
653 let object = Object::Literal(Literal::new("Hello"));
654 let triple = Triple::new(subject, predicate, object);
655 let quoted = QuotedTriple::new(triple);
656
657 let displayed = format!("{quoted}");
658 assert!(
659 !displayed.contains(". >>"),
660 "Display output must not contain a trailing statement terminator: {displayed}"
661 );
662
663 assert_eq!(displayed, serialize_quoted_triple("ed));
665
666 let reparsed = parse_quoted_triple(&displayed).expect("Display output must re-parse");
668 assert_eq!(reparsed, quoted);
669
670 let triple_ref = quoted.inner().clone();
672 let quoted_ref = QuotedTripleRef::new(&triple_ref);
673 assert_eq!(format!("{quoted_ref}"), displayed);
674 }
675
676 #[test]
677 fn test_annotation() {
678 let subject = NamedNode::new("http://example.org/alice").expect("valid IRI");
679 let predicate = NamedNode::new("http://example.org/age").expect("valid IRI");
680 let object = Object::Literal(Literal::new_typed(
681 "30",
682 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").expect("valid IRI"),
683 ));
684
685 let statement = Triple::new(subject, predicate, object);
686 let ann_property = NamedNode::new("http://example.org/confidence").expect("valid IRI");
687 let ann_value = Object::Literal(Literal::new_typed(
688 "0.9",
689 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").expect("valid IRI"),
690 ));
691
692 let annotation = Annotation::new(statement, ann_property, ann_value);
693 let ann_triple = annotation.to_triple();
694
695 assert!(matches!(ann_triple.subject(), Subject::QuotedTriple(_)));
696 }
697
698 #[test]
699 fn test_parse_quoted_triple_basic() {
700 use serialization::turtle_star::parse_quoted_triple;
701
702 let parsed = parse_quoted_triple(
703 "<< <http://example.org/alice> <http://example.org/says> \"Hello\" >>",
704 )
705 .expect("valid quoted triple");
706
707 assert_eq!(
708 parsed.subject(),
709 &Subject::NamedNode(NamedNode::new("http://example.org/alice").expect("valid IRI"))
710 );
711 assert_eq!(
712 parsed.predicate(),
713 &Predicate::NamedNode(NamedNode::new("http://example.org/says").expect("valid IRI"))
714 );
715 assert_eq!(parsed.object(), &Object::Literal(Literal::new("Hello")));
716 }
717
718 #[test]
719 fn test_parse_quoted_triple_roundtrip_with_serialize() {
720 use serialization::turtle_star::{parse_quoted_triple, serialize_quoted_triple};
721
722 let subject = NamedNode::new("http://example.org/alice").expect("valid IRI");
723 let predicate = NamedNode::new("http://example.org/age").expect("valid IRI");
724 let object = Object::Literal(Literal::new_typed(
725 "30",
726 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").expect("valid IRI"),
727 ));
728 let original = QuotedTriple::new(Triple::new(subject, predicate, object));
729
730 let text = serialize_quoted_triple(&original);
731 let parsed = parse_quoted_triple(&text).expect("round-trip parse");
732 assert_eq!(parsed, original);
733 }
734
735 #[test]
736 fn test_parse_quoted_triple_nested() {
737 use serialization::turtle_star::parse_quoted_triple;
738
739 let input = "<< << <http://example.org/a> <http://example.org/p> <http://example.org/b> >> <http://example.org/certainty> \"0.9\"^^<http://www.w3.org/2001/XMLSchema#double> >>";
740 let parsed = parse_quoted_triple(input).expect("valid nested quoted triple");
741 assert!(matches!(parsed.subject(), Subject::QuotedTriple(_)));
742 }
743
744 #[test]
745 fn test_parse_quoted_triple_rejects_bad_syntax() {
746 use serialization::turtle_star::parse_quoted_triple;
747
748 assert!(parse_quoted_triple("not a quoted triple").is_err());
749 assert!(
750 parse_quoted_triple("<< <http://example.org/a> <http://example.org/b> >>").is_err()
751 );
752 }
753
754 #[test]
759 fn regression_unescape_star_string_supports_full_echar_uchar_grammar() {
760 use serialization::turtle_star::{parse_quoted_triple, serialize_quoted_triple};
761
762 let value = "back\u{08}space form\u{0C}feed bell\u{07}end";
766 let subject = NamedNode::new("http://example.org/alice").expect("valid IRI");
767 let predicate = NamedNode::new("http://example.org/note").expect("valid IRI");
768 let original = QuotedTriple::new(Triple::new(
769 subject,
770 predicate,
771 Object::Literal(Literal::new(value)),
772 ));
773
774 let text = serialize_quoted_triple(&original);
775 assert!(text.contains("\\b"), "expected a \\b escape in: {text}");
776 assert!(text.contains("\\f"), "expected a \\f escape in: {text}");
777 assert!(
778 text.contains("\\u0007"),
779 "expected a \\u0007 escape in: {text}"
780 );
781
782 let parsed = parse_quoted_triple(&text).expect("must re-parse its own serialized form");
783 assert_eq!(parsed, original);
784 assert_eq!(parsed.object(), &Object::Literal(Literal::new(value)));
785 }
786
787 #[test]
791 fn regression_unescape_star_string_handles_u_and_big_u_escapes() {
792 use serialization::turtle_star::parse_quoted_triple;
793
794 let input = "<< <http://example.org/s> <http://example.org/p> \"\\u0041-\\U0001F600\" >>";
796 let parsed = parse_quoted_triple(input).expect("valid \\u/\\U escapes must parse");
797 assert_eq!(
798 parsed.object(),
799 &Object::Literal(Literal::new("A-\u{1F600}"))
800 );
801 }
802
803 #[test]
807 fn regression_unescape_star_string_rejects_truncated_unicode_escape() {
808 use serialization::turtle_star::parse_quoted_triple;
809
810 let truncated = "<< <http://example.org/s> <http://example.org/p> \"\\u12\" >>";
811 assert!(parse_quoted_triple(truncated).is_err());
812
813 let bad_digit = "<< <http://example.org/s> <http://example.org/p> \"\\u12ZZ\" >>";
814 assert!(parse_quoted_triple(bad_digit).is_err());
815 }
816}