use crate::store::{
config::AdapterConfig,
error::{Error as StoreError, Result as StoreResult},
};
#[cfg(feature = "ontology")]
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CommunityAlgorithm {
#[default]
ConnectedComponents,
Leiden,
}
#[derive(Debug, Clone)]
pub struct GraphConfig {
pub temporal_enabled: bool,
pub default_valid_from_now: bool,
pub changelog_enabled: bool,
pub current_state_reads: bool,
pub default_community_algorithm: CommunityAlgorithm,
pub max_causal_depth: usize,
pub max_causal_paths: usize,
#[cfg(feature = "ontology")]
pub ontology: Option<Arc<crate::ontology::Definition>>,
#[cfg(feature = "ontology")]
pub ontology_strict: bool,
}
impl Default for GraphConfig {
fn default() -> Self {
Self {
temporal_enabled: true,
default_valid_from_now: false,
changelog_enabled: true,
current_state_reads: false,
default_community_algorithm: CommunityAlgorithm::ConnectedComponents,
max_causal_depth: 7,
max_causal_paths: 20,
#[cfg(feature = "ontology")]
ontology: None,
#[cfg(feature = "ontology")]
ontology_strict: false,
}
}
}
impl GraphConfig {
pub fn temporal() -> Self {
Self::default()
}
pub fn without_temporal() -> Self {
Self {
temporal_enabled: false,
..Default::default()
}
}
#[cfg(feature = "ontology")]
pub fn validate_entity_type<'a>(
&self,
entity_type: &'a str,
) -> crate::graph::error::Result<std::borrow::Cow<'a, str>> {
let Some(ontology) = &self.ontology else {
return Ok(std::borrow::Cow::Borrowed(entity_type));
};
let validator = crate::ontology::OntologyValidator::new(ontology);
if let Some(canonical) = validator.canonical_entity_type(entity_type) {
return Ok(std::borrow::Cow::Owned(canonical.to_string()));
}
let msg = format!("unknown ontology entity type: {}", entity_type);
if self.ontology_strict {
Err(crate::graph::Error::ontology_violation(msg))
} else {
tracing::warn!("[fornix::graph] {}", msg);
Ok(std::borrow::Cow::Borrowed(entity_type))
}
}
#[cfg(feature = "ontology")]
pub fn validate_relation_type(
&self,
relation_type: &str,
source_entity_type: &str,
target_entity_type: &str,
) -> crate::graph::error::Result<()> {
let Some(ontology) = &self.ontology else {
return Ok(());
};
let validator = crate::ontology::OntologyValidator::new(ontology);
let result =
validator.validate_relation(relation_type, source_entity_type, target_entity_type, &Default::default());
if result.is_valid() {
return Ok(());
}
let msg = result.error_messages().join("; ");
if self.ontology_strict {
Err(crate::graph::Error::ontology_violation(msg))
} else {
tracing::warn!("[fornix::graph] {}", msg);
Ok(())
}
}
}
impl AdapterConfig for GraphConfig {
fn adapter_name(&self) -> &'static str {
"graph"
}
fn validate(&self) -> StoreResult<()> {
if self.max_causal_depth == 0 {
return Err(StoreError::config("max_causal_depth must be greater than zero"));
}
if self.max_causal_paths == 0 {
return Err(StoreError::config("max_causal_paths must be greater than zero"));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_temporal_enabled() {
assert!(GraphConfig::default().temporal_enabled);
}
#[test]
fn default_changelog_enabled() {
assert!(GraphConfig::default().changelog_enabled);
}
#[test]
fn default_current_state_reads_is_false() {
assert!(!GraphConfig::default().current_state_reads);
}
#[test]
fn default_valid_from_now_is_false() {
assert!(!GraphConfig::default().default_valid_from_now);
}
#[test]
fn default_max_causal_depth() {
assert_eq!(GraphConfig::default().max_causal_depth, 7);
}
#[test]
fn default_max_causal_paths() {
assert_eq!(GraphConfig::default().max_causal_paths, 20);
}
#[test]
fn adapter_name_is_graph() {
assert_eq!(GraphConfig::default().adapter_name(), "graph");
}
#[test]
fn validate_passes_for_defaults() {
assert!(GraphConfig::default().validate().is_ok());
}
#[test]
fn validate_fails_for_zero_causal_depth() {
let c = GraphConfig { max_causal_depth: 0, ..Default::default() };
assert!(c.validate().is_err());
}
#[test]
fn validate_fails_for_zero_causal_paths() {
let c = GraphConfig { max_causal_paths: 0, ..Default::default() };
assert!(c.validate().is_err());
}
#[test]
fn without_temporal_disables_temporal() {
assert!(!GraphConfig::without_temporal().temporal_enabled);
}
#[test]
fn community_algorithm_default_is_connected_components() {
assert_eq!(
GraphConfig::default().default_community_algorithm,
CommunityAlgorithm::ConnectedComponents
);
}
#[cfg(feature = "ontology")]
#[test]
fn ontology_default_is_none() {
assert!(GraphConfig::default().ontology.is_none());
}
#[cfg(feature = "ontology")]
#[test]
fn ontology_strict_default_is_false() {
assert!(!GraphConfig::default().ontology_strict);
}
#[cfg(feature = "ontology")]
#[test]
fn validate_entity_type_no_ontology_passes_through() {
let config = GraphConfig::default(); let result = config.validate_entity_type("Anything").unwrap();
assert_eq!(result.as_ref(), "Anything");
}
#[cfg(feature = "ontology")]
#[test]
fn validate_entity_type_known_type_returns_canonical() {
use crate::ontology::types::{Definition, EntityTypeDefinition};
use std::sync::Arc;
let mut def = Definition::new("test");
def.version = Some("1.0".to_string());
def.entity_types.push(EntityTypeDefinition {
name: "Regulation".to_string(),
description: None,
extraction_strategy: None,
extraction_patterns: Vec::new(),
aliases: vec!["Provision".to_string()],
properties: Vec::new(),
});
let config = GraphConfig {
ontology: Some(Arc::new(def)),
ontology_strict: false,
..Default::default()
};
let canonical = config.validate_entity_type("Provision").unwrap();
assert_eq!(canonical.as_ref(), "Regulation");
}
#[cfg(feature = "ontology")]
#[test]
fn validate_entity_type_unknown_strict_raises() {
use crate::ontology::types::Definition;
use crate::graph::Error;
use std::sync::Arc;
let mut def = Definition::new("test");
def.version = Some("1.0".to_string());
let config = GraphConfig {
ontology: Some(Arc::new(def)),
ontology_strict: true,
..Default::default()
};
let err = config.validate_entity_type("Unknown").unwrap_err();
assert!(matches!(err, Error::OntologyViolation(_)));
}
#[cfg(feature = "ontology")]
#[test]
fn validate_entity_type_unknown_soft_passes_through() {
use crate::ontology::types::Definition;
use std::sync::Arc;
let mut def = Definition::new("test");
def.version = Some("1.0".to_string());
let config = GraphConfig {
ontology: Some(Arc::new(def)),
ontology_strict: false,
..Default::default()
};
let result = config.validate_entity_type("Unknown").unwrap();
assert_eq!(result.as_ref(), "Unknown");
}
}