turul_mcp_protocol_2025_06_18/
meta.rs1use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Annotations {
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub title: Option<String>,
17 }
19
20impl Annotations {
21 pub fn new() -> Self {
22 Self { title: None }
23 }
24
25 pub fn with_title(mut self, title: impl Into<String>) -> Self {
26 self.title = Some(title.into());
27 self
28 }
29}
30
31impl Default for Annotations {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(transparent)]
40pub struct ProgressToken(pub String);
41
42impl ProgressToken {
43 pub fn new(token: impl Into<String>) -> Self {
44 Self(token.into())
45 }
46
47 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52impl From<String> for ProgressToken {
53 fn from(s: String) -> Self {
54 Self(s)
55 }
56}
57
58impl From<&str> for ProgressToken {
59 fn from(s: &str) -> Self {
60 Self(s.to_string())
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66#[serde(transparent)]
67pub struct Cursor(pub String);
68
69impl Cursor {
70 pub fn new(cursor: impl Into<String>) -> Self {
71 Self(cursor.into())
72 }
73
74 pub fn as_str(&self) -> &str {
75 &self.0
76 }
77}
78
79impl From<String> for Cursor {
80 fn from(s: String) -> Self {
81 Self(s)
82 }
83}
84
85impl From<&str> for Cursor {
86 fn from(s: &str) -> Self {
87 Self(s.to_string())
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, Default)]
93#[serde(rename_all = "camelCase")]
94pub struct Meta {
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub progress_token: Option<ProgressToken>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub cursor: Option<Cursor>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub total: Option<u64>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub has_more: Option<bool>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub estimated_remaining_seconds: Option<f64>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub progress: Option<f64>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub current_step: Option<u64>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub total_steps: Option<u64>,
126
127 #[serde(flatten)]
129 pub extra: HashMap<String, Value>,
130}
131
132impl Meta {
133 pub fn new() -> Self {
135 Self::default()
136 }
137
138 pub fn with_progress_token(token: impl Into<ProgressToken>) -> Self {
140 Self {
141 progress_token: Some(token.into()),
142 ..Default::default()
143 }
144 }
145
146 pub fn with_cursor(cursor: impl Into<Cursor>) -> Self {
148 Self {
149 cursor: Some(cursor.into()),
150 ..Default::default()
151 }
152 }
153
154 pub fn with_pagination(cursor: Option<Cursor>, total: Option<u64>, has_more: bool) -> Self {
156 Self {
157 cursor,
158 total,
159 has_more: Some(has_more),
160 ..Default::default()
161 }
162 }
163
164 pub fn with_progress(
166 progress: f64,
167 current_step: Option<u64>,
168 total_steps: Option<u64>,
169 ) -> Self {
170 Self {
171 progress: Some(progress.clamp(0.0, 1.0)),
172 current_step,
173 total_steps,
174 ..Default::default()
175 }
176 }
177
178 pub fn set_progress_token(mut self, token: impl Into<ProgressToken>) -> Self {
180 self.progress_token = Some(token.into());
181 self
182 }
183
184 pub fn set_cursor(mut self, cursor: impl Into<Cursor>) -> Self {
186 self.cursor = Some(cursor.into());
187 self
188 }
189
190 pub fn set_pagination(
192 mut self,
193 cursor: Option<Cursor>,
194 total: Option<u64>,
195 has_more: bool,
196 ) -> Self {
197 self.cursor = cursor;
198 self.total = total;
199 self.has_more = Some(has_more);
200 self
201 }
202
203 pub fn set_progress(
205 mut self,
206 progress: f64,
207 current_step: Option<u64>,
208 total_steps: Option<u64>,
209 ) -> Self {
210 self.progress = Some(progress.clamp(0.0, 1.0));
211 self.current_step = current_step;
212 self.total_steps = total_steps;
213 self
214 }
215
216 pub fn set_estimated_remaining(mut self, seconds: f64) -> Self {
218 self.estimated_remaining_seconds = Some(seconds);
219 self
220 }
221
222 pub fn add_extra(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
224 self.extra.insert(key.into(), value.into());
225 self
226 }
227
228 pub fn is_empty(&self) -> bool {
230 self.progress_token.is_none()
231 && self.cursor.is_none()
232 && self.total.is_none()
233 && self.has_more.is_none()
234 && self.estimated_remaining_seconds.is_none()
235 && self.progress.is_none()
236 && self.current_step.is_none()
237 && self.total_steps.is_none()
238 && self.extra.is_empty()
239 }
240
241 pub fn merge_request_extras(mut self, request_meta: Option<&HashMap<String, Value>>) -> Self {
244 if let Some(request_extras) = request_meta {
245 for (key, value) in request_extras {
246 match key.as_str() {
248 "progressToken"
249 | "cursor"
250 | "total"
251 | "hasMore"
252 | "estimatedRemainingSeconds"
253 | "progress"
254 | "currentStep"
255 | "totalSteps" => {
256 }
258 _ => {
259 self.extra.insert(key.clone(), value.clone());
260 }
261 }
262 }
263 }
264 self
265 }
266}
267
268pub trait WithMeta {
270 fn meta(&self) -> Option<&Meta>;
272
273 fn set_meta(&mut self, meta: Option<Meta>);
275
276 fn with_meta(mut self, meta: Meta) -> Self
278 where
279 Self: Sized,
280 {
281 self.set_meta(Some(meta));
282 self
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct PaginatedResponse<T> {
289 #[serde(flatten)]
291 pub data: T,
292
293 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
295 pub meta: Option<Meta>,
296}
297
298impl<T> PaginatedResponse<T> {
299 pub fn new(data: T) -> Self {
300 Self { data, meta: None }
301 }
302
303 pub fn with_pagination(
304 data: T,
305 cursor: Option<Cursor>,
306 total: Option<u64>,
307 has_more: bool,
308 ) -> Self {
309 Self {
310 data,
311 meta: Some(Meta::with_pagination(cursor, total, has_more)),
312 }
313 }
314}
315
316impl<T> WithMeta for PaginatedResponse<T> {
317 fn meta(&self) -> Option<&Meta> {
318 self.meta.as_ref()
319 }
320
321 fn set_meta(&mut self, meta: Option<Meta>) {
322 self.meta = meta;
323 }
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ProgressResponse<T> {
329 #[serde(flatten)]
331 pub data: T,
332
333 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
335 pub meta: Option<Meta>,
336}
337
338impl<T> ProgressResponse<T> {
339 pub fn new(data: T) -> Self {
340 Self { data, meta: None }
341 }
342
343 pub fn with_progress(
344 data: T,
345 progress_token: Option<ProgressToken>,
346 progress: f64,
347 current_step: Option<u64>,
348 total_steps: Option<u64>,
349 ) -> Self {
350 let mut meta = Meta::with_progress(progress, current_step, total_steps);
351 if let Some(token) = progress_token {
352 meta = meta.set_progress_token(token);
353 }
354
355 Self {
356 data,
357 meta: Some(meta),
358 }
359 }
360}
361
362impl<T> WithMeta for ProgressResponse<T> {
363 fn meta(&self) -> Option<&Meta> {
364 self.meta.as_ref()
365 }
366
367 fn set_meta(&mut self, meta: Option<Meta>) {
368 self.meta = meta;
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use serde_json::json;
376
377 #[test]
378 fn test_progress_token() {
379 let token = ProgressToken::new("task-123");
380 assert_eq!(token.as_str(), "task-123");
381
382 let from_string: ProgressToken = "task-456".into();
383 assert_eq!(from_string.as_str(), "task-456");
384 }
385
386 #[test]
387 fn test_cursor() {
388 let cursor = Cursor::new("page-2");
389 assert_eq!(cursor.as_str(), "page-2");
390
391 let from_string: Cursor = "page-3".into();
392 assert_eq!(from_string.as_str(), "page-3");
393 }
394
395 #[test]
396 fn test_meta_creation() {
397 let meta = Meta::new()
398 .set_progress_token("task-123")
399 .set_progress(0.5, Some(5), Some(10))
400 .add_extra("custom_field", "custom_value");
401
402 assert_eq!(meta.progress_token.as_ref().unwrap().as_str(), "task-123");
403 assert_eq!(meta.progress, Some(0.5));
404 assert_eq!(meta.current_step, Some(5));
405 assert_eq!(meta.total_steps, Some(10));
406 assert_eq!(meta.extra.get("custom_field"), Some(&json!("custom_value")));
407 }
408
409 #[test]
410 fn test_meta_serialization() {
411 let meta = Meta::with_progress_token("task-123")
412 .set_cursor("page-1")
413 .set_progress(0.75, Some(3), Some(4));
414
415 let json = serde_json::to_string(&meta).unwrap();
416 let deserialized: Meta = serde_json::from_str(&json).unwrap();
417
418 assert_eq!(meta.progress_token, deserialized.progress_token);
419 assert_eq!(meta.cursor, deserialized.cursor);
420 assert_eq!(meta.progress, deserialized.progress);
421 }
422
423 #[test]
424 fn test_paginated_response() {
425 #[derive(Serialize, Deserialize)]
426 struct TestData {
427 items: Vec<String>,
428 }
429
430 let data = TestData {
431 items: vec!["item1".to_string(), "item2".to_string()],
432 };
433
434 let response =
435 PaginatedResponse::with_pagination(data, Some("next-page".into()), Some(100), true);
436
437 let json = serde_json::to_string(&response).unwrap();
438 let deserialized: PaginatedResponse<TestData> = serde_json::from_str(&json).unwrap();
439
440 assert_eq!(deserialized.data.items.len(), 2);
441 assert!(deserialized.meta.is_some());
442 assert_eq!(
443 deserialized
444 .meta
445 .as_ref()
446 .unwrap()
447 .cursor
448 .as_ref()
449 .unwrap()
450 .as_str(),
451 "next-page"
452 );
453 assert_eq!(deserialized.meta.as_ref().unwrap().total, Some(100));
454 assert_eq!(deserialized.meta.as_ref().unwrap().has_more, Some(true));
455 }
456
457 #[test]
458 fn test_progress_response() {
459 #[derive(Serialize, Deserialize)]
460 struct TaskResult {
461 status: String,
462 }
463
464 let data = TaskResult {
465 status: "processing".to_string(),
466 };
467
468 let response =
469 ProgressResponse::with_progress(data, Some("task-456".into()), 0.8, Some(8), Some(10));
470
471 let json = serde_json::to_string(&response).unwrap();
472 let deserialized: ProgressResponse<TaskResult> = serde_json::from_str(&json).unwrap();
473
474 assert_eq!(deserialized.data.status, "processing");
475 assert!(deserialized.meta.is_some());
476 assert_eq!(
477 deserialized
478 .meta
479 .as_ref()
480 .unwrap()
481 .progress_token
482 .as_ref()
483 .unwrap()
484 .as_str(),
485 "task-456"
486 );
487 assert_eq!(deserialized.meta.as_ref().unwrap().progress, Some(0.8));
488 assert_eq!(deserialized.meta.as_ref().unwrap().current_step, Some(8));
489 assert_eq!(deserialized.meta.as_ref().unwrap().total_steps, Some(10));
490 }
491
492 #[test]
493 fn test_meta_is_empty() {
494 let empty_meta = Meta::new();
495 assert!(empty_meta.is_empty());
496
497 let non_empty_meta = Meta::new().set_progress_token("test");
498 assert!(!non_empty_meta.is_empty());
499 }
500
501 #[test]
502 fn test_merge_request_extras() {
503 let mut request_meta = HashMap::new();
504 request_meta.insert("customField".to_string(), json!("custom_value"));
505 request_meta.insert("userContext".to_string(), json!("user_123"));
506 request_meta.insert("progressToken".to_string(), json!("should_be_ignored"));
507 request_meta.insert("cursor".to_string(), json!("should_be_ignored"));
508
509 let meta = Meta::with_pagination(Some("page-1".into()), Some(100), true)
510 .merge_request_extras(Some(&request_meta));
511
512 assert_eq!(meta.cursor.as_ref().unwrap().as_str(), "page-1");
514 assert_eq!(meta.total, Some(100));
515 assert_eq!(meta.has_more, Some(true));
516
517 assert_eq!(meta.extra.get("customField"), Some(&json!("custom_value")));
519 assert_eq!(meta.extra.get("userContext"), Some(&json!("user_123")));
520
521 assert!(!meta.extra.contains_key("progressToken"));
523 assert!(!meta.extra.contains_key("cursor"));
524 }
525
526 #[test]
527 fn test_merge_request_extras_empty() {
528 let meta = Meta::with_cursor("test-cursor").merge_request_extras(None);
529
530 assert_eq!(meta.cursor.as_ref().unwrap().as_str(), "test-cursor");
531 assert!(meta.extra.is_empty());
532 }
533}