knowledge-base-extension-framework 0.1.0

Static extension contracts for the file-based knowledge base
Documentation
use knowledge_base_extension_framework::bindings::{BindingValue, ResolvedBindings};
use knowledge_base_extension_framework::contracts::{
    BindingDeclaration, BindingKey, BindingKind, BindingReference, ContractVersion, EntityTypeRequirement, ExtensionDependency, ExtensionId, ExtensionMetadata,
    KnowledgeBaseExtension, OntologyRequirements,
};
use knowledge_base_extension_framework::error::FrameworkError;
use knowledge_base_extension_framework::registry::ExtensionRegistry;
use knowledge_base_validation::KnowledgeBaseValidator;
use std::collections::BTreeMap;
use std::sync::Arc;

struct TestExtension(ExtensionMetadata);

impl KnowledgeBaseExtension for TestExtension {
    fn metadata(&self) -> &ExtensionMetadata {
        &self.0
    }

    fn validators(&self, _: &ResolvedBindings) -> Result<Vec<Arc<dyn KnowledgeBaseValidator>>, FrameworkError> {
        Ok(Vec::new())
    }
}

fn id(value: &str) -> ExtensionId {
    value.parse().unwrap()
}
fn key(value: &str) -> BindingKey {
    value.parse().unwrap()
}
fn reference(extension: &str, binding: &str) -> BindingReference {
    BindingReference::new(id(extension), key(binding))
}

fn extension(name: &str, dependencies: &[(&str, u32)], bindings: &[(&str, BindingKind)]) -> Arc<dyn KnowledgeBaseExtension> {
    Arc::new(TestExtension(ExtensionMetadata {
        id: id(name),
        contract: ContractVersion::new(1),
        dependencies: dependencies
            .iter()
            .map(|(dependency, version)| ExtensionDependency {
                id: id(dependency),
                contract: ContractVersion::new(*version),
            })
            .collect(),
        bindings: bindings.iter().map(|(binding, kind)| BindingDeclaration { key: key(binding), kind: *kind }).collect(),
        ontology_requirements: OntologyRequirements::default(),
    }))
}

#[test]
fn identifiers_and_references_are_canonical() {
    const STATIC_EXTENSION: ExtensionId = ExtensionId::from_static("wikidata");
    const STATIC_KEY: BindingKey = BindingKey::from_static("item_id_property");
    const STATIC_REFERENCE: BindingReference = BindingReference::from_static("wikidata", "item_id_property");

    for valid in ["wikidata", "public-transport", "v1"] {
        assert!(valid.parse::<ExtensionId>().is_ok());
    }
    for invalid in ["", "Wikidata", "public_transport", "public--transport", "1transport", "-transport", "transport-"] {
        assert!(invalid.parse::<ExtensionId>().is_err(), "{invalid} accepted");
    }
    for valid in ["item_id_property", "v1"] {
        assert!(valid.parse::<BindingKey>().is_ok());
    }
    for invalid in ["item-id", "Item_id", "1_item", "item__id"] {
        assert!(invalid.parse::<BindingKey>().is_err(), "{invalid} accepted");
    }
    assert_eq!("wikidata:item_id_property".parse::<BindingReference>().unwrap().to_string(), "wikidata:item_id_property");
    assert_eq!(STATIC_EXTENSION, "wikidata".parse().unwrap());
    assert_eq!(STATIC_KEY, "item_id_property".parse().unwrap());
    assert_eq!(STATIC_REFERENCE, "wikidata:item_id_property".parse().unwrap());
}

