use std::sync::Arc;
use crate::domain::RequestId;
use crate::domain::ir::extensions::Extensions;
use crate::domain::ir::features::FeatureSet;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MethodName(Arc<str>);
impl MethodName {
pub fn new(name: &str) -> Result<Self, MethodNameError> {
if name.is_empty() {
return Err(MethodNameError::Empty);
}
if name.starts_with('/') || name.ends_with('/') {
return Err(MethodNameError::NoSegment {
name: name.to_owned(),
});
}
if name.split('/').any(|segment| segment.trim() != segment) {
return Err(MethodNameError::Whitespace {
name: name.to_owned(),
});
}
Ok(Self(Arc::from(name)))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_notification(&self) -> bool {
self.0.starts_with("notifications/")
}
}
impl core::fmt::Display for MethodName {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(&self.0)
}
}
impl core::str::FromStr for MethodName {
type Err = MethodNameError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Self::new(text)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MethodNameError {
#[error("method name is empty")]
Empty,
#[error("method name {name:?} has an empty segment")]
NoSegment {
name: String,
},
#[error("method name {name:?} has a segment with surrounding whitespace")]
Whitespace {
name: String,
},
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct IrParams {
entries: Extensions,
}
impl IrParams {
#[must_use]
pub const fn empty() -> Self {
Self {
entries: Extensions::empty(),
}
}
#[must_use]
pub const fn from_extensions(entries: Extensions) -> Self {
Self { entries }
}
#[must_use]
pub fn with(
mut self,
name: impl Into<String>,
value: crate::domain::ir::ExtensionValue,
) -> Self {
self.entries.insert(name, value);
self
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&crate::domain::ir::ExtensionValue> {
self.entries.get(name)
}
#[must_use]
pub const fn as_extensions(&self) -> &Extensions {
&self.entries
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IrRequest {
pub method: MethodName,
pub id: Option<RequestId>,
pub params: IrParams,
pub extensions: Extensions,
}
impl IrRequest {
#[must_use]
pub const fn new(method: MethodName, id: RequestId) -> Self {
Self {
method,
id: Some(id),
params: IrParams::empty(),
extensions: Extensions::empty(),
}
}
#[must_use]
pub fn with_params(mut self, params: IrParams) -> Self {
self.params = params;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IrNotification {
pub method: MethodName,
pub params: IrParams,
pub extensions: Extensions,
}
impl IrNotification {
#[must_use]
pub fn new(method: MethodName) -> Self {
debug_assert!(
method.is_notification(),
"{method} is not a notification method",
);
Self {
method,
params: IrParams::empty(),
extensions: Extensions::empty(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IrResult {
pub id: RequestId,
pub params: IrParams,
pub extensions: Extensions,
}
impl IrResult {
#[must_use]
pub const fn new(id: RequestId) -> Self {
Self {
id,
params: IrParams::empty(),
extensions: Extensions::empty(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum IrErrorCode {
ParseError,
InvalidRequest,
MethodNotFound,
InvalidParams,
InternalError,
Implementation(i64),
Other(i64),
}
impl IrErrorCode {
pub const UNSUPPORTED_PROTOCOL_VERSION: Self = Self::Implementation(-32022);
pub const HEADER_MISMATCH: Self = Self::Implementation(-32020);
pub const MISSING_REQUIRED_CLIENT_CAPABILITY: Self = Self::Implementation(-32021);
#[must_use]
pub const fn code(self) -> i64 {
match self {
Self::ParseError => -32700,
Self::InvalidRequest => -32600,
Self::MethodNotFound => -32601,
Self::InvalidParams => -32602,
Self::InternalError => -32603,
Self::Implementation(code) | Self::Other(code) => code,
}
}
#[must_use]
pub fn from_code(code: i64) -> Self {
match code {
-32700 => Self::ParseError,
-32600 => Self::InvalidRequest,
-32601 => Self::MethodNotFound,
-32602 => Self::InvalidParams,
-32603 => Self::InternalError,
code if (-32099..=-32000).contains(&code) => Self::Implementation(code),
other => Self::Other(other),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IrError {
pub id: Option<RequestId>,
pub code: IrErrorCode,
pub message: String,
pub data: Option<crate::domain::ir::JsonObject>,
pub extensions: Extensions,
}
impl IrError {
#[must_use]
pub fn new(id: Option<RequestId>, code: IrErrorCode, message: impl Into<String>) -> Self {
Self {
id,
code,
message: message.into(),
data: None,
extensions: Extensions::empty(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum IrMessage {
Request(IrRequest),
Notification(IrNotification),
Result(IrResult),
Error(IrError),
}
impl IrMessage {
#[must_use]
pub const fn method(&self) -> Option<&MethodName> {
match self {
Self::Request(request) => Some(&request.method),
Self::Notification(notification) => Some(¬ification.method),
Self::Result(_) | Self::Error(_) => None,
}
}
#[must_use]
pub const fn id(&self) -> Option<&RequestId> {
match self {
Self::Request(request) => request.id.as_ref(),
Self::Notification(_) => None,
Self::Result(result) => Some(&result.id),
Self::Error(error) => error.id.as_ref(),
}
}
#[must_use]
pub const fn expects_reply(&self) -> bool {
matches!(self, Self::Request(_))
}
#[must_use]
pub const fn required_features(&self) -> FeatureSet {
FeatureSet::empty()
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests assert on fallible calls and unwrap with the failing precondition in the message"
)]
use super::{
IrError, IrErrorCode, IrMessage, IrNotification, IrParams, IrRequest, IrResult, MethodName,
MethodNameError,
};
use crate::domain::RequestId;
use crate::domain::ir::ExtensionValue;
fn id() -> RequestId {
RequestId::number(1)
}
#[test]
fn method_names_validate_their_shape() {
assert!(MethodName::new("tools/list").is_ok());
assert!(MethodName::new("notifications/initialized").is_ok());
assert!(MethodName::new("ping").is_ok());
assert_eq!(MethodName::new("").unwrap_err(), MethodNameError::Empty);
assert_eq!(
MethodName::new("/tools/list").unwrap_err(),
MethodNameError::NoSegment {
name: "/tools/list".into()
}
);
assert_eq!(
MethodName::new("tools/list/").unwrap_err(),
MethodNameError::NoSegment {
name: "tools/list/".into()
}
);
assert_eq!(
MethodName::new("tools/ list").unwrap_err(),
MethodNameError::Whitespace {
name: "tools/ list".into()
}
);
assert_eq!(
MethodName::new("tools/list ").unwrap_err(),
MethodNameError::Whitespace {
name: "tools/list ".into()
}
);
}
#[test]
fn method_names_round_trip() {
let name = MethodName::new("resources/read").unwrap();
assert_eq!(name.as_str(), "resources/read");
assert_eq!(name.to_string(), "resources/read");
assert_eq!(
"tools/call".parse::<MethodName>().unwrap().as_str(),
"tools/call"
);
}
#[test]
fn the_notification_prefix_is_the_convention() {
assert!(
MethodName::new("notifications/cancelled")
.unwrap()
.is_notification()
);
assert!(!MethodName::new("tools/list").unwrap().is_notification());
assert!(
!MethodName::new("notificationsish/x")
.unwrap()
.is_notification()
);
}
#[test]
fn params_hold_unmodelled_fields() {
let params = IrParams::empty()
.with("cursor", ExtensionValue::String("abc".into()))
.with("n", ExtensionValue::Number("5".into()));
assert_eq!(
params.get("cursor"),
Some(&ExtensionValue::String("abc".into()))
);
assert!(params.get("missing").is_none());
}
#[test]
fn requests_carry_a_method_an_id_and_params() {
let request = IrRequest::new(MethodName::new("tools/call").unwrap(), id())
.with_params(IrParams::empty().with("name", ExtensionValue::String("t".into())));
assert_eq!(request.method.as_str(), "tools/call");
assert_eq!(request.id.as_ref(), Some(&id()));
assert!(request.params.get("name").is_some());
}
#[test]
fn notifications_have_no_id_and_expects_reply_is_false() {
let notification =
IrNotification::new(MethodName::new("notifications/initialized").unwrap());
let message = IrMessage::Notification(notification);
assert!(message.id().is_none());
assert!(!message.expects_reply());
assert_eq!(
message.method().unwrap().as_str(),
"notifications/initialized"
);
}
#[test]
fn a_request_expects_a_reply_and_carries_its_id() {
let message = IrMessage::Request(IrRequest::new(MethodName::new("ping").unwrap(), id()));
assert!(message.expects_reply());
assert_eq!(message.id(), Some(&id()));
}
#[test]
fn a_result_points_back_at_its_request() {
let result = IrResult::new(id());
let message = IrMessage::Result(result);
assert_eq!(message.id(), Some(&id()));
assert!(!message.expects_reply());
assert!(message.method().is_none());
}
#[test]
fn a_parse_error_may_have_no_id() {
let error = IrError::new(None, IrErrorCode::ParseError, "bad json");
let message = IrMessage::Error(error);
assert!(message.id().is_none());
assert!(!message.expects_reply());
}
#[test]
fn an_error_with_an_id_points_back_at_its_request() {
let error = IrError::new(Some(id()), IrErrorCode::InvalidParams, "nope");
let message = IrMessage::Error(error);
assert_eq!(message.id(), Some(&id()));
}
#[test]
fn error_codes_round_trip() {
for code in [
IrErrorCode::ParseError,
IrErrorCode::InvalidRequest,
IrErrorCode::MethodNotFound,
IrErrorCode::InvalidParams,
IrErrorCode::InternalError,
] {
assert_eq!(IrErrorCode::from_code(code.code()), code, "{code:?}");
}
for raw in [-32000_i64, -32001, -32050, -32099] {
let code = IrErrorCode::Implementation(raw);
assert_eq!(code.code(), raw);
assert_eq!(IrErrorCode::from_code(raw), code);
}
for raw in -32099_i64..=-32000 {
assert!(
matches!(IrErrorCode::from_code(raw), IrErrorCode::Implementation(_)),
"{raw}",
);
}
}
#[test]
fn a_code_outside_every_reserved_range_is_other() {
for raw in [0_i64, 1, -1, -31999, 12345] {
assert_eq!(
IrErrorCode::from_code(raw),
IrErrorCode::Other(raw),
"{raw}",
);
assert_eq!(IrErrorCode::Other(raw).code(), raw);
}
}
#[test]
fn no_envelope_shape_requires_a_feature() {
let messages = [
IrMessage::Request(IrRequest::new(MethodName::new("ping").unwrap(), id())),
IrMessage::Notification(IrNotification::new(
MethodName::new("notifications/initialized").unwrap(),
)),
IrMessage::Result(IrResult::new(id())),
IrMessage::Error(IrError::new(Some(id()), IrErrorCode::InternalError, "x")),
];
for message in messages {
assert!(message.required_features().is_empty(), "{message:?}");
}
}
}