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