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(¤t_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}