sickle/model.rs
1//! CCL Model - the core data structure for navigating parsed CCL documents
2
3use crate::error::{Error, Result};
4use indexmap::IndexMap;
5use std::str::FromStr;
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10// ============================================================================
11// Type Aliases - prevent confusion between single-value and multi-value maps
12// ============================================================================
13
14/// The internal storage type for CclObject - maps keys to a Vec of values.
15/// Each key can have multiple values, preserving duplicate key semantics.
16pub(crate) type CclMap = IndexMap<String, Vec<CclObject>>;
17
18/// Iterator over key-value pairs where value is the full Vec.
19pub(crate) type CclMapIter<'a> = indexmap::map::Iter<'a, String, Vec<CclObject>>;
20
21/// Options for boolean access operations
22///
23/// Controls how `get_bool()` interprets string values as booleans.
24#[derive(Debug, Clone, Copy, Default)]
25pub struct BoolOptions {
26 /// When true, accepts "yes"/"no" in addition to "true"/"false".
27 /// When false (default), only "true" and "false" are accepted.
28 pub lenient: bool,
29}
30
31impl BoolOptions {
32 /// Create default options (strict mode - only "true"/"false")
33 pub fn new() -> Self {
34 Self::default()
35 }
36
37 /// Create options with lenient mode enabled (accepts "yes"/"no")
38 pub fn lenient() -> Self {
39 Self { lenient: true }
40 }
41}
42
43/// Options for list access operations
44///
45/// Controls how `get_list()` interprets the CCL data structure.
46#[derive(Debug, Clone, Copy, Default)]
47pub struct ListOptions {
48 /// When true, duplicate keys are coerced into lists and scalar literals are filtered.
49 /// When false (default), only bare list syntax (empty keys) produces lists.
50 pub coerce: bool,
51}
52
53impl ListOptions {
54 /// Create default options (coerce = false)
55 pub fn new() -> Self {
56 Self::default()
57 }
58
59 /// Create options with coercion enabled
60 pub fn with_coerce() -> Self {
61 Self { coerce: true }
62 }
63}
64
65/// Check if a string is a scalar literal (number or boolean)
66///
67/// CCL distinguishes between string values and scalar literals:
68/// - Numbers: integers and floats (e.g., "42", "3.14", "-17")
69/// - Booleans: true/false/yes/no
70///
71/// This helper is used to filter out scalar literals from string lists
72/// when coercion is enabled.
73fn is_scalar_literal(s: &str) -> bool {
74 // Check if it's parseable as an integer
75 if s.parse::<i64>().is_ok() {
76 return true;
77 }
78
79 // Check if it's parseable as a float
80 if s.parse::<f64>().is_ok() {
81 return true;
82 }
83
84 // Check if it's a boolean literal
85 matches!(s, "true" | "false" | "yes" | "no")
86}
87
88/// Represents a single parsed entry (key-value pair) from CCL
89///
90/// This is the output of the `parse()` function, representing a flat list
91/// of key-value pairs before hierarchy construction.
92#[derive(Debug, Clone, PartialEq, Eq)]
93#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
94pub struct Entry {
95 /// The key (can be empty for list entries)
96 pub key: String,
97 /// The value (can be multiline or contain nested CCL)
98 pub value: String,
99}
100
101impl Entry {
102 /// Create a new entry
103 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
104 Self {
105 key: key.into(),
106 value: value.into(),
107 }
108 }
109}
110
111/// Represents a parsed CCL document as a recursive map structure
112///
113/// Following the OCaml implementation: `type entry_map = value_entry list KeyMap.t`
114///
115/// A CCL document is a fixed-point recursive structure where:
116/// - Every Model is a map from String to `Vec<Model>`
117/// - An empty map {} represents a leaf/terminal value
118/// - String values are encoded in the recursive structure
119/// - Lists are represented as multiple entries with the same key
120/// - Uses IndexMap to preserve insertion order (keys ordered by first appearance)
121/// - Uses Vec to preserve order of values for each key (insertion order)
122#[derive(Debug, Clone, PartialEq, Eq)]
123#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
124pub struct CclObject(CclMap);
125
126impl CclObject {
127 /// Create a new empty model
128 pub fn new() -> Self {
129 CclObject(IndexMap::new())
130 }
131
132 /// Create a Model from an IndexMap
133 /// This is internal-only for crate operations
134 pub(crate) fn from_map(map: CclMap) -> Self {
135 CclObject(map)
136 }
137
138 /// Get a value by key, returning an error if the key doesn't exist
139 ///
140 /// If the key has multiple values, returns the first one (matching OCaml behavior).
141 /// Use `get_all()` to get all values for a key.
142 pub fn get(&self, key: &str) -> Result<&CclObject> {
143 self.0
144 .get(key)
145 .and_then(|vec| vec.first())
146 .ok_or_else(|| Error::MissingKey(key.to_string()))
147 }
148
149 /// Get all values for a key, returning an error if the key doesn't exist
150 pub fn get_all(&self, key: &str) -> Result<&[CclObject]> {
151 self.0
152 .get(key)
153 .map(|vec| vec.as_slice())
154 .ok_or_else(|| Error::MissingKey(key.to_string()))
155 }
156
157 /// Get an iterator over the keys in this model
158 pub fn keys(&self) -> impl Iterator<Item = &String> {
159 self.0.keys()
160 }
161
162 /// Get an iterator over the first value for each key
163 ///
164 /// This flattens the Vec structure, returning only the first value per key.
165 /// Use `iter_all()` to get all values.
166 pub fn values(&self) -> impl Iterator<Item = &CclObject> {
167 self.0.values().filter_map(|vec| vec.first())
168 }
169
170 /// Get an iterator over key-value pairs (first value only per key)
171 ///
172 /// This flattens the Vec structure, returning only the first value per key.
173 /// Use `iter_all()` to get all key-value pairs including duplicates.
174 pub fn iter(&self) -> impl Iterator<Item = (&String, &CclObject)> {
175 self.0
176 .iter()
177 .filter_map(|(k, vec)| vec.first().map(|v| (k, v)))
178 }
179
180 /// Get an iterator over all key-value pairs including duplicate keys
181 pub fn iter_all(&self) -> impl Iterator<Item = (&String, &CclObject)> {
182 self.0
183 .iter()
184 .flat_map(|(k, vec)| vec.iter().map(move |v| (k, v)))
185 }
186
187 /// Get the concrete IndexMap iterator for internal use (Serde)
188 pub(crate) fn iter_map(&self) -> indexmap::map::Iter<'_, String, Vec<CclObject>> {
189 self.0.iter()
190 }
191
192 /// Get the number of entries in this model
193 pub fn len(&self) -> usize {
194 self.0.len()
195 }
196
197 /// Check if this model is empty
198 pub fn is_empty(&self) -> bool {
199 self.0.is_empty()
200 }
201
202 // ========================================================================
203 // Builder API - Programmatic CCL Construction
204 // ========================================================================
205
206 /// Get mutable access to the internal IndexMap for direct manipulation
207 ///
208 /// This allows programmatic construction of CCL structures when you need
209 /// full control over the data model.
210 ///
211 /// # Example
212 ///
213 /// ```rust
214 /// use sickle::CclObject;
215 ///
216 /// let mut obj = CclObject::new();
217 /// let map = obj.inner_mut();
218 /// map.insert("key".to_string(), vec![CclObject::from_string("value")]);
219 /// ```
220 pub fn inner_mut(&mut self) -> &mut CclMap {
221 &mut self.0
222 }
223
224 /// Create an empty CclObject (represents an empty value in CCL: `key =`)
225 ///
226 /// # Example
227 ///
228 /// ```rust
229 /// use sickle::CclObject;
230 ///
231 /// let empty = CclObject::empty();
232 /// // Represents: key =
233 /// ```
234 pub fn empty() -> Self {
235 CclObject(IndexMap::new())
236 }
237
238 /// Create a CclObject representing a list using bare list syntax
239 ///
240 /// In CCL, a list is represented using the same empty key with multiple values.
241 /// Now that we use `Vec<CclObject>` internally, we can properly support duplicate keys.
242 ///
243 /// # Example
244 ///
245 /// ```rust
246 /// use sickle::CclObject;
247 ///
248 /// let list = CclObject::from_list(vec!["brew", "scoop", "pacman"]);
249 /// // Represents:
250 /// // packages =
251 /// // = brew
252 /// // = scoop
253 /// // = pacman
254 /// ```
255 pub fn from_list(items: Vec<impl Into<String>>) -> Self {
256 let mut map = IndexMap::new();
257 let values: Vec<CclObject> = items
258 .into_iter()
259 .map(|item| CclObject::from_string(item))
260 .collect();
261 map.insert("".to_string(), values);
262 CclObject(map)
263 }
264
265 /// Add a comment entry to this CclObject
266 ///
267 /// CCL comments use the `/=` prefix followed by the comment text as the key,
268 /// with an empty value. This method adds a comment entry directly to the object.
269 ///
270 /// # Example
271 ///
272 /// ```rust
273 /// use sickle::CclObject;
274 ///
275 /// let mut obj = CclObject::new();
276 /// obj.add_comment("Generated file - do not edit");
277 /// // When printed, represents: /= Generated file - do not edit
278 /// ```
279 pub fn add_comment(&mut self, text: impl Into<String>) {
280 let comment_key = format!("/= {}", text.into());
281 self.0.insert(comment_key, vec![CclObject::empty()]);
282 }
283
284 /// Add a blank line entry to this CclObject
285 ///
286 /// Blank lines in CCL output are represented as entries with an empty key
287 /// and empty value. This is useful for visual separation in generated files.
288 ///
289 /// # Example
290 ///
291 /// ```rust
292 /// use sickle::CclObject;
293 ///
294 /// let mut obj = CclObject::new();
295 /// obj.add_comment("Header section");
296 /// obj.add_blank_line();
297 /// // When printed, adds visual separation
298 /// ```
299 pub fn add_blank_line(&mut self) {
300 // Use a unique blank line marker that won't conflict with actual empty keys
301 // The printer will handle this specially
302 self.0.insert("".to_string(), vec![CclObject::empty()]);
303 }
304
305 // ========================================================================
306 // Composition API - CCL Monoid Operations
307 // ========================================================================
308
309 /// Compose two CCL objects together (monoid binary operation)
310 ///
311 /// This implements the fundamental CCL composition operation that makes CCL
312 /// a monoid. When composing two objects:
313 /// - Keys unique to either object are preserved
314 /// - Keys present in both are recursively composed
315 /// - The empty object is the identity element
316 ///
317 /// # Algebraic Properties
318 ///
319 /// - **Associativity**: `a.compose(&b).compose(&c) == a.compose(&b.compose(&c))`
320 /// - **Left Identity**: `CclObject::new().compose(&x) == x`
321 /// - **Right Identity**: `x.compose(&CclObject::new()) == x`
322 ///
323 /// # Example
324 ///
325 /// ```rust
326 /// use sickle::{load, CclObject};
327 ///
328 /// let a = load("config =\n host = localhost").unwrap();
329 /// let b = load("config =\n port = 8080").unwrap();
330 /// let composed = a.compose(&b);
331 /// // Result: config = { host = localhost, port = 8080 }
332 /// ```
333 pub fn compose(&self, other: &CclObject) -> CclObject {
334 let mut result: CclMap = CclMap::new();
335
336 // First, add all keys from self
337 for (key, self_values) in &self.0 {
338 if let Some(other_values) = other.0.get(key) {
339 // Key exists in both - compose the values
340 let composed_values = Self::compose_value_lists(self_values, other_values);
341 result.insert(key.clone(), composed_values);
342 } else {
343 // Key only in self
344 result.insert(key.clone(), self_values.clone());
345 }
346 }
347
348 // Add keys only in other
349 for (key, other_values) in &other.0 {
350 if !self.0.contains_key(key) {
351 result.insert(key.clone(), other_values.clone());
352 }
353 }
354
355 CclObject(result)
356 }
357
358 /// Compose two value lists (`Vec<CclObject>`) into one
359 ///
360 /// When composing values for the same key, we merge them into a single
361 /// composed value by recursively composing each pair.
362 fn compose_value_lists(a: &[CclObject], b: &[CclObject]) -> Vec<CclObject> {
363 // For composition, we merge all values from both lists into a single composed value
364 // This matches OCaml's behavior: fold_left merge empty [v1, v2, v3, ...]
365
366 // Start with empty, compose all from a, then all from b
367 let mut composed = CclObject::new();
368
369 for obj in a {
370 composed = composed.compose(obj);
371 }
372 for obj in b {
373 composed = composed.compose(obj);
374 }
375
376 vec![composed]
377 }
378
379 /// Check if composing three objects is associative
380 ///
381 /// Tests: `(a ∘ b) ∘ c == a ∘ (b ∘ c)`
382 ///
383 /// This is used for testing the algebraic properties of CCL.
384 pub fn compose_associative(a: &CclObject, b: &CclObject, c: &CclObject) -> bool {
385 let left = a.compose(b).compose(c);
386 let right = a.compose(&b.compose(c));
387 left == right
388 }
389
390 /// Check left identity property
391 ///
392 /// Tests: `empty ∘ x == x`
393 pub fn identity_left(x: &CclObject) -> bool {
394 let empty = CclObject::new();
395 empty.compose(x) == *x
396 }
397
398 /// Check right identity property
399 ///
400 /// Tests: `x ∘ empty == x`
401 pub fn identity_right(x: &CclObject) -> bool {
402 let empty = CclObject::new();
403 x.compose(&empty) == *x
404 }
405
406 /// Extract a string value from the model (no key lookup)
407 ///
408 /// A string value is represented as a map with a single key (the string) and empty value.
409 /// Example: `{"Alice": [{}]}` represents the string "Alice"
410 pub(crate) fn as_string(&self) -> Result<&str> {
411 if self.0.len() == 1 {
412 let (key, vec) = self.0.iter().next().unwrap();
413 if vec.len() == 1 && vec[0].0.is_empty() {
414 return Ok(key.as_str());
415 }
416 }
417 Err(Error::ValueError(
418 "expected single string value (map with one key and single empty value)".to_string(),
419 ))
420 }
421
422 /// Get a string value by key
423 ///
424 /// Looks up the key and extracts its string representation
425 pub fn get_string(&self, key: &str) -> Result<&str> {
426 self.get(key)?.as_string()
427 }
428
429 /// Extract a boolean value from the model (no key lookup)
430 ///
431 /// Parses the string representation as a boolean using strict mode
432 /// (only "true" and "false" accepted).
433 pub(crate) fn as_bool(&self) -> Result<bool> {
434 self.as_bool_with_options(BoolOptions::new())
435 }
436
437 /// Extract a boolean value from the model with options (no key lookup)
438 ///
439 /// Parses the string representation as a boolean.
440 /// - Strict mode (default): only "true" and "false" accepted
441 /// - Lenient mode: also accepts "yes" and "no"
442 pub(crate) fn as_bool_with_options(&self, options: BoolOptions) -> Result<bool> {
443 let s = self.as_string()?;
444 if options.lenient {
445 match s {
446 "true" | "yes" => Ok(true),
447 "false" | "no" => Ok(false),
448 _ => Err(Error::ValueError(format!(
449 "failed to parse '{}' as bool",
450 s
451 ))),
452 }
453 } else {
454 s.parse::<bool>()
455 .map_err(|_| Error::ValueError(format!("failed to parse '{}' as bool", s)))
456 }
457 }
458
459 /// Get a boolean value by key (strict mode)
460 ///
461 /// Only accepts "true" and "false". For lenient parsing that also
462 /// accepts "yes" and "no", use `get_bool_lenient()`.
463 pub fn get_bool(&self, key: &str) -> Result<bool> {
464 self.get(key)?.as_bool()
465 }
466
467 /// Get a boolean value by key with options
468 ///
469 /// Allows configuring boolean parsing behavior.
470 pub fn get_bool_with_options(&self, key: &str, options: BoolOptions) -> Result<bool> {
471 self.get(key)?.as_bool_with_options(options)
472 }
473
474 /// Get a boolean value by key (lenient mode)
475 ///
476 /// Accepts "true", "false", "yes", and "no".
477 /// For strict parsing, use `get_bool()`.
478 pub fn get_bool_lenient(&self, key: &str) -> Result<bool> {
479 self.get_bool_with_options(key, BoolOptions::lenient())
480 }
481
482 /// Extract an integer value from the model (no key lookup)
483 ///
484 /// Parses the string representation as an i64
485 pub(crate) fn as_int(&self) -> Result<i64> {
486 let s = self.as_string()?;
487 s.parse::<i64>()
488 .map_err(|_| Error::ValueError(format!("failed to parse '{}' as integer", s)))
489 }
490
491 /// Get an integer value by key
492 pub fn get_int(&self, key: &str) -> Result<i64> {
493 self.get(key)?.as_int()
494 }
495
496 /// Extract a float value from the model (no key lookup)
497 ///
498 /// Parses the string representation as an f64
499 pub(crate) fn as_float(&self) -> Result<f64> {
500 let s = self.as_string()?;
501 s.parse::<f64>()
502 .map_err(|_| Error::ValueError(format!("failed to parse '{}' as float", s)))
503 }
504
505 /// Get a float value by key
506 pub fn get_float(&self, key: &str) -> Result<f64> {
507 self.get(key)?.as_float()
508 }
509
510 /// Extract a list of string values from the model (no key lookup)
511 ///
512 /// In CCL, lists are represented using **bare list syntax** with empty keys:
513 /// ```text
514 /// servers =
515 /// = web1
516 /// = web2
517 /// ```
518 ///
519 /// Behavior varies by `options.coerce`:
520 ///
521 /// **With `coerce = false` (default, reference-compliant)**:
522 /// - Returns values ONLY when all keys are empty strings `""`
523 /// - Duplicate keys with values are NOT lists: `servers = web1` → `[]`
524 /// - Bare lists work: Access via `get("servers")?.as_list_with_options(...)` → `["web1", "web2"]`
525 /// - Matches OCaml reference implementation
526 ///
527 /// **With `coerce = true`**:
528 /// - Duplicate keys create lists: `servers = web1` → `["web1", "web2"]`
529 /// - Still filters scalar literals (numbers/booleans)
530 /// - Single values coerced to lists
531 pub(crate) fn as_list_with_options(&self, options: ListOptions) -> Vec<String> {
532 if options.coerce {
533 // Coercion mode: duplicate keys create lists, but filter scalars
534 self.keys()
535 .filter(|k| !is_scalar_literal(k))
536 .cloned()
537 .collect()
538 } else {
539 // Reference-compliant mode: only bare list syntax works
540 // Filter out comment keys (starting with '/') when checking for bare lists
541 let non_comment_keys: Vec<&String> =
542 self.keys().filter(|k| !k.starts_with('/')).collect();
543
544 // Handle bare list syntax: single empty-key child containing the list items
545 // With Vec structure: { "": [CclObject({item1}), CclObject({item2}), ...] }
546 // We need to get ALL values from the Vec at key ""
547 if non_comment_keys.len() == 1 && non_comment_keys[0].is_empty() {
548 if let Ok(children) = self.get_all("") {
549 // Found empty-key entries - each child contains one list item as its key
550 // Filter out comment keys from each child
551 return children
552 .iter()
553 .flat_map(|child| child.keys().filter(|k| !k.starts_with('/')).cloned())
554 .collect();
555 }
556 }
557
558 // Empty or single non-empty key = not a list
559 if non_comment_keys.len() <= 1 {
560 return Vec::new();
561 }
562
563 // Multiple non-comment keys = not a bare list in reference mode
564 Vec::new()
565 }
566 }
567
568 /// Get a list of string values by key (reference-compliant behavior)
569 ///
570 /// Only bare list syntax produces lists. Duplicate keys with values are NOT
571 /// treated as lists.
572 ///
573 /// For typed access to lists of scalars, use `get_list_typed::<T>()` instead.
574 /// For coercion behavior, use `get_list_coerced()`.
575 pub fn get_list(&self, key: &str) -> Result<Vec<String>> {
576 Ok(self.get(key)?.as_list_with_options(ListOptions::new()))
577 }
578
579 /// Get a list of string values by key (with coercion)
580 ///
581 /// Duplicate keys are coerced into lists, and scalar literals are filtered.
582 /// When multiple entries exist for the same key (e.g., `servers = web1\nservers = web2`),
583 /// all values are collected into a single list.
584 ///
585 /// For typed access to lists of scalars, use `get_list_typed::<T>()` instead.
586 /// For reference-compliant behavior, use `get_list()`.
587 pub fn get_list_coerced(&self, key: &str) -> Result<Vec<String>> {
588 let all_values = self.get_all(key)?;
589
590 // Collect string values from all entries for this key
591 // Each entry is a CclObject - extract its keys (which are the actual values)
592 let result: Vec<String> = all_values
593 .iter()
594 .flat_map(|obj| obj.keys().filter(|k| !is_scalar_literal(k)).cloned())
595 .collect();
596
597 Ok(result)
598 }
599
600 /// Get a typed list of values by key
601 ///
602 /// This method provides generic access to lists of any parseable type.
603 /// Unlike `get_list()`, this doesn't filter scalar literals - it parses all keys as type T.
604 ///
605 /// # Examples
606 ///
607 /// ```
608 /// # use sickle::{CclObject, parse, build_hierarchy};
609 /// # use sickle::error::Result;
610 /// # fn example() -> Result<()> {
611 /// // Numbers list
612 /// let input = "numbers = 1\nnumbers = 42\nnumbers = -17";
613 /// let entries = parse(input)?;
614 /// let model = build_hierarchy(&entries)?;
615 /// let numbers: Vec<i64> = model.get_list_typed("numbers")?;
616 /// assert_eq!(numbers, vec![1, 42, -17]);
617 ///
618 /// // Booleans list
619 /// let input = "flags = true\nflags = false";
620 /// let entries = parse(input)?;
621 /// let model = build_hierarchy(&entries)?;
622 /// let flags: Vec<bool> = model.get_list_typed("flags")?;
623 /// assert_eq!(flags, vec![true, false]);
624 /// # Ok(())
625 /// # }
626 /// ```
627 ///
628 /// # Errors
629 ///
630 /// Returns `Error::ValueError` if any key cannot be parsed as type T.
631 pub fn get_list_typed<T>(&self, key: &str) -> Result<Vec<T>>
632 where
633 T: FromStr,
634 T::Err: std::fmt::Display,
635 {
636 let model = self.get(key)?;
637
638 // For typed lists, we want ALL keys (including scalar literals)
639 if model.len() >= 2 {
640 model
641 .keys()
642 .map(|k| {
643 k.parse::<T>().map_err(|e| {
644 Error::ValueError(format!(
645 "Failed to parse '{}' as {}: {}",
646 k,
647 std::any::type_name::<T>(),
648 e
649 ))
650 })
651 })
652 .collect()
653 } else {
654 Ok(Vec::new())
655 }
656 }
657
658 /// Create a CclObject representing a string value
659 ///
660 /// In CCL, a string is represented as a map with a single key (the string)
661 /// and an empty value: `{"string_value": [{}]}`
662 ///
663 /// # Example
664 ///
665 /// ```rust
666 /// use sickle::CclObject;
667 ///
668 /// let val = CclObject::from_string("hello");
669 /// // Represents: key = hello
670 /// ```
671 pub fn from_string(s: impl Into<String>) -> Self {
672 let mut map = IndexMap::new();
673 map.insert(s.into(), vec![CclObject::new()]);
674 CclObject(map)
675 }
676
677 /// Insert a string value at the given key
678 /// Creates the CCL representation: `{key: {value: {}}}`
679 #[cfg(feature = "serde-serialize")]
680 pub(crate) fn insert_string(&mut self, key: &str, value: String) {
681 let mut inner = IndexMap::new();
682 inner.insert(value, vec![CclObject::new()]);
683 self.0.insert(key.to_string(), vec![CclObject(inner)]);
684 }
685
686 /// Insert a list of string values at the given key
687 /// Creates the CCL representation: `{key: {item1: {}, item2: {}, ...}}`
688 #[cfg(feature = "serde-serialize")]
689 pub(crate) fn insert_list(&mut self, key: &str, values: Vec<String>) {
690 let mut inner = IndexMap::new();
691 for value in values {
692 inner.insert(value, vec![CclObject::new()]);
693 }
694 self.0.insert(key.to_string(), vec![CclObject(inner)]);
695 }
696
697 /// Insert a nested object at the given key
698 #[cfg(feature = "serde-serialize")]
699 pub(crate) fn insert_object(&mut self, key: &str, obj: CclObject) {
700 self.0.insert(key.to_string(), vec![obj]);
701 }
702}
703
704impl Default for CclObject {
705 fn default() -> Self {
706 Self::new()
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713
714 // ========================================================================
715 // BoolOptions tests
716 // ========================================================================
717
718 #[test]
719 fn test_bool_options_default() {
720 let opts = BoolOptions::new();
721 assert!(!opts.lenient);
722 }
723
724 #[test]
725 fn test_bool_options_lenient() {
726 let opts = BoolOptions::lenient();
727 assert!(opts.lenient);
728 }
729
730 #[test]
731 fn test_bool_options_default_trait() {
732 let opts = BoolOptions::default();
733 assert!(!opts.lenient);
734 }
735
736 // ========================================================================
737 // ListOptions tests
738 // ========================================================================
739
740 #[test]
741 fn test_list_options_default() {
742 let opts = ListOptions::new();
743 assert!(!opts.coerce);
744 }
745
746 #[test]
747 fn test_list_options_with_coerce() {
748 let opts = ListOptions::with_coerce();
749 assert!(opts.coerce);
750 }
751
752 #[test]
753 fn test_list_options_default_trait() {
754 let opts = ListOptions::default();
755 assert!(!opts.coerce);
756 }
757
758 // ========================================================================
759 // is_scalar_literal tests
760 // ========================================================================
761
762 #[test]
763 fn test_is_scalar_literal_integers() {
764 assert!(is_scalar_literal("42"));
765 assert!(is_scalar_literal("-17"));
766 assert!(is_scalar_literal("0"));
767 assert!(is_scalar_literal("999999"));
768 }
769
770 #[test]
771 fn test_is_scalar_literal_floats() {
772 assert!(is_scalar_literal("3.14"));
773 assert!(is_scalar_literal("-2.5"));
774 assert!(is_scalar_literal("0.0"));
775 assert!(is_scalar_literal("1e10"));
776 }
777
778 #[test]
779 fn test_is_scalar_literal_booleans() {
780 assert!(is_scalar_literal("true"));
781 assert!(is_scalar_literal("false"));
782 assert!(is_scalar_literal("yes"));
783 assert!(is_scalar_literal("no"));
784 }
785
786 #[test]
787 fn test_is_scalar_literal_not_scalars() {
788 assert!(!is_scalar_literal("hello"));
789 assert!(!is_scalar_literal("web1"));
790 assert!(!is_scalar_literal(""));
791 assert!(!is_scalar_literal("True")); // case-sensitive
792 assert!(!is_scalar_literal("YES"));
793 }
794
795 // ========================================================================
796 // CclObject basic tests
797 // ========================================================================
798
799 #[test]
800 fn test_empty_model() {
801 let model = CclObject::new();
802 assert!(model.is_empty());
803 }
804
805 #[test]
806 fn test_map_navigation() {
807 let mut inner = IndexMap::new();
808 inner.insert("name".to_string(), vec![CclObject::new()]);
809 inner.insert("version".to_string(), vec![CclObject::new()]);
810
811 let model = CclObject(inner);
812 assert!(model.get("name").is_ok());
813 assert!(model.get("version").is_ok());
814 assert!(model.get("nonexistent").is_err());
815 }
816
817 #[test]
818 fn test_compose_disjoint_keys() {
819 // Composing objects with different keys should combine them
820 let a = CclObject::from_string("hello");
821 let b = CclObject::from_string("world");
822
823 let mut obj_a = CclObject::new();
824 obj_a.inner_mut().insert("a".to_string(), vec![a]);
825
826 let mut obj_b = CclObject::new();
827 obj_b.inner_mut().insert("b".to_string(), vec![b]);
828
829 let composed = obj_a.compose(&obj_b);
830 assert!(composed.get("a").is_ok());
831 assert!(composed.get("b").is_ok());
832 }
833
834 #[test]
835 fn test_compose_overlapping_keys() {
836 // Composing objects with same key should merge values
837 let mut obj_a = CclObject::new();
838 obj_a.inner_mut().insert(
839 "config".to_string(),
840 vec![{
841 let mut inner = CclObject::new();
842 inner.inner_mut().insert(
843 "host".to_string(),
844 vec![CclObject::from_string("localhost")],
845 );
846 inner
847 }],
848 );
849
850 let mut obj_b = CclObject::new();
851 obj_b.inner_mut().insert(
852 "config".to_string(),
853 vec![{
854 let mut inner = CclObject::new();
855 inner
856 .inner_mut()
857 .insert("port".to_string(), vec![CclObject::from_string("8080")]);
858 inner
859 }],
860 );
861
862 let composed = obj_a.compose(&obj_b);
863 let config = composed.get("config").unwrap();
864 assert!(config.get("host").is_ok());
865 assert!(config.get("port").is_ok());
866 }
867
868 #[test]
869 fn test_compose_left_identity() {
870 let mut obj = CclObject::new();
871 obj.inner_mut()
872 .insert("key".to_string(), vec![CclObject::from_string("value")]);
873
874 assert!(CclObject::identity_left(&obj));
875 }
876
877 #[test]
878 fn test_compose_right_identity() {
879 let mut obj = CclObject::new();
880 obj.inner_mut()
881 .insert("key".to_string(), vec![CclObject::from_string("value")]);
882
883 assert!(CclObject::identity_right(&obj));
884 }
885
886 #[test]
887 fn test_compose_associativity() {
888 let mut a = CclObject::new();
889 a.inner_mut()
890 .insert("a".to_string(), vec![CclObject::from_string("1")]);
891
892 let mut b = CclObject::new();
893 b.inner_mut()
894 .insert("b".to_string(), vec![CclObject::from_string("2")]);
895
896 let mut c = CclObject::new();
897 c.inner_mut()
898 .insert("c".to_string(), vec![CclObject::from_string("3")]);
899
900 assert!(CclObject::compose_associative(&a, &b, &c));
901 }
902
903 #[test]
904 fn test_compose_nested_associativity() {
905 // Test associativity with overlapping nested keys
906 let mut a = CclObject::new();
907 a.inner_mut().insert(
908 "config".to_string(),
909 vec![{
910 let mut inner = CclObject::new();
911 inner.inner_mut().insert(
912 "host".to_string(),
913 vec![CclObject::from_string("localhost")],
914 );
915 inner
916 }],
917 );
918
919 let mut b = CclObject::new();
920 b.inner_mut().insert(
921 "config".to_string(),
922 vec![{
923 let mut inner = CclObject::new();
924 inner
925 .inner_mut()
926 .insert("port".to_string(), vec![CclObject::from_string("8080")]);
927 inner
928 }],
929 );
930
931 let mut c = CclObject::new();
932 c.inner_mut().insert(
933 "db".to_string(),
934 vec![{
935 let mut inner = CclObject::new();
936 inner
937 .inner_mut()
938 .insert("name".to_string(), vec![CclObject::from_string("test")]);
939 inner
940 }],
941 );
942
943 assert!(CclObject::compose_associative(&a, &b, &c));
944 }
945}