bids_core/config.rs
1//! Layout configuration: entity patterns and path templates.
2//!
3//! A [`Config`] defines the entities a layout recognizes and the path patterns
4//! used to build BIDS-compliant filenames. Two built-in configs ship with the
5//! crate (`bids` and `derivatives`); custom configs can be loaded from JSON.
6
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10use crate::entities::Entity;
11use crate::error::{BidsError, Result};
12
13/// Configuration for a BIDS layout, defining entities and path patterns.
14///
15/// Corresponds to PyBIDS' `Config` class. Can be loaded from JSON config files
16/// (like `bids.json`) or constructed programmatically.
17///
18/// # Example
19///
20/// ```
21/// use bids_core::Config;
22///
23/// let config = Config::bids();
24/// assert_eq!(config.name, "bids");
25/// assert!(config.entity_count() > 20);
26/// assert!(config.get_entity("subject").is_some());
27/// ```
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Config {
30 pub name: String,
31 #[serde(default)]
32 pub entities: Vec<Entity>,
33 #[serde(default)]
34 pub default_path_patterns: Option<Vec<String>>,
35}
36
37impl Config {
38 /// Load a config from a JSON file.
39 ///
40 /// # Errors
41 ///
42 /// Returns an error if the file cannot be read or contains invalid JSON.
43 pub fn from_file(path: &Path) -> Result<Self> {
44 let contents = std::fs::read_to_string(path)?;
45 let config: Self = serde_json::from_str(&contents)?;
46 Ok(config)
47 }
48
49 /// Load the built-in BIDS config.
50 ///
51 /// # Panics
52 ///
53 /// Panics if the embedded `bids.json` is malformed (should never happen).
54 #[must_use]
55 pub fn bids() -> Self {
56 serde_json::from_str(include_str!("configs/bids.json"))
57 .expect("Built-in bids.json config should be valid")
58 }
59
60 /// Load the built-in derivatives config.
61 ///
62 /// # Panics
63 ///
64 /// Panics if the embedded `derivatives.json` is malformed (should never happen).
65 #[must_use]
66 pub fn derivatives() -> Self {
67 serde_json::from_str(include_str!("configs/derivatives.json"))
68 .expect("Built-in derivatives.json config should be valid")
69 }
70
71 /// Load a named config (`"bids"`, `"derivatives"`) or from a file path.
72 ///
73 /// # Errors
74 ///
75 /// Returns an error if the name is unrecognized and the path doesn't
76 /// exist or contains invalid JSON.
77 pub fn load(name_or_path: &str) -> Result<Self> {
78 match name_or_path {
79 "bids" => Ok(Self::bids()),
80 "derivatives" => Ok(Self::derivatives()),
81 path => {
82 let p = Path::new(path);
83 if p.exists() {
84 Self::from_file(p)
85 } else {
86 Err(BidsError::Config(format!(
87 "'{name_or_path}' is not a valid config name or path"
88 )))
89 }
90 }
91 }
92 }
93
94 /// Get mutable references to all entity definitions.
95 pub fn entities_mut(&mut self) -> &mut Vec<Entity> {
96 &mut self.entities
97 }
98
99 /// Look up an entity definition by name.
100 #[must_use]
101 pub fn get_entity(&self, name: &str) -> Option<&Entity> {
102 self.entities.iter().find(|e| e.name == name)
103 }
104
105 /// Returns the number of entity definitions in this config.
106 #[must_use]
107 pub fn entity_count(&self) -> usize {
108 self.entities.len()
109 }
110}
111
112impl std::fmt::Display for Config {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 write!(
115 f,
116 "Config('{}', {} entities",
117 self.name,
118 self.entities.len()
119 )?;
120 if let Some(patterns) = &self.default_path_patterns {
121 write!(f, ", {} patterns", patterns.len())?;
122 }
123 write!(f, ")")
124 }
125}