use super::oxrdf_helpers::{subject_to_string, term_to_string};
use crate::{KnowledgeGraph, Result, Triple};
use oxttl::TurtleParser;
use std::collections::{BTreeMap, HashMap};
use std::io::{Read, Write};
pub struct Turtle;
impl Turtle {
pub fn read<R: Read>(reader: R, base_iri: Option<&str>) -> Result<KnowledgeGraph> {
let mut parser = TurtleParser::new();
if let Some(base) = base_iri {
parser = parser
.with_base_iri(base)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
}
let mut kg = KnowledgeGraph::new();
for result in parser.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 = crate::rdf::iri_to_string(triple.predicate.as_str());
let o = term_to_string(&triple.object);
kg.add_triple(Triple::new(s, p, o));
}
Ok(kg)
}
pub fn write<W: Write>(
kg: &KnowledgeGraph,
mut writer: W,
prefixes: &HashMap<String, String>,
) -> Result<()> {
let prefixes: BTreeMap<_, _> = prefixes.iter().collect();
for (prefix, iri) in &prefixes {
writeln!(
writer,
"@prefix {}: <{}> .",
prefix,
crate::rdf::escape_iri(iri)
)?;
}
if !prefixes.is_empty() {
writeln!(writer)?;
}
let mut by_subject: BTreeMap<&str, Vec<_>> = BTreeMap::new();
for triple in kg.triples() {
by_subject
.entry(triple.subject().as_str())
.or_default()
.push(triple);
}
for (subject, triples) in &mut by_subject {
triples.sort_by(|a, b| {
a.predicate()
.as_str()
.cmp(b.predicate().as_str())
.then_with(|| a.object().as_str().cmp(b.object().as_str()))
});
let s = render_term(subject, &prefixes);
for (i, triple) in triples.iter().enumerate() {
let p = render_term(triple.predicate().as_str(), &prefixes);
let o = render_term(triple.object().as_str(), &prefixes);
if i == 0 {
write!(writer, "{} {} {}", s, p, o)?;
} else {
write!(writer, " ;\n {} {}", p, o)?;
}
}
writeln!(writer, " .")?;
}
Ok(())
}
pub fn write_default<W: Write>(kg: &KnowledgeGraph, writer: W) -> Result<()> {
let prefixes = default_prefixes();
Self::write(kg, writer, &prefixes)
}
pub fn to_string(kg: &KnowledgeGraph) -> Result<String> {
let mut buf = Vec::new();
Self::write_default(kg, &mut buf)?;
Ok(String::from_utf8_lossy(&buf).to_string())
}
}
fn render_term(s: &str, prefixes: &BTreeMap<&String, &String>) -> String {
if s.starts_with("_:") {
return s.to_string();
}
if s.starts_with('"') {
return crate::rdf::render_object(s);
}
let iri = s
.strip_prefix('<')
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(s);
if let Some((prefix, local)) = prefixes
.iter()
.filter_map(|(prefix, base)| {
let local = iri.strip_prefix(base.as_str())?;
is_simple_local_name(local).then_some((*prefix, *base, local))
})
.max_by_key(|(_, base, _)| base.len())
.map(|(prefix, _, local)| (prefix, local))
{
return format!("{prefix}:{local}");
}
format!("<{}>", crate::rdf::iri_body_for(iri))
}
fn is_simple_local_name(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
}
pub fn default_prefixes() -> HashMap<String, String> {
let mut m = HashMap::new();
m.insert(
"rdf".into(),
"http://www.w3.org/1999/02/22-rdf-syntax-ns#".into(),
);
m.insert(
"rdfs".into(),
"http://www.w3.org/2000/01/rdf-schema#".into(),
);
m
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Triple;
#[test]
fn test_turtle_parse() {
let input = r#"
@prefix ex: <http://example.org/> .
ex:Apple ex:founded_by ex:Steve_Jobs .
"#;
let kg = Turtle::read(std::io::Cursor::new(input), None).unwrap();
assert_eq!(kg.triple_count(), 1);
let triple = kg.triples().next().unwrap();
assert_eq!(triple.subject().as_str(), "http://example.org/Apple");
assert_eq!(triple.predicate().as_str(), "http://example.org/founded_by");
assert_eq!(triple.object().as_str(), "http://example.org/Steve_Jobs");
}
#[test]
fn test_turtle_output() {
let mut kg = KnowledgeGraph::new();
kg.add_triple(Triple::new(
"http://example.org/Apple",
"http://example.org/founded_by",
"http://example.org/Steve_Jobs",
));
let output = Turtle::to_string(&kg).unwrap();
assert!(output.contains("Apple"));
}
#[test]
fn test_turtle_with_base_iri() {
let input = r#"
@base <http://example.org/> .
<Apple> <founded_by> <Steve_Jobs> .
"#;
let kg = Turtle::read(std::io::Cursor::new(input), Some("http://example.org/")).unwrap();
assert_eq!(kg.triple_count(), 1);
}
}