Mountain/ApplicationState/DTO/
CustomDocumentStateDTO.rs

1//! # CustomDocumentStateDTO
2//!
3//! # RESPONSIBILITY
4//! - Data transfer object for custom editor document state
5//! - Serializable format for gRPC/IPC transmission
6//! - Used by Mountain to track custom document lifecycle
7//!
8//! # FIELDS
9//! - URI: Resource identifier for the document
10//! - ViewType: Custom editor type identifier
11//! - SideCarIdentifier: Sidecar process hosting the provider
12//! - IsEditable: User edit permission flag
13//! - BackupIdentifier: Optional backup file reference
14//! - Edits: Version tracking and edit history map
15
16use std::collections::HashMap;
17
18use serde::{Deserialize, Serialize};
19use url::Url;
20use CommonLibrary::Utility::Serialization::URLSerializationHelper;
21
22/// Maximum length for ViewType string to prevent allocation attacks
23const MAX_VIEW_TYPE_LENGTH:usize = 256;
24
25/// Maximum length for SideCarIdentifier string
26const MAX_SIDECAR_IDENTIFIER_LENGTH:usize = 128;
27
28/// Maximum number of edits to track per document
29const MAX_EDITS_PER_DOCUMENT:usize = 1000;
30
31/// A struct that holds the state for a document being handled by a custom
32/// editor. This is stored in `ApplicationState` to track the lifecycle of
33/// custom documents.
34#[derive(Serialize, Deserialize, Debug, Clone)]
35#[serde(rename_all = "PascalCase")]
36pub struct CustomDocumentStateDTO {
37	/// The URI of the document resource being edited.
38	#[serde(with = "URLSerializationHelper")]
39	pub URI:Url,
40
41	/// The view type of the custom editor responsible for this document.
42	#[serde(skip_serializing_if = "String::is_empty")]
43	pub ViewType:String,
44
45	/// The identifier of the sidecar process where the custom editor provider
46	/// lives.
47	#[serde(skip_serializing_if = "String::is_empty")]
48	pub SideCarIdentifier:String,
49
50	/// A flag indicating if the document is currently editable by the user.
51	pub IsEditable:bool,
52
53	/// An optional identifier for a backup copy of the file's content.
54	#[serde(skip_serializing_if = "Option::is_none")]
55	pub BackupIdentifier:Option<String>,
56
57	/// A map to store edit history or other versioning information.
58	/// In a real implementation, this might hold a more structured edit type.
59	#[serde(skip_serializing_if = "HashMap::is_empty")]
60	pub Edits:HashMap<u32, serde_json::Value>,
61}
62
63impl CustomDocumentStateDTO {
64	/// Creates a new CustomDocumentStateDTO with validation.
65	///
66	/// # Arguments
67	/// * `URI` - The document resource URI
68	/// * `ViewType` - The custom editor type identifier
69	/// * `SideCarIdentifier` - The sidecar process identifier
70	/// * `IsEditable` - Whether the document is user-editable
71	///
72	/// # Returns
73	/// Result containing the DTO or an error if validation fails
74	pub fn New(URI:Url, ViewType:String, SideCarIdentifier:String, IsEditable:bool) -> Result<Self, String> {
75		// Validate ViewType length
76		if ViewType.len() > MAX_VIEW_TYPE_LENGTH {
77			return Err(format!("ViewType exceeds maximum length of {} bytes", MAX_VIEW_TYPE_LENGTH));
78		}
79
80		// Validate SideCarIdentifier length
81		if SideCarIdentifier.len() > MAX_SIDECAR_IDENTIFIER_LENGTH {
82			return Err(format!(
83				"SideCarIdentifier exceeds maximum length of {} bytes",
84				MAX_SIDECAR_IDENTIFIER_LENGTH
85			));
86		}
87
88		// Ensure URI is not empty
89		if URI.as_str().is_empty() {
90			return Err("URI cannot be empty".to_string());
91		}
92
93		Ok(Self {
94			URI,
95			ViewType,
96			SideCarIdentifier,
97			IsEditable,
98			BackupIdentifier:None,
99			Edits:HashMap::new(),
100		})
101	}
102
103	/// Adds an edit entry to the edits map with bounds checking.
104	///
105	/// # Arguments
106	/// * `EditID` - The edit identifier
107	/// * `EditData` - The edit data
108	///
109	/// # Returns
110	/// Result indicating success or failure if map is full
111	pub fn AddEdit(&mut self, EditID:u32, EditData:serde_json::Value) -> Result<(), String> {
112		if self.Edits.len() >= MAX_EDITS_PER_DOCUMENT {
113			return Err(format!("Maximum edit limit of {} reached for document", MAX_EDITS_PER_DOCUMENT));
114		}
115
116		self.Edits.insert(EditID, EditData);
117		Ok(())
118	}
119
120	/// Clears all edit history for this document.
121	pub fn ClearEdits(&mut self) { self.Edits.clear(); }
122
123	/// Returns the count of edits tracked for this document.
124	pub fn GetEditCount(&self) -> usize { self.Edits.len() }
125}
126
127#[cfg(test)]
128mod tests {
129	use super::*;
130
131	#[test]
132	fn test_creation_success() {
133		let URI = Url::parse("file:///test/document.md").unwrap();
134		let dto =
135			CustomDocumentStateDTO::New(URI.clone(), "markdown.editor".to_string(), "sidecar-123".to_string(), true);
136		assert!(dto.is_ok());
137		assert_eq!(dto.unwrap().ViewType, "markdown.editor");
138	}
139
140	#[test]
141	fn test_invalid_view_type_length() {
142		let URI = Url::parse("file:///test/document.md").unwrap();
143		let LongViewType = "a".repeat(257);
144		let dto = CustomDocumentStateDTO::New(URI, LongViewType, "sidecar-123".to_string(), true);
145		assert!(dto.is_err());
146	}
147}