1use crate::protocol::{Message, Notification, Request, RequestId, Response};
7use std::collections::{HashMap, HashSet};
8
9#[derive(Debug, Clone, thiserror::Error)]
11pub enum ValidationError {
12 #[error("orphan response: no request found for ID {id:?}")]
14 OrphanResponse {
15 id: RequestId,
17 },
18
19 #[error("duplicate request ID: {id:?}")]
21 DuplicateRequestId {
22 id: RequestId,
24 },
25
26 #[error("unmatched request: {method} (ID: {id:?})")]
28 UnmatchedRequest {
29 id: RequestId,
31 method: String,
33 },
34
35 #[error("unknown method: {method}")]
37 UnknownMethod {
38 method: String,
40 },
41
42 #[error("invalid sequence: {message}")]
44 InvalidSequence {
45 message: String,
47 },
48
49 #[error("missing initialization: {message}")]
51 MissingInitialization {
52 message: String,
54 },
55}
56
57#[derive(Debug, Clone)]
59pub struct ValidationResult {
60 pub valid: bool,
62 pub errors: Vec<ValidationError>,
64 pub warnings: Vec<String>,
66 pub stats: ValidationStats,
68}
69
70impl ValidationResult {
71 #[must_use]
73 pub fn pass() -> Self {
74 Self {
75 valid: true,
76 errors: Vec::new(),
77 warnings: Vec::new(),
78 stats: ValidationStats::default(),
79 }
80 }
81
82 #[must_use]
84 pub fn fail(errors: Vec<ValidationError>) -> Self {
85 Self {
86 valid: false,
87 errors,
88 warnings: Vec::new(),
89 stats: ValidationStats::default(),
90 }
91 }
92
93 pub fn add_warning(&mut self, warning: impl Into<String>) {
95 self.warnings.push(warning.into());
96 }
97}
98
99#[derive(Debug, Clone, Default)]
101pub struct ValidationStats {
102 pub total_messages: usize,
104 pub requests: usize,
106 pub responses: usize,
108 pub notifications: usize,
110 pub matched_pairs: usize,
112}
113
114#[derive(Debug)]
122pub struct ProtocolValidator {
123 known_request_methods: HashSet<String>,
125 known_notification_methods: HashSet<String>,
127 pending_requests: HashMap<RequestId, String>,
129 seen_request_ids: HashSet<RequestId>,
131 initialized: bool,
133 errors: Vec<ValidationError>,
135 warnings: Vec<String>,
137 stats: ValidationStats,
139 strict_mode: bool,
141}
142
143impl Default for ProtocolValidator {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149impl ProtocolValidator {
150 #[must_use]
152 pub fn new() -> Self {
153 let mut validator = Self {
154 known_request_methods: HashSet::new(),
155 known_notification_methods: HashSet::new(),
156 pending_requests: HashMap::new(),
157 seen_request_ids: HashSet::new(),
158 initialized: false,
159 errors: Vec::new(),
160 warnings: Vec::new(),
161 stats: ValidationStats::default(),
162 strict_mode: false,
163 };
164
165 validator.register_request_methods(&[
167 "initialize",
168 "ping",
169 "tools/list",
170 "tools/call",
171 "resources/list",
172 "resources/read",
173 "resources/subscribe",
174 "resources/unsubscribe",
175 "prompts/list",
176 "prompts/get",
177 "logging/setLevel",
178 "completion/complete",
179 "sampling/createMessage",
180 "roots/list",
181 "elicitation/create",
182 "resources/templates/list",
183 "tasks/get",
184 "tasks/list",
185 "tasks/cancel",
186 "tasks/result",
187 ]);
188
189 validator.register_notification_methods(&[
190 "notifications/initialized",
191 "notifications/cancelled",
192 "notifications/progress",
193 "notifications/message",
194 "notifications/resources/updated",
195 "notifications/resources/list_changed",
196 "notifications/tools/list_changed",
197 "notifications/prompts/list_changed",
198 "notifications/roots/list_changed",
199 "notifications/elicitation/complete",
200 "notifications/tasks/status",
201 ]);
202
203 validator
204 }
205
206 #[must_use]
208 pub fn strict(mut self) -> Self {
209 self.strict_mode = true;
210 self
211 }
212
213 pub fn register_request_methods(&mut self, methods: &[&str]) {
215 for method in methods {
216 self.known_request_methods.insert((*method).to_string());
217 }
218 }
219
220 pub fn register_notification_methods(&mut self, methods: &[&str]) {
222 for method in methods {
223 self.known_notification_methods
224 .insert((*method).to_string());
225 }
226 }
227
228 pub fn validate(&mut self, message: &Message) {
230 self.stats.total_messages += 1;
231
232 match message {
233 Message::Request(req) => self.validate_request(req),
234 Message::Response(res) => self.validate_response(res),
235 Message::Notification(notif) => self.validate_notification(notif),
236 }
237 }
238
239 fn validate_request(&mut self, request: &Request) {
240 self.stats.requests += 1;
241
242 if self.seen_request_ids.contains(&request.id) {
244 self.errors.push(ValidationError::DuplicateRequestId {
245 id: request.id.clone(),
246 });
247 }
248 self.seen_request_ids.insert(request.id.clone());
249
250 self.pending_requests
252 .insert(request.id.clone(), request.method.to_string());
253
254 let method = request.method.as_ref();
256 if !self.known_request_methods.contains(method) {
257 if self.strict_mode {
258 self.errors.push(ValidationError::UnknownMethod {
259 method: method.to_string(),
260 });
261 } else {
262 self.warnings
263 .push(format!("Unknown request method: {method}"));
264 }
265 }
266
267 if method == "initialize" {
269 if self.initialized {
270 self.warnings
271 .push("Duplicate initialize request".to_string());
272 }
273 } else if !self.initialized && method != "ping" {
274 self.warnings
275 .push(format!("Request before initialization: {method}"));
276 }
277 }
278
279 fn validate_response(&mut self, response: &Response) {
280 self.stats.responses += 1;
281
282 if self.pending_requests.remove(&response.id).is_some() {
284 self.stats.matched_pairs += 1;
285 } else {
286 self.errors.push(ValidationError::OrphanResponse {
287 id: response.id.clone(),
288 });
289 }
290 }
291
292 fn validate_notification(&mut self, notification: &Notification) {
293 self.stats.notifications += 1;
294
295 let method = notification.method.as_ref();
296
297 if !self.known_notification_methods.contains(method) {
299 if self.strict_mode {
300 self.errors.push(ValidationError::UnknownMethod {
301 method: method.to_string(),
302 });
303 } else {
304 self.warnings
305 .push(format!("Unknown notification method: {method}"));
306 }
307 }
308
309 if method == "notifications/initialized" {
311 self.initialized = true;
312 }
313 }
314
315 pub fn check_unmatched_requests(&mut self) {
317 for (id, method) in self.pending_requests.drain() {
318 self.errors
319 .push(ValidationError::UnmatchedRequest { id, method });
320 }
321 }
322
323 #[must_use]
325 pub fn result(&self) -> ValidationResult {
326 ValidationResult {
327 valid: self.errors.is_empty(),
328 errors: self.errors.clone(),
329 warnings: self.warnings.clone(),
330 stats: self.stats.clone(),
331 }
332 }
333
334 #[must_use]
336 pub fn finalize(mut self) -> ValidationResult {
337 self.check_unmatched_requests();
338 self.result()
339 }
340
341 pub fn reset(&mut self) {
343 self.pending_requests.clear();
344 self.seen_request_ids.clear();
345 self.initialized = false;
346 self.errors.clear();
347 self.warnings.clear();
348 self.stats = ValidationStats::default();
349 }
350}
351
352#[must_use]
354pub fn validate_message_sequence(messages: &[Message]) -> ValidationResult {
355 let mut validator = ProtocolValidator::new();
356
357 for msg in messages {
358 validator.validate(msg);
359 }
360
361 validator.finalize()
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_valid_sequence() {
370 let messages = vec![
371 Message::Request(Request::new("initialize", 1)),
372 Message::Response(Response::success(RequestId::from(1), serde_json::json!({}))),
373 Message::Notification(Notification::new("notifications/initialized")),
374 Message::Request(Request::new("tools/list", 2)),
375 Message::Response(Response::success(
376 RequestId::from(2),
377 serde_json::json!({ "tools": [] }),
378 )),
379 ];
380
381 let result = validate_message_sequence(&messages);
382 assert!(result.valid);
383 assert!(result.errors.is_empty());
384 assert_eq!(result.stats.matched_pairs, 2);
385 }
386
387 #[test]
388 fn test_orphan_response() {
389 let messages = vec![Message::Response(Response::success(
390 RequestId::from(999),
391 serde_json::json!({}),
392 ))];
393
394 let result = validate_message_sequence(&messages);
395 assert!(!result.valid);
396 assert!(
397 result
398 .errors
399 .iter()
400 .any(|e| matches!(e, ValidationError::OrphanResponse { .. }))
401 );
402 }
403
404 #[test]
405 fn test_duplicate_request_id() {
406 let messages = vec![
407 Message::Request(Request::new("ping", 1)),
408 Message::Request(Request::new("ping", 1)), ];
410
411 let result = validate_message_sequence(&messages);
412 assert!(!result.valid);
413 assert!(
414 result
415 .errors
416 .iter()
417 .any(|e| matches!(e, ValidationError::DuplicateRequestId { .. }))
418 );
419 }
420
421 #[test]
422 fn test_unmatched_request() {
423 let messages = vec![
424 Message::Request(Request::new("ping", 1)),
425 ];
427
428 let result = validate_message_sequence(&messages);
429 assert!(!result.valid);
430 assert!(
431 result
432 .errors
433 .iter()
434 .any(|e| matches!(e, ValidationError::UnmatchedRequest { .. }))
435 );
436 }
437
438 #[test]
439 fn test_strict_mode_accepts_conforming_session() {
440 let mut validator = ProtocolValidator::new().strict();
441
442 for msg in [
443 Message::Request(Request::new("initialize", 1)),
444 Message::Response(Response::success(RequestId::from(1), serde_json::json!({}))),
445 Message::Notification(Notification::new("notifications/initialized")),
446 Message::Request(Request::new("tools/call", 2)),
447 Message::Response(Response::success(RequestId::from(2), serde_json::json!({}))),
448 Message::Request(Request::new("tasks/get", 3)),
449 Message::Response(Response::success(RequestId::from(3), serde_json::json!({}))),
450 ] {
451 validator.validate(&msg);
452 }
453
454 let result = validator.finalize();
455 assert!(
456 result.valid,
457 "conforming session rejected: {:?}",
458 result.errors
459 );
460 assert!(
463 !result
464 .warnings
465 .iter()
466 .any(|w| w.contains("before initialization")),
467 "initialized flag never flipped: {:?}",
468 result.warnings
469 );
470 }
471
472 #[test]
473 fn test_strict_mode() {
474 let mut validator = ProtocolValidator::new().strict();
475
476 validator.validate(&Message::Request(Request::new("unknown/method", 1)));
477
478 let result = validator.result();
479 assert!(!result.valid);
480 assert!(
481 result
482 .errors
483 .iter()
484 .any(|e| matches!(e, ValidationError::UnknownMethod { .. }))
485 );
486 }
487}