perl_dap/config/mod.rs
1//! Standalone DAP launch and attach configuration structures
2//!
3//! This module provides configuration types for DAP debugging sessions,
4//! supporting both launch (start new process) and attach (connect to running process) modes.
5//!
6//! # Examples
7//!
8//! ## Launch Configuration
9//!
10//! ```no_run
11//! use perl_dap_config::LaunchConfiguration;
12//! use std::path::PathBuf;
13//!
14//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
15//! let mut config = LaunchConfiguration {
16//! program: PathBuf::from("script.pl"),
17//! args: vec!["--verbose".to_string()],
18//! cwd: Some(PathBuf::from("/workspace")),
19//! env: std::collections::HashMap::new(),
20//! perl_path: None,
21//! include_paths: vec![],
22//! };
23//!
24//! config.validate()?;
25//! # Ok(())
26//! # }
27//! ```
28//!
29//! ## Attach Configuration
30//!
31//! ```
32//! use perl_dap_config::AttachConfiguration;
33//!
34//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
35//! let config = AttachConfiguration {
36//! host: "localhost".to_string(),
37//! port: 13603,
38//! timeout_ms: Some(5000),
39//! stop_on_entry: None,
40//! };
41//!
42//! config.validate()?;
43//! # Ok(())
44//! # }
45//! ```
46
47#![cfg_attr(test, allow(clippy::print_stderr, clippy::print_stdout))]
48
49use anyhow::Result;
50use serde::{Deserialize, Serialize};
51use std::collections::HashMap;
52use std::path::{Path, PathBuf};
53
54/// Validate that a path exists and is a file
55fn validate_file_exists(path: &Path, description: &str) -> Result<()> {
56 if !path.exists() {
57 anyhow::bail!("{} does not exist: {}", description, path.display());
58 }
59 if !path.is_file() {
60 anyhow::bail!("{} is not a file: {}", description, path.display());
61 }
62 Ok(())
63}
64
65/// Validate that a path exists and is a directory
66fn validate_directory_exists(path: &Path, description: &str) -> Result<()> {
67 if !path.exists() {
68 anyhow::bail!("{} does not exist: {}", description, path.display());
69 }
70 if !path.is_dir() {
71 anyhow::bail!("{} is not a directory: {}", description, path.display());
72 }
73 Ok(())
74}
75
76/// Launch configuration for starting a new Perl debugging session
77///
78/// This configuration is used when starting a new Perl process for debugging.
79/// It includes the program path, arguments, environment variables, and Perl-specific
80/// settings like include paths.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct LaunchConfiguration {
84 /// Path to the Perl script to debug (required)
85 pub program: PathBuf,
86
87 /// Command-line arguments to pass to the script
88 #[serde(default)]
89 pub args: Vec<String>,
90
91 /// Working directory for the debugged process
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub cwd: Option<PathBuf>,
94
95 /// Environment variables to set for the debugged process
96 #[serde(default)]
97 pub env: HashMap<String, String>,
98
99 /// Path to the perl binary (defaults to "perl" on PATH)
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub perl_path: Option<PathBuf>,
102
103 /// Additional paths to add to @INC (Perl's include path)
104 #[serde(default)]
105 pub include_paths: Vec<PathBuf>,
106}
107
108impl LaunchConfiguration {
109 /// Resolve workspace-relative paths to absolute paths
110 ///
111 /// This method converts relative paths in the configuration to absolute paths
112 /// based on the workspace root. It handles:
113 /// - Program path resolution
114 /// - Working directory resolution
115 /// - Include path resolution
116 ///
117 /// # Arguments
118 ///
119 /// * `workspace_root` - The workspace root directory
120 ///
121 /// # Errors
122 ///
123 /// Returns an error if path resolution fails
124 ///
125 /// # Examples
126 ///
127 /// ```
128 /// use perl_dap_config::LaunchConfiguration;
129 /// use std::path::PathBuf;
130 ///
131 /// # fn main() -> anyhow::Result<()> {
132 /// let mut config = LaunchConfiguration {
133 /// program: PathBuf::from("script.pl"),
134 /// args: vec![],
135 /// cwd: None,
136 /// env: std::collections::HashMap::new(),
137 /// perl_path: None,
138 /// include_paths: vec![PathBuf::from("lib")],
139 /// };
140 ///
141 /// config.resolve_paths(&PathBuf::from("/workspace"))?;
142 /// assert!(config.program.is_absolute());
143 /// # Ok(())
144 /// # }
145 /// ```
146 pub fn resolve_paths(&mut self, workspace_root: &Path) -> Result<()> {
147 // Resolve program path
148 if !self.program.is_absolute() {
149 self.program = workspace_root.join(&self.program);
150 }
151
152 // Resolve working directory
153 if let Some(ref mut cwd) = self.cwd
154 && !cwd.is_absolute()
155 {
156 *cwd = workspace_root.join(&cwd);
157 }
158
159 // Resolve include paths
160 for include_path in &mut self.include_paths {
161 if !include_path.is_absolute() {
162 *include_path = workspace_root.join(&include_path);
163 }
164 }
165
166 Ok(())
167 }
168
169 /// Validate the configuration
170 ///
171 /// This method checks that:
172 /// - Program path exists and is a file
173 /// - Working directory exists (if specified)
174 /// - Perl binary exists (if specified)
175 ///
176 /// # Errors
177 ///
178 /// Returns an error if validation fails
179 ///
180 /// # Examples
181 ///
182 /// ```no_run
183 /// use perl_dap_config::LaunchConfiguration;
184 /// use std::path::PathBuf;
185 ///
186 /// # fn main() -> anyhow::Result<()> {
187 /// let config = LaunchConfiguration {
188 /// program: PathBuf::from("/path/to/script.pl"),
189 /// args: vec![],
190 /// cwd: None,
191 /// env: std::collections::HashMap::new(),
192 /// perl_path: None,
193 /// include_paths: vec![],
194 /// };
195 ///
196 /// config.validate()?;
197 /// # Ok(())
198 /// # }
199 /// ```
200 pub fn validate(&self) -> Result<()> {
201 // Verify program exists
202 validate_file_exists(&self.program, "Program file")?;
203
204 // Verify working directory exists (if specified)
205 if let Some(ref cwd) = self.cwd {
206 validate_directory_exists(cwd, "Working directory")?;
207 }
208
209 // Verify perl binary exists (if specified)
210 if let Some(ref perl_path) = self.perl_path {
211 validate_file_exists(perl_path, "Perl binary")?;
212 }
213
214 Ok(())
215 }
216}
217
218/// Attach configuration for connecting to a running Perl debugging session
219///
220/// This configuration is used when attaching to an already-running Perl process
221/// that has been started with the Perl::LanguageServer DAP module.
222#[derive(Debug, Clone, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub struct AttachConfiguration {
225 /// Host to connect to (typically "localhost")
226 pub host: String,
227
228 /// Port number for the DAP server (default: 13603)
229 pub port: u16,
230
231 /// Connection timeout in milliseconds (optional)
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub timeout_ms: Option<u32>,
234
235 /// If true, pause execution at the first opportunity after attaching.
236 ///
237 /// Equivalent to the DAP `stopOnEntry` field. When set, the adapter emits a
238 /// `stopped` event with `reason = "entry"` immediately after the attach
239 /// handshake completes. Defaults to `false` when absent.
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub stop_on_entry: Option<bool>,
242}
243
244impl Default for AttachConfiguration {
245 fn default() -> Self {
246 Self {
247 host: "localhost".to_string(),
248 port: 13603,
249 timeout_ms: Some(5000),
250 stop_on_entry: None,
251 }
252 }
253}
254
255impl AttachConfiguration {
256 /// Validate the attach configuration
257 ///
258 /// This method checks that:
259 /// - Host is not empty
260 /// - Port is in valid range (1-65535)
261 /// - Timeout is reasonable (if specified)
262 ///
263 /// # Errors
264 ///
265 /// Returns an error if validation fails
266 ///
267 /// # Examples
268 ///
269 /// ```
270 /// use perl_dap_config::AttachConfiguration;
271 ///
272 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
273 /// let config = AttachConfiguration {
274 /// host: "localhost".to_string(),
275 /// port: 13603,
276 /// timeout_ms: Some(5000),
277 /// stop_on_entry: None,
278 /// };
279 ///
280 /// config.validate()?;
281 /// # Ok(())
282 /// # }
283 /// ```
284 pub fn validate(&self) -> Result<()> {
285 // Verify host is not empty
286 if self.host.trim().is_empty() {
287 anyhow::bail!("Host cannot be empty");
288 }
289
290 // Port is u16, so it's automatically in range 0-65535
291 // But we should reject port 0 as it's not valid for connecting
292 if self.port == 0 {
293 anyhow::bail!("Port must be in range 1-65535");
294 }
295
296 // Verify timeout is reasonable (if specified)
297 if let Some(timeout) = self.timeout_ms {
298 if timeout == 0 {
299 anyhow::bail!("Timeout must be greater than 0 milliseconds");
300 }
301 if timeout > 300_000 {
302 // 5 minutes max
303 anyhow::bail!("Timeout cannot exceed 300000 milliseconds (5 minutes)");
304 }
305 }
306
307 Ok(())
308 }
309}
310
311/// Create a launch.json configuration snippet
312///
313/// This function generates a JSON snippet suitable for use in VS Code's launch.json
314/// file. The snippet includes placeholders for the program path and other common options.
315///
316/// # Returns
317///
318/// A JSON string containing the launch configuration template
319///
320/// # Examples
321///
322/// ```
323/// use perl_dap_config::create_launch_json_snippet;
324///
325/// let snippet = create_launch_json_snippet();
326/// assert!(snippet.contains("\"type\""));
327/// assert!(snippet.contains("\"launch\""));
328/// ```
329pub fn create_launch_json_snippet() -> String {
330 let json = serde_json::json!({
331 "type": "perl",
332 "request": "launch",
333 "name": "Launch Perl Script",
334 "program": "${workspaceFolder}/script.pl",
335 "args": [],
336 "perlPath": "perl",
337 "includePaths": ["${workspaceFolder}/lib"],
338 "cwd": "${workspaceFolder}",
339 "env": {}
340 });
341 serde_json::to_string_pretty(&json).unwrap_or_else(|e| {
342 tracing::error!(error = %e, "Failed to serialize launch.json snippet");
343 "{}".to_string()
344 })
345}
346
347/// Create an attach.json configuration snippet
348///
349/// This function generates a JSON snippet for attaching to a running Perl::LanguageServer
350/// DAP session via TCP.
351///
352/// # Returns
353///
354/// A JSON string containing the attach configuration template
355///
356/// # Examples
357///
358/// ```
359/// use perl_dap_config::create_attach_json_snippet;
360///
361/// let snippet = create_attach_json_snippet();
362/// assert!(snippet.contains("\"type\""));
363/// assert!(snippet.contains("\"attach\""));
364/// assert!(snippet.contains("13603"));
365/// ```
366pub fn create_attach_json_snippet() -> String {
367 let json = serde_json::json!({
368 "type": "perl",
369 "request": "attach",
370 "name": "Attach to Perl::LanguageServer",
371 "host": "localhost",
372 "port": 13603,
373 "timeout": 5000,
374 "stopOnEntry": false
375 });
376 serde_json::to_string_pretty(&json).unwrap_or_else(|e| {
377 tracing::error!(error = %e, "Failed to serialize attach.json snippet");
378 "{}".to_string()
379 })
380}