sbol 0.2.1

Rust implementation of the SBOL 3.1.0 specification.
Documentation
use std::collections::{BTreeMap, BTreeSet};

use crate::vocab::*;
use crate::{Document, Iri, Object, Resource, Term};

mod constraint;
mod derivation;
mod feature;
mod location;

pub(crate) use constraint::*;
pub(crate) use derivation::*;
pub(crate) use feature::*;
pub(crate) use location::*;

#[cfg(test)]
mod tests;

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct OwnershipEdge {
    pub parent: Resource,
    pub predicate: &'static str,
}

#[derive(Clone, Debug, Default)]
pub(crate) struct OwnershipIndex {
    parents_by_child: BTreeMap<Resource, Vec<OwnershipEdge>>,
    children_by_parent: BTreeMap<(Resource, &'static str), Vec<Resource>>,
}

impl OwnershipIndex {
    pub(crate) fn new(document: &Document) -> Self {
        let mut index = Self::default();
        for object in document.objects().values() {
            for predicate in COMPOSITE_PREDICATES {
                for child in object.resources(predicate) {
                    index
                        .parents_by_child
                        .entry(child.clone())
                        .or_default()
                        .push(OwnershipEdge {
                            parent: object.identity().clone(),
                            predicate,
                        });
                    index
                        .children_by_parent
                        .entry((object.identity().clone(), predicate))
                        .or_default()
                        .push(child.clone());
                }
            }
        }
        index
    }

    pub(crate) fn parents(&self, child: &Resource, predicate: &str) -> Vec<&Resource> {
        self.parents_by_child
            .get(child)
            .into_iter()
            .flatten()
            .filter(|edge| edge.predicate == predicate)
            .map(|edge| &edge.parent)
            .collect()
    }

    pub(crate) fn single_parent(&self, child: &Resource, predicate: &str) -> Option<&Resource> {
        self.parents(child, predicate).into_iter().next()
    }

    pub(crate) fn children(&self, parent: &Resource, predicate: &str) -> Vec<&Resource> {
        self.children_by_parent
            .get(&(parent.clone(), owned_predicate(predicate)))
            .into_iter()
            .flatten()
            .collect()
    }

    pub(crate) fn contains(&self, parent: &Resource, predicate: &str, child: &Resource) -> bool {
        self.children(parent, predicate)
            .into_iter()
            .any(|candidate| candidate == child)
    }
}

pub(crate) fn directional_orientation_value(value: Option<&Iri>) -> Option<DirectionalOrientation> {
    match value.map(Iri::as_str)? {
        SBOL_INLINE | SO_INLINE => Some(DirectionalOrientation::Inline),
        SBOL_REVERSE_COMPLEMENT | SO_REVERSE_COMPLEMENT => {
            Some(DirectionalOrientation::ReverseComplement)
        }
        _ => None,
    }
}

pub(crate) fn parent_by_identity_prefix<'a>(
    document: &'a Document,
    object: &Object,
) -> Option<&'a Object> {
    let identity = object.identity().to_string();
    let parent_identity = identity.rsplit_once('/')?.0;
    document.get(&Resource::iri(parent_identity))
}

fn sequential_relation_satisfied(
    restriction: &str,
    subject: &LinearLocation,
    object: &LinearLocation,
) -> bool {
    match restriction {
        SBOL_PRECEDES => subject.start < object.start,
        SBOL_STRICTLY_PRECEDES => subject.end < object.start,
        SBOL_MEETS => subject.end == object.start,
        SBOL_OVERLAPS => {
            subject.start < object.start && subject.end > object.start && subject.end < object.end
        }
        SBOL_CONTAINS => subject.start <= object.start && subject.end >= object.end,
        SBOL_STRICTLY_CONTAINS => subject.start < object.start && subject.end > object.end,
        SBOL_EQUALS => subject.start == object.start && subject.end == object.end,
        SBOL_FINISHES => subject.start > object.start && subject.end == object.end,
        SBOL_STARTS => subject.start == object.start && subject.end < object.end,
        _ => true,
    }
}

fn integer_value(object: &Object, predicate: &str) -> Option<i64> {
    object
        .first_literal_value(predicate)
        .and_then(|value| value.parse::<i64>().ok())
}

fn iri_values(object: &Object, predicate: &str) -> BTreeSet<Iri> {
    object.iris(predicate).cloned().collect()
}

fn term_values(object: &Object, predicate: &str) -> BTreeSet<Term> {
    object.values(predicate).iter().cloned().collect()
}

fn owned_predicate(predicate: &str) -> &'static str {
    COMPOSITE_PREDICATES
        .iter()
        .copied()
        .find(|candidate| *candidate == predicate)
        .unwrap_or(SBOL_HAS_FEATURE)
}

const COMPOSITE_PREDICATES: &[&str] = &[
    SBOL_HAS_FEATURE,
    SBOL_HAS_CONSTRAINT,
    SBOL_HAS_INTERACTION,
    SBOL_HAS_INTERFACE,
    SBOL_HAS_LOCATION,
    SBOL_SOURCE_LOCATION,
    SBOL_HAS_PARTICIPATION,
    SBOL_HAS_VARIABLE_FEATURE,
    PROV_QUALIFIED_USAGE,
    PROV_QUALIFIED_ASSOCIATION,
];

const FEATURE_EQUIVALENCE_PREDICATES: &[&str] = &[
    RDF_TYPE,
    SBOL_ROLE,
    SBOL_ORIENTATION,
    SBOL_TYPE,
    SBOL_INSTANCE_OF,
    SBOL_DEFINITION,
    SBOL_ROLE_INTEGRATION,
];