Skip to main content

perl_dap/types/
mod.rs

1//! Shared DAP session model types for Perl debugging.
2
3use serde::{Deserialize, Serialize};
4
5/// Stack frame information used by the debug adapter.
6///
7/// Corresponds to the DAP `StackFrame` type in the `stackTrace` response.
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "camelCase")]
10pub struct StackFrame {
11    /// Unique numeric identifier for this frame within the current stopped state.
12    pub id: i32,
13    /// Human-readable display name for the frame (e.g. `"main::foo"`).
14    pub name: String,
15    /// Source file that contains this frame's code.
16    pub source: Source,
17    /// 1-based line number of the current instruction in the frame.
18    pub line: i32,
19    /// 1-based column number of the current instruction in the frame.
20    pub column: i32,
21    /// 1-based end line of the range covered by this frame, if known.
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub end_line: Option<i32>,
24    /// 1-based end column of the range covered by this frame, if known.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub end_column: Option<i32>,
27}
28
29impl StackFrame {
30    /// Create a new stack frame at `line` with column defaulting to 1.
31    #[must_use]
32    pub fn new(id: i32, name: impl Into<String>, source: Source, line: i32) -> Self {
33        Self { id, name: name.into(), source, line, column: 1, end_line: None, end_column: None }
34    }
35
36    /// Override the column for this frame.
37    #[must_use]
38    pub fn with_column(mut self, column: i32) -> Self {
39        self.column = column;
40        self
41    }
42
43    /// Set the end position (end line and end column) of this frame's source range.
44    #[must_use]
45    pub fn with_end(mut self, end_line: i32, end_column: i32) -> Self {
46        self.end_line = Some(end_line);
47        self.end_column = Some(end_column);
48        self
49    }
50}
51
52/// Source file information for stack frames.
53///
54/// Corresponds to the DAP `Source` type used in `stackTrace` and breakpoint responses.
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56pub struct Source {
57    /// Optional display name for the source file (typically the file's base name).
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub name: Option<String>,
60    /// Absolute or workspace-relative path to the source file.
61    pub path: String,
62    /// Optional DAP source reference for sources that have no file path.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub source_reference: Option<i32>,
65}
66
67impl Source {
68    /// Create a `Source` from a file path, deriving the display name from the final component.
69    #[must_use]
70    pub fn new(path: impl Into<String>) -> Self {
71        let path = path.into();
72        let path_name = std::path::Path::new(&path)
73            .file_name()
74            .and_then(|name| name.to_str())
75            .map(ToOwned::to_owned);
76        let name = if path_name.as_deref() == Some(path.as_str()) && path.contains('\\') {
77            path.rsplit('\\').find(|segment| !segment.is_empty()).map(ToOwned::to_owned)
78        } else {
79            path_name
80        };
81
82        Self { name, path, source_reference: None }
83    }
84}
85
86/// Variable information returned by the debug adapter.
87///
88/// Corresponds to the DAP `Variable` type in `variables` responses.
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90#[serde(rename_all = "camelCase")]
91pub struct Variable {
92    /// The variable's display name (e.g. `"$x"`, `"@arr"`).
93    pub name: String,
94    /// The variable's value rendered as a string for display.
95    pub value: String,
96    /// Optional type hint for the variable (e.g. `"SCALAR"`, `"ARRAY"`, `"HASH"`).
97    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
98    pub type_: Option<String>,
99    /// Reference handle for fetching nested variables; 0 means the variable has no children.
100    pub variables_reference: i32,
101    /// Hint for how many named child variables this variable has, if structured.
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub named_variables: Option<i32>,
104    /// Hint for how many indexed child variables this variable has, if it is an array.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub indexed_variables: Option<i32>,
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn stack_frame_new_defaults() {
115        let src = Source::new("/path/to/script.pl");
116        let frame = StackFrame::new(1, "main::foo", src, 42);
117        assert_eq!(frame.id, 1);
118        assert_eq!(frame.name, "main::foo");
119        assert_eq!(frame.line, 42);
120        assert_eq!(frame.column, 1);
121        assert!(frame.end_line.is_none());
122        assert!(frame.end_column.is_none());
123    }
124
125    #[test]
126    fn stack_frame_with_column_and_end() {
127        let src = Source::new("/a.pl");
128        let frame = StackFrame::new(2, "foo", src, 10).with_column(5).with_end(10, 20);
129        assert_eq!(frame.column, 5);
130        assert_eq!(frame.end_line, Some(10));
131        assert_eq!(frame.end_column, Some(20));
132    }
133
134    #[test]
135    fn source_new_extracts_filename() {
136        let src = Source::new("/path/to/Module.pm");
137        assert_eq!(src.path, "/path/to/Module.pm");
138        assert_eq!(src.name, Some("Module.pm".to_string()));
139        assert!(src.source_reference.is_none());
140    }
141
142    #[test]
143    fn stack_frame_serde_round_trip() -> serde_json::Result<()> {
144        let src = Source::new("/script.pl");
145        let frame = StackFrame::new(1, "run", src, 5);
146        let json = serde_json::to_string(&frame)?;
147        let back: StackFrame = serde_json::from_str(&json)?;
148        assert_eq!(back.id, 1);
149        assert_eq!(back.line, 5);
150        Ok(())
151    }
152
153    #[test]
154    fn stack_frame_optional_fields_omitted_in_json() -> serde_json::Result<()> {
155        let src = Source::new("/a.pl");
156        let frame = StackFrame::new(1, "foo", src, 1);
157        let json = serde_json::to_string(&frame)?;
158        assert!(!json.contains("endLine"), "endLine should be absent: {json}");
159        assert!(!json.contains("endColumn"), "endColumn should be absent: {json}");
160        Ok(())
161    }
162
163    #[test]
164    fn variable_type_field_serializes_as_type_not_type_underscore() -> serde_json::Result<()> {
165        let var = Variable {
166            name: "$x".to_string(),
167            value: "42".to_string(),
168            type_: Some("SCALAR".to_string()),
169            variables_reference: 0,
170            named_variables: None,
171            indexed_variables: None,
172        };
173        let json = serde_json::to_string(&var)?;
174        assert!(json.contains("\"type\":"), "must serialize as 'type' not 'type_': {json}");
175        assert!(!json.contains("type_"), "must not leak Rust field name: {json}");
176        Ok(())
177    }
178
179    #[test]
180    fn variable_optional_fields_omitted_when_none() -> serde_json::Result<()> {
181        let var = Variable {
182            name: "$x".to_string(),
183            value: "1".to_string(),
184            type_: None,
185            variables_reference: 0,
186            named_variables: None,
187            indexed_variables: None,
188        };
189        let json = serde_json::to_string(&var)?;
190        assert!(!json.contains("namedVariables"), "absent: {json}");
191        assert!(!json.contains("indexedVariables"), "absent: {json}");
192        Ok(())
193    }
194
195    #[test]
196    fn variable_serde_round_trip() -> serde_json::Result<()> {
197        let var = Variable {
198            name: "@arr".to_string(),
199            value: "(3 elements)".to_string(),
200            type_: Some("ARRAY".to_string()),
201            variables_reference: 7,
202            named_variables: None,
203            indexed_variables: Some(3),
204        };
205        let json = serde_json::to_string(&var)?;
206        let back: Variable = serde_json::from_str(&json)?;
207        assert_eq!(back.variables_reference, 7);
208        assert_eq!(back.indexed_variables, Some(3));
209        Ok(())
210    }
211}