fastobo 0.15.5

Faultless AST for Open Biomedical Ontologies.
Documentation
use std::str::FromStr;

use fastobo_derive_internal::FromStr;
use fastobo_derive_internal::OboClause;

use crate::ast::*;
use crate::error::SyntaxError;
use crate::parser::Cache;
use crate::parser::FromPair;
use crate::semantics::OboClause;
use crate::syntax::pest::iterators::Pair;
use crate::syntax::Rule;

/// A clause appearing in a header frame.
///
/// Header clauses are used to add metadata to OBO documents. They are all
/// optional, but every document should *at least* contain a `FormatVersion`
/// clause, to help with interoperability and to make sure the semantics of
/// the right OBO specification are in use.
///
/// # Comparison
/// `HeaderClause` implements `PartialOrd` following the semantics of the OBO
/// specification: clauses will compare based on their serialization order
/// rather than on their alphabetic order; clauses of the same kind will be
/// ranked in the alphabetic order.
#[derive(Clone, Debug, Eq, Hash, FromStr, OboClause, Ord, PartialEq, PartialOrd)]
pub enum HeaderClause {
    #[clause(tag = "format-version", cardinality = "ZeroOrOne")]
    FormatVersion(Box<UnquotedString>),
    #[clause(tag = "data-version", cardinality = "ZeroOrOne")]
    DataVersion(Box<UnquotedString>),
    #[clause(cardinality = "ZeroOrOne")]
    Date(Box<NaiveDateTime>),
    #[clause(tag = "saved-by", cardinality = "ZeroOrOne")]
    SavedBy(Box<UnquotedString>),
    #[clause(tag = "auto-generated-by", cardinality = "ZeroOrOne")]
    AutoGeneratedBy(Box<UnquotedString>),
    Import(Box<Import>),
    Subsetdef(Box<SubsetIdent>, Box<QuotedString>),
    #[clause(tag = "synonymtypedef")]
    SynonymTypedef(
        Box<SynonymTypeIdent>,
        Box<QuotedString>,
        Option<Box<SynonymScope>>,
    ),
    #[clause(tag = "default-namespace", cardinality = "ZeroOrOne")]
    DefaultNamespace(Box<NamespaceIdent>),
    #[clause(tag = "namespace-id-rule")]
    NamespaceIdRule(Box<UnquotedString>),
    Idspace(Box<IdentPrefix>, Box<Url>, Option<Box<QuotedString>>),
    #[clause(tag = "treat-xrefs-as-equivalent")]
    TreatXrefsAsEquivalent(Box<IdentPrefix>),
    #[clause(tag = "treat-xrefs-as-genus-differentia")]
    TreatXrefsAsGenusDifferentia(Box<IdentPrefix>, Box<RelationIdent>, Box<ClassIdent>),
    #[clause(tag = "treat-xrefs-as-reverse-genus-differentia")]
    TreatXrefsAsReverseGenusDifferentia(Box<IdentPrefix>, Box<RelationIdent>, Box<ClassIdent>),
    #[clause(tag = "treat-xrefs-as-relationship")]
    TreatXrefsAsRelationship(Box<IdentPrefix>, Box<RelationIdent>),
    #[clause(tag = "treat-xrefs-as-is_a")]
    TreatXrefsAsIsA(Box<IdentPrefix>),
    #[clause(tag = "treat-xrefs-as-has-subclass")]
    TreatXrefsAsHasSubclass(Box<IdentPrefix>),
    // FIXME(@althonos): Add support for hidden comment and qualifiers.
    PropertyValue(Box<PropertyValue>),
    Remark(Box<UnquotedString>),
    #[clause(cardinality = "ZeroOrOne")]
    Ontology(Box<UnquotedString>),
    #[clause(tag = "owl-axioms")]
    OwlAxioms(Box<UnquotedString>),
    #[clause(tag = 0, format = "{0}: {1}")]
    Unreserved(Box<UnquotedString>, Box<UnquotedString>),
}

