Skip to main content

qualia_client_core/wellfair/
ccf_resolver.rs

1//! Discover the CCF/HRA reference-organ GLB assets from the Human Reference Atlas **SPARQL endpoint**
2//! (the scalable, canonical source — no repo clone, no Git-LFS pointers). The HRA Knowledge Graph
3//! registers each reference organ as linked data; the GLB lives as a `foaf:depiction` `xsd:anyURI`
4//! pointing at `cdn.humanatlas.io` (the real binary). The CDN path encodes organ, sex, and version, so
5//! the discovered filename (e.g. `3d-vh-m-liver.glb`) feeds straight into
6//! [`body_system_for_organ`](wellfare_core::anatomy::body_system_for_organ) and
7//! [`compile_body`](super::anatomy_body::compile_body).
8//!
9//! This module is **pure** (query construction + result parsing + model filtering) — no HTTP, so it is
10//! unit-tested against captured real endpoint JSON. The live GET + the per-organ binary fetch are a thin
11//! transport layer (qualia-client-core's async HTTP is a separate lane); this owns the semantics.
12
13use serde::{Deserialize, Serialize};
14use wellfare_core::anatomy::AnatomyModel;
15
16/// The HRA Linked Open Data SPARQL endpoint (verified live: returns `application/sparql-results+json`).
17pub const HRA_SPARQL_ENDPOINT: &str = "https://lod.humanatlas.io/sparql";
18
19/// A descriptive User-Agent. Some asset hosts (e.g. NIH 3D's WAF) reject requests with **no**
20/// User-Agent (reqwest sends none by default) with a 403 — an explicit one is required.
21const HTTP_USER_AGENT: &str = "QualiaDB-anatomy/1.0";
22
23/// The query that lists every reference-organ GLB URL registered in the HRA KG (across named graphs).
24/// Bound variable is `glb`. Deterministic ordering so the manifest is stable/attestable.
25pub fn ref_organ_glb_query() -> String {
26    "SELECT DISTINCT ?glb WHERE { \
27       GRAPH ?g { ?s <http://xmlns.com/foaf/0.1/depiction> ?glb } \
28       FILTER(STRENDS(LCASE(STR(?glb)), \".glb\") && CONTAINS(STR(?glb), \"/ref-organ/\")) \
29     } ORDER BY ?glb"
30        .to_string()
31}
32
33/// One discovered reference-organ asset: the GLB filename (the organ key used everywhere downstream),
34/// its canonical CDN URL, and which reference model it belongs to.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct RefOrgan {
37    /// The GLB filename, e.g. `3d-vh-m-liver.glb` — this is the `organ_key` for `body_system_for_organ`.
38    pub filename: String,
39    /// The canonical CDN URL of the binary GLB.
40    pub glb_url: String,
41    /// The reference model this organ belongs to (from the `-f-`/`-m-` sex marker in the filename).
42    pub model: AnatomyModel,
43}
44
45/// Parse the SPARQL-results JSON from [`ref_organ_glb_query`] into the reference-organ manifest.
46///
47/// The sex/model is read from the unambiguous `-f-`/`-m-` filename infix (provider varies — `vh`,
48/// `allen`, `sbu`, `nih` — but the sex marker does not). A GLB with no sex infix (rare; unsexed asset)
49/// is skipped rather than guessed. Malformed JSON yields an empty manifest.
50pub fn parse_ref_organs(sparql_json: &str) -> Vec<RefOrgan> {
51    let root: serde_json::Value = match serde_json::from_str(sparql_json) {
52        Ok(v) => v,
53        Err(_) => return Vec::new(),
54    };
55    let bindings = root
56        .get("results")
57        .and_then(|r| r.get("bindings"))
58        .and_then(|b| b.as_array());
59    let mut out = Vec::new();
60    if let Some(bindings) = bindings {
61        for b in bindings {
62            let Some(url) = b
63                .get("glb")
64                .and_then(|g| g.get("value"))
65                .and_then(|v| v.as_str())
66            else {
67                continue;
68            };
69            let filename = url.rsplit('/').next().unwrap_or(url).to_string();
70            let Some(model) = model_from_filename(&filename) else {
71                continue;
72            };
73            out.push(RefOrgan {
74                filename,
75                glb_url: url.to_string(),
76                model,
77            });
78        }
79    }
80    out
81}
82
83/// The reference model a CCF GLB filename belongs to, from its `-f-`/`-m-` sex infix.
84fn model_from_filename(filename: &str) -> Option<AnatomyModel> {
85    if filename.contains("-f-") {
86        Some(AnatomyModel::Female)
87    } else if filename.contains("-m-") {
88        Some(AnatomyModel::Male)
89    } else {
90        None
91    }
92}
93
94/// The organs of a single model, in discovery order.
95pub fn organs_for_model(organs: &[RefOrgan], model: AnatomyModel) -> Vec<RefOrgan> {
96    organs
97        .iter()
98        .filter(|o| o.model == model)
99        .cloned()
100        .collect()
101}
102
103/// A live CCF discovery / fetch error.
104#[cfg(not(target_arch = "wasm32"))]
105#[derive(Debug)]
106pub enum CcfError {
107    Http(reqwest::Error),
108}
109
110#[cfg(not(target_arch = "wasm32"))]
111impl std::fmt::Display for CcfError {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            CcfError::Http(e) => write!(f, "CCF HTTP: {e}"),
115        }
116    }
117}
118
119#[cfg(not(target_arch = "wasm32"))]
120impl std::error::Error for CcfError {}
121
122#[cfg(not(target_arch = "wasm32"))]
123impl From<reqwest::Error> for CcfError {
124    fn from(e: reqwest::Error) -> Self {
125        CcfError::Http(e)
126    }
127}
128
129/// Discover the reference-organ manifest **live** from the HRA SPARQL endpoint (blocking network I/O —
130/// call off the async runtime, e.g. via `spawn_blocking`). The query/parse are pure and unit-tested;
131/// this only adds the transport.
132#[cfg(not(target_arch = "wasm32"))]
133pub fn discover_ref_organs(endpoint: &str) -> Result<Vec<RefOrgan>, CcfError> {
134    // SPARQL 1.1 Protocol: POST the query as the request body (content-type application/sparql-query).
135    let json = reqwest::blocking::Client::new()
136        .post(endpoint)
137        .header(reqwest::header::USER_AGENT, HTTP_USER_AGENT)
138        .header(reqwest::header::CONTENT_TYPE, "application/sparql-query")
139        .header(reqwest::header::ACCEPT, "application/sparql-results+json")
140        .body(ref_organ_glb_query())
141        .send()?
142        .error_for_status()?
143        .text()?;
144    Ok(parse_ref_organs(&json))
145}
146
147/// Fetch one organ's GLB bytes from its CDN URL (blocking).
148#[cfg(not(target_arch = "wasm32"))]
149pub fn fetch_glb(glb_url: &str) -> Result<Vec<u8>, CcfError> {
150    let bytes = reqwest::blocking::Client::new()
151        .get(glb_url)
152        .header(reqwest::header::USER_AGENT, HTTP_USER_AGENT)
153        .send()?
154        .error_for_status()?
155        .bytes()?;
156    Ok(bytes.to_vec())
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    // A captured, real fragment of the HRA endpoint response (three bindings — a male organ, a
164    // lateralized female organ, and a female-only organ).
165    const SAMPLE_JSON: &str = r#"{
166      "head": { "vars": ["glb"] },
167      "results": { "bindings": [
168        { "glb": { "datatype": "http://www.w3.org/2001/XMLSchema#anyURI", "type": "literal",
169          "value": "https://cdn.humanatlas.io/digital-objects/ref-organ/liver-male/v1.2/assets/3d-vh-m-liver.glb" } },
170        { "glb": { "datatype": "http://www.w3.org/2001/XMLSchema#anyURI", "type": "literal",
171          "value": "https://cdn.humanatlas.io/digital-objects/ref-organ/kidney-female-left/v1.3/assets/3d-vh-f-kidney-l.glb" } },
172        { "glb": { "datatype": "http://www.w3.org/2001/XMLSchema#anyURI", "type": "literal",
173          "value": "https://cdn.humanatlas.io/digital-objects/ref-organ/uterus-female/v1.2/assets/3d-vh-f-uterus.glb" } }
174      ]}
175    }"#;
176
177    #[test]
178    fn query_is_stable_and_targets_ref_organ_depictions() {
179        let q = ref_organ_glb_query();
180        assert!(q.contains("foaf/0.1/depiction"));
181        assert!(q.contains("/ref-organ/"));
182        assert!(q.contains("ORDER BY ?glb"), "deterministic manifest");
183    }
184
185    #[test]
186    fn parses_real_endpoint_json_into_typed_manifest() {
187        let organs = parse_ref_organs(SAMPLE_JSON);
188        assert_eq!(organs.len(), 3);
189        // Filename is the downstream organ key; the CDN URL is the real binary.
190        assert_eq!(organs[0].filename, "3d-vh-m-liver.glb");
191        assert!(organs[0].glb_url.starts_with("https://cdn.humanatlas.io/"));
192        // Sex/model read from the -m-/-f- infix.
193        assert_eq!(organs[0].model, AnatomyModel::Male);
194        assert_eq!(organs[1].model, AnatomyModel::Female);
195        assert_eq!(organs[2].model, AnatomyModel::Female);
196    }
197
198    #[test]
199    fn model_filter_splits_the_body() {
200        let organs = parse_ref_organs(SAMPLE_JSON);
201        assert_eq!(organs_for_model(&organs, AnatomyModel::Male).len(), 1);
202        assert_eq!(organs_for_model(&organs, AnatomyModel::Female).len(), 2);
203    }
204
205    #[test]
206    fn discovered_filenames_resolve_to_body_systems() {
207        // The whole point: a SPARQL-discovered filename feeds straight into the organ→system map.
208        use wellfare_core::anatomy::body_system_for_organ;
209        let organs = parse_ref_organs(SAMPLE_JSON);
210        assert_eq!(
211            body_system_for_organ(&organs[0].filename),
212            Some("digestive")
213        ); // liver
214        assert_eq!(body_system_for_organ(&organs[1].filename), Some("urinary")); // kidney
215        assert_eq!(
216            body_system_for_organ(&organs[2].filename),
217            Some("reproductive")
218        ); // uterus
219    }
220
221    #[test]
222    fn malformed_json_and_unsexed_assets_are_handled() {
223        assert!(parse_ref_organs("not json").is_empty());
224        // An asset with no -f-/-m- infix is skipped, not guessed.
225        let no_sex = r#"{"results":{"bindings":[
226          {"glb":{"type":"literal","value":"https://cdn.humanatlas.io/digital-objects/ref-organ/x/v1/assets/3d-vh-mystery.glb"}}
227        ]}}"#;
228        assert!(parse_ref_organs(no_sex).is_empty());
229    }
230}