turul_mcp_protocol_2025_06_18/
meta.rs1use std::collections::HashMap;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
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(progress: f64, current_step: Option<u64>, total_steps: Option<u64>) -> Self {
166 Self {
167 progress: Some(progress.clamp(0.0, 1.0)),
168 current_step,
169 total_steps,
170 ..Default::default()
171 }
172 }
173
174 pub fn set_progress_token(mut self, token: impl Into<ProgressToken>) -> Self {
176 self.progress_token = Some(token.into());
177 self
178 }
179
180 pub fn set_cursor(mut self, cursor: impl Into<Cursor>) -> Self {
182 self.cursor = Some(cursor.into());
183 self
184 }
185
186 pub fn set_pagination(mut self, cursor: Option<Cursor>, total: Option<u64>, has_more: bool) -> Self {
188 self.cursor = cursor;
189 self.total = total;
190 self.has_more = Some(has_more);
191 self
192 }
193
194 pub fn set_progress(mut self, progress: f64, current_step: Option<u64>, total_steps: Option<u64>) -> Self {
196 self.progress = Some(progress.clamp(0.0, 1.0));
197 self.current_step = current_step;
198 self.total_steps = total_steps;
199 self
200 }
201
202 pub fn set_estimated_remaining(mut self, seconds: f64) -> Self {
204 self.estimated_remaining_seconds = Some(seconds);
205 self
206 }
207
208 pub fn add_extra(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
210 self.extra.insert(key.into(), value.into());
211 self
212 }
213
214 pub fn is_empty(&self) -> bool {
216 self.progress_token.is_none() &&
217 self.cursor.is_none() &&
218 self.total.is_none() &&
219 self.has_more.is_none() &&
220 self.estimated_remaining_seconds.is_none() &&
221 self.progress.is_none() &&
222 self.current_step.is_none() &&
223 self.total_steps.is_none() &&
224 self.extra.is_empty()
225 }
226}
227
228pub trait WithMeta {
230 fn meta(&self) -> Option<&Meta>;
232
233 fn set_meta(&mut self, meta: Option<Meta>);
235
236 fn with_meta(mut self, meta: Meta) -> Self
238 where
239 Self: Sized,
240 {
241 self.set_meta(Some(meta));
242 self
243 }
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct PaginatedResponse<T> {
249 #[serde(flatten)]
251 pub data: T,
252
253 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
255 pub meta: Option<Meta>,
256}
257
258impl<T> PaginatedResponse<T> {
259 pub fn new(data: T) -> Self {
260 Self {
261 data,
262 meta: None,
263 }
264 }
265
266 pub fn with_pagination(data: T, cursor: Option<Cursor>, total: Option<u64>, has_more: bool) -> Self {
267 Self {
268 data,
269 meta: Some(Meta::with_pagination(cursor, total, has_more)),
270 }
271 }
272}
273
274impl<T> WithMeta for PaginatedResponse<T> {
275 fn meta(&self) -> Option<&Meta> {
276 self.meta.as_ref()
277 }
278
279 fn set_meta(&mut self, meta: Option<Meta>) {
280 self.meta = meta;
281 }
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct ProgressResponse<T> {
287 #[serde(flatten)]
289 pub data: T,
290
291 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
293 pub meta: Option<Meta>,
294}
295
296impl<T> ProgressResponse<T> {
297 pub fn new(data: T) -> Self {
298 Self {
299 data,
300 meta: None,
301 }
302 }
303
304 pub fn with_progress(
305 data: T,
306 progress_token: Option<ProgressToken>,
307 progress: f64,
308 current_step: Option<u64>,
309 total_steps: Option<u64>
310 ) -> Self {
311 let mut meta = Meta::with_progress(progress, current_step, total_steps);
312 if let Some(token) = progress_token {
313 meta = meta.set_progress_token(token);
314 }
315
316 Self {
317 data,
318 meta: Some(meta),
319 }
320 }
321}
322
323impl<T> WithMeta for ProgressResponse<T> {
324 fn meta(&self) -> Option<&Meta> {
325 self.meta.as_ref()
326 }
327
328 fn set_meta(&mut self, meta: Option<Meta>) {
329 self.meta = meta;
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use serde_json::json;
337
338 #[test]
339 fn test_progress_token() {
340 let token = ProgressToken::new("task-123");
341 assert_eq!(token.as_str(), "task-123");
342
343 let from_string: ProgressToken = "task-456".into();
344 assert_eq!(from_string.as_str(), "task-456");
345 }
346
347 #[test]
348 fn test_cursor() {
349 let cursor = Cursor::new("page-2");
350 assert_eq!(cursor.as_str(), "page-2");
351
352 let from_string: Cursor = "page-3".into();
353 assert_eq!(from_string.as_str(), "page-3");
354 }
355
356 #[test]
357 fn test_meta_creation() {
358 let meta = Meta::new()
359 .set_progress_token("task-123")
360 .set_progress(0.5, Some(5), Some(10))
361 .add_extra("custom_field", "custom_value");
362
363 assert_eq!(meta.progress_token.as_ref().unwrap().as_str(), "task-123");
364 assert_eq!(meta.progress, Some(0.5));
365 assert_eq!(meta.current_step, Some(5));
366 assert_eq!(meta.total_steps, Some(10));
367 assert_eq!(meta.extra.get("custom_field"), Some(&json!("custom_value")));
368 }
369
370 #[test]
371 fn test_meta_serialization() {
372 let meta = Meta::with_progress_token("task-123")
373 .set_cursor("page-1")
374 .set_progress(0.75, Some(3), Some(4));
375
376 let json = serde_json::to_string(&meta).unwrap();
377 let deserialized: Meta = serde_json::from_str(&json).unwrap();
378
379 assert_eq!(meta.progress_token, deserialized.progress_token);
380 assert_eq!(meta.cursor, deserialized.cursor);
381 assert_eq!(meta.progress, deserialized.progress);
382 }
383
384 #[test]
385 fn test_paginated_response() {
386 #[derive(Serialize, Deserialize)]
387 struct TestData {
388 items: Vec<String>,
389 }
390
391 let data = TestData {
392 items: vec!["item1".to_string(), "item2".to_string()],
393 };
394
395 let response = PaginatedResponse::with_pagination(
396 data,
397 Some("next-page".into()),
398 Some(100),
399 true
400 );
401
402 let json = serde_json::to_string(&response).unwrap();
403 let deserialized: PaginatedResponse<TestData> = serde_json::from_str(&json).unwrap();
404
405 assert_eq!(deserialized.data.items.len(), 2);
406 assert!(deserialized.meta.is_some());
407 assert_eq!(deserialized.meta.as_ref().unwrap().cursor.as_ref().unwrap().as_str(), "next-page");
408 assert_eq!(deserialized.meta.as_ref().unwrap().total, Some(100));
409 assert_eq!(deserialized.meta.as_ref().unwrap().has_more, Some(true));
410 }
411
412 #[test]
413 fn test_progress_response() {
414 #[derive(Serialize, Deserialize)]
415 struct TaskResult {
416 status: String,
417 }
418
419 let data = TaskResult {
420 status: "processing".to_string(),
421 };
422
423 let response = ProgressResponse::with_progress(
424 data,
425 Some("task-456".into()),
426 0.8,
427 Some(8),
428 Some(10)
429 );
430
431 let json = serde_json::to_string(&response).unwrap();
432 let deserialized: ProgressResponse<TaskResult> = serde_json::from_str(&json).unwrap();
433
434 assert_eq!(deserialized.data.status, "processing");
435 assert!(deserialized.meta.is_some());
436 assert_eq!(deserialized.meta.as_ref().unwrap().progress_token.as_ref().unwrap().as_str(), "task-456");
437 assert_eq!(deserialized.meta.as_ref().unwrap().progress, Some(0.8));
438 assert_eq!(deserialized.meta.as_ref().unwrap().current_step, Some(8));
439 assert_eq!(deserialized.meta.as_ref().unwrap().total_steps, Some(10));
440 }
441
442 #[test]
443 fn test_meta_is_empty() {
444 let empty_meta = Meta::new();
445 assert!(empty_meta.is_empty());
446
447 let non_empty_meta = Meta::new().set_progress_token("test");
448 assert!(!non_empty_meta.is_empty());
449 }
450}