arcella_types/
config.rs

1// arcella/arcella-types/src/config.rs
2//
3// Copyright (c) 2025 Alexey Rybakov, Arcella Team
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE>
6// or the MIT license <LICENSE-MIT>, at your option.
7// This file may not be copied, modified, or distributed
8// except according to those terms.
9
10//! Generic value types for Arcella.
11//!
12//! This module defines a universal value representation ([`Value`]) used across
13//! different parts of the Arcella system (e.g., configuration, ALME protocol, manifests)
14//! to handle structured data in a type-safe manner.
15//!
16//! The [`Value`] enum provides a flexible way to represent common data types
17//! that can be serialized/deserialized using `serde`.
18//!
19//! It also includes [`ConfigData`], a utility for managing hierarchical configurations
20//! where keys like `arcella.log.level` can be grouped into logical sections.
21
22use indexmap::IndexMap;
23use ordered_float::OrderedFloat;
24use serde::{Deserialize, Serialize};
25use std::collections::HashMap;
26
27/// Represents a specific error that occurred during data processing.
28///
29/// This struct is used inside the [`Value::TypedError`] variant to carry
30/// structured error information instead of just a string.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct TypedError {
33    /// Human-readable error message.
34    pub message: String,
35    /// A string identifying the type or category of the error.
36    pub error_type: String,
37}
38
39/// A generic value type that can represent data from configuration, ALME protocol,
40/// WIT interfaces, or other structured sources within the Arcella ecosystem.
41///
42/// This enum serves as a common interchange format for dynamic data, similar to
43/// `serde_json::Value` but tailored for Arcella's specific needs.
44///
45/// It supports:
46/// - Primitive types: `String`, `Integer`, `Float`, `Boolean`, `Null`
47/// - Compound types: `Array` (list of `Value`), `Map` (key-value pairs of `String` to `Value`)
48/// - Error signaling: `Error` (for representing failures during data processing)
49///
50/// # Examples
51///
52/// ```
53/// use arcella_types::config::Value;
54/// use ordered_float::OrderedFloat;
55///
56/// // Creating a simple value
57/// let string_val = Value::String("hello".to_string());
58/// let int_val = Value::Integer(42);
59/// let float_val = Value::Float(OrderedFloat(3.14));
60/// let bool_val = Value::Boolean(true);
61/// let null_val = Value::Null;
62///
63/// // Creating an array of values
64/// let array_val = Value::Array(vec![
65///     Value::String("item1".to_string()),
66///     Value::Integer(2),
67///     Value::Null,
68/// ]);
69///
70/// // Creating a map of values
71/// use std::collections::HashMap;
72/// let mut map = HashMap::new();
73/// map.insert("key1".to_string(), Value::Integer(42));
74/// map.insert("key2".to_string(), Value::Boolean(true));
75/// let map_val = Value::Map(map);
76/// ```
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub enum Value {
79    /// A sequence of `Value`s.
80    Array(Vec<Value>),
81
82    /// A UTF-8 string.
83    String(String),
84
85    /// A signed 64-bit integer.
86    Integer(i64),
87
88    /// A 64-bit floating-point number.
89    /// Uses `OrderedFloat` to ensure total ordering for use in collections.
90    Float(OrderedFloat<f64>),
91
92    /// A boolean value.
93    Boolean(bool),
94
95    /// A map of string keys to `Value`s.
96    /// Uses `HashMap` for fast lookups.
97    Map(HashMap<String, Value>),
98
99    /// An explicit null value, representing the absence of data.
100    Null,
101
102    /// A typed error value, useful for signaling errors within data structures.
103    TypedError(TypedError),
104}
105
106pub type ConfigValues = IndexMap<String, (Value, usize)>;
107
108/// Represents an entry within a section of the configuration.
109/// It can be either a reference to a value key or a name of a subsection.
110#[derive(Debug, Clone, PartialEq)]
111pub enum SectionEntry {
112    /// A reference to a value key by its index in the `values` map.
113    ValueKey(usize),
114
115    /// The name of a subsection.
116    SubSection(String),
117}
118
119/// Represents hierarchical configuration data with support for logical sections.
120///
121/// Keys in the configuration are expected to be in a dotted format (e.g., `arcella.log.level`).
122/// This struct allows grouping related keys into sections for easier access.
123///
124/// The underlying storage uses `IndexMap` to preserve the order of insertion for keys.
125#[derive(Debug, Clone)]
126pub struct ConfigData {
127    /// Original flat map of all parameters, sorted by key.
128    pub values: IndexMap<String, Value>,
129
130    /// Map of sections (e.g., "arcella", "arcella.log", "arcella.modules").
131    /// The value is a vector of `SectionEntry` items, representing
132    /// the next level keys that belong to this section.
133    ///
134    /// For example, if the key is "arcella.log.level", then only "arcella.log" sections will contain its index
135    pub sections: IndexMap<String, Vec<SectionEntry>>,
136}
137
138impl ConfigData {
139    /// Creates a new `ConfigData` instance from a flat map of key-value pairs.
140    /// It organizes the keys into hierarchical sections based on dot-separated prefixes.
141    ///
142    /// # Arguments
143    ///
144    /// * `values` - An `IndexMap` containing the configuration keys and their values.
145    ///
146    /// # Returns
147    ///
148    /// A new `ConfigData` instance with organized sections.
149    pub fn new(values: IndexMap<String, Value>) -> Self {
150        let mut sorted_values = values;
151        sorted_values.sort_keys();
152
153        let mut sections: IndexMap<String, Vec<SectionEntry>> = IndexMap::new();
154
155        for (i, key) in sorted_values.keys().enumerate() {
156            let parts: Vec<&str> = key.split('.').collect();
157
158            // Update all intermediate sections
159            let mut current_path = String::new();
160            for (j, &part) in parts.iter().enumerate() {
161                let old_path = current_path.clone();
162                if !current_path.is_empty() {
163                    current_path.push('.');
164                }
165                current_path.push_str(part);
166
167                let parent_section = sections
168                    .entry(old_path.clone())
169                        .or_default();
170
171                if j == parts.len() - 1 {
172                    parent_section.push(SectionEntry::ValueKey(i));
173                } else {
174                    let current_entry = SectionEntry::SubSection(current_path.clone());
175                    if !parent_section.contains(&current_entry) {
176                        parent_section.push(current_entry);
177                    }
178                    let _ = sections
179                        .entry(current_path.clone())
180                            .or_default();
181                }
182
183            }
184
185        }
186
187        sections.sort_keys();
188
189        ConfigData {
190            values: sorted_values,
191            sections,
192        }
193    }
194
195    /// Retrieves a reference to the value associated with the given key.
196    ///
197    /// # Arguments
198    ///
199    /// * `key` - The configuration key to look up.
200    ///
201    /// # Returns
202    ///
203    /// `Some(&Value)` if the key exists, otherwise `None`.
204    ///
205    /// # Example
206    ///
207    /// ```
208    /// use arcella_types::config::{ConfigData, Value};
209    /// use indexmap::IndexMap;
210    ///
211    /// let mut input = IndexMap::new();
212    /// input.insert("key1".to_string(), Value::Integer(42));
213    /// let config = ConfigData::new(input);
214    ///
215    /// assert_eq!(config.get("key1"), Some(&Value::Integer(42)));
216    /// assert_eq!(config.get("nonexistent"), None);
217    /// ```
218    pub fn get(&self, key: &str) -> Option<&Value> {
219        self.values.get(key)
220    }
221
222    /// Retrieves the indices of value keys belonging to the specified section.
223    ///
224    /// # Arguments
225    ///
226    /// * `section` - The name of the configuration section (e.g., "arcella.log").
227    ///
228    /// # Returns
229    ///
230    /// `Some(Vec<usize>)` containing the indices if the section exists, otherwise `None`.
231    pub fn get_section_keys(&self, section: &str) -> Option<Vec<usize>> {
232        self.sections.get(section).map(|entries| {
233            entries.iter()
234                .filter_map(|entry| match entry {
235                    SectionEntry::ValueKey(i) => Some(*i),
236                    SectionEntry::SubSection(_) => None,
237                })
238                .collect()
239        })
240    }
241
242    /// Retrieves the names of sub-sections belonging to the specified section.
243    ///
244    /// # Arguments
245    ///
246    /// * `section` - The name of the configuration section (e.g., "arcella.log").
247    ///
248    /// # Returns
249    ///
250    /// `Some(Vec<String>)` containing the names of sub-sections if the section exists, otherwise `None`.
251    pub fn get_subsection_names(&self, section: &str) -> Option<Vec<String>> {
252        self.sections.get(section).map(|entries| {
253            entries.iter()
254                .filter_map(|entry| match entry {
255                    SectionEntry::ValueKey(_) => None,
256                    SectionEntry::SubSection(name) => Some(name.clone()),
257                })
258                .collect()
259        })
260    }    
261
262    /// Retrieves the key-value pairs belonging to the specified section.
263    ///
264    /// This method returns an `IndexMap` where keys are the full configuration keys
265    /// (e.g., "arcella.log.level") and values are references to the corresponding `Value`s.
266    /// The order of the returned map reflects the sorted order of the original keys.
267    ///
268    /// # Arguments
269    ///
270    /// * `section` - The name of the configuration section (e.g., "arcella.log").
271    ///
272    /// # Returns
273    ///
274    /// `Some(IndexMap<String, &Value>)` if the section exists, otherwise `None`.
275    ///
276    /// # Example
277    ///
278    /// ```
279    /// use arcella_types::config::{ConfigData, Value};
280    /// use indexmap::IndexMap;
281    ///
282    /// let mut input = IndexMap::new();
283    /// input.insert("arcella.log.level".to_string(), Value::String("info".to_string()));
284    /// input.insert("arcella.log.file".to_string(), Value::String("log.txt".to_string()));
285    /// input.insert("arcella.modules.path".to_string(), Value::String("/mods".to_string()));
286    /// let config = ConfigData::new(input);
287    ///
288    /// let log_section = config.get_section_data("arcella.log").unwrap();
289    /// assert_eq!(log_section.len(), 2);
290    /// assert_eq!(log_section.get("arcella.log.level"), Some(&&Value::String("info".to_string())));
291    /// assert_eq!(log_section.get("arcella.log.file"), Some(&&Value::String("log.txt".to_string())));
292    /// ```
293    pub fn get_section_data(&self, section: &str) -> Option<IndexMap<String, &Value>> {
294        let indices = self.get_section_keys(section)?;
295        let mut section_data = IndexMap::new();
296        for idx in indices {
297            if let Some((key, value)) = self.values.get_index(idx) {
298                section_data.insert(key.clone(), value);
299            }
300        }
301        Some(section_data)
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn test_config_data_new() {
311        let mut input = IndexMap::new();
312        input.insert("arcella.modules.path".to_string(), Value::String("/mods".to_string()));
313        input.insert("arcella.log.level".to_string(), Value::String("info".to_string()));
314        input.insert("arcella.log.file".to_string(), Value::String("log.txt".to_string()));
315        input.insert("server.port".to_string(), Value::Integer(8080));
316        input.insert("server.host".to_string(), Value::String("localhost".to_string()));
317
318        let config = ConfigData::new(input);
319
320        assert_eq!(config.values.len(), 5);
321        assert!(config.values.get_index(0).unwrap().0 == "arcella.log.file");
322        assert!(config.values.get_index(1).unwrap().0 == "arcella.log.level");
323        assert!(config.values.get_index(2).unwrap().0 == "arcella.modules.path");
324        assert!(config.values.get_index(3).unwrap().0 == "server.host");
325        assert!(config.values.get_index(4).unwrap().0 == "server.port");
326
327        assert!(config.sections.contains_key(""));
328        assert!(config.sections.contains_key("arcella"));
329        assert!(config.sections.contains_key("arcella.log"));
330        assert!(config.sections.contains_key("arcella.modules"));
331        assert!(config.sections.contains_key("server"));
332    }
333
334    #[test]
335    fn test_config_data_get() {
336        let mut input = IndexMap::new();
337        input.insert("key1".to_string(), Value::Integer(42));
338        let config = ConfigData::new(input);
339
340        assert_eq!(config.get("key1"), Some(&Value::Integer(42)));
341        assert_eq!(config.get("nonexistent"), None);
342    }
343
344    #[test]
345    fn test_config_data_get_section_data() {
346        let mut input = IndexMap::new();
347        input.insert("arcella.modules.path".to_string(), Value::String("/mods".to_string()));
348        input.insert("arcella.log.level".to_string(), Value::String("info".to_string()));
349        input.insert("arcella.log.file".to_string(), Value::String("log.txt".to_string()));
350        input.insert("server.port".to_string(), Value::Integer(8080));
351        input.insert("server.host".to_string(), Value::String("localhost".to_string()));
352
353        let config = ConfigData::new(input);
354
355        let log_section = config.get_section_data("arcella.log").unwrap();
356        assert_eq!(log_section.len(), 2);
357        assert_eq!(log_section.get("arcella.log.level"), Some(&&Value::String("info".to_string())));
358        assert_eq!(log_section.get("arcella.log.file"), Some(&&Value::String("log.txt".to_string())));
359
360        let arcella_section = config.get_section_data("arcella").unwrap();
361        assert_eq!(arcella_section.len(), 0); // Includes log.file, log.level, modules.path
362    }
363
364    #[test]
365    fn test_config_data_get_subsection_names() {
366        let mut input = IndexMap::new();
367        input.insert("arcella.modules.path".to_string(), Value::String("/mods".to_string()));
368        input.insert("arcella.log.level".to_string(), Value::String("info".to_string()));
369        input.insert("arcella.log.file".to_string(), Value::String("log.txt".to_string()));
370        input.insert("server.port".to_string(), Value::Integer(8080));
371        input.insert("server.host".to_string(), Value::String("localhost".to_string()));
372
373        let config = ConfigData::new(input);
374
375        // Check subsections for "arcella"
376        let arcella_subsections = config.get_subsection_names("arcella").unwrap();
377        assert_eq!(arcella_subsections.len(), 2);
378        assert!(arcella_subsections.contains(&"arcella.log".to_string()));
379        assert!(arcella_subsections.contains(&"arcella.modules".to_string()));
380        // Check that the order corresponds to the insertion order
381        assert_eq!(arcella_subsections[0], "arcella.log");
382        assert_eq!(arcella_subsections[1], "arcella.modules");
383
384        // Check subsections for "server" (there are none)
385        let server_subsections = config.get_subsection_names("server").unwrap();
386        assert_eq!(server_subsections.len(), 0);
387
388        // Check subsections for "nonexistent" (section does not exist)
389        let nonexistent_subsections = config.get_subsection_names("nonexistent");
390        assert_eq!(nonexistent_subsections, None);
391
392        // Check subsections for "arcella.log" (also no sub-sections)
393        let log_subsections = config.get_subsection_names("arcella.log").unwrap();
394        assert_eq!(log_subsections.len(), 0);
395    }    
396}