Skip to main content

oxirs_core/format/
turtle.rs

1//! Turtle Format Parser and Serializer
2//!
3//! Extracted and adapted from OxiGraph oxttl with OxiRS enhancements.
4//! Based on W3C Turtle specification: <https://www.w3.org/TR/turtle/>
5
6use super::error::SerializeResult;
7use super::error::{ParseResult, RdfParseError};
8use super::serializer::QuadSerializer;
9use crate::model::{QuadRef, Triple, TripleRef};
10use std::collections::HashMap;
11use std::io::{Read, Write};
12
13/// Turtle parser implementation
14#[derive(Debug, Clone)]
15pub struct TurtleParser {
16    lenient: bool,
17    base_iri: Option<String>,
18    prefixes: HashMap<String, String>,
19}
20
21impl TurtleParser {
22    /// Create a new Turtle parser
23    pub fn new() -> Self {
24        Self {
25            lenient: false,
26            base_iri: None,
27            prefixes: HashMap::new(),
28        }
29    }
30
31    /// Enable lenient parsing (skip some validations)
32    pub fn lenient(mut self) -> Self {
33        self.lenient = true;
34        self
35    }
36
37    /// Set base IRI for resolving relative IRIs
38    pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Self {
39        self.base_iri = Some(base_iri.into());
40        self
41    }
42
43    /// Add a namespace prefix
44    pub fn with_prefix(mut self, prefix: impl Into<String>, iri: impl Into<String>) -> Self {
45        self.prefixes.insert(prefix.into(), iri.into());
46        self
47    }
48
49    /// Parse Turtle from a reader
50    pub fn parse_reader<R: Read>(&self, mut reader: R) -> ParseResult<Vec<Triple>> {
51        // Read all data from the reader
52        let mut buffer = String::new();
53        reader.read_to_string(&mut buffer)?;
54
55        // Use the string parser (handles basic Turtle syntax)
56        // Note: Current implementation handles simple triples, prefixes, and base directives
57        // Advanced Turtle features (collections, lists, multi-line literals) are partially supported
58        self.parse_str(&buffer)
59    }
60
61    /// Parse Turtle from a byte slice
62    pub fn parse_slice(&self, slice: &[u8]) -> ParseResult<Vec<Triple>> {
63        // Convert to string and parse
64        // Note: Future optimization could use zero-copy parsing with byte-level operations
65        let content = std::str::from_utf8(slice)
66            .map_err(|e| RdfParseError::syntax(format!("Invalid UTF-8: {e}")))?;
67
68        self.parse_str(content)
69    }
70
71    /// Parse Turtle from a string
72    pub fn parse_str(&self, input: &str) -> ParseResult<Vec<Triple>> {
73        use super::parser::helpers::convert_quad;
74        use std::io::Cursor;
75
76        // Build oxttl parser with configuration
77        let mut oxttl_parser = oxttl::TurtleParser::new();
78
79        // Apply base IRI if set
80        if let Some(ref base) = self.base_iri {
81            oxttl_parser = oxttl_parser
82                .with_base_iri(base.as_str())
83                .unwrap_or_else(|_| oxttl::TurtleParser::new());
84        }
85
86        // Enable lenient mode if requested
87        if self.lenient {
88            oxttl_parser = oxttl_parser.lenient();
89        }
90
91        // Parse and collect triples
92        let reader = Cursor::new(input.as_bytes());
93        let mut triples = Vec::new();
94
95        for result in oxttl_parser.for_reader(reader) {
96            match result {
97                Ok(triple) => {
98                    // Convert oxrdf Triple to oxirs Triple via Quad
99                    let quad = oxrdf::Quad::new(
100                        triple.subject,
101                        triple.predicate,
102                        triple.object,
103                        oxrdf::GraphName::DefaultGraph,
104                    );
105                    let oxirs_quad = convert_quad(quad)?;
106                    triples.push(oxirs_quad.to_triple());
107                }
108                Err(e) => {
109                    if !self.lenient {
110                        return Err(RdfParseError::syntax(e.to_string()));
111                    }
112                    // In lenient mode, skip errors
113                }
114            }
115        }
116
117        Ok(triples)
118    }
119
120    /// Get current prefixes
121    pub fn prefixes(&self) -> &HashMap<String, String> {
122        &self.prefixes
123    }
124
125    /// Get current base IRI
126    pub fn base_iri(&self) -> Option<&str> {
127        self.base_iri.as_deref()
128    }
129
130    /// Check if lenient parsing is enabled
131    pub fn is_lenient(&self) -> bool {
132        self.lenient
133    }
134}
135
136impl Default for TurtleParser {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142/// Turtle serializer implementation
143#[derive(Debug, Clone)]
144pub struct TurtleSerializer {
145    base_iri: Option<String>,
146    prefixes: HashMap<String, String>,
147    pretty: bool,
148}
149
150impl TurtleSerializer {
151    /// Create a new Turtle serializer
152    pub fn new() -> Self {
153        Self {
154            base_iri: None,
155            prefixes: HashMap::new(),
156            pretty: false,
157        }
158    }
159
160    /// Set base IRI for generating relative IRIs
161    pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Self {
162        self.base_iri = Some(base_iri.into());
163        self
164    }
165
166    /// Add a namespace prefix
167    pub fn with_prefix(mut self, prefix: impl Into<String>, iri: impl Into<String>) -> Self {
168        self.prefixes.insert(prefix.into(), iri.into());
169        self
170    }
171
172    /// Enable pretty formatting
173    pub fn pretty(mut self) -> Self {
174        self.pretty = true;
175        self
176    }
177
178    /// Create a writer-based serializer
179    pub fn for_writer<W: Write>(self, writer: W) -> WriterTurtleSerializer<W> {
180        WriterTurtleSerializer::new(writer, self)
181    }
182
183    /// Serialize triples to a string
184    pub fn serialize_to_string(&self, triples: &[Triple]) -> SerializeResult<String> {
185        let mut buffer = Vec::new();
186        {
187            let mut serializer = self.clone().for_writer(&mut buffer);
188            for triple in triples {
189                serializer.serialize_triple(triple.as_ref())?;
190            }
191            serializer.finish()?;
192        }
193        String::from_utf8(buffer)
194            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
195    }
196
197    /// Get the prefixes
198    pub fn prefixes(&self) -> &HashMap<String, String> {
199        &self.prefixes
200    }
201
202    /// Get the base IRI
203    pub fn base_iri(&self) -> Option<&str> {
204        self.base_iri.as_deref()
205    }
206
207    /// Check if pretty formatting is enabled
208    pub fn is_pretty(&self) -> bool {
209        self.pretty
210    }
211}
212
213impl Default for TurtleSerializer {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219/// Writer-based Turtle serializer
220pub struct WriterTurtleSerializer<W: Write> {
221    writer: W,
222    config: TurtleSerializer,
223    headers_written: bool,
224}
225
226impl<W: Write> WriterTurtleSerializer<W> {
227    /// Create a new writer serializer
228    pub fn new(writer: W, config: TurtleSerializer) -> Self {
229        Self {
230            writer,
231            config,
232            headers_written: false,
233        }
234    }
235
236    /// Serialize a triple
237    pub fn serialize_triple(&mut self, triple: TripleRef<'_>) -> SerializeResult<()> {
238        self.ensure_headers_written()?;
239
240        // Subject serialization
241        let subject_str = self.serialize_subject(triple.subject())?;
242
243        // Predicate serialization
244        let predicate_str = self.serialize_predicate(triple.predicate())?;
245
246        // Object serialization
247        let object_str = self.serialize_object(triple.object())?;
248
249        // Write the triple with proper formatting
250        if self.config.pretty {
251            writeln!(self.writer, "{subject_str} {predicate_str} {object_str} .")?;
252        } else {
253            writeln!(self.writer, "{subject_str} {predicate_str} {object_str}.")?;
254        }
255
256        Ok(())
257    }
258
259    /// Serialize a subject (NamedNode, BlankNode, or Variable)
260    fn serialize_subject(&self, subject: crate::model::SubjectRef<'_>) -> SerializeResult<String> {
261        use crate::model::SubjectRef;
262
263        match subject {
264            SubjectRef::NamedNode(node) => self.serialize_named_node(node.into()),
265            SubjectRef::BlankNode(node) => {
266                let node_str = node.as_str();
267                Ok(format!("_:{node_str}"))
268            }
269            SubjectRef::Variable(var) => {
270                let var_str = var.as_str();
271                Ok(format!("?{var_str}"))
272            }
273            SubjectRef::QuotedTriple(qt) => self.serialize_quoted_triple(qt.inner()),
274        }
275    }
276
277    /// Serialize an RDF-star quoted triple as `<< s p o >>`
278    fn serialize_quoted_triple(&self, inner: &crate::model::Triple) -> SerializeResult<String> {
279        let s = self.serialize_subject(inner.subject().into())?;
280        let p = self.serialize_predicate(inner.predicate().into())?;
281        let o = self.serialize_object(inner.object().into())?;
282        Ok(format!("<< {s} {p} {o} >>"))
283    }
284
285    /// Serialize a predicate (NamedNode or Variable)
286    fn serialize_predicate(
287        &self,
288        predicate: crate::model::PredicateRef<'_>,
289    ) -> SerializeResult<String> {
290        use crate::model::PredicateRef;
291
292        match predicate {
293            PredicateRef::NamedNode(node) => {
294                // Check for rdf:type shorthand
295                if node.as_str() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" {
296                    Ok("a".to_string())
297                } else {
298                    self.serialize_named_node(node.into())
299                }
300            }
301            PredicateRef::Variable(var) => {
302                let var_str = var.as_str();
303                Ok(format!("?{var_str}"))
304            }
305        }
306    }
307
308    /// Serialize an object (NamedNode, BlankNode, Literal, or Variable)
309    fn serialize_object(&self, object: crate::model::ObjectRef<'_>) -> SerializeResult<String> {
310        use crate::model::ObjectRef;
311
312        match object {
313            ObjectRef::NamedNode(node) => self.serialize_named_node(node.into()),
314            ObjectRef::BlankNode(node) => {
315                let node_str = node.as_str();
316                Ok(format!("_:{node_str}"))
317            }
318            ObjectRef::Literal(literal) => self.serialize_literal(literal),
319            ObjectRef::Variable(var) => {
320                let var_str = var.as_str();
321                Ok(format!("?{var_str}"))
322            }
323            ObjectRef::QuotedTriple(qt) => self.serialize_quoted_triple(qt.inner()),
324        }
325    }
326
327    /// Serialize a named node with prefix abbreviation
328    fn serialize_named_node(
329        &self,
330        node: crate::model::NamedNodeRef<'_>,
331    ) -> SerializeResult<String> {
332        let iri = node.as_str();
333
334        // Try to find a matching prefix
335        for (prefix, namespace) in &self.config.prefixes {
336            if iri.starts_with(namespace) {
337                let local = &iri[namespace.len()..];
338                // Check if local part is valid for prefixed name
339                if is_valid_local_name(local) {
340                    return Ok(format!("{prefix}:{local}"));
341                }
342            }
343        }
344
345        // Fall back to full IRI in angle brackets
346        Ok(format!("<{iri}>"))
347    }
348
349    /// Serialize a literal
350    fn serialize_literal(&self, literal: &crate::model::Literal) -> SerializeResult<String> {
351        let value = literal.value();
352
353        // Escape special characters in the string
354        let escaped_value = escape_turtle_string(value);
355
356        // Handle language tag
357        if let Some(lang) = literal.language() {
358            return Ok(format!("\"{escaped_value}\"@{lang}"));
359        }
360
361        // Handle datatype
362        let datatype = literal.datatype();
363        if datatype.as_str() == "http://www.w3.org/2001/XMLSchema#string" {
364            // XSD string is the default, no need to specify
365            Ok(format!("\"{escaped_value}\""))
366        } else {
367            // Serialize datatype as IRI
368            let datatype_str = self.serialize_named_node(datatype)?;
369            Ok(format!("\"{escaped_value}\"^^{datatype_str}"))
370        }
371    }
372
373    /// Finish serialization and return the writer
374    pub fn finish(self) -> SerializeResult<W> {
375        Ok(self.writer)
376    }
377
378    /// Ensure headers (prefixes, base) are written
379    fn ensure_headers_written(&mut self) -> SerializeResult<()> {
380        if self.headers_written {
381            return Ok(());
382        }
383
384        // Write base directive
385        if let Some(base) = &self.config.base_iri {
386            writeln!(self.writer, "@base <{base}> .")?;
387        }
388
389        // Write prefix directives
390        for (prefix, iri) in &self.config.prefixes {
391            writeln!(self.writer, "@prefix {prefix}: <{iri}> .")?;
392        }
393
394        // Add blank line after headers if we wrote any
395        if self.config.base_iri.is_some() || !self.config.prefixes.is_empty() {
396            writeln!(self.writer)?;
397        }
398
399        self.headers_written = true;
400        Ok(())
401    }
402}
403
404impl<W: Write> QuadSerializer<W> for WriterTurtleSerializer<W> {
405    fn serialize_quad(&mut self, quad: QuadRef<'_>) -> SerializeResult<()> {
406        // Turtle cannot express named graphs. Rather than silently dropping
407        // quads outside the default graph (which would produce an
408        // incomplete-but-"successful" serialization), fail loudly so the
409        // caller knows to pick a graph-aware format (TriG, N-Quads) instead.
410        if quad.graph_name().is_default_graph() {
411            self.serialize_triple(quad.triple())
412        } else {
413            Err(std::io::Error::new(
414                std::io::ErrorKind::Unsupported,
415                format!(
416                    "Turtle cannot represent named graphs; quad has graph name {}. \
417                     Use TriG or N-Quads to serialize datasets with named graphs.",
418                    quad.graph_name()
419                ),
420            ))
421        }
422    }
423
424    fn finish(self: Box<Self>) -> SerializeResult<W> {
425        Ok(self.writer)
426    }
427}
428
429/// Check if a string is a valid local name for Turtle prefixed names
430fn is_valid_local_name(local: &str) -> bool {
431    if local.is_empty() {
432        return true; // Empty local names are allowed
433    }
434
435    // First character must be a name start char or underscore
436    let first_char = local
437        .chars()
438        .next()
439        .expect("local name validated to be non-empty");
440    if !is_pn_chars_base(first_char) && first_char != '_' {
441        return false;
442    }
443
444    // Rest of characters must be name chars, underscore, dot, or hyphen
445    for ch in local.chars().skip(1) {
446        if !is_pn_chars(ch) && ch != '.' && ch != '-' {
447            return false;
448        }
449    }
450
451    // Cannot end with a dot
452    !local.ends_with('.')
453}
454
455/// Check if character is a PN_CHARS_BASE (per Turtle grammar)
456fn is_pn_chars_base(ch: char) -> bool {
457    ch.is_ascii_alphabetic()
458        || ('\u{00C0}'..='\u{00D6}').contains(&ch)
459        || ('\u{00D8}'..='\u{00F6}').contains(&ch)
460        || ('\u{00F8}'..='\u{02FF}').contains(&ch)
461        || ('\u{0370}'..='\u{037D}').contains(&ch)
462        || ('\u{037F}'..='\u{1FFF}').contains(&ch)
463        || ('\u{200C}'..='\u{200D}').contains(&ch)
464        || ('\u{2070}'..='\u{218F}').contains(&ch)
465        || ('\u{2C00}'..='\u{2FEF}').contains(&ch)
466        || ('\u{3001}'..='\u{D7FF}').contains(&ch)
467        || ('\u{F900}'..='\u{FDCF}').contains(&ch)
468        || ('\u{FDF0}'..='\u{FFFD}').contains(&ch)
469}
470
471/// Check if character is a PN_CHARS (per Turtle grammar)
472fn is_pn_chars(ch: char) -> bool {
473    is_pn_chars_base(ch)
474        || ch == '_'
475        || ch.is_ascii_digit()
476        || ch == '\u{00B7}'
477        || ('\u{0300}'..='\u{036F}').contains(&ch)
478        || ('\u{203F}'..='\u{2040}').contains(&ch)
479}
480
481/// Escape special characters in Turtle strings
482fn escape_turtle_string(input: &str) -> String {
483    let mut result = String::with_capacity(input.len());
484
485    for ch in input.chars() {
486        match ch {
487            '"' => result.push_str("\\\""),
488            '\\' => result.push_str("\\\\"),
489            '\n' => result.push_str("\\n"),
490            '\r' => result.push_str("\\r"),
491            '\t' => result.push_str("\\t"),
492            '\x08' => result.push_str("\\b"), // backspace
493            '\x0C' => result.push_str("\\f"), // form feed
494            c if c.is_control() => {
495                // Escape other control characters as Unicode escape sequences
496                let code = c as u32;
497                result.push_str(&format!("\\u{code:04X}"));
498            }
499            c => result.push(c),
500        }
501    }
502
503    result
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn test_turtle_parser_creation() {
512        let parser = TurtleParser::new();
513        assert!(!parser.is_lenient());
514        assert!(parser.base_iri().is_none());
515        assert!(parser.prefixes().is_empty());
516    }
517
518    #[test]
519    fn test_turtle_parser_configuration() {
520        let parser = TurtleParser::new()
521            .lenient()
522            .with_base_iri("http://example.org/")
523            .with_prefix("ex", "http://example.org/ns#");
524
525        assert!(parser.is_lenient());
526        assert_eq!(parser.base_iri(), Some("http://example.org/"));
527        assert_eq!(
528            parser.prefixes().get("ex"),
529            Some(&"http://example.org/ns#".to_string())
530        );
531    }
532
533    #[test]
534    fn test_turtle_serializer_creation() {
535        let serializer = TurtleSerializer::new();
536        assert!(!serializer.is_pretty());
537        assert!(serializer.base_iri().is_none());
538        assert!(serializer.prefixes().is_empty());
539    }
540
541    #[test]
542    fn test_turtle_serializer_configuration() {
543        let serializer = TurtleSerializer::new()
544            .pretty()
545            .with_base_iri("http://example.org/")
546            .with_prefix("ex", "http://example.org/ns#");
547
548        assert!(serializer.is_pretty());
549        assert_eq!(serializer.base_iri(), Some("http://example.org/"));
550        assert_eq!(
551            serializer.prefixes().get("ex"),
552            Some(&"http://example.org/ns#".to_string())
553        );
554    }
555
556    #[test]
557    fn test_empty_turtle_parsing() {
558        let parser = TurtleParser::new();
559        let result = parser.parse_str("");
560        assert!(result.is_ok());
561        assert!(result.expect("should have value").is_empty());
562    }
563
564    #[test]
565    fn test_turtle_comments() {
566        let parser = TurtleParser::new();
567        let turtle = "# This is a comment\n# Another comment";
568        let result = parser.parse_str(turtle);
569        assert!(result.is_ok());
570        assert!(result.expect("should have value").is_empty());
571    }
572}