Skip to main content

oxirs_core/data_factory/
mod.rs

1//! # W3C RDF DataFactory API
2//!
3//! A Rust implementation of the
4//! [W3C RDF/JS Data Model specification](https://rdf.js.org/data-model-spec/)
5//! `DataFactory` interface.  The API mirrors the JavaScript `N3.js` / `rdf-ext`
6//! ecosystem so that tooling that expects the W3C interface can be ported to
7//! Rust without surprises.
8//!
9//! ## Quick start
10//!
11//! ```rust
12//! use oxirs_core::data_factory::{DataFactory, xsd_types, vocab};
13//!
14//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
15//! // Named nodes (IRIs)
16//! let alice = DataFactory::named_node("http://example.org/alice")?;
17//! let knows = DataFactory::named_node("http://xmlns.com/foaf/0.1/knows")?;
18//! let bob   = DataFactory::named_node("http://example.org/bob")?;
19//!
20//! // Create a triple
21//! let triple = DataFactory::triple(alice.clone().into(), knows.clone(), bob.clone().into());
22//!
23//! // Language-tagged literal
24//! let hello = DataFactory::language_literal("Hello", "en")?;
25//!
26//! // Typed literal
27//! let age = DataFactory::typed_literal("42", xsd_types::integer());
28//!
29//! // Blank nodes
30//! let b = DataFactory::blank_node();
31//! let b2 = DataFactory::blank_node_with_id("my-id");
32//!
33//! // Quads
34//! let graph = DataFactory::default_graph();
35//! let quad  = DataFactory::quad(alice.into(), knows, bob.into(), graph);
36//! # Ok(())
37//! # }
38//! ```
39
40use crate::model::{BlankNode, GraphName, Literal, NamedNode, Object, Quad, Subject, Triple};
41use crate::{OxirsError, Result};
42use oxiri::Iri;
43
44// ── DataFactory ───────────────────────────────────────────────────────────────
45
46/// Stateless factory for constructing RDF terms, triples and quads.
47///
48/// Every method is `pub` and `fn` (not `self`); the struct itself needs no
49/// instance – it is used as a namespace.
50pub struct DataFactory;
51
52impl DataFactory {
53    // ── Named nodes ───────────────────────────────────────────────────────────
54
55    /// Create a [`NamedNode`] from an IRI string, validating it.
56    ///
57    /// Returns [`OxirsError::Parse`] if the string is not a well-formed IRI.
58    ///
59    /// ```rust
60    /// use oxirs_core::data_factory::DataFactory;
61    /// let n = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
62    /// assert_eq!(n.as_str(), "http://example.org/s");
63    /// ```
64    pub fn named_node(iri: impl Into<String>) -> Result<NamedNode> {
65        NamedNode::new(iri)
66    }
67
68    // ── Blank nodes ───────────────────────────────────────────────────────────
69
70    /// Create a [`BlankNode`] with an auto-generated unique identifier.
71    ///
72    /// ```rust
73    /// use oxirs_core::data_factory::DataFactory;
74    /// let b1 = DataFactory::blank_node();
75    /// let b2 = DataFactory::blank_node();
76    /// assert_ne!(b1.as_str(), b2.as_str());
77    /// ```
78    pub fn blank_node() -> BlankNode {
79        BlankNode::new_unique()
80    }
81
82    /// Create a [`BlankNode`] with a specific local identifier.
83    ///
84    /// The identifier must match `[a-zA-Z0-9_][a-zA-Z0-9_.-]*`; invalid
85    /// strings are silently replaced with a generated one.
86    ///
87    /// ```rust
88    /// use oxirs_core::data_factory::DataFactory;
89    /// let b = DataFactory::blank_node_with_id("my-node");
90    /// assert_eq!(b.as_str(), "my-node");
91    /// ```
92    pub fn blank_node_with_id(id: impl Into<String>) -> BlankNode {
93        let s = id.into();
94        BlankNode::new(s.clone()).unwrap_or_else(|_| BlankNode::new_unique())
95    }
96
97    // ── Literals ──────────────────────────────────────────────────────────────
98
99    /// Create a plain (xsd:string) [`Literal`].
100    ///
101    /// ```rust
102    /// use oxirs_core::data_factory::DataFactory;
103    /// let l = DataFactory::literal("hello");
104    /// assert_eq!(l.value(), "hello");
105    /// ```
106    pub fn literal(value: impl Into<String>) -> Literal {
107        Literal::new(value)
108    }
109
110    /// Create an explicitly-typed [`Literal`].
111    ///
112    /// ```rust
113    /// use oxirs_core::data_factory::{DataFactory, xsd_types};
114    /// let l = DataFactory::typed_literal("42", xsd_types::integer());
115    /// assert_eq!(l.value(), "42");
116    /// ```
117    pub fn typed_literal(value: impl Into<String>, datatype: NamedNode) -> Literal {
118        Literal::new_typed(value, datatype)
119    }
120
121    /// Create a language-tagged [`Literal`] (BCP 47 language tag).
122    ///
123    /// Returns [`OxirsError::Parse`] if `lang` is not a valid BCP 47 tag.
124    ///
125    /// ```rust
126    /// use oxirs_core::data_factory::DataFactory;
127    /// let l = DataFactory::language_literal("Bonjour", "fr").expect("valid language literal");
128    /// assert_eq!(l.language(), Some("fr"));
129    /// ```
130    pub fn language_literal(value: impl Into<String>, lang: impl Into<String>) -> Result<Literal> {
131        let lang_str = lang.into();
132        Self::validate_lang_tag(&lang_str)?;
133        Literal::new_lang(value, lang_str)
134    }
135
136    // ── Triples ───────────────────────────────────────────────────────────────
137
138    /// Create a [`Triple`] from subject, predicate, and object.
139    ///
140    /// ```rust
141    /// use oxirs_core::data_factory::DataFactory;
142    /// use oxirs_core::model::{Subject, Object};
143    /// let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
144    /// let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
145    /// let o = DataFactory::named_node("http://example.org/o").expect("valid IRI for named node");
146    /// let triple = DataFactory::triple(s.into(), p, o.into());
147    /// ```
148    pub fn triple(subject: Subject, predicate: NamedNode, object: Object) -> Triple {
149        Triple::new(subject, predicate, object)
150    }
151
152    // ── Quads ─────────────────────────────────────────────────────────────────
153
154    /// Create a [`Quad`] from subject, predicate, object, and graph name.
155    ///
156    /// Use [`Self::default_graph()`] for the default graph.
157    ///
158    /// ```rust
159    /// use oxirs_core::data_factory::DataFactory;
160    /// use oxirs_core::model::{Subject, Object};
161    /// let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
162    /// let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
163    /// let o = DataFactory::named_node("http://example.org/o").expect("valid IRI for named node");
164    /// let g = DataFactory::default_graph();
165    /// let quad = DataFactory::quad(s.into(), p, o.into(), g);
166    /// ```
167    pub fn quad(subject: Subject, predicate: NamedNode, object: Object, graph: GraphName) -> Quad {
168        Quad::new(subject, predicate, object, graph)
169    }
170
171    // ── Graph names ───────────────────────────────────────────────────────────
172
173    /// Return the default [`GraphName`].
174    pub fn default_graph() -> GraphName {
175        GraphName::DefaultGraph
176    }
177
178    /// Create a named-graph [`GraphName`] from a validated IRI.
179    ///
180    /// Returns [`OxirsError::Parse`] if the string is not a well-formed IRI.
181    pub fn named_graph(iri: impl Into<String>) -> Result<GraphName> {
182        let nn = NamedNode::new(iri)?;
183        Ok(GraphName::NamedNode(nn))
184    }
185
186    // ── Validation ────────────────────────────────────────────────────────────
187
188    /// Validate an IRI string without constructing a [`NamedNode`].
189    ///
190    /// ```rust
191    /// use oxirs_core::data_factory::DataFactory;
192    /// assert!(DataFactory::validate_iri("http://example.org/").is_ok());
193    /// assert!(DataFactory::validate_iri("not an IRI").is_err());
194    /// ```
195    pub fn validate_iri(iri: &str) -> Result<()> {
196        Iri::parse(iri.to_owned())
197            .map(|_| ())
198            .map_err(|e| OxirsError::Parse(format!("Invalid IRI '{iri}': {e}")))
199    }
200
201    /// Validate a BCP 47 language tag string.
202    ///
203    /// ```rust
204    /// use oxirs_core::data_factory::DataFactory;
205    /// assert!(DataFactory::validate_lang_tag("en").is_ok());
206    /// assert!(DataFactory::validate_lang_tag("en-US").is_ok());
207    /// assert!(DataFactory::validate_lang_tag("zh-Hans-CN").is_ok());
208    /// assert!(DataFactory::validate_lang_tag("").is_err());
209    /// ```
210    pub fn validate_lang_tag(lang: &str) -> Result<()> {
211        if lang.is_empty() {
212            return Err(OxirsError::Parse(
213                "Language tag must not be empty".to_string(),
214            ));
215        }
216        // BCP 47 primary subtag: 1–8 ASCII alpha characters.
217        // We apply a lightweight structural check sufficient for practical use.
218        for part in lang.split('-') {
219            if part.is_empty() {
220                return Err(OxirsError::Parse(format!(
221                    "Invalid language tag '{lang}': empty subtag"
222                )));
223            }
224            // Each subtag must be alphanumeric
225            if !part.chars().all(|c| c.is_ascii_alphanumeric()) {
226                return Err(OxirsError::Parse(format!(
227                    "Invalid language tag '{lang}': subtag '{part}' contains non-alphanumeric characters"
228                )));
229            }
230        }
231        // Primary subtag must be alphabetic (BCP 47 §2.2.1)
232        let primary = lang.split('-').next().unwrap_or(lang);
233        if !primary.chars().all(|c| c.is_ascii_alphabetic()) {
234            return Err(OxirsError::Parse(format!(
235                "Invalid language tag '{lang}': primary subtag must be alphabetic"
236            )));
237        }
238        Ok(())
239    }
240}
241
242// ── XSD Datatype helpers ──────────────────────────────────────────────────────
243
244/// Functions that return [`NamedNode`]s for the most common XSD datatypes.
245///
246/// These are unchecked because the IRIs are compile-time constants.
247pub mod xsd_types {
248    use crate::model::NamedNode;
249
250    const XSD: &str = "http://www.w3.org/2001/XMLSchema#";
251
252    /// `xsd:string`
253    pub fn string() -> NamedNode {
254        NamedNode::new_unchecked(format!("{XSD}string"))
255    }
256    /// `xsd:integer`
257    pub fn integer() -> NamedNode {
258        NamedNode::new_unchecked(format!("{XSD}integer"))
259    }
260    /// `xsd:float`
261    pub fn float() -> NamedNode {
262        NamedNode::new_unchecked(format!("{XSD}float"))
263    }
264    /// `xsd:double`
265    pub fn double() -> NamedNode {
266        NamedNode::new_unchecked(format!("{XSD}double"))
267    }
268    /// `xsd:boolean`
269    pub fn boolean() -> NamedNode {
270        NamedNode::new_unchecked(format!("{XSD}boolean"))
271    }
272    /// `xsd:dateTime`
273    pub fn date_time() -> NamedNode {
274        NamedNode::new_unchecked(format!("{XSD}dateTime"))
275    }
276    /// `xsd:date`
277    pub fn date() -> NamedNode {
278        NamedNode::new_unchecked(format!("{XSD}date"))
279    }
280    /// `xsd:decimal`
281    pub fn decimal() -> NamedNode {
282        NamedNode::new_unchecked(format!("{XSD}decimal"))
283    }
284    /// `xsd:long`
285    pub fn long() -> NamedNode {
286        NamedNode::new_unchecked(format!("{XSD}long"))
287    }
288    /// `xsd:int`
289    pub fn int() -> NamedNode {
290        NamedNode::new_unchecked(format!("{XSD}int"))
291    }
292    /// `xsd:short`
293    pub fn short() -> NamedNode {
294        NamedNode::new_unchecked(format!("{XSD}short"))
295    }
296    /// `xsd:byte`
297    pub fn byte() -> NamedNode {
298        NamedNode::new_unchecked(format!("{XSD}byte"))
299    }
300    /// `xsd:unsignedLong`
301    pub fn unsigned_long() -> NamedNode {
302        NamedNode::new_unchecked(format!("{XSD}unsignedLong"))
303    }
304    /// `xsd:unsignedInt`
305    pub fn unsigned_int() -> NamedNode {
306        NamedNode::new_unchecked(format!("{XSD}unsignedInt"))
307    }
308    /// `xsd:nonNegativeInteger`
309    pub fn non_negative_integer() -> NamedNode {
310        NamedNode::new_unchecked(format!("{XSD}nonNegativeInteger"))
311    }
312    /// `xsd:positiveInteger`
313    pub fn positive_integer() -> NamedNode {
314        NamedNode::new_unchecked(format!("{XSD}positiveInteger"))
315    }
316    /// `xsd:anyURI`
317    pub fn any_uri() -> NamedNode {
318        NamedNode::new_unchecked(format!("{XSD}anyURI"))
319    }
320    /// `xsd:base64Binary`
321    pub fn base64_binary() -> NamedNode {
322        NamedNode::new_unchecked(format!("{XSD}base64Binary"))
323    }
324    /// `xsd:hexBinary`
325    pub fn hex_binary() -> NamedNode {
326        NamedNode::new_unchecked(format!("{XSD}hexBinary"))
327    }
328    /// `xsd:gYear`
329    pub fn g_year() -> NamedNode {
330        NamedNode::new_unchecked(format!("{XSD}gYear"))
331    }
332    /// `xsd:duration`
333    pub fn duration() -> NamedNode {
334        NamedNode::new_unchecked(format!("{XSD}duration"))
335    }
336    /// `xsd:time`
337    pub fn time() -> NamedNode {
338        NamedNode::new_unchecked(format!("{XSD}time"))
339    }
340    /// `xsd:normalizedString`
341    pub fn normalized_string() -> NamedNode {
342        NamedNode::new_unchecked(format!("{XSD}normalizedString"))
343    }
344    /// `xsd:token`
345    pub fn token() -> NamedNode {
346        NamedNode::new_unchecked(format!("{XSD}token"))
347    }
348}
349
350// ── Vocabulary constants ──────────────────────────────────────────────────────
351
352/// Common RDF/RDFS/OWL/XSD vocabulary terms as [`NamedNode`] functions.
353///
354/// These mirror the JavaScript `rdf-ext` / `@rdfjs/namespace` pattern.
355pub mod vocab {
356    use crate::model::NamedNode;
357
358    /// Core RDF vocabulary (`rdf:` prefix).
359    pub mod rdf {
360        use super::NamedNode;
361        const NS: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
362        /// `rdf:type`
363        pub fn r#type() -> NamedNode {
364            NamedNode::new_unchecked(format!("{NS}type"))
365        }
366        /// `rdf:subject`
367        pub fn subject() -> NamedNode {
368            NamedNode::new_unchecked(format!("{NS}subject"))
369        }
370        /// `rdf:predicate`
371        pub fn predicate() -> NamedNode {
372            NamedNode::new_unchecked(format!("{NS}predicate"))
373        }
374        /// `rdf:object`
375        pub fn object() -> NamedNode {
376            NamedNode::new_unchecked(format!("{NS}object"))
377        }
378        /// `rdf:Property`
379        pub fn property() -> NamedNode {
380            NamedNode::new_unchecked(format!("{NS}Property"))
381        }
382        /// `rdf:Statement`
383        pub fn statement() -> NamedNode {
384            NamedNode::new_unchecked(format!("{NS}Statement"))
385        }
386        /// `rdf:first`
387        pub fn first() -> NamedNode {
388            NamedNode::new_unchecked(format!("{NS}first"))
389        }
390        /// `rdf:rest`
391        pub fn rest() -> NamedNode {
392            NamedNode::new_unchecked(format!("{NS}rest"))
393        }
394        /// `rdf:nil`
395        pub fn nil() -> NamedNode {
396            NamedNode::new_unchecked(format!("{NS}nil"))
397        }
398        /// `rdf:List`
399        pub fn list() -> NamedNode {
400            NamedNode::new_unchecked(format!("{NS}List"))
401        }
402        /// `rdf:Bag`
403        pub fn bag() -> NamedNode {
404            NamedNode::new_unchecked(format!("{NS}Bag"))
405        }
406        /// `rdf:Seq`
407        pub fn seq() -> NamedNode {
408            NamedNode::new_unchecked(format!("{NS}Seq"))
409        }
410        /// `rdf:Alt`
411        pub fn alt() -> NamedNode {
412            NamedNode::new_unchecked(format!("{NS}Alt"))
413        }
414        /// `rdf:value`
415        pub fn value() -> NamedNode {
416            NamedNode::new_unchecked(format!("{NS}value"))
417        }
418        /// `rdf:langString`
419        pub fn lang_string() -> NamedNode {
420            NamedNode::new_unchecked(format!("{NS}langString"))
421        }
422        /// RDF namespace IRI
423        pub const NAMESPACE: &str = NS;
424    }
425
426    /// RDFS vocabulary (`rdfs:` prefix).
427    pub mod rdfs {
428        use super::NamedNode;
429        const NS: &str = "http://www.w3.org/2000/01/rdf-schema#";
430        /// `rdfs:label`
431        pub fn label() -> NamedNode {
432            NamedNode::new_unchecked(format!("{NS}label"))
433        }
434        /// `rdfs:comment`
435        pub fn comment() -> NamedNode {
436            NamedNode::new_unchecked(format!("{NS}comment"))
437        }
438        /// `rdfs:subClassOf`
439        pub fn sub_class_of() -> NamedNode {
440            NamedNode::new_unchecked(format!("{NS}subClassOf"))
441        }
442        /// `rdfs:subPropertyOf`
443        pub fn sub_property_of() -> NamedNode {
444            NamedNode::new_unchecked(format!("{NS}subPropertyOf"))
445        }
446        /// `rdfs:domain`
447        pub fn domain() -> NamedNode {
448            NamedNode::new_unchecked(format!("{NS}domain"))
449        }
450        /// `rdfs:range`
451        pub fn range() -> NamedNode {
452            NamedNode::new_unchecked(format!("{NS}range"))
453        }
454        /// `rdfs:Class`
455        pub fn class() -> NamedNode {
456            NamedNode::new_unchecked(format!("{NS}Class"))
457        }
458        /// `rdfs:Resource`
459        pub fn resource() -> NamedNode {
460            NamedNode::new_unchecked(format!("{NS}Resource"))
461        }
462        /// `rdfs:Literal`
463        pub fn literal() -> NamedNode {
464            NamedNode::new_unchecked(format!("{NS}Literal"))
465        }
466        /// `rdfs:Datatype`
467        pub fn datatype() -> NamedNode {
468            NamedNode::new_unchecked(format!("{NS}Datatype"))
469        }
470        /// `rdfs:isDefinedBy`
471        pub fn is_defined_by() -> NamedNode {
472            NamedNode::new_unchecked(format!("{NS}isDefinedBy"))
473        }
474        /// `rdfs:seeAlso`
475        pub fn see_also() -> NamedNode {
476            NamedNode::new_unchecked(format!("{NS}seeAlso"))
477        }
478        /// `rdfs:member`
479        pub fn member() -> NamedNode {
480            NamedNode::new_unchecked(format!("{NS}member"))
481        }
482        /// `rdfs:Container`
483        pub fn container() -> NamedNode {
484            NamedNode::new_unchecked(format!("{NS}Container"))
485        }
486        /// RDFS namespace IRI
487        pub const NAMESPACE: &str = NS;
488    }
489
490    /// OWL vocabulary (`owl:` prefix).
491    pub mod owl {
492        use super::NamedNode;
493        const NS: &str = "http://www.w3.org/2002/07/owl#";
494        /// `owl:Class`
495        pub fn class() -> NamedNode {
496            NamedNode::new_unchecked(format!("{NS}Class"))
497        }
498        /// `owl:ObjectProperty`
499        pub fn object_property() -> NamedNode {
500            NamedNode::new_unchecked(format!("{NS}ObjectProperty"))
501        }
502        /// `owl:DatatypeProperty`
503        pub fn datatype_property() -> NamedNode {
504            NamedNode::new_unchecked(format!("{NS}DatatypeProperty"))
505        }
506        /// `owl:AnnotationProperty`
507        pub fn annotation_property() -> NamedNode {
508            NamedNode::new_unchecked(format!("{NS}AnnotationProperty"))
509        }
510        /// `owl:Thing`
511        pub fn thing() -> NamedNode {
512            NamedNode::new_unchecked(format!("{NS}Thing"))
513        }
514        /// `owl:Nothing`
515        pub fn nothing() -> NamedNode {
516            NamedNode::new_unchecked(format!("{NS}Nothing"))
517        }
518        /// `owl:sameAs`
519        pub fn same_as() -> NamedNode {
520            NamedNode::new_unchecked(format!("{NS}sameAs"))
521        }
522        /// `owl:equivalentClass`
523        pub fn equivalent_class() -> NamedNode {
524            NamedNode::new_unchecked(format!("{NS}equivalentClass"))
525        }
526        /// `owl:equivalentProperty`
527        pub fn equivalent_property() -> NamedNode {
528            NamedNode::new_unchecked(format!("{NS}equivalentProperty"))
529        }
530        /// `owl:inverseOf`
531        pub fn inverse_of() -> NamedNode {
532            NamedNode::new_unchecked(format!("{NS}inverseOf"))
533        }
534        /// `owl:disjointWith`
535        pub fn disjoint_with() -> NamedNode {
536            NamedNode::new_unchecked(format!("{NS}disjointWith"))
537        }
538        /// `owl:FunctionalProperty`
539        pub fn functional_property() -> NamedNode {
540            NamedNode::new_unchecked(format!("{NS}FunctionalProperty"))
541        }
542        /// `owl:Ontology`
543        pub fn ontology() -> NamedNode {
544            NamedNode::new_unchecked(format!("{NS}Ontology"))
545        }
546        /// OWL namespace IRI
547        pub const NAMESPACE: &str = NS;
548    }
549
550    /// XSD vocabulary (as named nodes, complements [`crate::data_factory::xsd_types`]).
551    pub mod xsd {
552        use super::NamedNode;
553        const NS: &str = "http://www.w3.org/2001/XMLSchema#";
554        /// `xsd:string`
555        pub fn string() -> NamedNode {
556            NamedNode::new_unchecked(format!("{NS}string"))
557        }
558        /// `xsd:integer`
559        pub fn integer() -> NamedNode {
560            NamedNode::new_unchecked(format!("{NS}integer"))
561        }
562        /// `xsd:boolean`
563        pub fn boolean() -> NamedNode {
564            NamedNode::new_unchecked(format!("{NS}boolean"))
565        }
566        /// `xsd:double`
567        pub fn double() -> NamedNode {
568            NamedNode::new_unchecked(format!("{NS}double"))
569        }
570        /// `xsd:dateTime`
571        pub fn date_time() -> NamedNode {
572            NamedNode::new_unchecked(format!("{NS}dateTime"))
573        }
574        /// XSD namespace IRI
575        pub const NAMESPACE: &str = NS;
576    }
577
578    /// FOAF vocabulary (`foaf:` prefix) – commonly used in examples.
579    pub mod foaf {
580        use super::NamedNode;
581        const NS: &str = "http://xmlns.com/foaf/0.1/";
582        /// `foaf:name`
583        pub fn name() -> NamedNode {
584            NamedNode::new_unchecked(format!("{NS}name"))
585        }
586        /// `foaf:Person`
587        pub fn person() -> NamedNode {
588            NamedNode::new_unchecked(format!("{NS}Person"))
589        }
590        /// `foaf:knows`
591        pub fn knows() -> NamedNode {
592            NamedNode::new_unchecked(format!("{NS}knows"))
593        }
594        /// `foaf:mbox`
595        pub fn mbox() -> NamedNode {
596            NamedNode::new_unchecked(format!("{NS}mbox"))
597        }
598        /// `foaf:homepage`
599        pub fn homepage() -> NamedNode {
600            NamedNode::new_unchecked(format!("{NS}homepage"))
601        }
602        /// FOAF namespace IRI
603        pub const NAMESPACE: &str = NS;
604    }
605}
606
607// ── Tests ─────────────────────────────────────────────────────────────────────
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612
613    // ── Named node ────────────────────────────────────────────────────────────
614
615    #[test]
616    fn test_named_node_valid_http() {
617        let n = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
618        assert_eq!(n.as_str(), "http://example.org/s");
619    }
620
621    #[test]
622    fn test_named_node_valid_https() {
623        let n =
624            DataFactory::named_node("https://schema.org/Person").expect("valid IRI for named node");
625        assert_eq!(n.as_str(), "https://schema.org/Person");
626    }
627
628    #[test]
629    fn test_named_node_valid_urn() {
630        let n = DataFactory::named_node("urn:example:foo").expect("valid IRI for named node");
631        assert_eq!(n.as_str(), "urn:example:foo");
632    }
633
634    #[test]
635    fn test_named_node_invalid_returns_err() {
636        assert!(DataFactory::named_node("not an IRI").is_err());
637    }
638
639    #[test]
640    fn test_named_node_empty_string_is_err() {
641        assert!(DataFactory::named_node("").is_err());
642    }
643
644    #[test]
645    fn test_named_node_with_fragment() {
646        let n = DataFactory::named_node("http://example.org/ont#Class")
647            .expect("valid IRI for named node");
648        assert!(n.as_str().ends_with("#Class"));
649    }
650
651    #[test]
652    fn test_named_node_with_query() {
653        let n =
654            DataFactory::named_node("http://example.org/q?a=1").expect("valid IRI for named node");
655        assert!(n.as_str().contains("a=1"));
656    }
657
658    // ── Blank nodes ───────────────────────────────────────────────────────────
659
660    #[test]
661    fn test_blank_node_auto_id_is_nonempty() {
662        let b = DataFactory::blank_node();
663        assert!(!b.as_str().is_empty());
664    }
665
666    #[test]
667    fn test_blank_node_auto_ids_are_unique() {
668        let b1 = DataFactory::blank_node();
669        let b2 = DataFactory::blank_node();
670        assert_ne!(b1.as_str(), b2.as_str());
671    }
672
673    #[test]
674    fn test_blank_node_with_id() {
675        let b = DataFactory::blank_node_with_id("my-node");
676        assert_eq!(b.as_str(), "my-node");
677    }
678
679    #[test]
680    fn test_blank_node_with_id_alpha() {
681        let b = DataFactory::blank_node_with_id("abc");
682        assert_eq!(b.as_str(), "abc");
683    }
684
685    #[test]
686    fn test_blank_node_with_id_alphanumeric() {
687        let b = DataFactory::blank_node_with_id("node1");
688        assert_eq!(b.as_str(), "node1");
689    }
690
691    #[test]
692    fn test_blank_node_with_invalid_id_still_returns_node() {
693        // Invalid IDs fall back to a generated one (no panic)
694        let b = DataFactory::blank_node_with_id("  spaces  ");
695        assert!(!b.as_str().is_empty());
696    }
697
698    // ── Plain literals ────────────────────────────────────────────────────────
699
700    #[test]
701    fn test_literal_plain_value() {
702        let l = DataFactory::literal("hello");
703        assert_eq!(l.value(), "hello");
704    }
705
706    #[test]
707    fn test_literal_plain_no_language() {
708        let l = DataFactory::literal("hello");
709        assert_eq!(l.language(), None);
710    }
711
712    #[test]
713    fn test_literal_plain_datatype_is_xsd_string() {
714        let l = DataFactory::literal("hello");
715        assert!(l.datatype().as_str().contains("string"));
716    }
717
718    #[test]
719    fn test_literal_empty_string() {
720        let l = DataFactory::literal("");
721        assert_eq!(l.value(), "");
722    }
723
724    // ── Typed literals ────────────────────────────────────────────────────────
725
726    #[test]
727    fn test_typed_literal_integer() {
728        let l = DataFactory::typed_literal("42", xsd_types::integer());
729        assert_eq!(l.value(), "42");
730        assert!(l.datatype().as_str().ends_with("integer"));
731    }
732
733    #[test]
734    fn test_typed_literal_boolean() {
735        let l = DataFactory::typed_literal("true", xsd_types::boolean());
736        assert_eq!(l.value(), "true");
737        assert!(l.datatype().as_str().ends_with("boolean"));
738    }
739
740    #[test]
741    fn test_typed_literal_double() {
742        let l = DataFactory::typed_literal("3.14", xsd_types::double());
743        assert!(l.datatype().as_str().ends_with("double"));
744    }
745
746    #[test]
747    fn test_typed_literal_date_time() {
748        let l = DataFactory::typed_literal("2026-02-24T00:00:00Z", xsd_types::date_time());
749        assert!(l.datatype().as_str().ends_with("dateTime"));
750    }
751
752    #[test]
753    fn test_typed_literal_custom_datatype() {
754        let dt =
755            DataFactory::named_node("http://example.org/myType").expect("valid IRI for named node");
756        let l = DataFactory::typed_literal("custom", dt);
757        assert!(l.datatype().as_str().contains("myType"));
758    }
759
760    // ── Language literals ─────────────────────────────────────────────────────
761
762    #[test]
763    fn test_language_literal_value_and_lang() {
764        let l = DataFactory::language_literal("Bonjour", "fr").expect("valid language literal");
765        assert_eq!(l.value(), "Bonjour");
766        assert_eq!(l.language(), Some("fr"));
767    }
768
769    #[test]
770    fn test_language_literal_en() {
771        let l = DataFactory::language_literal("Hello", "en").expect("valid language literal");
772        assert_eq!(l.language(), Some("en"));
773    }
774
775    #[test]
776    fn test_language_literal_zh_hans() {
777        let l = DataFactory::language_literal("你好", "zh-Hans").expect("valid language literal");
778        // Language tags are compared case-insensitively per RDF 1.1, but the
779        // lexical form is preserved as authored (round-trip fidelity) rather
780        // than destructively lowercased.
781        assert_eq!(l.language(), Some("zh-Hans"));
782    }
783
784    #[test]
785    fn test_language_literal_en_us() {
786        let l = DataFactory::language_literal("Color", "en-US").expect("valid language literal");
787        assert_eq!(l.language(), Some("en-US"));
788    }
789
790    #[test]
791    fn test_language_literal_empty_lang_is_err() {
792        assert!(DataFactory::language_literal("hello", "").is_err());
793    }
794
795    #[test]
796    fn test_language_literal_invalid_lang_is_err() {
797        // Contains space → invalid
798        assert!(DataFactory::language_literal("hello", "en US").is_err());
799    }
800
801    // ── Triples ───────────────────────────────────────────────────────────────
802
803    #[test]
804    fn test_triple_subject_predicate_object() {
805        let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
806        let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
807        let o = DataFactory::named_node("http://example.org/o").expect("valid IRI for named node");
808        let t = DataFactory::triple(s.into(), p.clone(), o.into());
809        // Access via Display
810        let text = format!("{t}");
811        assert!(text.contains("http://example.org/s"));
812    }
813
814    #[test]
815    fn test_triple_with_literal_object() {
816        let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
817        let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
818        let o: Object = DataFactory::literal("hello").into();
819        let t = DataFactory::triple(s.into(), p, o);
820        let text = format!("{t}");
821        assert!(text.contains("hello"));
822    }
823
824    #[test]
825    fn test_triple_with_blank_node_subject() {
826        let s: Subject = DataFactory::blank_node().into();
827        let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
828        let o: Object = DataFactory::literal("val").into();
829        let _t = DataFactory::triple(s, p, o);
830    }
831
832    // ── Quads ─────────────────────────────────────────────────────────────────
833
834    #[test]
835    fn test_quad_default_graph() {
836        let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
837        let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
838        let o = DataFactory::named_node("http://example.org/o").expect("valid IRI for named node");
839        let g = DataFactory::default_graph();
840        let q = DataFactory::quad(s.into(), p, o.into(), g);
841        let text = format!("{q}");
842        assert!(text.contains("http://example.org/s"));
843    }
844
845    #[test]
846    fn test_quad_named_graph() {
847        let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
848        let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
849        let o = DataFactory::named_node("http://example.org/o").expect("valid IRI for named node");
850        let g = DataFactory::named_graph("http://example.org/graph1")
851            .expect("construction should succeed");
852        let q = DataFactory::quad(s.into(), p, o.into(), g);
853        let text = format!("{q}");
854        assert!(text.contains("graph1"));
855    }
856
857    #[test]
858    fn test_quad_named_graph_invalid_iri_is_err() {
859        assert!(DataFactory::named_graph("not an IRI").is_err());
860    }
861
862    #[test]
863    fn test_default_graph_is_default_graph_variant() {
864        let g = DataFactory::default_graph();
865        assert!(matches!(g, GraphName::DefaultGraph));
866    }
867
868    // ── IRI validation ────────────────────────────────────────────────────────
869
870    #[test]
871    fn test_validate_iri_http_ok() {
872        assert!(DataFactory::validate_iri("http://example.org/").is_ok());
873    }
874
875    #[test]
876    fn test_validate_iri_https_ok() {
877        assert!(DataFactory::validate_iri("https://example.org/path").is_ok());
878    }
879
880    #[test]
881    fn test_validate_iri_urn_ok() {
882        assert!(DataFactory::validate_iri("urn:isbn:0451450523").is_ok());
883    }
884
885    #[test]
886    fn test_validate_iri_bare_word_is_err() {
887        assert!(DataFactory::validate_iri("hello").is_err());
888    }
889
890    #[test]
891    fn test_validate_iri_empty_is_err() {
892        assert!(DataFactory::validate_iri("").is_err());
893    }
894
895    #[test]
896    fn test_validate_iri_space_is_err() {
897        assert!(DataFactory::validate_iri("http://example.org/hello world").is_err());
898    }
899
900    // ── Language tag validation ───────────────────────────────────────────────
901
902    #[test]
903    fn test_validate_lang_tag_en_ok() {
904        assert!(DataFactory::validate_lang_tag("en").is_ok());
905    }
906
907    #[test]
908    fn test_validate_lang_tag_en_us_ok() {
909        assert!(DataFactory::validate_lang_tag("en-US").is_ok());
910    }
911
912    #[test]
913    fn test_validate_lang_tag_zh_hans_cn_ok() {
914        assert!(DataFactory::validate_lang_tag("zh-Hans-CN").is_ok());
915    }
916
917    #[test]
918    fn test_validate_lang_tag_empty_is_err() {
919        assert!(DataFactory::validate_lang_tag("").is_err());
920    }
921
922    #[test]
923    fn test_validate_lang_tag_space_is_err() {
924        assert!(DataFactory::validate_lang_tag("en US").is_err());
925    }
926
927    #[test]
928    fn test_validate_lang_tag_double_dash_is_err() {
929        assert!(DataFactory::validate_lang_tag("en--US").is_err());
930    }
931
932    #[test]
933    fn test_validate_lang_tag_numeric_primary_is_err() {
934        // primary subtag must be alphabetic
935        assert!(DataFactory::validate_lang_tag("123").is_err());
936    }
937
938    // ── XSD datatype helpers ──────────────────────────────────────────────────
939
940    #[test]
941    fn test_xsd_string_iri() {
942        assert_eq!(
943            xsd_types::string().as_str(),
944            "http://www.w3.org/2001/XMLSchema#string"
945        );
946    }
947
948    #[test]
949    fn test_xsd_integer_iri() {
950        assert_eq!(
951            xsd_types::integer().as_str(),
952            "http://www.w3.org/2001/XMLSchema#integer"
953        );
954    }
955
956    #[test]
957    fn test_xsd_float_iri() {
958        assert!(xsd_types::float().as_str().ends_with("float"));
959    }
960
961    #[test]
962    fn test_xsd_double_iri() {
963        assert!(xsd_types::double().as_str().ends_with("double"));
964    }
965
966    #[test]
967    fn test_xsd_boolean_iri() {
968        assert!(xsd_types::boolean().as_str().ends_with("boolean"));
969    }
970
971    #[test]
972    fn test_xsd_date_time_iri() {
973        assert!(xsd_types::date_time().as_str().ends_with("dateTime"));
974    }
975
976    #[test]
977    fn test_xsd_date_iri() {
978        assert!(xsd_types::date().as_str().ends_with("date"));
979    }
980
981    #[test]
982    fn test_xsd_decimal_iri() {
983        assert!(xsd_types::decimal().as_str().ends_with("decimal"));
984    }
985
986    #[test]
987    fn test_xsd_long_iri() {
988        assert!(xsd_types::long().as_str().ends_with("long"));
989    }
990
991    #[test]
992    fn test_xsd_int_iri() {
993        assert!(xsd_types::int().as_str().ends_with("#int"));
994    }
995
996    #[test]
997    fn test_xsd_any_uri_iri() {
998        assert!(xsd_types::any_uri().as_str().ends_with("anyURI"));
999    }
1000
1001    #[test]
1002    fn test_xsd_base64_binary_iri() {
1003        assert!(xsd_types::base64_binary()
1004            .as_str()
1005            .ends_with("base64Binary"));
1006    }
1007
1008    #[test]
1009    fn test_xsd_hex_binary_iri() {
1010        assert!(xsd_types::hex_binary().as_str().ends_with("hexBinary"));
1011    }
1012
1013    // ── Vocabulary constants ──────────────────────────────────────────────────
1014
1015    #[test]
1016    fn test_vocab_rdf_type() {
1017        assert_eq!(
1018            vocab::rdf::r#type().as_str(),
1019            "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
1020        );
1021    }
1022
1023    #[test]
1024    fn test_vocab_rdf_first_last() {
1025        assert!(vocab::rdf::first().as_str().ends_with("first"));
1026        assert!(vocab::rdf::rest().as_str().ends_with("rest"));
1027        assert!(vocab::rdf::nil().as_str().ends_with("nil"));
1028    }
1029
1030    #[test]
1031    fn test_vocab_rdfs_label() {
1032        assert_eq!(
1033            vocab::rdfs::label().as_str(),
1034            "http://www.w3.org/2000/01/rdf-schema#label"
1035        );
1036    }
1037
1038    #[test]
1039    fn test_vocab_rdfs_comment() {
1040        assert!(vocab::rdfs::comment().as_str().ends_with("comment"));
1041    }
1042
1043    #[test]
1044    fn test_vocab_rdfs_sub_class_of() {
1045        assert!(vocab::rdfs::sub_class_of().as_str().ends_with("subClassOf"));
1046    }
1047
1048    #[test]
1049    fn test_vocab_rdfs_domain_range() {
1050        assert!(vocab::rdfs::domain().as_str().ends_with("domain"));
1051        assert!(vocab::rdfs::range().as_str().ends_with("range"));
1052    }
1053
1054    #[test]
1055    fn test_vocab_owl_class() {
1056        assert_eq!(
1057            vocab::owl::class().as_str(),
1058            "http://www.w3.org/2002/07/owl#Class"
1059        );
1060    }
1061
1062    #[test]
1063    fn test_vocab_owl_same_as() {
1064        assert!(vocab::owl::same_as().as_str().ends_with("sameAs"));
1065    }
1066
1067    #[test]
1068    fn test_vocab_owl_thing_nothing() {
1069        assert!(vocab::owl::thing().as_str().ends_with("Thing"));
1070        assert!(vocab::owl::nothing().as_str().ends_with("Nothing"));
1071    }
1072
1073    #[test]
1074    fn test_vocab_owl_object_property() {
1075        assert!(vocab::owl::object_property()
1076            .as_str()
1077            .ends_with("ObjectProperty"));
1078    }
1079
1080    #[test]
1081    fn test_vocab_xsd_string() {
1082        assert!(vocab::xsd::string().as_str().ends_with("string"));
1083    }
1084
1085    #[test]
1086    fn test_vocab_foaf_name() {
1087        assert_eq!(
1088            vocab::foaf::name().as_str(),
1089            "http://xmlns.com/foaf/0.1/name"
1090        );
1091    }
1092
1093    #[test]
1094    fn test_vocab_foaf_person() {
1095        assert!(vocab::foaf::person().as_str().ends_with("Person"));
1096    }
1097
1098    #[test]
1099    fn test_vocab_foaf_knows() {
1100        assert!(vocab::foaf::knows().as_str().ends_with("knows"));
1101    }
1102
1103    // ── Round-trip tests ──────────────────────────────────────────────────────
1104
1105    #[test]
1106    fn test_roundtrip_named_node_via_string() {
1107        let iri = "http://example.org/roundtrip";
1108        let n = DataFactory::named_node(iri).expect("valid IRI for named node");
1109        let s = n.as_str().to_string();
1110        let n2 = DataFactory::named_node(s).expect("valid IRI for named node");
1111        assert_eq!(n, n2);
1112    }
1113
1114    #[test]
1115    fn test_roundtrip_typed_literal() {
1116        let l = DataFactory::typed_literal("123", xsd_types::integer());
1117        let val = l.value().to_string();
1118        let dt = l.datatype().into_owned();
1119        let l2 = DataFactory::typed_literal(val, dt);
1120        assert_eq!(l.value(), l2.value());
1121    }
1122
1123    #[test]
1124    fn test_roundtrip_language_literal() {
1125        let l = DataFactory::language_literal("Hola", "es").expect("valid language literal");
1126        let val = l.value().to_string();
1127        let lang = l.language().expect("operation should succeed").to_string();
1128        let l2 = DataFactory::language_literal(val, lang).expect("valid language literal");
1129        assert_eq!(l.value(), l2.value());
1130        assert_eq!(l.language(), l2.language());
1131    }
1132
1133    #[test]
1134    fn test_roundtrip_blank_node_with_id() {
1135        let b = DataFactory::blank_node_with_id("stable");
1136        let id = b.as_str().to_string();
1137        let b2 = DataFactory::blank_node_with_id(id.clone());
1138        assert_eq!(b2.as_str(), id);
1139    }
1140
1141    #[test]
1142    fn test_quad_default_graph_roundtrip() {
1143        let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
1144        let p = vocab::rdf::r#type();
1145        let o = vocab::owl::class();
1146        let g = DataFactory::default_graph();
1147        let q = DataFactory::quad(s.into(), p, o.into(), g.clone());
1148        // graph name is default
1149        assert!(matches!(q.graph_name(), GraphName::DefaultGraph));
1150    }
1151
1152    #[test]
1153    fn test_namespace_constants() {
1154        assert!(vocab::rdf::NAMESPACE.starts_with("http://"));
1155        assert!(vocab::rdfs::NAMESPACE.starts_with("http://"));
1156        assert!(vocab::owl::NAMESPACE.starts_with("http://"));
1157        assert!(vocab::xsd::NAMESPACE.starts_with("http://"));
1158        assert!(vocab::foaf::NAMESPACE.starts_with("http://"));
1159    }
1160
1161    #[test]
1162    fn test_xsd_types_all_in_xsd_namespace() {
1163        let checks = [
1164            xsd_types::string(),
1165            xsd_types::integer(),
1166            xsd_types::float(),
1167            xsd_types::double(),
1168            xsd_types::boolean(),
1169            xsd_types::date_time(),
1170            xsd_types::date(),
1171            xsd_types::decimal(),
1172            xsd_types::long(),
1173            xsd_types::int(),
1174            xsd_types::short(),
1175            xsd_types::byte(),
1176            xsd_types::unsigned_long(),
1177            xsd_types::unsigned_int(),
1178            xsd_types::non_negative_integer(),
1179            xsd_types::positive_integer(),
1180            xsd_types::any_uri(),
1181            xsd_types::base64_binary(),
1182            xsd_types::hex_binary(),
1183            xsd_types::g_year(),
1184            xsd_types::duration(),
1185            xsd_types::time(),
1186            xsd_types::normalized_string(),
1187            xsd_types::token(),
1188        ];
1189        for node in &checks {
1190            assert!(
1191                node.as_str()
1192                    .starts_with("http://www.w3.org/2001/XMLSchema#"),
1193                "Not in XSD namespace: {}",
1194                node.as_str()
1195            );
1196        }
1197    }
1198
1199    #[test]
1200    fn test_multiple_blank_nodes_in_triple() {
1201        let s: Subject = DataFactory::blank_node().into();
1202        let p = vocab::rdf::r#type();
1203        let o: Object = DataFactory::blank_node().into();
1204        let _t = DataFactory::triple(s, p, o);
1205    }
1206
1207    #[test]
1208    fn test_literal_with_unicode_value() {
1209        let l = DataFactory::literal("日本語テスト");
1210        assert_eq!(l.value(), "日本語テスト");
1211    }
1212
1213    #[test]
1214    fn test_typed_literal_float() {
1215        let l = DataFactory::typed_literal("1.5", xsd_types::float());
1216        assert!(l.datatype().as_str().ends_with("float"));
1217    }
1218
1219    #[test]
1220    fn test_typed_literal_decimal() {
1221        let l = DataFactory::typed_literal("9.99", xsd_types::decimal());
1222        assert!(l.datatype().as_str().ends_with("decimal"));
1223    }
1224
1225    #[test]
1226    fn test_rdfs_all_vocabs_are_valid_iris() {
1227        let nodes = [
1228            vocab::rdfs::label(),
1229            vocab::rdfs::comment(),
1230            vocab::rdfs::sub_class_of(),
1231            vocab::rdfs::sub_property_of(),
1232            vocab::rdfs::domain(),
1233            vocab::rdfs::range(),
1234            vocab::rdfs::class(),
1235            vocab::rdfs::resource(),
1236            vocab::rdfs::literal(),
1237            vocab::rdfs::datatype(),
1238            vocab::rdfs::is_defined_by(),
1239            vocab::rdfs::see_also(),
1240            vocab::rdfs::member(),
1241            vocab::rdfs::container(),
1242        ];
1243        for n in &nodes {
1244            assert!(
1245                DataFactory::validate_iri(n.as_str()).is_ok(),
1246                "Invalid IRI for vocab node: {}",
1247                n.as_str()
1248            );
1249        }
1250    }
1251
1252    #[test]
1253    fn test_rdf_all_vocabs_are_valid_iris() {
1254        let nodes = [
1255            vocab::rdf::r#type(),
1256            vocab::rdf::subject(),
1257            vocab::rdf::predicate(),
1258            vocab::rdf::object(),
1259            vocab::rdf::property(),
1260            vocab::rdf::statement(),
1261            vocab::rdf::first(),
1262            vocab::rdf::rest(),
1263            vocab::rdf::nil(),
1264            vocab::rdf::list(),
1265            vocab::rdf::bag(),
1266            vocab::rdf::seq(),
1267            vocab::rdf::alt(),
1268            vocab::rdf::value(),
1269            vocab::rdf::lang_string(),
1270        ];
1271        for n in &nodes {
1272            assert!(
1273                DataFactory::validate_iri(n.as_str()).is_ok(),
1274                "Invalid IRI: {}",
1275                n.as_str()
1276            );
1277        }
1278    }
1279
1280    #[test]
1281    fn test_owl_all_vocabs_are_valid_iris() {
1282        let nodes = [
1283            vocab::owl::class(),
1284            vocab::owl::object_property(),
1285            vocab::owl::datatype_property(),
1286            vocab::owl::annotation_property(),
1287            vocab::owl::thing(),
1288            vocab::owl::nothing(),
1289            vocab::owl::same_as(),
1290            vocab::owl::equivalent_class(),
1291            vocab::owl::equivalent_property(),
1292            vocab::owl::inverse_of(),
1293            vocab::owl::disjoint_with(),
1294            vocab::owl::functional_property(),
1295            vocab::owl::ontology(),
1296        ];
1297        for n in &nodes {
1298            assert!(
1299                DataFactory::validate_iri(n.as_str()).is_ok(),
1300                "Invalid IRI: {}",
1301                n.as_str()
1302            );
1303        }
1304    }
1305}