use std::collections::HashMap;
use std::fs;
use std::path::Path;
use serde::Deserialize;
pub use mif_ontology::EntityType;
use crate::error::MifRhError;
#[derive(Debug, Clone, Deserialize)]
pub struct DiscoveryPattern {
#[serde(default)]
pub content_pattern: Option<String>,
#[serde(default)]
pub file_pattern: Option<String>,
#[serde(default)]
pub suggest_entity: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct DiscoveryConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub patterns: Vec<DiscoveryPattern>,
}
#[derive(Debug, Deserialize)]
struct OntologyBlock {
id: String,
version: String,
#[serde(default)]
extends: Vec<String>,
#[serde(default)]
description: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RelationshipDecl {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub from: Vec<String>,
#[serde(default)]
pub to: Vec<String>,
#[serde(default)]
pub symmetric: bool,
}
#[derive(Debug, Deserialize)]
struct OntologyPackFile {
ontology: OntologyBlock,
#[serde(default)]
entity_types: Vec<EntityType>,
#[serde(default)]
relationships: HashMap<String, RelationshipDecl>,
#[serde(default)]
discovery: DiscoveryConfig,
}
#[derive(Debug, Clone)]
pub struct OntologyPack {
pub id: String,
pub version: String,
pub extends: Vec<String>,
pub description: Option<String>,
pub entity_types: Vec<EntityType>,
pub relationships: HashMap<String, RelationshipDecl>,
pub discovery: DiscoveryConfig,
}
pub fn parse_pack(yaml: &str, path: &str) -> Result<OntologyPack, MifRhError> {
let file: OntologyPackFile =
serde_norway::from_str(yaml).map_err(|source| MifRhError::OntologyPackYaml {
path: path.to_string(),
source,
})?;
Ok(OntologyPack {
id: file.ontology.id,
version: file.ontology.version,
extends: file.ontology.extends,
description: file.ontology.description,
entity_types: file.entity_types,
relationships: file.relationships,
discovery: file.discovery,
})
}
pub fn load_packs_from_dir(dir: &Path) -> Result<HashMap<String, OntologyPack>, MifRhError> {
let entries = fs::read_dir(dir).map_err(|source| MifRhError::Io {
path: dir.display().to_string(),
source,
})?;
let mut packs = HashMap::new();
for entry in entries {
let entry = entry.map_err(|source| MifRhError::Io {
path: dir.display().to_string(),
source,
})?;
let path = entry.path();
let is_yaml = path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext == "yaml" || ext == "yml");
if !is_yaml {
continue;
}
let path_display = path.display().to_string();
let contents = fs::read_to_string(&path).map_err(|source| MifRhError::Io {
path: path_display.clone(),
source,
})?;
let pack = parse_pack(&contents, &path_display)?;
packs.insert(pack.id.clone(), pack);
}
Ok(packs)
}
pub fn load_packs_via_catalog(
catalog: &crate::catalog::Catalog,
base_dir: &Path,
) -> Result<HashMap<String, OntologyPack>, MifRhError> {
let mut packs = HashMap::new();
for entry in &catalog.ontologies {
let Some(source) = &entry.source else {
continue;
};
let path = base_dir.join(source);
let path_display = path.display().to_string();
let contents = fs::read_to_string(&path).map_err(|source| MifRhError::Io {
path: path_display.clone(),
source,
})?;
let pack = parse_pack(&contents, &path_display)?;
packs.insert(pack.id.clone(), pack);
}
Ok(packs)
}
#[cfg(test)]
mod tests {
use std::fs;
use super::{load_packs_from_dir, load_packs_via_catalog, parse_pack};
use crate::catalog::{Catalog, CatalogEntry};
const EDU_FIXTURE_YAML: &str = "
ontology:
id: edu-fixture
version: \"0.1.0\"
extends: [mif-base]
entity_types:
- name: title
description: A published educational title
schema:
required: [name, isbn]
properties:
name: {type: string}
isbn: {type: string}
discovery:
enabled: true
patterns:
- content_pattern: \"\\\\b(ISBN|textbook)\\\\b\"
suggest_entity: title
";
#[test]
fn parses_entity_types_and_discovery() {
let pack = parse_pack(EDU_FIXTURE_YAML, "edu-fixture.yaml").unwrap();
assert_eq!(pack.id, "edu-fixture");
assert_eq!(pack.extends, ["mif-base"]);
assert_eq!(pack.entity_types.len(), 1);
assert_eq!(pack.entity_types[0].name, "title");
assert!(pack.discovery.enabled);
assert_eq!(pack.discovery.patterns.len(), 1);
assert_eq!(
pack.discovery.patterns[0].suggest_entity.as_deref(),
Some("title")
);
}
#[test]
fn parses_relationships_with_from_to_domains() {
let yaml = "
ontology:
id: engineering-base
version: \"0.1.0\"
relationships:
governs:
description: \"A control or policy governs a component or artifact\"
from: [control, policy]
to: [component, artifact]
symmetric: false
";
let pack = parse_pack(yaml, "engineering-base.yaml").unwrap();
let governs = pack.relationships.get("governs").unwrap();
assert_eq!(governs.from, ["control", "policy"]);
assert_eq!(governs.to, ["component", "artifact"]);
assert!(!governs.symmetric);
}
#[test]
fn defaults_to_no_relationships_when_absent() {
let pack = parse_pack(EDU_FIXTURE_YAML, "edu-fixture.yaml").unwrap();
assert!(pack.relationships.is_empty());
}
#[test]
fn load_packs_via_catalog_resolves_each_entries_source_path() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("packs")).unwrap();
fs::write(
dir.path().join("packs").join("edu-fixture.yaml"),
EDU_FIXTURE_YAML,
)
.unwrap();
let catalog = Catalog {
ontologies: vec![
CatalogEntry {
id: "edu-fixture".to_string(),
version: "0.1.0".to_string(),
source: Some("packs/edu-fixture.yaml".to_string()),
core: false,
},
CatalogEntry {
id: "no-source".to_string(),
version: "1.0.0".to_string(),
source: None,
core: false,
},
],
};
let packs = load_packs_via_catalog(&catalog, dir.path()).unwrap();
assert_eq!(packs.len(), 1);
assert!(packs.contains_key("edu-fixture"));
assert!(!packs.contains_key("no-source"));
}
#[test]
fn load_packs_from_dir_skips_non_yaml_and_keys_by_id() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("readme.txt"), b"not a pack").unwrap();
fs::write(dir.path().join("edu-fixture.yaml"), EDU_FIXTURE_YAML).unwrap();
let packs = load_packs_from_dir(dir.path()).unwrap();
assert_eq!(packs.len(), 1);
assert!(packs.contains_key("edu-fixture"));
}
#[test]
fn reports_malformed_yaml() {
let error = parse_pack("{", "broken.yaml").unwrap_err();
assert!(matches!(error, super::MifRhError::OntologyPackYaml { .. }));
}
}