Skip to main content

oxirs_core/model/
triple_term.rs

1//! SPARQL 1.2 Triple Term support
2//!
3//! This module implements the `TripleTerm` type, a first-class RDF term that wraps
4//! Subject + Predicate + Object for use in RDF-star reification. Unlike `QuotedTriple`
5//! (which uses `Arc<Triple>` and focuses on embedding triples as subjects/objects),
6//! `TripleTerm` is a lightweight, self-contained value type designed for SPARQL 1.2
7//! triple term syntax: `<< s p o >>` as a standalone term in expressions, BIND clauses,
8//! and CONSTRUCT templates.
9//!
10//! # SPARQL 1.2 Triple Terms
11//!
12//! SPARQL 1.2 introduces triple terms as first-class citizens in the query language.
13//! A triple term `<< s p o >>` can appear:
14//! - In BIND clauses: `BIND(<< :alice :knows :bob >> AS ?t)`
15//! - In CONSTRUCT templates: `CONSTRUCT { ?t :source "db1" }`
16//! - In FILTER expressions: `FILTER(?t = << :alice :knows :bob >>)`
17//! - As function arguments: `TRIPLE(?s, ?p, ?o)`
18//!
19//! # Differences from QuotedTriple
20//!
21//! | Feature | TripleTerm | QuotedTriple |
22//! |---------|-----------|--------------|
23//! | Storage | Inline (owned components) | `Arc<Triple>` |
24//! | Focus | SPARQL 1.2 expression term | RDF-star graph embedding |
25//! | Parsing | `<< s p o >>` syntax | RDF-star syntax |
26//! | Use case | Query algebra, BIND, FILTER | Subject/Object position |
27//! | Hashing | Component-wise | Via `Arc<Triple>` |
28
29use crate::model::{BlankNode, Literal, NamedNode, Object, Predicate, RdfTerm, Subject, Triple};
30use crate::OxirsError;
31use serde::{Deserialize, Serialize};
32use std::collections::hash_map::DefaultHasher;
33use std::fmt;
34use std::hash::{Hash, Hasher};
35
36/// A SPARQL 1.2 Triple Term: a first-class term wrapping Subject + Predicate + Object.
37///
38/// Triple terms allow triples to be treated as values in SPARQL expressions,
39/// enabling powerful reification patterns without the overhead of named graphs
40/// or blank node reification.
41///
42/// # Examples
43///
44/// ```
45/// use oxirs_core::model::{NamedNode, Literal};
46/// use oxirs_core::model::triple_term::TripleTerm;
47///
48/// let tt = TripleTerm::new(
49///     NamedNode::new("http://example.org/alice").expect("valid IRI"),
50///     NamedNode::new("http://example.org/knows").expect("valid IRI"),
51///     NamedNode::new("http://example.org/bob").expect("valid IRI"),
52/// );
53///
54/// assert_eq!(tt.to_string(), "<< <http://example.org/alice> <http://example.org/knows> <http://example.org/bob> >>");
55/// ```
56#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
57pub struct TripleTerm {
58    subject: Subject,
59    predicate: Predicate,
60    object: Object,
61}
62
63impl TripleTerm {
64    /// Creates a new triple term from subject, predicate, and object.
65    pub fn new(
66        subject: impl Into<Subject>,
67        predicate: impl Into<Predicate>,
68        object: impl Into<Object>,
69    ) -> Self {
70        Self {
71            subject: subject.into(),
72            predicate: predicate.into(),
73            object: object.into(),
74        }
75    }
76
77    /// Creates a triple term from an existing Triple by cloning its components.
78    pub fn from_triple(triple: &Triple) -> Self {
79        Self {
80            subject: triple.subject().clone(),
81            predicate: triple.predicate().clone(),
82            object: triple.object().clone(),
83        }
84    }
85
86    /// Creates a triple term by consuming a Triple.
87    pub fn from_owned_triple(triple: Triple) -> Self {
88        let (subject, predicate, object) = triple.into_parts();
89        Self {
90            subject,
91            predicate,
92            object,
93        }
94    }
95
96    /// Returns the subject of this triple term.
97    pub fn subject(&self) -> &Subject {
98        &self.subject
99    }
100
101    /// Returns the predicate of this triple term.
102    pub fn predicate(&self) -> &Predicate {
103        &self.predicate
104    }
105
106    /// Returns the object of this triple term.
107    pub fn object(&self) -> &Object {
108        &self.object
109    }
110
111    /// Decomposes this triple term into its components.
112    pub fn into_parts(self) -> (Subject, Predicate, Object) {
113        (self.subject, self.predicate, self.object)
114    }
115
116    /// Converts this triple term into a Triple.
117    pub fn into_triple(self) -> Triple {
118        Triple::new(self.subject, self.predicate, self.object)
119    }
120
121    /// Creates a Triple from this triple term (cloning components).
122    pub fn to_triple(&self) -> Triple {
123        Triple::new(
124            self.subject.clone(),
125            self.predicate.clone(),
126            self.object.clone(),
127        )
128    }
129
130    /// Returns a canonical N-Triples-style string representation.
131    ///
132    /// This is useful for serialization and comparison purposes.
133    pub fn to_ntriples_string(&self) -> String {
134        format!("<< {} {} {} >>", self.subject, self.predicate, self.object)
135    }
136
137    /// Parses a triple term from the SPARQL 1.2 `<< s p o >>` syntax.
138    ///
139    /// Supports named nodes (IRIs in angle brackets) and simple string literals.
140    ///
141    /// # Errors
142    ///
143    /// Returns `OxirsError::Parse` if the input is not valid triple term syntax.
144    pub fn parse(input: &str) -> Result<Self, OxirsError> {
145        let trimmed = input.trim();
146
147        if !trimmed.starts_with("<<") || !trimmed.ends_with(">>") {
148            return Err(OxirsError::Parse(
149                "Triple term must be enclosed in << >>".to_string(),
150            ));
151        }
152
153        // Strip the outer << >> delimiters
154        let inner = trimmed[2..trimmed.len() - 2].trim();
155
156        if inner.is_empty() {
157            return Err(OxirsError::Parse("Triple term cannot be empty".to_string()));
158        }
159
160        // Tokenize: split into exactly 3 tokens respecting <...> and "..."
161        let tokens = tokenize_triple_term(inner)?;
162
163        if tokens.len() != 3 {
164            return Err(OxirsError::Parse(format!(
165                "Triple term requires exactly 3 components (subject, predicate, object), got {}",
166                tokens.len()
167            )));
168        }
169
170        let subject = parse_subject(&tokens[0])?;
171        let predicate = parse_predicate(&tokens[1])?;
172        let object = parse_object(&tokens[2])?;
173
174        Ok(Self {
175            subject,
176            predicate,
177            object,
178        })
179    }
180
181    /// Returns true if the subject is a named node (IRI).
182    pub fn has_iri_subject(&self) -> bool {
183        matches!(self.subject, Subject::NamedNode(_))
184    }
185
186    /// Returns true if the subject is a blank node.
187    pub fn has_blank_subject(&self) -> bool {
188        matches!(self.subject, Subject::BlankNode(_))
189    }
190
191    /// Returns true if the object is a literal.
192    pub fn has_literal_object(&self) -> bool {
193        matches!(self.object, Object::Literal(_))
194    }
195
196    /// Returns true if the object is a named node (IRI).
197    pub fn has_iri_object(&self) -> bool {
198        matches!(self.object, Object::NamedNode(_))
199    }
200
201    /// Returns true if any component is a variable.
202    pub fn has_variables(&self) -> bool {
203        matches!(self.subject, Subject::Variable(_))
204            || matches!(self.predicate, Predicate::Variable(_))
205            || matches!(self.object, Object::Variable(_))
206    }
207
208    /// Returns a deterministic hash of this triple term.
209    pub fn deterministic_hash(&self) -> u64 {
210        let mut hasher = DefaultHasher::new();
211        self.hash(&mut hasher);
212        hasher.finish()
213    }
214
215    /// Checks structural equality: two triple terms are structurally equal
216    /// if all their components are equal.
217    pub fn structurally_equal(&self, other: &TripleTerm) -> bool {
218        self.subject == other.subject
219            && self.predicate == other.predicate
220            && self.object == other.object
221    }
222
223    /// Returns a new triple term with the subject replaced.
224    pub fn with_subject(mut self, subject: impl Into<Subject>) -> Self {
225        self.subject = subject.into();
226        self
227    }
228
229    /// Returns a new triple term with the predicate replaced.
230    pub fn with_predicate(mut self, predicate: impl Into<Predicate>) -> Self {
231        self.predicate = predicate.into();
232        self
233    }
234
235    /// Returns a new triple term with the object replaced.
236    pub fn with_object(mut self, object: impl Into<Object>) -> Self {
237        self.object = object.into();
238        self
239    }
240
241    /// Validates that this triple term is well-formed for SPARQL 1.2.
242    ///
243    /// A well-formed triple term has:
244    /// - A non-variable subject (NamedNode or BlankNode)
245    /// - A non-variable predicate (NamedNode)
246    /// - A non-variable object (NamedNode, BlankNode, or Literal)
247    pub fn is_ground(&self) -> bool {
248        !self.has_variables()
249    }
250
251    /// Returns the number of distinct IRIs used in this triple term.
252    pub fn iri_count(&self) -> usize {
253        let mut count = 0;
254        if matches!(self.subject, Subject::NamedNode(_)) {
255            count += 1;
256        }
257        if matches!(self.predicate, Predicate::NamedNode(_)) {
258            count += 1;
259        }
260        if matches!(self.object, Object::NamedNode(_)) {
261            count += 1;
262        }
263        count
264    }
265}
266
267impl fmt::Display for TripleTerm {
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        write!(
270            f,
271            "<< {} {} {} >>",
272            self.subject, self.predicate, self.object
273        )
274    }
275}
276
277impl RdfTerm for TripleTerm {
278    fn as_str(&self) -> &str {
279        "<<triple-term>>"
280    }
281
282    fn is_quoted_triple(&self) -> bool {
283        true
284    }
285}
286
287impl From<Triple> for TripleTerm {
288    fn from(triple: Triple) -> Self {
289        Self::from_owned_triple(triple)
290    }
291}
292
293impl From<TripleTerm> for Triple {
294    fn from(tt: TripleTerm) -> Self {
295        tt.into_triple()
296    }
297}
298
299impl From<&Triple> for TripleTerm {
300    fn from(triple: &Triple) -> Self {
301        Self::from_triple(triple)
302    }
303}
304
305/// A borrowed reference to a TripleTerm's components.
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
307pub struct TripleTermRef<'a> {
308    subject: &'a Subject,
309    predicate: &'a Predicate,
310    object: &'a Object,
311}
312
313impl<'a> TripleTermRef<'a> {
314    /// Creates a new triple term reference.
315    pub fn new(subject: &'a Subject, predicate: &'a Predicate, object: &'a Object) -> Self {
316        Self {
317            subject,
318            predicate,
319            object,
320        }
321    }
322
323    /// Creates a reference from a TripleTerm.
324    pub fn from_triple_term(tt: &'a TripleTerm) -> Self {
325        Self {
326            subject: &tt.subject,
327            predicate: &tt.predicate,
328            object: &tt.object,
329        }
330    }
331
332    /// Returns the subject.
333    pub fn subject(&self) -> &'a Subject {
334        self.subject
335    }
336
337    /// Returns the predicate.
338    pub fn predicate(&self) -> &'a Predicate {
339        self.predicate
340    }
341
342    /// Returns the object.
343    pub fn object(&self) -> &'a Object {
344        self.object
345    }
346
347    /// Converts to an owned TripleTerm.
348    pub fn to_owned(&self) -> TripleTerm {
349        TripleTerm {
350            subject: self.subject.clone(),
351            predicate: self.predicate.clone(),
352            object: self.object.clone(),
353        }
354    }
355}
356
357impl fmt::Display for TripleTermRef<'_> {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        write!(
360            f,
361            "<< {} {} {} >>",
362            self.subject, self.predicate, self.object
363        )
364    }
365}
366
367/// A collection of triple terms with lookup and filtering operations.
368#[derive(Debug, Clone, Default)]
369pub struct TripleTermSet {
370    terms: Vec<TripleTerm>,
371}
372
373impl TripleTermSet {
374    /// Creates a new empty triple term set.
375    pub fn new() -> Self {
376        Self { terms: Vec::new() }
377    }
378
379    /// Creates a triple term set with the given capacity.
380    pub fn with_capacity(capacity: usize) -> Self {
381        Self {
382            terms: Vec::with_capacity(capacity),
383        }
384    }
385
386    /// Adds a triple term to the set. Returns true if the term was not already present.
387    pub fn insert(&mut self, term: TripleTerm) -> bool {
388        if self.terms.contains(&term) {
389            false
390        } else {
391            self.terms.push(term);
392            true
393        }
394    }
395
396    /// Returns the number of triple terms in the set.
397    pub fn len(&self) -> usize {
398        self.terms.len()
399    }
400
401    /// Returns true if the set is empty.
402    pub fn is_empty(&self) -> bool {
403        self.terms.is_empty()
404    }
405
406    /// Returns true if the set contains the given triple term.
407    pub fn contains(&self, term: &TripleTerm) -> bool {
408        self.terms.contains(term)
409    }
410
411    /// Returns an iterator over the triple terms.
412    pub fn iter(&self) -> impl Iterator<Item = &TripleTerm> {
413        self.terms.iter()
414    }
415
416    /// Filters triple terms by predicate IRI.
417    pub fn filter_by_predicate(&self, predicate_iri: &str) -> Vec<&TripleTerm> {
418        self.terms
419            .iter()
420            .filter(|tt| match &tt.predicate {
421                Predicate::NamedNode(nn) => nn.as_str() == predicate_iri,
422                Predicate::Variable(_) => false,
423            })
424            .collect()
425    }
426
427    /// Filters triple terms by subject IRI.
428    pub fn filter_by_subject_iri(&self, subject_iri: &str) -> Vec<&TripleTerm> {
429        self.terms
430            .iter()
431            .filter(|tt| match &tt.subject {
432                Subject::NamedNode(nn) => nn.as_str() == subject_iri,
433                _ => false,
434            })
435            .collect()
436    }
437
438    /// Returns all ground triple terms (no variables).
439    pub fn ground_terms(&self) -> Vec<&TripleTerm> {
440        self.terms.iter().filter(|tt| tt.is_ground()).collect()
441    }
442
443    /// Removes a triple term from the set. Returns true if the term was present.
444    pub fn remove(&mut self, term: &TripleTerm) -> bool {
445        if let Some(pos) = self.terms.iter().position(|t| t == term) {
446            self.terms.swap_remove(pos);
447            true
448        } else {
449            false
450        }
451    }
452
453    /// Drains all triple terms from the set.
454    pub fn drain(&mut self) -> impl Iterator<Item = TripleTerm> + '_ {
455        self.terms.drain(..)
456    }
457
458    /// Converts all triple terms to Triples.
459    pub fn to_triples(&self) -> Vec<Triple> {
460        self.terms.iter().map(|tt| tt.to_triple()).collect()
461    }
462}
463
464impl IntoIterator for TripleTermSet {
465    type Item = TripleTerm;
466    type IntoIter = std::vec::IntoIter<TripleTerm>;
467
468    fn into_iter(self) -> Self::IntoIter {
469        self.terms.into_iter()
470    }
471}
472
473impl FromIterator<TripleTerm> for TripleTermSet {
474    fn from_iter<I: IntoIterator<Item = TripleTerm>>(iter: I) -> Self {
475        let mut set = Self::new();
476        for term in iter {
477            set.insert(term);
478        }
479        set
480    }
481}
482
483// ─── Parsing helpers ───────────────────────────────────────────────────────────
484
485/// Tokenizes the inner content of a `<< ... >>` triple term into exactly 3 tokens.
486fn tokenize_triple_term(input: &str) -> Result<Vec<String>, OxirsError> {
487    let mut tokens = Vec::new();
488    let chars: Vec<char> = input.chars().collect();
489    let mut i = 0;
490    let len = chars.len();
491
492    while i < len {
493        // Skip whitespace
494        while i < len && chars[i].is_whitespace() {
495            i += 1;
496        }
497        if i >= len {
498            break;
499        }
500
501        if chars[i] == '<' {
502            // IRI: <...>
503            let start = i;
504            i += 1;
505            while i < len && chars[i] != '>' {
506                i += 1;
507            }
508            if i >= len {
509                return Err(OxirsError::Parse(
510                    "Unterminated IRI in triple term".to_string(),
511                ));
512            }
513            i += 1; // consume '>'
514            let token: String = chars[start..i].iter().collect();
515            tokens.push(token);
516        } else if chars[i] == '"' {
517            // Literal: "..."
518            let start = i;
519            i += 1;
520            while i < len && chars[i] != '"' {
521                if chars[i] == '\\' {
522                    i += 1; // skip escaped char
523                }
524                i += 1;
525            }
526            if i >= len {
527                return Err(OxirsError::Parse(
528                    "Unterminated literal in triple term".to_string(),
529                ));
530            }
531            i += 1; // consume closing '"'
532
533            // Check for language tag or datatype
534            if i < len && chars[i] == '@' {
535                // Language tag: "..."@en
536                i += 1;
537                while i < len && (chars[i].is_alphanumeric() || chars[i] == '-') {
538                    i += 1;
539                }
540            } else if i + 1 < len && chars[i] == '^' && chars[i + 1] == '^' {
541                // Datatype: "..."^^<...>
542                i += 2;
543                if i < len && chars[i] == '<' {
544                    while i < len && chars[i] != '>' {
545                        i += 1;
546                    }
547                    if i < len {
548                        i += 1; // consume '>'
549                    }
550                }
551            }
552
553            let token: String = chars[start..i].iter().collect();
554            tokens.push(token);
555        } else if chars[i] == '_' && i + 1 < len && chars[i + 1] == ':' {
556            // Blank node: _:...
557            let start = i;
558            i += 2;
559            while i < len && !chars[i].is_whitespace() {
560                i += 1;
561            }
562            let token: String = chars[start..i].iter().collect();
563            tokens.push(token);
564        } else if chars[i] == '?' || chars[i] == '$' {
565            // Variable: ?var or $var
566            let start = i;
567            i += 1;
568            while i < len && (chars[i].is_alphanumeric() || chars[i] == '_') {
569                i += 1;
570            }
571            let token: String = chars[start..i].iter().collect();
572            tokens.push(token);
573        } else {
574            // Unknown token - consume until whitespace
575            let start = i;
576            while i < len && !chars[i].is_whitespace() {
577                i += 1;
578            }
579            let token: String = chars[start..i].iter().collect();
580            tokens.push(token);
581        }
582    }
583
584    Ok(tokens)
585}
586
587/// Parses a subject token.
588fn parse_subject(token: &str) -> Result<Subject, OxirsError> {
589    if token.starts_with('<') && token.ends_with('>') {
590        let iri = &token[1..token.len() - 1];
591        Ok(Subject::NamedNode(NamedNode::new(iri)?))
592    } else if let Some(stripped) = token.strip_prefix("_:") {
593        Ok(Subject::BlankNode(BlankNode::new(stripped)?))
594    } else if token.starts_with('?') || token.starts_with('$') {
595        let var_name = &token[1..];
596        Ok(Subject::Variable(crate::model::Variable::new(var_name)?))
597    } else {
598        Err(OxirsError::Parse(format!(
599            "Invalid subject in triple term: '{token}'"
600        )))
601    }
602}
603
604/// Parses a predicate token.
605fn parse_predicate(token: &str) -> Result<Predicate, OxirsError> {
606    if token.starts_with('<') && token.ends_with('>') {
607        let iri = &token[1..token.len() - 1];
608        Ok(Predicate::NamedNode(NamedNode::new(iri)?))
609    } else if token.starts_with('?') || token.starts_with('$') {
610        let var_name = &token[1..];
611        Ok(Predicate::Variable(crate::model::Variable::new(var_name)?))
612    } else {
613        Err(OxirsError::Parse(format!(
614            "Invalid predicate in triple term: '{token}'. Predicates must be IRIs or variables."
615        )))
616    }
617}
618
619/// Parses an object token.
620fn parse_object(token: &str) -> Result<Object, OxirsError> {
621    if token.starts_with('<') && token.ends_with('>') {
622        let iri = &token[1..token.len() - 1];
623        Ok(Object::NamedNode(NamedNode::new(iri)?))
624    } else if let Some(stripped) = token.strip_prefix("_:") {
625        Ok(Object::BlankNode(BlankNode::new(stripped)?))
626    } else if token.starts_with('"') {
627        // Parse literal with optional language tag or datatype
628        let literal = parse_literal_token(token)?;
629        Ok(Object::Literal(literal))
630    } else if token.starts_with('?') || token.starts_with('$') {
631        let var_name = &token[1..];
632        Ok(Object::Variable(crate::model::Variable::new(var_name)?))
633    } else {
634        Err(OxirsError::Parse(format!(
635            "Invalid object in triple term: '{token}'"
636        )))
637    }
638}
639
640/// Parses a literal token with optional language tag or datatype.
641fn parse_literal_token(token: &str) -> Result<Literal, OxirsError> {
642    if !token.starts_with('"') {
643        return Err(OxirsError::Parse(format!("Invalid literal: '{token}'")));
644    }
645
646    // Find the closing quote (handle escapes)
647    let bytes = token.as_bytes();
648    let mut end_quote = 1;
649    while end_quote < bytes.len() {
650        if bytes[end_quote] == b'"' && (end_quote == 1 || bytes[end_quote - 1] != b'\\') {
651            break;
652        }
653        end_quote += 1;
654    }
655
656    if end_quote >= bytes.len() {
657        return Err(OxirsError::Parse(format!(
658            "Unterminated literal: '{token}'"
659        )));
660    }
661
662    let value = &token[1..end_quote];
663    let rest = &token[end_quote + 1..];
664
665    if let Some(lang) = rest.strip_prefix('@') {
666        // Language-tagged literal
667        Literal::new_lang(value, lang)
668            .map_err(|e| OxirsError::Parse(format!("Invalid language tag '{lang}': {e}")))
669    } else if let Some(dt_part) = rest.strip_prefix("^^") {
670        // Typed literal
671        if dt_part.starts_with('<') && dt_part.ends_with('>') {
672            let dt_iri = &dt_part[1..dt_part.len() - 1];
673            let datatype = NamedNode::new(dt_iri)?;
674            Ok(Literal::new_typed(value, datatype))
675        } else {
676            Err(OxirsError::Parse(format!(
677                "Invalid datatype IRI in literal: '{dt_part}'"
678            )))
679        }
680    } else {
681        // Simple literal
682        Ok(Literal::new(value))
683    }
684}
685
686// ─── Tests ─────────────────────────────────────────────────────────────────────
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691    use crate::model::{BlankNode, Literal, NamedNode, Variable};
692
693    fn example_iri(local: &str) -> NamedNode {
694        NamedNode::new(format!("http://example.org/{local}")).expect("valid IRI")
695    }
696
697    fn make_tt(s: &str, p: &str, o_iri: &str) -> TripleTerm {
698        TripleTerm::new(example_iri(s), example_iri(p), example_iri(o_iri))
699    }
700
701    // ── Construction tests ──────────────────────────────────────────────────
702
703    #[test]
704    fn test_new_with_named_nodes() {
705        let tt = make_tt("alice", "knows", "bob");
706        assert!(tt.has_iri_subject());
707        assert!(tt.has_iri_object());
708        assert!(!tt.has_literal_object());
709        assert!(!tt.has_blank_subject());
710        assert!(!tt.has_variables());
711        assert!(tt.is_ground());
712    }
713
714    #[test]
715    fn test_new_with_literal_object() {
716        let tt = TripleTerm::new(
717            example_iri("alice"),
718            example_iri("name"),
719            Literal::new("Alice"),
720        );
721        assert!(tt.has_literal_object());
722        assert!(!tt.has_iri_object());
723        assert!(tt.is_ground());
724    }
725
726    #[test]
727    fn test_new_with_blank_node_subject() {
728        let bn = BlankNode::new("b1").expect("valid blank node");
729        let tt = TripleTerm::new(bn, example_iri("type"), example_iri("Person"));
730        assert!(tt.has_blank_subject());
731        assert!(!tt.has_iri_subject());
732        assert!(tt.is_ground());
733    }
734
735    #[test]
736    fn test_new_with_variable() {
737        let var = Variable::new("x").expect("valid var");
738        let tt = TripleTerm::new(var, example_iri("knows"), example_iri("bob"));
739        assert!(tt.has_variables());
740        assert!(!tt.is_ground());
741    }
742
743    #[test]
744    fn test_from_triple() {
745        let triple = Triple::new(
746            example_iri("alice"),
747            example_iri("knows"),
748            example_iri("bob"),
749        );
750        let tt = TripleTerm::from_triple(&triple);
751        assert_eq!(tt.subject(), triple.subject());
752        assert_eq!(tt.predicate(), triple.predicate());
753        assert_eq!(tt.object(), triple.object());
754    }
755
756    #[test]
757    fn test_from_owned_triple() {
758        let triple = Triple::new(
759            example_iri("alice"),
760            example_iri("knows"),
761            example_iri("bob"),
762        );
763        let triple_clone = triple.clone();
764        let tt = TripleTerm::from_owned_triple(triple);
765        assert_eq!(tt.subject(), triple_clone.subject());
766    }
767
768    // ── Into/From conversions ───────────────────────────────────────────────
769
770    #[test]
771    fn test_into_triple() {
772        let tt = make_tt("alice", "knows", "bob");
773        let triple = tt.into_triple();
774        assert_eq!(triple.subject(), &Subject::NamedNode(example_iri("alice")));
775    }
776
777    #[test]
778    fn test_from_triple_trait() {
779        let triple = Triple::new(
780            example_iri("alice"),
781            example_iri("knows"),
782            example_iri("bob"),
783        );
784        let tt: TripleTerm = triple.clone().into();
785        assert_eq!(tt.to_triple(), triple);
786    }
787
788    #[test]
789    fn test_from_ref_triple() {
790        let triple = Triple::new(
791            example_iri("alice"),
792            example_iri("knows"),
793            example_iri("bob"),
794        );
795        let tt: TripleTerm = (&triple).into();
796        assert_eq!(tt.to_triple(), triple);
797    }
798
799    #[test]
800    fn test_to_triple_roundtrip() {
801        let tt = make_tt("s", "p", "o");
802        let triple = tt.to_triple();
803        let tt2 = TripleTerm::from_triple(&triple);
804        assert_eq!(tt2, make_tt("s", "p", "o"));
805    }
806
807    // ── Display and parsing ─────────────────────────────────────────────────
808
809    #[test]
810    fn test_display() {
811        let tt = make_tt("alice", "knows", "bob");
812        let display = tt.to_string();
813        assert!(display.starts_with("<<"));
814        assert!(display.ends_with(">>"));
815        assert!(display.contains("http://example.org/alice"));
816        assert!(display.contains("http://example.org/knows"));
817        assert!(display.contains("http://example.org/bob"));
818    }
819
820    #[test]
821    fn test_ntriples_string() {
822        let tt = make_tt("alice", "knows", "bob");
823        let s = tt.to_ntriples_string();
824        assert_eq!(
825            s,
826            "<< <http://example.org/alice> <http://example.org/knows> <http://example.org/bob> >>"
827        );
828    }
829
830    #[test]
831    fn test_parse_basic_iris() {
832        let input =
833            "<< <http://example.org/alice> <http://example.org/knows> <http://example.org/bob> >>";
834        let tt = TripleTerm::parse(input).expect("should parse");
835        assert_eq!(tt, make_tt("alice", "knows", "bob"));
836    }
837
838    #[test]
839    fn test_parse_with_literal() {
840        let input = r#"<< <http://example.org/alice> <http://example.org/name> "Alice" >>"#;
841        let tt = TripleTerm::parse(input).expect("should parse");
842        assert!(tt.has_literal_object());
843    }
844
845    #[test]
846    fn test_parse_with_lang_literal() {
847        let input = r#"<< <http://example.org/alice> <http://example.org/name> "Alice"@en >>"#;
848        let tt = TripleTerm::parse(input).expect("should parse");
849        if let Object::Literal(lit) = tt.object() {
850            assert_eq!(lit.language(), Some("en"));
851        } else {
852            panic!("Expected literal object");
853        }
854    }
855
856    #[test]
857    fn test_parse_with_typed_literal() {
858        let input = r#"<< <http://example.org/alice> <http://example.org/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> >>"#;
859        let tt = TripleTerm::parse(input).expect("should parse");
860        if let Object::Literal(lit) = tt.object() {
861            assert_eq!(lit.value(), "30");
862        } else {
863            panic!("Expected literal object");
864        }
865    }
866
867    #[test]
868    fn test_parse_with_blank_node() {
869        let input = "<< _:b1 <http://example.org/type> <http://example.org/Person> >>";
870        let tt = TripleTerm::parse(input).expect("should parse");
871        assert!(tt.has_blank_subject());
872    }
873
874    #[test]
875    fn test_parse_with_variable() {
876        let input = "<< ?s <http://example.org/knows> ?o >>";
877        let tt = TripleTerm::parse(input).expect("should parse");
878        assert!(tt.has_variables());
879    }
880
881    #[test]
882    fn test_parse_error_no_delimiters() {
883        let result = TripleTerm::parse("not a triple term");
884        assert!(result.is_err());
885    }
886
887    #[test]
888    fn test_parse_error_empty() {
889        let result = TripleTerm::parse("<<  >>");
890        assert!(result.is_err());
891    }
892
893    #[test]
894    fn test_parse_error_too_few_components() {
895        let result = TripleTerm::parse("<< <http://example.org/a> <http://example.org/b> >>");
896        assert!(result.is_err());
897    }
898
899    #[test]
900    fn test_parse_roundtrip() {
901        let tt = make_tt("alice", "knows", "bob");
902        let s = tt.to_ntriples_string();
903        let parsed = TripleTerm::parse(&s).expect("should parse");
904        assert_eq!(parsed, tt);
905    }
906
907    // ── Equality and hashing ────────────────────────────────────────────────
908
909    #[test]
910    fn test_equality_same_components() {
911        let tt1 = make_tt("alice", "knows", "bob");
912        let tt2 = make_tt("alice", "knows", "bob");
913        assert_eq!(tt1, tt2);
914    }
915
916    #[test]
917    fn test_inequality_different_subject() {
918        let tt1 = make_tt("alice", "knows", "bob");
919        let tt2 = make_tt("carol", "knows", "bob");
920        assert_ne!(tt1, tt2);
921    }
922
923    #[test]
924    fn test_inequality_different_predicate() {
925        let tt1 = make_tt("alice", "knows", "bob");
926        let tt2 = make_tt("alice", "likes", "bob");
927        assert_ne!(tt1, tt2);
928    }
929
930    #[test]
931    fn test_inequality_different_object() {
932        let tt1 = make_tt("alice", "knows", "bob");
933        let tt2 = make_tt("alice", "knows", "carol");
934        assert_ne!(tt1, tt2);
935    }
936
937    #[test]
938    fn test_hash_consistency() {
939        let tt1 = make_tt("alice", "knows", "bob");
940        let tt2 = make_tt("alice", "knows", "bob");
941        assert_eq!(tt1.deterministic_hash(), tt2.deterministic_hash());
942    }
943
944    #[test]
945    fn test_hash_different_for_different_terms() {
946        let tt1 = make_tt("alice", "knows", "bob");
947        let tt2 = make_tt("alice", "knows", "carol");
948        // Different terms should (very likely) have different hashes
949        assert_ne!(tt1.deterministic_hash(), tt2.deterministic_hash());
950    }
951
952    #[test]
953    fn test_structural_equality() {
954        let tt1 = make_tt("alice", "knows", "bob");
955        let tt2 = make_tt("alice", "knows", "bob");
956        assert!(tt1.structurally_equal(&tt2));
957    }
958
959    #[test]
960    fn test_hash_in_hashset() {
961        use std::collections::HashSet;
962        let mut set = HashSet::new();
963        set.insert(make_tt("alice", "knows", "bob"));
964        set.insert(make_tt("alice", "knows", "bob")); // duplicate
965        set.insert(make_tt("alice", "knows", "carol"));
966        assert_eq!(set.len(), 2);
967    }
968
969    // ── Ordering ────────────────────────────────────────────────────────────
970
971    #[test]
972    fn test_ordering() {
973        let tt1 = make_tt("a", "b", "c");
974        let tt2 = make_tt("a", "b", "d");
975        assert!(tt1 < tt2);
976    }
977
978    #[test]
979    fn test_sort_triple_terms() {
980        let mut terms = [
981            make_tt("c", "p", "o"),
982            make_tt("a", "p", "o"),
983            make_tt("b", "p", "o"),
984        ];
985        terms.sort();
986        assert_eq!(terms[0], make_tt("a", "p", "o"));
987        assert_eq!(terms[1], make_tt("b", "p", "o"));
988        assert_eq!(terms[2], make_tt("c", "p", "o"));
989    }
990
991    // ── Builder methods ─────────────────────────────────────────────────────
992
993    #[test]
994    fn test_with_subject() {
995        let tt = make_tt("alice", "knows", "bob").with_subject(example_iri("carol"));
996        assert_eq!(tt.subject(), &Subject::NamedNode(example_iri("carol")));
997    }
998
999    #[test]
1000    fn test_with_predicate() {
1001        let tt = make_tt("alice", "knows", "bob").with_predicate(example_iri("likes"));
1002        assert_eq!(tt.predicate(), &Predicate::NamedNode(example_iri("likes")));
1003    }
1004
1005    #[test]
1006    fn test_with_object() {
1007        let tt = make_tt("alice", "knows", "bob").with_object(Literal::new("hello"));
1008        assert!(tt.has_literal_object());
1009    }
1010
1011    // ── TripleTermRef ───────────────────────────────────────────────────────
1012
1013    #[test]
1014    fn test_triple_term_ref() {
1015        let tt = make_tt("alice", "knows", "bob");
1016        let ttref = TripleTermRef::from_triple_term(&tt);
1017        assert_eq!(ttref.subject(), tt.subject());
1018        assert_eq!(ttref.predicate(), tt.predicate());
1019        assert_eq!(ttref.object(), tt.object());
1020    }
1021
1022    #[test]
1023    fn test_triple_term_ref_to_owned() {
1024        let tt = make_tt("alice", "knows", "bob");
1025        let ttref = TripleTermRef::from_triple_term(&tt);
1026        let owned = ttref.to_owned();
1027        assert_eq!(owned, tt);
1028    }
1029
1030    #[test]
1031    fn test_triple_term_ref_display() {
1032        let tt = make_tt("alice", "knows", "bob");
1033        let ttref = TripleTermRef::from_triple_term(&tt);
1034        let display = ttref.to_string();
1035        assert!(display.contains("alice"));
1036    }
1037
1038    // ── TripleTermSet ───────────────────────────────────────────────────────
1039
1040    #[test]
1041    fn test_set_insert_and_len() {
1042        let mut set = TripleTermSet::new();
1043        assert!(set.is_empty());
1044        assert!(set.insert(make_tt("alice", "knows", "bob")));
1045        assert_eq!(set.len(), 1);
1046        assert!(!set.is_empty());
1047    }
1048
1049    #[test]
1050    fn test_set_no_duplicates() {
1051        let mut set = TripleTermSet::new();
1052        assert!(set.insert(make_tt("alice", "knows", "bob")));
1053        assert!(!set.insert(make_tt("alice", "knows", "bob")));
1054        assert_eq!(set.len(), 1);
1055    }
1056
1057    #[test]
1058    fn test_set_contains() {
1059        let mut set = TripleTermSet::new();
1060        let tt = make_tt("alice", "knows", "bob");
1061        set.insert(tt.clone());
1062        assert!(set.contains(&tt));
1063        assert!(!set.contains(&make_tt("alice", "knows", "carol")));
1064    }
1065
1066    #[test]
1067    fn test_set_remove() {
1068        let mut set = TripleTermSet::new();
1069        let tt = make_tt("alice", "knows", "bob");
1070        set.insert(tt.clone());
1071        assert!(set.remove(&tt));
1072        assert!(set.is_empty());
1073        assert!(!set.remove(&tt)); // already removed
1074    }
1075
1076    #[test]
1077    fn test_set_filter_by_predicate() {
1078        let mut set = TripleTermSet::new();
1079        set.insert(make_tt("alice", "knows", "bob"));
1080        set.insert(make_tt("alice", "likes", "carol"));
1081        set.insert(make_tt("bob", "knows", "carol"));
1082
1083        let filtered = set.filter_by_predicate("http://example.org/knows");
1084        assert_eq!(filtered.len(), 2);
1085    }
1086
1087    #[test]
1088    fn test_set_filter_by_subject() {
1089        let mut set = TripleTermSet::new();
1090        set.insert(make_tt("alice", "knows", "bob"));
1091        set.insert(make_tt("alice", "likes", "carol"));
1092        set.insert(make_tt("bob", "knows", "carol"));
1093
1094        let filtered = set.filter_by_subject_iri("http://example.org/alice");
1095        assert_eq!(filtered.len(), 2);
1096    }
1097
1098    #[test]
1099    fn test_set_ground_terms() {
1100        let mut set = TripleTermSet::new();
1101        set.insert(make_tt("alice", "knows", "bob"));
1102        let var = Variable::new("x").expect("valid var");
1103        set.insert(TripleTerm::new(
1104            var,
1105            example_iri("knows"),
1106            example_iri("bob"),
1107        ));
1108
1109        let ground = set.ground_terms();
1110        assert_eq!(ground.len(), 1);
1111    }
1112
1113    #[test]
1114    fn test_set_to_triples() {
1115        let mut set = TripleTermSet::new();
1116        set.insert(make_tt("alice", "knows", "bob"));
1117        set.insert(make_tt("alice", "knows", "carol"));
1118        let triples = set.to_triples();
1119        assert_eq!(triples.len(), 2);
1120    }
1121
1122    #[test]
1123    fn test_set_from_iterator() {
1124        let terms = vec![
1125            make_tt("alice", "knows", "bob"),
1126            make_tt("alice", "knows", "bob"), // duplicate
1127            make_tt("alice", "knows", "carol"),
1128        ];
1129        let set: TripleTermSet = terms.into_iter().collect();
1130        assert_eq!(set.len(), 2);
1131    }
1132
1133    #[test]
1134    fn test_set_into_iterator() {
1135        let mut set = TripleTermSet::new();
1136        set.insert(make_tt("alice", "knows", "bob"));
1137        set.insert(make_tt("alice", "knows", "carol"));
1138        let collected: Vec<_> = set.into_iter().collect();
1139        assert_eq!(collected.len(), 2);
1140    }
1141
1142    #[test]
1143    fn test_set_drain() {
1144        let mut set = TripleTermSet::new();
1145        set.insert(make_tt("a", "b", "c"));
1146        set.insert(make_tt("d", "e", "f"));
1147        let drained: Vec<_> = set.drain().collect();
1148        assert_eq!(drained.len(), 2);
1149        assert!(set.is_empty());
1150    }
1151
1152    // ── RdfTerm trait ───────────────────────────────────────────────────────
1153
1154    #[test]
1155    fn test_rdf_term_as_str() {
1156        let tt = make_tt("alice", "knows", "bob");
1157        assert_eq!(tt.as_str(), "<<triple-term>>");
1158    }
1159
1160    #[test]
1161    fn test_rdf_term_is_quoted_triple() {
1162        let tt = make_tt("alice", "knows", "bob");
1163        assert!(tt.is_quoted_triple());
1164        assert!(!tt.is_named_node());
1165        assert!(!tt.is_blank_node());
1166        assert!(!tt.is_literal());
1167        assert!(!tt.is_variable());
1168    }
1169
1170    // ── iri_count ───────────────────────────────────────────────────────────
1171
1172    #[test]
1173    fn test_iri_count_all_iris() {
1174        let tt = make_tt("alice", "knows", "bob");
1175        assert_eq!(tt.iri_count(), 3);
1176    }
1177
1178    #[test]
1179    fn test_iri_count_with_literal() {
1180        let tt = TripleTerm::new(
1181            example_iri("alice"),
1182            example_iri("name"),
1183            Literal::new("Alice"),
1184        );
1185        assert_eq!(tt.iri_count(), 2);
1186    }
1187
1188    #[test]
1189    fn test_iri_count_with_blank_and_variable() {
1190        let bn = BlankNode::new("b1").expect("valid blank node");
1191        let var = Variable::new("x").expect("valid var");
1192        let tt = TripleTerm::new(bn, example_iri("type"), var);
1193        assert_eq!(tt.iri_count(), 1);
1194    }
1195
1196    // ── into_parts ──────────────────────────────────────────────────────────
1197
1198    #[test]
1199    fn test_into_parts() {
1200        let tt = make_tt("alice", "knows", "bob");
1201        let (s, p, o) = tt.into_parts();
1202        assert_eq!(s, Subject::NamedNode(example_iri("alice")));
1203        assert_eq!(p, Predicate::NamedNode(example_iri("knows")));
1204        assert_eq!(o, Object::NamedNode(example_iri("bob")));
1205    }
1206
1207    // ── Serde ───────────────────────────────────────────────────────────────
1208
1209    #[test]
1210    fn test_serde_roundtrip() {
1211        let tt = make_tt("alice", "knows", "bob");
1212        let json = serde_json::to_string(&tt).expect("serialize");
1213        let deserialized: TripleTerm = serde_json::from_str(&json).expect("deserialize");
1214        assert_eq!(deserialized, tt);
1215    }
1216
1217    #[test]
1218    fn test_serde_with_literal() {
1219        let tt = TripleTerm::new(
1220            example_iri("alice"),
1221            example_iri("name"),
1222            Literal::new("Alice"),
1223        );
1224        let json = serde_json::to_string(&tt).expect("serialize");
1225        let deserialized: TripleTerm = serde_json::from_str(&json).expect("deserialize");
1226        assert_eq!(deserialized, tt);
1227    }
1228
1229    // ── Clone and Debug ─────────────────────────────────────────────────────
1230
1231    #[test]
1232    fn test_clone() {
1233        let tt = make_tt("alice", "knows", "bob");
1234        let tt2 = tt.clone();
1235        assert_eq!(tt, tt2);
1236    }
1237
1238    #[test]
1239    fn test_debug_format() {
1240        let tt = make_tt("alice", "knows", "bob");
1241        let debug = format!("{:?}", tt);
1242        assert!(debug.contains("TripleTerm"));
1243    }
1244
1245    #[test]
1246    fn test_with_capacity() {
1247        let set = TripleTermSet::with_capacity(100);
1248        assert!(set.is_empty());
1249        assert_eq!(set.len(), 0);
1250    }
1251}