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 = "PascalCase")]
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 if Depth > MAX_CONFIGURATION_DEPTH {
52 dev_log!(
53 "config",
54 "warn: configuration path depth {} exceeds maximum of {}",
55 Depth,
56 MAX_CONFIGURATION_DEPTH
57 );
58 return Value::Null;
59 }
60
61 Path.split('.')
62 .try_fold(&self.Data, |Node, Key| Node.get(Key))
63 .unwrap_or(&Value::Null)
64 .clone()
65 } else {
66 self.Data.clone()
67 }
68 }
69
70 /// Sets a value in the configuration using a dot-separated path.
71 /// Creates nested objects as needed.
72 ///
73 /// # Arguments
74 /// * `Section` - Dot-separated path
75 /// * `Value` - Value to set
76 ///
77 /// # Returns
78 /// Result indicating success or error if path too deep
79 pub fn SetValue(&mut self, Section:&str, Value:Value) -> Result<(), String> {
80 let Depth = Section.matches('.').count();
81 if Depth > MAX_CONFIGURATION_DEPTH {
82 return Err(format!(
83 "Configuration path depth {} exceeds maximum of {}",
84 Depth, MAX_CONFIGURATION_DEPTH
85 ));
86 }
87
88 let Keys:Vec<&str> = Section.split('.').collect();
89
90 if Keys.is_empty() {
91 return Err("Section path cannot be empty".to_string());
92 }
93
94 // Navigate or create nested structure
95 let MutData = &mut self.Data;
96 Self::SetValueRecursive(MutData, &Keys, 0, Value);
97 Ok(())
98 }
99
100 /// Recursively navigates and sets values in nested structure.
101 fn SetValueRecursive(Data:&mut Value, Keys:&[&str], Index:usize, Value:Value) {
102 if Index == Keys.len() - 1 {
103 // At final key, set the value
104 *Data = Value;
105 } else if let Some(Map) = Data.as_object_mut() {
106 // Get or create nested object
107 Map.entry(Keys[Index]).or_insert_with(|| Value::Object(serde_json::Map::new()));
108 if let Some(Nested) = Map.get_mut(Keys[Index]) {
109 Self::SetValueRecursive(Nested, Keys, Index + 1, Value);
110 }
111 }
112 }
113
114 /// Gets a boolean value from configuration with default fallback.
115 ///
116 /// # Arguments
117 /// * `Section` - Dot-separated path
118 /// * `Default` - Default value if path doesn't exist or isn't a boolean
119 ///
120 /// # Returns
121 /// Boolean value or default
122 pub fn GetBool(&self, Section:&str, Default:bool) -> bool {
123 self.GetValue(Some(Section)).as_bool().unwrap_or(Default)
124 }
125
126 /// Gets a numeric value from configuration with default fallback.
127 ///
128 /// # Arguments
129 /// * `Section` - Dot-separated path
130 /// * `Default` - Default value if path doesn't exist or isn't a number
131 ///
132 /// # Returns
133 /// f64 value or default
134 pub fn GetNumber(&self, Section:&str, Default:f64) -> f64 {
135 self.GetValue(Some(Section)).as_f64().unwrap_or(Default)
136 }
137
138 /// Gets a string value from configuration with default fallback.
139 ///
140 /// # Arguments
141 /// * `Section` - Dot-separated path
142 /// * `Default` - Default value if path doesn't exist or isn't a string
143 ///
144 /// # Returns
145 /// String value or default
146 pub fn GetString(&self, Section:&str, Default:&str) -> String {
147 self.GetValue(Some(Section)).as_str().unwrap_or(Default).to_string()
148 }
149}