impl<'i> FromPair<'i> for HeaderClause {
    const RULE: Rule = Rule::HeaderClause;
    unsafe fn from_pair_unchecked(
        pair: Pair<'i, Rule>,
        cache: &Cache,
    ) -> Result<Self, SyntaxError> {
        let mut inner = pair.into_inner();
        let tag = inner.next().unwrap();
        match tag.as_rule() {
            Rule::FormatVersionTag => {
                let version = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::FormatVersion(Box::new(version)))
            }
            Rule::DataVersionTag => {
                let version = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::DataVersion(Box::new(version)))
            }
            Rule::DateTag => {
                let date = NaiveDateTime::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::Date(Box::new(date)))
            }
            Rule::SavedByTag => {
                let person = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::SavedBy(Box::new(person)))
            }
            Rule::AutoGeneratedByTag => {
                let soft = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::AutoGeneratedBy(Box::new(soft)))
            }
            Rule::ImportTag => {
                let import = Import::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::Import(Box::new(import)))
            }
            Rule::SubsetdefTag => {
                let subset = SubsetIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
                let desc = QuotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::Subsetdef(Box::new(subset), Box::new(desc)))
            }
            Rule::SynonymTypedefTag => {
                let id = SynonymTypeIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
                let desc = QuotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                let scope = match inner.next() {
                    Some(pair) => Some(Box::new(SynonymScope::from_pair_unchecked(pair, cache)?)),
                    None => None,
                };
                Ok(HeaderClause::SynonymTypedef(
                    Box::new(id),
                    Box::new(desc),
                    scope,
                ))
            }
            Rule::DefaultNamespaceTag => {
                let id = NamespaceIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::DefaultNamespace(Box::new(id)))
            }
            Rule::NamespaceIdRuleTag => {
                let value = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::NamespaceIdRule(Box::new(value)))
            }
            Rule::IdspaceTag => {
                let prefix = IdentPrefix::from_pair_unchecked(inner.next().unwrap(), cache)?;
                let url = Url::from_pair_unchecked(inner.next().unwrap(), cache)?;
                let desc = match inner.next() {
                    Some(pair) => Some(Box::new(QuotedString::from_pair_unchecked(pair, cache)?)),
                    None => None,
                };
                Ok(HeaderClause::Idspace(Box::new(prefix), Box::new(url), desc))
            }
            Rule::TreatXrefsAsEquivalentTag => {
                let prefix = IdentPrefix::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::TreatXrefsAsEquivalent(Box::new(prefix)))
            }
            Rule::TreatXrefsAsGenusDifferentiaTag => {
                let prefix = IdentPrefix::from_pair_unchecked(inner.next().unwrap(), cache)?;
                let rel = RelationIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
                let cls = ClassIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::TreatXrefsAsGenusDifferentia(
                    Box::new(prefix),
                    Box::new(rel),
                    Box::new(cls),
                ))
            }
            Rule::TreatXrefsAsReverseGenusDifferentiaTag => {
                let prefix = IdentPrefix::from_pair_unchecked(inner.next().unwrap(), cache)?;
                let rel = RelationIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
                let cls = ClassIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::TreatXrefsAsReverseGenusDifferentia(
                    Box::new(prefix),
                    Box::new(rel),
                    Box::new(cls),
                ))
            }
            Rule::TreatXrefsAsRelationshipTag => {
                let prefix = IdentPrefix::from_pair_unchecked(inner.next().unwrap(), cache)?;
                let rel = RelationIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::TreatXrefsAsRelationship(
                    Box::new(prefix),
                    Box::new(rel),
                ))
            }
            Rule::TreatXrefsAsIsATag => {
                let prefix = IdentPrefix::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::TreatXrefsAsIsA(Box::new(prefix)))
            }
            Rule::TreatXrefsAsHasSubclassTag => {
                let prefix = IdentPrefix::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::TreatXrefsAsHasSubclass(Box::new(prefix)))
            }
            Rule::PropertyValueTag => {
                let pv = PropertyValue::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::PropertyValue(Box::new(pv)))
            }
            Rule::RemarkTag => {
                let remark = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::Remark(Box::new(remark)))
            }
            Rule::OntologyTag => {
                let ont = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::Ontology(Box::new(ont)))
            }
            Rule::OwlAxiomsTag => {
                let axioms = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::OwlAxioms(Box::new(axioms)))
            }
            Rule::Unreserved => {
                let tag = UnquotedString::from_str(tag.as_str())?;
                let value = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
                Ok(HeaderClause::Unreserved(Box::new(tag), Box::new(value)))
            }
            _ => unreachable!(),
        }
    }
}

