1use crate::models::{Platform, OS};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum PackageDefinition {
13 Simple(Vec<String>),
15 Complex(ComplexPackageDefinition),
17}
18
19impl PackageDefinition {
20 pub fn get_sources(&self) -> Vec<&str> {
22 match self {
23 PackageDefinition::Simple(sources) => sources.iter().map(|s| s.as_str()).collect(),
24 PackageDefinition::Complex(complex) => complex.get_sources(),
25 }
26 }
27
28 pub fn get_source_config(&self, source: &str) -> Option<&SourceSpecificConfig> {
30 match self {
31 PackageDefinition::Simple(_) => None,
32 PackageDefinition::Complex(complex) => complex.get_source_config(source),
33 }
34 }
35
36 pub fn is_available_in(&self, source: &str) -> bool {
38 self.get_sources().contains(&source)
39 }
40
41 pub fn get_description(&self) -> Option<&str> {
43 match self {
44 PackageDefinition::Simple(_) => None,
45 PackageDefinition::Complex(complex) => complex.description.as_deref(),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, Default)]
51#[serde(default)]
52#[non_exhaustive]
53pub struct ComplexPackageDefinition {
54 #[serde(rename = "_description", skip_serializing_if = "Option::is_none")]
56 pub description: Option<String>,
57
58 #[serde(rename = "_sources", skip_serializing_if = "Option::is_none")]
60 pub sources: Option<Vec<String>>,
61
62 #[serde(rename = "_platforms", skip_serializing_if = "Option::is_none")]
64 pub platforms: Option<Vec<String>>,
65
66 #[serde(rename = "_aliases", skip_serializing_if = "Option::is_none")]
68 pub aliases: Option<Vec<String>>,
69
70 #[serde(flatten)]
72 pub source_configs: HashMap<String, SourceSpecificConfig>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(untagged)]
78pub enum SourceSpecificConfig {
79 Name(String),
81 Complex(SourceConfig),
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct SourceConfig {
87 pub name: Option<String>,
89 pub pre: Option<String>,
91 pub post: Option<String>,
93 pub prefix: Option<String>,
95 pub install_suffix: Option<String>,
97}
98
99pub type SourcesDefinition = HashMap<String, SourceDefinition>;
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SourceDefinition {
104 pub emoji: String,
106 pub install: String,
108 pub check: String,
110 pub prefix: Option<String>,
112 #[serde(rename = "_overrides", skip_serializing_if = "Option::is_none")]
114 pub overrides: Option<HashMap<String, PlatformOverride>>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct PlatformOverride {
119 pub install: Option<String>,
120 pub check: Option<String>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ConfigDefinition {
126 pub sources: Vec<String>,
128 pub packages: Vec<String>,
130 #[serde(rename = "_settings", skip_serializing_if = "Option::is_none")]
132 pub settings: Option<ConfigSettings>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ConfigSettings {
137 #[serde(default)]
139 pub auto_update: bool,
140 #[serde(default = "default_parallel_installs")]
142 pub parallel_installs: u8,
143 #[serde(default = "default_true")]
145 pub confirm_before_install: bool,
146}
147
148fn default_parallel_installs() -> u8 {
149 3
150}
151fn default_true() -> bool {
152 true
153}
154
155impl ComplexPackageDefinition {
156 pub fn with_sources(sources: Vec<String>) -> Self {
158 Self {
159 sources: Some(sources),
160 ..Default::default()
161 }
162 }
163
164 pub fn set_platforms(&mut self, platforms: Vec<String>) {
166 self.platforms = Some(platforms);
167 }
168
169 pub fn set_aliases(&mut self, aliases: Vec<String>) {
171 self.aliases = Some(aliases);
172 }
173
174 pub fn set_description(&mut self, description: String) {
176 self.description = Some(description);
177 }
178
179 pub fn get_sources(&self) -> Vec<&str> {
181 let mut all_sources = Vec::new();
182
183 if let Some(sources) = &self.sources {
185 all_sources.extend(sources.iter().map(|s| s.as_str()));
186 }
187
188 all_sources.extend(self.source_configs.keys().map(|s| s.as_str()));
190
191 all_sources
192 }
193
194 pub fn get_source_config(&self, source: &str) -> Option<&SourceSpecificConfig> {
196 self.source_configs.get(source)
197 }
198
199 pub fn is_available_in(&self, source: &str) -> bool {
201 self.get_sources().contains(&source)
202 }
203}
204
205impl SourceDefinition {
206 pub fn get_install_command(&self, platform: &Platform) -> &str {
208 if let Some(overrides) = &self.overrides {
209 let platform_key = match platform.os {
210 OS::Windows => "windows",
211 OS::Linux => "linux",
212 OS::Macos => "macos",
213 };
214
215 if let Some(platform_override) = overrides.get(platform_key) {
216 if let Some(install) = &platform_override.install {
217 return install;
218 }
219 }
220 }
221 &self.install
222 }
223
224 pub fn get_check_command(&self, platform: &Platform) -> &str {
226 if let Some(overrides) = &self.overrides {
227 let platform_key = match platform.os {
228 OS::Windows => "windows",
229 OS::Linux => "linux",
230 OS::Macos => "macos",
231 };
232
233 if let Some(platform_override) = overrides.get(platform_key) {
234 if let Some(check) = &platform_override.check {
235 return check;
236 }
237 }
238 }
239 &self.check
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_package_definition_simple_format() {
249 let ccl = r#"
251bat =
252 = brew
253 = scoop
254 = pacman
255 = nix
256"#;
257 let packages: HashMap<String, PackageDefinition> = crate::parse_ccl_to(ccl).unwrap();
258 let def = packages.get("bat").unwrap();
259
260 assert!(def.is_available_in("brew"));
261 assert!(def.is_available_in("scoop"));
262 assert!(def.is_available_in("pacman"));
263 assert!(def.is_available_in("nix"));
264
265 assert!(def.get_source_config("brew").is_none());
267
268 let sources = def.get_sources();
270 assert_eq!(sources.len(), 4);
271 assert!(sources.contains(&"brew"));
272 assert!(sources.contains(&"scoop"));
273 assert!(sources.contains(&"pacman"));
274 assert!(sources.contains(&"nix"));
275 }
276
277 #[test]
278 fn test_package_definition_complex_format() {
279 let ccl = r#"
280ripgrep =
281 brew = gh
282 _sources =
283 = scoop
284 = apt
285 = pacman
286 = nix
287"#;
288 let packages: HashMap<String, PackageDefinition> = crate::parse_ccl_to(ccl).unwrap();
289 let def = packages.get("ripgrep").unwrap();
290
291 assert!(def.is_available_in("brew"));
292 assert!(def.is_available_in("scoop"));
293 assert!(def.get_source_config("brew").is_some());
294
295 let sources = def.get_sources();
297 assert!(sources.contains(&"scoop"));
298 assert!(sources.contains(&"apt"));
299 assert!(sources.contains(&"pacman"));
300 assert!(sources.contains(&"nix"));
301 assert!(sources.contains(&"brew"));
302 }
303
304 #[test]
305 fn test_source_definition() {
306 let ccl = r#"
307emoji = 🍺
308install = brew install {package}
309check = brew leaves --installed-on-request
310"#;
311 let def: SourceDefinition = sickle::from_str(ccl).unwrap();
312
313 assert_eq!(def.emoji, "🍺");
314 assert!(def.install.contains("{package}"));
315 }
316
317 #[test]
318 fn test_package_with_description() {
319 let ccl = r#"
320bat =
321 _description = A cat clone with syntax highlighting.
322 _sources =
323 = brew
324 = scoop
325"#;
326 let packages: HashMap<String, PackageDefinition> = crate::parse_ccl_to(ccl).unwrap();
327 let def = packages.get("bat").unwrap();
328
329 assert_eq!(
331 def.get_description(),
332 Some("A cat clone with syntax highlighting.")
333 );
334
335 assert!(def.is_available_in("brew"));
337 assert!(def.is_available_in("scoop"));
338 }
339
340 #[test]
341 fn test_simple_package_no_description() {
342 let ccl = r#"
343jq =
344 = brew
345 = apt
346"#;
347 let packages: HashMap<String, PackageDefinition> = crate::parse_ccl_to(ccl).unwrap();
348 let def = packages.get("jq").unwrap();
349
350 assert_eq!(def.get_description(), None);
352 }
353}