sbol 0.2.1

Rust implementation of the SBOL 3.1.0 specification.
Documentation
use crate::validation::resolver::*;

const PREFIXES: &str = r#"BASE <https://example.org/>
PREFIX : <https://example.org/>
PREFIX SBO: <https://identifiers.org/SBO:>
PREFIX sbol: <http://sbols.org/v3#>
"#;

fn document(body: &str) -> Document {
    Document::read_turtle(&format!("{PREFIXES}\n{body}")).unwrap()
}

#[test]
fn ownership_index_supports_parent_and_child_lookup() {
    let document = document(
        r#":component a sbol:Component;
    sbol:displayId "component";
    sbol:hasFeature <component/feature>;
    sbol:hasNamespace <https://example.org>;
    sbol:type SBO:0000251 .
<component/feature> a sbol:LocalSubComponent;
    sbol:displayId "feature";
    sbol:type SBO:0000251 .
"#,
    );
    let index = OwnershipIndex::new(&document);
    let component = Resource::iri("https://example.org/component");
    let feature = Resource::iri("https://example.org/component/feature");

    assert_eq!(
        index.single_parent(&feature, SBOL_HAS_FEATURE),
        Some(&component)
    );
    assert!(index.contains(&component, SBOL_HAS_FEATURE, &feature));
}

#[test]
fn ownership_index_reports_multiple_parents() {
    let document = document(
        r#":component_a a sbol:Component;
    sbol:displayId "component_a";
    sbol:hasFeature :shared_feature;
    sbol:hasNamespace <https://example.org>;
    sbol:type SBO:0000251 .
:component_b a sbol:Component;
    sbol:displayId "component_b";
    sbol:hasFeature :shared_feature;
    sbol:hasNamespace <https://example.org>;
    sbol:type SBO:0000251 .
:shared_feature a sbol:LocalSubComponent;
    sbol:displayId "shared_feature";
    sbol:type SBO:0000251 .
"#,
    );
    let index = OwnershipIndex::new(&document);
    let feature = Resource::iri("https://example.org/shared_feature");
    let parents = index.parents(&feature, SBOL_HAS_FEATURE);

    assert_eq!(parents.len(), 2);
    assert!(parents.contains(&&Resource::iri("https://example.org/component_a")));
    assert!(parents.contains(&&Resource::iri("https://example.org/component_b")));
}

#[test]
fn component_reference_resolver_walks_nested_references() {
    let document = document(
        r#"<component/reference> a sbol:ComponentReference;
    sbol:displayId "reference";
    sbol:refersTo <definition/reference> .
<definition/reference> a sbol:ComponentReference;
    sbol:displayId "reference";
    sbol:refersTo <leaf_component/feature> .
<leaf_component/feature> a sbol:SequenceFeature;
    sbol:displayId "feature" .
"#,
    );
    let index = OwnershipIndex::new(&document);
    let resolver = ComponentReferenceResolver::new(&document, &index);
    let feature = Resource::iri("https://example.org/component/reference");
    let trace = resolver.trace_feature(&feature).unwrap();

    assert_eq!(
        trace.target,
        Resource::iri("https://example.org/leaf_component/feature")
    );
    assert_eq!(trace.path.len(), 2);
}

#[test]
fn component_reference_resolver_reports_missing_targets() {
    let document = document(
        r#"<component/reference> a sbol:ComponentReference;
    sbol:displayId "reference";
    sbol:refersTo <missing/feature> .
"#,
    );
    let index = OwnershipIndex::new(&document);
    let resolver = ComponentReferenceResolver::new(&document, &index);
    let feature = Resource::iri("https://example.org/component/reference");

    assert!(matches!(
        resolver.trace_feature(&feature),
        Err(FeatureResolveError::MissingObject(resource))
            if resource == Resource::iri("https://example.org/missing/feature")
    ));
}

#[test]
fn component_reference_resolver_reports_cycles() {
    let document = document(
        r#"<component/a> a sbol:ComponentReference;
    sbol:displayId "a";
    sbol:refersTo <component/b> .
<component/b> a sbol:ComponentReference;
    sbol:displayId "b";
    sbol:refersTo <component/a> .
"#,
    );
    let index = OwnershipIndex::new(&document);
    let resolver = ComponentReferenceResolver::new(&document, &index);
    let feature = Resource::iri("https://example.org/component/a");

    assert!(matches!(
        resolver.trace_feature(&feature),
        Err(FeatureResolveError::Cycle(_))
    ));
}

