1use serde_json::Value;
7use std::collections::HashMap;
8
9use turul_mcp_json_rpc_server::types::RequestId;
11use turul_mcp_protocol::logging::LoggingLevel;
12use turul_mcp_protocol::notifications::{
13 CancelledNotification, InitializedNotification, LoggingMessageNotification, Notification,
14 NotificationParams, ProgressNotification, PromptListChangedNotification,
15 ResourceListChangedNotification, ResourceUpdatedNotification, RootsListChangedNotification,
16 ToolListChangedNotification,
17};
18
19use crate::traits::{HasNotificationMetadata, HasNotificationPayload, HasNotificationRules};
21
22pub struct NotificationBuilder {
24 method: String,
25 params: Option<NotificationParams>,
26 priority: u32,
27 can_batch: bool,
28 max_retries: u32,
29}
30
31impl NotificationBuilder {
32 pub fn new(method: impl Into<String>) -> Self {
34 Self {
35 method: method.into(),
36 params: None,
37 priority: 0,
38 can_batch: true,
39 max_retries: 3,
40 }
41 }
42
43 pub fn params(mut self, params: NotificationParams) -> Self {
45 self.params = Some(params);
46 self
47 }
48
49 pub fn param(mut self, key: impl Into<String>, value: Value) -> Self {
51 if self.params.is_none() {
52 self.params = Some(NotificationParams::new());
53 }
54 self.params
55 .as_mut()
56 .unwrap()
57 .other
58 .insert(key.into(), value);
59 self
60 }
61
62 pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
64 if self.params.is_none() {
65 self.params = Some(NotificationParams::new());
66 }
67 self.params.as_mut().unwrap().meta = Some(meta);
68 self
69 }
70
71 pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
73 if self.params.is_none() {
74 self.params = Some(NotificationParams::new());
75 }
76 let params = self.params.as_mut().unwrap();
77 if params.meta.is_none() {
78 params.meta = Some(HashMap::new());
79 }
80 params.meta.as_mut().unwrap().insert(key.into(), value);
81 self
82 }
83
84 pub fn priority(mut self, priority: u32) -> Self {
86 self.priority = priority;
87 self
88 }
89
90 pub fn can_batch(mut self, can_batch: bool) -> Self {
92 self.can_batch = can_batch;
93 self
94 }
95
96 pub fn max_retries(mut self, max_retries: u32) -> Self {
98 self.max_retries = max_retries;
99 self
100 }
101
102 pub fn build(self) -> Notification {
104 let mut notification = Notification::new(self.method);
105 if let Some(params) = self.params {
106 notification = notification.with_params(params);
107 }
108 notification
109 }
110
111 pub fn build_dynamic(self) -> DynamicNotification {
113 DynamicNotification {
114 method: self.method,
115 params: self.params,
116 priority: self.priority,
117 can_batch: self.can_batch,
118 max_retries: self.max_retries,
119 }
120 }
121}
122
123#[derive(Debug)]
125pub struct DynamicNotification {
126 method: String,
127 #[allow(dead_code)]
128 params: Option<NotificationParams>,
129 priority: u32,
130 can_batch: bool,
131 max_retries: u32,
132}
133
134impl HasNotificationMetadata for DynamicNotification {
136 fn method(&self) -> &str {
137 &self.method
138 }
139
140 fn requires_ack(&self) -> bool {
141 self.priority >= 5
143 }
144}
145
146impl HasNotificationPayload for DynamicNotification {
147 fn payload(&self) -> Option<Value> {
148 None }
151}
152
153impl HasNotificationRules for DynamicNotification {
154 fn priority(&self) -> u32 {
155 self.priority
156 }
157
158 fn can_batch(&self) -> bool {
159 self.can_batch
160 }
161
162 fn max_retries(&self) -> u32 {
163 self.max_retries
164 }
165}
166
167pub struct ProgressNotificationBuilder {
171 progress_token: String,
172 progress: u64,
173 total: Option<u64>,
174 message: Option<String>,
175 meta: Option<HashMap<String, Value>>,
176}
177
178impl ProgressNotificationBuilder {
179 pub fn new(progress_token: impl Into<String>, progress: u64) -> Self {
180 Self {
181 progress_token: progress_token.into(),
182 progress,
183 total: None,
184 message: None,
185 meta: None,
186 }
187 }
188
189 pub fn total(mut self, total: u64) -> Self {
191 self.total = Some(total);
192 self
193 }
194
195 pub fn message(mut self, message: impl Into<String>) -> Self {
197 self.message = Some(message.into());
198 self
199 }
200
201 pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
203 self.meta = Some(meta);
204 self
205 }
206
207 pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
209 if self.meta.is_none() {
210 self.meta = Some(HashMap::new());
211 }
212 self.meta.as_mut().unwrap().insert(key.into(), value);
213 self
214 }
215
216 pub fn build(self) -> ProgressNotification {
218 let mut notification = ProgressNotification::new(self.progress_token, self.progress);
219 if let Some(total) = self.total {
220 notification = notification.with_total(total);
221 }
222 if let Some(message) = self.message {
223 notification = notification.with_message(message);
224 }
225 if let Some(meta) = self.meta {
226 notification = notification.with_meta(meta);
227 }
228 notification
229 }
230}
231
232pub struct ResourceUpdatedNotificationBuilder {
234 uri: String,
235 meta: Option<HashMap<String, Value>>,
236}
237
238impl ResourceUpdatedNotificationBuilder {
239 pub fn new(uri: impl Into<String>) -> Self {
240 Self {
241 uri: uri.into(),
242 meta: None,
243 }
244 }
245
246 pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
248 self.meta = Some(meta);
249 self
250 }
251
252 pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
254 if self.meta.is_none() {
255 self.meta = Some(HashMap::new());
256 }
257 self.meta.as_mut().unwrap().insert(key.into(), value);
258 self
259 }
260
261 pub fn build(self) -> ResourceUpdatedNotification {
263 let mut notification = ResourceUpdatedNotification::new(self.uri);
264 if let Some(meta) = self.meta {
265 notification = notification.with_meta(meta);
266 }
267 notification
268 }
269}
270
271pub struct CancelledNotificationBuilder {
273 request_id: RequestId,
274 reason: Option<String>,
275 meta: Option<HashMap<String, Value>>,
276}
277
278impl CancelledNotificationBuilder {
279 pub fn new(request_id: RequestId) -> Self {
280 Self {
281 request_id,
282 reason: None,
283 meta: None,
284 }
285 }
286
287 pub fn reason(mut self, reason: impl Into<String>) -> Self {
289 self.reason = Some(reason.into());
290 self
291 }
292
293 pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
295 self.meta = Some(meta);
296 self
297 }
298
299 pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
301 if self.meta.is_none() {
302 self.meta = Some(HashMap::new());
303 }
304 self.meta.as_mut().unwrap().insert(key.into(), value);
305 self
306 }
307
308 pub fn build(self) -> CancelledNotification {
310 let mut notification = CancelledNotification::new(self.request_id);
311 if let Some(reason) = self.reason {
312 notification = notification.with_reason(reason);
313 }
314 if let Some(meta) = self.meta {
315 notification = notification.with_meta(meta);
316 }
317 notification
318 }
319}
320
321impl NotificationBuilder {
323 pub fn resource_list_changed() -> ResourceListChangedNotification {
325 ResourceListChangedNotification::new()
326 }
327
328 pub fn tool_list_changed() -> ToolListChangedNotification {
330 ToolListChangedNotification::new()
331 }
332
333 pub fn prompt_list_changed() -> PromptListChangedNotification {
335 PromptListChangedNotification::new()
336 }
337
338 pub fn roots_list_changed() -> RootsListChangedNotification {
340 RootsListChangedNotification::new()
341 }
342
343 pub fn initialized() -> InitializedNotification {
345 InitializedNotification::new()
346 }
347
348 pub fn progress(
350 progress_token: impl Into<String>,
351 progress: u64,
352 ) -> ProgressNotificationBuilder {
353 ProgressNotificationBuilder::new(progress_token, progress)
354 }
355
356 pub fn resource_updated(uri: impl Into<String>) -> ResourceUpdatedNotificationBuilder {
358 ResourceUpdatedNotificationBuilder::new(uri)
359 }
360
361 pub fn cancelled(request_id: RequestId) -> CancelledNotificationBuilder {
363 CancelledNotificationBuilder::new(request_id)
364 }
365
366 pub fn logging_message(level: LoggingLevel, data: Value) -> LoggingMessageNotification {
368 LoggingMessageNotification::new(level, data)
369 }
370
371 pub fn custom(method: impl Into<String>) -> Self {
373 Self::new(method)
374 }
375
376 pub fn server_notification(method: impl Into<String>) -> Self {
378 let method = method.into();
379 if !method.starts_with("notifications/") {
381 Self::new(format!("notifications/{}", method))
382 } else {
383 Self::new(method)
384 }
385 }
386}
387
388pub mod methods {
390 pub const RESOURCE_LIST_CHANGED: &str = "notifications/resources/listChanged";
391 pub const TOOL_LIST_CHANGED: &str = "notifications/tools/listChanged";
392 pub const PROMPT_LIST_CHANGED: &str = "notifications/prompts/listChanged";
393 pub const ROOTS_LIST_CHANGED: &str = "notifications/roots/listChanged";
394 pub const PROGRESS: &str = "notifications/progress";
395 pub const RESOURCE_UPDATED: &str = "notifications/resources/updated";
396 pub const CANCELLED: &str = "notifications/cancelled";
397 pub const INITIALIZED: &str = "notifications/initialized";
398 pub const MESSAGE: &str = "notifications/message";
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use serde_json::json;
405 use crate::traits::NotificationDefinition;
406
407 #[test]
408 fn test_notification_builder_basic() {
409 let notification = NotificationBuilder::new("notifications/test")
410 .param("key1", json!("value1"))
411 .param("key2", json!(42))
412 .priority(3)
413 .can_batch(false)
414 .build();
415
416 assert_eq!(notification.method, "notifications/test");
417 assert!(notification.params.is_some());
418
419 let params = notification.params.unwrap();
420 assert_eq!(params.other.get("key1"), Some(&json!("value1")));
421 assert_eq!(params.other.get("key2"), Some(&json!(42)));
422 }
423
424 #[test]
425 fn test_notification_builder_meta() {
426 let mut meta = HashMap::new();
427 meta.insert("source".to_string(), json!("test"));
428 meta.insert("timestamp".to_string(), json!("2025-01-01T00:00:00Z"));
429
430 let notification = NotificationBuilder::new("notifications/test")
431 .meta(meta.clone())
432 .build();
433
434 let params = notification.params.expect("Expected params");
435 assert_eq!(params.meta, Some(meta));
436 }
437
438 #[test]
439 fn test_notification_builder_fluent_meta() {
440 let notification = NotificationBuilder::new("notifications/test")
441 .meta_value("request_id", json!("req-123"))
442 .meta_value("user_id", json!("user-456"))
443 .build();
444
445 let params = notification.params.expect("Expected params");
446 let meta = params.meta.expect("Expected meta");
447 assert_eq!(meta.get("request_id"), Some(&json!("req-123")));
448 assert_eq!(meta.get("user_id"), Some(&json!("user-456")));
449 }
450
451 #[test]
452 fn test_progress_notification_builder() {
453 let notification = ProgressNotificationBuilder::new("token-123", 75)
454 .total(100)
455 .message("Processing files...")
456 .meta_value("stage", json!("validation"))
457 .build();
458
459 assert_eq!(notification.method, "notifications/progress");
460 assert_eq!(notification.params.progress_token, "token-123");
461 assert_eq!(notification.params.progress, 75);
462 assert_eq!(notification.params.total, Some(100));
463 assert_eq!(
464 notification.params.message,
465 Some("Processing files...".to_string())
466 );
467
468 let meta = notification.params.meta.expect("Expected meta");
469 assert_eq!(meta.get("stage"), Some(&json!("validation")));
470 }
471
472 #[test]
473 fn test_resource_updated_notification_builder() {
474 let notification = ResourceUpdatedNotificationBuilder::new("file:///test.txt")
475 .meta_value("change_type", json!("modified"))
476 .build();
477
478 assert_eq!(notification.method, "notifications/resources/updated");
479 assert_eq!(notification.params.uri, "file:///test.txt");
480
481 let meta = notification.params.meta.expect("Expected meta");
482 assert_eq!(meta.get("change_type"), Some(&json!("modified")));
483 }
484
485 #[test]
486 fn test_cancelled_notification_builder() {
487 let notification = CancelledNotificationBuilder::new(RequestId::Number(123))
488 .reason("User cancelled operation")
489 .meta_value("cancellation_time", json!("2025-01-01T00:00:00Z"))
490 .build();
491
492 assert_eq!(notification.method, "notifications/cancelled");
493 assert_eq!(notification.params.request_id, RequestId::Number(123));
494 assert_eq!(
495 notification.params.reason,
496 Some("User cancelled operation".to_string())
497 );
498
499 let meta = notification.params.meta.expect("Expected meta");
500 assert_eq!(
501 meta.get("cancellation_time"),
502 Some(&json!("2025-01-01T00:00:00Z"))
503 );
504 }
505
506 #[test]
507 fn test_convenience_methods() {
508 let resource_list = NotificationBuilder::resource_list_changed();
510 assert_eq!(resource_list.method, "notifications/resources/listChanged");
511
512 let tool_list = NotificationBuilder::tool_list_changed();
513 assert_eq!(tool_list.method, "notifications/tools/listChanged");
514
515 let prompt_list = NotificationBuilder::prompt_list_changed();
516 assert_eq!(prompt_list.method, "notifications/prompts/listChanged");
517
518 let roots_list = NotificationBuilder::roots_list_changed();
519 assert_eq!(roots_list.method, "notifications/roots/listChanged");
520
521 let initialized = NotificationBuilder::initialized();
522 assert_eq!(initialized.method, "notifications/initialized");
523
524 let logging = NotificationBuilder::logging_message(
526 LoggingLevel::Info,
527 json!({"message": "Test log"}),
528 );
529 assert_eq!(logging.method, "notifications/message");
530 }
531
532 #[test]
533 fn test_custom_notifications() {
534 let custom = NotificationBuilder::custom("custom/event")
536 .param("event_type", json!("user_action"))
537 .build();
538 assert_eq!(custom.method, "custom/event");
539
540 let server = NotificationBuilder::server_notification("server/status")
542 .param("status", json!("ready"))
543 .build();
544 assert_eq!(server.method, "notifications/server/status");
545
546 let already_prefixed =
548 NotificationBuilder::server_notification("notifications/already/prefixed").build();
549 assert_eq!(already_prefixed.method, "notifications/already/prefixed");
550 }
551
552 #[test]
553 fn test_dynamic_notification_traits() {
554 let notification = NotificationBuilder::new("notifications/test")
555 .priority(7)
556 .can_batch(false)
557 .max_retries(5)
558 .build_dynamic();
559
560 assert_eq!(notification.method(), "notifications/test");
562 assert!(notification.requires_ack()); assert_eq!(notification.priority(), 7);
566 assert!(!notification.can_batch());
567 assert_eq!(notification.max_retries(), 5);
568
569 assert!(notification.validate().is_ok());
571 let base_notification = notification.to_notification();
572 assert_eq!(base_notification.method, "notifications/test");
573 }
574
575 #[test]
576 fn test_notification_validation() {
577 let valid = NotificationBuilder::new("notifications/valid").build_dynamic();
578 assert!(valid.validate().is_ok());
579
580 let invalid_empty = NotificationBuilder::new("").build_dynamic();
581 assert!(invalid_empty.validate().is_err());
582
583 let invalid_prefix = NotificationBuilder::new("invalid/method").build_dynamic();
584 assert!(invalid_prefix.validate().is_err());
585 }
586
587 #[test]
588 fn test_method_constants() {
589 use super::methods::*;
590
591 assert_eq!(RESOURCE_LIST_CHANGED, "notifications/resources/listChanged");
592 assert_eq!(TOOL_LIST_CHANGED, "notifications/tools/listChanged");
593 assert_eq!(PROMPT_LIST_CHANGED, "notifications/prompts/listChanged");
594 assert_eq!(ROOTS_LIST_CHANGED, "notifications/roots/listChanged");
595 assert_eq!(PROGRESS, "notifications/progress");
596 assert_eq!(RESOURCE_UPDATED, "notifications/resources/updated");
597 assert_eq!(CANCELLED, "notifications/cancelled");
598 assert_eq!(INITIALIZED, "notifications/initialized");
599 assert_eq!(MESSAGE, "notifications/message");
600 }
601}