Skip to main content

sbox/config/
load.rs

1use std::fs;
2use std::path::{Component, Path, PathBuf};
3
4use crate::error::SboxError;
5
6use super::model::Config;
7use super::validate::validate_config;
8
9#[derive(Debug, Clone)]
10pub struct LoadOptions {
11    pub workspace: Option<PathBuf>,
12    pub config: Option<PathBuf>,
13}
14
15#[derive(Debug, Clone)]
16pub struct LoadedConfig {
17    pub invocation_dir: PathBuf,
18    pub workspace_root: PathBuf,
19    pub config_path: PathBuf,
20    pub config: Config,
21}
22
23pub fn load_config(options: &LoadOptions) -> Result<LoadedConfig, SboxError> {
24    let invocation_dir =
25        std::env::current_dir().map_err(|source| SboxError::CurrentDirectory { source })?;
26
27    let config_path = match options.config.as_deref() {
28        Some(path) => absolutize_from(path, &invocation_dir),
29        None => {
30            let search_root = absolutize_path(options.workspace.as_deref(), &invocation_dir)
31                .unwrap_or_else(|| invocation_dir.clone());
32            find_default_config_path(&search_root)?
33        }
34    };
35
36    if !config_path.exists() {
37        return Err(SboxError::ConfigNotFound(config_path));
38    }
39
40    let raw = fs::read_to_string(&config_path).map_err(|source| SboxError::ConfigRead {
41        path: config_path.clone(),
42        source,
43    })?;
44
45    let mut config = serde_yaml::from_str::<Config>(&raw).map_err(|source| SboxError::ConfigParse {
46        path: config_path.clone(),
47        source,
48    })?;
49
50    super::package_manager::elaborate(&mut config)?;
51    validate_config(&config)?;
52
53    let config_dir = config_path
54        .parent()
55        .map(Path::to_path_buf)
56        .unwrap_or_else(|| invocation_dir.clone());
57
58    let workspace_root = match options.workspace.as_deref() {
59        Some(path) => absolutize_from(path, &invocation_dir),
60        None => {
61            let configured_root = config
62                .workspace
63                .as_ref()
64                .and_then(|workspace| workspace.root.as_deref());
65
66            absolutize_path(configured_root, &config_dir).unwrap_or(config_dir)
67        }
68    };
69
70    Ok(LoadedConfig {
71        invocation_dir,
72        workspace_root,
73        config_path,
74        config,
75    })
76}
77
78fn absolutize_path(path: Option<&Path>, base: &Path) -> Option<PathBuf> {
79    path.map(|path| absolutize_from(path, base))
80}
81
82fn absolutize_from(path: &Path, base: &Path) -> PathBuf {
83    let combined = if path.is_absolute() {
84        path.to_path_buf()
85    } else {
86        base.join(path)
87    };
88
89    normalize_path(&combined)
90}
91
92fn find_default_config_path(start: &Path) -> Result<PathBuf, SboxError> {
93    for directory in start.ancestors() {
94        let candidate = directory.join("sbox.yaml");
95        if candidate.exists() {
96            return Ok(candidate);
97        }
98    }
99
100    Err(SboxError::ConfigNotFound(start.join("sbox.yaml")))
101}
102
103fn normalize_path(path: &Path) -> PathBuf {
104    let mut normalized = PathBuf::new();
105
106    for component in path.components() {
107        match component {
108            Component::CurDir => {}
109            Component::ParentDir => {
110                normalized.pop();
111            }
112            other => normalized.push(other.as_os_str()),
113        }
114    }
115
116    if normalized.as_os_str().is_empty() {
117        PathBuf::from(".")
118    } else {
119        normalized
120    }
121}