#[cfg(test)]
mod tests {

    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn tag() {
        let clause = HeaderClause::FormatVersion(Box::new(UnquotedString::from("1.2")));
        assert_eq!(clause.tag(), "format-version");

        let clause = HeaderClause::Unreserved(
            Box::new(UnquotedString::from("something")),
            Box::new(UnquotedString::new(String::new())),
        );
        assert_eq!(clause.tag(), "something");
    }

    #[test]
    fn cardinality() {
        let clause = HeaderClause::FormatVersion(Box::new(UnquotedString::from("1.2")));
        assert_eq!(
            clause.cardinality(),
            crate::semantics::Cardinality::ZeroOrOne
        );

        let clause = HeaderClause::Unreserved(
            Box::new(UnquotedString::from("something")),
            Box::new(UnquotedString::new(String::new())),
        );
        assert_eq!(clause.cardinality(), crate::semantics::Cardinality::Any);
    }

    #[test]
    fn from_str() {
        let actual = HeaderClause::from_str("format-version: 1.2").unwrap();
        let expected =
            HeaderClause::FormatVersion(Box::new(UnquotedString::new(String::from("1.2"))));
        assert_eq!(actual, expected);

        let actual = HeaderClause::from_str("subsetdef: GO_SLIM \"GO Slim\"").unwrap();
        let expected = HeaderClause::Subsetdef(
            Box::new(SubsetIdent::from(UnprefixedIdent::new("GO_SLIM"))),
            Box::new(QuotedString::new("GO Slim")),
        );
        assert_eq!(actual, expected);

        let actual = HeaderClause::from_str("date: 17:03:2019 20:16").unwrap();
        let expected = HeaderClause::Date(Box::new(NaiveDateTime::new(17, 3, 2019, 20, 16)));
        assert_eq!(actual, expected);

        let actual =
            HeaderClause::from_str("namespace-id-rule: * XAO:$sequence(7,5000,9999999)$").unwrap();
        let expected = HeaderClause::NamespaceIdRule(Box::new(UnquotedString::new(
            "* XAO:$sequence(7,5000,9999999)$",
        )));
        assert_eq!(actual, expected);

        let actual = HeaderClause::from_str("treat-xrefs-as-relationship: TEST rel").unwrap();
        let expected = HeaderClause::TreatXrefsAsRelationship(
            Box::new(IdentPrefix::new("TEST")),
            Box::new(RelationIdent::from(UnprefixedIdent::new("rel"))),
        );
        assert_eq!(actual, expected);

        let actual = HeaderClause::from_str("tag: value").unwrap();
        let expected = HeaderClause::Unreserved(
            Box::new(UnquotedString::new("tag")),
            Box::new(UnquotedString::new("value")),
        );
        assert_eq!(actual, expected);
    }

    #[test]
    fn partial_cmp() {
        macro_rules! assert_lt {
            ($l:ident, $r:ident) => {
                assert!($l < $r, "'{}' < '{}' is not true!", $l, $r)
            };
        }

        let fv1 = HeaderClause::FormatVersion(Box::new(UnquotedString::new("1.4")));
        let fv2 = HeaderClause::FormatVersion(Box::new(UnquotedString::new("2")));
        assert_lt!(fv1, fv2);

        let dv1 = HeaderClause::DataVersion(Box::new(UnquotedString::new("1.4")));
        let dv2 = HeaderClause::DataVersion(Box::new(UnquotedString::new("2")));
        assert_lt!(dv1, dv2);

        assert_lt!(fv1, dv1);
        assert_lt!(fv1, dv2);
        assert_lt!(fv2, dv1);
        assert_lt!(fv2, dv2);
    }

    #[test]
    fn display() {
        let expected = "unreserved-thing: something";
        let actual = HeaderClause::Unreserved(
            Box::new(UnquotedString::new("unreserved-thing")),
            Box::new(UnquotedString::new("something")),
        );
        assert_eq!(&actual.to_string(), expected);

        let expected = r#"synonymtypedef: EXAMPLE "example""#;
        let actual = HeaderClause::SynonymTypedef(
            Box::new(SynonymTypeIdent::from(UnprefixedIdent::new("EXAMPLE"))),
            Box::new(QuotedString::new("example")),
            None,
        );
        assert_eq!(&actual.to_string(), expected);
    }
}