1use std::collections::BTreeMap;
13
14use schemars::{JsonSchema, Schema};
15use serde::{Deserialize, Serialize};
16use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
17
18use super::Meta;
19use crate::{IntoOption, SkipListener};
20
21#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
36#[serde(tag = "type", rename_all = "snake_case")]
37#[schemars(extend("discriminator" = {"propertyName": "type"}))]
38#[non_exhaustive]
39pub enum ContentBlock {
40 Text(TextContent),
45 Image(ImageContent),
49 Audio(AudioContent),
53 ResourceLink(ResourceLink),
57 Resource(EmbeddedResource),
63 #[serde(untagged)]
73 Other(OtherContentBlock),
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
78#[schemars(inline)]
79#[schemars(transform = other_content_block_schema)]
80#[serde(rename_all = "camelCase")]
81#[non_exhaustive]
82pub struct OtherContentBlock {
83 #[serde(rename = "type")]
89 pub type_: String,
90 #[serde(flatten)]
92 pub fields: BTreeMap<String, serde_json::Value>,
93}
94
95impl OtherContentBlock {
96 #[must_use]
97 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
98 fields.remove("type");
99 Self {
100 type_: type_.into(),
101 fields,
102 }
103 }
104}
105
106impl<'de> Deserialize<'de> for OtherContentBlock {
107 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108 where
109 D: serde::Deserializer<'de>,
110 {
111 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
112 let type_ = fields
113 .remove("type")
114 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
115 let serde_json::Value::String(type_) = type_ else {
116 return Err(serde::de::Error::custom("`type` must be a string"));
117 };
118
119 if is_known_content_block_type(&type_) {
120 return Err(serde::de::Error::custom(format!(
121 "known content block `{type_}` did not match its schema"
122 )));
123 }
124
125 Ok(Self { type_, fields })
126 }
127}
128
129fn is_known_content_block_type(type_: &str) -> bool {
130 matches!(
131 type_,
132 "text" | "image" | "audio" | "resource_link" | "resource"
133 )
134}
135
136fn other_content_block_schema(schema: &mut Schema) {
137 super::schema_util::reject_known_string_discriminators(
138 schema,
139 "type",
140 &["text", "image", "audio", "resource_link", "resource"],
141 );
142}
143
144#[serde_as]
146#[skip_serializing_none]
147#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
148#[non_exhaustive]
149pub struct TextContent {
150 #[serde_as(deserialize_as = "DefaultOnError")]
151 #[schemars(extend("x-deserialize-default-on-error" = true))]
152 #[serde(default)]
153 pub annotations: Option<Annotations>,
154 pub text: String,
155 #[serde(rename = "_meta")]
161 pub meta: Option<Meta>,
162}
163
164impl TextContent {
165 #[must_use]
166 pub fn new(text: impl Into<String>) -> Self {
167 Self {
168 annotations: None,
169 text: text.into(),
170 meta: None,
171 }
172 }
173
174 #[must_use]
175 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
176 self.annotations = annotations.into_option();
177 self
178 }
179
180 #[must_use]
186 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
187 self.meta = meta.into_option();
188 self
189 }
190}
191
192impl<T: Into<String>> From<T> for ContentBlock {
193 fn from(value: T) -> Self {
194 Self::Text(TextContent::new(value))
195 }
196}
197
198#[serde_as]
200#[skip_serializing_none]
201#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
202#[serde(rename_all = "camelCase")]
203#[non_exhaustive]
204pub struct ImageContent {
205 #[serde_as(deserialize_as = "DefaultOnError")]
206 #[schemars(extend("x-deserialize-default-on-error" = true))]
207 #[serde(default)]
208 pub annotations: Option<Annotations>,
209 pub data: String,
210 pub mime_type: String,
211 pub uri: Option<String>,
212 #[serde(rename = "_meta")]
218 pub meta: Option<Meta>,
219}
220
221impl ImageContent {
222 #[must_use]
223 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
224 Self {
225 annotations: None,
226 data: data.into(),
227 mime_type: mime_type.into(),
228 uri: None,
229 meta: None,
230 }
231 }
232
233 #[must_use]
234 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
235 self.annotations = annotations.into_option();
236 self
237 }
238
239 #[must_use]
240 pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
241 self.uri = uri.into_option();
242 self
243 }
244
245 #[must_use]
251 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
252 self.meta = meta.into_option();
253 self
254 }
255}
256
257#[serde_as]
259#[skip_serializing_none]
260#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
261#[serde(rename_all = "camelCase")]
262#[non_exhaustive]
263pub struct AudioContent {
264 #[serde_as(deserialize_as = "DefaultOnError")]
265 #[schemars(extend("x-deserialize-default-on-error" = true))]
266 #[serde(default)]
267 pub annotations: Option<Annotations>,
268 pub data: String,
269 pub mime_type: String,
270 #[serde(rename = "_meta")]
276 pub meta: Option<Meta>,
277}
278
279impl AudioContent {
280 #[must_use]
281 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
282 Self {
283 annotations: None,
284 data: data.into(),
285 mime_type: mime_type.into(),
286 meta: None,
287 }
288 }
289
290 #[must_use]
291 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
292 self.annotations = annotations.into_option();
293 self
294 }
295
296 #[must_use]
302 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
303 self.meta = meta.into_option();
304 self
305 }
306}
307
308#[serde_as]
310#[skip_serializing_none]
311#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
312#[non_exhaustive]
313pub struct EmbeddedResource {
314 #[serde_as(deserialize_as = "DefaultOnError")]
315 #[schemars(extend("x-deserialize-default-on-error" = true))]
316 #[serde(default)]
317 pub annotations: Option<Annotations>,
318 pub resource: EmbeddedResourceResource,
319 #[serde(rename = "_meta")]
325 pub meta: Option<Meta>,
326}
327
328impl EmbeddedResource {
329 #[must_use]
330 pub fn new(resource: EmbeddedResourceResource) -> Self {
331 Self {
332 annotations: None,
333 resource,
334 meta: None,
335 }
336 }
337
338 #[must_use]
339 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
340 self.annotations = annotations.into_option();
341 self
342 }
343
344 #[must_use]
350 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
351 self.meta = meta.into_option();
352 self
353 }
354}
355
356#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
358#[serde(untagged)]
359#[non_exhaustive]
360pub enum EmbeddedResourceResource {
361 TextResourceContents(TextResourceContents),
362 BlobResourceContents(BlobResourceContents),
363}
364
365#[skip_serializing_none]
367#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
368#[serde(rename_all = "camelCase")]
369#[non_exhaustive]
370pub struct TextResourceContents {
371 pub mime_type: Option<String>,
372 pub text: String,
373 pub uri: String,
374 #[serde(rename = "_meta")]
380 pub meta: Option<Meta>,
381}
382
383impl TextResourceContents {
384 #[must_use]
385 pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
386 Self {
387 mime_type: None,
388 text: text.into(),
389 uri: uri.into(),
390 meta: None,
391 }
392 }
393
394 #[must_use]
395 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
396 self.mime_type = mime_type.into_option();
397 self
398 }
399
400 #[must_use]
406 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
407 self.meta = meta.into_option();
408 self
409 }
410}
411
412#[skip_serializing_none]
414#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
415#[serde(rename_all = "camelCase")]
416#[non_exhaustive]
417pub struct BlobResourceContents {
418 pub blob: String,
419 pub mime_type: Option<String>,
420 pub uri: String,
421 #[serde(rename = "_meta")]
427 pub meta: Option<Meta>,
428}
429
430impl BlobResourceContents {
431 #[must_use]
432 pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
433 Self {
434 blob: blob.into(),
435 mime_type: None,
436 uri: uri.into(),
437 meta: None,
438 }
439 }
440
441 #[must_use]
442 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
443 self.mime_type = mime_type.into_option();
444 self
445 }
446
447 #[must_use]
453 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
454 self.meta = meta.into_option();
455 self
456 }
457}
458
459#[serde_as]
461#[skip_serializing_none]
462#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
463#[serde(rename_all = "camelCase")]
464#[non_exhaustive]
465pub struct ResourceLink {
466 #[serde_as(deserialize_as = "DefaultOnError")]
467 #[schemars(extend("x-deserialize-default-on-error" = true))]
468 #[serde(default)]
469 pub annotations: Option<Annotations>,
470 pub description: Option<String>,
471 pub mime_type: Option<String>,
472 pub name: String,
473 pub size: Option<i64>,
474 pub title: Option<String>,
475 pub uri: String,
476 #[serde(rename = "_meta")]
482 pub meta: Option<Meta>,
483}
484
485impl ResourceLink {
486 #[must_use]
487 pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
488 Self {
489 annotations: None,
490 description: None,
491 mime_type: None,
492 name: name.into(),
493 size: None,
494 title: None,
495 uri: uri.into(),
496 meta: None,
497 }
498 }
499
500 #[must_use]
501 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
502 self.annotations = annotations.into_option();
503 self
504 }
505
506 #[must_use]
507 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
508 self.description = description.into_option();
509 self
510 }
511
512 #[must_use]
513 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
514 self.mime_type = mime_type.into_option();
515 self
516 }
517
518 #[must_use]
519 pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
520 self.size = size.into_option();
521 self
522 }
523
524 #[must_use]
525 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
526 self.title = title.into_option();
527 self
528 }
529
530 #[must_use]
536 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
537 self.meta = meta.into_option();
538 self
539 }
540}
541
542#[serde_as]
544#[skip_serializing_none]
545#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
546#[serde(rename_all = "camelCase")]
547#[non_exhaustive]
548pub struct Annotations {
549 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
550 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
551 #[serde(default)]
552 pub audience: Option<Vec<Role>>,
553 pub last_modified: Option<String>,
554 pub priority: Option<f64>,
555 #[serde(rename = "_meta")]
561 pub meta: Option<Meta>,
562}
563
564impl Annotations {
565 #[must_use]
566 pub fn new() -> Self {
567 Self::default()
568 }
569
570 #[must_use]
571 pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
572 self.audience = audience.into_option();
573 self
574 }
575
576 #[must_use]
577 pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
578 self.last_modified = last_modified.into_option();
579 self
580 }
581
582 #[must_use]
583 pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
584 self.priority = priority.into_option();
585 self
586 }
587
588 #[must_use]
594 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
595 self.meta = meta.into_option();
596 self
597 }
598}
599
600#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
602#[serde(rename_all = "camelCase")]
603#[non_exhaustive]
604pub enum Role {
605 Assistant,
606 User,
607 #[serde(untagged)]
613 Other(String),
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619
620 #[test]
621 fn test_text_content_roundtrip() {
622 let content = TextContent::new("hello world");
623 let json = serde_json::to_value(&content).unwrap();
624 let parsed: TextContent = serde_json::from_value(json).unwrap();
625 assert_eq!(content, parsed);
626 }
627
628 #[test]
629 fn test_text_content_omits_optional_fields() {
630 let content = TextContent::new("hello");
631 let json = serde_json::to_value(&content).unwrap();
632 assert!(!json.as_object().unwrap().contains_key("annotations"));
633 assert!(!json.as_object().unwrap().contains_key("meta"));
634 }
635
636 #[test]
637 fn test_text_content_from_string() {
638 let block: ContentBlock = "hello".into();
639 match block {
640 ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
641 _ => panic!("Expected Text variant"),
642 }
643 }
644
645 #[test]
646 fn role_preserves_unknown_variant() {
647 let role: Role = serde_json::from_str("\"critic\"").unwrap();
648 assert_eq!(role, Role::Other("critic".to_string()));
649 assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
650 }
651
652 #[test]
653 fn content_block_preserves_unknown_variant() {
654 let block: ContentBlock = serde_json::from_value(serde_json::json!({
655 "type": "_widget",
656 "title": "Status",
657 "state": {"ok": true}
658 }))
659 .unwrap();
660
661 let ContentBlock::Other(unknown) = block else {
662 panic!("expected unknown content block");
663 };
664
665 assert_eq!(unknown.type_, "_widget");
666 assert_eq!(
667 unknown.fields.get("title"),
668 Some(&serde_json::json!("Status"))
669 );
670 assert_eq!(
671 serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
672 serde_json::json!({
673 "type": "_widget",
674 "title": "Status",
675 "state": {"ok": true}
676 })
677 );
678 }
679
680 #[test]
681 fn content_block_does_not_hide_malformed_known_variant() {
682 assert!(
683 serde_json::from_value::<ContentBlock>(serde_json::json!({
684 "type": "text"
685 }))
686 .is_err()
687 );
688 }
689
690 #[test]
691 fn test_image_content_roundtrip() {
692 let content = ImageContent::new("base64data", "image/png");
693 let json = serde_json::to_value(&content).unwrap();
694 let parsed: ImageContent = serde_json::from_value(json).unwrap();
695 assert_eq!(content, parsed);
696 }
697
698 #[test]
699 fn test_image_content_omits_optional_fields() {
700 let content = ImageContent::new("data", "image/png");
701 let json = serde_json::to_value(&content).unwrap();
702 assert!(!json.as_object().unwrap().contains_key("uri"));
703 assert!(!json.as_object().unwrap().contains_key("annotations"));
704 assert!(!json.as_object().unwrap().contains_key("meta"));
705 }
706
707 #[test]
708 fn test_image_content_with_uri() {
709 let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
710 let json = serde_json::to_value(&content).unwrap();
711 assert_eq!(json["uri"], "https://example.com/image.png");
712 }
713
714 #[test]
715 fn test_audio_content_roundtrip() {
716 let content = AudioContent::new("base64audio", "audio/mp3");
717 let json = serde_json::to_value(&content).unwrap();
718 let parsed: AudioContent = serde_json::from_value(json).unwrap();
719 assert_eq!(content, parsed);
720 }
721
722 #[test]
723 fn test_audio_content_omits_optional_fields() {
724 let content = AudioContent::new("data", "audio/mp3");
725 let json = serde_json::to_value(&content).unwrap();
726 assert!(!json.as_object().unwrap().contains_key("annotations"));
727 assert!(!json.as_object().unwrap().contains_key("meta"));
728 }
729}