Skip to main content

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}