mod discovery;
mod resolver;
pub use discovery::*;
pub use resolver::*;
use crate::ast::{self, OntologyFile as ParsedOntologyFile, PackageFile, QualifiedName};
use crate::comment::CommentMap;
use crate::error::ParseError;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PackageError {
#[error("Package manifest not found: {0}")]
ManifestNotFound(PathBuf),
#[error("Failed to read file '{path}': {source}")]
IoError {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Parse error in '{path}': {source}")]
ParseError {
path: PathBuf,
#[source]
source: Box<ParseError>,
},
#[error("Invalid package structure: {0}")]
InvalidStructure(String),
#[error("Unresolved prefix '{prefix}' in file '{path}'")]
UnresolvedPrefix {
prefix: String,
path: PathBuf,
},
#[error("Circular dependency detected: {0}")]
CircularDependency(String),
#[error("Duplicate definition '{name}' in '{path1}' and '{path2}'")]
DuplicateDefinition {
name: String,
path1: PathBuf,
path2: PathBuf,
},
}
#[derive(Debug, Clone)]
pub struct Package {
pub manifest: PackageFile,
pub root: PathBuf,
pub ontologies: HashMap<QualifiedName, OntologyFile>,
}
impl Package {
pub fn namespace(&self) -> &QualifiedName {
&self.manifest.name
}
pub fn dolfin_version(&self) -> &str {
&self.manifest.dolfin_version
}
pub fn version(&self) -> &str {
&self.manifest.version
}
pub fn iter_ontologies(&self) -> impl Iterator<Item = (&QualifiedName, &OntologyFile)> {
self.ontologies.iter()
}
pub fn get_ontology(&self, namespace: &QualifiedName) -> Option<&OntologyFile> {
self.ontologies.get(namespace)
}
pub fn all_concepts(&self) -> Vec<(&QualifiedName, &ast::ConceptDef)> {
self.ontologies
.iter()
.flat_map(|(ns, onto)| onto.ast.concepts_as_ref().into_iter().map(move |c| (ns, c)))
.collect()
}
pub fn all_properties(&self) -> Vec<(&QualifiedName, &ast::PropertyDef)> {
self.ontologies
.iter()
.flat_map(|(ns, onto)| {
onto.ast
.properties_as_ref()
.into_iter()
.map(move |p| (ns, p))
})
.collect()
}
pub fn all_enums(&self) -> Vec<(&QualifiedName, &ast::ConceptDef)> {
self.ontologies
.iter()
.flat_map(|(ns, onto)| {
onto.ast.concepts_as_ref().into_iter().filter_map(move |c| {
if let Some(_) = c.one_of {
Some((ns, c))
} else {
None
}
})
})
.collect()
}
pub fn all_rules(&self) -> Vec<(&QualifiedName, &ast::RuleDef)> {
self.ontologies
.iter()
.flat_map(|(ns, onto)| onto.ast.rules_as_ref().into_iter().map(move |r| (ns, r)))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct OntologyFile {
pub relative_path: PathBuf,
pub absolute_path: PathBuf,
pub namespace: QualifiedName,
pub iri_name: Option<String>,
pub resolved_prefixes: HashMap<String, QualifiedName>,
pub ast: ParsedOntologyFile,
pub comment_map: CommentMap,
}
impl OntologyFile {
pub fn iri_segment(&self) -> &str {
self.iri_name.as_deref().unwrap_or_else(|| {
self.namespace
.parts
.last()
.map(|s| s.as_str())
.unwrap_or("")
})
}
}
pub fn load_package<P: AsRef<Path>>(path: P) -> Result<Package, Box<PackageError>> {
let root = path.as_ref().to_path_buf();
let manifest_path = root.join("package.dlf");
if !manifest_path.exists() {
return Err(Box::new(PackageError::ManifestNotFound(manifest_path)));
}
let manifest_source =
std::fs::read_to_string(&manifest_path).map_err(|e| PackageError::IoError {
path: manifest_path.clone(),
source: e,
})?;
let manifest =
crate::parser::parse_package(&manifest_source).map_err(|e| PackageError::ParseError {
path: manifest_path,
source: e,
})?;
let discovered_files = discovery::discover_ontology_files(&root)?;
let mut parsed_files = Vec::new();
for file_info in discovered_files {
let source = std::fs::read_to_string(&file_info.absolute_path).map_err(|e| {
PackageError::IoError {
path: file_info.absolute_path.clone(),
source: e,
}
})?;
let parsed = crate::parser::parse_ontology_with_comments(&source);
let ast = parsed.result.map_err(|e| PackageError::ParseError {
path: file_info.absolute_path.clone(),
source: Box::new(e),
})?;
let comment_map = CommentMap::build(&ast, parsed.comments);
parsed_files.push((file_info, ast, comment_map));
}
let ontologies = resolver::resolve_package(&manifest, parsed_files)?;
Ok(Package {
manifest,
root,
ontologies,
})
}
pub fn load_package_from_memory(
files: &HashMap<String, String>,
) -> Result<Package, Box<PackageError>> {
let manifest_source = files
.get("package.dlf")
.ok_or_else(|| Box::new(PackageError::ManifestNotFound(PathBuf::from("package.dlf"))))?;
let manifest =
crate::parser::parse_package(manifest_source).map_err(|e| PackageError::ParseError {
path: PathBuf::from("package.dlf"),
source: e,
})?;
let mut parsed_files = Vec::new();
for (path_str, content) in files {
if path_str == "package.dlf" || !path_str.ends_with(".dlf") {
continue;
}
let relative_path: PathBuf = path_str
.split('/')
.filter(|s| !s.is_empty() && *s != "." && *s != "..")
.collect();
let derived_namespace = discovery::path_to_namespace(&relative_path).map_err(Box::new)?;
let file_info = discovery::DiscoveredFile {
absolute_path: relative_path.clone(),
relative_path,
derived_namespace,
};
let parsed = crate::parser::parse_ontology_with_comments(content);
let ast = parsed.result.map_err(|e| PackageError::ParseError {
path: PathBuf::from(path_str),
source: Box::new(e),
})?;
let comment_map = CommentMap::build(&ast, parsed.comments);
parsed_files.push((file_info, ast, comment_map));
}
let ontologies = resolver::resolve_package(&manifest, parsed_files)?;
Ok(Package {
manifest,
root: PathBuf::from("."),
ontologies,
})
}
pub fn check_package<P: AsRef<Path>>(path: P) -> Result<Vec<String>, Box<PackageError>> {
let package = load_package(path)?;
let mut warnings = Vec::new();
for (ns, onto) in &package.ontologies {
if onto.ast.declarations.is_empty() {
warnings.push(format!(
"Ontology '{}' ({}) has no declarations",
ns.full(),
onto.relative_path.display()
));
}
}
Ok(warnings)
}