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)]
37#[serde(tag = "type", rename_all = "snake_case")]
38#[schemars(extend("discriminator" = {"propertyName": "type"}))]
39#[non_exhaustive]
40pub enum ContentBlock {
41 Text(TextContent),
46 Image(ImageContent),
50 Audio(AudioContent),
54 ResourceLink(ResourceLink),
58 Resource(EmbeddedResource),
64 #[serde(untagged)]
74 Other(OtherContentBlock),
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
79#[schemars(inline)]
80#[schemars(transform = other_content_block_schema)]
81#[serde(rename_all = "camelCase")]
82#[non_exhaustive]
83pub struct OtherContentBlock {
84 #[serde(rename = "type")]
90 pub type_: String,
91 #[serde(flatten)]
93 pub fields: BTreeMap<String, serde_json::Value>,
94}
95
96impl OtherContentBlock {
97 #[must_use]
99 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
100 fields.remove("type");
101 Self {
102 type_: type_.into(),
103 fields,
104 }
105 }
106}
107
108impl<'de> Deserialize<'de> for OtherContentBlock {
109 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110 where
111 D: serde::Deserializer<'de>,
112 {
113 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
114 let type_ = fields
115 .remove("type")
116 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
117 let serde_json::Value::String(type_) = type_ else {
118 return Err(serde::de::Error::custom("`type` must be a string"));
119 };
120
121 if is_known_content_block_type(&type_) {
122 return Err(serde::de::Error::custom(format!(
123 "known content block `{type_}` did not match its schema"
124 )));
125 }
126
127 Ok(Self { type_, fields })
128 }
129}
130
131fn is_known_content_block_type(type_: &str) -> bool {
132 matches!(
133 type_,
134 "text" | "image" | "audio" | "resource_link" | "resource"
135 )
136}
137
138fn other_content_block_schema(schema: &mut Schema) {
139 super::schema_util::reject_known_string_discriminators(
140 schema,
141 "type",
142 &["text", "image", "audio", "resource_link", "resource"],
143 );
144}
145
146#[serde_as]
148#[skip_serializing_none]
149#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
150#[non_exhaustive]
151pub struct TextContent {
152 pub text: String,
154 #[serde_as(deserialize_as = "DefaultOnError")]
156 #[schemars(extend("x-deserialize-default-on-error" = true))]
157 #[serde(default)]
158 pub annotations: Option<Annotations>,
159 #[serde_as(deserialize_as = "DefaultOnError")]
165 #[schemars(extend("x-deserialize-default-on-error" = true))]
166 #[serde(default)]
167 #[serde(rename = "_meta")]
168 pub meta: Option<Meta>,
169}
170
171impl TextContent {
172 #[must_use]
174 pub fn new(text: impl Into<String>) -> Self {
175 Self {
176 annotations: None,
177 text: text.into(),
178 meta: None,
179 }
180 }
181
182 #[must_use]
184 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
185 self.annotations = annotations.into_option();
186 self
187 }
188
189 #[must_use]
195 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
196 self.meta = meta.into_option();
197 self
198 }
199}
200
201impl<T: Into<String>> From<T> for ContentBlock {
202 fn from(value: T) -> Self {
203 Self::Text(TextContent::new(value))
204 }
205}
206
207#[serde_as]
209#[skip_serializing_none]
210#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
211#[serde(rename_all = "camelCase")]
212#[non_exhaustive]
213pub struct ImageContent {
214 pub data: String,
216 pub mime_type: String,
218 #[serde_as(deserialize_as = "DefaultOnError")]
220 #[schemars(extend("x-deserialize-default-on-error" = true))]
221 #[serde(default)]
222 pub uri: Option<String>,
223 #[serde_as(deserialize_as = "DefaultOnError")]
225 #[schemars(extend("x-deserialize-default-on-error" = true))]
226 #[serde(default)]
227 pub annotations: Option<Annotations>,
228 #[serde_as(deserialize_as = "DefaultOnError")]
234 #[schemars(extend("x-deserialize-default-on-error" = true))]
235 #[serde(default)]
236 #[serde(rename = "_meta")]
237 pub meta: Option<Meta>,
238}
239
240impl ImageContent {
241 #[must_use]
243 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
244 Self {
245 annotations: None,
246 data: data.into(),
247 mime_type: mime_type.into(),
248 uri: None,
249 meta: None,
250 }
251 }
252
253 #[must_use]
255 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
256 self.annotations = annotations.into_option();
257 self
258 }
259
260 #[must_use]
262 pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
263 self.uri = uri.into_option();
264 self
265 }
266
267 #[must_use]
273 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
274 self.meta = meta.into_option();
275 self
276 }
277}
278
279#[serde_as]
281#[skip_serializing_none]
282#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
283#[serde(rename_all = "camelCase")]
284#[non_exhaustive]
285pub struct AudioContent {
286 pub data: String,
288 pub mime_type: String,
290 #[serde_as(deserialize_as = "DefaultOnError")]
292 #[schemars(extend("x-deserialize-default-on-error" = true))]
293 #[serde(default)]
294 pub annotations: Option<Annotations>,
295 #[serde_as(deserialize_as = "DefaultOnError")]
301 #[schemars(extend("x-deserialize-default-on-error" = true))]
302 #[serde(default)]
303 #[serde(rename = "_meta")]
304 pub meta: Option<Meta>,
305}
306
307impl AudioContent {
308 #[must_use]
310 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
311 Self {
312 annotations: None,
313 data: data.into(),
314 mime_type: mime_type.into(),
315 meta: None,
316 }
317 }
318
319 #[must_use]
321 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
322 self.annotations = annotations.into_option();
323 self
324 }
325
326 #[must_use]
332 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
333 self.meta = meta.into_option();
334 self
335 }
336}
337
338#[serde_as]
340#[skip_serializing_none]
341#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
342#[non_exhaustive]
343pub struct EmbeddedResource {
344 pub resource: EmbeddedResourceResource,
346 #[serde_as(deserialize_as = "DefaultOnError")]
348 #[schemars(extend("x-deserialize-default-on-error" = true))]
349 #[serde(default)]
350 pub annotations: Option<Annotations>,
351 #[serde_as(deserialize_as = "DefaultOnError")]
357 #[schemars(extend("x-deserialize-default-on-error" = true))]
358 #[serde(default)]
359 #[serde(rename = "_meta")]
360 pub meta: Option<Meta>,
361}
362
363impl EmbeddedResource {
364 #[must_use]
366 pub fn new(resource: EmbeddedResourceResource) -> Self {
367 Self {
368 annotations: None,
369 resource,
370 meta: None,
371 }
372 }
373
374 #[must_use]
376 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
377 self.annotations = annotations.into_option();
378 self
379 }
380
381 #[must_use]
387 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
388 self.meta = meta.into_option();
389 self
390 }
391}
392
393#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
395#[serde(untagged)]
396#[non_exhaustive]
397pub enum EmbeddedResourceResource {
398 TextResourceContents(TextResourceContents),
400 BlobResourceContents(BlobResourceContents),
402}
403
404#[serde_as]
406#[skip_serializing_none]
407#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
408#[serde(rename_all = "camelCase")]
409#[non_exhaustive]
410pub struct TextResourceContents {
411 pub text: String,
413 pub uri: String,
415 #[serde_as(deserialize_as = "DefaultOnError")]
417 #[schemars(extend("x-deserialize-default-on-error" = true))]
418 #[serde(default)]
419 pub mime_type: Option<String>,
420 #[serde_as(deserialize_as = "DefaultOnError")]
426 #[schemars(extend("x-deserialize-default-on-error" = true))]
427 #[serde(default)]
428 #[serde(rename = "_meta")]
429 pub meta: Option<Meta>,
430}
431
432impl TextResourceContents {
433 #[must_use]
435 pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
436 Self {
437 mime_type: None,
438 text: text.into(),
439 uri: uri.into(),
440 meta: None,
441 }
442 }
443
444 #[must_use]
446 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
447 self.mime_type = mime_type.into_option();
448 self
449 }
450
451 #[must_use]
457 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
458 self.meta = meta.into_option();
459 self
460 }
461}
462
463#[serde_as]
465#[skip_serializing_none]
466#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
467#[serde(rename_all = "camelCase")]
468#[non_exhaustive]
469pub struct BlobResourceContents {
470 pub blob: String,
472 pub uri: String,
474 #[serde_as(deserialize_as = "DefaultOnError")]
476 #[schemars(extend("x-deserialize-default-on-error" = true))]
477 #[serde(default)]
478 pub mime_type: Option<String>,
479 #[serde_as(deserialize_as = "DefaultOnError")]
485 #[schemars(extend("x-deserialize-default-on-error" = true))]
486 #[serde(default)]
487 #[serde(rename = "_meta")]
488 pub meta: Option<Meta>,
489}
490
491impl BlobResourceContents {
492 #[must_use]
494 pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
495 Self {
496 blob: blob.into(),
497 mime_type: None,
498 uri: uri.into(),
499 meta: None,
500 }
501 }
502
503 #[must_use]
505 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
506 self.mime_type = mime_type.into_option();
507 self
508 }
509
510 #[must_use]
516 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
517 self.meta = meta.into_option();
518 self
519 }
520}
521
522#[serde_as]
524#[skip_serializing_none]
525#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
526#[serde(rename_all = "camelCase")]
527#[non_exhaustive]
528pub struct ResourceLink {
529 pub name: String,
531 pub uri: String,
533 #[serde_as(deserialize_as = "DefaultOnError")]
535 #[schemars(extend("x-deserialize-default-on-error" = true))]
536 #[serde(default)]
537 pub title: Option<String>,
538 #[serde_as(deserialize_as = "DefaultOnError")]
540 #[schemars(extend("x-deserialize-default-on-error" = true))]
541 #[serde(default)]
542 pub description: Option<String>,
543 #[serde_as(deserialize_as = "DefaultOnError")]
545 #[schemars(extend("x-deserialize-default-on-error" = true))]
546 #[serde(default)]
547 pub mime_type: Option<String>,
548 #[serde_as(deserialize_as = "DefaultOnError")]
550 #[schemars(extend("x-deserialize-default-on-error" = true))]
551 #[serde(default)]
552 pub size: Option<i64>,
553 #[serde_as(deserialize_as = "DefaultOnError")]
555 #[schemars(extend("x-deserialize-default-on-error" = true))]
556 #[serde(default)]
557 pub annotations: Option<Annotations>,
558 #[serde_as(deserialize_as = "DefaultOnError")]
564 #[schemars(extend("x-deserialize-default-on-error" = true))]
565 #[serde(default)]
566 #[serde(rename = "_meta")]
567 pub meta: Option<Meta>,
568}
569
570impl ResourceLink {
571 #[must_use]
573 pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
574 Self {
575 annotations: None,
576 description: None,
577 mime_type: None,
578 name: name.into(),
579 size: None,
580 title: None,
581 uri: uri.into(),
582 meta: None,
583 }
584 }
585
586 #[must_use]
588 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
589 self.annotations = annotations.into_option();
590 self
591 }
592
593 #[must_use]
595 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
596 self.description = description.into_option();
597 self
598 }
599
600 #[must_use]
602 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
603 self.mime_type = mime_type.into_option();
604 self
605 }
606
607 #[must_use]
609 pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
610 self.size = size.into_option();
611 self
612 }
613
614 #[must_use]
616 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
617 self.title = title.into_option();
618 self
619 }
620
621 #[must_use]
627 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
628 self.meta = meta.into_option();
629 self
630 }
631}
632
633#[serde_as]
635#[skip_serializing_none]
636#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
637#[serde(rename_all = "camelCase")]
638#[non_exhaustive]
639pub struct Annotations {
640 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
642 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
643 #[serde(default)]
644 pub audience: Option<Vec<Role>>,
645 #[serde_as(deserialize_as = "DefaultOnError")]
647 #[schemars(extend("x-deserialize-default-on-error" = true))]
648 #[serde(default)]
649 pub last_modified: Option<String>,
650 #[serde_as(deserialize_as = "DefaultOnError")]
652 #[schemars(extend("x-deserialize-default-on-error" = true))]
653 #[serde(default)]
654 pub priority: Option<f64>,
655 #[serde_as(deserialize_as = "DefaultOnError")]
661 #[schemars(extend("x-deserialize-default-on-error" = true))]
662 #[serde(default)]
663 #[serde(rename = "_meta")]
664 pub meta: Option<Meta>,
665}
666
667impl Annotations {
668 #[must_use]
670 pub fn new() -> Self {
671 Self::default()
672 }
673
674 #[must_use]
676 pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
677 self.audience = audience.into_option();
678 self
679 }
680
681 #[must_use]
683 pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
684 self.last_modified = last_modified.into_option();
685 self
686 }
687
688 #[must_use]
690 pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
691 self.priority = priority.into_option();
692 self
693 }
694
695 #[must_use]
701 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
702 self.meta = meta.into_option();
703 self
704 }
705}
706
707#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
709#[serde(rename_all = "camelCase")]
710#[non_exhaustive]
711pub enum Role {
712 Assistant,
714 User,
716 #[serde(untagged)]
722 Other(String),
723}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728
729 #[test]
730 fn test_text_content_roundtrip() {
731 let content = TextContent::new("hello world");
732 let json = serde_json::to_value(&content).unwrap();
733 let parsed: TextContent = serde_json::from_value(json).unwrap();
734 assert_eq!(content, parsed);
735 }
736
737 #[test]
738 fn test_text_content_omits_optional_fields() {
739 let content = TextContent::new("hello");
740 let json = serde_json::to_value(&content).unwrap();
741 assert!(!json.as_object().unwrap().contains_key("annotations"));
742 assert!(!json.as_object().unwrap().contains_key("meta"));
743 }
744
745 #[test]
746 fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
747 let missing: TextContent = serde_json::from_value(serde_json::json!({
748 "text": "hello"
749 }))
750 .unwrap();
751 assert_eq!(missing.meta, None);
752
753 let malformed: TextContent = serde_json::from_value(serde_json::json!({
754 "text": "hello",
755 "_meta": false
756 }))
757 .unwrap();
758 assert_eq!(malformed.meta, None);
759 }
760
761 #[test]
762 fn test_text_content_from_string() {
763 let block: ContentBlock = "hello".into();
764 match block {
765 ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
766 _ => panic!("Expected Text variant"),
767 }
768 }
769
770 #[test]
771 fn role_preserves_unknown_variant() {
772 let role: Role = serde_json::from_str("\"critic\"").unwrap();
773 assert_eq!(role, Role::Other("critic".to_string()));
774 assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
775 }
776
777 #[test]
778 fn content_block_preserves_unknown_variant() {
779 let block: ContentBlock = serde_json::from_value(serde_json::json!({
780 "type": "_widget",
781 "title": "Status",
782 "state": {"ok": true}
783 }))
784 .unwrap();
785
786 let ContentBlock::Other(unknown) = block else {
787 panic!("expected unknown content block");
788 };
789
790 assert_eq!(unknown.type_, "_widget");
791 assert_eq!(
792 unknown.fields.get("title"),
793 Some(&serde_json::json!("Status"))
794 );
795 assert_eq!(
796 serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
797 serde_json::json!({
798 "type": "_widget",
799 "title": "Status",
800 "state": {"ok": true}
801 })
802 );
803 }
804
805 #[test]
806 fn content_block_does_not_hide_malformed_known_variant() {
807 assert!(
808 serde_json::from_value::<ContentBlock>(serde_json::json!({
809 "type": "text"
810 }))
811 .is_err()
812 );
813 }
814
815 #[test]
816 fn test_image_content_roundtrip() {
817 let content = ImageContent::new("base64data", "image/png");
818 let json = serde_json::to_value(&content).unwrap();
819 let parsed: ImageContent = serde_json::from_value(json).unwrap();
820 assert_eq!(content, parsed);
821 }
822
823 #[test]
824 fn test_image_content_omits_optional_fields() {
825 let content = ImageContent::new("data", "image/png");
826 let json = serde_json::to_value(&content).unwrap();
827 assert!(!json.as_object().unwrap().contains_key("uri"));
828 assert!(!json.as_object().unwrap().contains_key("annotations"));
829 assert!(!json.as_object().unwrap().contains_key("meta"));
830 }
831
832 #[test]
833 fn test_image_content_with_uri() {
834 let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
835 let json = serde_json::to_value(&content).unwrap();
836 assert_eq!(json["uri"], "https://example.com/image.png");
837 }
838
839 #[test]
840 fn test_audio_content_roundtrip() {
841 let content = AudioContent::new("base64audio", "audio/mp3");
842 let json = serde_json::to_value(&content).unwrap();
843 let parsed: AudioContent = serde_json::from_value(json).unwrap();
844 assert_eq!(content, parsed);
845 }
846
847 #[test]
848 fn test_audio_content_omits_optional_fields() {
849 let content = AudioContent::new("data", "audio/mp3");
850 let json = serde_json::to_value(&content).unwrap();
851 assert!(!json.as_object().unwrap().contains_key("annotations"));
852 assert!(!json.as_object().unwrap().contains_key("meta"));
853 }
854}