Skip to main content

bids_core/
genetic.rs

1//! Genetic descriptor files (`genetic_info.json`, `genetic_database.json`).
2//!
3//! BIDS datasets may include genetic descriptor files at the root level
4//! describing the genetic data associated with the dataset. See the
5//! [BIDS specification appendix](https://bids-specification.readthedocs.io/en/stable/appendices/genetic-database.html).
6//!
7//! # Example
8//!
9//! ```
10//! use bids_core::genetic::GeneticInfo;
11//!
12//! let json = r#"{"Dataset": "OpenNeuro", "Genetics": {"Database": "https://example.com", "Descriptors": "APOE"}}"#;
13//! let info: GeneticInfo = serde_json::from_str(json).unwrap();
14//! assert_eq!(info.genetics.as_ref().unwrap().database.as_deref(), Some("https://example.com"));
15//! ```
16
17use serde::{Deserialize, Serialize};
18use std::path::Path;
19
20use crate::error::Result;
21
22/// Contents of `genetic_info.json`.
23///
24/// Describes the genetic information associated with participants in the
25/// dataset. This is a root-level file that links to external genetic databases.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "PascalCase")]
28pub struct GeneticInfo {
29    /// Name of the dataset.
30    #[serde(default)]
31    pub dataset: Option<String>,
32    /// Genetic database reference information.
33    #[serde(default)]
34    pub genetics: Option<GeneticsField>,
35}
36
37/// Genetic database reference.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "PascalCase")]
40pub struct GeneticsField {
41    /// URI of the genetic database.
42    #[serde(default)]
43    pub database: Option<String>,
44    /// Descriptors of the genetic data (e.g., genotype, SNP, allele).
45    #[serde(default)]
46    pub descriptors: Option<serde_json::Value>,
47}
48
49/// Contents of `genetic_database.json`.
50///
51/// Maps participant IDs to genetic database identifiers, enabling linkage
52/// between BIDS participant data and external genetic resources.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct GeneticDatabase {
55    /// Mapping of `participant_id` → database record ID.
56    #[serde(flatten)]
57    pub entries: std::collections::HashMap<String, serde_json::Value>,
58}
59
60impl GeneticInfo {
61    /// Load `genetic_info.json` from a dataset root directory.
62    ///
63    /// Returns `Ok(None)` if the file doesn't exist.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if the file exists but can't be read or parsed.
68    pub fn from_dir(dir: &Path) -> Result<Option<Self>> {
69        let path = dir.join("genetic_info.json");
70        if !path.exists() {
71            return Ok(None);
72        }
73        let contents = std::fs::read_to_string(&path)?;
74        let info: Self = serde_json::from_str(&contents)?;
75        Ok(Some(info))
76    }
77}
78
79impl GeneticDatabase {
80    /// Load `genetic_database.json` from a dataset root directory.
81    ///
82    /// Returns `Ok(None)` if the file doesn't exist.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error if the file exists but can't be read or parsed.
87    pub fn from_dir(dir: &Path) -> Result<Option<Self>> {
88        let path = dir.join("genetic_database.json");
89        if !path.exists() {
90            return Ok(None);
91        }
92        let contents = std::fs::read_to_string(&path)?;
93        let db: Self = serde_json::from_str(&contents)?;
94        Ok(Some(db))
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_genetic_info_parse() {
104        let json = r#"{
105            "Dataset": "Example",
106            "Genetics": {
107                "Database": "https://www.ncbi.nlm.nih.gov/gap/",
108                "Descriptors": ["APOE", "BDNF"]
109            }
110        }"#;
111        let info: GeneticInfo = serde_json::from_str(json).unwrap();
112        assert_eq!(info.dataset.as_deref(), Some("Example"));
113        let genetics = info.genetics.unwrap();
114        assert!(genetics.database.as_ref().unwrap().contains("ncbi"));
115    }
116
117    #[test]
118    fn test_genetic_database_parse() {
119        let json = r#"{
120            "sub-01": {"sample_id": "GENO_001"},
121            "sub-02": {"sample_id": "GENO_002"}
122        }"#;
123        let db: GeneticDatabase = serde_json::from_str(json).unwrap();
124        assert_eq!(db.entries.len(), 2);
125        assert!(db.entries.contains_key("sub-01"));
126    }
127
128    #[test]
129    fn test_genetic_info_missing_file() {
130        let dir = std::env::temp_dir().join("bids_genetic_test_missing");
131        std::fs::create_dir_all(&dir).unwrap();
132        let result = GeneticInfo::from_dir(&dir).unwrap();
133        assert!(result.is_none());
134        std::fs::remove_dir_all(&dir).unwrap();
135    }
136}