Skip to main content

oxirs_core/model/
term.rs

1//! Core RDF term types and implementations
2
3use crate::model::{
4    Literal, LiteralRef, NamedNode, NamedNodeRef, ObjectTerm, PredicateTerm, RdfTerm, SubjectTerm,
5};
6use crate::OxirsError;
7use once_cell::sync::Lazy;
8// use rand::random; // Replaced with fastrand
9use regex::Regex;
10// use serde::{Deserialize, Serialize}; // Available for serialization features
11use std::fmt;
12use std::hash::Hash;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15/// Parses a string as a hexadecimal u128 ID (similar to OxiGraph's optimization)
16/// Check if an ID is a pure hex numeric ID that should be optimized
17/// This should only return true for IDs that are clearly system-generated hex numbers,
18/// not user-provided identifiers like "b1", "a2b", etc.
19fn is_pure_hex_numeric_id(id: &str) -> bool {
20    // Only consider optimization for IDs that:
21    // 1. Are pure hex digits (lowercase letters and digits) to avoid mixed case user input
22    // 2. Start with 'a'-'f' (as per the unique generation pattern)
23    // 3. Are not obvious user patterns like single letters or very short common patterns
24    !id.is_empty()
25        && id
26            .chars()
27            .all(|c| c.is_ascii_hexdigit() && (c.is_ascii_lowercase() || c.is_ascii_digit()))
28        && id.starts_with(|c: char| ('a'..='f').contains(&c))
29        && id.len() >= 3 // Avoid very short patterns like "a", "ab", etc.
30}
31
32fn to_integer_id(id: &str) -> Option<u128> {
33    let digits = id.as_bytes();
34    let mut value: u128 = 0;
35    if let None | Some(b'0') = digits.first() {
36        return None; // No empty string or leading zeros
37    }
38    for digit in digits {
39        value = value.checked_mul(16)?.checked_add(
40            match *digit {
41                b'0'..=b'9' => digit - b'0',
42                b'a'..=b'f' => digit - b'a' + 10,
43                _ => return None,
44            }
45            .into(),
46        )?;
47    }
48    Some(value)
49}
50
51/// Regex for validating blank node IDs according to Turtle/N-Triples specification
52static BLANK_NODE_REGEX: Lazy<Regex> = Lazy::new(|| {
53    Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_.-]*$").expect("Blank node regex compilation failed")
54});
55
56/// Regex for validating SPARQL variable names according to SPARQL 1.1 specification
57static VARIABLE_REGEX: Lazy<Regex> = Lazy::new(|| {
58    Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*$").expect("Variable regex compilation failed")
59});
60
61/// Global counter for unique blank node generation
62static BLANK_NODE_COUNTER: AtomicU64 = AtomicU64::new(0);
63
64/// Validates a blank node identifier according to RDF specifications
65fn validate_blank_node_id(id: &str) -> Result<(), OxirsError> {
66    if id.is_empty() {
67        return Err(OxirsError::Parse(
68            "Blank node ID cannot be empty".to_string(),
69        ));
70    }
71
72    // Remove _: prefix if present for validation
73    let clean_id = if let Some(stripped) = id.strip_prefix("_:") {
74        stripped
75    } else {
76        id
77    };
78
79    if clean_id.is_empty() {
80        return Err(OxirsError::Parse(
81            "Blank node ID cannot be just '_:'".to_string(),
82        ));
83    }
84
85    if !BLANK_NODE_REGEX.is_match(clean_id) {
86        return Err(OxirsError::Parse(format!(
87            "Invalid blank node ID format: '{clean_id}'. Must match [a-zA-Z0-9_][a-zA-Z0-9_.-]*"
88        )));
89    }
90
91    Ok(())
92}
93
94/// Validates a SPARQL variable name according to SPARQL 1.1 specification
95fn validate_variable_name(name: &str) -> Result<(), OxirsError> {
96    if name.is_empty() {
97        return Err(OxirsError::Parse(
98            "Variable name cannot be empty".to_string(),
99        ));
100    }
101
102    // Remove ? or $ prefix if present for validation
103    let clean_name = if name.starts_with('?') || name.starts_with('$') {
104        &name[1..]
105    } else {
106        name
107    };
108
109    if clean_name.is_empty() {
110        return Err(OxirsError::Parse(
111            "Variable name cannot be just '?' or '$'".to_string(),
112        ));
113    }
114
115    if !VARIABLE_REGEX.is_match(clean_name) {
116        return Err(OxirsError::Parse(format!(
117            "Invalid variable name format: '{clean_name}'. Must match [a-zA-Z_][a-zA-Z0-9_]*"
118        )));
119    }
120
121    // Check for reserved keywords
122    match clean_name.to_lowercase().as_str() {
123        "select" | "where" | "from" | "order" | "group" | "having" | "limit" | "offset"
124        | "distinct" | "reduced" | "construct" | "describe" | "ask" | "union" | "optional"
125        | "filter" | "bind" | "values" | "graph" | "service" | "minus" | "exists" | "not" => {
126            return Err(OxirsError::Parse(format!(
127                "Variable name '{clean_name}' is a reserved SPARQL keyword"
128            )));
129        }
130        _ => {}
131    }
132
133    Ok(())
134}
135
136/// A blank node identifier
137///
138/// Blank nodes are local identifiers used in RDF graphs that don't have global meaning.
139/// Supports both named identifiers and efficient numerical IDs.
140#[derive(
141    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
142)]
143pub struct BlankNode {
144    content: BlankNodeContent,
145}
146
147#[derive(
148    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
149)]
150enum BlankNodeContent {
151    Named(String),
152    Anonymous { id: u128, str: String },
153}
154
155impl Default for BlankNode {
156    /// Builds a new RDF blank node with a unique id.
157    /// Ensures the ID does not start with a number to be also valid with RDF/XML
158    fn default() -> Self {
159        loop {
160            let id: u128 = fastrand::u128(..);
161            let str = format!("{id:x}"); // Use hexadecimal format instead of decimal
162            if matches!(str.as_bytes().first(), Some(b'a'..=b'f')) {
163                return Self {
164                    content: BlankNodeContent::Anonymous { id, str },
165                };
166            }
167        }
168    }
169}
170
171impl BlankNode {
172    /// Creates a new blank node with the given identifier
173    ///
174    /// # Arguments
175    /// * `id` - The blank node identifier (with or without _: prefix)
176    ///
177    /// # Errors
178    /// Returns an error if the ID format is invalid according to RDF specifications
179    pub fn new(id: impl Into<String>) -> Result<Self, OxirsError> {
180        let id = id.into();
181        // Remove _: prefix for validation if present
182        let clean_id = if let Some(stripped) = id.strip_prefix("_:") {
183            stripped
184        } else {
185            &id
186        };
187        validate_blank_node_id(clean_id)?;
188
189        // Only optimize IDs that are pure hex numbers (no mixed letters/numbers)
190        // to avoid incorrectly converting user IDs like "b1"
191        if is_pure_hex_numeric_id(clean_id) {
192            if let Some(numerical_id) = to_integer_id(clean_id) {
193                // For hex-optimized IDs, preserve the original hex string as the display representation
194                Ok(BlankNode {
195                    content: BlankNodeContent::Anonymous {
196                        id: numerical_id,
197                        str: clean_id.to_string(),
198                    },
199                })
200            } else {
201                Ok(BlankNode {
202                    content: BlankNodeContent::Named(id),
203                })
204            }
205        } else {
206            Ok(BlankNode {
207                content: BlankNodeContent::Named(id),
208            })
209        }
210    }
211
212    /// Creates a new blank node without validation
213    ///
214    /// # Safety
215    /// The caller must ensure the ID is valid and properly formatted
216    pub fn new_unchecked(id: impl Into<String>) -> Self {
217        let id = id.into();
218        // Remove _: prefix if present
219        let clean_id = if let Some(stripped) = id.strip_prefix("_:") {
220            stripped
221        } else {
222            &id
223        };
224        if let Some(numerical_id) = to_integer_id(clean_id) {
225            Self::new_from_unique_id(numerical_id)
226        } else {
227            BlankNode {
228                content: BlankNodeContent::Named(id),
229            }
230        }
231    }
232
233    /// Creates a blank node from a unique numerical id.
234    ///
235    /// In most cases, it is much more convenient to create a blank node using [`BlankNode::default()`].
236    pub fn new_from_unique_id(id: u128) -> Self {
237        Self {
238            content: BlankNodeContent::Anonymous {
239                id,
240                str: format!("{id:x}"), // Use hex representation for consistency
241            },
242        }
243    }
244
245    /// Generates a new unique blank node with collision detection
246    ///
247    /// This method ensures global uniqueness across all threads and sessions
248    pub fn new_unique() -> Self {
249        Self::default()
250    }
251
252    /// Generates a new unique blank node with a custom prefix
253    pub fn new_unique_with_prefix(prefix: &str) -> Result<Self, OxirsError> {
254        // Validate prefix
255        if !BLANK_NODE_REGEX.is_match(prefix) {
256            return Err(OxirsError::Parse(format!(
257                "Invalid blank node prefix: '{prefix}'. Must match [a-zA-Z0-9_][a-zA-Z0-9_.-]*"
258            )));
259        }
260
261        let counter = BLANK_NODE_COUNTER.fetch_add(1, Ordering::SeqCst);
262        let id = format!("{prefix}_{counter}");
263        Ok(BlankNode {
264            content: BlankNodeContent::Named(id),
265        })
266    }
267
268    /// Returns the blank node identifier
269    pub fn id(&self) -> &str {
270        match &self.content {
271            BlankNodeContent::Named(id) => id,
272            BlankNodeContent::Anonymous { str, .. } => str,
273        }
274    }
275
276    /// Returns the blank node identifier as a string slice
277    pub fn as_str(&self) -> &str {
278        self.id()
279    }
280
281    /// Returns the internal numerical ID of this blank node if it has been created using [`BlankNode::new_from_unique_id`] or [`BlankNode::default`].
282    pub fn unique_id(&self) -> Option<u128> {
283        match &self.content {
284            BlankNodeContent::Named(_) => None,
285            BlankNodeContent::Anonymous { id, .. } => Some(*id),
286        }
287    }
288
289    /// Returns the blank node identifier without the _: prefix
290    pub fn local_id(&self) -> &str {
291        let id = self.id();
292        if let Some(stripped) = id.strip_prefix("_:") {
293            stripped
294        } else {
295            id
296        }
297    }
298
299    /// Returns a reference to this BlankNode as a BlankNodeRef
300    pub fn as_ref(&self) -> BlankNodeRef<'_> {
301        BlankNodeRef {
302            content: match &self.content {
303                BlankNodeContent::Named(id) => BlankNodeRefContent::Named(id.as_str()),
304                BlankNodeContent::Anonymous { id, str } => BlankNodeRefContent::Anonymous {
305                    id: *id,
306                    str: str.as_str(),
307                },
308            },
309        }
310    }
311}
312
313impl fmt::Display for BlankNode {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        write!(f, "_:{}", self.local_id())
316    }
317}
318
319impl RdfTerm for BlankNode {
320    fn as_str(&self) -> &str {
321        self.id()
322    }
323
324    fn is_blank_node(&self) -> bool {
325        true
326    }
327}
328
329impl SubjectTerm for BlankNode {}
330impl ObjectTerm for BlankNode {}
331
332/// A borrowed blank node reference for zero-copy operations
333///
334/// This is an optimized version for temporary references that avoids allocations
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
336pub struct BlankNodeRef<'a> {
337    content: BlankNodeRefContent<'a>,
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
341enum BlankNodeRefContent<'a> {
342    Named(&'a str),
343    Anonymous { id: u128, str: &'a str },
344}
345
346impl<'a> BlankNodeRef<'a> {
347    /// Creates a new blank node reference with the given identifier
348    ///
349    /// # Arguments
350    /// * `id` - The blank node identifier (with or without _: prefix)
351    ///
352    /// # Errors
353    /// Returns an error if the ID format is invalid according to RDF specifications
354    pub fn new(id: &'a str) -> Result<Self, OxirsError> {
355        // Remove _: prefix for validation if present
356        let clean_id = if let Some(stripped) = id.strip_prefix("_:") {
357            stripped
358        } else {
359            id
360        };
361        validate_blank_node_id(clean_id)?;
362
363        // Check if it's a hex numeric ID that can be optimized
364        if let Some(numerical_id) = to_integer_id(clean_id) {
365            Ok(BlankNodeRef {
366                content: BlankNodeRefContent::Anonymous {
367                    id: numerical_id,
368                    str: clean_id,
369                },
370            })
371        } else {
372            Ok(BlankNodeRef {
373                content: BlankNodeRefContent::Named(id),
374            })
375        }
376    }
377
378    /// Creates a new blank node reference without validation
379    ///
380    /// # Safety
381    /// The caller must ensure the ID is valid and properly formatted
382    pub fn new_unchecked(id: &'a str) -> Self {
383        // Remove _: prefix if present
384        let clean_id = if let Some(stripped) = id.strip_prefix("_:") {
385            stripped
386        } else {
387            id
388        };
389        if let Some(numerical_id) = to_integer_id(clean_id) {
390            BlankNodeRef {
391                content: BlankNodeRefContent::Anonymous {
392                    id: numerical_id,
393                    str: clean_id,
394                },
395            }
396        } else {
397            BlankNodeRef {
398                content: BlankNodeRefContent::Named(id),
399            }
400        }
401    }
402
403    /// Returns the blank node identifier
404    pub fn id(&self) -> &str {
405        match &self.content {
406            BlankNodeRefContent::Named(id) => id,
407            BlankNodeRefContent::Anonymous { str, .. } => str,
408        }
409    }
410
411    /// Returns the blank node identifier as a string slice
412    pub fn as_str(&self) -> &str {
413        self.id()
414    }
415
416    /// Returns the internal numerical ID of this blank node if it has been created using numerical ID optimization
417    pub fn unique_id(&self) -> Option<u128> {
418        match &self.content {
419            BlankNodeRefContent::Named(_) => None,
420            BlankNodeRefContent::Anonymous { id, .. } => Some(*id),
421        }
422    }
423
424    /// Returns the blank node identifier without the _: prefix
425    pub fn local_id(&self) -> &str {
426        let id = self.id();
427        if let Some(stripped) = id.strip_prefix("_:") {
428            stripped
429        } else {
430            id
431        }
432    }
433
434    /// Converts to an owned BlankNode
435    pub fn to_owned(&self) -> BlankNode {
436        BlankNode::new_unchecked(self.id().to_string())
437    }
438}
439
440impl<'a> fmt::Display for BlankNodeRef<'a> {
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        write!(f, "_:{}", self.local_id())
443    }
444}
445
446impl<'a> RdfTerm for BlankNodeRef<'a> {
447    fn as_str(&self) -> &str {
448        self.id()
449    }
450
451    fn is_blank_node(&self) -> bool {
452        true
453    }
454}
455
456impl<'a> SubjectTerm for BlankNodeRef<'a> {}
457impl<'a> ObjectTerm for BlankNodeRef<'a> {}
458
459impl<'a> From<BlankNodeRef<'a>> for BlankNode {
460    fn from(node_ref: BlankNodeRef<'a>) -> Self {
461        node_ref.to_owned()
462    }
463}
464
465impl<'a> From<&'a BlankNode> for BlankNodeRef<'a> {
466    fn from(node: &'a BlankNode) -> Self {
467        node.as_ref()
468    }
469}
470
471/// A SPARQL variable
472///
473/// Variables are used in SPARQL queries and updates to represent unknown values.
474#[derive(
475    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
476)]
477pub struct Variable {
478    name: String,
479}
480
481impl Variable {
482    /// Creates a new variable with the given name
483    ///
484    /// # Arguments  
485    /// * `name` - The variable name (with or without ? or $ prefix)
486    ///
487    /// # Errors
488    /// Returns an error if the name format is invalid according to SPARQL 1.1 specification
489    pub fn new(name: impl Into<String>) -> Result<Self, OxirsError> {
490        let name = name.into();
491        validate_variable_name(&name)?;
492
493        // Store name without prefix for consistency
494        let clean_name = if let Some(stripped) = name.strip_prefix('?') {
495            stripped.to_string()
496        } else if let Some(stripped) = name.strip_prefix('$') {
497            stripped.to_string()
498        } else {
499            name
500        };
501
502        Ok(Variable { name: clean_name })
503    }
504
505    /// Creates a new variable without validation
506    ///
507    /// # Safety
508    /// The caller must ensure the name is valid according to SPARQL rules
509    pub fn new_unchecked(name: impl Into<String>) -> Self {
510        Variable { name: name.into() }
511    }
512
513    /// Returns the variable name (without prefix)
514    pub fn name(&self) -> &str {
515        &self.name
516    }
517
518    /// Returns the variable name as a string slice (without prefix)
519    pub fn as_str(&self) -> &str {
520        &self.name
521    }
522
523    /// Returns the variable name with ? prefix for SPARQL syntax
524    pub fn with_prefix(&self) -> String {
525        format!("?{}", self.name)
526    }
527}
528
529impl fmt::Display for Variable {
530    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
531        write!(f, "?{}", self.name)
532    }
533}
534
535impl Variable {
536    /// Formats using the SPARQL S-Expression syntax
537    pub fn fmt_sse(&self, f: &mut impl fmt::Write) -> fmt::Result {
538        write!(f, "?{}", self.name)
539    }
540}
541
542impl RdfTerm for Variable {
543    fn as_str(&self) -> &str {
544        &self.name
545    }
546
547    fn is_variable(&self) -> bool {
548        true
549    }
550}
551
552/// Union type for all RDF terms
553///
554/// This enum can hold any type of RDF term and is used when the specific
555/// type is not known at compile time. It supports the full RDF 1.2 specification
556/// including RDF-star quoted triples.
557///
558/// # Examples
559///
560/// ```rust
561/// use oxirs_core::model::{Term, NamedNode, Literal, BlankNode, Variable};
562///
563/// // Create different types of terms
564/// let named_node = Term::NamedNode(NamedNode::new("http://example.org/resource").expect("valid IRI"));
565/// let literal = Term::Literal(Literal::new("Hello World"));
566/// let blank_node = Term::BlankNode(BlankNode::new("b1").expect("valid blank node id"));
567/// let variable = Term::Variable(Variable::new("x").expect("valid variable name"));
568///
569/// // Check term types
570/// assert!(named_node.is_named_node());
571/// assert!(literal.is_literal());
572/// assert!(blank_node.is_blank_node());
573/// assert!(variable.is_variable());
574/// ```
575///
576/// # Variants
577///
578/// - [`NamedNode`]: An IRI reference (e.g., `<http://example.org/resource>`)
579/// - [`BlankNode`]: An anonymous node (e.g., `_:b1`)
580/// - [`Literal`]: A literal value with optional datatype and language tag
581/// - [`Variable`]: A query variable (e.g., `?x`)
582/// - `QuotedTriple`: A quoted triple for RDF-star support
583#[derive(
584    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
585)]
586pub enum Term {
587    /// A named node (IRI reference)
588    ///
589    /// Represents a resource identified by an IRI according to RFC 3987.
590    /// Used for subjects, predicates, and objects in RDF triples.
591    NamedNode(NamedNode),
592
593    /// A blank node (anonymous resource)
594    ///
595    /// Represents an anonymous resource that can be used as a subject or object
596    /// but not as a predicate. Blank nodes have local scope within a graph.
597    BlankNode(BlankNode),
598
599    /// A literal value
600    ///
601    /// Represents a data value with an optional datatype and language tag.
602    /// Can only be used as objects in RDF triples.
603    Literal(Literal),
604
605    /// A query variable
606    ///
607    /// Represents a variable in SPARQL queries or graph patterns.
608    /// Variables are prefixed with '?' or '$' in SPARQL syntax.
609    Variable(Variable),
610
611    /// A quoted triple (RDF-star)
612    ///
613    /// Represents a triple that can itself be used as a subject or object
614    /// in another triple, enabling statement-level metadata.
615    QuotedTriple(Box<crate::model::star::QuotedTriple>),
616}
617
618impl Term {
619    /// Returns `true` if this term is a named node (IRI reference)
620    ///
621    /// # Examples
622    ///
623    /// ```rust
624    /// use oxirs_core::model::{Term, NamedNode, Literal};
625    ///
626    /// let named_node = Term::NamedNode(NamedNode::new("http://example.org/resource").expect("valid IRI"));
627    /// let literal = Term::Literal(Literal::new("Hello"));
628    ///
629    /// assert!(named_node.is_named_node());
630    /// assert!(!literal.is_named_node());
631    /// ```
632    pub fn is_named_node(&self) -> bool {
633        matches!(self, Term::NamedNode(_))
634    }
635
636    /// Returns `true` if this term is a blank node
637    ///
638    /// # Examples
639    ///
640    /// ```rust
641    /// use oxirs_core::model::{Term, BlankNode, Literal};
642    ///
643    /// let blank_node = Term::BlankNode(BlankNode::new("b1").expect("valid blank node id"));
644    /// let literal = Term::Literal(Literal::new("Hello"));
645    ///
646    /// assert!(blank_node.is_blank_node());
647    /// assert!(!literal.is_blank_node());
648    /// ```
649    pub fn is_blank_node(&self) -> bool {
650        matches!(self, Term::BlankNode(_))
651    }
652
653    /// Returns `true` if this term is a literal value
654    ///
655    /// # Examples
656    ///
657    /// ```rust
658    /// use oxirs_core::model::{Term, NamedNode, Literal};
659    ///
660    /// let literal = Term::Literal(Literal::new("Hello"));
661    /// let named_node = Term::NamedNode(NamedNode::new("http://example.org/resource").expect("valid IRI"));
662    ///
663    /// assert!(literal.is_literal());
664    /// assert!(!named_node.is_literal());
665    /// ```
666    pub fn is_literal(&self) -> bool {
667        matches!(self, Term::Literal(_))
668    }
669
670    /// Returns `true` if this term is a query variable
671    ///
672    /// # Examples
673    ///
674    /// ```rust
675    /// use oxirs_core::model::{Term, Variable, Literal};
676    ///
677    /// let variable = Term::Variable(Variable::new("x").expect("valid variable name"));
678    /// let literal = Term::Literal(Literal::new("Hello"));
679    ///
680    /// assert!(variable.is_variable());
681    /// assert!(!literal.is_variable());
682    /// ```
683    pub fn is_variable(&self) -> bool {
684        matches!(self, Term::Variable(_))
685    }
686
687    /// Returns `true` if this term is a quoted triple (RDF-star)
688    ///
689    /// # Examples
690    ///
691    /// ```rust
692    /// use oxirs_core::model::{Term, Literal};
693    ///
694    /// let literal = Term::Literal(Literal::new("Hello"));
695    ///
696    /// assert!(!literal.is_quoted_triple());
697    /// // Note: Creating QuotedTriple examples requires more complex setup
698    /// ```
699    pub fn is_quoted_triple(&self) -> bool {
700        matches!(self, Term::QuotedTriple(_))
701    }
702
703    /// Returns the named node if this term is a named node
704    pub fn as_named_node(&self) -> Option<&NamedNode> {
705        match self {
706            Term::NamedNode(n) => Some(n),
707            _ => None,
708        }
709    }
710
711    /// Returns the blank node if this term is a blank node
712    pub fn as_blank_node(&self) -> Option<&BlankNode> {
713        match self {
714            Term::BlankNode(b) => Some(b),
715            _ => None,
716        }
717    }
718
719    /// Returns the literal if this term is a literal
720    pub fn as_literal(&self) -> Option<&Literal> {
721        match self {
722            Term::Literal(l) => Some(l),
723            _ => None,
724        }
725    }
726
727    /// Returns the variable if this term is a variable
728    pub fn as_variable(&self) -> Option<&Variable> {
729        match self {
730            Term::Variable(v) => Some(v),
731            _ => None,
732        }
733    }
734
735    /// Returns the quoted triple if this term is a quoted triple
736    pub fn as_quoted_triple(&self) -> Option<&crate::model::star::QuotedTriple> {
737        match self {
738            Term::QuotedTriple(qt) => Some(qt),
739            _ => None,
740        }
741    }
742
743    /// Convert a Subject to a Term
744    pub fn from_subject(subject: &crate::model::Subject) -> Term {
745        match subject {
746            crate::model::Subject::NamedNode(n) => Term::NamedNode(n.clone()),
747            crate::model::Subject::BlankNode(b) => Term::BlankNode(b.clone()),
748            crate::model::Subject::Variable(v) => Term::Variable(v.clone()),
749            crate::model::Subject::QuotedTriple(qt) => Term::QuotedTriple(qt.clone()),
750        }
751    }
752
753    /// Convert a Predicate to a Term
754    pub fn from_predicate(predicate: &crate::model::Predicate) -> Term {
755        match predicate {
756            crate::model::Predicate::NamedNode(n) => Term::NamedNode(n.clone()),
757            crate::model::Predicate::Variable(v) => Term::Variable(v.clone()),
758        }
759    }
760
761    /// Convert an Object to a Term
762    pub fn from_object(object: &crate::model::Object) -> Term {
763        match object {
764            crate::model::Object::NamedNode(n) => Term::NamedNode(n.clone()),
765            crate::model::Object::BlankNode(b) => Term::BlankNode(b.clone()),
766            crate::model::Object::Literal(l) => Term::Literal(l.clone()),
767            crate::model::Object::Variable(v) => Term::Variable(v.clone()),
768            crate::model::Object::QuotedTriple(qt) => Term::QuotedTriple(qt.clone()),
769        }
770    }
771}
772
773impl fmt::Display for Term {
774    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
775        match self {
776            Term::NamedNode(n) => write!(f, "{n}"),
777            Term::BlankNode(b) => write!(f, "{b}"),
778            Term::Literal(l) => write!(f, "{l}"),
779            Term::Variable(v) => write!(f, "{v}"),
780            Term::QuotedTriple(qt) => write!(f, "{qt}"),
781        }
782    }
783}
784
785impl RdfTerm for Term {
786    fn as_str(&self) -> &str {
787        match self {
788            Term::NamedNode(n) => n.as_str(),
789            Term::BlankNode(b) => b.as_str(),
790            Term::Literal(l) => l.as_str(),
791            Term::Variable(v) => v.as_str(),
792            Term::QuotedTriple(_) => "<<quoted-triple>>",
793        }
794    }
795
796    fn is_named_node(&self) -> bool {
797        self.is_named_node()
798    }
799
800    fn is_blank_node(&self) -> bool {
801        self.is_blank_node()
802    }
803
804    fn is_literal(&self) -> bool {
805        self.is_literal()
806    }
807
808    fn is_variable(&self) -> bool {
809        self.is_variable()
810    }
811
812    fn is_quoted_triple(&self) -> bool {
813        self.is_quoted_triple()
814    }
815}
816
817// Conversion implementations
818impl From<NamedNode> for Term {
819    fn from(node: NamedNode) -> Self {
820        Term::NamedNode(node)
821    }
822}
823
824impl From<BlankNode> for Term {
825    fn from(node: BlankNode) -> Self {
826        Term::BlankNode(node)
827    }
828}
829
830impl From<Literal> for Term {
831    fn from(literal: Literal) -> Self {
832        Term::Literal(literal)
833    }
834}
835
836impl From<Variable> for Term {
837    fn from(variable: Variable) -> Self {
838        Term::Variable(variable)
839    }
840}
841
842// Conversion implementations for union types
843impl From<Subject> for Term {
844    fn from(subject: Subject) -> Self {
845        match subject {
846            Subject::NamedNode(nn) => Term::NamedNode(nn),
847            Subject::BlankNode(bn) => Term::BlankNode(bn),
848            Subject::Variable(v) => Term::Variable(v),
849            Subject::QuotedTriple(qt) => Term::QuotedTriple(qt),
850        }
851    }
852}
853
854impl From<Predicate> for Term {
855    fn from(predicate: Predicate) -> Self {
856        match predicate {
857            Predicate::NamedNode(nn) => Term::NamedNode(nn),
858            Predicate::Variable(v) => Term::Variable(v),
859        }
860    }
861}
862
863impl From<Object> for Term {
864    fn from(object: Object) -> Self {
865        match object {
866            Object::NamedNode(nn) => Term::NamedNode(nn),
867            Object::BlankNode(bn) => Term::BlankNode(bn),
868            Object::Literal(l) => Term::Literal(l),
869            Object::Variable(v) => Term::Variable(v),
870            Object::QuotedTriple(qt) => Term::QuotedTriple(qt),
871        }
872    }
873}
874
875/// Union type for terms that can be subjects in RDF triples
876#[derive(
877    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
878)]
879pub enum Subject {
880    NamedNode(NamedNode),
881    BlankNode(BlankNode),
882    Variable(Variable),
883    QuotedTriple(Box<crate::model::star::QuotedTriple>),
884}
885
886impl From<NamedNode> for Subject {
887    fn from(node: NamedNode) -> Self {
888        Subject::NamedNode(node)
889    }
890}
891
892impl From<BlankNode> for Subject {
893    fn from(node: BlankNode) -> Self {
894        Subject::BlankNode(node)
895    }
896}
897
898impl From<Variable> for Subject {
899    fn from(variable: Variable) -> Self {
900        Subject::Variable(variable)
901    }
902}
903
904impl RdfTerm for Subject {
905    fn as_str(&self) -> &str {
906        match self {
907            Subject::NamedNode(n) => n.as_str(),
908            Subject::BlankNode(b) => b.as_str(),
909            Subject::Variable(v) => v.as_str(),
910            Subject::QuotedTriple(_) => "<<quoted-triple>>",
911        }
912    }
913
914    fn is_named_node(&self) -> bool {
915        matches!(self, Subject::NamedNode(_))
916    }
917
918    fn is_blank_node(&self) -> bool {
919        matches!(self, Subject::BlankNode(_))
920    }
921
922    fn is_variable(&self) -> bool {
923        matches!(self, Subject::Variable(_))
924    }
925
926    fn is_quoted_triple(&self) -> bool {
927        matches!(self, Subject::QuotedTriple(_))
928    }
929}
930
931impl SubjectTerm for Subject {}
932
933/// Union type for terms that can be predicates in RDF triples
934#[derive(
935    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
936)]
937pub enum Predicate {
938    NamedNode(NamedNode),
939    Variable(Variable),
940}
941
942impl From<NamedNode> for Predicate {
943    fn from(node: NamedNode) -> Self {
944        Predicate::NamedNode(node)
945    }
946}
947
948impl From<Variable> for Predicate {
949    fn from(variable: Variable) -> Self {
950        Predicate::Variable(variable)
951    }
952}
953
954impl RdfTerm for Predicate {
955    fn as_str(&self) -> &str {
956        match self {
957            Predicate::NamedNode(n) => n.as_str(),
958            Predicate::Variable(v) => v.as_str(),
959        }
960    }
961
962    fn is_named_node(&self) -> bool {
963        matches!(self, Predicate::NamedNode(_))
964    }
965
966    fn is_variable(&self) -> bool {
967        matches!(self, Predicate::Variable(_))
968    }
969}
970
971impl PredicateTerm for Predicate {}
972
973/// Union type for terms that can be objects in RDF triples
974#[derive(
975    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
976)]
977pub enum Object {
978    NamedNode(NamedNode),
979    BlankNode(BlankNode),
980    Literal(Literal),
981    Variable(Variable),
982    QuotedTriple(Box<crate::model::star::QuotedTriple>),
983}
984
985impl From<NamedNode> for Object {
986    fn from(node: NamedNode) -> Self {
987        Object::NamedNode(node)
988    }
989}
990
991impl From<BlankNode> for Object {
992    fn from(node: BlankNode) -> Self {
993        Object::BlankNode(node)
994    }
995}
996
997impl From<Literal> for Object {
998    fn from(literal: Literal) -> Self {
999        Object::Literal(literal)
1000    }
1001}
1002
1003impl From<Variable> for Object {
1004    fn from(variable: Variable) -> Self {
1005        Object::Variable(variable)
1006    }
1007}
1008
1009// Conversion from Subject to Object
1010impl From<Subject> for Object {
1011    fn from(subject: Subject) -> Self {
1012        match subject {
1013            Subject::NamedNode(n) => Object::NamedNode(n),
1014            Subject::BlankNode(b) => Object::BlankNode(b),
1015            Subject::Variable(v) => Object::Variable(v),
1016            Subject::QuotedTriple(qt) => Object::QuotedTriple(qt),
1017        }
1018    }
1019}
1020
1021// Conversion from Predicate to Object
1022impl From<Predicate> for Object {
1023    fn from(predicate: Predicate) -> Self {
1024        match predicate {
1025            Predicate::NamedNode(n) => Object::NamedNode(n),
1026            Predicate::Variable(v) => Object::Variable(v),
1027        }
1028    }
1029}
1030
1031// Conversion from Term to Object
1032impl From<Term> for Object {
1033    fn from(term: Term) -> Self {
1034        match term {
1035            Term::NamedNode(n) => Object::NamedNode(n),
1036            Term::BlankNode(b) => Object::BlankNode(b),
1037            Term::Literal(l) => Object::Literal(l),
1038            Term::Variable(v) => Object::Variable(v),
1039            Term::QuotedTriple(qt) => Object::QuotedTriple(qt),
1040        }
1041    }
1042}
1043
1044impl RdfTerm for Object {
1045    fn as_str(&self) -> &str {
1046        match self {
1047            Object::NamedNode(n) => n.as_str(),
1048            Object::BlankNode(b) => b.as_str(),
1049            Object::Literal(l) => l.as_str(),
1050            Object::Variable(v) => v.as_str(),
1051            Object::QuotedTriple(_) => "<<quoted-triple>>",
1052        }
1053    }
1054
1055    fn is_named_node(&self) -> bool {
1056        matches!(self, Object::NamedNode(_))
1057    }
1058
1059    fn is_blank_node(&self) -> bool {
1060        matches!(self, Object::BlankNode(_))
1061    }
1062
1063    fn is_literal(&self) -> bool {
1064        matches!(self, Object::Literal(_))
1065    }
1066
1067    fn is_variable(&self) -> bool {
1068        matches!(self, Object::Variable(_))
1069    }
1070
1071    fn is_quoted_triple(&self) -> bool {
1072        matches!(self, Object::QuotedTriple(_))
1073    }
1074}
1075
1076impl ObjectTerm for Object {}
1077
1078// Term to position conversions (needed for rdfxml parser)
1079impl TryFrom<Term> for Subject {
1080    type Error = OxirsError;
1081
1082    fn try_from(term: Term) -> Result<Self, Self::Error> {
1083        match term {
1084            Term::NamedNode(n) => Ok(Subject::NamedNode(n)),
1085            Term::BlankNode(b) => Ok(Subject::BlankNode(b)),
1086            Term::Variable(v) => Ok(Subject::Variable(v)),
1087            Term::QuotedTriple(qt) => Ok(Subject::QuotedTriple(qt)),
1088            Term::Literal(_) => Err(OxirsError::Parse(
1089                "Literals cannot be used as subjects".to_string(),
1090            )),
1091        }
1092    }
1093}
1094
1095impl TryFrom<Term> for Predicate {
1096    type Error = OxirsError;
1097
1098    fn try_from(term: Term) -> Result<Self, Self::Error> {
1099        match term {
1100            Term::NamedNode(n) => Ok(Predicate::NamedNode(n)),
1101            Term::Variable(v) => Ok(Predicate::Variable(v)),
1102            Term::BlankNode(_) => Err(OxirsError::Parse(
1103                "Blank nodes cannot be used as predicates".to_string(),
1104            )),
1105            Term::Literal(_) => Err(OxirsError::Parse(
1106                "Literals cannot be used as predicates".to_string(),
1107            )),
1108            Term::QuotedTriple(_) => Err(OxirsError::Parse(
1109                "Quoted triples cannot be used as predicates".to_string(),
1110            )),
1111        }
1112    }
1113}
1114
1115// Note: We already have From<Term> for Object above, so TryFrom is not needed
1116// The From implementation handles all cases infallibly
1117
1118/// A reference to an RDF term
1119///
1120/// This enum provides a lightweight way to work with term references without ownership.
1121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1122pub enum TermRef<'a> {
1123    /// Named node reference
1124    NamedNode(NamedNodeRef<'a>),
1125    /// Blank node reference
1126    BlankNode(BlankNodeRef<'a>),
1127    /// Literal reference
1128    Literal(LiteralRef<'a>),
1129    /// Variable reference
1130    Variable(&'a Variable),
1131    /// Quoted triple reference (RDF-star)
1132    #[cfg(feature = "rdf-star")]
1133    Triple(&'a crate::model::star::QuotedTriple),
1134}
1135
1136impl<'a> TermRef<'a> {
1137    /// Returns true if this is a named node
1138    pub fn is_named_node(&self) -> bool {
1139        matches!(self, TermRef::NamedNode(_))
1140    }
1141
1142    /// Returns true if this is a blank node
1143    pub fn is_blank_node(&self) -> bool {
1144        matches!(self, TermRef::BlankNode(_))
1145    }
1146
1147    /// Returns true if this is a literal
1148    pub fn is_literal(&self) -> bool {
1149        matches!(self, TermRef::Literal(_))
1150    }
1151
1152    /// Returns true if this is a variable
1153    pub fn is_variable(&self) -> bool {
1154        matches!(self, TermRef::Variable(_))
1155    }
1156
1157    /// Returns true if this is a quoted triple
1158    #[cfg(feature = "rdf-star")]
1159    pub fn is_triple(&self) -> bool {
1160        matches!(self, TermRef::Triple(_))
1161    }
1162}
1163
1164impl<'a> From<&'a Term> for TermRef<'a> {
1165    fn from(term: &'a Term) -> Self {
1166        match term {
1167            Term::NamedNode(n) => TermRef::NamedNode(n.as_ref()),
1168            Term::BlankNode(b) => TermRef::BlankNode(b.as_ref()),
1169            Term::Literal(l) => TermRef::Literal(l.as_ref()),
1170            Term::Variable(v) => TermRef::Variable(v),
1171            #[allow(unused_variables)]
1172            Term::QuotedTriple(t) => {
1173                #[cfg(feature = "rdf-star")]
1174                {
1175                    TermRef::Triple(t.as_ref())
1176                }
1177                #[cfg(not(feature = "rdf-star"))]
1178                {
1179                    panic!("RDF-star feature not enabled")
1180                }
1181            }
1182        }
1183    }
1184}
1185
1186impl<'a> RdfTerm for TermRef<'a> {
1187    fn as_str(&self) -> &str {
1188        match self {
1189            TermRef::NamedNode(n) => n.as_str(),
1190            TermRef::BlankNode(b) => b.as_str(),
1191            TermRef::Literal(l) => l.value(),
1192            TermRef::Variable(v) => v.name(),
1193            #[cfg(feature = "rdf-star")]
1194            TermRef::Triple(_) => "<<quoted triple>>", // Placeholder
1195        }
1196    }
1197
1198    fn is_named_node(&self) -> bool {
1199        self.is_named_node()
1200    }
1201
1202    fn is_blank_node(&self) -> bool {
1203        self.is_blank_node()
1204    }
1205
1206    fn is_literal(&self) -> bool {
1207        self.is_literal()
1208    }
1209
1210    fn is_variable(&self) -> bool {
1211        self.is_variable()
1212    }
1213}
1214
1215#[cfg(test)]
1216mod tests {
1217    use super::*;
1218
1219    #[test]
1220    fn test_blank_node() {
1221        let blank = BlankNode::new("b1").expect("valid blank node id");
1222        assert_eq!(blank.id(), "b1");
1223        assert_eq!(blank.local_id(), "b1");
1224        assert!(blank.is_blank_node());
1225        assert_eq!(format!("{blank}"), "_:b1");
1226    }
1227
1228    #[test]
1229    fn test_blank_node_with_prefix() {
1230        let blank = BlankNode::new("_:test").expect("valid blank node id");
1231        assert_eq!(blank.id(), "_:test");
1232        assert_eq!(blank.local_id(), "test");
1233    }
1234
1235    #[test]
1236    fn test_blank_node_unique() {
1237        let blank1 = BlankNode::new_unique();
1238        let blank2 = BlankNode::new_unique();
1239        assert_ne!(blank1.id(), blank2.id());
1240        // New unique uses Default() which creates hex IDs that start with a-f
1241        assert!(matches!(blank1.id().as_bytes().first(), Some(b'a'..=b'f')));
1242        assert!(matches!(blank2.id().as_bytes().first(), Some(b'a'..=b'f')));
1243    }
1244
1245    #[test]
1246    fn test_blank_node_unique_with_prefix() {
1247        let blank1 =
1248            BlankNode::new_unique_with_prefix("test").expect("construction should succeed");
1249        let blank2 =
1250            BlankNode::new_unique_with_prefix("test").expect("construction should succeed");
1251        assert_ne!(blank1.id(), blank2.id());
1252        assert!(blank1.id().starts_with("test_"));
1253        assert!(blank2.id().starts_with("test_"));
1254    }
1255
1256    #[test]
1257    fn test_blank_node_validation() {
1258        // Valid IDs
1259        assert!(BlankNode::new("test123").is_ok());
1260        assert!(BlankNode::new("Test_Node").is_ok());
1261        assert!(BlankNode::new("node-1.2").is_ok());
1262
1263        // Invalid IDs
1264        assert!(BlankNode::new("").is_err());
1265        assert!(BlankNode::new("_:").is_err());
1266        assert!(BlankNode::new("123invalid").is_err()); // Can't start with number
1267        assert!(BlankNode::new("invalid@char").is_err());
1268        assert!(BlankNode::new("invalid space").is_err());
1269    }
1270
1271    #[test]
1272    fn test_blank_node_serde() {
1273        let blank = BlankNode::new("serializable").expect("valid blank node id");
1274        let json = serde_json::to_string(&blank).expect("construction should succeed");
1275        let deserialized: BlankNode =
1276            serde_json::from_str(&json).expect("construction should succeed");
1277        assert_eq!(blank, deserialized);
1278    }
1279
1280    #[test]
1281    fn test_variable() {
1282        let var = Variable::new("x").expect("valid variable name");
1283        assert_eq!(var.name(), "x");
1284        assert!(var.is_variable());
1285        assert_eq!(format!("{var}"), "?x");
1286        assert_eq!(var.with_prefix(), "?x");
1287    }
1288
1289    #[test]
1290    fn test_variable_with_prefix() {
1291        let var1 = Variable::new("?test").expect("valid variable name");
1292        let var2 = Variable::new("$test").expect("valid variable name");
1293        assert_eq!(var1.name(), "test");
1294        assert_eq!(var2.name(), "test");
1295        assert_eq!(var1, var2); // Same after normalization
1296    }
1297
1298    #[test]
1299    fn test_variable_validation() {
1300        // Valid names
1301        assert!(Variable::new("x").is_ok());
1302        assert!(Variable::new("test123").is_ok());
1303        assert!(Variable::new("_underscore").is_ok());
1304        assert!(Variable::new("?prefixed").is_ok());
1305        assert!(Variable::new("$prefixed").is_ok());
1306
1307        // Invalid names
1308        assert!(Variable::new("").is_err());
1309        assert!(Variable::new("?").is_err());
1310        assert!(Variable::new("$").is_err());
1311        assert!(Variable::new("123invalid").is_err()); // Can't start with number
1312        assert!(Variable::new("invalid-char").is_err());
1313        assert!(Variable::new("invalid space").is_err());
1314
1315        // Reserved keywords
1316        assert!(Variable::new("select").is_err());
1317        assert!(Variable::new("WHERE").is_err()); // Case insensitive
1318        assert!(Variable::new("?from").is_err());
1319    }
1320
1321    #[test]
1322    fn test_variable_serde() {
1323        let var = Variable::new("serializable").expect("valid variable name");
1324        let json = serde_json::to_string(&var).expect("construction should succeed");
1325        let deserialized: Variable =
1326            serde_json::from_str(&json).expect("construction should succeed");
1327        assert_eq!(var, deserialized);
1328    }
1329
1330    #[test]
1331    fn test_term_enum() {
1332        let term = Term::NamedNode(NamedNode::new("http://example.org").expect("valid IRI"));
1333        assert!(term.is_named_node());
1334        assert!(term.as_named_node().is_some());
1335        assert!(term.as_blank_node().is_none());
1336    }
1337
1338    #[test]
1339    fn test_blank_node_numerical() {
1340        // Test hex ID optimization - hex IDs must still follow blank node rules (can't start with digit)
1341        let blank1 = BlankNode::new("a100a").expect("valid blank node id");
1342        assert_eq!(blank1.unique_id(), Some(0xa100a));
1343        assert_eq!(blank1.local_id(), "a100a");
1344
1345        // Non-hex IDs should not get numerical IDs
1346        let blank2 = BlankNode::new("a100A").expect("valid blank node id"); // Capital letters not supported in hex optimization
1347        assert_eq!(blank2.unique_id(), None);
1348
1349        // Direct numerical ID creation
1350        let blank3 = BlankNode::new_from_unique_id(0x42);
1351        assert_eq!(blank3.unique_id(), Some(0x42));
1352        assert_eq!(blank3.local_id(), "42");
1353    }
1354
1355    #[test]
1356    fn test_blank_node_default() {
1357        let blank = BlankNode::default();
1358        assert!(blank.unique_id().is_some());
1359        // Default blank nodes start with a-f (for RDF/XML compatibility)
1360        assert!(matches!(blank.id().as_bytes().first(), Some(b'a'..=b'f')));
1361    }
1362}