Thanks to visit codestin.com
Credit goes to docs.rs

Skip to main content

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}