Skip to main content

perl_dap_types/
lib.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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7#[serde(rename_all = "camelCase")]
8pub struct StackFrame {
9    pub id: i32,
10    pub name: String,
11    pub source: Source,
12    pub line: i32,
13    pub column: i32,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub end_line: Option<i32>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub end_column: Option<i32>,
18}
19
20impl StackFrame {
21    #[must_use]
22    pub fn new(id: i32, name: impl Into<String>, source: Source, line: i32) -> Self {
23        Self { id, name: name.into(), source, line, column: 1, end_line: None, end_column: None }
24    }
25
26    #[must_use]
27    pub fn with_column(mut self, column: i32) -> Self {
28        self.column = column;
29        self
30    }
31
32    #[must_use]
33    pub fn with_end(mut self, end_line: i32, end_column: i32) -> Self {
34        self.end_line = Some(end_line);
35        self.end_column = Some(end_column);
36        self
37    }
38}
39
40/// Source file information for stack frames.
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct Source {
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub name: Option<String>,
45    pub path: String,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub source_reference: Option<i32>,
48}
49
50impl Source {
51    #[must_use]
52    pub fn new(path: impl Into<String>) -> Self {
53        let path = path.into();
54        let name = std::path::Path::new(&path)
55            .file_name()
56            .and_then(|name| name.to_str())
57            .map(ToOwned::to_owned);
58
59        Self { name, path, source_reference: None }
60    }
61}
62
63/// Variable information returned by the debug adapter.
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65#[serde(rename_all = "camelCase")]
66pub struct Variable {
67    pub name: String,
68    pub value: String,
69    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
70    pub type_: Option<String>,
71    pub variables_reference: i32,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub named_variables: Option<i32>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub indexed_variables: Option<i32>,
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn stack_frame_new_defaults() {
84        let src = Source::new("/path/to/script.pl");
85        let frame = StackFrame::new(1, "main::foo", src, 42);
86        assert_eq!(frame.id, 1);
87        assert_eq!(frame.name, "main::foo");
88        assert_eq!(frame.line, 42);
89        assert_eq!(frame.column, 1);
90        assert!(frame.end_line.is_none());
91        assert!(frame.end_column.is_none());
92    }
93
94    #[test]
95    fn stack_frame_with_column_and_end() {
96        let src = Source::new("/a.pl");
97        let frame = StackFrame::new(2, "foo", src, 10).with_column(5).with_end(10, 20);
98        assert_eq!(frame.column, 5);
99        assert_eq!(frame.end_line, Some(10));
100        assert_eq!(frame.end_column, Some(20));
101    }
102
103    #[test]
104    fn source_new_extracts_filename() {
105        let src = Source::new("/path/to/Module.pm");
106        assert_eq!(src.path, "/path/to/Module.pm");
107        assert_eq!(src.name, Some("Module.pm".to_string()));
108        assert!(src.source_reference.is_none());
109    }
110
111    #[test]
112    fn stack_frame_serde_round_trip() {
113        let src = Source::new("/script.pl");
114        let frame = StackFrame::new(1, "run", src, 5);
115        let json = serde_json::to_string(&frame).unwrap();
116        let back: StackFrame = serde_json::from_str(&json).unwrap();
117        assert_eq!(back.id, 1);
118        assert_eq!(back.line, 5);
119    }
120
121    #[test]
122    fn stack_frame_optional_fields_omitted_in_json() {
123        let src = Source::new("/a.pl");
124        let frame = StackFrame::new(1, "foo", src, 1);
125        let json = serde_json::to_string(&frame).unwrap();
126        assert!(!json.contains("endLine"), "endLine should be absent: {json}");
127        assert!(!json.contains("endColumn"), "endColumn should be absent: {json}");
128    }
129
130    #[test]
131    fn variable_type_field_serializes_as_type_not_type_underscore() {
132        let var = Variable {
133            name: "$x".to_string(),
134            value: "42".to_string(),
135            type_: Some("SCALAR".to_string()),
136            variables_reference: 0,
137            named_variables: None,
138            indexed_variables: None,
139        };
140        let json = serde_json::to_string(&var).unwrap();
141        assert!(json.contains("\"type\":"), "must serialize as 'type' not 'type_': {json}");
142        assert!(!json.contains("type_"), "must not leak Rust field name: {json}");
143    }
144
145    #[test]
146    fn variable_optional_fields_omitted_when_none() {
147        let var = Variable {
148            name: "$x".to_string(),
149            value: "1".to_string(),
150            type_: None,
151            variables_reference: 0,
152            named_variables: None,
153            indexed_variables: None,
154        };
155        let json = serde_json::to_string(&var).unwrap();
156        assert!(!json.contains("namedVariables"), "absent: {json}");
157        assert!(!json.contains("indexedVariables"), "absent: {json}");
158    }
159
160    #[test]
161    fn variable_serde_round_trip() {
162        let var = Variable {
163            name: "@arr".to_string(),
164            value: "(3 elements)".to_string(),
165            type_: Some("ARRAY".to_string()),
166            variables_reference: 7,
167            named_variables: None,
168            indexed_variables: Some(3),
169        };
170        let json = serde_json::to_string(&var).unwrap();
171        let back: Variable = serde_json::from_str(&json).unwrap();
172        assert_eq!(back.variables_reference, 7);
173        assert_eq!(back.indexed_variables, Some(3));
174    }
175}