fastobo/ast/term/
clause.rs

1use fastobo_derive_internal::FromStr;
2use fastobo_derive_internal::OboClause;
3
4use crate::ast::*;
5use crate::error::SyntaxError;
6use crate::parser::Cache;
7use crate::parser::FromPair;
8use crate::semantics::OboClause;
9use crate::syntax::pest::iterators::Pair;
10use crate::syntax::Rule;
11
12/// A clause appearing in a term frame.
13///
14/// # Comparison
15/// `TermClause` implements `PartialOrd` following the semantics of the OBO
16/// specification: clauses will compare based on their serialization order
17/// rather than on their alphabetic order; clauses of the same kind will be
18/// ranked in the alphabetic order.
19#[derive(Clone, Debug, Eq, Hash, FromStr, Ord, OboClause, PartialEq, PartialOrd)]
20pub enum TermClause {
21    #[clause(cardinality = "ZeroOrOne")]
22    IsAnonymous(bool),
23    #[clause(cardinality = "ZeroOrOne")]
24    Name(Box<UnquotedString>),
25    #[clause(cardinality = "One")]
26    Namespace(Box<NamespaceIdent>),
27    AltId(Box<Ident>),
28    #[clause(cardinality = "ZeroOrOne")]
29    Def(Box<Definition>),
30    #[clause(cardinality = "ZeroOrOne")]
31    Comment(Box<UnquotedString>),
32    Subset(Box<SubsetIdent>),
33    Synonym(Box<Synonym>),
34    Xref(Box<Xref>),
35    #[clause(cardinality = "ZeroOrOne")]
36    Builtin(bool),
37    PropertyValue(Box<PropertyValue>),
38    IsA(Box<ClassIdent>),
39    #[clause(cardinality = "NotOne")]
40    IntersectionOf(Option<Box<RelationIdent>>, Box<ClassIdent>),
41    #[clause(cardinality = "NotOne")]
42    UnionOf(Box<ClassIdent>),
43    EquivalentTo(Box<ClassIdent>),
44    DisjointFrom(Box<ClassIdent>),
45    Relationship(Box<RelationIdent>, Box<ClassIdent>),
46    #[clause(cardinality = "ZeroOrOne")]
47    CreatedBy(Box<UnquotedString>),
48    #[clause(cardinality = "ZeroOrOne")]
49    CreationDate(Box<CreationDate>),
50    #[clause(cardinality = "ZeroOrOne")]
51    IsObsolete(bool),
52    ReplacedBy(Box<ClassIdent>),
53    Consider(Box<ClassIdent>),
54}
55
56clause_impl_from!(TermClause);
57
58impl<'i> FromPair<'i> for Line<TermClause> {
59    const RULE: Rule = Rule::TermClauseLine;
60    unsafe fn from_pair_unchecked(
61        pair: Pair<'i, Rule>,
62        cache: &Cache,
63    ) -> Result<Self, SyntaxError> {
64        let mut inner = pair.into_inner();
65        let clause = TermClause::from_pair_unchecked(inner.next().unwrap(), cache)?;
66        let eol = inner.next().unwrap();
67        Ok(Eol::from_pair_unchecked(eol, cache)?.and_inner(clause))
68    }
69}
70
71impl<'i> FromPair<'i> for TermClause {
72    const RULE: Rule = Rule::TermClause;
73    unsafe fn from_pair_unchecked(pair: Pair<Rule>, cache: &Cache) -> Result<Self, SyntaxError> {
74        let mut inner = pair.into_inner();
75        match inner.next().unwrap().as_rule() {
76            Rule::IsAnonymousTag => {
77                let b = bool::from_pair_unchecked(inner.next().unwrap(), cache)?;
78                Ok(TermClause::IsAnonymous(b))
79            }
80            Rule::NameTag => {
81                let name = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
82                Ok(TermClause::Name(Box::new(name)))
83            }
84            Rule::NamespaceTag => {
85                let ns = NamespaceIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
86                Ok(TermClause::Namespace(Box::new(ns)))
87            }
88            Rule::AltIdTag => {
89                let id = Ident::from_pair_unchecked(inner.next().unwrap(), cache)?;
90                Ok(TermClause::AltId(Box::new(id)))
91            }
92            Rule::DefTag => {
93                let def = Definition::from_pair_unchecked(inner.next().unwrap(), cache)?;
94                Ok(TermClause::Def(Box::new(def)))
95            }
96            Rule::CommentTag => {
97                let comment = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
98                Ok(TermClause::Comment(Box::new(comment)))
99            }
100            Rule::SubsetTag => {
101                let id = SubsetIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
102                Ok(TermClause::Subset(Box::new(id)))
103            }
104            Rule::SynonymTag => {
105                let syn = Synonym::from_pair_unchecked(inner.next().unwrap(), cache)?;
106                Ok(TermClause::Synonym(Box::new(syn)))
107            }
108            Rule::XrefTag => {
109                let xref = Xref::from_pair_unchecked(inner.next().unwrap(), cache)?;
110                Ok(TermClause::Xref(Box::new(xref)))
111            }
112            Rule::BuiltinTag => {
113                let b = bool::from_pair_unchecked(inner.next().unwrap(), cache)?;
114                Ok(TermClause::Builtin(b))
115            }
116            Rule::PropertyValueTag => {
117                let pv = PropertyValue::from_pair_unchecked(inner.next().unwrap(), cache)?;
118                Ok(TermClause::PropertyValue(Box::new(pv)))
119            }
120            Rule::IsATag => {
121                let id = ClassIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
122                Ok(TermClause::IsA(Box::new(id)))
123            }
124            Rule::IntersectionOfTag => {
125                let id = inner.next().unwrap();
126                if id.as_rule() == Rule::ClassId {
127                    let classid = ClassIdent::from_pair_unchecked(id, cache)?;
128                    Ok(TermClause::IntersectionOf(None, Box::new(classid)))
129                } else {
130                    let relid = RelationIdent::from_pair_unchecked(id, cache)?;
131                    let classid = ClassIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
132                    Ok(TermClause::IntersectionOf(
133                        Some(Box::new(relid)),
134                        Box::new(classid),
135                    ))
136                }
137            }
138            Rule::UnionOfTag => {
139                let id = ClassIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
140                Ok(TermClause::UnionOf(Box::new(id)))
141            }
142            Rule::EquivalentToTag => {
143                let id = ClassIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
144                Ok(TermClause::EquivalentTo(Box::new(id)))
145            }
146            Rule::DisjointFromTag => {
147                let id = ClassIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
148                Ok(TermClause::DisjointFrom(Box::new(id)))
149            }
150            Rule::RelationshipTag => {
151                let rel = RelationIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
152                let id = ClassIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
153                Ok(TermClause::Relationship(Box::new(rel), Box::new(id)))
154            }
155            Rule::IsObsoleteTag => {
156                let b = bool::from_pair_unchecked(inner.next().unwrap(), cache)?;
157                Ok(TermClause::IsObsolete(b))
158            }
159            Rule::ReplacedByTag => {
160                let id = ClassIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
161                Ok(TermClause::ReplacedBy(Box::new(id)))
162            }
163            Rule::ConsiderTag => {
164                let id = ClassIdent::from_pair_unchecked(inner.next().unwrap(), cache)?;
165                Ok(TermClause::Consider(Box::new(id)))
166            }
167            Rule::CreatedByTag => {
168                let s = UnquotedString::from_pair_unchecked(inner.next().unwrap(), cache)?;
169                Ok(TermClause::CreatedBy(Box::new(s)))
170            }
171            Rule::CreationDateTag => {
172                let dt = CreationDate::from_pair_unchecked(inner.next().unwrap(), cache)?;
173                Ok(TermClause::CreationDate(Box::new(dt)))
174            }
175            _ => unreachable!(),
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182
183    use super::*;
184    use pretty_assertions::assert_eq;
185    use std::str::FromStr;
186
187    mod is_anonymous {
188        use super::*;
189
190        #[test]
191        fn from_str() {
192            let actual = TermClause::from_str("is_anonymous: true").unwrap();
193            let expected = TermClause::IsAnonymous(true);
194            self::assert_eq!(actual, expected);
195        }
196
197        #[test]
198        fn to_string() {
199            let clause = TermClause::Name(Box::new(UnquotedString::new("sample name")));
200            self::assert_eq!(clause.to_string(), "name: sample name")
201        }
202    }
203
204    mod name {
205        use super::*;
206
207        #[test]
208        fn from_str() {
209            let actual = TermClause::from_str("name: sample name").unwrap();
210            let expected = TermClause::Name(Box::new(UnquotedString::new("sample name")));
211            self::assert_eq!(actual, expected);
212        }
213
214        #[test]
215        fn to_string() {
216            let clause = TermClause::Name(Box::new(UnquotedString::new("sample name")));
217            self::assert_eq!(clause.to_string(), "name: sample name")
218        }
219    }
220
221    mod namespace {}
222
223    mod alt_id {}
224
225    mod def {
226        use super::*;
227
228        #[test]
229        fn from_str() {
230            let actual = TermClause::from_str(
231                "def: \"A reference string relevant to the sample under study.\" [PSI:MS]",
232            )
233            .unwrap();
234            let expected = TermClause::Def(Box::new(Definition::with_xrefs(
235                QuotedString::new(String::from(
236                    "A reference string relevant to the sample under study.",
237                )),
238                XrefList::from(vec![Xref::new(PrefixedIdent::new("PSI", "MS"))]),
239            )));
240            self::assert_eq!(actual, expected);
241
242            let actual = TermClause::from_str("def: \"OBSOLETE: There is Phenyx:ScoringModel for Phenyx! Scoring model (more detailed granularity). TODO: add some child terms.\" [PSI:PI]").unwrap();
243            let expected = TermClause::Def(Box::new(Definition::with_xrefs(
244                QuotedString::new("OBSOLETE: There is Phenyx:ScoringModel for Phenyx! Scoring model (more detailed granularity). TODO: add some child terms."),
245                XrefList::from(vec![Xref::new(PrefixedIdent::new("PSI", "PI"))])
246            )));
247            self::assert_eq!(actual, expected);
248        }
249    }
250
251    mod comment {}
252
253    mod subset {}
254
255    mod synonym {
256        use super::*;
257
258        #[test]
259        fn from_str() {
260            let actual =
261                TermClause::from_str("synonym: \"chemical entity\" EXACT [UniProt]").unwrap();
262            let expected = TermClause::Synonym(Box::new(Synonym::with_xrefs(
263                QuotedString::new("chemical entity"),
264                SynonymScope::Exact,
265                XrefList::from(vec![Xref::new(UnprefixedIdent::new("UniProt"))]),
266            )));
267            self::assert_eq!(actual, expected);
268        }
269    }
270
271    mod xref {
272        use super::*;
273
274        #[test]
275        fn from_str() {
276            let actual =
277                TermClause::from_str("xref: CAS:22325-47-9 \"NIST Chemistry WebBook\"").unwrap();
278            let expected = TermClause::Xref(Box::new(Xref::with_desc(
279                Ident::from(PrefixedIdent::new("CAS", "22325-47-9")),
280                QuotedString::new("NIST Chemistry WebBook"),
281            )));
282            self::assert_eq!(actual, expected);
283
284            let actual =
285                TermClause::from_str("xref: Wikipedia:https\\://en.wikipedia.org/wiki/Gas")
286                    .unwrap();
287            let expected = TermClause::Xref(Box::new(Xref::new(PrefixedIdent::new(
288                "Wikipedia",
289                "https://en.wikipedia.org/wiki/Gas",
290            ))));
291            self::assert_eq!(actual, expected);
292        }
293    }
294
295    mod builtin {}
296
297    mod property_value {}
298
299    mod is_a {}
300
301    mod intersection_of {
302        use super::*;
303        use textwrap_macros::dedent;
304
305        #[test]
306        fn from_str() {
307            let actual = TermFrame::from_str(dedent!(
308                "[Term]
309                    id: TST:001
310                    intersection_of: part_of PO:0020039 ! leaf lamina
311                    "
312            ))
313            .unwrap()
314            .into_iter()
315            .next()
316            .unwrap();
317
318            let expected = Line::from(TermClause::IntersectionOf(
319                Some(Box::new(RelationIdent::from(UnprefixedIdent::new(
320                    "part_of",
321                )))),
322                Box::new(ClassIdent::from(PrefixedIdent::new("PO", "0020039"))),
323            ))
324            .and_comment(Comment::new("leaf lamina"));
325            self::assert_eq!(actual, expected);
326
327            let actual = TermFrame::from_str(dedent!(
328                "[Term]
329                    id: TST:001
330                    intersection_of: PO:0006016 ! leaf epidermis
331                    "
332            ))
333            .unwrap()
334            .into_iter()
335            .next()
336            .unwrap();
337            let expected = Line::with_comment(Comment::new("leaf epidermis")).and_inner(
338                TermClause::IntersectionOf(
339                    None,
340                    Box::new(ClassIdent::from(PrefixedIdent::new("PO", "0006016"))),
341                ),
342            );
343            self::assert_eq!(actual, expected);
344        }
345    }
346
347    mod union_of {}
348
349    mod equivalent_to {}
350
351    mod disjoint_from {}
352
353    mod relationship {}
354
355    mod is_obsolete {}
356
357    mod replaced_by {}
358
359    mod consider {}
360
361    mod created_by {}
362
363    mod creation_date {}
364}