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