santa_data/
lib.rs

1//! Santa Data - Data models, configuration, and CCL parser for Santa Package Manager
2//!
3//! This crate provides:
4//! - Core data models (Platform, KnownSources, PackageData, etc.)
5//! - Configuration loading and management (SantaConfig, ConfigLoader)
6//! - CCL schema definitions (PackageDefinition, SourceDefinition, etc.)
7//! - CCL parser that handles both simple and complex formats
8
9use anyhow::{Context, Result};
10use serde::de::DeserializeOwned;
11use serde_json::Value;
12use std::collections::HashMap;
13
14pub mod config;
15pub mod models;
16mod parser;
17pub mod schemas;
18
19pub use config::*;
20pub use models::*;
21pub use parser::{parse_ccl, CclValue};
22pub use schemas::*;
23
24/// Parse CCL string into a HashMap where values can be either arrays or objects
25///
26/// With sickle, this function directly deserializes CCL into proper Value types.
27///
28/// # Examples
29///
30/// ```
31/// use santa_data::parse_to_hashmap;
32/// use serde_json::Value;
33///
34/// let ccl = r#"
35/// simple_pkg =
36///   = brew
37///   = scoop
38///
39/// complex_pkg =
40///   _sources =
41///     = brew
42///   brew = gh
43/// "#;
44///
45/// let result = parse_to_hashmap(ccl).unwrap();
46/// assert!(result.contains_key("simple_pkg"));
47/// assert!(result.contains_key("complex_pkg"));
48/// ```
49pub fn parse_to_hashmap(ccl_content: &str) -> Result<HashMap<String, Value>> {
50    // Parse using sickle's load function (parse + build_hierarchy)
51    let model = sickle::load(ccl_content).context("Failed to parse CCL with sickle")?;
52
53    // Convert the model to a HashMap<String, Value>
54    model_to_hashmap(&model)
55}
56
57/// Convert a sickle Model to a HashMap<String, Value>
58fn model_to_hashmap(model: &sickle::Model) -> Result<HashMap<String, Value>> {
59    let mut result = HashMap::new();
60
61    for (key, value) in model.iter() {
62        result.insert(key.clone(), model_to_value(value)?);
63    }
64
65    Ok(result)
66}
67
68/// Convert a sickle Model to a serde_json Value
69fn model_to_value(model: &sickle::Model) -> Result<Value> {
70    // Fast path for singleton maps
71    if model.len() == 1 {
72        let (key, value) = model.iter().next().unwrap();
73
74        // Check if this is a list represented with empty key
75        // CCL: bat = \n  = brew\n  = scoop
76        // Becomes: {"": {"brew": {}, "scoop": {}}}
77        if key.is_empty() && !value.is_empty() && value.values().all(|v| v.is_empty()) {
78            // The value is a list (keys with empty values)
79            let values: Vec<Value> = value.keys().map(|k| Value::String(k.clone())).collect();
80            return Ok(Value::Array(values));
81        }
82
83        // Check if this is a singleton string: {"value": {}}
84        if value.is_empty() {
85            return Ok(Value::String(key.clone()));
86        }
87    }
88
89    // Check if this is a list (multiple keys all with empty values)
90    if model.len() > 1 && model.values().all(|v| v.is_empty()) {
91        // This is a list - keys are the list items
92        let values: Vec<Value> = model.keys().map(|k| Value::String(k.clone())).collect();
93        return Ok(Value::Array(values));
94    }
95
96    // Check if there are multiple empty keys (alternative list representation)
97    let empty_key_count = model.keys().filter(|k| k.is_empty()).count();
98    if empty_key_count > 1 {
99        // Extract all values from empty keys
100        let mut values = Vec::new();
101        for (k, v) in model.iter() {
102            if k.is_empty() {
103                values.push(model_to_value(v)?);
104            }
105        }
106        return Ok(Value::Array(values));
107    }
108
109    // Otherwise, it's a map (object)
110    let mut obj = serde_json::Map::new();
111    for (k, v) in model.iter() {
112        obj.insert(k.clone(), model_to_value(v)?);
113    }
114    Ok(Value::Object(obj))
115}
116
117/// Parse CCL string and deserialize into a specific type
118///
119/// # Examples
120///
121/// ```
122/// use santa_data::parse_ccl_to;
123/// use serde::Deserialize;
124/// use std::collections::HashMap;
125///
126/// #[derive(Deserialize)]
127/// struct Package {
128///     #[serde(rename = "_sources")]
129///     sources: Option<Vec<String>>,
130/// }
131///
132/// let ccl = r#"
133/// bat =
134///   _sources =
135///     = brew
136///     = scoop
137/// "#;
138///
139/// let packages: HashMap<String, Package> = parse_ccl_to(ccl).unwrap();
140/// assert!(packages.contains_key("bat"));
141/// ```
142pub fn parse_ccl_to<T: DeserializeOwned>(ccl_content: &str) -> Result<T> {
143    // Use sickle's deserializer directly instead of going through JSON
144    sickle::from_str(ccl_content).context("Failed to deserialize parsed CCL")
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_parse_simple_array() {
153        let ccl = r#"
154test_pkg =
155  = brew
156  = scoop
157  = pacman
158"#;
159        let result = parse_to_hashmap(ccl).unwrap();
160
161        assert!(result.contains_key("test_pkg"));
162        let value = &result["test_pkg"];
163        println!("DEBUG test_pkg value: {:#?}", value);
164        assert!(value.is_array());
165
166        let arr = value.as_array().unwrap();
167        assert_eq!(arr.len(), 3);
168        assert_eq!(arr[0].as_str().unwrap(), "brew");
169        assert_eq!(arr[1].as_str().unwrap(), "scoop");
170        assert_eq!(arr[2].as_str().unwrap(), "pacman");
171    }
172
173    #[test]
174    fn test_parse_complex_object() {
175        let ccl = r#"
176test_pkg =
177  _sources =
178    = brew
179    = scoop
180  brew = gh
181"#;
182        let result = parse_to_hashmap(ccl).unwrap();
183
184        assert!(result.contains_key("test_pkg"));
185        let value = &result["test_pkg"];
186        println!("Parsed value: {:#?}", value);
187        assert!(value.is_object());
188
189        let obj = value.as_object().unwrap();
190        println!("Object keys: {:?}", obj.keys().collect::<Vec<_>>());
191        assert!(obj.contains_key("_sources"));
192        assert!(obj.contains_key("brew"));
193
194        let sources_value = &obj["_sources"];
195        println!("_sources value: {:#?}", sources_value);
196        let sources = sources_value.as_array().unwrap();
197        assert_eq!(sources.len(), 2);
198
199        let brew_override = obj["brew"].as_str().unwrap();
200        assert_eq!(brew_override, "gh");
201    }
202
203    #[test]
204    fn test_parse_multiple_packages() {
205        let ccl = r#"
206simple =
207  = brew
208  = scoop
209
210complex =
211  _sources =
212    = pacman
213  _platforms =
214    = linux
215"#;
216        let result = parse_to_hashmap(ccl).unwrap();
217
218        assert_eq!(result.len(), 2);
219        assert!(result["simple"].is_array());
220        assert!(result["complex"].is_object());
221    }
222}