1use 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
24pub fn parse_to_hashmap(ccl_content: &str) -> Result<HashMap<String, Value>> {
50 let model = sickle::load(ccl_content).context("Failed to parse CCL with sickle")?;
52
53 model_to_hashmap(&model)
55}
56
57fn 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
68fn model_to_value(model: &sickle::CclObject) -> Result<Value> {
70 if let Ok(empty_key_values) = model.get_all("") {
73 if !empty_key_values.is_empty() {
74 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 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 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 if model.len() == 1 {
100 let (key, value) = model.iter().next().unwrap();
101
102 if value.is_empty() {
104 return Ok(Value::String(key.clone()));
105 }
106 }
107
108 if model.len() > 1 && model.values().all(|v| v.is_empty()) {
110 let values: Vec<Value> = model.keys().map(|k| Value::String(k.clone())).collect();
112 return Ok(Value::Array(values));
113 }
114
115 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
123pub fn parse_ccl_to<T: DeserializeOwned>(ccl_content: &str) -> Result<T> {
149 let options =
151 sickle::ParserOptions::default().with_crlf(sickle::options::CrlfBehavior::NormalizeToLf);
152 sickle::from_str_with_options(ccl_content, &options).context("Failed to deserialize parsed CCL")
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_parse_simple_array() {
161 let ccl = r#"
162test_pkg =
163 = brew
164 = scoop
165 = pacman
166"#;
167 let result = parse_to_hashmap(ccl).unwrap();
168
169 assert!(result.contains_key("test_pkg"));
170 let value = &result["test_pkg"];
171 println!("DEBUG test_pkg value: {:#?}", value);
172 assert!(value.is_array());
173
174 let arr = value.as_array().unwrap();
175 assert_eq!(arr.len(), 3);
176 assert_eq!(arr[0].as_str().unwrap(), "brew");
177 assert_eq!(arr[1].as_str().unwrap(), "scoop");
178 assert_eq!(arr[2].as_str().unwrap(), "pacman");
179 }
180
181 #[test]
182 fn test_parse_complex_object() {
183 let ccl = r#"
184test_pkg =
185 _sources =
186 = brew
187 = scoop
188 brew = gh
189"#;
190 let result = parse_to_hashmap(ccl).unwrap();
191
192 assert!(result.contains_key("test_pkg"));
193 let value = &result["test_pkg"];
194 println!("Parsed value: {:#?}", value);
195 assert!(value.is_object());
196
197 let obj = value.as_object().unwrap();
198 println!("Object keys: {:?}", obj.keys().collect::<Vec<_>>());
199 assert!(obj.contains_key("_sources"));
200 assert!(obj.contains_key("brew"));
201
202 let sources_value = &obj["_sources"];
203 println!("_sources value: {:#?}", sources_value);
204 let sources = sources_value.as_array().unwrap();
205 assert_eq!(sources.len(), 2);
206
207 let brew_override = obj["brew"].as_str().unwrap();
208 assert_eq!(brew_override, "gh");
209 }
210
211 #[test]
212 fn test_parse_multiple_packages() {
213 let ccl = r#"
214simple =
215 = brew
216 = scoop
217
218complex =
219 _sources =
220 = pacman
221 _platforms =
222 = linux
223"#;
224 let result = parse_to_hashmap(ccl).unwrap();
225
226 assert_eq!(result.len(), 2);
227 assert!(result["simple"].is_array());
228 assert!(result["complex"].is_object());
229 }
230}