#[test]
fn location_resolver_normalizes_entire_sequence() {
    let document = document(
        r#":sequence a sbol:Sequence;
    sbol:displayId "sequence";
    sbol:elements "ATGC";
    sbol:hasNamespace <https://example.org> .
:component a sbol:Component;
    sbol:displayId "component";
    sbol:hasFeature <component/feature>;
    sbol:hasNamespace <https://example.org>;
    sbol:type SBO:0000251 .
<component/feature> a sbol:SequenceFeature;
    sbol:displayId "feature";
    sbol:hasLocation <component/feature/location> .
<component/feature/location> a sbol:EntireSequence;
    sbol:displayId "location";
    sbol:hasSequence :sequence .
"#,
    );
    let index = OwnershipIndex::new(&document);
    let resolver = LocationResolver::new(&document, &index);
    let feature = Resource::iri("https://example.org/component/feature");
    let locations = resolver.locations_for_feature(&feature);

    assert_eq!(locations.len(), 1);
    assert_eq!(locations[0].start, 0);
    assert_eq!(locations[0].end, 4);
}

#[test]
fn location_resolver_follows_component_references_to_target_locations() {
    let document = document(
        r#":sequence a sbol:Sequence;
    sbol:displayId "sequence";
    sbol:elements "ATGC";
    sbol:hasNamespace <https://example.org> .
<definition/feature> a sbol:SequenceFeature;
    sbol:displayId "feature";
    sbol:hasLocation <definition/feature/location> .
<definition/feature/location> a sbol:Range;
    sbol:displayId "location";
    sbol:end "3";
    sbol:hasSequence :sequence;
    sbol:start "2" .
<component/reference> a sbol:ComponentReference;
    sbol:displayId "reference";
    sbol:refersTo <definition/feature> .
"#,
    );
    let index = OwnershipIndex::new(&document);
    let resolver = LocationResolver::new(&document, &index);
    let reference = Resource::iri("https://example.org/component/reference");
    let locations = resolver.locations_for_feature(&reference);

    assert_eq!(locations.len(), 1);
    assert_eq!(locations[0].start, 1);
    assert_eq!(locations[0].end, 3);
}

#[test]
fn location_resolver_leaves_entire_sequence_without_length_unresolved() {
    let document = document(
        r#":sequence a sbol:Sequence;
    sbol:displayId "sequence";
    sbol:hasNamespace <https://example.org> .
<component/feature> a sbol:SequenceFeature;
    sbol:displayId "feature";
    sbol:hasLocation <component/feature/location> .
<component/feature/location> a sbol:EntireSequence;
    sbol:displayId "location";
    sbol:hasSequence :sequence .
"#,
    );
    let index = OwnershipIndex::new(&document);
    let resolver = LocationResolver::new(&document, &index);
    let feature = Resource::iri("https://example.org/component/feature");

    assert!(resolver.locations_for_feature(&feature).is_empty());
}

#[test]
fn constraint_engine_detects_direct_identity_contradictions() {
    let document = document(
        r#":definition_a a sbol:Component;
    sbol:displayId "definition_a";
    sbol:hasNamespace <https://example.org>;
    sbol:type SBO:0000251 .
:definition_b a sbol:Component;
    sbol:displayId "definition_b";
    sbol:hasNamespace <https://example.org>;
    sbol:type SBO:0000251 .
:component a sbol:Component;
    sbol:displayId "component";
    sbol:hasFeature <component/a>, <component/b>;
    sbol:hasNamespace <https://example.org>;
    sbol:type SBO:0000251 .
<component/a> a sbol:SubComponent;
    sbol:displayId "a";
    sbol:instanceOf :definition_a .
<component/b> a sbol:SubComponent;
    sbol:displayId "b";
    sbol:instanceOf :definition_b .
"#,
    );
    let index = OwnershipIndex::new(&document);
    let engine = ConstraintEngine::new(&document, &index);
    let subject = Resource::iri("https://example.org/component/a");
    let object = Resource::iri("https://example.org/component/b");

    assert!(matches!(
        engine.table8_relation(SBOL_VERIFY_IDENTICAL, &subject, &object),
        RelationOutcome::Contradicted { .. }
    ));
}

#[test]
fn constraint_engine_keeps_replaces_and_spatial_table9_relations_undecided() {
    let document = document(
        r#":component a sbol:Component;
    sbol:displayId "component";
    sbol:hasFeature <component/a>, <component/b>;
    sbol:hasNamespace <https://example.org>;
    sbol:type SBO:0000251 .
<component/a> a sbol:LocalSubComponent;
    sbol:displayId "a";
    sbol:type SBO:0000251 .
<component/b> a sbol:LocalSubComponent;
    sbol:displayId "b";
    sbol:type SBO:0000251 .
"#,
    );
    let index = OwnershipIndex::new(&document);
    let engine = ConstraintEngine::new(&document, &index);
    let subject = Resource::iri("https://example.org/component/a");
    let object = Resource::iri("https://example.org/component/b");

    assert_eq!(
        engine.table8_relation(SBOL_REPLACES, &subject, &object),
        RelationOutcome::Unknown
    );
    assert_eq!(
        engine.table8_relation(SBOL_COVERS, &subject, &object),
        RelationOutcome::Unsupported
    );
}