use crate::consts::*;
use crate::util::{read_file, read_url};
use anyhow::Result;
use chrono::prelude::*;
use log::{debug, info, warn};
use oxigraph::model::{
Graph as OxigraphGraph, GraphName, NamedNode, NamedNodeRef, Subject, SubjectRef, TermRef,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::{serde_as, DeserializeAs, SerializeAs};
use std::collections::HashMap;
use std::path::PathBuf;
fn namednode_ser<S>(namednode: &NamedNode, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(namednode.as_str())
}
fn namednode_de<'de, D>(deserializer: D) -> Result<NamedNode, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
NamedNode::new(s).map_err(serde::de::Error::custom)
}
#[derive(Serialize, Deserialize, Eq, Debug, Clone, Hash)]
pub struct GraphIdentifier {
location: OntologyLocation,
#[serde(serialize_with = "namednode_ser", deserialize_with = "namednode_de")]
name: NamedNode,
}
impl PartialEq for GraphIdentifier {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.location == other.location
}
}
impl GraphIdentifier {
pub fn location(&self) -> &OntologyLocation {
&self.location
}
pub fn name(&self) -> NamedNodeRef {
self.name.as_ref()
}
pub fn to_filename(&self) -> String {
let name = self.name.as_str().replace(':', "+");
let location = self.location.as_str().replace("file://", "");
format!("{}-{}", name, location).replace('/', "_")
}
pub fn graphname(&self) -> Result<GraphName> {
let name = self.name.as_str().replace(':', "+");
let location = self.location.as_str().replace("file://", "");
Ok(GraphName::NamedNode(NamedNode::new(format!(
"urn://{}-{}",
name, location
))?))
}
}
#[derive(Serialize, Deserialize, Hash, Clone, Eq, PartialEq, Debug)]
pub enum OntologyLocation {
File(PathBuf),
Url(String),
}
impl std::fmt::Display for OntologyLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OntologyLocation::File(p) => write!(f, "file://{}", p.to_str().unwrap_or_default()),
OntologyLocation::Url(u) => write!(f, "{}", u),
}
}
}
impl Default for OntologyLocation {
fn default() -> Self {
OntologyLocation::File(PathBuf::new())
}
}
impl OntologyLocation {
pub fn as_str(&self) -> &str {
match self {
OntologyLocation::File(p) => p.to_str().unwrap_or_default(),
OntologyLocation::Url(u) => u.as_str(),
}
}
pub fn graph(&self) -> Result<OxigraphGraph> {
match self {
OntologyLocation::File(p) => read_file(p),
OntologyLocation::Url(u) => read_url(u),
}
}
pub fn is_file(&self) -> bool {
match self {
OntologyLocation::File(_) => true,
OntologyLocation::Url(_) => false,
}
}
pub fn is_url(&self) -> bool {
match self {
OntologyLocation::File(_) => false,
OntologyLocation::Url(_) => true,
}
}
pub fn from_str(s: &str) -> Result<Self> {
if s.starts_with("http") {
Ok(OntologyLocation::Url(s.to_string()))
} else {
let s = s.trim_start_matches("file://");
let mut p = PathBuf::from(s);
if !p.is_absolute() {
p = std::env::current_dir()?.join(p);
}
Ok(OntologyLocation::File(p))
}
}
pub fn to_iri(&self) -> NamedNode {
match self {
OntologyLocation::File(p) => {
let p = p.to_str().unwrap_or_default();
NamedNode::new(format!("file://{}", p)).unwrap()
}
OntologyLocation::Url(u) => NamedNode::new(u.clone()).unwrap(),
}
}
pub fn as_path(&self) -> Option<&PathBuf> {
match self {
OntologyLocation::File(p) => Some(p),
OntologyLocation::Url(_) => None,
}
}
}
struct LocalType;
impl SerializeAs<NamedNode> for LocalType {
fn serialize_as<S>(value: &NamedNode, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
namednode_ser(value, serializer)
}
}
impl<'de> DeserializeAs<'de, NamedNode> for LocalType {
fn deserialize_as<D>(deserializer: D) -> Result<NamedNode, D::Error>
where
D: Deserializer<'de>,
{
namednode_de(deserializer)
}
}
#[serde_as]
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
pub struct Ontology {
id: GraphIdentifier,
#[serde(serialize_with = "namednode_ser", deserialize_with = "namednode_de")]
name: NamedNode,
#[serde_as(as = "Vec<LocalType>")]
pub imports: Vec<NamedNode>,
location: Option<OntologyLocation>,
pub last_updated: Option<DateTime<Utc>>,
#[serde_as(as = "HashMap<LocalType, _>")]
version_properties: HashMap<NamedNode, String>,
}
impl std::fmt::Display for Ontology {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Ontology: {}\nLocation: {}\nVersion Properties:\n",
self.name,
self.id.location.as_str()
)?;
for (k, v) in self.version_properties.iter() {
writeln!(f, " {}: {}", k, v)?;
}
Ok(())
}
}
impl Default for Ontology {
fn default() -> Self {
Ontology {
id: GraphIdentifier {
location: OntologyLocation::File(PathBuf::new()),
name: NamedNode::new("<n/a>").unwrap(),
},
name: NamedNode::new("<n/a>").unwrap(),
imports: vec![],
location: None,
last_updated: None,
version_properties: HashMap::new(),
}
}
}
impl Ontology {
pub fn with_location(&mut self, location: OntologyLocation) {
self.location = Some(location);
}
pub fn with_last_updated(&mut self, last_updated: DateTime<Utc>) {
self.last_updated = Some(last_updated);
}
pub fn id(&self) -> &GraphIdentifier {
&self.id
}
pub fn version_properties(&self) -> &HashMap<NamedNode, String> {
&self.version_properties
}
pub fn location(&self) -> Option<&OntologyLocation> {
self.location.as_ref()
}
pub fn graph(&self) -> Result<OxigraphGraph> {
if let Some(location) = &self.location {
return location.graph();
}
return OntologyLocation::from_str(self.name.as_str()).and_then(|loc| loc.graph());
}
pub fn name(&self) -> NamedNode {
self.name.clone()
}
pub fn dump(&self) -> String {
serde_json::to_string_pretty(self).unwrap()
}
pub fn from_graph(
graph: &OxigraphGraph,
location: OntologyLocation,
require_ontology_names: bool,
) -> Result<Self> {
let decls: Vec<SubjectRef> = graph
.subjects_for_predicate_object(TYPE, ONTOLOGY)
.collect::<Vec<_>>();
let ontology_name: Subject = match decls.first() {
Some(decl) => match decl {
SubjectRef::NamedNode(s) => Subject::NamedNode((*s).into()),
_ => return Err(anyhow::anyhow!("Ontology name is not an IRI")),
},
None => {
if require_ontology_names {
return Err(anyhow::anyhow!(
"No ontology declaration found in {}",
location
));
}
warn!(
"No ontology declaration found in {}. Using this as the ontology name",
location
);
Subject::NamedNode(location.to_iri())
}
};
debug!("got ontology name: {}", ontology_name);
let imports: Vec<TermRef> = graph
.objects_for_subject_predicate(ontology_name.as_ref(), IMPORTS)
.collect::<Vec<_>>();
let mut version_properties: HashMap<NamedNode, String> =
ONTOLOGY_VERSION_IRIS
.iter()
.fold(HashMap::new(), |mut acc, &iri| {
if let Some(o) = graph.object_for_subject_predicate(ontology_name.as_ref(), iri)
{
match o {
TermRef::NamedNode(s) => {
acc.insert(iri.into(), s.to_string());
}
TermRef::Literal(lit) => {
acc.insert(iri.into(), lit.to_string());
}
_ => (),
}
}
acc
});
let graph_metadata: Vec<TermRef> = graph
.objects_for_subject_predicate(ontology_name.as_ref(), HAS_GRAPH_METADATA)
.collect::<Vec<_>>();
for value in graph_metadata {
let graph_iri = match value {
TermRef::NamedNode(s) => s,
_ => continue,
};
for iri in ONTOLOGY_VERSION_IRIS.iter() {
if let Some(value) = graph.object_for_subject_predicate(graph_iri, *iri) {
match value {
TermRef::NamedNode(s) => {
version_properties.insert((*iri).into(), s.to_string());
}
TermRef::Literal(lit) => {
version_properties.insert((*iri).into(), lit.to_string());
}
_ => (),
}
}
}
}
for (k, v) in version_properties.iter() {
debug!("{}: {}", k, v);
}
info!(
"Fetched graph {} from location: {:?}",
ontology_name, location
);
let ontology_name: NamedNode = match ontology_name {
Subject::NamedNode(s) => s,
_ => panic!("Ontology name is not an IRI"),
};
let imports: Vec<NamedNode> = imports
.iter()
.map(|t| match t {
TermRef::NamedNode(s) => Ok(NamedNode::new(s.as_str())?),
_ => panic!("Import is not an IRI"),
})
.collect::<Result<Vec<NamedNode>>>()?;
Ok(Ontology {
id: GraphIdentifier {
location: location.clone(),
name: ontology_name.clone(),
},
name: ontology_name,
imports,
location: Some(location),
version_properties,
last_updated: None,
})
}
pub fn from_str(s: &str) -> Result<Self> {
Ok(serde_json::from_str(s)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxigraph::model::NamedNode;
#[test]
fn test_ontology_location() {
let url = "http://example.com/ontology.ttl";
let file = "/tmp/ontology.ttl";
let url_location = OntologyLocation::from_str(url).unwrap();
let file_location = OntologyLocation::from_str(file).unwrap();
assert!(url_location.is_url());
assert!(!url_location.is_file());
assert!(!file_location.is_url());
assert!(file_location.is_file());
}
#[test]
fn test_ontology_location_display() {
let url = "http://example.com/ontology.ttl";
let file = "/tmp/ontology.ttl";
let url_location = OntologyLocation::from_str(url).unwrap();
let file_location = OntologyLocation::from_str(file).unwrap();
assert_eq!(url_location.to_string(), url);
assert_eq!(file_location.to_string(), format!("file://{}", file));
}
#[test]
fn test_ontology_location_to_iri() {
let url = "http://example.com/ontology.ttl";
let file = "/tmp/ontology.ttl";
let url_location = OntologyLocation::from_str(url).unwrap();
let file_location = OntologyLocation::from_str(file).unwrap();
assert_eq!(url_location.to_iri(), NamedNode::new(url).unwrap());
assert_eq!(
file_location.to_iri(),
NamedNode::new(format!("file://{}", file)).unwrap()
);
}
}