ml_cellar/
cellar.rs

1use serde::{Deserialize, Serialize};
2use std::env;
3use std::path::{Path, PathBuf};
4
5/// Configuration for the ml-cellar repository.
6#[derive(Debug, Deserialize, Serialize, Default)]
7#[serde(default)]
8pub struct CellarConfig {
9    pub ml_cellar: MlCellarConfig,
10    pub aws: AWSConfig,
11}
12
13/// Configuration specific to ml-cellar behavior.
14#[derive(Debug, Deserialize, Serialize, Default)]
15#[serde(default)]
16pub struct MlCellarConfig {
17    /// Whether to use a custom transfer agent for Git LFS (e.g., for AWS S3 backend).
18    ///
19    /// When enabled, ml-cellar can use alternative storage backends instead of
20    /// the default Git LFS storage.
21    pub use_custom_transfer_agent: bool,
22}
23
24/// Configuration for AWS integration.
25#[derive(Debug, Deserialize, Serialize, Default)]
26#[serde(default)]
27pub struct AWSConfig {
28    /// AWS profile name to use for authentication.
29    /// If specified, ml-cellar will use credentials from this AWS CLI profile.
30    pub profile: Option<String>,
31}
32
33/// Loads the cellar configuration by searching for `.mlcellar.toml` in the directory tree.
34///
35/// This function searches upward from the given path through parent directories
36/// until it finds a `.mlcellar.toml` file. This allows ml-cellar commands to be
37/// run from any subdirectory within a repository.
38///
39/// # Arguments
40///
41/// - `path` - The starting path to search from (can be a file or directory)
42///
43/// # Returns
44///
45/// A tuple `(CellarConfig, PathBuf)` where:
46/// - `CellarConfig`: The parsed configuration from `.mlcellar.toml`
47/// - `PathBuf`: The absolute path to the directory containing `.mlcellar.toml`
48///   (this is the root of the ml-cellar repository)
49///
50/// # Panics
51///
52/// Panics if:
53/// - `.mlcellar.toml` is not found in any parent directory up to the filesystem root
54/// - The configuration file cannot be read
55/// - The TOML content cannot be parsed
56///
57pub fn load_cellar_config(path: &Path) -> (CellarConfig, PathBuf) {
58    // Set directory to start searching
59    let relative_dir = if path.is_dir() {
60        path.to_path_buf()
61    } else {
62        path.parent().unwrap().to_path_buf()
63    };
64
65    // Convert to absolute path
66    let mut absolute_dir = if relative_dir.is_absolute() {
67        relative_dir
68    } else {
69        env::current_dir().unwrap().join(relative_dir)
70    };
71
72    // Search for .mlcellar.toml in the directory and its parents
73    loop {
74        let candidate = absolute_dir.join(".mlcellar.toml");
75        if candidate.is_file() {
76            // Found .mlcellar.toml, read and parse it
77            log::info!("Loading config from {:?}", candidate);
78            let config_content = std::fs::read_to_string(&candidate).unwrap();
79            return (toml::from_str(&config_content).unwrap(), absolute_dir);
80        }
81
82        match absolute_dir.parent() {
83            Some(parent) => absolute_dir = parent.to_path_buf(),
84            None => {
85                log::error!(
86                    ".mlcellar.toml not found; reached filesystem root at {:?}\n\
87                     Please ensure that .mlcellar.toml exists in the directory tree starting from the directory of the provided path.",
88                    absolute_dir
89                );
90                panic!(
91                    ".mlcellar.toml not found in the directory tree starting from {:?}",
92                    path
93                );
94            }
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use tempfile::TempDir;
103    use toml::to_string_pretty;
104
105    #[test]
106    fn test_load_cellar_config() {
107        let temp = TempDir::new().unwrap();
108        let root_directory = temp.path();
109
110        let toml_str = to_string_pretty(&CellarConfig::default())
111            .expect("failed to serialize cellar config to TOML");
112        std::fs::write(root_directory.join(".mlcellar.toml"), toml_str)
113            .expect("failed to write .mlcellar.toml");
114
115        let (config, config_dir) = load_cellar_config(root_directory);
116        assert_eq!(config_dir, root_directory);
117        assert!(!config.ml_cellar.use_custom_transfer_agent);
118    }
119}