Mountain/Environment/
ConfigurationProvider.rs

1// File: Mountain/Source/Environment/ConfigurationProvider.rs
2// Role: Implements the `ConfigurationProvider` and `ConfigurationInspector`
3// traits. Responsibilities:
4//   - Core logic for reading, merging, updating, and inspecting settings.
5//   - Handles the configuration cascade (Default -> User -> WorkSpace).
6//   - Interacts with the file system via effects for persistence.
7
8//! # ConfigurationProvider Implementation
9//!
10//! Implements the `ConfigurationProvider` and `ConfigurationInspector` traits
11//! for the `MountainEnvironment`. This provider contains the core logic for
12//! configuration management, including reading, merging, updating, and
13//! inspecting settings from various sources.
14
15#![allow(non_snake_case, non_camel_case_types)]
16
17use std::{path::PathBuf, sync::Arc};
18
19use Common::{
20	Configuration::{
21		ConfigurationInspector::ConfigurationInspector,
22		ConfigurationProvider::ConfigurationProvider,
23		DTO::{
24			ConfigurationOverridesDTO::ConfigurationOverridesDTO,
25			ConfigurationTarget::ConfigurationTarget,
26			InspectResultDataDTO::InspectResultDataDTO,
27		},
28	},
29	Effect::ApplicationRunTime::ApplicationRunTime as _,
30	Error::CommonError::CommonError,
31	FileSystem::{ReadFile::ReadFile, WriteFileBytes::WriteFileBytes},
32};
33use async_trait::async_trait;
34use log::{debug, info, warn};
35use serde_json::{Map, Value};
36use tauri::Manager;
37
38use super::{MountainEnvironment::MountainEnvironment, Utility};
39use crate::{
40	ApplicationState::DTO::MergedConfigurationStateDTO::MergedConfigurationStateDTO,
41	ExtensionManagement::Scanner::CollectDefaultConfigurations,
42	RunTime::ApplicationRunTime::ApplicationRunTime as MountainRunTime,
43};
44
45#[async_trait]
46impl ConfigurationProvider for MountainEnvironment {
47	/// Retrieves a configuration value from the cached, merged configuration.
48	async fn GetConfigurationValue(
49		&self,
50
51		Section:Option<String>,
52
53		_Overrides:ConfigurationOverridesDTO,
54	) -> Result<Value, CommonError> {
55		debug!("[ConfigurationProvider] Getting configuration for section: {:?}", Section);
56
57		let ConfigurationGuard = self
58			.ApplicationState
59			.Configuration
60			.lock()
61			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
62
63		Ok(ConfigurationGuard.GetValue(Section.as_deref()))
64	}
65
66	/// Updates a configuration value in the appropriate `settings.json` file.
67	async fn UpdateConfigurationValue(
68		&self,
69
70		Key:String,
71
72		ValueToSet:Value,
73
74		Target:ConfigurationTarget,
75
76		_Overrides:ConfigurationOverridesDTO,
77
78		_ScopeToLanguage:Option<bool>,
79	) -> Result<(), CommonError> {
80		info!("[ConfigurationProvider] Updating key '{}' in target {:?}", Key, Target);
81
82		let RunTime = self.ApplicationHandle.state::<Arc<MountainRunTime>>().inner().clone();
83
84		let ConfigPath:PathBuf = match Target {
85			ConfigurationTarget::User => {
86				self.ApplicationHandle
87					.path()
88					.app_config_dir()
89					.map(|p| p.join("settings.json"))
90					.map_err(|Error| {
91						CommonError::ConfigurationLoad {
92							Description:format!("Could not resolve user config path: {}", Error),
93						}
94					})?
95			},
96
97			ConfigurationTarget::WorkSpace => {
98				self.ApplicationState
99					.WorkSpaceConfigurationPath
100					.lock()
101					.map_err(Utility::MapApplicationStateLockErrorToCommonError)?
102					.clone()
103					.ok_or_else(|| {
104						CommonError::ConfigurationLoad { Description:"No workspace configuration path set".into() }
105					})?
106			},
107
108			_ => {
109				warn!("[ConfigurationProvider] Unsupported configuration target: {:?}", Target);
110
111				return Err(CommonError::NotImplemented {
112					FeatureName:"This configuration target is not supported".into(),
113				});
114			},
115		};
116
117		// Read the file, modify it, and write it back.
118		let Bytes = RunTime.Run(ReadFile(ConfigPath.clone())).await.unwrap_or_default();
119
120		let mut CurrentConfig:Value = serde_json::from_slice(&Bytes).unwrap_or_else(|_| Value::Object(Map::new()));
121
122		if let Value::Object(Map) = &mut CurrentConfig {
123			if ValueToSet.is_null() {
124				Map.remove(&Key);
125			} else {
126				Map.insert(Key.clone(), ValueToSet);
127			}
128		}
129
130		let ContentBytes = serde_json::to_vec_pretty(&CurrentConfig)?;
131
132		RunTime
133			.Run(WriteFileBytes(ConfigPath.clone(), ContentBytes, true, true))
134			.await?;
135
136		// Re-merge all configurations to update the live state.
137		InitializeAndMergeConfigurations(self).await?;
138
139		Ok(())
140	}
141}
142
143#[async_trait]
144impl ConfigurationInspector for MountainEnvironment {
145	/// Inspects a configuration key to get its value from all relevant scopes.
146	async fn InspectConfigurationValue(
147		&self,
148
149		Key:String,
150
151		_Overrides:ConfigurationOverridesDTO,
152	) -> Result<Option<InspectResultDataDTO>, CommonError> {
153		info!("[ConfigurationProvider] Inspecting key: {}", Key);
154
155		let UserSettingsPath = self
156			.ApplicationHandle
157			.path()
158			.app_config_dir()
159			.map(|p| p.join("settings.json"))
160			.ok();
161
162		let WorkSpaceSettingsPath = self
163			.ApplicationState
164			.WorkSpaceConfigurationPath
165			.lock()
166			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?
167			.clone();
168
169		// Read each configuration layer individually.
170		let DefaultConfig = CollectDefaultConfigurations(&self.ApplicationState)?;
171
172		let UserConfig = ReadAndParseConfigurationFile(self, &UserSettingsPath).await?;
173
174		let WorkSpaceConfig = ReadAndParseConfigurationFile(self, &WorkSpaceSettingsPath).await?;
175
176		let GetValueFromDotPath =
177			|Node:&Value, Path:&str| -> Option<Value> { Path.split('.').try_fold(Node, |n, k| n.get(k)).cloned() };
178
179		let mut ResultDTO = InspectResultDataDTO::default();
180
181		ResultDTO.DefaultValue = GetValueFromDotPath(&DefaultConfig, &Key);
182
183		ResultDTO.UserValue = GetValueFromDotPath(&UserConfig, &Key);
184
185		ResultDTO.WorkSpaceValue = GetValueFromDotPath(&WorkSpaceConfig, &Key);
186
187		// Determine the final effective value based on the correct cascade order.
188		ResultDTO.EffectiveValue = ResultDTO
189			.WorkSpaceValue
190			.clone()
191			.or_else(|| ResultDTO.UserValue.clone())
192			.or_else(|| ResultDTO.DefaultValue.clone());
193
194		if ResultDTO.EffectiveValue.is_some() { Ok(Some(ResultDTO)) } else { Ok(None) }
195	}
196}
197
198/// An internal helper to read and parse a single JSON configuration file.
199async fn ReadAndParseConfigurationFile(
200	Environment:&MountainEnvironment,
201
202	Path:&Option<PathBuf>,
203) -> Result<Value, CommonError> {
204	if let Some(p) = Path {
205		let RunTime = Environment.ApplicationHandle.state::<Arc<MountainRunTime>>().inner().clone();
206
207		if let Ok(Bytes) = RunTime.Run(ReadFile(p.clone())).await {
208			return Ok(serde_json::from_slice(&Bytes).unwrap_or(Value::Object(Map::new())));
209		}
210	}
211
212	Ok(Value::Object(Map::new()))
213}
214
215/// Logic to load and merge all configuration files into the effective
216/// configuration stored in `ApplicationState`.
217pub async fn InitializeAndMergeConfigurations(Environment:&MountainEnvironment) -> Result<(), CommonError> {
218	info!("[ConfigurationProvider] Re-initializing and merging all configurations...");
219
220	let DefaultConfig = CollectDefaultConfigurations(&Environment.ApplicationState)?;
221
222	let UserSettingsPath = Environment
223		.ApplicationHandle
224		.path()
225		.app_config_dir()
226		.map(|p| p.join("settings.json"))
227		.ok();
228
229	let WorkSpaceSettingsPath = Environment
230		.ApplicationState
231		.WorkSpaceConfigurationPath
232		.lock()
233		.map_err(Utility::MapApplicationStateLockErrorToCommonError)?
234		.clone();
235
236	let UserConfig = ReadAndParseConfigurationFile(Environment, &UserSettingsPath).await?;
237
238	let WorkSpaceConfig = ReadAndParseConfigurationFile(Environment, &WorkSpaceSettingsPath).await?;
239
240	// A true deep merge is required here.
241	let mut Merged = DefaultConfig.as_object().cloned().unwrap_or_default();
242
243	if let Some(UserMap) = UserConfig.as_object() {
244		for (k, v) in UserMap {
245			Merged.insert(k.clone(), v.clone());
246		}
247	}
248
249	if let Some(WorkSpaceMap) = WorkSpaceConfig.as_object() {
250		for (k, v) in WorkSpaceMap {
251			Merged.insert(k.clone(), v.clone());
252		}
253	}
254
255	let FinalConfig = MergedConfigurationStateDTO::Create(Value::Object(Merged));
256
257	*Environment
258		.ApplicationState
259		.Configuration
260		.lock()
261		.map_err(Utility::MapApplicationStateLockErrorToCommonError)? = FinalConfig;
262
263	info!("[ConfigurationProvider] Configuration state updated and merged.");
264
265	Ok(())
266}