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