Mountain/ApplicationState/DTO/MergedConfigurationStateDTO.rs
1//! # MergedConfigurationStateDTO
2//!
3//! # RESPONSIBILITY
4//! - Data transfer object for merged application configuration
5//! - Serializable format for gRPC/IPC transmission
6//! - Used by Mountain to provide final effective configuration to UI features
7//!
8//! # FIELDS
9//! - Data: Merged configuration JSON object from all sources
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14use crate::dev_log;
15
16/// Maximum configuration depth to prevent stack overflow from deeply nested
17/// paths
18const MAX_CONFIGURATION_DEPTH:usize = 50;
19
20/// Represents the final, effective configuration after merging settings from
21/// all sources (default, user, workspace, folder). This merged view is what
22/// is queried by application features.
23#[derive(Serialize, Deserialize, Clone, Debug, Default)]
24#[serde(rename_all = "camelCase")]
25pub struct MergedConfigurationStateDTO {
26 /// Merged configuration data from all sources
27 pub Data:Value,
28}
29
30impl MergedConfigurationStateDTO {
31 /// Creates a new `MergedConfigurationStateDTO` from a `serde_json::Value`.
32 ///
33 /// # Arguments
34 /// * `Data` - The merged configuration JSON value
35 ///
36 /// # Returns
37 /// New MergedConfigurationStateDTO instance
38 pub fn Create(Data:Value) -> Self { Self { Data } }
39
40 /// Gets a specific value from the configuration using a dot-separated path.
41 /// If the section is `None`, it returns the entire configuration object.
42 ///
43 /// # Arguments
44 /// * `Section` - Optional dot-separated path (e.g., "editor.fontSize")
45 ///
46 /// # Returns
47 /// The configuration value at the path, or Null if not found
48 pub fn GetValue(&self, Section:Option<&str>) -> Value {
49 if let Some(Path) = Section {
50 let Depth = Path.matches('.').count();
51
52 if Depth > MAX_CONFIGURATION_DEPTH {
53 dev_log!(
54 "config",
55 "warn: configuration path depth {} exceeds maximum of {}",
56 Depth,
57 MAX_CONFIGURATION_DEPTH
58 );
59
60 return Value::Null;
61 }
62
63 Path.split('.')
64 .try_fold(&self.Data, |Node, Key| Node.get(Key))
65 .unwrap_or(&Value::Null)
66 .clone()
67 } else {
68 self.Data.clone()
69 }
70 }
71
72 /// Sets a value in the configuration using a dot-separated path.
73 /// Creates nested objects as needed.
74 ///
75 /// # Arguments
76 /// * `Section` - Dot-separated path
77 /// * `Value` - Value to set
78 ///
79 /// # Returns
80 /// Result indicating success or error if path too deep
81 pub fn SetValue(&mut self, Section:&str, Value:Value) -> Result<(), String> {
82 let Depth = Section.matches('.').count();
83
84 if Depth > MAX_CONFIGURATION_DEPTH {
85 return Err(format!(
86 "Configuration path depth {} exceeds maximum of {}",
87 Depth, MAX_CONFIGURATION_DEPTH
88 ));
89 }
90
91 let Keys:Vec<&str> = Section.split('.').collect();
92
93 if Keys.is_empty() {
94 return Err("Section path cannot be empty".to_string());
95 }
96
97 // Navigate or create nested structure
98 let MutData = &mut self.Data;
99
100 Self::SetValueRecursive(MutData, &Keys, 0, Value);
101
102 Ok(())
103 }
104
105 /// Recursively navigates and sets values in nested structure.
106 fn SetValueRecursive(Data:&mut Value, Keys:&[&str], Index:usize, Value:Value) {
107 if Index == Keys.len() - 1 {
108 // At final key, set the value
109 *Data = Value;
110 } else if let Some(Map) = Data.as_object_mut() {
111 // Get or create nested object
112 Map.entry(Keys[Index]).or_insert_with(|| Value::Object(serde_json::Map::new()));
113
114 if let Some(Nested) = Map.get_mut(Keys[Index]) {
115 Self::SetValueRecursive(Nested, Keys, Index + 1, Value);
116 }
117 }
118 }
119
120 /// Gets a boolean value from configuration with default fallback.
121 ///
122 /// # Arguments
123 /// * `Section` - Dot-separated path
124 /// * `Default` - Default value if path doesn't exist or isn't a boolean
125 ///
126 /// # Returns
127 /// Boolean value or default
128 pub fn GetBool(&self, Section:&str, Default:bool) -> bool {
129 self.GetValue(Some(Section)).as_bool().unwrap_or(Default)
130 }
131
132 /// Gets a numeric value from configuration with default fallback.
133 ///
134 /// # Arguments
135 /// * `Section` - Dot-separated path
136 /// * `Default` - Default value if path doesn't exist or isn't a number
137 ///
138 /// # Returns
139 /// f64 value or default
140 pub fn GetNumber(&self, Section:&str, Default:f64) -> f64 {
141 self.GetValue(Some(Section)).as_f64().unwrap_or(Default)
142 }
143
144 /// Gets a string value from configuration with default fallback.
145 ///
146 /// # Arguments
147 /// * `Section` - Dot-separated path
148 /// * `Default` - Default value if path doesn't exist or isn't a string
149 ///
150 /// # Returns
151 /// String value or default
152 pub fn GetString(&self, Section:&str, Default:&str) -> String {
153 self.GetValue(Some(Section)).as_str().unwrap_or(Default).to_string()
154 }
155}