Skip to main content

strixonomy_obo/
obofoundry.rs

1//! OBO Foundry registry JSON model (Protégé Wave 3 port).
2//!
3//! Parses the OBO Foundry registry shape used by Protégé's `OboFoundry*` types.
4//! Does **not** fetch live HTTP — tests use vendored fixtures.
5
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use thiserror::Error;
9
10#[derive(Debug, Error)]
11pub enum OboFoundryError {
12    #[error("invalid OBO Foundry registry JSON: {0}")]
13    Json(#[from] serde_json::Error),
14}
15
16pub type Result<T> = std::result::Result<T, OboFoundryError>;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct OboFoundryContact {
20    #[serde(default)]
21    pub email: Option<String>,
22    #[serde(default)]
23    pub github: Option<String>,
24    #[serde(default)]
25    pub label: Option<String>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct OboFoundryLicense {
30    #[serde(default)]
31    pub label: Option<String>,
32    #[serde(default)]
33    pub url: Option<String>,
34    #[serde(default)]
35    pub logo: Option<String>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct OboFoundryEntry {
40    pub id: String,
41    #[serde(default)]
42    pub title: Option<String>,
43    #[serde(default)]
44    pub description: Option<String>,
45    #[serde(default)]
46    pub activity_status: Option<String>,
47    #[serde(default)]
48    pub ontology_purl: Option<String>,
49    #[serde(default)]
50    pub homepage: Option<String>,
51    #[serde(default)]
52    pub contact: Option<OboFoundryContact>,
53    #[serde(default)]
54    pub license: Option<OboFoundryLicense>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58struct RegistryFile {
59    #[serde(default)]
60    ontologies: Vec<OboFoundryEntry>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct OboFoundryRegistry {
65    ontologies: Vec<OboFoundryEntry>,
66    by_id: BTreeMap<String, usize>,
67}
68
69impl OboFoundryRegistry {
70    pub fn empty() -> Self {
71        Self { ontologies: Vec::new(), by_id: BTreeMap::new() }
72    }
73
74    pub fn from_entries(ontologies: Vec<OboFoundryEntry>) -> Self {
75        let mut by_id = BTreeMap::new();
76        for (i, e) in ontologies.iter().enumerate() {
77            by_id.insert(e.id.clone(), i);
78        }
79        Self { ontologies, by_id }
80    }
81
82    pub fn ontologies(&self) -> &[OboFoundryEntry] {
83        &self.ontologies
84    }
85
86    pub fn get(&self, id: &str) -> Option<&OboFoundryEntry> {
87        self.by_id.get(id).map(|&i| &self.ontologies[i])
88    }
89
90    pub fn len(&self) -> usize {
91        self.ontologies.len()
92    }
93
94    pub fn is_empty(&self) -> bool {
95        self.ontologies.is_empty()
96    }
97}
98
99/// Parse an OBO Foundry registry JSON document (`{"ontologies":[...]}`).
100pub fn parse_registry_json(bytes: &[u8]) -> Result<OboFoundryRegistry> {
101    let file: RegistryFile = serde_json::from_slice(bytes)?;
102    Ok(OboFoundryRegistry::from_entries(file.ontologies))
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn parse_minimal_registry() {
111        let json = br#"{
112            "ontologies": [
113                {
114                    "id": "bfo",
115                    "title": "Basic Formal Ontology",
116                    "activity_status": "active",
117                    "ontology_purl": "http://purl.obolibrary.org/obo/bfo.owl",
118                    "contact": {"email": "a@b.c", "label": "Alice"},
119                    "license": {"label": "CC-BY", "url": "http://creativecommons.org/licenses/by/4.0/"}
120                }
121            ]
122        }"#;
123        let reg = parse_registry_json(json).expect("parse");
124        assert_eq!(reg.len(), 1);
125        let e = reg.get("bfo").expect("bfo");
126        assert_eq!(e.title.as_deref(), Some("Basic Formal Ontology"));
127        assert_eq!(e.contact.as_ref().unwrap().label.as_deref(), Some("Alice"));
128        assert_eq!(e.license.as_ref().unwrap().label.as_deref(), Some("CC-BY"));
129    }
130
131    #[test]
132    fn empty_registry() {
133        let reg = parse_registry_json(br#"{"ontologies":[]}"#).unwrap();
134        assert!(reg.is_empty());
135        assert!(reg.get("go").is_none());
136    }
137}