1use std::sync::Arc;
30
31use crate::domain::RequestId;
32use crate::domain::ir::extensions::Extensions;
33use crate::domain::ir::features::FeatureSet;
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub struct MethodName(Arc<str>);
44
45impl MethodName {
46 pub fn new(name: &str) -> Result<Self, MethodNameError> {
56 if name.is_empty() {
57 return Err(MethodNameError::Empty);
58 }
59 if name.starts_with('/') || name.ends_with('/') {
60 return Err(MethodNameError::NoSegment {
61 name: name.to_owned(),
62 });
63 }
64 if name.split('/').any(|segment| segment.trim() != segment) {
65 return Err(MethodNameError::Whitespace {
66 name: name.to_owned(),
67 });
68 }
69 Ok(Self(Arc::from(name)))
70 }
71
72 #[must_use]
74 pub fn as_str(&self) -> &str {
75 &self.0
76 }
77
78 #[must_use]
81 pub fn is_notification(&self) -> bool {
82 self.0.starts_with("notifications/")
83 }
84}
85
86impl core::fmt::Display for MethodName {
87 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88 formatter.write_str(&self.0)
89 }
90}
91
92impl core::str::FromStr for MethodName {
93 type Err = MethodNameError;
94
95 fn from_str(text: &str) -> Result<Self, Self::Err> {
96 Self::new(text)
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
102pub enum MethodNameError {
103 #[error("method name is empty")]
105 Empty,
106 #[error("method name {name:?} has an empty segment")]
108 NoSegment {
109 name: String,
111 },
112 #[error("method name {name:?} has a segment with surrounding whitespace")]
114 Whitespace {
115 name: String,
117 },
118}
119
120#[derive(Debug, Clone, PartialEq, Default)]
126pub struct IrParams {
127 entries: Extensions,
128}
129
130impl IrParams {
131 #[must_use]
133 pub const fn empty() -> Self {
134 Self {
135 entries: Extensions::empty(),
136 }
137 }
138
139 #[must_use]
141 pub const fn from_extensions(entries: Extensions) -> Self {
142 Self { entries }
143 }
144
145 #[must_use]
147 pub fn with(
148 mut self,
149 name: impl Into<String>,
150 value: crate::domain::ir::ExtensionValue,
151 ) -> Self {
152 self.entries.insert(name, value);
153 self
154 }
155
156 #[must_use]
158 pub fn get(&self, name: &str) -> Option<&crate::domain::ir::ExtensionValue> {
159 self.entries.get(name)
160 }
161
162 #[must_use]
164 pub const fn as_extensions(&self) -> &Extensions {
165 &self.entries
166 }
167}
168
169#[derive(Debug, Clone, PartialEq)]
171pub struct IrRequest {
172 pub method: MethodName,
174 pub id: Option<RequestId>,
176 pub params: IrParams,
178 pub extensions: Extensions,
180}
181
182impl IrRequest {
183 #[must_use]
185 pub const fn new(method: MethodName, id: RequestId) -> Self {
186 Self {
187 method,
188 id: Some(id),
189 params: IrParams::empty(),
190 extensions: Extensions::empty(),
191 }
192 }
193
194 #[must_use]
196 pub fn with_params(mut self, params: IrParams) -> Self {
197 self.params = params;
198 self
199 }
200}
201
202#[derive(Debug, Clone, PartialEq)]
204pub struct IrNotification {
205 pub method: MethodName,
207 pub params: IrParams,
209 pub extensions: Extensions,
211}
212
213impl IrNotification {
214 #[must_use]
222 pub fn new(method: MethodName) -> Self {
223 debug_assert!(
224 method.is_notification(),
225 "{method} is not a notification method",
226 );
227 Self {
228 method,
229 params: IrParams::empty(),
230 extensions: Extensions::empty(),
231 }
232 }
233}
234
235#[derive(Debug, Clone, PartialEq)]
237pub struct IrResult {
238 pub id: RequestId,
240 pub params: IrParams,
242 pub extensions: Extensions,
244}
245
246impl IrResult {
247 #[must_use]
249 pub const fn new(id: RequestId) -> Self {
250 Self {
251 id,
252 params: IrParams::empty(),
253 extensions: Extensions::empty(),
254 }
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
260pub enum IrErrorCode {
261 ParseError,
263 InvalidRequest,
265 MethodNotFound,
267 InvalidParams,
269 InternalError,
271 Implementation(i64),
281 Other(i64),
283}
284
285impl IrErrorCode {
286 pub const UNSUPPORTED_PROTOCOL_VERSION: Self = Self::Implementation(-32022);
299
300 pub const HEADER_MISMATCH: Self = Self::Implementation(-32020);
306
307 pub const MISSING_REQUIRED_CLIENT_CAPABILITY: Self = Self::Implementation(-32021);
313
314 #[must_use]
316 pub const fn code(self) -> i64 {
317 match self {
318 Self::ParseError => -32700,
319 Self::InvalidRequest => -32600,
320 Self::MethodNotFound => -32601,
321 Self::InvalidParams => -32602,
322 Self::InternalError => -32603,
323 Self::Implementation(code) | Self::Other(code) => code,
325 }
326 }
327
328 #[must_use]
333 pub fn from_code(code: i64) -> Self {
334 match code {
335 -32700 => Self::ParseError,
336 -32600 => Self::InvalidRequest,
337 -32601 => Self::MethodNotFound,
338 -32602 => Self::InvalidParams,
339 -32603 => Self::InternalError,
340 code if (-32099..=-32000).contains(&code) => Self::Implementation(code),
341 other => Self::Other(other),
342 }
343 }
344}
345
346#[derive(Debug, Clone, PartialEq)]
348pub struct IrError {
349 pub id: Option<RequestId>,
351 pub code: IrErrorCode,
353 pub message: String,
355 pub data: Option<crate::domain::ir::JsonObject>,
357 pub extensions: Extensions,
359}
360
361impl IrError {
362 #[must_use]
364 pub fn new(id: Option<RequestId>, code: IrErrorCode, message: impl Into<String>) -> Self {
365 Self {
366 id,
367 code,
368 message: message.into(),
369 data: None,
370 extensions: Extensions::empty(),
371 }
372 }
373}
374
375#[derive(Debug, Clone, PartialEq)]
377pub enum IrMessage {
378 Request(IrRequest),
380 Notification(IrNotification),
382 Result(IrResult),
384 Error(IrError),
386}
387
388impl IrMessage {
389 #[must_use]
391 pub const fn method(&self) -> Option<&MethodName> {
392 match self {
393 Self::Request(request) => Some(&request.method),
394 Self::Notification(notification) => Some(¬ification.method),
395 Self::Result(_) | Self::Error(_) => None,
396 }
397 }
398
399 #[must_use]
404 pub const fn id(&self) -> Option<&RequestId> {
405 match self {
406 Self::Request(request) => request.id.as_ref(),
407 Self::Notification(_) => None,
408 Self::Result(result) => Some(&result.id),
409 Self::Error(error) => error.id.as_ref(),
410 }
411 }
412
413 #[must_use]
415 pub const fn expects_reply(&self) -> bool {
416 matches!(self, Self::Request(_))
417 }
418
419 #[must_use]
426 pub const fn required_features(&self) -> FeatureSet {
427 FeatureSet::empty()
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 #![expect(
434 clippy::unwrap_used,
435 reason = "tests assert on fallible calls and unwrap with the failing precondition in the message"
436 )]
437
438 use super::{
439 IrError, IrErrorCode, IrMessage, IrNotification, IrParams, IrRequest, IrResult, MethodName,
440 MethodNameError,
441 };
442 use crate::domain::RequestId;
443 use crate::domain::ir::ExtensionValue;
444
445 fn id() -> RequestId {
446 RequestId::number(1)
447 }
448
449 #[test]
450 fn method_names_validate_their_shape() {
451 assert!(MethodName::new("tools/list").is_ok());
452 assert!(MethodName::new("notifications/initialized").is_ok());
453 assert!(MethodName::new("ping").is_ok());
454
455 assert_eq!(MethodName::new("").unwrap_err(), MethodNameError::Empty);
456 assert_eq!(
457 MethodName::new("/tools/list").unwrap_err(),
458 MethodNameError::NoSegment {
459 name: "/tools/list".into()
460 }
461 );
462 assert_eq!(
463 MethodName::new("tools/list/").unwrap_err(),
464 MethodNameError::NoSegment {
465 name: "tools/list/".into()
466 }
467 );
468 assert_eq!(
469 MethodName::new("tools/ list").unwrap_err(),
470 MethodNameError::Whitespace {
471 name: "tools/ list".into()
472 }
473 );
474 assert_eq!(
475 MethodName::new("tools/list ").unwrap_err(),
476 MethodNameError::Whitespace {
477 name: "tools/list ".into()
478 }
479 );
480 }
481
482 #[test]
483 fn method_names_round_trip() {
484 let name = MethodName::new("resources/read").unwrap();
485 assert_eq!(name.as_str(), "resources/read");
486 assert_eq!(name.to_string(), "resources/read");
487 assert_eq!(
488 "tools/call".parse::<MethodName>().unwrap().as_str(),
489 "tools/call"
490 );
491 }
492
493 #[test]
494 fn the_notification_prefix_is_the_convention() {
495 assert!(
496 MethodName::new("notifications/cancelled")
497 .unwrap()
498 .is_notification()
499 );
500 assert!(!MethodName::new("tools/list").unwrap().is_notification());
501 assert!(
503 !MethodName::new("notificationsish/x")
504 .unwrap()
505 .is_notification()
506 );
507 }
508
509 #[test]
510 fn params_hold_unmodelled_fields() {
511 let params = IrParams::empty()
512 .with("cursor", ExtensionValue::String("abc".into()))
513 .with("n", ExtensionValue::Number("5".into()));
514 assert_eq!(
515 params.get("cursor"),
516 Some(&ExtensionValue::String("abc".into()))
517 );
518 assert!(params.get("missing").is_none());
519 }
520
521 #[test]
522 fn requests_carry_a_method_an_id_and_params() {
523 let request = IrRequest::new(MethodName::new("tools/call").unwrap(), id())
524 .with_params(IrParams::empty().with("name", ExtensionValue::String("t".into())));
525 assert_eq!(request.method.as_str(), "tools/call");
526 assert_eq!(request.id.as_ref(), Some(&id()));
527 assert!(request.params.get("name").is_some());
528 }
529
530 #[test]
531 fn notifications_have_no_id_and_expects_reply_is_false() {
532 let notification =
533 IrNotification::new(MethodName::new("notifications/initialized").unwrap());
534 let message = IrMessage::Notification(notification);
535 assert!(message.id().is_none());
536 assert!(!message.expects_reply());
537 assert_eq!(
538 message.method().unwrap().as_str(),
539 "notifications/initialized"
540 );
541 }
542
543 #[test]
544 fn a_request_expects_a_reply_and_carries_its_id() {
545 let message = IrMessage::Request(IrRequest::new(MethodName::new("ping").unwrap(), id()));
546 assert!(message.expects_reply());
547 assert_eq!(message.id(), Some(&id()));
548 }
549
550 #[test]
551 fn a_result_points_back_at_its_request() {
552 let result = IrResult::new(id());
553 let message = IrMessage::Result(result);
554 assert_eq!(message.id(), Some(&id()));
555 assert!(!message.expects_reply());
556 assert!(message.method().is_none());
557 }
558
559 #[test]
562 fn a_parse_error_may_have_no_id() {
563 let error = IrError::new(None, IrErrorCode::ParseError, "bad json");
564 let message = IrMessage::Error(error);
565 assert!(message.id().is_none());
566 assert!(!message.expects_reply());
567 }
568
569 #[test]
570 fn an_error_with_an_id_points_back_at_its_request() {
571 let error = IrError::new(Some(id()), IrErrorCode::InvalidParams, "nope");
572 let message = IrMessage::Error(error);
573 assert_eq!(message.id(), Some(&id()));
574 }
575
576 #[test]
578 fn error_codes_round_trip() {
579 for code in [
580 IrErrorCode::ParseError,
581 IrErrorCode::InvalidRequest,
582 IrErrorCode::MethodNotFound,
583 IrErrorCode::InvalidParams,
584 IrErrorCode::InternalError,
585 ] {
586 assert_eq!(IrErrorCode::from_code(code.code()), code, "{code:?}");
587 }
588 for raw in [-32000_i64, -32001, -32050, -32099] {
589 let code = IrErrorCode::Implementation(raw);
590 assert_eq!(code.code(), raw);
591 assert_eq!(IrErrorCode::from_code(raw), code);
592 }
593 for raw in -32099_i64..=-32000 {
596 assert!(
597 matches!(IrErrorCode::from_code(raw), IrErrorCode::Implementation(_)),
598 "{raw}",
599 );
600 }
601 }
602
603 #[test]
604 fn a_code_outside_every_reserved_range_is_other() {
605 for raw in [0_i64, 1, -1, -31999, 12345] {
606 assert_eq!(
607 IrErrorCode::from_code(raw),
608 IrErrorCode::Other(raw),
609 "{raw}",
610 );
611 assert_eq!(IrErrorCode::Other(raw).code(), raw);
612 }
613 }
614
615 #[test]
618 fn no_envelope_shape_requires_a_feature() {
619 let messages = [
620 IrMessage::Request(IrRequest::new(MethodName::new("ping").unwrap(), id())),
621 IrMessage::Notification(IrNotification::new(
622 MethodName::new("notifications/initialized").unwrap(),
623 )),
624 IrMessage::Result(IrResult::new(id())),
625 IrMessage::Error(IrError::new(Some(id()), IrErrorCode::InternalError, "x")),
626 ];
627 for message in messages {
628 assert!(message.required_features().is_empty(), "{message:?}");
629 }
630 }
631}