1use indexmap::IndexMap;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
37pub struct BidsMetadata {
38 inner: IndexMap<String, Value>,
39 #[serde(skip)]
41 pub source_file: Option<String>,
42}
43
44impl BidsMetadata {
45 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn with_source(source_file: &str) -> Self {
52 Self {
53 inner: IndexMap::new(),
54 source_file: Some(source_file.to_string()),
55 }
56 }
57
58 #[must_use]
60 pub fn get(&self, key: &str) -> Option<&Value> {
61 self.inner.get(key)
62 }
63
64 #[must_use]
66 pub fn get_str(&self, key: &str) -> Option<&str> {
67 self.inner.get(key).and_then(|v| v.as_str())
68 }
69
70 #[must_use]
72 pub fn get_f64(&self, key: &str) -> Option<f64> {
73 self.inner.get(key).and_then(serde_json::Value::as_f64)
74 }
75
76 #[must_use]
78 pub fn get_i64(&self, key: &str) -> Option<i64> {
79 self.inner.get(key).and_then(serde_json::Value::as_i64)
80 }
81
82 #[must_use]
84 pub fn get_bool(&self, key: &str) -> Option<bool> {
85 self.inner.get(key).and_then(serde_json::Value::as_bool)
86 }
87
88 #[must_use]
90 pub fn get_array(&self, key: &str) -> Option<&Vec<Value>> {
91 self.inner.get(key).and_then(|v| v.as_array())
92 }
93
94 pub fn insert(&mut self, key: String, value: Value) {
96 self.inner.insert(key, value);
97 }
98
99 pub fn extend(&mut self, other: BidsMetadata) {
101 self.inner.extend(other.inner);
102 }
103
104 pub fn update_from_map(&mut self, map: IndexMap<String, Value>) {
106 self.inner.extend(map);
107 }
108
109 #[must_use]
111 pub fn contains_key(&self, key: &str) -> bool {
112 self.inner.contains_key(key)
113 }
114
115 #[must_use]
117 pub fn keys(&self) -> indexmap::map::Keys<'_, String, Value> {
118 self.inner.keys()
119 }
120
121 #[must_use]
123 pub fn iter(&self) -> indexmap::map::Iter<'_, String, Value> {
124 self.inner.iter()
125 }
126
127 #[must_use]
129 pub fn len(&self) -> usize {
130 self.inner.len()
131 }
132
133 #[must_use]
135 pub fn is_empty(&self) -> bool {
136 self.inner.is_empty()
137 }
138}
139
140impl FromIterator<(String, Value)> for BidsMetadata {
141 fn from_iter<I: IntoIterator<Item = (String, Value)>>(iter: I) -> Self {
142 Self {
143 inner: IndexMap::from_iter(iter),
144 source_file: None,
145 }
146 }
147}
148
149impl BidsMetadata {
150 pub fn deserialize_as<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
154 let json = serde_json::to_value(&self.inner).ok()?;
155 serde_json::from_value(json).ok()
156 }
157}
158
159impl IntoIterator for BidsMetadata {
160 type Item = (String, Value);
161 type IntoIter = indexmap::map::IntoIter<String, Value>;
162
163 fn into_iter(self) -> Self::IntoIter {
164 self.inner.into_iter()
165 }
166}
167
168impl std::ops::Index<&str> for BidsMetadata {
169 type Output = Value;
170
171 fn index(&self, key: &str) -> &Value {
178 &self.inner[key]
179 }
180}
181
182impl From<IndexMap<String, Value>> for BidsMetadata {
183 fn from(map: IndexMap<String, Value>) -> Self {
184 Self {
185 inner: map,
186 source_file: None,
187 }
188 }
189}
190
191impl From<serde_json::Map<String, Value>> for BidsMetadata {
192 fn from(map: serde_json::Map<String, Value>) -> Self {
193 Self {
194 inner: map.into_iter().collect(),
195 source_file: None,
196 }
197 }
198}
199
200impl std::fmt::Display for BidsMetadata {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 write!(f, "BidsMetadata({} keys", self.inner.len())?;
203 if let Some(src) = &self.source_file {
204 write!(f, " from {src}")?;
205 }
206 write!(f, ")")
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use serde_json::json;
214
215 #[test]
216 fn test_metadata_typed_accessors() {
217 let mut md = BidsMetadata::new();
218 md.insert("SamplingFrequency".into(), json!(256.0));
219 md.insert("EEGReference".into(), json!("Cz"));
220 md.insert("RecordingDuration".into(), json!(600));
221 md.insert("EEGGround".into(), json!(true));
222 md.insert("TaskName".into(), json!(null));
223
224 assert_eq!(md.get_f64("SamplingFrequency"), Some(256.0));
225 assert_eq!(md.get_str("EEGReference"), Some("Cz"));
226 assert_eq!(md.get_i64("RecordingDuration"), Some(600));
227 assert_eq!(md.get_bool("EEGGround"), Some(true));
228 assert!(md.get_str("TaskName").is_none());
229 assert!(md.get_f64("Missing").is_none());
230 }
231
232 #[test]
233 fn test_metadata_extend_overrides() {
234 let mut base = BidsMetadata::new();
235 base.insert("A".into(), json!(1));
236 base.insert("B".into(), json!(2));
237
238 let mut child = BidsMetadata::new();
239 child.insert("B".into(), json!(99));
240 child.insert("C".into(), json!(3));
241
242 base.extend(child);
243 assert_eq!(base.get_i64("A"), Some(1));
244 assert_eq!(base.get_i64("B"), Some(99)); assert_eq!(base.get_i64("C"), Some(3));
246 assert_eq!(base.len(), 3);
247 }
248
249 #[test]
250 fn test_metadata_deserialize_as() {
251 #[derive(serde::Deserialize)]
252 struct EegMeta {
253 #[serde(rename = "SamplingFrequency")]
254 sampling_frequency: f64,
255 #[serde(rename = "EEGReference")]
256 eeg_reference: String,
257 }
258
259 let mut md = BidsMetadata::new();
260 md.insert("SamplingFrequency".into(), json!(256.0));
261 md.insert("EEGReference".into(), json!("Cz"));
262
263 let typed: EegMeta = md.deserialize_as().unwrap();
264 assert_eq!(typed.sampling_frequency, 256.0);
265 assert_eq!(typed.eeg_reference, "Cz");
266 }
267
268 #[test]
269 fn test_metadata_from_iterator() {
270 let md: BidsMetadata = vec![
271 ("A".to_string(), json!(1)),
272 ("B".to_string(), json!("hello")),
273 ]
274 .into_iter()
275 .collect();
276 assert_eq!(md.len(), 2);
277 assert_eq!(md.get_i64("A"), Some(1));
278 }
279
280 #[test]
281 fn test_metadata_source_file() {
282 let md = BidsMetadata::with_source("/data/sub-01_eeg.json");
283 assert_eq!(md.source_file.as_deref(), Some("/data/sub-01_eeg.json"));
284 assert!(md.is_empty());
285 }
286}