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}