mcp_host/content/
builder.rs

1//! Fluent content builders
2//!
3//! Provides ergonomic builder API for creating content with annotations
4
5use super::annotations::Annotations;
6use super::types::{AudioContent, ImageContent, ResourceLink, TextContent};
7
8/// Builder for text content
9#[derive(Debug, Clone)]
10pub struct TextContentBuilder {
11    text: String,
12    annotations: Option<Annotations>,
13}
14
15impl TextContentBuilder {
16    /// Create new text content builder
17    pub fn new(text: impl Into<String>) -> Self {
18        Self {
19            text: text.into(),
20            annotations: None,
21        }
22    }
23
24    /// Add audience annotation
25    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
26        self.annotations
27            .get_or_insert_with(Annotations::new)
28            .audience
29            .get_or_insert_with(Vec::new)
30            .push(audience.into());
31        self
32    }
33
34    /// Set priority annotation (0.0 to 1.0)
35    pub fn with_priority(mut self, priority: f64) -> Self {
36        self.annotations
37            .get_or_insert_with(Annotations::new)
38            .priority = Some(priority.clamp(0.0, 1.0));
39        self
40    }
41
42    /// Set last modified annotation
43    pub fn with_last_modified(mut self, timestamp: impl Into<String>) -> Self {
44        self.annotations
45            .get_or_insert_with(Annotations::new)
46            .last_modified = Some(timestamp.into());
47        self
48    }
49
50    /// Set language annotation
51    pub fn with_language(mut self, language: impl Into<String>) -> Self {
52        self.annotations
53            .get_or_insert_with(Annotations::new)
54            .language = Some(language.into());
55        self
56    }
57
58    /// Build text content
59    pub fn build(self) -> TextContent {
60        TextContent {
61            r#type: "text".to_string(),
62            text: self.text,
63            annotations: self.annotations,
64        }
65    }
66}
67
68/// Builder for image content
69#[derive(Debug, Clone)]
70pub struct ImageContentBuilder {
71    data: String,
72    mime_type: String,
73    annotations: Option<Annotations>,
74}
75
76impl ImageContentBuilder {
77    /// Create new image content builder
78    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
79        Self {
80            data: data.into(),
81            mime_type: mime_type.into(),
82            annotations: None,
83        }
84    }
85
86    /// Add audience annotation
87    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
88        self.annotations
89            .get_or_insert_with(Annotations::new)
90            .audience
91            .get_or_insert_with(Vec::new)
92            .push(audience.into());
93        self
94    }
95
96    /// Set priority annotation
97    pub fn with_priority(mut self, priority: f64) -> Self {
98        self.annotations
99            .get_or_insert_with(Annotations::new)
100            .priority = Some(priority.clamp(0.0, 1.0));
101        self
102    }
103
104    /// Set last modified annotation
105    pub fn with_last_modified(mut self, timestamp: impl Into<String>) -> Self {
106        self.annotations
107            .get_or_insert_with(Annotations::new)
108            .last_modified = Some(timestamp.into());
109        self
110    }
111
112    /// Set language annotation
113    pub fn with_language(mut self, language: impl Into<String>) -> Self {
114        self.annotations
115            .get_or_insert_with(Annotations::new)
116            .language = Some(language.into());
117        self
118    }
119
120    /// Build image content
121    pub fn build(self) -> ImageContent {
122        ImageContent {
123            r#type: "image".to_string(),
124            data: self.data,
125            mime_type: self.mime_type,
126            annotations: self.annotations,
127        }
128    }
129}
130
131/// Builder for audio content
132#[derive(Debug, Clone)]
133pub struct AudioContentBuilder {
134    data: String,
135    mime_type: String,
136    annotations: Option<Annotations>,
137}
138
139impl AudioContentBuilder {
140    /// Create new audio content builder
141    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
142        Self {
143            data: data.into(),
144            mime_type: mime_type.into(),
145            annotations: None,
146        }
147    }
148
149    /// Add audience annotation
150    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
151        self.annotations
152            .get_or_insert_with(Annotations::new)
153            .audience
154            .get_or_insert_with(Vec::new)
155            .push(audience.into());
156        self
157    }
158
159    /// Set priority annotation
160    pub fn with_priority(mut self, priority: f64) -> Self {
161        self.annotations
162            .get_or_insert_with(Annotations::new)
163            .priority = Some(priority.clamp(0.0, 1.0));
164        self
165    }
166
167    /// Set last modified annotation
168    pub fn with_last_modified(mut self, timestamp: impl Into<String>) -> Self {
169        self.annotations
170            .get_or_insert_with(Annotations::new)
171            .last_modified = Some(timestamp.into());
172        self
173    }
174
175    /// Set language annotation
176    pub fn with_language(mut self, language: impl Into<String>) -> Self {
177        self.annotations
178            .get_or_insert_with(Annotations::new)
179            .language = Some(language.into());
180        self
181    }
182
183    /// Build audio content
184    pub fn build(self) -> AudioContent {
185        AudioContent {
186            r#type: "audio".to_string(),
187            data: self.data,
188            mime_type: self.mime_type,
189            annotations: self.annotations,
190        }
191    }
192}
193
194/// Builder for resource links
195#[derive(Debug, Clone)]
196pub struct ResourceLinkBuilder {
197    uri: String,
198    title: Option<String>,
199    annotations: Option<Annotations>,
200}
201
202impl ResourceLinkBuilder {
203    /// Create new resource link builder
204    pub fn new(uri: impl Into<String>) -> Self {
205        Self {
206            uri: uri.into(),
207            title: None,
208            annotations: None,
209        }
210    }
211
212    /// Set title
213    pub fn with_title(mut self, title: impl Into<String>) -> Self {
214        self.title = Some(title.into());
215        self
216    }
217
218    /// Add audience annotation
219    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
220        self.annotations
221            .get_or_insert_with(Annotations::new)
222            .audience
223            .get_or_insert_with(Vec::new)
224            .push(audience.into());
225        self
226    }
227
228    /// Set priority annotation
229    pub fn with_priority(mut self, priority: f64) -> Self {
230        self.annotations
231            .get_or_insert_with(Annotations::new)
232            .priority = Some(priority.clamp(0.0, 1.0));
233        self
234    }
235
236    /// Set last modified annotation
237    pub fn with_last_modified(mut self, timestamp: impl Into<String>) -> Self {
238        self.annotations
239            .get_or_insert_with(Annotations::new)
240            .last_modified = Some(timestamp.into());
241        self
242    }
243
244    /// Set language annotation
245    pub fn with_language(mut self, language: impl Into<String>) -> Self {
246        self.annotations
247            .get_or_insert_with(Annotations::new)
248            .language = Some(language.into());
249        self
250    }
251
252    /// Build resource link
253    pub fn build(self) -> ResourceLink {
254        ResourceLink {
255            r#type: "resource".to_string(),
256            uri: self.uri,
257            title: self.title,
258            annotations: self.annotations,
259        }
260    }
261}
262
263/// Convenience function to create text content builder
264pub fn text(text: impl Into<String>) -> TextContentBuilder {
265    TextContentBuilder::new(text)
266}
267
268/// Convenience function to create image content builder
269pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> ImageContentBuilder {
270    ImageContentBuilder::new(data, mime_type)
271}
272
273/// Convenience function to create audio content builder
274pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> AudioContentBuilder {
275    AudioContentBuilder::new(data, mime_type)
276}
277
278/// Convenience function to create resource link builder
279pub fn resource(uri: impl Into<String>) -> ResourceLinkBuilder {
280    ResourceLinkBuilder::new(uri)
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_text_builder() {
289        let content = text("Hello, world!")
290            .with_priority(0.9)
291            .with_audience("user")
292            .with_language("en")
293            .build();
294
295        assert_eq!(content.text, "Hello, world!");
296        assert!(content.annotations.is_some());
297
298        let ann = content.annotations.unwrap();
299        assert_eq!(ann.priority, Some(0.9));
300        assert_eq!(ann.audience, Some(vec!["user".to_string()]));
301        assert_eq!(ann.language, Some("en".to_string()));
302    }
303
304    #[test]
305    fn test_text_builder_multiple_audiences() {
306        let content = text("Important message")
307            .with_audience("user")
308            .with_audience("admin")
309            .build();
310
311        let ann = content.annotations.unwrap();
312        assert_eq!(
313            ann.audience,
314            Some(vec!["user".to_string(), "admin".to_string()])
315        );
316    }
317
318    #[test]
319    fn test_image_builder() {
320        let content = image("base64data", "image/png")
321            .with_priority(0.8)
322            .with_last_modified("2025-01-01T00:00:00Z")
323            .build();
324
325        assert_eq!(content.data, "base64data");
326        assert_eq!(content.mime_type, "image/png");
327
328        let ann = content.annotations.unwrap();
329        assert_eq!(ann.priority, Some(0.8));
330        assert_eq!(ann.last_modified, Some("2025-01-01T00:00:00Z".to_string()));
331    }
332
333    #[test]
334    fn test_audio_builder() {
335        let content = audio("audiodata", "audio/wav")
336            .with_priority(0.7)
337            .with_language("en")
338            .build();
339
340        assert_eq!(content.data, "audiodata");
341        assert_eq!(content.mime_type, "audio/wav");
342
343        let ann = content.annotations.unwrap();
344        assert_eq!(ann.priority, Some(0.7));
345        assert_eq!(ann.language, Some("en".to_string()));
346    }
347
348    #[test]
349    fn test_resource_builder() {
350        let link = resource("file://test.txt")
351            .with_title("Test File")
352            .with_priority(0.9)
353            .build();
354
355        assert_eq!(link.uri, "file://test.txt");
356        assert_eq!(link.title, Some("Test File".to_string()));
357
358        let ann = link.annotations.unwrap();
359        assert_eq!(ann.priority, Some(0.9));
360    }
361
362    #[test]
363    fn test_builder_without_annotations() {
364        let content = text("Plain text").build();
365
366        assert_eq!(content.text, "Plain text");
367        assert!(content.annotations.is_none());
368    }
369
370    #[test]
371    fn test_priority_clamping() {
372        let content1 = text("High").with_priority(1.5).build();
373        let content2 = text("Low").with_priority(-0.5).build();
374
375        assert_eq!(content1.annotations.unwrap().priority, Some(1.0));
376        assert_eq!(content2.annotations.unwrap().priority, Some(0.0));
377    }
378
379    #[test]
380    fn test_builder_type_usage() {
381        // Test that builders can be constructed explicitly
382        let builder = TextContentBuilder::new("test");
383        let content = builder.with_priority(0.5).build();
384
385        assert_eq!(content.text, "test");
386        assert_eq!(content.annotations.unwrap().priority, Some(0.5));
387    }
388}