1#[cfg(feature = "async")]
164mod async_parser;
165
166#[cfg(feature = "async")]
167pub use async_parser::{AsyncRdfSink, AsyncStreamingParser, MemoryAsyncSink, ParseProgress};
168
169use crate::model::{
171 BlankNode, GraphName, Literal, NamedNode, Object, Predicate, Quad, Subject, Triple,
172};
173use crate::{OxirsError, Result};
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
177pub enum RdfFormat {
178 Turtle,
180 NTriples,
182 TriG,
184 NQuads,
186 RdfXml,
188 JsonLd,
190}
191
192impl RdfFormat {
193 pub fn from_extension(ext: &str) -> Option<Self> {
195 match ext.to_lowercase().as_str() {
196 "ttl" | "turtle" => Some(RdfFormat::Turtle),
197 "nt" | "ntriples" => Some(RdfFormat::NTriples),
198 "trig" => Some(RdfFormat::TriG),
199 "nq" | "nquads" => Some(RdfFormat::NQuads),
200 "rdf" | "xml" | "rdfxml" => Some(RdfFormat::RdfXml),
201 "jsonld" | "json-ld" => Some(RdfFormat::JsonLd),
202 _ => None,
203 }
204 }
205
206 pub fn media_type(&self) -> &'static str {
208 match self {
209 RdfFormat::Turtle => "text/turtle",
210 RdfFormat::NTriples => "application/n-triples",
211 RdfFormat::TriG => "application/trig",
212 RdfFormat::NQuads => "application/n-quads",
213 RdfFormat::RdfXml => "application/rdf+xml",
214 RdfFormat::JsonLd => "application/ld+json",
215 }
216 }
217
218 pub fn extension(&self) -> &'static str {
220 match self {
221 RdfFormat::Turtle => "ttl",
222 RdfFormat::NTriples => "nt",
223 RdfFormat::TriG => "trig",
224 RdfFormat::NQuads => "nq",
225 RdfFormat::RdfXml => "rdf",
226 RdfFormat::JsonLd => "jsonld",
227 }
228 }
229
230 pub fn supports_quads(&self) -> bool {
232 matches!(self, RdfFormat::TriG | RdfFormat::NQuads)
233 }
234}
235
236#[derive(Debug, Clone, Default)]
238pub struct ParserConfig {
239 pub base_iri: Option<String>,
241 pub ignore_errors: bool,
243 pub max_errors: Option<usize>,
245}
246
247#[derive(Debug, Clone)]
249pub struct Parser {
250 format: RdfFormat,
251 config: ParserConfig,
252}
253
254impl Parser {
255 pub fn new(format: RdfFormat) -> Self {
257 Parser {
258 format,
259 config: ParserConfig::default(),
260 }
261 }
262
263 pub fn with_config(format: RdfFormat, config: ParserConfig) -> Self {
265 Parser { format, config }
266 }
267
268 pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Self {
270 self.config.base_iri = Some(base_iri.into());
271 self
272 }
273
274 pub fn with_error_tolerance(mut self, ignore_errors: bool) -> Self {
276 self.config.ignore_errors = ignore_errors;
277 self
278 }
279
280 pub fn parse_str_to_quads(&self, data: &str) -> Result<Vec<Quad>> {
282 let mut quads = Vec::new();
283 self.parse_str_with_handler(data, |quad| {
284 quads.push(quad);
285 Ok(())
286 })?;
287 Ok(quads)
288 }
289
290 pub fn parse_str_to_triples(&self, data: &str) -> Result<Vec<Triple>> {
292 let quads = self.parse_str_to_quads(data)?;
293 Ok(quads
294 .into_iter()
295 .filter(|quad| quad.is_default_graph())
296 .map(|quad| quad.to_triple())
297 .collect())
298 }
299
300 pub fn parse_str_with_handler<F>(&self, data: &str, handler: F) -> Result<()>
302 where
303 F: FnMut(Quad) -> Result<()>,
304 {
305 match self.format {
306 RdfFormat::Turtle => self.parse_turtle(data, handler),
307 RdfFormat::NTriples => self.parse_ntriples(data, handler),
308 RdfFormat::TriG => self.parse_trig(data, handler),
309 RdfFormat::NQuads => self.parse_nquads(data, handler),
310 RdfFormat::RdfXml => self.parse_rdfxml(data, handler),
311 RdfFormat::JsonLd => self.parse_jsonld(data, handler),
312 }
313 }
314
315 pub fn parse_bytes_to_quads(&self, data: &[u8]) -> Result<Vec<Quad>> {
317 let data_str = std::str::from_utf8(data)
318 .map_err(|e| OxirsError::Parse(format!("Invalid UTF-8: {e}")))?;
319 self.parse_str_to_quads(data_str)
320 }
321
322 fn parse_turtle<F>(&self, data: &str, mut handler: F) -> Result<()>
323 where
324 F: FnMut(Quad) -> Result<()>,
325 {
326 let mut internal_parser = crate::format::RdfParser::new(crate::format::RdfFormat::Turtle);
333 if let Some(base) = &self.config.base_iri {
334 internal_parser = internal_parser.with_base_iri(base.clone());
335 }
336
337 for result in internal_parser.for_slice(data.as_bytes()) {
338 match result {
339 Ok(quad) => handler(quad)?,
340 Err(e) => {
341 if self.config.ignore_errors {
342 tracing::warn!("Turtle parse error: {e}");
343 continue;
344 } else {
345 return Err(OxirsError::Parse(format!("Turtle parse error: {e}")));
346 }
347 }
348 }
349 }
350
351 Ok(())
352 }
353
354 fn parse_ntriples<F>(&self, data: &str, mut handler: F) -> Result<()>
355 where
356 F: FnMut(Quad) -> Result<()>,
357 {
358 for (line_num, line) in data.lines().enumerate() {
359 let line = line.trim();
360
361 if line.is_empty() || line.starts_with('#') {
363 continue;
364 }
365
366 match self.parse_ntriples_line(line) {
368 Ok(Some(quad)) => {
369 handler(quad)?;
370 }
371 Ok(None) => {
372 continue;
374 }
375 Err(e) => {
376 if self.config.ignore_errors {
377 tracing::warn!("Parse error on line {}: {}", line_num + 1, e);
378 continue;
379 } else {
380 return Err(OxirsError::Parse(format!(
381 "Parse error on line {}: {}",
382 line_num + 1,
383 e
384 )));
385 }
386 }
387 }
388 }
389
390 Ok(())
391 }
392
393 pub fn parse_ntriples_line(&self, line: &str) -> Result<Option<Quad>> {
394 let line = line.trim();
396
397 if line.is_empty() || line.starts_with('#') {
398 return Ok(None);
399 }
400
401 if !line.ends_with('.') {
403 return Err(OxirsError::Parse("Line must end with '.'".to_string()));
404 }
405
406 let line = &line[..line.len() - 1].trim(); let tokens = self.tokenize_ntriples_line(line)?;
410
411 if tokens.len() != 3 {
412 return Err(OxirsError::Parse(format!(
413 "Expected 3 tokens (subject, predicate, object), found {}",
414 tokens.len()
415 )));
416 }
417
418 let subject = self.parse_subject(&tokens[0])?;
420
421 let predicate = self.parse_predicate(&tokens[1])?;
423
424 let object = self.parse_object(&tokens[2])?;
426
427 let triple = Triple::new(subject, predicate, object);
428 let quad = Quad::from_triple(triple);
429
430 Ok(Some(quad))
431 }
432
433 fn tokenize_ntriples_line(&self, line: &str) -> Result<Vec<String>> {
434 let mut tokens = Vec::new();
435 let mut current_token = String::new();
436 let mut in_quotes = false;
437 let mut escaped = false;
438 let mut chars = line.chars().peekable();
439
440 while let Some(c) = chars.next() {
441 if escaped {
442 current_token.push('\\');
444 current_token.push(c);
445 escaped = false;
446 } else if c == '\\' && in_quotes {
447 escaped = true;
448 } else if c == '"' && !escaped {
449 current_token.push(c);
450 if in_quotes {
451 if let Some(&'@') = chars.peek() {
453 current_token.push(chars.next().expect("peeked '@' should be available"));
455 while let Some(&next_char) = chars.peek() {
456 if next_char.is_alphanumeric() || next_char == '-' {
457 current_token
458 .push(chars.next().expect("peeked char should be available"));
459 } else {
460 break;
461 }
462 }
463 } else if chars.peek() == Some(&'^') {
464 chars.next(); if chars.peek() == Some(&'^') {
467 chars.next(); current_token.push_str("^^");
469 if chars.peek() == Some(&'<') {
470 for next_char in chars.by_ref() {
472 current_token.push(next_char);
473 if next_char == '>' {
474 break;
475 }
476 }
477 }
478 }
479 }
480 in_quotes = false;
481 } else {
482 in_quotes = true;
483 }
484 } else if c == '"' && escaped {
485 current_token.push(c);
487 escaped = false;
488 } else if c.is_whitespace() && !in_quotes {
489 if !current_token.is_empty() {
490 tokens.push(current_token.clone());
491 current_token.clear();
492 }
493 } else {
494 current_token.push(c);
495 }
496 }
497
498 if !current_token.is_empty() {
499 tokens.push(current_token);
500 }
501
502 Ok(tokens)
503 }
504
505 fn parse_subject(&self, token: &str) -> Result<Subject> {
506 if token.starts_with('<') && token.ends_with('>') {
507 let iri = &token[1..token.len() - 1];
508 let named_node = NamedNode::new(iri)?;
509 Ok(Subject::NamedNode(named_node))
510 } else if token.starts_with("_:") {
511 let blank_node = BlankNode::new(token)?;
512 Ok(Subject::BlankNode(blank_node))
513 } else {
514 Err(OxirsError::Parse(format!(
515 "Invalid subject: {token}. Must be IRI or blank node"
516 )))
517 }
518 }
519
520 fn parse_predicate(&self, token: &str) -> Result<Predicate> {
521 if token.starts_with('<') && token.ends_with('>') {
522 let iri = &token[1..token.len() - 1];
523 let named_node = NamedNode::new(iri)?;
524 Ok(Predicate::NamedNode(named_node))
525 } else {
526 Err(OxirsError::Parse(format!(
527 "Invalid predicate: {token}. Must be IRI"
528 )))
529 }
530 }
531
532 fn parse_object(&self, token: &str) -> Result<Object> {
533 if token.starts_with('<') && token.ends_with('>') {
534 let iri = &token[1..token.len() - 1];
536 let named_node = NamedNode::new(iri)?;
537 Ok(Object::NamedNode(named_node))
538 } else if token.starts_with("_:") {
539 let blank_node = BlankNode::new(token)?;
541 Ok(Object::BlankNode(blank_node))
542 } else if token.starts_with('"') {
543 self.parse_literal(token)
545 } else {
546 Err(OxirsError::Parse(format!(
547 "Invalid object: {token}. Must be IRI, blank node, or literal"
548 )))
549 }
550 }
551
552 fn parse_literal(&self, token: &str) -> Result<Object> {
553 if !token.starts_with('"') {
554 return Err(OxirsError::Parse(
555 "Literal must start with quote".to_string(),
556 ));
557 }
558
559 let mut end_quote_pos = None;
561 let mut escaped = false;
562 let chars: Vec<char> = token.chars().collect();
563
564 for (i, &ch) in chars.iter().enumerate().skip(1) {
565 if escaped {
566 escaped = false;
567 continue;
568 }
569
570 if ch == '\\' {
571 escaped = true;
572 } else if ch == '"' {
573 end_quote_pos = Some(i);
574 break;
575 }
576 }
577
578 let end_quote_pos =
579 end_quote_pos.ok_or_else(|| OxirsError::Parse("Unterminated literal".to_string()))?;
580
581 let raw_value: String = chars[1..end_quote_pos].iter().collect();
583 let literal_value = self.unescape_literal_value(&raw_value)?;
584
585 let remaining = &token[end_quote_pos + 1..];
587
588 if let Some(lang_tag) = remaining.strip_prefix('@') {
589 let literal = Literal::new_lang(literal_value, lang_tag)?;
591 Ok(Object::Literal(literal))
592 } else if remaining.starts_with("^^<") && remaining.ends_with('>') {
593 let datatype_iri = &remaining[3..remaining.len() - 1];
595 let datatype = NamedNode::new(datatype_iri)?;
596 let literal = Literal::new_typed(literal_value, datatype);
597 Ok(Object::Literal(literal))
598 } else if remaining.is_empty() {
599 let literal = Literal::new(literal_value);
601 Ok(Object::Literal(literal))
602 } else {
603 Err(OxirsError::Parse(format!(
604 "Invalid literal syntax: {token}"
605 )))
606 }
607 }
608
609 fn parse_trig<F>(&self, data: &str, mut handler: F) -> Result<()>
610 where
611 F: FnMut(Quad) -> Result<()>,
612 {
613 let mut internal_parser = crate::format::RdfParser::new(crate::format::RdfFormat::TriG);
617 if let Some(base) = &self.config.base_iri {
618 internal_parser = internal_parser.with_base_iri(base.clone());
619 }
620
621 for result in internal_parser.for_slice(data.as_bytes()) {
622 match result {
623 Ok(quad) => handler(quad)?,
624 Err(e) => {
625 if self.config.ignore_errors {
626 tracing::warn!("TriG parse error: {e}");
627 continue;
628 } else {
629 return Err(OxirsError::Parse(format!("TriG parse error: {e}")));
630 }
631 }
632 }
633 }
634
635 Ok(())
636 }
637
638 fn parse_nquads<F>(&self, data: &str, mut handler: F) -> Result<()>
639 where
640 F: FnMut(Quad) -> Result<()>,
641 {
642 for (line_num, line) in data.lines().enumerate() {
643 let line = line.trim();
644
645 if line.is_empty() || line.starts_with('#') {
647 continue;
648 }
649
650 match self.parse_nquads_line(line) {
652 Ok(Some(quad)) => {
653 handler(quad)?;
654 }
655 Ok(None) => {
656 continue;
658 }
659 Err(e) => {
660 if self.config.ignore_errors {
661 tracing::warn!("Parse error on line {}: {}", line_num + 1, e);
662 continue;
663 } else {
664 return Err(OxirsError::Parse(format!(
665 "Parse error on line {}: {}",
666 line_num + 1,
667 e
668 )));
669 }
670 }
671 }
672 }
673
674 Ok(())
675 }
676
677 pub fn parse_nquads_line(&self, line: &str) -> Result<Option<Quad>> {
678 let line = line.trim();
680
681 if line.is_empty() || line.starts_with('#') {
682 return Ok(None);
683 }
684
685 if !line.ends_with('.') {
687 return Err(OxirsError::Parse("Line must end with '.'".to_string()));
688 }
689
690 let line = &line[..line.len() - 1].trim(); let tokens = self.tokenize_ntriples_line(line)?;
694
695 if tokens.len() != 4 {
696 return Err(OxirsError::Parse(format!(
697 "Expected 4 tokens (subject, predicate, object, graph), found {}",
698 tokens.len()
699 )));
700 }
701
702 let subject = self.parse_subject(&tokens[0])?;
704
705 let predicate = self.parse_predicate(&tokens[1])?;
707
708 let object = self.parse_object(&tokens[2])?;
710
711 let graph_name = self.parse_graph_name(&tokens[3])?;
713
714 let quad = Quad::new(subject, predicate, object, graph_name);
715
716 Ok(Some(quad))
717 }
718
719 fn parse_graph_name(&self, token: &str) -> Result<GraphName> {
720 if token.starts_with('<') && token.ends_with('>') {
721 let iri = &token[1..token.len() - 1];
722 let named_node = NamedNode::new(iri)?;
723 Ok(GraphName::NamedNode(named_node))
724 } else if token.starts_with("_:") {
725 let blank_node = BlankNode::new(token)?;
726 Ok(GraphName::BlankNode(blank_node))
727 } else {
728 Err(OxirsError::Parse(format!(
729 "Invalid graph name: {token}. Must be IRI or blank node"
730 )))
731 }
732 }
733
734 fn parse_rdfxml<F>(&self, data: &str, mut handler: F) -> Result<()>
735 where
736 F: FnMut(Quad) -> Result<()>,
737 {
738 use crate::rdfxml::wrapper::parse_rdfxml;
739 use std::io::Cursor;
740
741 let reader = Cursor::new(data.as_bytes());
743 let base_iri = self.config.base_iri.as_deref();
744 let quads = parse_rdfxml(reader, base_iri, self.config.ignore_errors)?;
745
746 for quad in quads {
748 handler(quad)?;
749 }
750
751 Ok(())
752 }
753
754 fn parse_jsonld<F>(&self, data: &str, mut handler: F) -> Result<()>
755 where
756 F: FnMut(Quad) -> Result<()>,
757 {
758 use crate::jsonld::to_rdf::JsonLdParser;
760
761 let parser = JsonLdParser::new();
762 let parser = if let Some(base_iri) = &self.config.base_iri {
763 parser
764 .with_base_iri(base_iri.clone())
765 .map_err(|e| OxirsError::Parse(format!("Invalid base IRI: {e}")))?
766 } else {
767 parser
768 };
769
770 for result in parser.for_slice(data.as_bytes()) {
772 match result {
773 Ok(quad) => handler(quad)?,
774 Err(e) => {
775 if self.config.ignore_errors {
776 tracing::warn!("JSON-LD parse error: {}", e);
777 continue;
778 } else {
779 return Err(OxirsError::Parse(format!("JSON-LD parse error: {e}")));
780 }
781 }
782 }
783 }
784
785 Ok(())
786 }
787
788 fn unescape_literal_value(&self, value: &str) -> Result<String> {
790 let mut result = String::new();
791 let mut chars = value.chars();
792
793 while let Some(c) = chars.next() {
794 if c == '\\' {
795 match chars.next() {
796 Some('"') => result.push('"'),
797 Some('\\') => result.push('\\'),
798 Some('n') => result.push('\n'),
799 Some('r') => result.push('\r'),
800 Some('t') => result.push('\t'),
801 Some('u') => {
802 let hex_chars: String = chars.by_ref().take(4).collect();
804 if hex_chars.len() != 4 {
805 return Err(OxirsError::Parse(
806 "Invalid Unicode escape sequence \\uHHHH - expected 4 hex digits"
807 .to_string(),
808 ));
809 }
810 let code_point = u32::from_str_radix(&hex_chars, 16).map_err(|_| {
811 OxirsError::Parse(
812 "Invalid hex digits in Unicode escape sequence".to_string(),
813 )
814 })?;
815 let unicode_char = char::from_u32(code_point).ok_or_else(|| {
816 OxirsError::Parse("Invalid Unicode code point".to_string())
817 })?;
818 result.push(unicode_char);
819 }
820 Some('U') => {
821 let hex_chars: String = chars.by_ref().take(8).collect();
823 if hex_chars.len() != 8 {
824 return Err(OxirsError::Parse(
825 "Invalid Unicode escape sequence \\UHHHHHHHH - expected 8 hex digits".to_string()
826 ));
827 }
828 let code_point = u32::from_str_radix(&hex_chars, 16).map_err(|_| {
829 OxirsError::Parse(
830 "Invalid hex digits in Unicode escape sequence".to_string(),
831 )
832 })?;
833 let unicode_char = char::from_u32(code_point).ok_or_else(|| {
834 OxirsError::Parse("Invalid Unicode code point".to_string())
835 })?;
836 result.push(unicode_char);
837 }
838 Some(other) => {
839 return Err(OxirsError::Parse(format!(
840 "Invalid escape sequence \\{other}"
841 )));
842 }
843 None => {
844 return Err(OxirsError::Parse(
845 "Incomplete escape sequence at end of literal".to_string(),
846 ));
847 }
848 }
849 } else {
850 result.push(c);
851 }
852 }
853
854 Ok(result)
855 }
856
857 }
859
860pub fn detect_format_from_content(content: &str) -> Option<RdfFormat> {
862 let content = content.trim();
863
864 if content.starts_with("<?xml")
866 || content.starts_with("<rdf:RDF")
867 || content.starts_with("<RDF")
868 {
869 return Some(RdfFormat::RdfXml);
870 }
871
872 if content.starts_with('{') && (content.contains("@context") || content.contains("@type")) {
874 return Some(RdfFormat::JsonLd);
875 }
876
877 if content.contains("@prefix") || content.contains("@base") || content.contains(';') {
879 return Some(RdfFormat::Turtle);
880 }
881
882 if content.contains('{') && content.contains('}') {
884 return Some(RdfFormat::TriG);
885 }
886
887 for line in content.lines() {
889 let line = line.trim();
890 if !line.is_empty() && !line.starts_with('#') {
891 let parts: Vec<&str> = line.split_whitespace().collect();
892 if parts.len() == 4 && parts[3] == "." {
893 return Some(RdfFormat::NTriples);
895 } else if parts.len() == 5 && parts[4] == "." {
896 return Some(RdfFormat::NQuads);
898 } else if parts.len() >= 3 && parts[parts.len() - 1] == "." {
899 return Some(RdfFormat::NTriples);
901 }
902 break; }
904 }
905
906 None
907}
908
909#[cfg(test)]
910mod tests {
911 use super::*;
912 use crate::model::graph::Graph;
913
914 #[test]
915 fn test_format_detection_from_extension() {
916 assert_eq!(RdfFormat::from_extension("ttl"), Some(RdfFormat::Turtle));
917 assert_eq!(RdfFormat::from_extension("turtle"), Some(RdfFormat::Turtle));
918 assert_eq!(RdfFormat::from_extension("nt"), Some(RdfFormat::NTriples));
919 assert_eq!(
920 RdfFormat::from_extension("ntriples"),
921 Some(RdfFormat::NTriples)
922 );
923 assert_eq!(RdfFormat::from_extension("trig"), Some(RdfFormat::TriG));
924 assert_eq!(RdfFormat::from_extension("nq"), Some(RdfFormat::NQuads));
925 assert_eq!(RdfFormat::from_extension("rdf"), Some(RdfFormat::RdfXml));
926 assert_eq!(RdfFormat::from_extension("jsonld"), Some(RdfFormat::JsonLd));
927 assert_eq!(RdfFormat::from_extension("unknown"), None);
928 }
929
930 #[test]
931 fn test_format_properties() {
932 assert_eq!(RdfFormat::Turtle.media_type(), "text/turtle");
933 assert_eq!(RdfFormat::NTriples.extension(), "nt");
934 assert!(RdfFormat::TriG.supports_quads());
935 assert!(!RdfFormat::Turtle.supports_quads());
936 }
937
938 #[test]
939 fn test_format_detection_from_content() {
940 let xml_content = "<?xml version=\"1.0\"?>\n<rdf:RDF>";
942 assert_eq!(
943 detect_format_from_content(xml_content),
944 Some(RdfFormat::RdfXml)
945 );
946
947 let jsonld_content = r#"{"@context": "http://example.org", "@type": "Person"}"#;
949 assert_eq!(
950 detect_format_from_content(jsonld_content),
951 Some(RdfFormat::JsonLd)
952 );
953
954 let turtle_content = "@prefix foaf: <http://xmlns.com/foaf/0.1/> .";
956 assert_eq!(
957 detect_format_from_content(turtle_content),
958 Some(RdfFormat::Turtle)
959 );
960
961 let ntriples_content = "<http://example.org/s> <http://example.org/p> \"object\" .";
963 assert_eq!(
964 detect_format_from_content(ntriples_content),
965 Some(RdfFormat::NTriples)
966 );
967 }
968
969 #[test]
970 fn test_ntriples_parsing_simple() {
971 let ntriples_data = r#"<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" .
972<http://example.org/alice> <http://xmlns.com/foaf/0.1/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
973_:person1 <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> ."#;
974
975 let parser = Parser::new(RdfFormat::NTriples);
976 let result = parser.parse_str_to_quads(ntriples_data);
977
978 assert!(result.is_ok());
979 let quads = result.expect("should have value");
980 assert_eq!(quads.len(), 3);
981
982 for quad in &quads {
984 assert!(quad.is_default_graph());
985 }
986
987 let triples: Vec<_> = quads.into_iter().map(|q| q.to_triple()).collect();
989
990 let alice_iri = NamedNode::new("http://example.org/alice").expect("valid IRI");
992 let name_pred = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
993 let name_literal = Literal::new("Alice Smith");
994 let expected_triple1 = Triple::new(alice_iri.clone(), name_pred, name_literal);
995 assert!(triples.contains(&expected_triple1));
996
997 let age_pred = NamedNode::new("http://xmlns.com/foaf/0.1/age").expect("valid IRI");
999 let integer_type =
1000 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").expect("valid IRI");
1001 let age_literal = Literal::new_typed("30", integer_type);
1002 let expected_triple2 = Triple::new(alice_iri, age_pred, age_literal);
1003 assert!(triples.contains(&expected_triple2));
1004
1005 let blank_node = BlankNode::new("_:person1").expect("valid blank node id");
1007 let knows_pred = NamedNode::new("http://xmlns.com/foaf/0.1/knows").expect("valid IRI");
1008 let bob_iri = NamedNode::new("http://example.org/bob").expect("valid IRI");
1009 let expected_triple3 = Triple::new(blank_node, knows_pred, bob_iri);
1010 assert!(triples.contains(&expected_triple3));
1011 }
1012
1013 #[test]
1014 fn test_ntriples_parsing_language_tag() {
1015 let ntriples_data =
1016 r#"<http://example.org/alice> <http://example.org/description> "Une personne"@fr ."#;
1017
1018 let parser = Parser::new(RdfFormat::NTriples);
1019 let result = parser.parse_str_to_quads(ntriples_data);
1020
1021 assert!(result.is_ok());
1022 let quads = result.expect("should have value");
1023 assert_eq!(quads.len(), 1);
1024
1025 let triple = quads[0].to_triple();
1026 if let Object::Literal(literal) = triple.object() {
1027 assert_eq!(literal.value(), "Une personne");
1028 assert_eq!(literal.language(), Some("fr"));
1029 assert!(literal.is_lang_string());
1030 } else {
1031 panic!("Expected literal object");
1032 }
1033 }
1034
1035 #[test]
1036 fn test_ntriples_parsing_escaped_literals() {
1037 let ntriples_data = r#"<http://example.org/test> <http://example.org/desc> "Text with \"quotes\" and \n newlines" ."#;
1038
1039 let parser = Parser::new(RdfFormat::NTriples);
1040 let result = parser.parse_str_to_quads(ntriples_data);
1041
1042 if let Err(e) = &result {
1043 println!("Parse error: {e}");
1044 }
1045 assert!(result.is_ok(), "Parse failed: {result:?}");
1046
1047 let quads = result.expect("should have value");
1048 assert_eq!(quads.len(), 1);
1049
1050 let triple = quads[0].to_triple();
1051 if let Object::Literal(literal) = triple.object() {
1052 assert!(literal.value().contains("\"quotes\""));
1053 assert!(literal.value().contains("\n"));
1054 } else {
1055 panic!("Expected literal object");
1056 }
1057 }
1058
1059 #[test]
1060 fn test_ntriples_parsing_comments_and_empty_lines() {
1061 let ntriples_data = r#"
1062# This is a comment
1063<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" .
1064
1065# Another comment
1066<http://example.org/bob> <http://xmlns.com/foaf/0.1/name> "Bob Jones" .
1067"#;
1068
1069 let parser = Parser::new(RdfFormat::NTriples);
1070 let result = parser.parse_str_to_quads(ntriples_data);
1071
1072 assert!(result.is_ok());
1073 let quads = result.expect("should have value");
1074 assert_eq!(quads.len(), 2);
1075 }
1076
1077 #[test]
1078 fn test_ntriples_parsing_error_handling() {
1079 let invalid_data = "invalid ntriples data";
1081 let parser = Parser::new(RdfFormat::NTriples);
1082 let result = parser.parse_str_to_quads(invalid_data);
1083 assert!(result.is_err());
1084
1085 let mixed_data = r#"<http://example.org/valid> <http://example.org/pred> "Valid triple" .
1087invalid line here
1088<http://example.org/valid2> <http://example.org/pred> "Another valid triple" ."#;
1089
1090 let parser_strict = Parser::new(RdfFormat::NTriples);
1091 let result_strict = parser_strict.parse_str_to_quads(mixed_data);
1092 assert!(result_strict.is_err());
1093
1094 let parser_tolerant = Parser::new(RdfFormat::NTriples).with_error_tolerance(true);
1095 let result_tolerant = parser_tolerant.parse_str_to_quads(mixed_data);
1096 assert!(result_tolerant.is_ok());
1097 let quads = result_tolerant.expect("tolerant parse should succeed");
1098 assert_eq!(quads.len(), 2); }
1100
1101 #[test]
1102 fn test_nquads_parsing() {
1103 let nquads_data = r#"<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" <http://example.org/graph1> .
1104<http://example.org/alice> <http://xmlns.com/foaf/0.1/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> <http://example.org/graph2> .
1105_:person1 <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> _:graph1 ."#;
1106
1107 let parser = Parser::new(RdfFormat::NQuads);
1108 let result = parser.parse_str_to_quads(nquads_data);
1109
1110 assert!(result.is_ok());
1111 let quads = result.expect("should have value");
1112 assert_eq!(quads.len(), 3);
1113
1114 let first_quad = &quads[0];
1116 assert!(!first_quad.is_default_graph());
1117
1118 if let GraphName::NamedNode(graph_name) = first_quad.graph_name() {
1120 assert!(graph_name.as_str().contains("example.org"));
1121 } else {
1122 panic!("Expected named graph");
1123 }
1124 }
1125
1126 #[test]
1127 fn test_turtle_parsing_basic() {
1128 let turtle_data = r#"@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1129@prefix ex: <http://example.org/> .
1130
1131ex:alice foaf:name "Alice Smith" .
1132ex:alice foaf:age "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
1133ex:alice foaf:knows ex:bob ."#;
1134
1135 let parser = Parser::new(RdfFormat::Turtle);
1136 let result = parser.parse_str_to_quads(turtle_data);
1137
1138 assert!(result.is_ok());
1139 let quads = result.expect("should have value");
1140 assert_eq!(quads.len(), 3);
1141
1142 for quad in &quads {
1144 assert!(quad.is_default_graph());
1145 }
1146 }
1147
1148 #[test]
1149 fn test_turtle_parsing_prefixes() {
1150 let turtle_data = r#"@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1151foaf:Person a foaf:Person ."#;
1152
1153 let parser = Parser::new(RdfFormat::Turtle);
1154 let result = parser.parse_str_to_quads(turtle_data);
1155
1156 assert!(result.is_ok());
1157 let quads = result.expect("should have value");
1158 assert_eq!(quads.len(), 1);
1159
1160 let triple = quads[0].to_triple();
1161 if let Subject::NamedNode(subj) = triple.subject() {
1163 assert!(subj.as_str().contains("xmlns.com/foaf"));
1164 } else {
1165 panic!("Expected named node subject");
1166 }
1167
1168 if let Predicate::NamedNode(pred) = triple.predicate() {
1170 assert!(pred.as_str().contains("rdf-syntax-ns#type"));
1171 } else {
1172 panic!("Expected named node predicate");
1173 }
1174 }
1175
1176 #[test]
1177 fn test_turtle_parsing_abbreviated_syntax() {
1178 let turtle_data = r#"@prefix ex: <http://example.org/> .
1179@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1180
1181ex:alice foaf:name "Alice" ;
1182 foaf:age "30" ."#;
1183
1184 let parser = Parser::new(RdfFormat::Turtle);
1185 let result = parser.parse_str_to_quads(turtle_data);
1186
1187 assert!(result.is_ok());
1188 let quads = result.expect("should have value");
1189 assert_eq!(quads.len(), 2);
1190
1191 let subjects: Vec<_> = quads
1193 .iter()
1194 .map(|q| q.to_triple().subject().clone())
1195 .collect();
1196 assert_eq!(subjects[0], subjects[1]);
1197 }
1198
1199 #[test]
1206 fn test_turtle_semicolon_inside_literal_not_split() {
1207 let turtle_data = r#"@prefix ex: <http://example.org/> .
1208ex:alice ex:bio "Loves cats; dogs; and turtles" ;
1209 ex:name "Alice" ."#;
1210
1211 let parser = Parser::new(RdfFormat::Turtle);
1212 let quads = parser
1213 .parse_str_to_quads(turtle_data)
1214 .expect("semicolon inside a literal must not corrupt parsing");
1215
1216 assert_eq!(quads.len(), 2, "expected exactly 2 triples, got {quads:?}");
1217
1218 let bio_triple = quads
1219 .iter()
1220 .map(|q| q.to_triple())
1221 .find(|t| t.predicate().to_string().contains("bio"))
1222 .expect("bio triple should be present");
1223 if let Object::Literal(lit) = bio_triple.object() {
1224 assert_eq!(lit.value(), "Loves cats; dogs; and turtles");
1225 } else {
1226 panic!("Expected literal object for ex:bio");
1227 }
1228 }
1229
1230 #[test]
1233 fn test_turtle_comma_object_list() {
1234 let turtle_data = r#"@prefix ex: <http://example.org/> .
1235ex:alice ex:knows ex:bob, ex:carol, ex:dave ."#;
1236
1237 let parser = Parser::new(RdfFormat::Turtle);
1238 let quads = parser
1239 .parse_str_to_quads(turtle_data)
1240 .expect("comma object lists must parse");
1241
1242 assert_eq!(quads.len(), 3, "expected 3 triples, got {quads:?}");
1243 let objects: std::collections::HashSet<String> = quads
1244 .iter()
1245 .map(|q| q.to_triple().object().to_string())
1246 .collect();
1247 assert!(objects.iter().any(|o| o.contains("bob")));
1248 assert!(objects.iter().any(|o| o.contains("carol")));
1249 assert!(objects.iter().any(|o| o.contains("dave")));
1250 }
1251
1252 #[test]
1255 fn test_turtle_blank_node_property_list_and_collection() {
1256 let turtle_data = r#"@prefix ex: <http://example.org/> .
1257ex:alice ex:address [ ex:city "Springfield" ; ex:zip "12345" ] ;
1258 ex:favorites ( ex:tea ex:coffee ex:cocoa ) ."#;
1259
1260 let parser = Parser::new(RdfFormat::Turtle);
1261 let quads = parser
1262 .parse_str_to_quads(turtle_data)
1263 .expect("blank-node property lists and collections must parse");
1264
1265 assert!(
1269 quads.len() >= 7,
1270 "expected at least 7 triples for property list + collection, got {} ({quads:?})",
1271 quads.len()
1272 );
1273
1274 let has_city = quads.iter().any(|q| {
1275 let t = q.to_triple();
1276 t.predicate().to_string().contains("city")
1277 && matches!(t.object(), Object::Literal(l) if l.value() == "Springfield")
1278 });
1279 assert!(has_city, "blank-node property list content missing");
1280
1281 let has_first = quads.iter().any(|q| {
1282 q.to_triple()
1283 .predicate()
1284 .to_string()
1285 .contains("rdf-syntax-ns#first")
1286 });
1287 assert!(
1288 has_first,
1289 "RDF collection must expand to rdf:first/rdf:rest"
1290 );
1291 }
1292
1293 #[test]
1297 fn test_turtle_triple_quoted_multiline_literal() {
1298 let turtle_data = "@prefix ex: <http://example.org/> .\nex:alice ex:bio \"\"\"Line one\nLine two\nLine three\"\"\" .";
1299
1300 let parser = Parser::new(RdfFormat::Turtle);
1301 let quads = parser
1302 .parse_str_to_quads(turtle_data)
1303 .expect("triple-quoted multi-line literals must parse");
1304
1305 assert_eq!(quads.len(), 1);
1306 let triple = quads[0].to_triple();
1307 if let Object::Literal(lit) = triple.object() {
1308 assert_eq!(lit.value(), "Line one\nLine two\nLine three");
1309 } else {
1310 panic!("Expected literal object");
1311 }
1312 }
1313
1314 #[test]
1317 fn test_trig_triple_quoted_multiline_literal_in_named_graph() {
1318 let trig_data = "@prefix ex: <http://example.org/> .\nex:g1 { ex:alice ex:bio \"\"\"Line one\nLine two\"\"\" . }";
1319
1320 let parser = Parser::new(RdfFormat::TriG);
1321 let quads = parser
1322 .parse_str_to_quads(trig_data)
1323 .expect("triple-quoted multi-line literals must parse in TriG too");
1324
1325 assert_eq!(quads.len(), 1);
1326 assert!(!quads[0].is_default_graph());
1327 let triple = quads[0].to_triple();
1328 if let Object::Literal(lit) = triple.object() {
1329 assert_eq!(lit.value(), "Line one\nLine two");
1330 } else {
1331 panic!("Expected literal object");
1332 }
1333 }
1334
1335 #[test]
1336 fn test_turtle_parsing_base_iri() {
1337 let turtle_data = r#"@base <http://example.org/> .
1338<alice> <knows> <bob> ."#;
1339
1340 let parser = Parser::new(RdfFormat::Turtle);
1341 let result = parser.parse_str_to_quads(turtle_data);
1342
1343 assert!(result.is_ok());
1344 let quads = result.expect("should have value");
1345 assert_eq!(quads.len(), 1);
1346
1347 let triple = quads[0].to_triple();
1348 if let Subject::NamedNode(subj) = triple.subject() {
1350 assert!(subj.as_str().contains("example.org"));
1351 } else {
1352 panic!("Expected named node subject");
1353 }
1354 }
1355
1356 #[test]
1357 fn test_turtle_parsing_literals() {
1358 let turtle_data = r#"@prefix ex: <http://example.org/> .
1359ex:alice ex:name "Alice"@en .
1360ex:alice ex:age "30"^^<http://www.w3.org/2001/XMLSchema#integer> ."#;
1361
1362 let parser = Parser::new(RdfFormat::Turtle);
1363 let result = parser.parse_str_to_quads(turtle_data);
1364
1365 assert!(result.is_ok());
1366 let quads = result.expect("should have value");
1367 assert_eq!(quads.len(), 2);
1368
1369 let triples: Vec<_> = quads.into_iter().map(|q| q.to_triple()).collect();
1371
1372 let mut found_lang_literal = false;
1373 let mut found_typed_literal = false;
1374
1375 for triple in triples {
1376 if let Object::Literal(literal) = triple.object() {
1377 if literal.language().is_some() {
1378 found_lang_literal = true;
1379 assert_eq!(literal.language(), Some("en"));
1380 } else {
1381 let datatype = literal.datatype();
1382 if datatype.as_str() != "http://www.w3.org/2001/XMLSchema#string"
1384 && datatype.as_str()
1385 != "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString"
1386 {
1387 found_typed_literal = true;
1388 assert!(
1389 datatype.as_str().contains("integer"),
1390 "Expected integer datatype but got: {}",
1391 datatype.as_str()
1392 );
1393 }
1394 }
1395 }
1396 }
1397
1398 assert!(found_lang_literal);
1399 assert!(found_typed_literal);
1400 }
1401
1402 #[test]
1403 fn test_parser_round_trip() {
1404 use crate::serializer::Serializer;
1405
1406 let mut original_graph = Graph::new();
1408
1409 let alice = NamedNode::new("http://example.org/alice").expect("valid IRI");
1410 let name_pred = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
1411 let name_literal = Literal::new("Alice Smith");
1412 original_graph.insert(Triple::new(alice.clone(), name_pred, name_literal));
1413
1414 let age_pred = NamedNode::new("http://xmlns.com/foaf/0.1/age").expect("valid IRI");
1415 let age_literal = Literal::new_typed("30", crate::vocab::xsd::INTEGER.clone());
1416 original_graph.insert(Triple::new(alice.clone(), age_pred, age_literal));
1417
1418 let desc_pred = NamedNode::new("http://example.org/description").expect("valid IRI");
1419 let desc_literal =
1420 Literal::new_lang("Une personne", "fr").expect("construction should succeed");
1421 original_graph.insert(Triple::new(alice, desc_pred, desc_literal));
1422
1423 let serializer = Serializer::new(RdfFormat::NTriples);
1425 let ntriples = serializer
1426 .serialize_graph(&original_graph)
1427 .expect("operation should succeed");
1428
1429 let parser = Parser::new(RdfFormat::NTriples);
1431 let quads = parser
1432 .parse_str_to_quads(&ntriples)
1433 .expect("operation should succeed");
1434
1435 let parsed_graph = Graph::from_iter(quads.into_iter().map(|q| q.to_triple()));
1437
1438 assert_eq!(original_graph.len(), parsed_graph.len());
1440
1441 for triple in original_graph.iter() {
1443 assert!(
1444 parsed_graph.contains(triple),
1445 "Parsed graph missing triple: {triple}"
1446 );
1447 }
1448 }
1449
1450 #[test]
1451 fn test_trig_parser() {
1452 let trig_data = r#"
1453@prefix ex: <http://example.org/> .
1454@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
1455
1456# Default graph
1457{
1458 ex:alice rdf:type ex:Person .
1459 ex:alice ex:name "Alice" .
1460}
1461
1462# Named graph
1463ex:graph1 {
1464 ex:bob rdf:type ex:Person .
1465 ex:bob ex:name "Bob" .
1466 ex:bob ex:age "30" .
1467}
1468"#;
1469
1470 let parser = Parser::new(RdfFormat::TriG);
1471 let quads = parser
1472 .parse_str_to_quads(trig_data)
1473 .expect("operation should succeed");
1474
1475 assert!(
1477 quads.len() >= 5,
1478 "Should parse at least 5 quads, got {}",
1479 quads.len()
1480 );
1481
1482 let default_graph_count = quads.iter().filter(|q| q.is_default_graph()).count();
1484 let named_graph_count = quads.len() - default_graph_count;
1485
1486 assert!(
1487 default_graph_count >= 2,
1488 "Should have at least 2 default graph quads, got {default_graph_count}"
1489 );
1490 assert!(
1491 named_graph_count >= 3,
1492 "Should have at least 3 named graph quads, got {named_graph_count}"
1493 );
1494
1495 let alice_uri = "http://example.org/alice";
1497 let bob_uri = "http://example.org/bob";
1498 let person_uri = "http://example.org/Person";
1499
1500 let alice_type_found = quads.iter().any(|q| {
1502 q.is_default_graph()
1503 && q.subject().to_string().contains(alice_uri)
1504 && q.object().to_string().contains(person_uri)
1505 });
1506 assert!(
1507 alice_type_found,
1508 "Should find Alice type assertion in default graph"
1509 );
1510
1511 let bob_in_named_graph = quads
1513 .iter()
1514 .any(|q| !q.is_default_graph() && q.subject().to_string().contains(bob_uri));
1515 assert!(
1516 bob_in_named_graph,
1517 "Should find Bob statements in named graph"
1518 );
1519 }
1520
1521 #[test]
1522 fn test_trig_parser_prefixes() {
1523 let trig_data = r#"
1524@prefix ex: <http://example.org/> .
1525@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1526
1527ex:person1 foaf:name "John Doe" .
1528"#;
1529
1530 let parser = Parser::new(RdfFormat::TriG);
1531 let quads = parser
1532 .parse_str_to_quads(trig_data)
1533 .expect("operation should succeed");
1534
1535 assert!(!quads.is_empty(), "Should parse prefixed statements");
1536
1537 let expanded_found = quads.iter().any(|q| {
1539 q.subject()
1540 .to_string()
1541 .contains("http://example.org/person1")
1542 && q.predicate()
1543 .to_string()
1544 .contains("http://xmlns.com/foaf/0.1/name")
1545 });
1546 assert!(expanded_found, "Should expand prefixes correctly");
1547 }
1548
1549 #[test]
1550 fn test_jsonld_parser() {
1551 let jsonld_data = r#"{
1552 "@context": {
1553 "name": "http://xmlns.com/foaf/0.1/name",
1554 "Person": "http://schema.org/Person"
1555 },
1556 "@type": "Person",
1557 "@id": "http://example.org/john",
1558 "name": "John Doe"
1559}"#;
1560
1561 let parser = Parser::new(RdfFormat::JsonLd);
1562 let result = parser.parse_str_to_quads(jsonld_data);
1563
1564 match result {
1565 Ok(quads) => {
1566 println!("JSON-LD parsed {} quads:", quads.len());
1567 for quad in &quads {
1568 println!(" {quad}");
1569 }
1570 assert!(!quads.is_empty(), "Should parse some quads from JSON-LD");
1571 }
1572 Err(e) => {
1573 println!("JSON-LD parsing error (expected during development): {e}");
1575 }
1577 }
1578 }
1579
1580 #[test]
1581 fn test_jsonld_parser_simple() {
1582 let jsonld_data = r#"{
1583 "@context": "http://schema.org/",
1584 "@type": "Person",
1585 "name": "Alice"
1586}"#;
1587
1588 let parser = Parser::new(RdfFormat::JsonLd);
1589 let result = parser.parse_str_to_quads(jsonld_data);
1590
1591 match result {
1593 Ok(quads) => {
1594 println!("Simple JSON-LD parsed {} quads", quads.len());
1595 }
1596 Err(e) => {
1597 println!("Simple JSON-LD parsing error: {e}");
1598 }
1600 }
1601 }
1602}