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        assert_eq!(l.language(), Some("zh-hans"));
779    }
780
781    #[test]
782    fn test_language_literal_en_us() {
783        let l = DataFactory::language_literal("Color", "en-US").expect("valid language literal");
784        assert_eq!(l.language(), Some("en-us"));
785    }
786
787    #[test]
788    fn test_language_literal_empty_lang_is_err() {
789        assert!(DataFactory::language_literal("hello", "").is_err());
790    }
791
792    #[test]
793    fn test_language_literal_invalid_lang_is_err() {
794        // Contains space → invalid
795        assert!(DataFactory::language_literal("hello", "en US").is_err());
796    }
797
798    // ── Triples ───────────────────────────────────────────────────────────────
799
800    #[test]
801    fn test_triple_subject_predicate_object() {
802        let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
803        let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
804        let o = DataFactory::named_node("http://example.org/o").expect("valid IRI for named node");
805        let t = DataFactory::triple(s.into(), p.clone(), o.into());
806        // Access via Display
807        let text = format!("{t}");
808        assert!(text.contains("http://example.org/s"));
809    }
810
811    #[test]
812    fn test_triple_with_literal_object() {
813        let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
814        let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
815        let o: Object = DataFactory::literal("hello").into();
816        let t = DataFactory::triple(s.into(), p, o);
817        let text = format!("{t}");
818        assert!(text.contains("hello"));
819    }
820
821    #[test]
822    fn test_triple_with_blank_node_subject() {
823        let s: Subject = DataFactory::blank_node().into();
824        let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
825        let o: Object = DataFactory::literal("val").into();
826        let _t = DataFactory::triple(s, p, o);
827    }
828
829    // ── Quads ─────────────────────────────────────────────────────────────────
830
831    #[test]
832    fn test_quad_default_graph() {
833        let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
834        let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
835        let o = DataFactory::named_node("http://example.org/o").expect("valid IRI for named node");
836        let g = DataFactory::default_graph();
837        let q = DataFactory::quad(s.into(), p, o.into(), g);
838        let text = format!("{q}");
839        assert!(text.contains("http://example.org/s"));
840    }
841
842    #[test]
843    fn test_quad_named_graph() {
844        let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
845        let p = DataFactory::named_node("http://example.org/p").expect("valid IRI for named node");
846        let o = DataFactory::named_node("http://example.org/o").expect("valid IRI for named node");
847        let g = DataFactory::named_graph("http://example.org/graph1")
848            .expect("construction should succeed");
849        let q = DataFactory::quad(s.into(), p, o.into(), g);
850        let text = format!("{q}");
851        assert!(text.contains("graph1"));
852    }
853
854    #[test]
855    fn test_quad_named_graph_invalid_iri_is_err() {
856        assert!(DataFactory::named_graph("not an IRI").is_err());
857    }
858
859    #[test]
860    fn test_default_graph_is_default_graph_variant() {
861        let g = DataFactory::default_graph();
862        assert!(matches!(g, GraphName::DefaultGraph));
863    }
864
865    // ── IRI validation ────────────────────────────────────────────────────────
866
867    #[test]
868    fn test_validate_iri_http_ok() {
869        assert!(DataFactory::validate_iri("http://example.org/").is_ok());
870    }
871
872    #[test]
873    fn test_validate_iri_https_ok() {
874        assert!(DataFactory::validate_iri("https://example.org/path").is_ok());
875    }
876
877    #[test]
878    fn test_validate_iri_urn_ok() {
879        assert!(DataFactory::validate_iri("urn:isbn:0451450523").is_ok());
880    }
881
882    #[test]
883    fn test_validate_iri_bare_word_is_err() {
884        assert!(DataFactory::validate_iri("hello").is_err());
885    }
886
887    #[test]
888    fn test_validate_iri_empty_is_err() {
889        assert!(DataFactory::validate_iri("").is_err());
890    }
891
892    #[test]
893    fn test_validate_iri_space_is_err() {
894        assert!(DataFactory::validate_iri("http://example.org/hello world").is_err());
895    }
896
897    // ── Language tag validation ───────────────────────────────────────────────
898
899    #[test]
900    fn test_validate_lang_tag_en_ok() {
901        assert!(DataFactory::validate_lang_tag("en").is_ok());
902    }
903
904    #[test]
905    fn test_validate_lang_tag_en_us_ok() {
906        assert!(DataFactory::validate_lang_tag("en-US").is_ok());
907    }
908
909    #[test]
910    fn test_validate_lang_tag_zh_hans_cn_ok() {
911        assert!(DataFactory::validate_lang_tag("zh-Hans-CN").is_ok());
912    }
913
914    #[test]
915    fn test_validate_lang_tag_empty_is_err() {
916        assert!(DataFactory::validate_lang_tag("").is_err());
917    }
918
919    #[test]
920    fn test_validate_lang_tag_space_is_err() {
921        assert!(DataFactory::validate_lang_tag("en US").is_err());
922    }
923
924    #[test]
925    fn test_validate_lang_tag_double_dash_is_err() {
926        assert!(DataFactory::validate_lang_tag("en--US").is_err());
927    }
928
929    #[test]
930    fn test_validate_lang_tag_numeric_primary_is_err() {
931        // primary subtag must be alphabetic
932        assert!(DataFactory::validate_lang_tag("123").is_err());
933    }
934
935    // ── XSD datatype helpers ──────────────────────────────────────────────────
936
937    #[test]
938    fn test_xsd_string_iri() {
939        assert_eq!(
940            xsd_types::string().as_str(),
941            "http://www.w3.org/2001/XMLSchema#string"
942        );
943    }
944
945    #[test]
946    fn test_xsd_integer_iri() {
947        assert_eq!(
948            xsd_types::integer().as_str(),
949            "http://www.w3.org/2001/XMLSchema#integer"
950        );
951    }
952
953    #[test]
954    fn test_xsd_float_iri() {
955        assert!(xsd_types::float().as_str().ends_with("float"));
956    }
957
958    #[test]
959    fn test_xsd_double_iri() {
960        assert!(xsd_types::double().as_str().ends_with("double"));
961    }
962
963    #[test]
964    fn test_xsd_boolean_iri() {
965        assert!(xsd_types::boolean().as_str().ends_with("boolean"));
966    }
967
968    #[test]
969    fn test_xsd_date_time_iri() {
970        assert!(xsd_types::date_time().as_str().ends_with("dateTime"));
971    }
972
973    #[test]
974    fn test_xsd_date_iri() {
975        assert!(xsd_types::date().as_str().ends_with("date"));
976    }
977
978    #[test]
979    fn test_xsd_decimal_iri() {
980        assert!(xsd_types::decimal().as_str().ends_with("decimal"));
981    }
982
983    #[test]
984    fn test_xsd_long_iri() {
985        assert!(xsd_types::long().as_str().ends_with("long"));
986    }
987
988    #[test]
989    fn test_xsd_int_iri() {
990        assert!(xsd_types::int().as_str().ends_with("#int"));
991    }
992
993    #[test]
994    fn test_xsd_any_uri_iri() {
995        assert!(xsd_types::any_uri().as_str().ends_with("anyURI"));
996    }
997
998    #[test]
999    fn test_xsd_base64_binary_iri() {
1000        assert!(xsd_types::base64_binary()
1001            .as_str()
1002            .ends_with("base64Binary"));
1003    }
1004
1005    #[test]
1006    fn test_xsd_hex_binary_iri() {
1007        assert!(xsd_types::hex_binary().as_str().ends_with("hexBinary"));
1008    }
1009
1010    // ── Vocabulary constants ──────────────────────────────────────────────────
1011
1012    #[test]
1013    fn test_vocab_rdf_type() {
1014        assert_eq!(
1015            vocab::rdf::r#type().as_str(),
1016            "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
1017        );
1018    }
1019
1020    #[test]
1021    fn test_vocab_rdf_first_last() {
1022        assert!(vocab::rdf::first().as_str().ends_with("first"));
1023        assert!(vocab::rdf::rest().as_str().ends_with("rest"));
1024        assert!(vocab::rdf::nil().as_str().ends_with("nil"));
1025    }
1026
1027    #[test]
1028    fn test_vocab_rdfs_label() {
1029        assert_eq!(
1030            vocab::rdfs::label().as_str(),
1031            "http://www.w3.org/2000/01/rdf-schema#label"
1032        );
1033    }
1034
1035    #[test]
1036    fn test_vocab_rdfs_comment() {
1037        assert!(vocab::rdfs::comment().as_str().ends_with("comment"));
1038    }
1039
1040    #[test]
1041    fn test_vocab_rdfs_sub_class_of() {
1042        assert!(vocab::rdfs::sub_class_of().as_str().ends_with("subClassOf"));
1043    }
1044
1045    #[test]
1046    fn test_vocab_rdfs_domain_range() {
1047        assert!(vocab::rdfs::domain().as_str().ends_with("domain"));
1048        assert!(vocab::rdfs::range().as_str().ends_with("range"));
1049    }
1050
1051    #[test]
1052    fn test_vocab_owl_class() {
1053        assert_eq!(
1054            vocab::owl::class().as_str(),
1055            "http://www.w3.org/2002/07/owl#Class"
1056        );
1057    }
1058
1059    #[test]
1060    fn test_vocab_owl_same_as() {
1061        assert!(vocab::owl::same_as().as_str().ends_with("sameAs"));
1062    }
1063
1064    #[test]
1065    fn test_vocab_owl_thing_nothing() {
1066        assert!(vocab::owl::thing().as_str().ends_with("Thing"));
1067        assert!(vocab::owl::nothing().as_str().ends_with("Nothing"));
1068    }
1069
1070    #[test]
1071    fn test_vocab_owl_object_property() {
1072        assert!(vocab::owl::object_property()
1073            .as_str()
1074            .ends_with("ObjectProperty"));
1075    }
1076
1077    #[test]
1078    fn test_vocab_xsd_string() {
1079        assert!(vocab::xsd::string().as_str().ends_with("string"));
1080    }
1081
1082    #[test]
1083    fn test_vocab_foaf_name() {
1084        assert_eq!(
1085            vocab::foaf::name().as_str(),
1086            "http://xmlns.com/foaf/0.1/name"
1087        );
1088    }
1089
1090    #[test]
1091    fn test_vocab_foaf_person() {
1092        assert!(vocab::foaf::person().as_str().ends_with("Person"));
1093    }
1094
1095    #[test]
1096    fn test_vocab_foaf_knows() {
1097        assert!(vocab::foaf::knows().as_str().ends_with("knows"));
1098    }
1099
1100    // ── Round-trip tests ──────────────────────────────────────────────────────
1101
1102    #[test]
1103    fn test_roundtrip_named_node_via_string() {
1104        let iri = "http://example.org/roundtrip";
1105        let n = DataFactory::named_node(iri).expect("valid IRI for named node");
1106        let s = n.as_str().to_string();
1107        let n2 = DataFactory::named_node(s).expect("valid IRI for named node");
1108        assert_eq!(n, n2);
1109    }
1110
1111    #[test]
1112    fn test_roundtrip_typed_literal() {
1113        let l = DataFactory::typed_literal("123", xsd_types::integer());
1114        let val = l.value().to_string();
1115        let dt = l.datatype().into_owned();
1116        let l2 = DataFactory::typed_literal(val, dt);
1117        assert_eq!(l.value(), l2.value());
1118    }
1119
1120    #[test]
1121    fn test_roundtrip_language_literal() {
1122        let l = DataFactory::language_literal("Hola", "es").expect("valid language literal");
1123        let val = l.value().to_string();
1124        let lang = l.language().expect("operation should succeed").to_string();
1125        let l2 = DataFactory::language_literal(val, lang).expect("valid language literal");
1126        assert_eq!(l.value(), l2.value());
1127        assert_eq!(l.language(), l2.language());
1128    }
1129
1130    #[test]
1131    fn test_roundtrip_blank_node_with_id() {
1132        let b = DataFactory::blank_node_with_id("stable");
1133        let id = b.as_str().to_string();
1134        let b2 = DataFactory::blank_node_with_id(id.clone());
1135        assert_eq!(b2.as_str(), id);
1136    }
1137
1138    #[test]
1139    fn test_quad_default_graph_roundtrip() {
1140        let s = DataFactory::named_node("http://example.org/s").expect("valid IRI for named node");
1141        let p = vocab::rdf::r#type();
1142        let o = vocab::owl::class();
1143        let g = DataFactory::default_graph();
1144        let q = DataFactory::quad(s.into(), p, o.into(), g.clone());
1145        // graph name is default
1146        assert!(matches!(q.graph_name(), GraphName::DefaultGraph));
1147    }
1148
1149    #[test]
1150    fn test_namespace_constants() {
1151        assert!(vocab::rdf::NAMESPACE.starts_with("http://"));
1152        assert!(vocab::rdfs::NAMESPACE.starts_with("http://"));
1153        assert!(vocab::owl::NAMESPACE.starts_with("http://"));
1154        assert!(vocab::xsd::NAMESPACE.starts_with("http://"));
1155        assert!(vocab::foaf::NAMESPACE.starts_with("http://"));
1156    }
1157
1158    #[test]
1159    fn test_xsd_types_all_in_xsd_namespace() {
1160        let checks = [
1161            xsd_types::string(),
1162            xsd_types::integer(),
1163            xsd_types::float(),
1164            xsd_types::double(),
1165            xsd_types::boolean(),
1166            xsd_types::date_time(),
1167            xsd_types::date(),
1168            xsd_types::decimal(),
1169            xsd_types::long(),
1170            xsd_types::int(),
1171            xsd_types::short(),
1172            xsd_types::byte(),
1173            xsd_types::unsigned_long(),
1174            xsd_types::unsigned_int(),
1175            xsd_types::non_negative_integer(),
1176            xsd_types::positive_integer(),
1177            xsd_types::any_uri(),
1178            xsd_types::base64_binary(),
1179            xsd_types::hex_binary(),
1180            xsd_types::g_year(),
1181            xsd_types::duration(),
1182            xsd_types::time(),
1183            xsd_types::normalized_string(),
1184            xsd_types::token(),
1185        ];
1186        for node in &checks {
1187            assert!(
1188                node.as_str()
1189                    .starts_with("http://www.w3.org/2001/XMLSchema#"),
1190                "Not in XSD namespace: {}",
1191                node.as_str()
1192            );
1193        }
1194    }
1195
1196    #[test]
1197    fn test_multiple_blank_nodes_in_triple() {
1198        let s: Subject = DataFactory::blank_node().into();
1199        let p = vocab::rdf::r#type();
1200        let o: Object = DataFactory::blank_node().into();
1201        let _t = DataFactory::triple(s, p, o);
1202    }
1203
1204    #[test]
1205    fn test_literal_with_unicode_value() {
1206        let l = DataFactory::literal("日本語テスト");
1207        assert_eq!(l.value(), "日本語テスト");
1208    }
1209
1210    #[test]
1211    fn test_typed_literal_float() {
1212        let l = DataFactory::typed_literal("1.5", xsd_types::float());
1213        assert!(l.datatype().as_str().ends_with("float"));
1214    }
1215
1216    #[test]
1217    fn test_typed_literal_decimal() {
1218        let l = DataFactory::typed_literal("9.99", xsd_types::decimal());
1219        assert!(l.datatype().as_str().ends_with("decimal"));
1220    }
1221
1222    #[test]
1223    fn test_rdfs_all_vocabs_are_valid_iris() {
1224        let nodes = [
1225            vocab::rdfs::label(),
1226            vocab::rdfs::comment(),
1227            vocab::rdfs::sub_class_of(),
1228            vocab::rdfs::sub_property_of(),
1229            vocab::rdfs::domain(),
1230            vocab::rdfs::range(),
1231            vocab::rdfs::class(),
1232            vocab::rdfs::resource(),
1233            vocab::rdfs::literal(),
1234            vocab::rdfs::datatype(),
1235            vocab::rdfs::is_defined_by(),
1236            vocab::rdfs::see_also(),
1237            vocab::rdfs::member(),
1238            vocab::rdfs::container(),
1239        ];
1240        for n in &nodes {
1241            assert!(
1242                DataFactory::validate_iri(n.as_str()).is_ok(),
1243                "Invalid IRI for vocab node: {}",
1244                n.as_str()
1245            );
1246        }
1247    }
1248
1249    #[test]
1250    fn test_rdf_all_vocabs_are_valid_iris() {
1251        let nodes = [
1252            vocab::rdf::r#type(),
1253            vocab::rdf::subject(),
1254            vocab::rdf::predicate(),
1255            vocab::rdf::object(),
1256            vocab::rdf::property(),
1257            vocab::rdf::statement(),
1258            vocab::rdf::first(),
1259            vocab::rdf::rest(),
1260            vocab::rdf::nil(),
1261            vocab::rdf::list(),
1262            vocab::rdf::bag(),
1263            vocab::rdf::seq(),
1264            vocab::rdf::alt(),
1265            vocab::rdf::value(),
1266            vocab::rdf::lang_string(),
1267        ];
1268        for n in &nodes {
1269            assert!(
1270                DataFactory::validate_iri(n.as_str()).is_ok(),
1271                "Invalid IRI: {}",
1272                n.as_str()
1273            );
1274        }
1275    }
1276
1277    #[test]
1278    fn test_owl_all_vocabs_are_valid_iris() {
1279        let nodes = [
1280            vocab::owl::class(),
1281            vocab::owl::object_property(),
1282            vocab::owl::datatype_property(),
1283            vocab::owl::annotation_property(),
1284            vocab::owl::thing(),
1285            vocab::owl::nothing(),
1286            vocab::owl::same_as(),
1287            vocab::owl::equivalent_class(),
1288            vocab::owl::equivalent_property(),
1289            vocab::owl::inverse_of(),
1290            vocab::owl::disjoint_with(),
1291            vocab::owl::functional_property(),
1292            vocab::owl::ontology(),
1293        ];
1294        for n in &nodes {
1295            assert!(
1296                DataFactory::validate_iri(n.as_str()).is_ok(),
1297                "Invalid IRI: {}",
1298                n.as_str()
1299            );
1300        }
1301    }
1302}