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
42#[derive(Debug, Clone, Serialize, Deserialize, Default)]
43#[serde(default)]
44pub struct ComplexPackageDefinition {
45    /// List of sources where package is available with same name as key
46    #[serde(rename = "_sources", skip_serializing_if = "Option::is_none")]
47    pub sources: Option<Vec<String>>,
48
49    /// Platforms where this package is available
50    #[serde(rename = "_platforms", skip_serializing_if = "Option::is_none")]
51    pub platforms: Option<Vec<String>>,
52
53    /// Alternative names for search and discovery
54    #[serde(rename = "_aliases", skip_serializing_if = "Option::is_none")]
55    pub aliases: Option<Vec<String>>,
56
57    /// Source-specific configurations (flatten other fields)
58    #[serde(flatten)]
59    pub source_configs: HashMap<String, SourceSpecificConfig>,
60}
61
62/// Source-specific configuration for a package
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(untagged)]
65pub enum SourceSpecificConfig {
66    /// Simple name override
67    Name(String),
68    /// Complex configuration with hooks and modifications
69    Complex(SourceConfig),
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct SourceConfig {
74    /// Override package name for this source
75    pub name: Option<String>,
76    /// Command to run before installation
77    pub pre: Option<String>,
78    /// Command to run after successful installation  
79    pub post: Option<String>,
80    /// String to prepend to package name during installation
81    pub prefix: Option<String>,
82    /// String to append to the install command
83    pub install_suffix: Option<String>,
84}
85
86/// Sources configuration matching sources_schema.yaml
87pub type SourcesDefinition = HashMap<String, SourceDefinition>;
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct SourceDefinition {
91    /// Emoji icon to represent this source
92    pub emoji: String,
93    /// Command template to install packages
94    pub install: String,
95    /// Command to list installed packages from this source
96    pub check: String,
97    /// String to prepend to package names (optional)
98    pub prefix: Option<String>,
99    /// Platform-specific command overrides
100    #[serde(rename = "_overrides", skip_serializing_if = "Option::is_none")]
101    pub overrides: Option<HashMap<String, PlatformOverride>>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct PlatformOverride {
106    pub install: Option<String>,
107    pub check: Option<String>,
108}
109
110/// Configuration matching config_schema.yaml
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ConfigDefinition {
113    /// List of package sources to use (in priority order)
114    pub sources: Vec<String>,
115    /// List of packages to install/manage
116    pub packages: Vec<String>,
117    /// Advanced configuration options
118    #[serde(rename = "_settings", skip_serializing_if = "Option::is_none")]
119    pub settings: Option<ConfigSettings>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ConfigSettings {
124    /// Automatically update packages
125    #[serde(default)]
126    pub auto_update: bool,
127    /// Maximum parallel package installations
128    #[serde(default = "default_parallel_installs")]
129    pub parallel_installs: u8,
130    /// Ask for confirmation before installing packages
131    #[serde(default = "default_true")]
132    pub confirm_before_install: bool,
133}
134
135fn default_parallel_installs() -> u8 {
136    3
137}
138fn default_true() -> bool {
139    true
140}
141
142impl ComplexPackageDefinition {
143    /// Get all sources where this package is available
144    pub fn get_sources(&self) -> Vec<&str> {
145        let mut all_sources = Vec::new();
146
147        // Add sources from _sources array
148        if let Some(sources) = &self.sources {
149            all_sources.extend(sources.iter().map(|s| s.as_str()));
150        }
151
152        // Add sources from explicit configurations
153        all_sources.extend(self.source_configs.keys().map(|s| s.as_str()));
154
155        all_sources
156    }
157
158    /// Get source-specific configuration for a source
159    pub fn get_source_config(&self, source: &str) -> Option<&SourceSpecificConfig> {
160        self.source_configs.get(source)
161    }
162
163    /// Check if package is available in a specific source
164    pub fn is_available_in(&self, source: &str) -> bool {
165        self.get_sources().contains(&source)
166    }
167}
168
169impl SourceDefinition {
170    /// Get the appropriate command for the current platform
171    pub fn get_install_command(&self, platform: &Platform) -> &str {
172        if let Some(overrides) = &self.overrides {
173            let platform_key = match platform.os {
174                OS::Windows => "windows",
175                OS::Linux => "linux",
176                OS::Macos => "macos",
177            };
178
179            if let Some(platform_override) = overrides.get(platform_key) {
180                if let Some(install) = &platform_override.install {
181                    return install;
182                }
183            }
184        }
185        &self.install
186    }
187
188    /// Get the appropriate check command for the current platform
189    pub fn get_check_command(&self, platform: &Platform) -> &str {
190        if let Some(overrides) = &self.overrides {
191            let platform_key = match platform.os {
192                OS::Windows => "windows",
193                OS::Linux => "linux",
194                OS::Macos => "macos",
195            };
196
197            if let Some(platform_override) = overrides.get(platform_key) {
198                if let Some(check) = &platform_override.check {
199                    return check;
200                }
201            }
202        }
203        &self.check
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_package_definition_simple_format() {
213        // Test simple array format using our custom ccl-parser
214        let ccl = r#"
215bat =
216  = brew
217  = scoop
218  = pacman
219  = nix
220"#;
221        let packages: HashMap<String, PackageDefinition> = crate::parse_ccl_to(ccl).unwrap();
222        let def = packages.get("bat").unwrap();
223
224        assert!(def.is_available_in("brew"));
225        assert!(def.is_available_in("scoop"));
226        assert!(def.is_available_in("pacman"));
227        assert!(def.is_available_in("nix"));
228
229        // Simple format should not have source configs
230        assert!(def.get_source_config("brew").is_none());
231
232        // Check that all sources are present
233        let sources = def.get_sources();
234        assert_eq!(sources.len(), 4);
235        assert!(sources.contains(&"brew"));
236        assert!(sources.contains(&"scoop"));
237        assert!(sources.contains(&"pacman"));
238        assert!(sources.contains(&"nix"));
239    }
240
241    #[test]
242    fn test_package_definition_complex_format() {
243        let ccl = r#"
244ripgrep =
245  brew = gh
246  _sources =
247    = scoop
248    = apt
249    = pacman
250    = nix
251"#;
252        let packages: HashMap<String, PackageDefinition> = crate::parse_ccl_to(ccl).unwrap();
253        let def = packages.get("ripgrep").unwrap();
254
255        assert!(def.is_available_in("brew"));
256        assert!(def.is_available_in("scoop"));
257        assert!(def.get_source_config("brew").is_some());
258
259        // Check that sources list includes all sources
260        let sources = def.get_sources();
261        assert!(sources.contains(&"scoop"));
262        assert!(sources.contains(&"apt"));
263        assert!(sources.contains(&"pacman"));
264        assert!(sources.contains(&"nix"));
265        assert!(sources.contains(&"brew"));
266    }
267
268    #[test]
269    fn test_source_definition() {
270        let ccl = r#"
271emoji = 🍺
272install = brew install {package}
273check = brew leaves --installed-on-request
274"#;
275        let def: SourceDefinition = serde_ccl::from_str(ccl).unwrap();
276
277        assert_eq!(def.emoji, "🍺");
278        assert!(def.install.contains("{package}"));
279    }
280}