use crate::catalog::{
CatalogError, CatalogStats, ClassHierarchy, EntityDetail, GraphBuilder, GraphFilters,
GraphKind, GraphPayload, GraphRequest, IndexBuilder, OntologyCatalog,
};
use crate::diff::DiffResult;
use crate::query::{query_catalog, sparql_catalog, QueryError, QueryResult, SparqlResult};
use ontocore_core::Diagnostic;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct WorkspaceOptions {
pub root: PathBuf,
pub scan_roots: Vec<PathBuf>,
pub disk_cache: bool,
}
impl WorkspaceOptions {
pub fn single(root: impl Into<PathBuf>) -> Self {
Self { root: root.into(), scan_roots: Vec::new(), disk_cache: false }
}
pub fn with_disk_cache(mut self, enabled: bool) -> Self {
self.disk_cache = enabled;
self
}
pub fn with_scan_roots(mut self, roots: Vec<PathBuf>) -> Self {
self.scan_roots = roots;
self
}
}
pub struct Workspace {
options: WorkspaceOptions,
catalog: OntologyCatalog,
class_hierarchy: ClassHierarchy,
}
impl Workspace {
pub fn open(path: impl AsRef<Path>) -> Result<Self, CatalogError> {
Self::open_with_options(WorkspaceOptions::single(path.as_ref().to_path_buf()))
}
pub fn open_with_options(options: WorkspaceOptions) -> Result<Self, CatalogError> {
let mut builder =
IndexBuilder::new().workspace(options.root.clone()).disk_cache(options.disk_cache);
if !options.scan_roots.is_empty() {
builder = builder.scan_roots(options.scan_roots.clone());
}
let catalog = builder.build()?;
let class_hierarchy = catalog.class_hierarchy();
Ok(Self { options, catalog, class_hierarchy })
}
pub fn reindex(&mut self) -> Result<CatalogStats, CatalogError> {
let catalog = self.build_fresh()?;
self.apply_catalog(catalog);
Ok(self.stats())
}
pub fn reindex_incremental(&mut self) -> Result<CatalogStats, CatalogError> {
let previous = &self.catalog;
let catalog = self.build_fresh_incremental(previous)?;
self.apply_catalog(catalog);
Ok(self.stats())
}
pub fn root(&self) -> &Path {
&self.options.root
}
pub fn scan_roots(&self) -> &[PathBuf] {
if self.options.scan_roots.is_empty() {
std::slice::from_ref(&self.options.root)
} else {
&self.options.scan_roots
}
}
pub fn catalog(&self) -> &OntologyCatalog {
&self.catalog
}
pub fn stats(&self) -> CatalogStats {
self.catalog.data().stats()
}
pub fn import_graph(&self) -> GraphPayload {
GraphBuilder::new(&self.catalog)
.build(&GraphRequest {
graph_kind: GraphKind::Import.as_str().to_string(),
root_iri: None,
depth: 2,
include_inferred: false,
filters: GraphFilters::default(),
})
.unwrap_or(GraphPayload {
nodes: vec![],
edges: vec![],
truncated: false,
graph_kind: GraphKind::Import.as_str().to_string(),
})
}
pub fn diagnostics(&self) -> Vec<Diagnostic> {
self.catalog.data().diagnostics.clone()
}
pub fn query(&self, sql: &str) -> Result<QueryResult, QueryError> {
query_catalog(&self.catalog, sql)
}
pub fn sparql(&self, sparql: &str) -> Result<SparqlResult, QueryError> {
sparql_catalog(&self.catalog, sparql)
}
pub fn diff(&self, other: &Workspace) -> DiffResult {
crate::diff::diff_catalogs(self.catalog(), other.catalog())
}
pub fn diff_against_path(&self, other: impl AsRef<Path>) -> Result<DiffResult, CatalogError> {
let other_ws = Workspace::open(other)?;
Ok(self.diff(&other_ws))
}
pub fn search(&self, term: &str) -> Vec<EntityDetail> {
let needle = term.to_ascii_lowercase();
if needle.is_empty() {
return Vec::new();
}
let mut matches: Vec<EntityDetail> = self
.catalog
.data()
.entities
.iter()
.filter(|entity| entity_matches_term(entity, &needle))
.filter_map(|entity| {
self.catalog.entity_detail_with_hierarchy(&entity.iri, &self.class_hierarchy)
})
.collect();
matches.sort_by(|a, b| a.entity.short_name.cmp(&b.entity.short_name));
matches.dedup_by(|a, b| a.entity.iri == b.entity.iri);
matches
}
fn build_fresh(&self) -> Result<OntologyCatalog, CatalogError> {
let mut builder = IndexBuilder::new()
.workspace(self.options.root.clone())
.disk_cache(self.options.disk_cache);
if !self.options.scan_roots.is_empty() {
builder = builder.scan_roots(self.options.scan_roots.clone());
}
builder.build()
}
fn build_fresh_incremental(
&self,
previous: &OntologyCatalog,
) -> Result<OntologyCatalog, CatalogError> {
let mut builder = IndexBuilder::new()
.workspace(self.options.root.clone())
.disk_cache(self.options.disk_cache);
if !self.options.scan_roots.is_empty() {
builder = builder.scan_roots(self.options.scan_roots.clone());
}
builder.build_incremental(previous)
}
fn apply_catalog(&mut self, catalog: OntologyCatalog) {
self.class_hierarchy = catalog.class_hierarchy();
self.catalog = catalog;
}
}
fn entity_matches_term(entity: &ontocore_core::Entity, needle: &str) -> bool {
if entity.iri.to_ascii_lowercase().contains(needle) {
return true;
}
if entity.short_name.to_ascii_lowercase().contains(needle) {
return true;
}
if entity.obo_id.as_ref().is_some_and(|id| id.to_ascii_lowercase().contains(needle)) {
return true;
}
entity.labels.iter().any(|label| label.to_ascii_lowercase().contains(needle))
}
#[cfg(test)]
mod tests {
use super::*;
fn fixtures_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../fixtures")
}
#[test]
fn workspace_open_fixtures() {
let ws = Workspace::open(fixtures_path()).expect("open fixtures");
assert!(ws.stats().class_count > 0);
}
#[test]
fn workspace_query_classes() {
let ws = Workspace::open(fixtures_path()).expect("open fixtures");
let result = ws.query("SELECT short_name FROM classes").expect("query");
assert!(!result.rows.is_empty());
}
#[test]
fn workspace_search_person() {
let ws = Workspace::open(fixtures_path()).expect("open fixtures");
let hits = ws.search("Person");
assert!(!hits.is_empty());
}
#[test]
fn workspace_import_graph_non_empty() {
let ws = Workspace::open(fixtures_path()).expect("open fixtures");
let graph = ws.import_graph();
assert!(!graph.nodes.is_empty());
}
}