Skip to main content

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 = "camelCase")]
26pub struct WorkspaceFolderStateDTO {
27	/// The URI of the folder.
28	#[serde(rename = "uri", 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
91		Ok(())
92	}
93
94	/// Gets the folder name as a human-readable string.
95	/// Returns the name if present, otherwise extracts from URI.
96	pub fn GetDisplayName(&self) -> String {
97		if !self.Name.is_empty() {
98			self.Name.clone()
99		} else {
100			// Extract folder name from URI
101			self.URI
102				.path_segments()
103				.and_then(|Segments| Segments.last())
104				.unwrap_or("Untitled")
105				.to_string()
106		}
107	}
108
109	/// Checks if this is the root folder (index 0).
110	pub fn IsRoot(&self) -> bool { self.Index == 0 }
111
112	/// Creates a new instance from a file path URI.
113	///
114	/// # Arguments
115	/// * `FolderPath` - Folder path as string
116	/// * `Index` - Folder index
117	///
118	/// # Returns
119	/// Result containing the DTO or validation error
120	pub fn FromPath(FolderPath:&str, Index:usize) -> Result<Self, String> {
121		let URI = Url::parse(FolderPath).map_err(|Error| format!("Invalid folder path: {}", Error))?;
122
123		// Check if the URI represents a directory by checking if it ends with a slash
124		// or if the file path exists and is a directory
125		let IsDirectory =
126			URI.path().ends_with('/') || (URI.scheme() == "file" && URI.to_file_path().map_or(false, |p| p.is_dir()));
127
128		if !IsDirectory {
129			return Err("URI does not represent a directory".to_string());
130		}
131
132		let Name = Self::ExtractFolderName(&URI);
133
134		Self::New(URI, Name, Index)
135	}
136
137	/// Extracts the folder name from a URI.
138	fn ExtractFolderName(URI:&Url) -> String {
139		URI.path_segments()
140			.and_then(|Segments| Segments.last())
141			.map(String::from)
142			.unwrap_or_else(|| "Untitled".to_string())
143	}
144}
145
146#[cfg(test)]
147mod tests {
148
149	use super::*;
150
151	#[test]
152	fn test_creation_success() {
153		let URI = Url::parse("file:///workspace/project").unwrap();
154
155		let dto = WorkspaceFolderStateDTO::New(URI.clone(), "project".to_string(), 0);
156
157		assert!(dto.is_ok());
158
159		assert_eq!(dto.unwrap().Name, "project");
160	}
161
162	#[test]
163	fn test_invalid_name_length() {
164		let URI = Url::parse("file:///workspace/project").unwrap();
165
166		let LongName = "a".repeat(257);
167
168		let dto = WorkspaceFolderStateDTO::New(URI, LongName, 0);
169
170		assert!(dto.is_err());
171	}
172
173	#[test]
174	fn test_invalid_index() {
175		let URI = Url::parse("file:///workspace/project").unwrap();
176
177		let dto = WorkspaceFolderStateDTO::New(URI, "project".to_string(), 100);
178
179		assert!(dto.is_err());
180	}
181}