use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum LifecycleState {
#[default]
Uninitialized,
Initializing,
Running,
ShuttingDown,
Exited,
}
impl LifecycleState {
#[must_use]
pub fn is_request_allowed(&self, method: &str) -> bool {
match self {
Self::Uninitialized => method == "initialize",
Self::Initializing | Self::ShuttingDown | Self::Exited => false,
Self::Running => method != "initialize",
}
}
#[must_use]
pub fn is_notification_allowed(&self, method: &str) -> bool {
match self {
Self::Uninitialized | Self::ShuttingDown => method == "exit",
Self::Initializing => method == "initialized",
Self::Running => true,
Self::Exited => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum ExitCode {
Success = 0,
Error = 1,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ProtocolError {
#[error("expected initialize request, got: {0}")]
ExpectedInitialize(String),
#[error("expected initialized notification, got: {0}")]
ExpectedInitialized(String),
#[error("connection disconnected unexpectedly")]
Disconnected,
#[error("received request after shutdown: {0}")]
AfterShutdown(String),
#[error("timed out waiting for initialized notification (60s)")]
InitializeTimeout,
#[error("timed out waiting for response to server request")]
RequestTimeout,
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lifecycle_state_default_is_uninitialized() {
assert_eq!(LifecycleState::default(), LifecycleState::Uninitialized);
}
#[test]
fn lifecycle_state_is_copy() {
let state = LifecycleState::Running;
let copy = state;
assert_eq!(state, copy);
}
#[test]
fn uninitialized_allows_only_initialize_request() {
let state = LifecycleState::Uninitialized;
assert!(state.is_request_allowed("initialize"));
assert!(!state.is_request_allowed("shutdown"));
assert!(!state.is_request_allowed("textDocument/hover"));
assert!(!state.is_request_allowed("workspace/symbol"));
}
#[test]
fn initializing_allows_no_requests() {
let state = LifecycleState::Initializing;
assert!(!state.is_request_allowed("initialize"));
assert!(!state.is_request_allowed("shutdown"));
assert!(!state.is_request_allowed("textDocument/hover"));
}
#[test]
fn running_allows_all_requests_except_initialize() {
let state = LifecycleState::Running;
assert!(!state.is_request_allowed("initialize"));
assert!(state.is_request_allowed("shutdown"));
assert!(state.is_request_allowed("textDocument/hover"));
assert!(state.is_request_allowed("textDocument/completion"));
assert!(state.is_request_allowed("workspace/symbol"));
}
#[test]
fn shutting_down_allows_no_requests() {
let state = LifecycleState::ShuttingDown;
assert!(!state.is_request_allowed("initialize"));
assert!(!state.is_request_allowed("shutdown"));
assert!(!state.is_request_allowed("textDocument/hover"));
}
#[test]
fn exited_allows_no_requests() {
let state = LifecycleState::Exited;
assert!(!state.is_request_allowed("initialize"));
assert!(!state.is_request_allowed("shutdown"));
assert!(!state.is_request_allowed("textDocument/hover"));
}
#[test]
fn uninitialized_allows_only_exit_notification() {
let state = LifecycleState::Uninitialized;
assert!(state.is_notification_allowed("exit"));
assert!(!state.is_notification_allowed("initialized"));
assert!(!state.is_notification_allowed("textDocument/didOpen"));
}
#[test]
fn initializing_allows_only_initialized_notification() {
let state = LifecycleState::Initializing;
assert!(state.is_notification_allowed("initialized"));
assert!(!state.is_notification_allowed("exit"));
assert!(!state.is_notification_allowed("textDocument/didOpen"));
}
#[test]
fn running_allows_all_notifications() {
let state = LifecycleState::Running;
assert!(state.is_notification_allowed("exit"));
assert!(state.is_notification_allowed("initialized"));
assert!(state.is_notification_allowed("textDocument/didOpen"));
assert!(state.is_notification_allowed("textDocument/didChange"));
assert!(state.is_notification_allowed("$/cancelRequest"));
}
#[test]
fn shutting_down_allows_only_exit_notification() {
let state = LifecycleState::ShuttingDown;
assert!(state.is_notification_allowed("exit"));
assert!(!state.is_notification_allowed("initialized"));
assert!(!state.is_notification_allowed("textDocument/didOpen"));
}
#[test]
fn exited_allows_no_notifications() {
let state = LifecycleState::Exited;
assert!(!state.is_notification_allowed("exit"));
assert!(!state.is_notification_allowed("initialized"));
assert!(!state.is_notification_allowed("textDocument/didOpen"));
}
#[test]
fn exit_code_success_is_zero() {
assert_eq!(ExitCode::Success as i32, 0);
}
#[test]
fn exit_code_error_is_one() {
assert_eq!(ExitCode::Error as i32, 1);
}
#[test]
fn exit_code_is_copy() {
let code = ExitCode::Success;
let copy = code;
assert_eq!(code, copy);
}
#[test]
fn protocol_error_expected_initialize_message() {
let err = ProtocolError::ExpectedInitialize("textDocument/hover".to_string());
let msg = err.to_string();
assert!(msg.contains("expected initialize request"));
assert!(msg.contains("textDocument/hover"));
}
#[test]
fn protocol_error_expected_initialized_message() {
let err = ProtocolError::ExpectedInitialized("textDocument/didOpen".to_string());
let msg = err.to_string();
assert!(msg.contains("expected initialized notification"));
assert!(msg.contains("textDocument/didOpen"));
}
#[test]
fn protocol_error_disconnected_message() {
let err = ProtocolError::Disconnected;
let msg = err.to_string();
assert!(msg.contains("disconnected unexpectedly"));
}
#[test]
fn protocol_error_after_shutdown_message() {
let err = ProtocolError::AfterShutdown("textDocument/hover".to_string());
let msg = err.to_string();
assert!(msg.contains("after shutdown"));
assert!(msg.contains("textDocument/hover"));
}
#[test]
fn protocol_error_request_timeout_message() {
let err = ProtocolError::RequestTimeout;
let msg = err.to_string();
assert!(msg.contains("timed out waiting for response to server request"));
}
#[test]
fn protocol_error_initialize_timeout_message() {
let err = ProtocolError::InitializeTimeout;
let msg = err.to_string();
assert!(msg.contains("timed out waiting for initialized notification"));
assert!(msg.contains("60s"));
}
#[test]
fn protocol_error_io_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "connection lost");
let err: ProtocolError = io_err.into();
let msg = err.to_string();
assert!(msg.contains("I/O error"));
assert!(msg.contains("connection lost"));
}
#[test]
fn protocol_error_is_debug() {
let err = ProtocolError::Disconnected;
let debug = format!("{err:?}");
assert!(debug.contains("Disconnected"));
}
}