Mountain/ExtensionManagement/
Scanner.rs

1// File: Mountain/Source/ExtensionManagement/Scanner.rs
2// Role: Contains logic for discovering and parsing installed extensions.
3// Responsibilities:
4//   - Scan directories on the filesystem for extensions by reading
5//     `package.json`.
6//   - Collect and merge default configurations from all discovered extensions.
7
8//! # Extension Scanner
9//!
10//! Contains the logic for scanning directories on the filesystem to discover
11//! installed extensions by reading their `package.json` manifests.
12
13#![allow(non_snake_case, non_camel_case_types)]
14
15use std::{path::PathBuf, sync::Arc};
16
17use Common::{
18	Effect::ApplicationRunTime::ApplicationRunTime as _,
19	Error::CommonError::CommonError,
20	FileSystem::{DTO::FileTypeDTO::FileTypeDTO, ReadDirectory::ReadDirectory, ReadFile::ReadFile},
21};
22use log::{trace, warn};
23use serde_json::{Map, Value};
24use tauri::Manager;
25
26use crate::{
27	ApplicationState::{
28		ApplicationState::ApplicationState,
29		DTO::ExtensionDescriptionStateDTO::ExtensionDescriptionStateDTO,
30	},
31	Environment::Utility,
32	RunTime::ApplicationRunTime::ApplicationRunTime,
33};
34
35/// Scans a single directory for valid extensions.
36///
37/// This function iterates through a given directory, looking for subdirectories
38/// that contain a `package.json` file. It then attempts to parse this file
39/// into an `ExtensionDescriptionStateDTO`.
40pub async fn ScanDirectoryForExtensions(
41	ApplicationHandle:tauri::AppHandle,
42
43	DirectoryPath:PathBuf,
44) -> Result<Vec<ExtensionDescriptionStateDTO>, CommonError> {
45	let RunTime = ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
46
47	let mut FoundExtensions = Vec::new();
48
49	let TopLevelEntries = match RunTime.Run(ReadDirectory(DirectoryPath.clone())).await {
50		Ok(entries) => entries,
51
52		Err(error) => {
53			warn!(
54				"[ExtensionScanner] Could not read extension directory '{}': {}. Skipping.",
55				DirectoryPath.display(),
56				error
57			);
58
59			return Ok(Vec::new());
60		},
61	};
62
63	for (EntryName, FileType) in TopLevelEntries {
64		if FileType == FileTypeDTO::Directory {
65			let PotentialExtensionPath = DirectoryPath.join(EntryName);
66
67			let PackageJsonPath = PotentialExtensionPath.join("package.json");
68
69			trace!(
70				"[ExtensionScanner] Checking for package.json in: {}",
71				PotentialExtensionPath.display()
72			);
73
74			if let Ok(PackageJsonContent) = RunTime.Run(ReadFile(PackageJsonPath)).await {
75				match serde_json::from_slice::<ExtensionDescriptionStateDTO>(&PackageJsonContent) {
76					Ok(mut Description) => {
77						// Augment the description with its location on disk.
78						Description.ExtensionLocation =
79							serde_json::to_value(url::Url::from_directory_path(PotentialExtensionPath).unwrap())
80								.unwrap_or(Value::Null);
81
82						FoundExtensions.push(Description);
83					},
84
85					Err(error) => {
86						warn!(
87							"[ExtensionScanner] Failed to parse package.json for extension at '{}': {}",
88							PotentialExtensionPath.display(),
89							error
90						);
91					},
92				}
93			}
94		}
95	}
96
97	Ok(FoundExtensions)
98}
99
100/// A helper function to extract default configuration values from all
101/// scanned extensions.
102pub fn CollectDefaultConfigurations(State:&ApplicationState) -> Result<Value, CommonError> {
103	let mut MergedDefaults = Map::new();
104
105	let Extensions = State
106		.ScannedExtensions
107		.lock()
108		.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
109
110	for Extension in Extensions.values() {
111		if let Some(contributes) = Extension.Contributes.as_ref().and_then(|v| v.as_object()) {
112			if let Some(configuration) = contributes.get("configuration").and_then(|v| v.as_object()) {
113				if let Some(properties) = configuration.get("properties").and_then(|v| v.as_object()) {
114					for (key, value) in properties {
115						if let Some(prop_details) = value.as_object() {
116							if let Some(default_value) = prop_details.get("default") {
117								// TODO: A full implementation would handle nested objects.
118								MergedDefaults.insert(key.clone(), default_value.clone());
119							}
120						}
121					}
122				}
123			}
124		}
125	}
126
127	Ok(Value::Object(MergedDefaults))
128}