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}