1use serde::{Deserialize, Serialize};
4
5#[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#[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#[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}