Mountain/ApplicationState/DTO/
WorkspaceFolderStateDTO.rs

1//! # WorkspaceFolderStateDTO
2//!
3//! # RESPONSIBILITY
4//! - Data transfer object for workspace folder state
5//! - Serializable format for gRPC/IPC transmission
6//! - Used by Mountain to track workspace folder configuration
7//!
8//! # FIELDS
9//! - URI: Folder resource URI
10//! - Name: Display name
11//! - Index: Zero-based position in workspace
12use serde::{Deserialize, Serialize};
13use url::Url;
14use CommonLibrary::Utility::Serialization::URLSerializationHelper;
15
16/// Maximum folder name length
17const MAX_FOLDER_NAME_LENGTH:usize = 256;
18
19/// Maximum number of folders in a workspace
20const MAX_WORKSPACE_FOLDERS:usize = 100;
21
22/// Represents a single folder that is part of the current workspace.
23/// Compatible with VS Code's WorkspaceFolder interface.
24#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
25#[serde(rename_all = "PascalCase")]
26pub struct WorkspaceFolderStateDTO {
27	/// The URI of the folder.
28	#[serde(with = "URLSerializationHelper")]
29	pub URI:Url,
30
31	/// The display name of the folder.
32	#[serde(skip_serializing_if = "String::is_empty")]
33	pub Name:String,
34
35	/// The zero-based index of the folder in the workspace.
36	pub Index:usize,
37}
38
39impl WorkspaceFolderStateDTO {
40	/// Creates a new WorkspaceFolderStateDTO with validation.
41	///
42	/// # Arguments
43	/// * `URI` - Folder URI
44	/// * `Name` - Display name
45	/// * `Index` - Zero-based index in workspace
46	///
47	/// # Returns
48	/// Result containing the DTO or validation error
49	pub fn New(URI:Url, Name:String, Index:usize) -> Result<Self, String> {
50		// Validate URI is not empty
51		if URI.as_str().is_empty() {
52			return Err("URI cannot be empty".to_string());
53		}
54
55		// Validate name length
56		if Name.len() > MAX_FOLDER_NAME_LENGTH {
57			return Err(format!(
58				"Folder name exceeds maximum length of {} bytes",
59				MAX_FOLDER_NAME_LENGTH
60			));
61		}
62
63		// Validate index range
64		if Index >= MAX_WORKSPACE_FOLDERS {
65			return Err(format!(
66				"Folder index {} exceeds maximum workspace folders count of {}",
67				Index, MAX_WORKSPACE_FOLDERS
68			));
69		}
70
71		Ok(Self { URI, Name, Index })
72	}
73
74	/// Updates the name with validation.
75	///
76	/// # Arguments
77	/// * `Name` - New display name
78	///
79	/// # Returns
80	/// Result indicating success or error if name too long
81	pub fn UpdateName(&mut self, Name:String) -> Result<(), String> {
82		if Name.len() > MAX_FOLDER_NAME_LENGTH {
83			return Err(format!(
84				"Folder name exceeds maximum length of {} bytes",
85				MAX_FOLDER_NAME_LENGTH
86			));
87		}
88
89		self.Name = Name;
90		Ok(())
91	}
92
93	/// Gets the folder name as a human-readable string.
94	/// Returns the name if present, otherwise extracts from URI.
95	pub fn GetDisplayName(&self) -> String {
96		if !self.Name.is_empty() {
97			self.Name.clone()
98		} else {
99			// Extract folder name from URI
100			self.URI
101				.path_segments()
102				.and_then(|Segments| Segments.last())
103				.unwrap_or("Untitled")
104				.to_string()
105		}
106	}
107
108	/// Checks if this is the root folder (index 0).
109	pub fn IsRoot(&self) -> bool { self.Index == 0 }
110
111	/// Creates a new instance from a file path URI.
112	///
113	/// # Arguments
114	/// * `FolderPath` - Folder path as string
115	/// * `Index` - Folder index
116	///
117	/// # Returns
118	/// Result containing the DTO or validation error
119	pub fn FromPath(FolderPath:&str, Index:usize) -> Result<Self, String> {
120		let URI = Url::parse(FolderPath).map_err(|Error| format!("Invalid folder path: {}", Error))?;
121
122		// Check if the URI represents a directory by checking if it ends with a slash
123		// or if the file path exists and is a directory
124		let IsDirectory =
125			URI.path().ends_with('/') || (URI.scheme() == "file" && URI.to_file_path().map_or(false, |p| p.is_dir()));
126
127		if !IsDirectory {
128			return Err("URI does not represent a directory".to_string());
129		}
130
131		let Name = Self::ExtractFolderName(&URI);
132
133		Self::New(URI, Name, Index)
134	}
135
136	/// Extracts the folder name from a URI.
137	fn ExtractFolderName(URI:&Url) -> String {
138		URI.path_segments()
139			.and_then(|Segments| Segments.last())
140			.map(String::from)
141			.unwrap_or_else(|| "Untitled".to_string())
142	}
143}
144
145#[cfg(test)]
146mod tests {
147	use super::*;
148
149	#[test]
150	fn test_creation_success() {
151		let URI = Url::parse("file:///workspace/project").unwrap();
152		let dto = WorkspaceFolderStateDTO::New(URI.clone(), "project".to_string(), 0);
153		assert!(dto.is_ok());
154		assert_eq!(dto.unwrap().Name, "project");
155	}
156
157	#[test]
158	fn test_invalid_name_length() {
159		let URI = Url::parse("file:///workspace/project").unwrap();
160		let LongName = "a".repeat(257);
161		let dto = WorkspaceFolderStateDTO::New(URI, LongName, 0);
162		assert!(dto.is_err());
163	}
164
165	#[test]
166	fn test_invalid_index() {
167		let URI = Url::parse("file:///workspace/project").unwrap();
168		let dto = WorkspaceFolderStateDTO::New(URI, "project".to_string(), 100);
169		assert!(dto.is_err());
170	}
171}