Skip to main content

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}