use super::oxrdf_helpers::{subject_to_string, term_to_string};
use crate::{KnowledgeGraph, Result, Triple};
use oxttl::NTriplesParser;
use std::io::{Read, Write};
fn write_nt_subject(w: &mut impl Write, s: &str) -> std::io::Result<()> {
if s.starts_with("_:") {
w.write_all(s.as_bytes())
} else {
w.write_all(b"<")?;
w.write_all(s.as_bytes())?;
w.write_all(b">")
}
}
fn write_nt_object(w: &mut impl Write, s: &str) -> std::io::Result<()> {
if s.starts_with("_:") || s.starts_with('"') {
w.write_all(s.as_bytes())
} else {
w.write_all(b"<")?;
w.write_all(s.as_bytes())?;
w.write_all(b">")
}
}
pub struct NTriples;
impl NTriples {
pub fn read<R: Read>(reader: R) -> Result<KnowledgeGraph> {
let mut kg = KnowledgeGraph::new();
for result in NTriplesParser::new().for_reader(reader) {
let triple =
result.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let s = subject_to_string(&triple.subject);
let p = triple.predicate.as_str().to_string();
let o = term_to_string(&triple.object);
kg.add_triple(Triple::new(s, p, o));
}
Ok(kg)
}
pub fn write<W: Write>(kg: &KnowledgeGraph, writer: W) -> Result<()> {
let mut writer = std::io::BufWriter::new(writer);
for triple in kg.triples() {
write_nt_subject(&mut writer, triple.subject().as_str())?;
writer.write_all(b" <")?;
writer.write_all(triple.predicate().as_str().as_bytes())?;
writer.write_all(b"> ")?;
write_nt_object(&mut writer, triple.object().as_str())?;
writer.write_all(b" .\n")?;
}
Ok(())
}
pub fn parse(s: &str) -> Result<KnowledgeGraph> {
Self::read(std::io::Cursor::new(s))
}
pub fn to_string(kg: &KnowledgeGraph) -> Result<String> {
let mut buf = Vec::new();
Self::write(kg, &mut buf)?;
Ok(String::from_utf8_lossy(&buf).to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_roundtrip() {
let input = r#"<http://example.org/Apple> <http://example.org/founded_by> <http://example.org/Steve_Jobs> .
<http://example.org/Apple> <http://example.org/type> <http://example.org/Company> .
"#;
let kg = NTriples::parse(input).unwrap();
assert_eq!(kg.triple_count(), 2);
let output = NTriples::to_string(&kg).unwrap();
assert!(output.contains("Apple"));
assert!(output.contains("founded_by"));
}
#[test]
fn test_blank_nodes() {
let input = "<http://example.org/s> <http://example.org/p> _:b0 .\n";
let kg = NTriples::parse(input).unwrap();
assert_eq!(kg.triple_count(), 1);
let triple = kg.triples().next().unwrap();
assert_eq!(triple.object().as_str(), "_:b0");
}
#[test]
fn test_blank_node_subject_roundtrip() {
let input = "_:b0 <http://example.org/p> <http://example.org/o> .\n";
let kg = NTriples::parse(input).unwrap();
assert_eq!(kg.triple_count(), 1);
let triple = kg.triples().next().unwrap();
assert_eq!(triple.subject().as_str(), "_:b0");
let output = NTriples::to_string(&kg).unwrap();
assert!(
output.contains("_:b0 "),
"blank node subject must not be wrapped in <>"
);
assert!(
!output.contains("<_:b0>"),
"blank node must not get angle brackets"
);
}
#[test]
fn test_literals() {
let input = "<http://example.org/s> <http://example.org/p> \"hello\"@en .\n";
let kg = NTriples::parse(input).unwrap();
let triple = kg.triples().next().unwrap();
assert_eq!(triple.object().as_str(), "\"hello\"@en");
}
#[test]
fn test_typed_literal() {
let input = "<http://example.org/s> <http://example.org/p> \"42\"^^<http://www.w3.org/2001/XMLSchema#integer> .\n";
let kg = NTriples::parse(input).unwrap();
let triple = kg.triples().next().unwrap();
assert_eq!(
triple.object().as_str(),
"\"42\"^^<http://www.w3.org/2001/XMLSchema#integer>"
);
}
}