santa_data/
schemas.rs

1// Schema-based data structures for Santa Package Manager
2// These structs match the YAML schemas defined in /data/*.yaml files
3
4use crate::models::{Platform, OS};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Package definition matching package_schema.yaml
9/// Supports both simple array format and complex object format
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum PackageDefinition {
13    /// Simple array format: just a list of source names
14    Simple(Vec<String>),
15    /// Complex object format: with metadata and source-specific configs
16    Complex(ComplexPackageDefinition),
17}
18
19impl PackageDefinition {
20    /// Get all sources where this package is available
21    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    /// Get source-specific configuration for a source
29    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    /// Check if package is available in a specific source
37    pub fn is_available_in(&self, source: &str) -> bool {
38        self.get_sources().contains(&source)
39    }
40
41    /// Get the package description if available
42    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    /// Short description of the package
55    #[serde(rename = "_description", skip_serializing_if = "Option::is_none")]
56    pub description: Option<String>,
57
58    /// List of sources where package is available with same name as key
59    #[serde(rename = "_sources", skip_serializing_if = "Option::is_none")]
60    pub sources: Option<Vec<String>>,
61
62    /// Platforms where this package is available
63    #[serde(rename = "_platforms", skip_serializing_if = "Option::is_none")]
64    pub platforms: Option<Vec<String>>,
65
66    /// Alternative names for search and discovery
67    #[serde(rename = "_aliases", skip_serializing_if = "Option::is_none")]
68    pub aliases: Option<Vec<String>>,
69
70    /// Source-specific configurations (flatten other fields)
71    #[serde(flatten)]
72    pub source_configs: HashMap<String, SourceSpecificConfig>,
73}
74
75/// Source-specific configuration for a package
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(untagged)]
78pub enum SourceSpecificConfig {
79    /// Simple name override
80    Name(String),
81    /// Complex configuration with hooks and modifications
82    Complex(SourceConfig),
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct SourceConfig {
87    /// Override package name for this source
88    pub name: Option<String>,
89    /// Command to run before installation
90    pub pre: Option<String>,
91    /// Command to run after successful installation  
92    pub post: Option<String>,
93    /// String to prepend to package name during installation
94    pub prefix: Option<String>,
95    /// String to append to the install command
96    pub install_suffix: Option<String>,
97}
98
99/// Sources configuration matching sources_schema.yaml
100pub type SourcesDefinition = HashMap<String, SourceDefinition>;
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SourceDefinition {
104    /// Emoji icon to represent this source
105    pub emoji: String,
106    /// Command template to install packages
107    pub install: String,
108    /// Command to list installed packages from this source
109    pub check: String,
110    /// String to prepend to package names (optional)
111    pub prefix: Option<String>,
112    /// Platform-specific command overrides
113    #[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/// Configuration matching config_schema.yaml
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ConfigDefinition {
126    /// List of package sources to use (in priority order)
127    pub sources: Vec<String>,
128    /// List of packages to install/manage
129    pub packages: Vec<String>,
130    /// Advanced configuration options
131    #[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    /// Automatically update packages
138    #[serde(default)]
139    pub auto_update: bool,
140    /// Maximum parallel package installations
141    #[serde(default = "default_parallel_installs")]
142    pub parallel_installs: u8,
143    /// Ask for confirmation before installing packages
144    #[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    /// Create a new ComplexPackageDefinition with the given sources
157    pub fn with_sources(sources: Vec<String>) -> Self {
158        Self {
159            sources: Some(sources),
160            ..Default::default()
161        }
162    }
163
164    /// Set the platforms for this package definition
165    pub fn set_platforms(&mut self, platforms: Vec<String>) {
166        self.platforms = Some(platforms);
167    }
168
169    /// Set the aliases for this package definition
170    pub fn set_aliases(&mut self, aliases: Vec<String>) {
171        self.aliases = Some(aliases);
172    }
173
174    /// Set the description for this package definition
175    pub fn set_description(&mut self, description: String) {
176        self.description = Some(description);
177    }
178
179    /// Get all sources where this package is available
180    pub fn get_sources(&self) -> Vec<&str> {
181        let mut all_sources = Vec::new();
182
183        // Add sources from _sources array
184        if let Some(sources) = &self.sources {
185            all_sources.extend(sources.iter().map(|s| s.as_str()));
186        }
187
188        // Add sources from explicit configurations
189        all_sources.extend(self.source_configs.keys().map(|s| s.as_str()));
190
191        all_sources
192    }
193
194    /// Get source-specific configuration for a source
195    pub fn get_source_config(&self, source: &str) -> Option<&SourceSpecificConfig> {
196        self.source_configs.get(source)
197    }
198
199    /// Check if package is available in a specific source
200    pub fn is_available_in(&self, source: &str) -> bool {
201        self.get_sources().contains(&source)
202    }
203}
204
205impl SourceDefinition {
206    /// Get the appropriate command for the current platform
207    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    /// Get the appropriate check command for the current platform
225    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        // Test simple array format using our custom ccl-parser
250        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        // Simple format should not have source configs
266        assert!(def.get_source_config("brew").is_none());
267
268        // Check that all sources are present
269        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        // Check that sources list includes all sources
296        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        // Should have description
330        assert_eq!(
331            def.get_description(),
332            Some("A cat clone with syntax highlighting.")
333        );
334
335        // Should still have sources
336        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        // Simple format has no description
351        assert_eq!(def.get_description(), None);
352    }
353}