Skip to main content

cargo_image_runner/config/
loader.rs

1use super::Config;
2use crate::core::error::{Error, Result};
3use cargo_metadata::MetadataCommand;
4use std::path::{Path, PathBuf};
5
6/// Configuration loader that supports multiple sources.
7pub struct ConfigLoader {
8    /// Path to workspace root.
9    workspace_root: Option<PathBuf>,
10    /// Path to standalone config file.
11    config_file: Option<PathBuf>,
12    /// Whether to load from Cargo.toml metadata.
13    use_cargo_metadata: bool,
14}
15
16impl ConfigLoader {
17    /// Create a new configuration loader.
18    pub fn new() -> Self {
19        Self {
20            workspace_root: None,
21            config_file: None,
22            use_cargo_metadata: true,
23        }
24    }
25
26    /// Set the workspace root directory.
27    pub fn workspace_root(mut self, root: impl Into<PathBuf>) -> Self {
28        self.workspace_root = Some(root.into());
29        self
30    }
31
32    /// Set a standalone configuration file path.
33    pub fn config_file(mut self, path: impl Into<PathBuf>) -> Self {
34        self.config_file = Some(path.into());
35        self
36    }
37
38    /// Disable loading from Cargo.toml metadata.
39    pub fn no_cargo_metadata(mut self) -> Self {
40        self.use_cargo_metadata = false;
41        self
42    }
43
44    /// Load configuration from all enabled sources.
45    ///
46    /// Priority (later sources override earlier):
47    /// 1. Default values
48    /// 2. Cargo.toml metadata
49    /// 3. Standalone TOML file
50    pub fn load(self) -> Result<(Config, PathBuf)> {
51        let mut config = Config::default();
52        let workspace_root;
53
54        // Load from Cargo metadata if enabled
55        if self.use_cargo_metadata {
56            let (root, cargo_config) = self.load_cargo_metadata()?;
57            workspace_root = root;
58            config = Self::merge_configs(config, cargo_config);
59        } else {
60            workspace_root = self
61                .workspace_root
62                .clone()
63                .ok_or_else(|| Error::config("workspace root not specified"))?;
64        }
65
66        // Load from standalone file if specified
67        if let Some(ref config_path) = self.config_file {
68            let file_config = self.load_toml_file(config_path)?;
69            config = Self::merge_configs(config, file_config);
70        }
71
72        Ok((config, workspace_root))
73    }
74
75    /// Load configuration from Cargo.toml metadata.
76    ///
77    /// Priority: package metadata > workspace metadata > defaults.
78    fn load_cargo_metadata(&self) -> Result<(PathBuf, Config)> {
79        let manifest_path = std::env::var("CARGO_MANIFEST_PATH").ok();
80
81        let mut cmd = MetadataCommand::new();
82        if let Some(manifest_path) = manifest_path {
83            cmd.manifest_path(manifest_path);
84        }
85
86        let metadata = cmd.exec()?;
87        let workspace_root = metadata.workspace_root.clone().into_std_path_buf();
88
89        // Parse workspace metadata: [workspace.metadata.image-runner]
90        let workspace_config = if let Some(ws_value) = metadata.workspace_metadata.get("image-runner") {
91            Some(
92                serde_json::from_value::<Config>(ws_value.clone())
93                    .map_err(|e| Error::config(format!("invalid workspace metadata: {}", e)))?,
94            )
95        } else {
96            None
97        };
98
99        // Try to find the package metadata
100        let pkg_name = std::env::var("CARGO_PKG_NAME").ok();
101        let package = if let Some(ref pkg_name) = pkg_name {
102            metadata
103                .packages
104                .iter()
105                .find(|p| &p.name == pkg_name)
106                .or_else(|| metadata.root_package())
107        } else {
108            metadata.root_package()
109        };
110
111        // Parse package metadata: [package.metadata.image-runner]
112        let package_config = if let Some(package) = package {
113            if let Some(metadata_value) = package.metadata.get("image-runner") {
114                Some(
115                    serde_json::from_value::<Config>(metadata_value.clone())
116                        .map_err(|e| Error::config(format!("invalid Cargo.toml metadata: {}", e)))?,
117                )
118            } else {
119                None
120            }
121        } else {
122            None
123        };
124
125        // Merge: defaults <- workspace <- package
126        let mut config = Config::default();
127        if let Some(ws_config) = workspace_config {
128            config = Self::merge_configs(config, ws_config);
129        }
130        if let Some(pkg_config) = package_config {
131            config = Self::merge_configs(config, pkg_config);
132        }
133
134        Ok((workspace_root, config))
135    }
136
137    /// Load configuration from a standalone TOML file.
138    fn load_toml_file(&self, path: &Path) -> Result<Config> {
139        let content = std::fs::read_to_string(path)
140            .map_err(|e| Error::config(format!("failed to read config file: {}", e)))?;
141
142        toml::from_str(&content)
143            .map_err(|e| Error::config(format!("failed to parse TOML config: {}", e)))
144    }
145
146    /// Merge two configurations, with `override_config` taking precedence.
147    fn merge_configs(mut base: Config, override_cfg: Config) -> Config {
148        // For now, we do a simple override strategy
149        // In Phase 5, we'll implement more sophisticated merging
150        // that handles individual fields properly
151
152        // Simple merge: non-default values from override take precedence
153        // This is a placeholder - full implementation in Phase 5
154        base.boot = override_cfg.boot;
155        base.bootloader = override_cfg.bootloader;
156        base.image = override_cfg.image;
157        base.runner = override_cfg.runner;
158        base.test = override_cfg.test;
159        base.run = override_cfg.run;
160
161        // Merge variables
162        for (k, v) in override_cfg.variables {
163            base.variables.insert(k, v);
164        }
165
166        base
167    }
168}
169
170impl Default for ConfigLoader {
171    fn default() -> Self {
172        Self::new()
173    }
174}