1use core::fmt;
12
13use mcp_conformance_core::trace::TraceEvent;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct Limits {
19 pub max_events: usize,
21 pub max_line_bytes: usize,
23}
24
25impl Default for Limits {
26 fn default() -> Self {
27 Self {
28 max_events: 100_000,
31 max_line_bytes: 1024 * 1024,
32 }
33 }
34}
35
36#[derive(Debug)]
38#[non_exhaustive]
39pub enum TraceParseError {
40 LineTooLong {
42 line: usize,
44 length: usize,
46 limit: usize,
48 },
49 TooManyEvents {
51 limit: usize,
53 },
54 BlankLine {
57 line: usize,
59 },
60 Malformed {
62 line: usize,
64 source: serde_json::Error,
66 },
67 NonMonotonicSeq {
69 line: usize,
71 seq: u64,
73 previous: u64,
75 },
76}
77
78impl fmt::Display for TraceParseError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::LineTooLong {
82 line,
83 length,
84 limit,
85 } => write!(
86 f,
87 "line {line}: record is {length} bytes, exceeding the {limit}-byte limit"
88 ),
89 Self::TooManyEvents { limit } => {
90 write!(f, "trace exceeds the {limit}-event limit")
91 }
92 Self::BlankLine { line } => {
93 write!(
94 f,
95 "line {line}: blank line (JSON Lines forbids blank records)"
96 )
97 }
98 Self::Malformed { line, source } => {
99 write!(f, "line {line}: not a valid trace event: {source}")
100 }
101 Self::NonMonotonicSeq {
102 line,
103 seq,
104 previous,
105 } => write!(
106 f,
107 "line {line}: seq {seq} is not greater than the previous event's seq {previous}"
108 ),
109 }
110 }
111}
112
113impl core::error::Error for TraceParseError {
114 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
115 match self {
116 Self::Malformed { source, .. } => Some(source),
117 _ => None,
118 }
119 }
120}
121
122pub fn parse_trace(input: &str, limits: &Limits) -> Result<Vec<TraceEvent>, TraceParseError> {
139 let mut events = Vec::new();
140 let mut previous_seq: Option<u64> = None;
141 for (index, line) in input.lines().enumerate() {
142 let line_number = index + 1;
143 if line.len() > limits.max_line_bytes {
144 return Err(TraceParseError::LineTooLong {
145 line: line_number,
146 length: line.len(),
147 limit: limits.max_line_bytes,
148 });
149 }
150 if line.trim().is_empty() {
151 return Err(TraceParseError::BlankLine { line: line_number });
152 }
153 if events.len() >= limits.max_events {
154 return Err(TraceParseError::TooManyEvents {
155 limit: limits.max_events,
156 });
157 }
158 let event: TraceEvent =
159 serde_json::from_str(line).map_err(|source| TraceParseError::Malformed {
160 line: line_number,
161 source,
162 })?;
163 if let Some(previous) = previous_seq
164 && event.seq <= previous
165 {
166 return Err(TraceParseError::NonMonotonicSeq {
167 line: line_number,
168 seq: event.seq,
169 previous,
170 });
171 }
172 previous_seq = Some(event.seq);
173 events.push(event);
174 }
175 Ok(events)
176}
177
178#[cfg(test)]
179#[allow(clippy::unwrap_used)]
180mod tests {
181 use super::*;
182
183 const VALID_EVENT: &str = r#"{"seq":0,"direction":"client-to-server","transport":"stdio","kind":"lifecycle","event":"transport-open"}"#;
184
185 #[test]
186 fn parses_valid_lines_and_empty_documents() {
187 assert!(parse_trace("", &Limits::default()).unwrap().is_empty());
188 let one = parse_trace(VALID_EVENT, &Limits::default()).unwrap();
189 assert_eq!(one.len(), 1);
190 let with_newline = format!("{VALID_EVENT}\n");
192 assert_eq!(
193 parse_trace(&with_newline, &Limits::default())
194 .unwrap()
195 .len(),
196 1
197 );
198 }
199
200 #[test]
201 fn rejects_blank_interior_lines() {
202 let doc = format!("{VALID_EVENT}\n\n");
203 assert!(matches!(
204 parse_trace(&doc, &Limits::default()),
205 Err(TraceParseError::BlankLine { line: 2 })
206 ));
207 }
208
209 #[test]
210 fn rejects_oversized_lines() {
211 let limits = Limits {
212 max_line_bytes: 16,
213 ..Limits::default()
214 };
215 assert!(matches!(
216 parse_trace(VALID_EVENT, &limits),
217 Err(TraceParseError::LineTooLong { line: 1, .. })
218 ));
219 }
220
221 #[test]
222 fn rejects_too_many_events() {
223 let limits = Limits {
224 max_events: 1,
225 ..Limits::default()
226 };
227 let second = VALID_EVENT.replace("\"seq\":0", "\"seq\":1");
228 let doc = format!("{VALID_EVENT}\n{second}");
229 assert!(matches!(
230 parse_trace(&doc, &limits),
231 Err(TraceParseError::TooManyEvents { limit: 1 })
232 ));
233 }
234
235 #[test]
236 fn rejects_malformed_records_with_line_numbers() {
237 let doc = format!("{VALID_EVENT}\n{{\"seq\":1}}");
238 match parse_trace(&doc, &Limits::default()) {
239 Err(TraceParseError::Malformed { line, .. }) => assert_eq!(line, 2),
240 other => panic!("expected malformed at line 2, got {other:?}"),
241 }
242 }
243
244 #[test]
245 fn rejects_non_monotonic_seq() {
246 let duplicate = format!("{VALID_EVENT}\n{VALID_EVENT}");
247 assert!(matches!(
248 parse_trace(&duplicate, &Limits::default()),
249 Err(TraceParseError::NonMonotonicSeq {
250 line: 2,
251 seq: 0,
252 previous: 0
253 })
254 ));
255 }
256
257 #[test]
258 fn error_messages_are_line_addressed() {
259 let doc = format!("{VALID_EVENT}\nnot json");
260 let error = parse_trace(&doc, &Limits::default()).unwrap_err();
261 assert!(error.to_string().starts_with("line 2:"), "{error}");
262 }
263
264 #[test]
265 fn line_exactly_at_the_byte_limit_is_accepted() {
266 let limits = Limits {
268 max_line_bytes: VALID_EVENT.len(),
269 ..Limits::default()
270 };
271 assert_eq!(parse_trace(VALID_EVENT, &limits).unwrap().len(), 1);
272 }
273
274 #[test]
275 fn error_source_is_exposed_for_malformed_records_only() {
276 use core::error::Error as _;
277 let malformed = parse_trace("nope", &Limits::default()).unwrap_err();
278 assert!(malformed.source().is_some());
279 let blank = parse_trace(" \n", &Limits::default()).unwrap_err();
280 assert!(blank.source().is_none());
281 }
282}