1use anyhow::{Context, Result};
11use serde::de::DeserializeOwned;
12use serde_json::Value;
13use std::collections::HashMap;
14
15pub mod models;
16mod parser;
17pub mod schemas;
18
19pub use models::*;
20pub use parser::{parse_ccl, CclValue};
21pub use schemas::*;
22
23pub fn parse_to_hashmap(ccl_content: &str) -> Result<HashMap<String, Value>> {
50 let raw: HashMap<String, String> =
54 serde_ccl::from_str(ccl_content).context("Failed to parse CCL with serde_ccl")?;
55
56 let mut result = HashMap::new();
57
58 for (key, value_str) in raw.into_iter() {
59 let parsed_value = parse_value_string(&value_str)?;
61 result.insert(key, parsed_value);
62 }
63
64 Ok(result)
65}
66
67fn parse_value_string(s: &str) -> Result<Value> {
69 let trimmed = s.trim();
70
71 if trimmed.starts_with('=') {
73 return parse_simple_array(trimmed);
74 }
75
76 if trimmed.contains('=') {
78 return parse_complex_object(trimmed);
79 }
80
81 Ok(Value::String(s.to_string()))
83}
84
85fn parse_simple_array(s: &str) -> Result<Value> {
87 let items: Vec<String> = s
88 .lines()
89 .filter_map(|line| {
90 let trimmed = line.trim();
91 if let Some(stripped) = trimmed.strip_prefix('=') {
92 let value = stripped.trim();
93 if !value.is_empty() {
94 Some(value.to_string())
95 } else {
96 None
97 }
98 } else {
99 None
100 }
101 })
102 .collect();
103
104 Ok(Value::Array(items.into_iter().map(Value::String).collect()))
105}
106
107fn parse_complex_object(s: &str) -> Result<Value> {
109 let mut obj = serde_json::Map::new();
110 let mut current_key: Option<String> = None;
111 let mut current_lines: Vec<String> = Vec::new();
112 let mut current_indent = 0;
113
114 for line in s.lines() {
115 let line_indent = line.len() - line.trim_start().len();
116 let trimmed = line.trim();
117
118 if trimmed.is_empty() {
119 continue;
120 }
121
122 if let Some(eq_pos) = trimmed.find('=') {
124 let is_array_element = trimmed.starts_with('=');
125
126 if is_array_element && current_key.is_some() && line_indent > current_indent {
128 current_lines.push(line.to_string());
129 continue;
130 }
131
132 if let Some(key) = current_key.take() {
134 let value_str = current_lines.join("\n");
135 let value = parse_value_string(&value_str)?;
136 obj.insert(key, value);
137 current_lines.clear();
138 }
139
140 if is_array_element {
141 current_lines.push(line.to_string());
144 continue;
145 }
146
147 let field_name = trimmed[..eq_pos].trim();
149 let field_value = trimmed[eq_pos + 1..].trim();
150
151 current_indent = line_indent;
152
153 if !field_value.is_empty() {
154 obj.insert(
156 field_name.to_string(),
157 Value::String(field_value.to_string()),
158 );
159 } else {
160 current_key = Some(field_name.to_string());
162 }
163 } else if current_key.is_some() {
164 current_lines.push(line.to_string());
166 }
167 }
168
169 if let Some(key) = current_key {
171 let value_str = current_lines.join("\n");
172 let value = parse_value_string(&value_str)?;
173 obj.insert(key, value);
174 }
175
176 Ok(Value::Object(obj))
177}
178
179pub fn parse_ccl_to<T: DeserializeOwned>(ccl_content: &str) -> Result<T> {
205 let hashmap = parse_to_hashmap(ccl_content)?;
206 let value = serde_json::to_value(hashmap)?;
207 serde_json::from_value(value).context("Failed to deserialize parsed CCL")
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_parse_simple_array() {
216 let ccl = r#"
217test_pkg =
218 = brew
219 = scoop
220 = pacman
221"#;
222 let result = parse_to_hashmap(ccl).unwrap();
223
224 assert!(result.contains_key("test_pkg"));
225 let value = &result["test_pkg"];
226 assert!(value.is_array());
227
228 let arr = value.as_array().unwrap();
229 assert_eq!(arr.len(), 3);
230 assert_eq!(arr[0].as_str().unwrap(), "brew");
231 assert_eq!(arr[1].as_str().unwrap(), "scoop");
232 assert_eq!(arr[2].as_str().unwrap(), "pacman");
233 }
234
235 #[test]
236 fn test_parse_complex_object() {
237 let ccl = r#"
238test_pkg =
239 _sources =
240 = brew
241 = scoop
242 brew = gh
243"#;
244 let result = parse_to_hashmap(ccl).unwrap();
245
246 assert!(result.contains_key("test_pkg"));
247 let value = &result["test_pkg"];
248 println!("Parsed value: {:#?}", value);
249 assert!(value.is_object());
250
251 let obj = value.as_object().unwrap();
252 println!("Object keys: {:?}", obj.keys().collect::<Vec<_>>());
253 assert!(obj.contains_key("_sources"));
254 assert!(obj.contains_key("brew"));
255
256 let sources_value = &obj["_sources"];
257 println!("_sources value: {:#?}", sources_value);
258 let sources = sources_value.as_array().unwrap();
259 assert_eq!(sources.len(), 2);
260
261 let brew_override = obj["brew"].as_str().unwrap();
262 assert_eq!(brew_override, "gh");
263 }
264
265 #[test]
266 fn test_parse_multiple_packages() {
267 let ccl = r#"
268simple =
269 = brew
270 = scoop
271
272complex =
273 _sources =
274 = pacman
275 _platforms =
276 = linux
277"#;
278 let result = parse_to_hashmap(ccl).unwrap();
279
280 assert_eq!(result.len(), 2);
281 assert!(result["simple"].is_array());
282 assert!(result["complex"].is_object());
283 }
284}