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 = "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}