Skip to main content

Mountain/ApplicationState/DTO/
ExtensionDescriptionStateDTO.rs

1//! # ExtensionDescriptionStateDTO
2//!
3//! # RESPONSIBILITY
4//! - Data transfer object for extension description/state
5//! - Serializable format for gRPC/IPC transmission
6//! - Used by Mountain to track extension metadata and capabilities
7//!
8//! # FIELDS
9//! - Identifier: Unique extension identifier
10//! - Name: Extension display name
11//! - Version: Semantic version string
12//! - Publisher: Publisher name
13//! - Engines: Engine compatibility requirements
14//! - Main: Main entry point path (Node.js)
15//! - Browser: Browser entry point path
16//! - ModuleType: Module type (commonjs/esm)
17//! - IsBuiltin: Built-in extension flag
18//! - IsUnderDevelopment: Development flag
19//! - ExtensionLocation: Installation location URI
20//! - ActivationEvents: Activation event triggers
21//! - Contributes: Extension contributions configuration
22
23use serde::{Deserialize, Serialize};
24use serde_json::Value;
25
26/// Maximum length for extension name
27const MAX_EXTENSION_NAME_LENGTH:usize = 128;
28
29/// Maximum length for version string
30const MAX_VERSION_LENGTH:usize = 64;
31
32/// Maximum length for publisher name
33const MAX_PUBLISHER_LENGTH:usize = 64;
34
35/// Maximum number of activation events
36const MAX_ACTIVATION_EVENTS:usize = 100;
37
38/// Represents the deserialized content of an extension's `package.json` file,
39/// augmented with location information and other metadata.
40///
41/// This is stored in `ApplicationState` to provide the extension host with the
42/// list of available extensions and their capabilities.
43/// VS Code extensions use camelCase in package.json. Serde renames from
44/// PascalCase Rust fields to camelCase JSON automatically. Fields that
45/// don't exist in package.json (Identifier, ExtensionLocation, IsBuiltin)
46/// default to their zero values on deserialization.
47#[derive(Serialize, Deserialize, Clone, Debug)]
48#[serde(rename_all = "camelCase")]
49pub struct ExtensionDescriptionStateDTO {
50	// --- Core Metadata ---
51	/// Extension identifier: { value: string, uuid?: string }
52	/// Not present in package.json - constructed from publisher.name after
53	/// parsing.
54	#[serde(default)]
55	pub Identifier:Value,
56
57	/// Extension name (from package.json "name")
58	///
59	/// Always serialized, even when empty, because VS Code's scanner and
60	/// the trusted-publishers migration (`extensions.contribution.ts`) both
61	/// evaluate `extension.manifest.name.toLowerCase()` unconditionally.
62	/// Dropping the field would leave a bare `undefined` and crash the
63	/// renderer with `TypeError: undefined is not an object`.
64	#[serde(default)]
65	pub Name:String,
66
67	/// Semantic version string (e.g., "1.0.0").
68	///
69	/// Always serialized for the same reason as `Name` / `Publisher`: the
70	/// renderer reads `manifest.version` in several hot paths and crashes
71	/// if the field is missing outright.
72	#[serde(default)]
73	pub Version:String,
74
75	/// Publisher name or identifier.
76	///
77	/// Always serialized, even when empty. VS Code's
78	/// `extensions.contribution.ts` trusted-publishers migration runs on
79	/// every User-extension at workbench boot and executes
80	/// `extension.manifest.publisher.toLowerCase()`. If the key is omitted
81	/// the renderer crashes with
82	/// `TypeError: undefined is not an object (evaluating
83	/// 'manifest.publisher')`.
84	#[serde(default)]
85	pub Publisher:String,
86
87	/// Engine compatibility requirements: { vscode: string }
88	#[serde(default)]
89	pub Engines:Value,
90
91	// --- Entry Points ---
92	/// Main entry point path (Node.js runtime)
93	#[serde(default, skip_serializing_if = "Option::is_none")]
94	pub Main:Option<String>,
95
96	/// Browser entry point path (web extension)
97	#[serde(default, skip_serializing_if = "Option::is_none")]
98	pub Browser:Option<String>,
99
100	// --- Type & Flags ---
101	/// Module type: commonjs or esm
102	#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
103	pub ModuleType:Option<String>,
104
105	/// Whether this is a built-in extension (not in package.json, set by
106	/// scanner)
107	#[serde(default)]
108	pub IsBuiltin:bool,
109
110	/// Whether extension is under active development
111	#[serde(default)]
112	pub IsUnderDevelopment:bool,
113
114	// --- Location & Activation ---
115	/// Installation location URI (set by scanner, not in package.json)
116	#[serde(default)]
117	pub ExtensionLocation:Value,
118
119	/// Activation event triggers (e.g., "onStartupFinished")
120	#[serde(default, skip_serializing_if = "Option::is_none")]
121	pub ActivationEvents:Option<Vec<String>>,
122
123	// --- Contributions ---
124	/// Extension contributions (commands, views, etc.)
125	#[serde(default, skip_serializing_if = "Option::is_none")]
126	pub Contributes:Option<Value>,
127
128	// --- Discovery metadata ---
129	/// VS Code category tags ("Themes", "Programming Languages",
130	/// "Snippets", "Language Packs", "Debuggers", "Formatters",
131	/// "Keymaps", "SCM Providers", "Testing", "Education", "Other").
132	///
133	/// Atom TH1: Wind's Extensions sidebar filters `@builtin category:themes`
134	/// against this array. Without the field the filter never matches -
135	/// user reported theme extensions absent on @builtin search despite
136	/// being on disk. Scanner-passthrough surfaces the raw package.json
137	/// value; resolved NLS placeholders survive because the serde
138	/// deserialisation happens after the NLS rewrite.
139	#[serde(default, skip_serializing_if = "Option::is_none")]
140	pub Categories:Option<Vec<String>>,
141
142	/// Human-readable display name, usually a VS Code NLS placeholder like
143	/// `%displayName%` that the scanner resolves against `package.nls.json`.
144	/// Atom TH1: sidebar row rendering falls back to `name` when this is
145	/// absent; having the real value populates tooltips and the
146	/// details editor.
147	#[serde(default, skip_serializing_if = "Option::is_none")]
148	pub DisplayName:Option<String>,
149
150	/// Short prose description. Same NLS-placeholder rules as DisplayName.
151	#[serde(default, skip_serializing_if = "Option::is_none")]
152	pub Description:Option<String>,
153
154	/// Extension keywords array - searched by the sidebar when the query
155	/// doesn't match `name`, `displayName`, or `description`.
156	#[serde(default, skip_serializing_if = "Option::is_none")]
157	pub Keywords:Option<Vec<String>>,
158
159	/// Repository info: Either a string URL or `{ type, url }` object.
160	#[serde(default, skip_serializing_if = "Option::is_none")]
161	pub Repository:Option<Value>,
162
163	/// Bug-tracker URL or object.
164	#[serde(default, skip_serializing_if = "Option::is_none")]
165	pub Bugs:Option<Value>,
166
167	/// Homepage URL.
168	#[serde(default, skip_serializing_if = "Option::is_none")]
169	pub Homepage:Option<String>,
170
171	/// License identifier (SPDX short code) or URL.
172	#[serde(default, skip_serializing_if = "Option::is_none")]
173	pub License:Option<String>,
174
175	/// Icon path relative to the extension root (for sidebar thumbnails).
176	#[serde(default, skip_serializing_if = "Option::is_none")]
177	pub Icon:Option<String>,
178
179	/// Marketplace API key placeholder - still present in some upstream
180	/// built-in manifests. `@vscode/extension-telemetry` reads its
181	/// length on construction; if missing the activate throws
182	/// `Cannot read properties of undefined (reading 'length')`.
183	#[serde(default, skip_serializing_if = "Option::is_none", rename = "aiKey")]
184	pub AiKey:Option<String>,
185
186	/// Marketplace-side extension kind (`["ui"]`, `["workspace"]`,
187	/// `["web"]`). Wind uses this to decide which host to run the
188	/// extension in. Missing → VS Code falls back to heuristics.
189	#[serde(default, skip_serializing_if = "Option::is_none", rename = "extensionKind")]
190	pub ExtensionKind:Option<Value>,
191
192	/// Capabilities descriptor - `untrustedWorkspaces`, `virtualWorkspaces`.
193	#[serde(default, skip_serializing_if = "Option::is_none")]
194	pub Capabilities:Option<Value>,
195
196	/// Dependency list - other extensions this one needs activated first.
197	#[serde(
198		default,
199		skip_serializing_if = "Option::is_none",
200		rename = "extensionDependencies"
201	)]
202	pub ExtensionDependencies:Option<Vec<String>>,
203
204	/// Extension-pack children - extensions this one bundles by reference.
205	#[serde(default, skip_serializing_if = "Option::is_none", rename = "extensionPack")]
206	pub ExtensionPack:Option<Vec<String>>,
207}
208
209impl ExtensionDescriptionStateDTO {
210	/// Validates the extension description data.
211	///
212	/// # Returns
213	/// Result indicating success or validation error with reason
214	pub fn Validate(&self) -> Result<(), String> {
215		// Validate Name length
216		if self.Name.len() > MAX_EXTENSION_NAME_LENGTH {
217			return Err(format!(
218				"Extension name exceeds maximum length of {} bytes",
219				MAX_EXTENSION_NAME_LENGTH
220			));
221		}
222
223		// Validate Version length
224		if self.Version.len() > MAX_VERSION_LENGTH {
225			return Err(format!("Version string exceeds maximum length of {} bytes", MAX_VERSION_LENGTH));
226		}
227
228		// Validate Publisher length
229		if self.Publisher.len() > MAX_PUBLISHER_LENGTH {
230			return Err(format!("Publisher exceeds maximum length of {} bytes", MAX_PUBLISHER_LENGTH));
231		}
232
233		// Validate ActivationEvents count
234		if let Some(Events) = &self.ActivationEvents {
235			if Events.len() > MAX_ACTIVATION_EVENTS {
236				return Err(format!("Activation events exceed maximum count of {}", MAX_ACTIVATION_EVENTS));
237			}
238		}
239
240		Ok(())
241	}
242
243	/// Creates a minimal extension description for testing or placeholder use.
244	///
245	/// # Arguments
246	/// * `Identifier` - Extension identifier value
247	/// * `Name` - Extension name
248	/// * `Version` - Extension version
249	/// * `Publisher` - Publisher name
250	///
251	/// # Returns
252	/// A new ExtensionDescriptionStateDTO with minimal required fields
253	pub fn CreateMinimal(Identifier:Value, Name:String, Version:String, Publisher:String) -> Result<Self, String> {
254		let Description = Self {
255			Identifier,
256			Name:Name.clone(),
257			Version:Version.clone(),
258			Publisher:Publisher.clone(),
259			Engines:serde_json::json!({ "vscode": "*" }),
260			Main:None,
261			Browser:None,
262			ModuleType:None,
263			IsBuiltin:false,
264			IsUnderDevelopment:false,
265			ExtensionLocation:serde_json::json!(null),
266			ActivationEvents:None,
267			Contributes:None,
268			Categories:None,
269			DisplayName:None,
270			Description:None,
271			Keywords:None,
272			Repository:None,
273			Bugs:None,
274			Homepage:None,
275			License:None,
276			Icon:None,
277			AiKey:None,
278			ExtensionKind:None,
279			Capabilities:None,
280			ExtensionDependencies:None,
281			ExtensionPack:None,
282		};
283
284		Description.Validate()?;
285		Ok(Description)
286	}
287}