use crate::protocol::{Message, Notification, Request, RequestId, Response};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, thiserror::Error)]
pub enum ValidationError {
#[error("orphan response: no request found for ID {id:?}")]
OrphanResponse {
id: RequestId,
},
#[error("duplicate request ID: {id:?}")]
DuplicateRequestId {
id: RequestId,
},
#[error("unmatched request: {method} (ID: {id:?})")]
UnmatchedRequest {
id: RequestId,
method: String,
},
#[error("unknown method: {method}")]
UnknownMethod {
method: String,
},
#[error("invalid sequence: {message}")]
InvalidSequence {
message: String,
},
#[error("missing initialization: {message}")]
MissingInitialization {
message: String,
},
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<String>,
pub stats: ValidationStats,
}
impl ValidationResult {
#[must_use]
pub fn pass() -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
stats: ValidationStats::default(),
}
}
#[must_use]
pub fn fail(errors: Vec<ValidationError>) -> Self {
Self {
valid: false,
errors,
warnings: Vec::new(),
stats: ValidationStats::default(),
}
}
pub fn add_warning(&mut self, warning: impl Into<String>) {
self.warnings.push(warning.into());
}
}
#[derive(Debug, Clone, Default)]
pub struct ValidationStats {
pub total_messages: usize,
pub requests: usize,
pub responses: usize,
pub notifications: usize,
pub matched_pairs: usize,
}
#[derive(Debug)]
pub struct ProtocolValidator {
known_request_methods: HashSet<String>,
known_notification_methods: HashSet<String>,
pending_requests: HashMap<RequestId, String>,
seen_request_ids: HashSet<RequestId>,
initialized: bool,
errors: Vec<ValidationError>,
warnings: Vec<String>,
stats: ValidationStats,
strict_mode: bool,
}
impl Default for ProtocolValidator {
fn default() -> Self {
Self::new()
}
}
impl ProtocolValidator {
#[must_use]
pub fn new() -> Self {
let mut validator = Self {
known_request_methods: HashSet::new(),
known_notification_methods: HashSet::new(),
pending_requests: HashMap::new(),
seen_request_ids: HashSet::new(),
initialized: false,
errors: Vec::new(),
warnings: Vec::new(),
stats: ValidationStats::default(),
strict_mode: false,
};
validator.register_request_methods(&[
"initialize",
"ping",
"tools/list",
"tools/call",
"resources/list",
"resources/read",
"resources/subscribe",
"resources/unsubscribe",
"prompts/list",
"prompts/get",
"logging/setLevel",
"completion/complete",
"sampling/createMessage",
"roots/list",
]);
validator.register_notification_methods(&[
"initialized",
"notifications/cancelled",
"notifications/progress",
"notifications/message",
"notifications/resources/updated",
"notifications/resources/list_changed",
"notifications/tools/list_changed",
"notifications/prompts/list_changed",
"notifications/roots/list_changed",
]);
validator
}
#[must_use]
pub fn strict(mut self) -> Self {
self.strict_mode = true;
self
}
pub fn register_request_methods(&mut self, methods: &[&str]) {
for method in methods {
self.known_request_methods.insert((*method).to_string());
}
}
pub fn register_notification_methods(&mut self, methods: &[&str]) {
for method in methods {
self.known_notification_methods
.insert((*method).to_string());
}
}
pub fn validate(&mut self, message: &Message) {
self.stats.total_messages += 1;
match message {
Message::Request(req) => self.validate_request(req),
Message::Response(res) => self.validate_response(res),
Message::Notification(notif) => self.validate_notification(notif),
}
}
fn validate_request(&mut self, request: &Request) {
self.stats.requests += 1;
if self.seen_request_ids.contains(&request.id) {
self.errors.push(ValidationError::DuplicateRequestId {
id: request.id.clone(),
});
}
self.seen_request_ids.insert(request.id.clone());
self.pending_requests
.insert(request.id.clone(), request.method.to_string());
let method = request.method.as_ref();
if !self.known_request_methods.contains(method) {
if self.strict_mode {
self.errors.push(ValidationError::UnknownMethod {
method: method.to_string(),
});
} else {
self.warnings
.push(format!("Unknown request method: {method}"));
}
}
if method == "initialize" {
if self.initialized {
self.warnings
.push("Duplicate initialize request".to_string());
}
} else if !self.initialized && method != "ping" {
self.warnings
.push(format!("Request before initialization: {method}"));
}
}
fn validate_response(&mut self, response: &Response) {
self.stats.responses += 1;
if self.pending_requests.remove(&response.id).is_some() {
self.stats.matched_pairs += 1;
} else {
self.errors.push(ValidationError::OrphanResponse {
id: response.id.clone(),
});
}
}
fn validate_notification(&mut self, notification: &Notification) {
self.stats.notifications += 1;
let method = notification.method.as_ref();
if !self.known_notification_methods.contains(method) {
if self.strict_mode {
self.errors.push(ValidationError::UnknownMethod {
method: method.to_string(),
});
} else {
self.warnings
.push(format!("Unknown notification method: {method}"));
}
}
if method == "initialized" {
self.initialized = true;
}
}
pub fn check_unmatched_requests(&mut self) {
for (id, method) in self.pending_requests.drain() {
self.errors
.push(ValidationError::UnmatchedRequest { id, method });
}
}
#[must_use]
pub fn result(&self) -> ValidationResult {
ValidationResult {
valid: self.errors.is_empty(),
errors: self.errors.clone(),
warnings: self.warnings.clone(),
stats: self.stats.clone(),
}
}
#[must_use]
pub fn finalize(mut self) -> ValidationResult {
self.check_unmatched_requests();
self.result()
}
pub fn reset(&mut self) {
self.pending_requests.clear();
self.seen_request_ids.clear();
self.initialized = false;
self.errors.clear();
self.warnings.clear();
self.stats = ValidationStats::default();
}
}
#[must_use]
pub fn validate_message_sequence(messages: &[Message]) -> ValidationResult {
let mut validator = ProtocolValidator::new();
for msg in messages {
validator.validate(msg);
}
validator.finalize()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_sequence() {
let messages = vec![
Message::Request(Request::new("initialize", 1)),
Message::Response(Response::success(RequestId::from(1), serde_json::json!({}))),
Message::Notification(Notification::new("initialized")),
Message::Request(Request::new("tools/list", 2)),
Message::Response(Response::success(
RequestId::from(2),
serde_json::json!({ "tools": [] }),
)),
];
let result = validate_message_sequence(&messages);
assert!(result.valid);
assert!(result.errors.is_empty());
assert_eq!(result.stats.matched_pairs, 2);
}
#[test]
fn test_orphan_response() {
let messages = vec![Message::Response(Response::success(
RequestId::from(999),
serde_json::json!({}),
))];
let result = validate_message_sequence(&messages);
assert!(!result.valid);
assert!(
result
.errors
.iter()
.any(|e| matches!(e, ValidationError::OrphanResponse { .. }))
);
}
#[test]
fn test_duplicate_request_id() {
let messages = vec![
Message::Request(Request::new("ping", 1)),
Message::Request(Request::new("ping", 1)), ];
let result = validate_message_sequence(&messages);
assert!(!result.valid);
assert!(
result
.errors
.iter()
.any(|e| matches!(e, ValidationError::DuplicateRequestId { .. }))
);
}
#[test]
fn test_unmatched_request() {
let messages = vec![
Message::Request(Request::new("ping", 1)),
];
let result = validate_message_sequence(&messages);
assert!(!result.valid);
assert!(
result
.errors
.iter()
.any(|e| matches!(e, ValidationError::UnmatchedRequest { .. }))
);
}
#[test]
fn test_strict_mode() {
let mut validator = ProtocolValidator::new().strict();
validator.validate(&Message::Request(Request::new("unknown/method", 1)));
let result = validator.result();
assert!(!result.valid);
assert!(
result
.errors
.iter()
.any(|e| matches!(e, ValidationError::UnknownMethod { .. }))
);
}
}