1use std::collections::HashMap;
5
6#[derive(Debug, Clone, PartialEq)]
8pub enum ParsedTerm {
9 Iri(String),
11 PrefixedName(String),
13 Literal {
15 value: String,
17 datatype: Option<String>,
19 lang: Option<String>,
21 },
22 BlankNode(String),
24}
25
26#[derive(Debug, Clone, PartialEq)]
28pub struct ParsedTriple {
29 pub subject: ParsedTerm,
31 pub predicate: ParsedTerm,
33 pub object: ParsedTerm,
35}
36
37#[derive(Debug)]
39pub enum StreamParseError {
40 InvalidTurtle(String),
42 UnknownPrefix(String),
44 UnexpectedEof,
46 InvalidIri(String),
48}
49
50impl std::fmt::Display for StreamParseError {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 StreamParseError::InvalidTurtle(s) => write!(f, "Invalid Turtle: {s}"),
54 StreamParseError::UnknownPrefix(p) => write!(f, "Unknown prefix: {p}"),
55 StreamParseError::UnexpectedEof => write!(f, "Unexpected end of input"),
56 StreamParseError::InvalidIri(s) => write!(f, "Invalid IRI: {s}"),
57 }
58 }
59}
60
61impl std::error::Error for StreamParseError {}
62
63pub struct StreamingParser {
65 prefixes: HashMap<String, String>,
66 buffer: String,
67 triples_parsed: u64,
68 base_iri: Option<String>,
69}
70
71impl StreamingParser {
72 pub fn new() -> Self {
74 Self {
75 prefixes: HashMap::new(),
76 buffer: String::new(),
77 triples_parsed: 0,
78 base_iri: None,
79 }
80 }
81
82 pub fn with_base(base_iri: impl Into<String>) -> Self {
84 Self {
85 prefixes: HashMap::new(),
86 buffer: String::new(),
87 triples_parsed: 0,
88 base_iri: Some(base_iri.into()),
89 }
90 }
91
92 pub fn feed(&mut self, chunk: &str) -> Result<Vec<ParsedTriple>, StreamParseError> {
94 self.buffer.push_str(chunk);
95 self.parse_buffer()
96 }
97
98 pub fn flush(&mut self) -> Result<Vec<ParsedTriple>, StreamParseError> {
112 let remaining = self
113 .skip_whitespace_and_comments(self.buffer.trim_start())
114 .trim_end()
115 .to_string();
116 self.buffer.clear();
117 if remaining.is_empty() {
118 Ok(vec![])
119 } else {
120 Err(StreamParseError::UnexpectedEof)
121 }
122 }
123
124 pub fn triples_parsed(&self) -> u64 {
126 self.triples_parsed
127 }
128
129 pub fn prefixes(&self) -> &HashMap<String, String> {
131 &self.prefixes
132 }
133
134 pub fn set_base(&mut self, base: impl Into<String>) {
136 self.base_iri = Some(base.into());
137 }
138
139 pub fn reset(&mut self) {
141 self.prefixes.clear();
142 self.buffer.clear();
143 self.triples_parsed = 0;
144 self.base_iri = None;
145 }
146
147 pub fn parse_complete(input: &str) -> Result<Vec<ParsedTriple>, StreamParseError> {
149 let mut parser = Self::new();
150 let mut triples = parser.feed(input)?;
151 triples.extend(parser.flush()?);
152 Ok(triples)
153 }
154
155 pub fn expand_prefix(&self, prefixed: &str) -> Result<String, StreamParseError> {
157 if let Some(colon) = prefixed.find(':') {
158 let prefix = &prefixed[..colon];
159 let local = &prefixed[colon + 1..];
160 match self.prefixes.get(prefix) {
161 Some(base) => Ok(format!("{base}{local}")),
162 None => Err(StreamParseError::UnknownPrefix(prefix.to_string())),
163 }
164 } else {
165 Err(StreamParseError::InvalidTurtle(format!(
166 "'{prefixed}' is not a prefixed name"
167 )))
168 }
169 }
170
171 fn parse_buffer(&mut self) -> Result<Vec<ParsedTriple>, StreamParseError> {
173 let mut triples = Vec::new();
174 loop {
175 let current = self.buffer.clone();
177 let trimmed = self
178 .skip_whitespace_and_comments(current.trim_start())
179 .to_string();
180 match self.try_parse_statement(&trimmed) {
182 Ok(Some((stmt_triples, rest))) => {
183 let rest_owned = rest.to_string();
185 let consumed_len = trimmed.len() - rest.len();
187 let leading_ws = current.len() - current.trim_start().len();
188 let total_consumed = leading_ws + consumed_len;
189 self.buffer = current[total_consumed..].to_string();
190 triples.extend(stmt_triples);
191 let _ = rest_owned;
192 }
193 Ok(None) => {
194 break;
196 }
197 Err(e) => return Err(e),
198 }
199 }
200 Ok(triples)
201 }
202
203 fn skip_whitespace_and_comments<'a>(&self, input: &'a str) -> &'a str {
204 let mut s = input;
205 loop {
206 let trimmed = s.trim_start();
207 if trimmed.starts_with('#') {
208 if let Some(nl) = trimmed.find('\n') {
210 s = &trimmed[nl + 1..];
211 } else {
212 return "";
213 }
214 } else {
215 return trimmed;
216 }
217 }
218 }
219
220 fn try_parse_statement<'a>(
223 &mut self,
224 input: &'a str,
225 ) -> Result<Option<(Vec<ParsedTriple>, &'a str)>, StreamParseError> {
226 let s = self.skip_whitespace_and_comments(input);
227 if s.is_empty() {
228 return Ok(None);
229 }
230
231 if s.starts_with("@prefix") || s.starts_with("@PREFIX") {
233 return self.parse_prefix_decl(s);
234 }
235 if s.to_uppercase().starts_with("PREFIX ") {
236 return self.parse_sparql_prefix_decl(s);
237 }
238 if s.starts_with("@base") || s.starts_with("@BASE") {
240 return self.parse_base_decl(s);
241 }
242 if s.to_uppercase().starts_with("BASE ") {
243 return self.parse_sparql_base_decl(s);
244 }
245
246 self.try_parse_triple_statement(s)
249 }
250
251 fn parse_prefix_decl<'a>(
252 &mut self,
253 input: &'a str,
254 ) -> Result<Option<(Vec<ParsedTriple>, &'a str)>, StreamParseError> {
255 let rest = input
257 .trim_start_matches("@prefix")
258 .trim_start_matches("@PREFIX");
259 let rest = rest.trim_start();
260 let colon_pos = rest.find(':').ok_or(StreamParseError::UnexpectedEof)?;
262 let prefix_name = rest[..colon_pos].trim().to_string();
263 let rest = rest[colon_pos + 1..].trim_start();
264 let after_lt = match rest.strip_prefix('<') {
266 Some(r) => r,
267 None => {
268 if rest.is_empty() {
269 return Ok(None);
270 }
271 return Err(StreamParseError::InvalidTurtle(
272 "Expected '<' after prefix in @prefix declaration".to_string(),
273 ));
274 }
275 };
276 let iri_end = after_lt.find('>').ok_or(StreamParseError::UnexpectedEof)?;
277 let iri = after_lt[..iri_end].to_string();
278 let rest = after_lt[iri_end + 1..].trim_start();
279 let rest = rest.strip_prefix('.').unwrap_or(rest);
281 self.prefixes.insert(prefix_name, iri);
282 Ok(Some((vec![], rest)))
283 }
284
285 fn parse_sparql_prefix_decl<'a>(
286 &mut self,
287 input: &'a str,
288 ) -> Result<Option<(Vec<ParsedTriple>, &'a str)>, StreamParseError> {
289 let rest = input[6..].trim_start(); let colon_pos = rest.find(':').ok_or(StreamParseError::UnexpectedEof)?;
293 let prefix_name = rest[..colon_pos].trim().to_string();
294 let rest = rest[colon_pos + 1..].trim_start();
295 let after_lt2 = match rest.strip_prefix('<') {
296 Some(r) => r,
297 None => {
298 if rest.is_empty() {
299 return Ok(None);
300 }
301 return Err(StreamParseError::InvalidTurtle(
302 "Expected '<' in PREFIX declaration".to_string(),
303 ));
304 }
305 };
306 let iri_end = after_lt2.find('>').ok_or(StreamParseError::UnexpectedEof)?;
307 let iri = after_lt2[..iri_end].to_string();
308 let rest = &after_lt2[iri_end + 1..];
309 self.prefixes.insert(prefix_name, iri);
310 Ok(Some((vec![], rest)))
311 }
312
313 fn parse_base_decl<'a>(
314 &mut self,
315 input: &'a str,
316 ) -> Result<Option<(Vec<ParsedTriple>, &'a str)>, StreamParseError> {
317 let rest = &input[5..];
319 let rest = rest.trim_start();
320 let after_lt = match rest.strip_prefix('<') {
321 Some(r) => r,
322 None => return Ok(None),
323 };
324 let iri_end = after_lt.find('>').ok_or(StreamParseError::UnexpectedEof)?;
325 let iri = after_lt[..iri_end].to_string();
326 let rest = after_lt[iri_end + 1..].trim_start();
327 let rest = rest.strip_prefix('.').unwrap_or(rest);
328 self.base_iri = Some(iri);
329 Ok(Some((vec![], rest)))
330 }
331
332 fn parse_sparql_base_decl<'a>(
333 &mut self,
334 input: &'a str,
335 ) -> Result<Option<(Vec<ParsedTriple>, &'a str)>, StreamParseError> {
336 let rest = input[4..].trim_start();
338 let after_lt = match rest.strip_prefix('<') {
339 Some(r) => r,
340 None => return Ok(None),
341 };
342 let iri_end = after_lt.find('>').ok_or(StreamParseError::UnexpectedEof)?;
343 let iri = after_lt[..iri_end].to_string();
344 let rest = &after_lt[iri_end + 1..];
345 self.base_iri = Some(iri);
346 Ok(Some((vec![], rest)))
347 }
348
349 fn try_parse_triple_statement<'a>(
350 &mut self,
351 input: &'a str,
352 ) -> Result<Option<(Vec<ParsedTriple>, &'a str)>, StreamParseError> {
353 let dot_pos = find_statement_end(input);
355 let dot_pos = match dot_pos {
356 Some(p) => p,
357 None => return Ok(None), };
359
360 let statement = &input[..dot_pos];
361 let rest = &input[dot_pos + 1..]; let triple = self.parse_triple_text(statement.trim())?;
364 self.triples_parsed += 1;
365 Ok(Some((vec![triple], rest)))
366 }
367
368 fn parse_triple_text(&mut self, text: &str) -> Result<ParsedTriple, StreamParseError> {
369 let (subject, rest) = self.parse_term(text)?;
370 let rest = self.skip_whitespace_and_comments(rest.trim_start());
371 let (predicate, rest) = self.parse_term(rest)?;
372 let rest = self.skip_whitespace_and_comments(rest.trim_start());
373 let (object, _rest) = self.parse_term(rest)?;
374 Ok(ParsedTriple {
375 subject,
376 predicate,
377 object,
378 })
379 }
380
381 fn parse_term<'a>(&self, input: &'a str) -> Result<(ParsedTerm, &'a str), StreamParseError> {
382 let s = input.trim_start();
383 if let Some(after_lt) = s.strip_prefix('<') {
384 let end = after_lt
386 .find('>')
387 .ok_or(StreamParseError::InvalidIri("Unclosed '<'".to_string()))?;
388 let iri = after_lt[..end].to_string();
389 let term = ParsedTerm::Iri(iri);
390 return Ok((term, &after_lt[end + 1..]));
391 }
392 if s.starts_with('"') {
393 return self.parse_literal(s);
395 }
396 if s.starts_with('\'') {
397 return self.parse_single_quoted_literal(s);
399 }
400 if let Some(after_bn) = s.strip_prefix("_:") {
401 let end = after_bn
403 .find(|c: char| c.is_whitespace() || c == '.' || c == ';' || c == ',')
404 .unwrap_or(after_bn.len());
405 let name = after_bn[..end].to_string();
406 return Ok((ParsedTerm::BlankNode(name), &after_bn[end..]));
407 }
408 if let Some(after_true) = s.strip_prefix("true") {
409 return Ok((
410 ParsedTerm::Literal {
411 value: "true".to_string(),
412 datatype: Some("http://www.w3.org/2001/XMLSchema#boolean".to_string()),
413 lang: None,
414 },
415 after_true,
416 ));
417 }
418 if let Some(after_false) = s.strip_prefix("false") {
419 return Ok((
420 ParsedTerm::Literal {
421 value: "false".to_string(),
422 datatype: Some("http://www.w3.org/2001/XMLSchema#boolean".to_string()),
423 lang: None,
424 },
425 after_false,
426 ));
427 }
428 if let Some(after_a) = s.strip_prefix('a') {
430 if after_a.is_empty() || after_a.starts_with(|c: char| c.is_whitespace()) {
431 let expanded = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string();
433 return Ok((ParsedTerm::Iri(expanded), after_a));
434 }
435 }
436 if s.starts_with(|c: char| c.is_ascii_digit() || c == '-') {
438 let end = s
439 .find(|c: char| c.is_whitespace() || c == '.' || c == ';' || c == ',')
440 .unwrap_or(s.len());
441 let num_str = &s[..end];
443 if !num_str.is_empty() {
444 let datatype = if num_str.contains('.') {
445 "http://www.w3.org/2001/XMLSchema#decimal"
446 } else {
447 "http://www.w3.org/2001/XMLSchema#integer"
448 };
449 return Ok((
450 ParsedTerm::Literal {
451 value: num_str.to_string(),
452 datatype: Some(datatype.to_string()),
453 lang: None,
454 },
455 &s[end..],
456 ));
457 }
458 }
459 let name_end = s
461 .find(|c: char| c.is_whitespace() || c == '.' || c == ';' || c == ',' || c == ')')
462 .unwrap_or(s.len());
463 let name = &s[..name_end];
464 if name.is_empty() {
465 return Err(StreamParseError::InvalidTurtle(format!(
466 "Unexpected input: '{}'",
467 &s[..s.len().min(20)]
468 )));
469 }
470 if name.contains(':') {
471 let expanded = self.expand_prefix(name)?;
472 return Ok((ParsedTerm::PrefixedName(expanded), &s[name_end..]));
473 }
474 Err(StreamParseError::InvalidTurtle(format!(
475 "Cannot parse term from: '{}'",
476 &s[..s.len().min(30)]
477 )))
478 }
479
480 fn parse_literal<'a>(&self, input: &'a str) -> Result<(ParsedTerm, &'a str), StreamParseError> {
481 if let Some(after_tq) = input.strip_prefix("\"\"\"") {
483 let end = after_tq
484 .find("\"\"\"")
485 .ok_or(StreamParseError::InvalidTurtle(
486 "Unclosed triple-quoted string".to_string(),
487 ))?;
488 let value = after_tq[..end].to_string();
489 let rest = &after_tq[end + 3..];
490 return self.parse_literal_suffix(value, rest);
491 }
492 let rest = input.strip_prefix('"').unwrap_or(&input[1..]); let mut value = String::new();
495 let mut chars = rest.char_indices();
496 let closing_idx = loop {
497 match chars.next() {
498 Some((i, '"')) => break i,
499 Some((_, '\\')) => {
500 match chars.next() {
501 Some((_, 'n')) => value.push('\n'),
502 Some((_, 't')) => value.push('\t'),
503 Some((_, '"')) => value.push('"'),
504 Some((_, '\\')) => value.push('\\'),
505 Some((_, 'u')) => {
506 let mut hex = String::new();
508 for _ in 0..4 {
509 if let Some((_, c)) = chars.next() {
510 hex.push(c);
511 }
512 }
513 if let Ok(code) = u32::from_str_radix(&hex, 16) {
514 if let Some(c) = char::from_u32(code) {
515 value.push(c);
516 }
517 }
518 }
519 Some((_, c)) => {
520 value.push('\\');
521 value.push(c);
522 }
523 None => return Err(StreamParseError::UnexpectedEof),
524 }
525 }
526 Some((_, c)) => value.push(c),
527 None => {
528 return Err(StreamParseError::InvalidTurtle(
529 "Unclosed string literal".to_string(),
530 ))
531 }
532 }
533 };
534 let rest = &rest[closing_idx + 1..]; self.parse_literal_suffix(value, rest)
536 }
537
538 fn parse_single_quoted_literal<'a>(
539 &self,
540 input: &'a str,
541 ) -> Result<(ParsedTerm, &'a str), StreamParseError> {
542 let rest = input.strip_prefix('\'').unwrap_or(&input[1..]);
543 let mut value = String::new();
544 let mut chars = rest.char_indices();
545 let closing_idx = loop {
546 match chars.next() {
547 Some((i, '\'')) => break i,
548 Some((_, '\\')) => match chars.next() {
549 Some((_, c)) => value.push(c),
550 None => return Err(StreamParseError::UnexpectedEof),
551 },
552 Some((_, c)) => value.push(c),
553 None => {
554 return Err(StreamParseError::InvalidTurtle(
555 "Unclosed single-quoted literal".to_string(),
556 ))
557 }
558 }
559 };
560 let rest = &rest[closing_idx + 1..];
561 self.parse_literal_suffix(value, rest)
562 }
563
564 fn parse_literal_suffix<'a>(
565 &self,
566 value: String,
567 rest: &'a str,
568 ) -> Result<(ParsedTerm, &'a str), StreamParseError> {
569 if let Some(after_hat) = rest.strip_prefix("^^") {
570 let rest = after_hat;
571 if let Some(after_lt) = rest.strip_prefix('<') {
572 let end = after_lt.find('>').ok_or(StreamParseError::InvalidIri(
573 "Unclosed datatype IRI".to_string(),
574 ))?;
575 let datatype = after_lt[..end].to_string();
576 let rest = &after_lt[end + 1..];
577 return Ok((
578 ParsedTerm::Literal {
579 value,
580 datatype: Some(datatype),
581 lang: None,
582 },
583 rest,
584 ));
585 }
586 let end = rest
588 .find(|c: char| c.is_whitespace() || c == '.' || c == ';' || c == ',')
589 .unwrap_or(rest.len());
590 let pname = &rest[..end];
591 let datatype = self
592 .expand_prefix(pname)
593 .unwrap_or_else(|_| pname.to_string());
594 let rest = &rest[end..];
595 return Ok((
596 ParsedTerm::Literal {
597 value,
598 datatype: Some(datatype),
599 lang: None,
600 },
601 rest,
602 ));
603 }
604 if let Some(after_at) = rest.strip_prefix('@') {
605 let end = after_at
606 .find(|c: char| c.is_whitespace() || c == '.' || c == ';' || c == ',')
607 .unwrap_or(after_at.len());
608 let lang = after_at[..end].to_string();
609 let rest = &after_at[end..];
610 return Ok((
611 ParsedTerm::Literal {
612 value,
613 datatype: None,
614 lang: Some(lang),
615 },
616 rest,
617 ));
618 }
619 Ok((
620 ParsedTerm::Literal {
621 value,
622 datatype: None,
623 lang: None,
624 },
625 rest,
626 ))
627 }
628}
629
630impl Default for StreamingParser {
631 fn default() -> Self {
632 Self::new()
633 }
634}
635
636fn find_statement_end(input: &str) -> Option<usize> {
639 let mut in_string = false;
640 let mut in_iri = false;
641 let mut triple_quote = false;
642 let chars: Vec<char> = input.chars().collect();
643 let n = chars.len();
644 let mut i = 0;
645 while i < n {
646 let c = chars[i];
647 if in_iri {
648 if c == '>' {
649 in_iri = false;
650 }
651 i += 1;
652 continue;
653 }
654 if in_string {
655 if triple_quote {
656 if i + 2 < n && c == '"' && chars[i + 1] == '"' && chars[i + 2] == '"' {
657 in_string = false;
658 triple_quote = false;
659 i += 3;
660 continue;
661 }
662 } else {
663 if c == '\\' {
664 i += 2;
665 continue;
666 }
667 if c == '"' {
668 in_string = false;
669 }
670 }
671 i += 1;
672 continue;
673 }
674 if c == '#' {
676 while i < n && chars[i] != '\n' {
678 i += 1;
679 }
680 continue;
681 }
682 if c == '<' {
683 in_iri = true;
684 i += 1;
685 continue;
686 }
687 if c == '"' {
688 if i + 2 < n && chars[i + 1] == '"' && chars[i + 2] == '"' {
689 triple_quote = true;
690 in_string = true;
691 i += 3;
692 continue;
693 }
694 in_string = true;
695 i += 1;
696 continue;
697 }
698 if c == '.' {
699 let next = chars.get(i + 1);
701 match next {
702 None => return Some(i),
703 Some(nc) if nc.is_whitespace() || *nc == '#' => return Some(i),
704 _ => {}
705 }
706 }
707 i += 1;
708 }
709 None
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715
716 #[test]
719 fn test_parse_complete_simple() {
720 let input = "<http://s> <http://p> <http://o> .";
721 let triples = StreamingParser::parse_complete(input).expect("should succeed");
722 assert_eq!(triples.len(), 1);
723 assert_eq!(triples[0].subject, ParsedTerm::Iri("http://s".to_string()));
724 assert_eq!(
725 triples[0].predicate,
726 ParsedTerm::Iri("http://p".to_string())
727 );
728 assert_eq!(triples[0].object, ParsedTerm::Iri("http://o".to_string()));
729 }
730
731 #[test]
732 fn test_parse_complete_empty() {
733 let triples = StreamingParser::parse_complete("").expect("should succeed");
734 assert!(triples.is_empty());
735 }
736
737 #[test]
738 fn test_parse_complete_comment_only() {
739 let triples =
740 StreamingParser::parse_complete("# just a comment\n").expect("should succeed");
741 assert!(triples.is_empty());
742 }
743
744 #[test]
745 fn test_parse_complete_multiple_triples() {
746 let input = "<http://s1> <http://p> <http://o1> .\n<http://s2> <http://p> <http://o2> .";
747 let triples = StreamingParser::parse_complete(input).expect("should succeed");
748 assert_eq!(triples.len(), 2);
749 }
750
751 #[test]
754 fn test_parse_prefix_declaration() {
755 let input = "@prefix ex: <http://example.org/> .\n<http://s> <http://p> <http://o> .";
756 let triples = StreamingParser::parse_complete(input).expect("should succeed");
757 assert_eq!(triples.len(), 1);
758 }
759
760 #[test]
761 fn test_parse_prefixed_name_expansion() {
762 let input = "@prefix ex: <http://example.org/> .\nex:Alice ex:knows ex:Bob .";
763 let triples = StreamingParser::parse_complete(input).expect("should succeed");
764 assert_eq!(triples.len(), 1);
765 assert_eq!(
766 triples[0].subject,
767 ParsedTerm::PrefixedName("http://example.org/Alice".to_string())
768 );
769 }
770
771 #[test]
772 fn test_parse_sparql_prefix() {
773 let input = "PREFIX ex: <http://example.org/>\n<http://s> <http://p> <http://o> .";
774 let triples = StreamingParser::parse_complete(input).expect("should succeed");
775 assert_eq!(triples.len(), 1);
776 }
777
778 #[test]
781 fn test_expand_prefix_known() {
782 let mut parser = StreamingParser::new();
783 parser
784 .prefixes
785 .insert("ex".to_string(), "http://example.org/".to_string());
786 let expanded = parser.expand_prefix("ex:Alice").expect("should succeed");
787 assert_eq!(expanded, "http://example.org/Alice");
788 }
789
790 #[test]
791 fn test_expand_prefix_unknown_error() {
792 let parser = StreamingParser::new();
793 let result = parser.expand_prefix("unknown:Thing");
794 assert!(matches!(result, Err(StreamParseError::UnknownPrefix(_))));
795 }
796
797 #[test]
798 fn test_expand_prefix_no_colon_error() {
799 let parser = StreamingParser::new();
800 let result = parser.expand_prefix("nocolon");
801 assert!(matches!(result, Err(StreamParseError::InvalidTurtle(_))));
802 }
803
804 #[test]
807 fn test_parse_string_literal() {
808 let input = "<http://s> <http://p> \"hello\" .";
809 let triples = StreamingParser::parse_complete(input).expect("should succeed");
810 assert_eq!(triples.len(), 1);
811 assert_eq!(
812 triples[0].object,
813 ParsedTerm::Literal {
814 value: "hello".to_string(),
815 datatype: None,
816 lang: None
817 }
818 );
819 }
820
821 #[test]
822 fn test_parse_literal_with_datatype() {
823 let input = "<http://s> <http://p> \"42\"^^<http://www.w3.org/2001/XMLSchema#integer> .";
824 let triples = StreamingParser::parse_complete(input).expect("should succeed");
825 assert_eq!(triples.len(), 1);
826 match &triples[0].object {
827 ParsedTerm::Literal {
828 value, datatype, ..
829 } => {
830 assert_eq!(value, "42");
831 assert!(datatype.as_deref().unwrap_or("").contains("integer"));
832 }
833 _ => panic!("expected literal"),
834 }
835 }
836
837 #[test]
838 fn test_parse_literal_with_lang() {
839 let input = "<http://s> <http://p> \"hello\"@en .";
840 let triples = StreamingParser::parse_complete(input).expect("should succeed");
841 assert_eq!(triples.len(), 1);
842 match &triples[0].object {
843 ParsedTerm::Literal { value, lang, .. } => {
844 assert_eq!(value, "hello");
845 assert_eq!(lang.as_deref(), Some("en"));
846 }
847 _ => panic!("expected literal"),
848 }
849 }
850
851 #[test]
854 fn test_parse_blank_node() {
855 let input = "_:b0 <http://p> <http://o> .";
856 let triples = StreamingParser::parse_complete(input).expect("should succeed");
857 assert_eq!(triples.len(), 1);
858 assert_eq!(triples[0].subject, ParsedTerm::BlankNode("b0".to_string()));
859 }
860
861 #[test]
862 fn test_parse_blank_node_object() {
863 let input = "<http://s> <http://p> _:b1 .";
864 let triples = StreamingParser::parse_complete(input).expect("should succeed");
865 assert_eq!(triples.len(), 1);
866 assert_eq!(triples[0].object, ParsedTerm::BlankNode("b1".to_string()));
867 }
868
869 #[test]
872 fn test_parse_in_two_chunks_simple() {
873 let mut parser = StreamingParser::new();
874 let chunk1 = "<http://s> <http://p>";
875 let chunk2 = " <http://o> .";
876 let t1 = parser.feed(chunk1).expect("should succeed");
877 assert!(t1.is_empty()); let t2 = parser.feed(chunk2).expect("should succeed");
879 assert_eq!(t2.len(), 1);
880 assert_eq!(parser.triples_parsed(), 1);
881 }
882
883 #[test]
884 fn test_parse_prefix_then_triple_in_chunks() {
885 let mut parser = StreamingParser::new();
886 let chunk1 = "@prefix ex: <http://example.org/> .\n";
887 let chunk2 = "ex:s ex:p ex:o .";
888 let t1 = parser.feed(chunk1).expect("should succeed");
889 assert!(t1.is_empty());
890 let t2 = parser.feed(chunk2).expect("should succeed");
891 assert_eq!(t2.len(), 1);
892 }
893
894 #[test]
895 fn test_parse_10_triples_chunked() {
896 let mut input = String::new();
897 for i in 0..10 {
898 input.push_str(&format!("<http://s{i}> <http://p> <http://o{i}> .\n"));
899 }
900 let mut parser = StreamingParser::new();
901 let mid = input.len() / 2;
902 let t1 = parser.feed(&input[..mid]).expect("should succeed");
903 let t2 = parser.feed(&input[mid..]).expect("should succeed");
904 let total = t1.len() + t2.len();
905 assert_eq!(total, 10, "Expected 10 triples, got {total}");
906 }
907
908 #[test]
911 fn test_triples_parsed_counter() {
912 let mut parser = StreamingParser::new();
913 parser
914 .feed("<http://s1> <http://p> <http://o1> .")
915 .expect("should succeed");
916 parser
917 .feed("<http://s2> <http://p> <http://o2> .")
918 .expect("should succeed");
919 assert_eq!(parser.triples_parsed(), 2);
920 }
921
922 #[test]
925 fn test_prefixes_accessor() {
926 let mut parser = StreamingParser::new();
927 parser
928 .feed("@prefix ex: <http://example.org/> .")
929 .expect("should succeed");
930 assert!(parser.prefixes().contains_key("ex"));
931 }
932
933 #[test]
936 fn test_reset() {
937 let mut parser = StreamingParser::new();
938 parser
939 .feed("<http://s> <http://p> <http://o> .")
940 .expect("should succeed");
941 parser
942 .feed("@prefix ex: <http://example.org/> .")
943 .expect("should succeed");
944 parser.reset();
945 assert_eq!(parser.triples_parsed(), 0);
946 assert!(parser.prefixes().is_empty());
947 }
948
949 #[test]
952 fn test_with_base_iri() {
953 let parser = StreamingParser::with_base("http://base.example.org/");
954 assert_eq!(
955 parser.base_iri,
956 Some("http://base.example.org/".to_string())
957 );
958 }
959
960 #[test]
961 fn test_set_base() {
962 let mut parser = StreamingParser::new();
963 parser.set_base("http://new-base.example.org/");
964 assert_eq!(
965 parser.base_iri,
966 Some("http://new-base.example.org/".to_string())
967 );
968 }
969
970 #[test]
973 fn test_flush_empty_buffer() {
974 let mut parser = StreamingParser::new();
975 let result = parser.flush().expect("should succeed");
976 assert!(result.is_empty());
977 }
978
979 #[test]
980 fn test_flush_after_complete_triple() {
981 let mut parser = StreamingParser::new();
982 parser
983 .feed("<http://s> <http://p> <http://o> .")
984 .expect("should succeed");
985 let result = parser.flush().expect("should succeed");
986 assert!(result.is_empty()); }
988
989 #[test]
990 fn regression_flush_errors_on_truncated_trailing_statement() {
991 let mut parser = StreamingParser::new();
995 parser
996 .feed("<http://s> <http://p> <http://o>")
997 .expect("feed of an incomplete statement should not itself error");
998 let result = parser.flush();
999 assert!(
1000 matches!(result, Err(StreamParseError::UnexpectedEof)),
1001 "flush() on a truncated trailing statement must return Err(UnexpectedEof), got {result:?}"
1002 );
1003 }
1004
1005 #[test]
1006 fn regression_flush_errors_on_partial_iri_token() {
1007 let mut parser = StreamingParser::new();
1008 parser
1009 .feed("<http://s> <http://p> <http://incomplete")
1010 .expect("feed of a partial token should not itself error");
1011 let result = parser.flush();
1012 assert!(
1013 result.is_err(),
1014 "a dangling, unterminated token must error at flush"
1015 );
1016 }
1017
1018 #[test]
1019 fn regression_parse_complete_errors_on_truncated_document() {
1020 let input = "<http://s1> <http://p> <http://o1> .\n<http://s2> <http://p> <http://o2>";
1024 let result = StreamingParser::parse_complete(input);
1025 assert!(
1026 result.is_err(),
1027 "a document truncated mid-statement must fail rather than succeed with a silently dropped triple"
1028 );
1029 }
1030
1031 #[test]
1032 fn regression_flush_still_ok_for_trailing_comment_only() {
1033 let mut parser = StreamingParser::new();
1036 parser
1037 .feed("<http://s> <http://p> <http://o> .\n# trailing comment")
1038 .expect("should succeed");
1039 let result = parser
1040 .flush()
1041 .expect("trailing comment-only remainder should not error");
1042 assert!(result.is_empty());
1043 }
1044
1045 #[test]
1048 fn test_stream_parse_error_display() {
1049 let e = StreamParseError::InvalidTurtle("oops".to_string());
1050 assert!(format!("{e}").contains("oops"));
1051 let e2 = StreamParseError::UnknownPrefix("foo".to_string());
1052 assert!(format!("{e2}").contains("foo"));
1053 let e3 = StreamParseError::UnexpectedEof;
1054 assert!(!format!("{e3}").is_empty());
1055 let e4 = StreamParseError::InvalidIri("bad".to_string());
1056 assert!(format!("{e4}").contains("bad"));
1057 }
1058
1059 #[test]
1062 fn test_default() {
1063 let parser = StreamingParser::default();
1064 assert_eq!(parser.triples_parsed(), 0);
1065 assert!(parser.prefixes().is_empty());
1066 }
1067
1068 #[test]
1071 fn test_rdf_type_shorthand() {
1072 let input = "<http://s> a <http://Type> .";
1073 let triples = StreamingParser::parse_complete(input).expect("should succeed");
1074 assert_eq!(triples.len(), 1);
1075 assert_eq!(
1076 triples[0].predicate,
1077 ParsedTerm::Iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string())
1078 );
1079 }
1080}