zoi/project/
config.rs

1use anyhow::{Result, anyhow};
2use serde::{Deserialize, Deserializer};
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug, Deserialize, Default)]
8pub struct ProjectLocalConfig {
9    #[serde(default)]
10    pub local: bool,
11}
12
13#[derive(Debug, Deserialize)]
14#[allow(dead_code)]
15pub struct ProjectConfig {
16    pub name: String,
17    #[serde(default)]
18    pub registries: Option<Vec<String>>,
19    #[serde(default)]
20    pub packages: Vec<PackageCheck>,
21    #[serde(default, deserialize_with = "deserialize_pkgs")]
22    pub pkgs: Vec<String>,
23    #[serde(default)]
24    pub config: ProjectLocalConfig,
25    #[serde(default)]
26    pub commands: Vec<CommandSpec>,
27    #[serde(default)]
28    pub environments: Vec<EnvironmentSpec>,
29}
30
31#[derive(Debug, Deserialize)]
32#[serde(untagged)]
33enum PkgOrPkgWithVersion {
34    Name(String),
35    Versioned(HashMap<String, String>),
36}
37
38fn deserialize_pkgs<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
39where
40    D: Deserializer<'de>,
41{
42    let v = Vec::<PkgOrPkgWithVersion>::deserialize(deserializer)?;
43    Ok(v.into_iter()
44        .flat_map(|item| {
45            let strings: Vec<String> = match item {
46                PkgOrPkgWithVersion::Name(name) => vec![name],
47                PkgOrPkgWithVersion::Versioned(map) => map
48                    .into_iter()
49                    .map(|(k, v)| format!("{}@{}", k, v))
50                    .collect(),
51            };
52            strings
53        })
54        .collect())
55}
56
57#[derive(Debug, Deserialize)]
58pub struct PackageCheck {
59    pub name: String,
60    pub check: String,
61}
62
63#[derive(Debug, Deserialize, Clone)]
64#[serde(untagged)]
65pub enum PlatformOrString {
66    String(String),
67    Platform(HashMap<String, String>),
68}
69
70#[derive(Debug, Deserialize, Clone)]
71#[serde(untagged)]
72pub enum PlatformOrStringVec {
73    StringVec(Vec<String>),
74    Platform(HashMap<String, Vec<String>>),
75}
76
77#[derive(Debug, Deserialize, Clone)]
78#[serde(untagged)]
79pub enum PlatformOrEnvMap {
80    EnvMap(HashMap<String, String>),
81    Platform(HashMap<String, HashMap<String, String>>),
82}
83
84impl Default for PlatformOrEnvMap {
85    fn default() -> Self {
86        PlatformOrEnvMap::EnvMap(HashMap::new())
87    }
88}
89
90#[derive(Debug, Deserialize, Clone)]
91pub struct CommandSpec {
92    pub cmd: String,
93    pub run: PlatformOrString,
94    #[serde(default)]
95    pub env: PlatformOrEnvMap,
96}
97
98#[derive(Debug, Deserialize, Clone)]
99pub struct EnvironmentSpec {
100    pub name: String,
101    pub cmd: String,
102    pub run: PlatformOrStringVec,
103    #[serde(default)]
104    pub env: PlatformOrEnvMap,
105}
106
107pub fn load() -> Result<ProjectConfig> {
108    let config_path = Path::new("zoi.yaml");
109    if !config_path.exists() {
110        return Err(anyhow!(
111            "No 'zoi.yaml' file found in the current directory."
112        ));
113    }
114
115    let content = fs::read_to_string(config_path)?;
116    let config: ProjectConfig = serde_yaml::from_str(&content)?;
117    Ok(config)
118}
119
120pub fn add_packages_to_config(packages: &[String]) -> Result<()> {
121    let config_path = Path::new("zoi.yaml");
122    if !config_path.exists() {
123        return Err(anyhow!(
124            "No 'zoi.yaml' file found in the current directory."
125        ));
126    }
127
128    let content = fs::read_to_string(config_path)?;
129    let mut yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)?;
130
131    if let Some(mapping) = yaml_value.as_mapping_mut() {
132        let pkgs_key = serde_yaml::Value::String("pkgs".to_string());
133        let pkgs_list = mapping
134            .entry(pkgs_key)
135            .or_insert_with(|| serde_yaml::Value::Sequence(Vec::new()));
136
137        if let Some(sequence) = pkgs_list.as_sequence_mut() {
138            for package in packages {
139                let new_pkg_value = serde_yaml::Value::String(package.clone());
140                if !sequence.contains(&new_pkg_value) {
141                    sequence.push(new_pkg_value);
142                }
143            }
144        }
145    }
146
147    let new_content = serde_yaml::to_string(&yaml_value)?;
148    fs::write(config_path, new_content)?;
149
150    Ok(())
151}
152
153pub fn remove_packages_from_config(packages_to_remove: &[String]) -> Result<()> {
154    let config_path = Path::new("zoi.yaml");
155    if !config_path.exists() {
156        return Ok(());
157    }
158
159    let content = fs::read_to_string(config_path)?;
160    let mut yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)?;
161
162    if let Some(mapping) = yaml_value.as_mapping_mut()
163        && let Some(pkgs_list) = mapping.get_mut("pkgs")
164        && let Some(sequence) = pkgs_list.as_sequence_mut()
165    {
166        let packages_to_remove_names: Vec<_> = packages_to_remove
167            .iter()
168            .map(|p| {
169                crate::pkg::resolve::parse_source_string(p)
170                    .map(|req| req.name)
171                    .unwrap_or_else(|_| p.to_string())
172            })
173            .collect();
174
175        sequence.retain(|v| {
176            if let Some(s) = v.as_str() {
177                if let Ok(req) = crate::pkg::resolve::parse_source_string(s) {
178                    !packages_to_remove_names.contains(&req.name)
179                } else {
180                    true
181                }
182            } else {
183                true
184            }
185        });
186    }
187
188    let new_content = serde_yaml::to_string(&yaml_value)?;
189    fs::write(config_path, new_content)?;
190
191    Ok(())
192}