use std::collections::HashMap;
use std::fmt;
use std::hash::Hash;
use serde::{Deserialize, Serialize};
use crate::{StarError, StarResult};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum StarTerm {
NamedNode(NamedNode),
BlankNode(BlankNode),
Literal(Literal),
QuotedTriple(Box<StarTriple>),
Variable(Variable),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NamedNode {
pub iri: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BlankNode {
pub id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Literal {
pub value: String,
pub language: Option<String>,
pub datatype: Option<NamedNode>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Variable {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StarTriple {
pub subject: StarTerm,
pub predicate: StarTerm,
pub object: StarTerm,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StarQuad {
pub subject: StarTerm,
pub predicate: StarTerm,
pub object: StarTerm,
pub graph: Option<StarTerm>,
}
impl StarTerm {
pub fn iri(iri: &str) -> StarResult<Self> {
if iri.is_empty() {
return Err(StarError::invalid_term_type("IRI cannot be empty"));
}
Ok(StarTerm::NamedNode(NamedNode {
iri: iri.to_string(),
}))
}
pub fn blank_node(id: &str) -> StarResult<Self> {
if id.is_empty() {
return Err(StarError::invalid_term_type(
"Blank node ID cannot be empty",
));
}
Ok(StarTerm::BlankNode(BlankNode { id: id.to_string() }))
}
pub fn literal(value: &str) -> StarResult<Self> {
Ok(StarTerm::Literal(Literal {
value: value.to_string(),
language: None,
datatype: None,
}))
}
pub fn literal_with_language(value: &str, language: &str) -> StarResult<Self> {
Ok(StarTerm::Literal(Literal {
value: value.to_string(),
language: Some(language.to_string()),
datatype: None,
}))
}
pub fn literal_with_datatype(value: &str, datatype: &str) -> StarResult<Self> {
Ok(StarTerm::Literal(Literal {
value: value.to_string(),
language: None,
datatype: Some(NamedNode {
iri: datatype.to_string(),
}),
}))
}
pub fn quoted_triple(triple: StarTriple) -> Self {
StarTerm::QuotedTriple(Box::new(triple))
}
pub fn variable(name: &str) -> StarResult<Self> {
if name.is_empty() {
return Err(StarError::invalid_term_type(
"Variable name cannot be empty",
));
}
Ok(StarTerm::Variable(Variable {
name: name.to_string(),
}))
}
pub fn is_named_node(&self) -> bool {
matches!(self, StarTerm::NamedNode(_))
}
pub fn is_blank_node(&self) -> bool {
matches!(self, StarTerm::BlankNode(_))
}
pub fn is_literal(&self) -> bool {
matches!(self, StarTerm::Literal(_))
}
pub fn is_quoted_triple(&self) -> bool {
matches!(self, StarTerm::QuotedTriple(_))
}
pub fn is_variable(&self) -> bool {
matches!(self, StarTerm::Variable(_))
}
pub fn as_named_node(&self) -> Option<&NamedNode> {
match self {
StarTerm::NamedNode(node) => Some(node),
_ => None,
}
}
pub fn as_blank_node(&self) -> Option<&BlankNode> {
match self {
StarTerm::BlankNode(node) => Some(node),
_ => None,
}
}
pub fn as_literal(&self) -> Option<&Literal> {
match self {
StarTerm::Literal(literal) => Some(literal),
_ => None,
}
}
pub fn as_quoted_triple(&self) -> Option<&StarTriple> {
match self {
StarTerm::QuotedTriple(triple) => Some(triple),
_ => None,
}
}
pub fn as_variable(&self) -> Option<&Variable> {
match self {
StarTerm::Variable(var) => Some(var),
_ => None,
}
}
pub fn can_be_subject(&self) -> bool {
matches!(
self,
StarTerm::NamedNode(_) | StarTerm::BlankNode(_) | StarTerm::QuotedTriple(_)
)
}
pub fn can_be_predicate(&self) -> bool {
matches!(self, StarTerm::NamedNode(_))
}
pub fn can_be_object(&self) -> bool {
true }
pub fn nesting_depth(&self) -> usize {
match self {
StarTerm::QuotedTriple(triple) => {
1 + triple
.subject
.nesting_depth()
.max(triple.predicate.nesting_depth())
.max(triple.object.nesting_depth())
}
_ => 0,
}
}
}
impl StarTriple {
pub fn new(subject: StarTerm, predicate: StarTerm, object: StarTerm) -> Self {
Self {
subject,
predicate,
object,
}
}
pub fn validate(&self) -> StarResult<()> {
if !self.subject.can_be_subject() {
return Err(StarError::invalid_quoted_triple(format!(
"Invalid subject term: {:?}",
self.subject
)));
}
if !self.predicate.can_be_predicate() {
return Err(StarError::invalid_quoted_triple(format!(
"Invalid predicate term: {:?}",
self.predicate
)));
}
if !self.object.can_be_object() {
return Err(StarError::invalid_quoted_triple(format!(
"Invalid object term: {:?}",
self.object
)));
}
if self.is_self_contained() {
return Err(StarError::invalid_quoted_triple(
"Triple cannot contain itself (self-containment is not allowed in RDF-star)"
.to_string(),
));
}
Ok(())
}
pub fn contains_triple(&self, target: &StarTriple) -> bool {
self.check_contains_triple(target, 0, 100)
}
fn check_contains_triple(&self, target: &StarTriple, depth: usize, max_depth: usize) -> bool {
if depth > max_depth {
return false;
}
if let StarTerm::QuotedTriple(qt) = &self.subject {
if **qt == *target {
return true;
}
if qt.check_contains_triple(target, depth + 1, max_depth) {
return true;
}
}
if let StarTerm::QuotedTriple(qt) = &self.predicate {
if **qt == *target || qt.check_contains_triple(target, depth + 1, max_depth) {
return true;
}
}
if let StarTerm::QuotedTriple(qt) = &self.object {
if **qt == *target || qt.check_contains_triple(target, depth + 1, max_depth) {
return true;
}
}
false
}
pub fn is_self_contained(&self) -> bool {
self.contains_triple(self)
}
pub fn nesting_depth(&self) -> usize {
self.subject
.nesting_depth()
.max(self.predicate.nesting_depth())
.max(self.object.nesting_depth())
}
pub fn contains_quoted_triples(&self) -> bool {
self.subject.is_quoted_triple()
|| self.predicate.is_quoted_triple()
|| self.object.is_quoted_triple()
}
pub fn count_quoted_triples(&self) -> usize {
let mut count = 0;
if let StarTerm::QuotedTriple(inner) = &self.subject {
count += 1 + inner.count_quoted_triples();
}
if let StarTerm::QuotedTriple(inner) = &self.predicate {
count += 1 + inner.count_quoted_triples();
}
if let StarTerm::QuotedTriple(inner) = &self.object {
count += 1 + inner.count_quoted_triples();
}
count
}
pub fn to_quad(self, graph: Option<StarTerm>) -> StarQuad {
StarQuad {
subject: self.subject,
predicate: self.predicate,
object: self.object,
graph,
}
}
}
impl StarQuad {
pub fn new(
subject: StarTerm,
predicate: StarTerm,
object: StarTerm,
graph: Option<StarTerm>,
) -> Self {
Self {
subject,
predicate,
object,
graph,
}
}
pub fn to_triple(self) -> StarTriple {
StarTriple {
subject: self.subject,
predicate: self.predicate,
object: self.object,
}
}
pub fn validate(&self) -> StarResult<()> {
let triple = StarTriple {
subject: self.subject.clone(),
predicate: self.predicate.clone(),
object: self.object.clone(),
};
triple.validate()?;
if let Some(ref graph) = self.graph {
if !matches!(graph, StarTerm::NamedNode(_) | StarTerm::BlankNode(_)) {
return Err(StarError::invalid_quoted_triple(
"Graph name must be a named node or blank node",
));
}
}
Ok(())
}
}
impl fmt::Display for StarTerm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StarTerm::NamedNode(node) => write!(f, "<{}>", node.iri),
StarTerm::BlankNode(node) => write!(f, "_:{}", node.id),
StarTerm::Literal(literal) => {
write!(f, "\"{}\"", literal.value)?;
if let Some(ref lang) = literal.language {
write!(f, "@{lang}")?;
}
if let Some(ref datatype) = literal.datatype {
write!(f, "^^<{}>", datatype.iri)?;
}
Ok(())
}
StarTerm::QuotedTriple(triple) => write!(
f,
"<<{} {} {}>>",
triple.subject, triple.predicate, triple.object
),
StarTerm::Variable(var) => write!(f, "?{}", var.name),
}
}
}
impl fmt::Display for StarTriple {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {} {} .", self.subject, self.predicate, self.object)
}
}
impl fmt::Display for StarQuad {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {} {}", self.subject, self.predicate, self.object)?;
if let Some(ref graph) = self.graph {
write!(f, " {graph}")?;
}
write!(f, " .")
}
}
pub trait StarTermVisitor {
fn visit_term(&mut self, term: &StarTerm);
}
impl StarTriple {
pub fn visit_terms<V: StarTermVisitor>(&self, visitor: &mut V) {
visitor.visit_term(&self.subject);
visitor.visit_term(&self.predicate);
visitor.visit_term(&self.object);
if let StarTerm::QuotedTriple(triple) = &self.subject {
triple.visit_terms(visitor);
}
if let StarTerm::QuotedTriple(triple) = &self.predicate {
triple.visit_terms(visitor);
}
if let StarTerm::QuotedTriple(triple) = &self.object {
triple.visit_terms(visitor);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StarGraph {
triples: Vec<StarTriple>,
named_graphs: HashMap<String, Vec<StarTriple>>,
quads: Vec<StarQuad>,
statistics: HashMap<String, usize>,
}
impl StarGraph {
pub fn new() -> Self {
let mut statistics = HashMap::new();
statistics.insert("triples".to_string(), 0);
Self {
triples: Vec::new(),
named_graphs: HashMap::new(),
quads: Vec::new(),
statistics,
}
}
pub fn insert(&mut self, triple: StarTriple) -> StarResult<()> {
triple.validate()?;
self.triples.push(triple.clone());
let quad = StarQuad::new(triple.subject, triple.predicate, triple.object, None);
self.quads.push(quad);
*self.statistics.entry("triples".to_string()).or_insert(0) += 1;
Ok(())
}
pub fn insert_quad(&mut self, quad: StarQuad) -> StarResult<()> {
quad.validate()?;
let triple = StarTriple::new(
quad.subject.clone(),
quad.predicate.clone(),
quad.object.clone(),
);
if let Some(ref graph_term) = quad.graph {
let graph_key = match graph_term {
StarTerm::NamedNode(node) => node.iri.clone(),
StarTerm::BlankNode(node) => format!("_:{}", node.id),
_ => {
return Err(StarError::invalid_quoted_triple(
"Graph name must be a named node or blank node",
))
}
};
self.named_graphs
.entry(graph_key.clone())
.or_default()
.push(triple);
*self
.statistics
.entry(format!("graph_{graph_key}"))
.or_insert(0) += 1;
} else {
self.triples.push(triple);
*self.statistics.entry("triples".to_string()).or_insert(0) += 1;
}
self.quads.push(quad);
*self.statistics.entry("quads".to_string()).or_insert(0) += 1;
Ok(())
}
pub fn triples(&self) -> &[StarTriple] {
&self.triples
}
pub fn quads(&self) -> &[StarQuad] {
&self.quads
}
pub fn named_graph_triples(&self, graph_name: &str) -> Option<&Vec<StarTriple>> {
self.named_graphs.get(graph_name)
}
pub fn named_graph_names(&self) -> Vec<&String> {
self.named_graphs.keys().collect()
}
pub fn all_triples(&self) -> Vec<StarTriple> {
let mut all = self.triples.clone();
for triples in self.named_graphs.values() {
all.extend(triples.clone());
}
all
}
pub fn contains(&self, triple: &StarTriple) -> bool {
self.triples.contains(triple)
|| self
.named_graphs
.values()
.any(|triples| triples.contains(triple))
}
pub fn contains_named_graph(&self, graph_name: &str) -> bool {
self.named_graphs.contains_key(graph_name)
}
pub fn remove(&mut self, triple: &StarTriple) -> bool {
if let Some(pos) = self.triples.iter().position(|t| t == triple) {
self.triples.remove(pos);
self.quads.retain(|q| {
let q_triple =
StarTriple::new(q.subject.clone(), q.predicate.clone(), q.object.clone());
q_triple != *triple || q.graph.is_some()
});
if let Some(count) = self.statistics.get_mut("triples") {
*count = count.saturating_sub(1);
}
true
} else {
false
}
}
pub fn remove_quad(&mut self, quad: &StarQuad) -> bool {
if let Some(pos) = self.quads.iter().position(|q| q == quad) {
let removed_quad = self.quads.remove(pos);
if let Some(ref graph_term) = removed_quad.graph {
let graph_key = match graph_term {
StarTerm::NamedNode(node) => node.iri.clone(),
StarTerm::BlankNode(node) => format!("_:{}", node.id),
_ => return false,
};
if let Some(triples) = self.named_graphs.get_mut(&graph_key) {
let triple = StarTriple::new(
removed_quad.subject,
removed_quad.predicate,
removed_quad.object,
);
triples.retain(|t| t != &triple);
if triples.is_empty() {
self.named_graphs.remove(&graph_key);
}
}
} else {
let triple = StarTriple::new(
removed_quad.subject,
removed_quad.predicate,
removed_quad.object,
);
self.triples.retain(|t| t != &triple);
}
if let Some(count) = self.statistics.get_mut("quads") {
*count = count.saturating_sub(1);
}
true
} else {
false
}
}
pub fn len(&self) -> usize {
self.triples.len()
}
pub fn total_len(&self) -> usize {
self.triples.len() + self.named_graphs.values().map(|v| v.len()).sum::<usize>()
}
pub fn quad_len(&self) -> usize {
self.quads.len()
}
pub fn is_empty(&self) -> bool {
self.triples.is_empty()
}
pub fn is_completely_empty(&self) -> bool {
self.triples.is_empty() && self.named_graphs.is_empty()
}
pub fn clear(&mut self) {
self.triples.clear();
self.named_graphs.clear();
self.quads.clear();
self.statistics.clear();
}
pub fn clear_named_graph(&mut self, graph_name: &str) {
if let Some(triples) = self.named_graphs.remove(graph_name) {
self.quads.retain(|q| {
if let Some(ref graph_term) = q.graph {
let key = match graph_term {
StarTerm::NamedNode(node) => node.iri.clone(),
StarTerm::BlankNode(node) => format!("_:{}", node.id),
_ => String::new(),
};
key != graph_name
} else {
true
}
});
self.statistics.remove(&format!("graph_{graph_name}"));
if let Some(count) = self.statistics.get_mut("quads") {
*count = count.saturating_sub(triples.len());
}
}
}
pub fn statistics(&self) -> &HashMap<String, usize> {
&self.statistics
}
pub fn count_quoted_triples(&self) -> usize {
let mut count = 0;
for triple in &self.triples {
if triple.contains_quoted_triples() {
count += 1;
}
}
for triples in self.named_graphs.values() {
for triple in triples {
if triple.contains_quoted_triples() {
count += 1;
}
}
}
count
}
pub fn max_nesting_depth(&self) -> usize {
let default_max = self
.triples
.iter()
.map(|t| t.nesting_depth())
.max()
.unwrap_or(0);
let named_max = self
.named_graphs
.values()
.flat_map(|triples| triples.iter())
.map(|t| t.nesting_depth())
.max()
.unwrap_or(0);
default_max.max(named_max)
}
pub fn is_valid(&self) -> bool {
for triple in &self.triples {
if triple.validate().is_err() {
return false;
}
}
for triples in self.named_graphs.values() {
for triple in triples {
if triple.validate().is_err() {
return false;
}
}
}
true
}
pub fn iter(&self) -> impl Iterator<Item = &StarTriple> {
self.triples
.iter()
.chain(self.named_graphs.values().flatten())
}
pub fn subjects(&self) -> impl Iterator<Item = &StarTerm> {
self.iter().map(|triple| &triple.subject)
}
pub fn predicates(&self) -> impl Iterator<Item = &StarTerm> {
self.iter().map(|triple| &triple.predicate)
}
pub fn objects(&self) -> impl Iterator<Item = &StarTerm> {
self.iter().map(|triple| &triple.object)
}
}
impl<'a> IntoIterator for &'a StarGraph {
type Item = &'a StarTriple;
type IntoIter = std::iter::Chain<
std::slice::Iter<'a, StarTriple>,
std::iter::Flatten<std::collections::hash_map::Values<'a, String, Vec<StarTriple>>>,
>;
fn into_iter(self) -> Self::IntoIter {
self.triples
.iter()
.chain(self.named_graphs.values().flatten())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_star_term_creation() {
let iri = StarTerm::iri("http://example.org/test").unwrap();
assert!(iri.is_named_node());
assert!(!iri.is_literal());
let literal = StarTerm::literal("test value").unwrap();
assert!(literal.is_literal());
assert!(!literal.is_named_node());
let blank = StarTerm::blank_node("b1").unwrap();
assert!(blank.is_blank_node());
}
#[test]
fn test_quoted_triple_creation() {
let subject = StarTerm::iri("http://example.org/alice").unwrap();
let predicate = StarTerm::iri("http://example.org/age").unwrap();
let object = StarTerm::literal("25").unwrap();
let triple = StarTriple::new(subject, predicate, object);
assert!(triple.validate().is_ok());
let quoted = StarTerm::quoted_triple(triple);
assert!(quoted.is_quoted_triple());
assert_eq!(quoted.nesting_depth(), 1);
}
#[test]
fn test_nested_quoted_triples() {
let inner = StarTriple::new(
StarTerm::iri("http://example.org/alice").unwrap(),
StarTerm::iri("http://example.org/knows").unwrap(),
StarTerm::iri("http://example.org/bob").unwrap(),
);
let outer = StarTriple::new(
StarTerm::quoted_triple(inner),
StarTerm::iri("http://example.org/certainty").unwrap(),
StarTerm::literal("0.9").unwrap(),
);
assert_eq!(outer.nesting_depth(), 1);
assert!(outer.contains_quoted_triples());
}
#[test]
fn test_star_graph_operations() {
let mut graph = StarGraph::new();
assert!(graph.is_empty());
let triple = StarTriple::new(
StarTerm::iri("http://example.org/s").unwrap(),
StarTerm::iri("http://example.org/p").unwrap(),
StarTerm::iri("http://example.org/o").unwrap(),
);
graph.insert(triple.clone()).unwrap();
assert_eq!(graph.len(), 1);
assert!(graph.contains(&triple));
graph.remove(&triple);
assert!(graph.is_empty());
}
#[test]
fn test_validation() {
let valid = StarTriple::new(
StarTerm::iri("http://example.org/s").unwrap(),
StarTerm::iri("http://example.org/p").unwrap(),
StarTerm::literal("object").unwrap(),
);
assert!(valid.validate().is_ok());
let invalid = StarTriple::new(
StarTerm::iri("http://example.org/s").unwrap(),
StarTerm::literal("invalid_predicate").unwrap(),
StarTerm::literal("object").unwrap(),
);
assert!(invalid.validate().is_err());
}
#[test]
fn test_display_formatting() {
let triple = StarTriple::new(
StarTerm::iri("http://example.org/alice").unwrap(),
StarTerm::iri("http://example.org/age").unwrap(),
StarTerm::literal("25").unwrap(),
);
let display_str = format!("{triple}");
assert!(display_str.contains("<http://example.org/alice>"));
assert!(display_str.contains("\"25\""));
let quoted = StarTerm::quoted_triple(triple.clone());
let quoted_str = format!("{quoted}");
assert!(quoted_str.starts_with("<<"));
assert!(quoted_str.ends_with(">>"));
}
}