#[test]
fn registry_validates_metadata_and_dependency_graphs() {
    let duplicate = ExtensionRegistry::new([extension("wikidata", &[], &[]), extension("wikidata", &[], &[])])
        .err()
        .expect("duplicate registration must fail");
    assert_eq!(duplicate, FrameworkError::DuplicateExtension(id("wikidata")));

    let missing = ExtensionRegistry::new([extension("public-transport", &[("wikidata", 1)], &[])])
        .err()
        .expect("missing dependency must fail");
    assert!(matches!(missing, FrameworkError::MissingDependency { .. }));
    let version = ExtensionRegistry::new([extension("public-transport", &[("wikidata", 2)], &[]), extension("wikidata", &[], &[])])
        .err()
        .expect("contract mismatch must fail");
    assert!(matches!(version, FrameworkError::UnsupportedContract { .. }));

    let cycle = ExtensionRegistry::new([extension("a", &[("b", 1)], &[]), extension("b", &[("a", 1)], &[])]).unwrap();
    assert!(matches!(cycle.resolve_active([id("a"), id("b")]), Err(FrameworkError::DependencyCycle(_))));
}

#[test]
fn registry_resolves_active_extensions_in_dependency_first_order() {
    let registry = ExtensionRegistry::new([
        extension("public-transport", &[("wikidata", 1)], &[]),
        extension("base-data", &[], &[]),
        extension("wikidata", &[("base-data", 1)], &[]),
    ])
    .unwrap();
    let active = registry.resolve_active([id("wikidata"), id("base-data"), id("public-transport")]).unwrap();
    assert_eq!(
        active.extensions().iter().map(|item| item.metadata().id.to_string()).collect::<Vec<_>>(),
        ["base-data", "wikidata", "public-transport"]
    );
    assert!(matches!(registry.resolve_active([id("public-transport")]), Err(FrameworkError::InactiveDependency { .. })));
}

#[test]
fn bindings_require_complete_matching_values_and_declared_access() {
    let registry = ExtensionRegistry::new([
        extension("wikidata", &[], &[("item_id_property", BindingKind::Property)]),
        extension("public-transport", &[("wikidata", 1)], &[("stop_place_type", BindingKind::EntityType)]),
        extension("other", &[], &[("other_type", BindingKind::EntityType)]),
    ])
    .unwrap();
    let active = registry.resolve_active([id("wikidata"), id("public-transport")]).unwrap();

    let missing = active
        .resolve_bindings(BTreeMap::from([(reference("wikidata", "item_id_property"), BindingValue::Property("P1".parse().unwrap()))]))
        .unwrap_err();
    assert!(matches!(missing, FrameworkError::MissingBinding(_)));
    let wrong_kind = active
        .resolve_bindings(BTreeMap::from([
            (reference("wikidata", "item_id_property"), BindingValue::EntityType("T1".parse().unwrap())),
            (reference("public-transport", "stop_place_type"), BindingValue::EntityType("T2".parse().unwrap())),
        ]))
        .unwrap_err();
    assert!(matches!(wrong_kind, FrameworkError::BindingKindMismatch { .. }));

    let bindings = active
        .resolve_bindings(BTreeMap::from([
            (reference("wikidata", "item_id_property"), BindingValue::Property("P1".parse().unwrap())),
            (reference("public-transport", "stop_place_type"), BindingValue::EntityType("T1".parse().unwrap())),
        ]))
        .unwrap();
    let transport = active.extensions().last().unwrap().metadata();
    assert_eq!(bindings.entity_type(transport, &reference("public-transport", "stop_place_type")).unwrap().as_str(), "T1");
    assert_eq!(bindings.property(transport, &reference("wikidata", "item_id_property")).unwrap().as_str(), "P1");
    assert!(matches!(
        bindings.get(transport, &reference("other", "other_type")),
        Err(FrameworkError::InaccessibleBinding { .. })
    ));
}

#[test]
fn requirements_must_reference_matching_declared_binding_kinds() {
    let extension: Arc<dyn KnowledgeBaseExtension> = Arc::new(TestExtension(ExtensionMetadata {
        id: id("example"),
        contract: ContractVersion::new(1),
        dependencies: vec![],
        bindings: vec![BindingDeclaration {
            key: key("property"),
            kind: BindingKind::Property,
        }],
        ontology_requirements: OntologyRequirements {
            entity_types: vec![EntityTypeRequirement {
                binding: reference("example", "property"),
            }],
            properties: vec![],
        },
    }));
    assert!(matches!(ExtensionRegistry::new([extension]), Err(FrameworkError::InvalidRequirement { .. })));
}