Skip to main content

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::CclObject) -> 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::CclObject) -> Result<Value> {
70    // Check if this is a list with empty keys (CCL: = item1\n = item2)
71    // Empty keys have all their values stored in a Vec under the "" key
72    if let Ok(empty_key_values) = model.get_all("") {
73        if !empty_key_values.is_empty() {
74            // Check if all values are simple string values (single key with empty value)
75            let all_simple_strings = empty_key_values
76                .iter()
77                .all(|v| v.len() == 1 && v.values().all(|child| child.is_empty()));
78
79            if all_simple_strings {
80                // Extract string values
81                let values: Vec<Value> = empty_key_values
82                    .iter()
83                    .filter_map(|v| v.keys().next().cloned())
84                    .map(Value::String)
85                    .collect();
86                return Ok(Value::Array(values));
87            } else {
88                // Convert each value recursively
89                let values: Vec<Value> = empty_key_values
90                    .iter()
91                    .map(model_to_value)
92                    .collect::<Result<Vec<_>>>()?;
93                return Ok(Value::Array(values));
94            }
95        }
96    }
97
98    // Fast path for singleton maps
99    if model.len() == 1 {
100        let (key, value) = model.iter().next().unwrap();
101
102        // Check if this is a singleton string: {"value": {}}
103        if value.is_empty() {
104            return Ok(Value::String(key.clone()));
105        }
106    }
107
108    // Check if this is a list (multiple keys all with empty values)
109    if model.len() > 1 && model.values().all(|v| v.is_empty()) {
110        // This is a list - keys are the list items
111        let values: Vec<Value> = model.keys().map(|k| Value::String(k.clone())).collect();
112        return Ok(Value::Array(values));
113    }
114
115    // Otherwise, it's a map (object)
116    let mut obj = serde_json::Map::new();
117    for (k, v) in model.iter() {
118        obj.insert(k.clone(), model_to_value(v)?);
119    }
120    Ok(Value::Object(obj))
121}
122
123/// Parse CCL string and deserialize into a specific type
124///
125/// # Examples
126///
127/// ```
128/// use santa_data::parse_ccl_to;
129/// use serde::Deserialize;
130/// use std::collections::HashMap;
131///
132/// #[derive(Deserialize)]
133/// struct Package {
134///     #[serde(rename = "_sources")]
135///     sources: Option<Vec<String>>,
136/// }
137///
138/// let ccl = r#"
139/// bat =
140///   _sources =
141///     = brew
142///     = scoop
143/// "#;
144///
145/// let packages: HashMap<String, Package> = parse_ccl_to(ccl).unwrap();
146/// assert!(packages.contains_key("bat"));
147/// ```
148pub fn parse_ccl_to<T: DeserializeOwned>(ccl_content: &str) -> Result<T> {
149    // Use sickle's deserializer directly instead of going through JSON
150    sickle::from_str(ccl_content).context("Failed to deserialize parsed CCL")
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_parse_simple_array() {
159        let ccl = r#"
160test_pkg =
161  = brew
162  = scoop
163  = pacman
164"#;
165        let result = parse_to_hashmap(ccl).unwrap();
166
167        assert!(result.contains_key("test_pkg"));
168        let value = &result["test_pkg"];
169        println!("DEBUG test_pkg value: {:#?}", value);
170        assert!(value.is_array());
171
172        let arr = value.as_array().unwrap();
173        assert_eq!(arr.len(), 3);
174        assert_eq!(arr[0].as_str().unwrap(), "brew");
175        assert_eq!(arr[1].as_str().unwrap(), "scoop");
176        assert_eq!(arr[2].as_str().unwrap(), "pacman");
177    }
178
179    #[test]
180    fn test_parse_complex_object() {
181        let ccl = r#"
182test_pkg =
183  _sources =
184    = brew
185    = scoop
186  brew = gh
187"#;
188        let result = parse_to_hashmap(ccl).unwrap();
189
190        assert!(result.contains_key("test_pkg"));
191        let value = &result["test_pkg"];
192        println!("Parsed value: {:#?}", value);
193        assert!(value.is_object());
194
195        let obj = value.as_object().unwrap();
196        println!("Object keys: {:?}", obj.keys().collect::<Vec<_>>());
197        assert!(obj.contains_key("_sources"));
198        assert!(obj.contains_key("brew"));
199
200        let sources_value = &obj["_sources"];
201        println!("_sources value: {:#?}", sources_value);
202        let sources = sources_value.as_array().unwrap();
203        assert_eq!(sources.len(), 2);
204
205        let brew_override = obj["brew"].as_str().unwrap();
206        assert_eq!(brew_override, "gh");
207    }
208
209    #[test]
210    fn test_parse_multiple_packages() {
211        let ccl = r#"
212simple =
213  = brew
214  = scoop
215
216complex =
217  _sources =
218    = pacman
219  _platforms =
220    = linux
221"#;
222        let result = parse_to_hashmap(ccl).unwrap();
223
224        assert_eq!(result.len(), 2);
225        assert!(result["simple"].is_array());
226        assert!(result["complex"].is_object());
227    }
228}