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;
568 let mut escaped = false;
569
570 for (i, ch) in token.char_indices().skip(1) {
571 if escaped {
572 escaped = false;
573 continue;
574 }
575
576 if ch == '\\' {
577 escaped = true;
578 } else if ch == '"' {
579 end_quote_pos = Some(i);
580 break;
581 }
582 }
583
584 let end_quote_pos =
585 end_quote_pos.ok_or_else(|| OxirsError::Parse("Unterminated literal".to_string()))?;
586
587 let raw_value = &token[1..end_quote_pos];
593 let literal_value = self.unescape_literal_value(raw_value)?;
594
595 let remaining = &token[end_quote_pos + 1..];
597
598 if let Some(lang_tag) = remaining.strip_prefix('@') {
599 let literal = Literal::new_lang(literal_value, lang_tag)?;
601 Ok(Object::Literal(literal))
602 } else if remaining.starts_with("^^<") && remaining.ends_with('>') {
603 let datatype_iri = &remaining[3..remaining.len() - 1];
605 let datatype = NamedNode::new(datatype_iri)?;
606 let literal = Literal::new_typed(literal_value, datatype);
607 Ok(Object::Literal(literal))
608 } else if remaining.is_empty() {
609 let literal = Literal::new(literal_value);
611 Ok(Object::Literal(literal))
612 } else {
613 Err(OxirsError::Parse(format!(
614 "Invalid literal syntax: {token}"
615 )))
616 }
617 }
618
619 fn parse_trig<F>(&self, data: &str, mut handler: F) -> Result<()>
620 where
621 F: FnMut(Quad) -> Result<()>,
622 {
623 let mut internal_parser = crate::format::RdfParser::new(crate::format::RdfFormat::TriG);
627 if let Some(base) = &self.config.base_iri {
628 internal_parser = internal_parser.with_base_iri(base.clone());
629 }
630
631 for result in internal_parser.for_slice(data.as_bytes()) {
632 match result {
633 Ok(quad) => handler(quad)?,
634 Err(e) => {
635 if self.config.ignore_errors {
636 tracing::warn!("TriG parse error: {e}");
637 continue;
638 } else {
639 return Err(OxirsError::Parse(format!("TriG parse error: {e}")));
640 }
641 }
642 }
643 }
644
645 Ok(())
646 }
647
648 fn parse_nquads<F>(&self, data: &str, mut handler: F) -> Result<()>
649 where
650 F: FnMut(Quad) -> Result<()>,
651 {
652 for (line_num, line) in data.lines().enumerate() {
653 let line = line.trim();
654
655 if line.is_empty() || line.starts_with('#') {
657 continue;
658 }
659
660 match self.parse_nquads_line(line) {
662 Ok(Some(quad)) => {
663 handler(quad)?;
664 }
665 Ok(None) => {
666 continue;
668 }
669 Err(e) => {
670 if self.config.ignore_errors {
671 tracing::warn!("Parse error on line {}: {}", line_num + 1, e);
672 continue;
673 } else {
674 return Err(OxirsError::Parse(format!(
675 "Parse error on line {}: {}",
676 line_num + 1,
677 e
678 )));
679 }
680 }
681 }
682 }
683
684 Ok(())
685 }
686
687 pub fn parse_nquads_line(&self, line: &str) -> Result<Option<Quad>> {
688 let line = line.trim();
690
691 if line.is_empty() || line.starts_with('#') {
692 return Ok(None);
693 }
694
695 if !line.ends_with('.') {
697 return Err(OxirsError::Parse("Line must end with '.'".to_string()));
698 }
699
700 let line = &line[..line.len() - 1].trim(); let tokens = self.tokenize_ntriples_line(line)?;
704
705 if tokens.len() != 4 {
706 return Err(OxirsError::Parse(format!(
707 "Expected 4 tokens (subject, predicate, object, graph), found {}",
708 tokens.len()
709 )));
710 }
711
712 let subject = self.parse_subject(&tokens[0])?;
714
715 let predicate = self.parse_predicate(&tokens[1])?;
717
718 let object = self.parse_object(&tokens[2])?;
720
721 let graph_name = self.parse_graph_name(&tokens[3])?;
723
724 let quad = Quad::new(subject, predicate, object, graph_name);
725
726 Ok(Some(quad))
727 }
728
729 fn parse_graph_name(&self, token: &str) -> Result<GraphName> {
730 if token.starts_with('<') && token.ends_with('>') {
731 let iri = &token[1..token.len() - 1];
732 let named_node = NamedNode::new(iri)?;
733 Ok(GraphName::NamedNode(named_node))
734 } else if token.starts_with("_:") {
735 let blank_node = BlankNode::new(token)?;
736 Ok(GraphName::BlankNode(blank_node))
737 } else {
738 Err(OxirsError::Parse(format!(
739 "Invalid graph name: {token}. Must be IRI or blank node"
740 )))
741 }
742 }
743
744 fn parse_rdfxml<F>(&self, data: &str, mut handler: F) -> Result<()>
745 where
746 F: FnMut(Quad) -> Result<()>,
747 {
748 use crate::rdfxml::wrapper::parse_rdfxml;
749 use std::io::Cursor;
750
751 let reader = Cursor::new(data.as_bytes());
753 let base_iri = self.config.base_iri.as_deref();
754 let quads = parse_rdfxml(reader, base_iri, self.config.ignore_errors)?;
755
756 for quad in quads {
758 handler(quad)?;
759 }
760
761 Ok(())
762 }
763
764 fn parse_jsonld<F>(&self, data: &str, mut handler: F) -> Result<()>
765 where
766 F: FnMut(Quad) -> Result<()>,
767 {
768 use crate::jsonld::to_rdf::JsonLdParser;
770
771 let parser = JsonLdParser::new();
772 let parser = if let Some(base_iri) = &self.config.base_iri {
773 parser
774 .with_base_iri(base_iri.clone())
775 .map_err(|e| OxirsError::Parse(format!("Invalid base IRI: {e}")))?
776 } else {
777 parser
778 };
779
780 for result in parser.for_slice(data.as_bytes()) {
782 match result {
783 Ok(quad) => handler(quad)?,
784 Err(e) => {
785 if self.config.ignore_errors {
786 tracing::warn!("JSON-LD parse error: {}", e);
787 continue;
788 } else {
789 return Err(OxirsError::Parse(format!("JSON-LD parse error: {e}")));
790 }
791 }
792 }
793 }
794
795 Ok(())
796 }
797
798 fn unescape_literal_value(&self, value: &str) -> Result<String> {
800 let mut result = String::new();
801 let mut chars = value.chars();
802
803 while let Some(c) = chars.next() {
804 if c == '\\' {
805 match chars.next() {
806 Some('"') => result.push('"'),
807 Some('\\') => result.push('\\'),
808 Some('n') => result.push('\n'),
809 Some('r') => result.push('\r'),
810 Some('t') => result.push('\t'),
811 Some('u') => {
812 let hex_chars: String = chars.by_ref().take(4).collect();
814 if hex_chars.len() != 4 {
815 return Err(OxirsError::Parse(
816 "Invalid Unicode escape sequence \\uHHHH - expected 4 hex digits"
817 .to_string(),
818 ));
819 }
820 let code_point = u32::from_str_radix(&hex_chars, 16).map_err(|_| {
821 OxirsError::Parse(
822 "Invalid hex digits in Unicode escape sequence".to_string(),
823 )
824 })?;
825 let unicode_char = char::from_u32(code_point).ok_or_else(|| {
826 OxirsError::Parse("Invalid Unicode code point".to_string())
827 })?;
828 result.push(unicode_char);
829 }
830 Some('U') => {
831 let hex_chars: String = chars.by_ref().take(8).collect();
833 if hex_chars.len() != 8 {
834 return Err(OxirsError::Parse(
835 "Invalid Unicode escape sequence \\UHHHHHHHH - expected 8 hex digits".to_string()
836 ));
837 }
838 let code_point = u32::from_str_radix(&hex_chars, 16).map_err(|_| {
839 OxirsError::Parse(
840 "Invalid hex digits in Unicode escape sequence".to_string(),
841 )
842 })?;
843 let unicode_char = char::from_u32(code_point).ok_or_else(|| {
844 OxirsError::Parse("Invalid Unicode code point".to_string())
845 })?;
846 result.push(unicode_char);
847 }
848 Some(other) => {
849 return Err(OxirsError::Parse(format!(
850 "Invalid escape sequence \\{other}"
851 )));
852 }
853 None => {
854 return Err(OxirsError::Parse(
855 "Incomplete escape sequence at end of literal".to_string(),
856 ));
857 }
858 }
859 } else {
860 result.push(c);
861 }
862 }
863
864 Ok(result)
865 }
866
867 }
869
870pub fn detect_format_from_content(content: &str) -> Option<RdfFormat> {
872 let content = content.trim();
873
874 if content.starts_with("<?xml")
876 || content.starts_with("<rdf:RDF")
877 || content.starts_with("<RDF")
878 {
879 return Some(RdfFormat::RdfXml);
880 }
881
882 if content.starts_with('{') && (content.contains("@context") || content.contains("@type")) {
884 return Some(RdfFormat::JsonLd);
885 }
886
887 if content.contains("@prefix") || content.contains("@base") || content.contains(';') {
889 return Some(RdfFormat::Turtle);
890 }
891
892 if content.contains('{') && content.contains('}') {
894 return Some(RdfFormat::TriG);
895 }
896
897 for line in content.lines() {
899 let line = line.trim();
900 if !line.is_empty() && !line.starts_with('#') {
901 let parts: Vec<&str> = line.split_whitespace().collect();
902 if parts.len() == 4 && parts[3] == "." {
903 return Some(RdfFormat::NTriples);
905 } else if parts.len() == 5 && parts[4] == "." {
906 return Some(RdfFormat::NQuads);
908 } else if parts.len() >= 3 && parts[parts.len() - 1] == "." {
909 return Some(RdfFormat::NTriples);
911 }
912 break; }
914 }
915
916 None
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922 use crate::model::graph::Graph;
923
924 #[test]
925 fn test_format_detection_from_extension() {
926 assert_eq!(RdfFormat::from_extension("ttl"), Some(RdfFormat::Turtle));
927 assert_eq!(RdfFormat::from_extension("turtle"), Some(RdfFormat::Turtle));
928 assert_eq!(RdfFormat::from_extension("nt"), Some(RdfFormat::NTriples));
929 assert_eq!(
930 RdfFormat::from_extension("ntriples"),
931 Some(RdfFormat::NTriples)
932 );
933 assert_eq!(RdfFormat::from_extension("trig"), Some(RdfFormat::TriG));
934 assert_eq!(RdfFormat::from_extension("nq"), Some(RdfFormat::NQuads));
935 assert_eq!(RdfFormat::from_extension("rdf"), Some(RdfFormat::RdfXml));
936 assert_eq!(RdfFormat::from_extension("jsonld"), Some(RdfFormat::JsonLd));
937 assert_eq!(RdfFormat::from_extension("unknown"), None);
938 }
939
940 #[test]
941 fn test_format_properties() {
942 assert_eq!(RdfFormat::Turtle.media_type(), "text/turtle");
943 assert_eq!(RdfFormat::NTriples.extension(), "nt");
944 assert!(RdfFormat::TriG.supports_quads());
945 assert!(!RdfFormat::Turtle.supports_quads());
946 }
947
948 #[test]
949 fn test_format_detection_from_content() {
950 let xml_content = "<?xml version=\"1.0\"?>\n<rdf:RDF>";
952 assert_eq!(
953 detect_format_from_content(xml_content),
954 Some(RdfFormat::RdfXml)
955 );
956
957 let jsonld_content = r#"{"@context": "http://example.org", "@type": "Person"}"#;
959 assert_eq!(
960 detect_format_from_content(jsonld_content),
961 Some(RdfFormat::JsonLd)
962 );
963
964 let turtle_content = "@prefix foaf: <http://xmlns.com/foaf/0.1/> .";
966 assert_eq!(
967 detect_format_from_content(turtle_content),
968 Some(RdfFormat::Turtle)
969 );
970
971 let ntriples_content = "<http://example.org/s> <http://example.org/p> \"object\" .";
973 assert_eq!(
974 detect_format_from_content(ntriples_content),
975 Some(RdfFormat::NTriples)
976 );
977 }
978
979 #[test]
980 fn test_ntriples_parsing_simple() {
981 let ntriples_data = r#"<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" .
982<http://example.org/alice> <http://xmlns.com/foaf/0.1/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
983_:person1 <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> ."#;
984
985 let parser = Parser::new(RdfFormat::NTriples);
986 let result = parser.parse_str_to_quads(ntriples_data);
987
988 assert!(result.is_ok());
989 let quads = result.expect("should have value");
990 assert_eq!(quads.len(), 3);
991
992 for quad in &quads {
994 assert!(quad.is_default_graph());
995 }
996
997 let triples: Vec<_> = quads.into_iter().map(|q| q.to_triple()).collect();
999
1000 let alice_iri = NamedNode::new("http://example.org/alice").expect("valid IRI");
1002 let name_pred = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
1003 let name_literal = Literal::new("Alice Smith");
1004 let expected_triple1 = Triple::new(alice_iri.clone(), name_pred, name_literal);
1005 assert!(triples.contains(&expected_triple1));
1006
1007 let age_pred = NamedNode::new("http://xmlns.com/foaf/0.1/age").expect("valid IRI");
1009 let integer_type =
1010 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").expect("valid IRI");
1011 let age_literal = Literal::new_typed("30", integer_type);
1012 let expected_triple2 = Triple::new(alice_iri, age_pred, age_literal);
1013 assert!(triples.contains(&expected_triple2));
1014
1015 let blank_node = BlankNode::new("_:person1").expect("valid blank node id");
1017 let knows_pred = NamedNode::new("http://xmlns.com/foaf/0.1/knows").expect("valid IRI");
1018 let bob_iri = NamedNode::new("http://example.org/bob").expect("valid IRI");
1019 let expected_triple3 = Triple::new(blank_node, knows_pred, bob_iri);
1020 assert!(triples.contains(&expected_triple3));
1021 }
1022
1023 #[test]
1024 fn test_ntriples_parsing_language_tag() {
1025 let ntriples_data =
1026 r#"<http://example.org/alice> <http://example.org/description> "Une personne"@fr ."#;
1027
1028 let parser = Parser::new(RdfFormat::NTriples);
1029 let result = parser.parse_str_to_quads(ntriples_data);
1030
1031 assert!(result.is_ok());
1032 let quads = result.expect("should have value");
1033 assert_eq!(quads.len(), 1);
1034
1035 let triple = quads[0].to_triple();
1036 if let Object::Literal(literal) = triple.object() {
1037 assert_eq!(literal.value(), "Une personne");
1038 assert_eq!(literal.language(), Some("fr"));
1039 assert!(literal.is_lang_string());
1040 } else {
1041 panic!("Expected literal object");
1042 }
1043 }
1044
1045 #[test]
1046 fn test_ntriples_parsing_escaped_literals() {
1047 let ntriples_data = r#"<http://example.org/test> <http://example.org/desc> "Text with \"quotes\" and \n newlines" ."#;
1048
1049 let parser = Parser::new(RdfFormat::NTriples);
1050 let result = parser.parse_str_to_quads(ntriples_data);
1051
1052 if let Err(e) = &result {
1053 println!("Parse error: {e}");
1054 }
1055 assert!(result.is_ok(), "Parse failed: {result:?}");
1056
1057 let quads = result.expect("should have value");
1058 assert_eq!(quads.len(), 1);
1059
1060 let triple = quads[0].to_triple();
1061 if let Object::Literal(literal) = triple.object() {
1062 assert!(literal.value().contains("\"quotes\""));
1063 assert!(literal.value().contains("\n"));
1064 } else {
1065 panic!("Expected literal object");
1066 }
1067 }
1068
1069 #[test]
1070 fn test_ntriples_parsing_comments_and_empty_lines() {
1071 let ntriples_data = r#"
1072# This is a comment
1073<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" .
1074
1075# Another comment
1076<http://example.org/bob> <http://xmlns.com/foaf/0.1/name> "Bob Jones" .
1077"#;
1078
1079 let parser = Parser::new(RdfFormat::NTriples);
1080 let result = parser.parse_str_to_quads(ntriples_data);
1081
1082 assert!(result.is_ok());
1083 let quads = result.expect("should have value");
1084 assert_eq!(quads.len(), 2);
1085 }
1086
1087 #[test]
1088 fn test_ntriples_parsing_error_handling() {
1089 let invalid_data = "invalid ntriples data";
1091 let parser = Parser::new(RdfFormat::NTriples);
1092 let result = parser.parse_str_to_quads(invalid_data);
1093 assert!(result.is_err());
1094
1095 let mixed_data = r#"<http://example.org/valid> <http://example.org/pred> "Valid triple" .
1097invalid line here
1098<http://example.org/valid2> <http://example.org/pred> "Another valid triple" ."#;
1099
1100 let parser_strict = Parser::new(RdfFormat::NTriples);
1101 let result_strict = parser_strict.parse_str_to_quads(mixed_data);
1102 assert!(result_strict.is_err());
1103
1104 let parser_tolerant = Parser::new(RdfFormat::NTriples).with_error_tolerance(true);
1105 let result_tolerant = parser_tolerant.parse_str_to_quads(mixed_data);
1106 assert!(result_tolerant.is_ok());
1107 let quads = result_tolerant.expect("tolerant parse should succeed");
1108 assert_eq!(quads.len(), 2); }
1110
1111 #[test]
1112 fn test_nquads_parsing() {
1113 let nquads_data = r#"<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" <http://example.org/graph1> .
1114<http://example.org/alice> <http://xmlns.com/foaf/0.1/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> <http://example.org/graph2> .
1115_:person1 <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> _:graph1 ."#;
1116
1117 let parser = Parser::new(RdfFormat::NQuads);
1118 let result = parser.parse_str_to_quads(nquads_data);
1119
1120 assert!(result.is_ok());
1121 let quads = result.expect("should have value");
1122 assert_eq!(quads.len(), 3);
1123
1124 let first_quad = &quads[0];
1126 assert!(!first_quad.is_default_graph());
1127
1128 if let GraphName::NamedNode(graph_name) = first_quad.graph_name() {
1130 assert!(graph_name.as_str().contains("example.org"));
1131 } else {
1132 panic!("Expected named graph");
1133 }
1134 }
1135
1136 #[test]
1137 fn test_turtle_parsing_basic() {
1138 let turtle_data = r#"@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1139@prefix ex: <http://example.org/> .
1140
1141ex:alice foaf:name "Alice Smith" .
1142ex:alice foaf:age "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
1143ex:alice foaf:knows ex:bob ."#;
1144
1145 let parser = Parser::new(RdfFormat::Turtle);
1146 let result = parser.parse_str_to_quads(turtle_data);
1147
1148 assert!(result.is_ok());
1149 let quads = result.expect("should have value");
1150 assert_eq!(quads.len(), 3);
1151
1152 for quad in &quads {
1154 assert!(quad.is_default_graph());
1155 }
1156 }
1157
1158 #[test]
1159 fn test_turtle_parsing_prefixes() {
1160 let turtle_data = r#"@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1161foaf:Person a foaf:Person ."#;
1162
1163 let parser = Parser::new(RdfFormat::Turtle);
1164 let result = parser.parse_str_to_quads(turtle_data);
1165
1166 assert!(result.is_ok());
1167 let quads = result.expect("should have value");
1168 assert_eq!(quads.len(), 1);
1169
1170 let triple = quads[0].to_triple();
1171 if let Subject::NamedNode(subj) = triple.subject() {
1173 assert!(subj.as_str().contains("xmlns.com/foaf"));
1174 } else {
1175 panic!("Expected named node subject");
1176 }
1177
1178 if let Predicate::NamedNode(pred) = triple.predicate() {
1180 assert!(pred.as_str().contains("rdf-syntax-ns#type"));
1181 } else {
1182 panic!("Expected named node predicate");
1183 }
1184 }
1185
1186 #[test]
1187 fn test_turtle_parsing_abbreviated_syntax() {
1188 let turtle_data = r#"@prefix ex: <http://example.org/> .
1189@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1190
1191ex:alice foaf:name "Alice" ;
1192 foaf:age "30" ."#;
1193
1194 let parser = Parser::new(RdfFormat::Turtle);
1195 let result = parser.parse_str_to_quads(turtle_data);
1196
1197 assert!(result.is_ok());
1198 let quads = result.expect("should have value");
1199 assert_eq!(quads.len(), 2);
1200
1201 let subjects: Vec<_> = quads
1203 .iter()
1204 .map(|q| q.to_triple().subject().clone())
1205 .collect();
1206 assert_eq!(subjects[0], subjects[1]);
1207 }
1208
1209 #[test]
1216 fn test_turtle_semicolon_inside_literal_not_split() {
1217 let turtle_data = r#"@prefix ex: <http://example.org/> .
1218ex:alice ex:bio "Loves cats; dogs; and turtles" ;
1219 ex:name "Alice" ."#;
1220
1221 let parser = Parser::new(RdfFormat::Turtle);
1222 let quads = parser
1223 .parse_str_to_quads(turtle_data)
1224 .expect("semicolon inside a literal must not corrupt parsing");
1225
1226 assert_eq!(quads.len(), 2, "expected exactly 2 triples, got {quads:?}");
1227
1228 let bio_triple = quads
1229 .iter()
1230 .map(|q| q.to_triple())
1231 .find(|t| t.predicate().to_string().contains("bio"))
1232 .expect("bio triple should be present");
1233 if let Object::Literal(lit) = bio_triple.object() {
1234 assert_eq!(lit.value(), "Loves cats; dogs; and turtles");
1235 } else {
1236 panic!("Expected literal object for ex:bio");
1237 }
1238 }
1239
1240 #[test]
1243 fn test_turtle_comma_object_list() {
1244 let turtle_data = r#"@prefix ex: <http://example.org/> .
1245ex:alice ex:knows ex:bob, ex:carol, ex:dave ."#;
1246
1247 let parser = Parser::new(RdfFormat::Turtle);
1248 let quads = parser
1249 .parse_str_to_quads(turtle_data)
1250 .expect("comma object lists must parse");
1251
1252 assert_eq!(quads.len(), 3, "expected 3 triples, got {quads:?}");
1253 let objects: std::collections::HashSet<String> = quads
1254 .iter()
1255 .map(|q| q.to_triple().object().to_string())
1256 .collect();
1257 assert!(objects.iter().any(|o| o.contains("bob")));
1258 assert!(objects.iter().any(|o| o.contains("carol")));
1259 assert!(objects.iter().any(|o| o.contains("dave")));
1260 }
1261
1262 #[test]
1265 fn test_turtle_blank_node_property_list_and_collection() {
1266 let turtle_data = r#"@prefix ex: <http://example.org/> .
1267ex:alice ex:address [ ex:city "Springfield" ; ex:zip "12345" ] ;
1268 ex:favorites ( ex:tea ex:coffee ex:cocoa ) ."#;
1269
1270 let parser = Parser::new(RdfFormat::Turtle);
1271 let quads = parser
1272 .parse_str_to_quads(turtle_data)
1273 .expect("blank-node property lists and collections must parse");
1274
1275 assert!(
1279 quads.len() >= 7,
1280 "expected at least 7 triples for property list + collection, got {} ({quads:?})",
1281 quads.len()
1282 );
1283
1284 let has_city = quads.iter().any(|q| {
1285 let t = q.to_triple();
1286 t.predicate().to_string().contains("city")
1287 && matches!(t.object(), Object::Literal(l) if l.value() == "Springfield")
1288 });
1289 assert!(has_city, "blank-node property list content missing");
1290
1291 let has_first = quads.iter().any(|q| {
1292 q.to_triple()
1293 .predicate()
1294 .to_string()
1295 .contains("rdf-syntax-ns#first")
1296 });
1297 assert!(
1298 has_first,
1299 "RDF collection must expand to rdf:first/rdf:rest"
1300 );
1301 }
1302
1303 #[test]
1307 fn test_turtle_triple_quoted_multiline_literal() {
1308 let turtle_data = "@prefix ex: <http://example.org/> .\nex:alice ex:bio \"\"\"Line one\nLine two\nLine three\"\"\" .";
1309
1310 let parser = Parser::new(RdfFormat::Turtle);
1311 let quads = parser
1312 .parse_str_to_quads(turtle_data)
1313 .expect("triple-quoted multi-line literals must parse");
1314
1315 assert_eq!(quads.len(), 1);
1316 let triple = quads[0].to_triple();
1317 if let Object::Literal(lit) = triple.object() {
1318 assert_eq!(lit.value(), "Line one\nLine two\nLine three");
1319 } else {
1320 panic!("Expected literal object");
1321 }
1322 }
1323
1324 #[test]
1327 fn test_trig_triple_quoted_multiline_literal_in_named_graph() {
1328 let trig_data = "@prefix ex: <http://example.org/> .\nex:g1 { ex:alice ex:bio \"\"\"Line one\nLine two\"\"\" . }";
1329
1330 let parser = Parser::new(RdfFormat::TriG);
1331 let quads = parser
1332 .parse_str_to_quads(trig_data)
1333 .expect("triple-quoted multi-line literals must parse in TriG too");
1334
1335 assert_eq!(quads.len(), 1);
1336 assert!(!quads[0].is_default_graph());
1337 let triple = quads[0].to_triple();
1338 if let Object::Literal(lit) = triple.object() {
1339 assert_eq!(lit.value(), "Line one\nLine two");
1340 } else {
1341 panic!("Expected literal object");
1342 }
1343 }
1344
1345 #[test]
1346 fn test_turtle_parsing_base_iri() {
1347 let turtle_data = r#"@base <http://example.org/> .
1348<alice> <knows> <bob> ."#;
1349
1350 let parser = Parser::new(RdfFormat::Turtle);
1351 let result = parser.parse_str_to_quads(turtle_data);
1352
1353 assert!(result.is_ok());
1354 let quads = result.expect("should have value");
1355 assert_eq!(quads.len(), 1);
1356
1357 let triple = quads[0].to_triple();
1358 if let Subject::NamedNode(subj) = triple.subject() {
1360 assert!(subj.as_str().contains("example.org"));
1361 } else {
1362 panic!("Expected named node subject");
1363 }
1364 }
1365
1366 #[test]
1367 fn test_turtle_parsing_literals() {
1368 let turtle_data = r#"@prefix ex: <http://example.org/> .
1369ex:alice ex:name "Alice"@en .
1370ex:alice ex:age "30"^^<http://www.w3.org/2001/XMLSchema#integer> ."#;
1371
1372 let parser = Parser::new(RdfFormat::Turtle);
1373 let result = parser.parse_str_to_quads(turtle_data);
1374
1375 assert!(result.is_ok());
1376 let quads = result.expect("should have value");
1377 assert_eq!(quads.len(), 2);
1378
1379 let triples: Vec<_> = quads.into_iter().map(|q| q.to_triple()).collect();
1381
1382 let mut found_lang_literal = false;
1383 let mut found_typed_literal = false;
1384
1385 for triple in triples {
1386 if let Object::Literal(literal) = triple.object() {
1387 if literal.language().is_some() {
1388 found_lang_literal = true;
1389 assert_eq!(literal.language(), Some("en"));
1390 } else {
1391 let datatype = literal.datatype();
1392 if datatype.as_str() != "http://www.w3.org/2001/XMLSchema#string"
1394 && datatype.as_str()
1395 != "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString"
1396 {
1397 found_typed_literal = true;
1398 assert!(
1399 datatype.as_str().contains("integer"),
1400 "Expected integer datatype but got: {}",
1401 datatype.as_str()
1402 );
1403 }
1404 }
1405 }
1406 }
1407
1408 assert!(found_lang_literal);
1409 assert!(found_typed_literal);
1410 }
1411
1412 #[test]
1413 fn test_parser_round_trip() {
1414 use crate::serializer::Serializer;
1415
1416 let mut original_graph = Graph::new();
1418
1419 let alice = NamedNode::new("http://example.org/alice").expect("valid IRI");
1420 let name_pred = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
1421 let name_literal = Literal::new("Alice Smith");
1422 original_graph.insert(Triple::new(alice.clone(), name_pred, name_literal));
1423
1424 let age_pred = NamedNode::new("http://xmlns.com/foaf/0.1/age").expect("valid IRI");
1425 let age_literal = Literal::new_typed("30", crate::vocab::xsd::INTEGER.clone());
1426 original_graph.insert(Triple::new(alice.clone(), age_pred, age_literal));
1427
1428 let desc_pred = NamedNode::new("http://example.org/description").expect("valid IRI");
1429 let desc_literal =
1430 Literal::new_lang("Une personne", "fr").expect("construction should succeed");
1431 original_graph.insert(Triple::new(alice, desc_pred, desc_literal));
1432
1433 let serializer = Serializer::new(RdfFormat::NTriples);
1435 let ntriples = serializer
1436 .serialize_graph(&original_graph)
1437 .expect("operation should succeed");
1438
1439 let parser = Parser::new(RdfFormat::NTriples);
1441 let quads = parser
1442 .parse_str_to_quads(&ntriples)
1443 .expect("operation should succeed");
1444
1445 let parsed_graph = Graph::from_iter(quads.into_iter().map(|q| q.to_triple()));
1447
1448 assert_eq!(original_graph.len(), parsed_graph.len());
1450
1451 for triple in original_graph.iter() {
1453 assert!(
1454 parsed_graph.contains(triple),
1455 "Parsed graph missing triple: {triple}"
1456 );
1457 }
1458 }
1459
1460 #[test]
1461 fn test_trig_parser() {
1462 let trig_data = r#"
1463@prefix ex: <http://example.org/> .
1464@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
1465
1466# Default graph
1467{
1468 ex:alice rdf:type ex:Person .
1469 ex:alice ex:name "Alice" .
1470}
1471
1472# Named graph
1473ex:graph1 {
1474 ex:bob rdf:type ex:Person .
1475 ex:bob ex:name "Bob" .
1476 ex:bob ex:age "30" .
1477}
1478"#;
1479
1480 let parser = Parser::new(RdfFormat::TriG);
1481 let quads = parser
1482 .parse_str_to_quads(trig_data)
1483 .expect("operation should succeed");
1484
1485 assert!(
1487 quads.len() >= 5,
1488 "Should parse at least 5 quads, got {}",
1489 quads.len()
1490 );
1491
1492 let default_graph_count = quads.iter().filter(|q| q.is_default_graph()).count();
1494 let named_graph_count = quads.len() - default_graph_count;
1495
1496 assert!(
1497 default_graph_count >= 2,
1498 "Should have at least 2 default graph quads, got {default_graph_count}"
1499 );
1500 assert!(
1501 named_graph_count >= 3,
1502 "Should have at least 3 named graph quads, got {named_graph_count}"
1503 );
1504
1505 let alice_uri = "http://example.org/alice";
1507 let bob_uri = "http://example.org/bob";
1508 let person_uri = "http://example.org/Person";
1509
1510 let alice_type_found = quads.iter().any(|q| {
1512 q.is_default_graph()
1513 && q.subject().to_string().contains(alice_uri)
1514 && q.object().to_string().contains(person_uri)
1515 });
1516 assert!(
1517 alice_type_found,
1518 "Should find Alice type assertion in default graph"
1519 );
1520
1521 let bob_in_named_graph = quads
1523 .iter()
1524 .any(|q| !q.is_default_graph() && q.subject().to_string().contains(bob_uri));
1525 assert!(
1526 bob_in_named_graph,
1527 "Should find Bob statements in named graph"
1528 );
1529 }
1530
1531 #[test]
1532 fn test_trig_parser_prefixes() {
1533 let trig_data = r#"
1534@prefix ex: <http://example.org/> .
1535@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1536
1537ex:person1 foaf:name "John Doe" .
1538"#;
1539
1540 let parser = Parser::new(RdfFormat::TriG);
1541 let quads = parser
1542 .parse_str_to_quads(trig_data)
1543 .expect("operation should succeed");
1544
1545 assert!(!quads.is_empty(), "Should parse prefixed statements");
1546
1547 let expanded_found = quads.iter().any(|q| {
1549 q.subject()
1550 .to_string()
1551 .contains("http://example.org/person1")
1552 && q.predicate()
1553 .to_string()
1554 .contains("http://xmlns.com/foaf/0.1/name")
1555 });
1556 assert!(expanded_found, "Should expand prefixes correctly");
1557 }
1558
1559 #[test]
1560 fn test_jsonld_parser() {
1561 let jsonld_data = r#"{
1562 "@context": {
1563 "name": "http://xmlns.com/foaf/0.1/name",
1564 "Person": "http://schema.org/Person"
1565 },
1566 "@type": "Person",
1567 "@id": "http://example.org/john",
1568 "name": "John Doe"
1569}"#;
1570
1571 let parser = Parser::new(RdfFormat::JsonLd);
1572 let result = parser.parse_str_to_quads(jsonld_data);
1573
1574 match result {
1575 Ok(quads) => {
1576 println!("JSON-LD parsed {} quads:", quads.len());
1577 for quad in &quads {
1578 println!(" {quad}");
1579 }
1580 assert!(!quads.is_empty(), "Should parse some quads from JSON-LD");
1581 }
1582 Err(e) => {
1583 println!("JSON-LD parsing error (expected during development): {e}");
1585 }
1587 }
1588 }
1589
1590 #[test]
1591 fn test_jsonld_parser_simple() {
1592 let jsonld_data = r#"{
1593 "@context": "http://schema.org/",
1594 "@type": "Person",
1595 "name": "Alice"
1596}"#;
1597
1598 let parser = Parser::new(RdfFormat::JsonLd);
1599 let result = parser.parse_str_to_quads(jsonld_data);
1600
1601 match result {
1603 Ok(quads) => {
1604 println!("Simple JSON-LD parsed {} quads", quads.len());
1605 }
1606 Err(e) => {
1607 println!("Simple JSON-LD parsing error: {e}");
1608 }
1610 }
1611 }
1612}