Files
visualizador_instanciados/radial_sugiyama/src/ttl.rs
2026-03-23 11:13:27 -03:00

201 lines
5.9 KiB
Rust

use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;
use oxrdf::vocab::rdfs;
use oxrdf::{NamedOrBlankNode, Term};
use oxttl::{TurtleParseError, TurtleParser};
use crate::model::{Edge, Graph, Node};
#[derive(Debug)]
pub enum TtlImportError {
Io(std::io::Error),
Parse(TurtleParseError),
NoSubclassTriples,
}
impl Display for TtlImportError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TtlImportError::Io(error) => write!(f, "failed to read Turtle input: {error}"),
TtlImportError::Parse(error) => write!(f, "failed to parse Turtle input: {error}"),
TtlImportError::NoSubclassTriples => {
write!(
f,
"no usable rdfs:subClassOf triples were found in the Turtle input"
)
}
}
}
}
impl Error for TtlImportError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
TtlImportError::Io(error) => Some(error),
TtlImportError::Parse(error) => Some(error),
TtlImportError::NoSubclassTriples => None,
}
}
}
impl From<std::io::Error> for TtlImportError {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}
impl From<TurtleParseError> for TtlImportError {
fn from(error: TurtleParseError) -> Self {
Self::Parse(error)
}
}
pub fn graph_from_ttl_reader<R: Read>(reader: R) -> Result<Graph, TtlImportError> {
let mut nodes = Vec::new();
let mut node_indices = HashMap::new();
let mut edges = Vec::new();
let mut seen_edges = HashSet::new();
for triple in TurtleParser::new().for_reader(reader) {
let triple = triple?;
if triple.predicate.as_ref() != rdfs::SUB_CLASS_OF {
continue;
}
let NamedOrBlankNode::NamedNode(subject) = triple.subject else {
continue;
};
let Term::NamedNode(object) = triple.object else {
continue;
};
let subclass = get_or_insert_node(&mut nodes, &mut node_indices, subject.as_str());
let superclass = get_or_insert_node(&mut nodes, &mut node_indices, object.as_str());
if seen_edges.insert((superclass, subclass)) {
edges.push(Edge::new(superclass, subclass));
}
}
if edges.is_empty() {
return Err(TtlImportError::NoSubclassTriples);
}
Ok(Graph::new(nodes, edges))
}
pub fn graph_from_ttl_path(path: impl AsRef<Path>) -> Result<Graph, TtlImportError> {
let file = File::open(path)?;
graph_from_ttl_reader(BufReader::new(file))
}
fn get_or_insert_node(
nodes: &mut Vec<Node>,
node_indices: &mut HashMap<String, usize>,
iri: &str,
) -> usize {
if let Some(&index) = node_indices.get(iri) {
return index;
}
let index = nodes.len();
nodes.push(Node {
label: Some(iri.to_owned()),
..Node::default()
});
node_indices.insert(iri.to_owned(), index);
index
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{layout_radial_hierarchy, LayoutConfig};
const TTL_PREFIXES: &str = "@prefix ex: <http://example.com/> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix schema: <http://schema.org/> .\n";
#[test]
fn imports_only_subclass_triples() {
let ttl = format!(
"{TTL_PREFIXES}ex:A rdfs:subClassOf ex:B .\nex:A schema:name \"Alpha\" .\nex:B schema:name \"Beta\" .\n"
);
let graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges, vec![Edge::new(1, 0)]);
assert_eq!(
graph.nodes[0].label.as_deref(),
Some("http://example.com/A")
);
assert_eq!(
graph.nodes[1].label.as_deref(),
Some("http://example.com/B")
);
}
#[test]
fn deduplicates_repeated_subclass_triples() {
let ttl =
format!("{TTL_PREFIXES}ex:A rdfs:subClassOf ex:B .\nex:A rdfs:subClassOf ex:B .\n");
let graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();
assert_eq!(graph.edges, vec![Edge::new(1, 0)]);
}
#[test]
fn ignores_blank_node_and_literal_targets() {
let ttl = format!(
"{TTL_PREFIXES}ex:A rdfs:subClassOf [ a ex:Anonymous ] .\nex:B rdfs:subClassOf \"Literal\" .\nex:C rdfs:subClassOf ex:D .\n"
);
let graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges, vec![Edge::new(1, 0)]);
assert_eq!(
graph.nodes[0].label.as_deref(),
Some("http://example.com/C")
);
assert_eq!(
graph.nodes[1].label.as_deref(),
Some("http://example.com/D")
);
}
#[test]
fn imports_can_flow_into_layout() {
let ttl =
format!("{TTL_PREFIXES}ex:A rdfs:subClassOf ex:B .\nex:B rdfs:subClassOf ex:C .\n");
let mut graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();
layout_radial_hierarchy(&mut graph, LayoutConfig::default()).unwrap();
assert!(graph
.nodes
.iter()
.all(|node| node.x.is_finite() && node.y.is_finite()));
}
#[test]
fn returns_clear_error_for_invalid_turtle() {
let ttl = "@prefix ex: <http://example.com/> .\nex:A rdfs:subClassOf .\n";
let error = graph_from_ttl_reader(ttl.as_bytes()).unwrap_err();
assert!(matches!(error, TtlImportError::Parse(_)));
}
#[test]
fn returns_clear_error_when_no_subclass_triples_exist() {
let ttl = format!("{TTL_PREFIXES}ex:A schema:name \"Alpha\" .\n");
let error = graph_from_ttl_reader(ttl.as_bytes()).unwrap_err();
assert!(matches!(error, TtlImportError::NoSubclassTriples));
}
}