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 #[serde_as(deserialize_as = "DefaultOnError")]
154 #[schemars(extend("x-deserialize-default-on-error" = true))]
155 #[serde(default)]
156 pub annotations: Option<Annotations>,
157 pub text: String,
159 #[serde(rename = "_meta")]
165 pub meta: Option<Meta>,
166}
167
168impl TextContent {
169 #[must_use]
171 pub fn new(text: impl Into<String>) -> Self {
172 Self {
173 annotations: None,
174 text: text.into(),
175 meta: None,
176 }
177 }
178
179 #[must_use]
181 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
182 self.annotations = annotations.into_option();
183 self
184 }
185
186 #[must_use]
192 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
193 self.meta = meta.into_option();
194 self
195 }
196}
197
198impl<T: Into<String>> From<T> for ContentBlock {
199 fn from(value: T) -> Self {
200 Self::Text(TextContent::new(value))
201 }
202}
203
204#[serde_as]
206#[skip_serializing_none]
207#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
208#[serde(rename_all = "camelCase")]
209#[non_exhaustive]
210pub struct ImageContent {
211 #[serde_as(deserialize_as = "DefaultOnError")]
213 #[schemars(extend("x-deserialize-default-on-error" = true))]
214 #[serde(default)]
215 pub annotations: Option<Annotations>,
216 pub data: String,
218 pub mime_type: String,
220 pub uri: Option<String>,
222 #[serde(rename = "_meta")]
228 pub meta: Option<Meta>,
229}
230
231impl ImageContent {
232 #[must_use]
234 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
235 Self {
236 annotations: None,
237 data: data.into(),
238 mime_type: mime_type.into(),
239 uri: None,
240 meta: None,
241 }
242 }
243
244 #[must_use]
246 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
247 self.annotations = annotations.into_option();
248 self
249 }
250
251 #[must_use]
253 pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
254 self.uri = uri.into_option();
255 self
256 }
257
258 #[must_use]
264 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
265 self.meta = meta.into_option();
266 self
267 }
268}
269
270#[serde_as]
272#[skip_serializing_none]
273#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
274#[serde(rename_all = "camelCase")]
275#[non_exhaustive]
276pub struct AudioContent {
277 #[serde_as(deserialize_as = "DefaultOnError")]
279 #[schemars(extend("x-deserialize-default-on-error" = true))]
280 #[serde(default)]
281 pub annotations: Option<Annotations>,
282 pub data: String,
284 pub mime_type: String,
286 #[serde(rename = "_meta")]
292 pub meta: Option<Meta>,
293}
294
295impl AudioContent {
296 #[must_use]
298 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
299 Self {
300 annotations: None,
301 data: data.into(),
302 mime_type: mime_type.into(),
303 meta: None,
304 }
305 }
306
307 #[must_use]
309 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
310 self.annotations = annotations.into_option();
311 self
312 }
313
314 #[must_use]
320 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
321 self.meta = meta.into_option();
322 self
323 }
324}
325
326#[serde_as]
328#[skip_serializing_none]
329#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
330#[non_exhaustive]
331pub struct EmbeddedResource {
332 #[serde_as(deserialize_as = "DefaultOnError")]
334 #[schemars(extend("x-deserialize-default-on-error" = true))]
335 #[serde(default)]
336 pub annotations: Option<Annotations>,
337 pub resource: EmbeddedResourceResource,
339 #[serde(rename = "_meta")]
345 pub meta: Option<Meta>,
346}
347
348impl EmbeddedResource {
349 #[must_use]
351 pub fn new(resource: EmbeddedResourceResource) -> Self {
352 Self {
353 annotations: None,
354 resource,
355 meta: None,
356 }
357 }
358
359 #[must_use]
361 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
362 self.annotations = annotations.into_option();
363 self
364 }
365
366 #[must_use]
372 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
373 self.meta = meta.into_option();
374 self
375 }
376}
377
378#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
380#[serde(untagged)]
381#[non_exhaustive]
382pub enum EmbeddedResourceResource {
383 TextResourceContents(TextResourceContents),
385 BlobResourceContents(BlobResourceContents),
387}
388
389#[skip_serializing_none]
391#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
392#[serde(rename_all = "camelCase")]
393#[non_exhaustive]
394pub struct TextResourceContents {
395 pub mime_type: Option<String>,
397 pub text: String,
399 pub uri: String,
401 #[serde(rename = "_meta")]
407 pub meta: Option<Meta>,
408}
409
410impl TextResourceContents {
411 #[must_use]
413 pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
414 Self {
415 mime_type: None,
416 text: text.into(),
417 uri: uri.into(),
418 meta: None,
419 }
420 }
421
422 #[must_use]
424 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
425 self.mime_type = mime_type.into_option();
426 self
427 }
428
429 #[must_use]
435 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
436 self.meta = meta.into_option();
437 self
438 }
439}
440
441#[skip_serializing_none]
443#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
444#[serde(rename_all = "camelCase")]
445#[non_exhaustive]
446pub struct BlobResourceContents {
447 pub blob: String,
449 pub mime_type: Option<String>,
451 pub uri: String,
453 #[serde(rename = "_meta")]
459 pub meta: Option<Meta>,
460}
461
462impl BlobResourceContents {
463 #[must_use]
465 pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
466 Self {
467 blob: blob.into(),
468 mime_type: None,
469 uri: uri.into(),
470 meta: None,
471 }
472 }
473
474 #[must_use]
476 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
477 self.mime_type = mime_type.into_option();
478 self
479 }
480
481 #[must_use]
487 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
488 self.meta = meta.into_option();
489 self
490 }
491}
492
493#[serde_as]
495#[skip_serializing_none]
496#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
497#[serde(rename_all = "camelCase")]
498#[non_exhaustive]
499pub struct ResourceLink {
500 #[serde_as(deserialize_as = "DefaultOnError")]
502 #[schemars(extend("x-deserialize-default-on-error" = true))]
503 #[serde(default)]
504 pub annotations: Option<Annotations>,
505 pub description: Option<String>,
507 pub mime_type: Option<String>,
509 pub name: String,
511 pub size: Option<i64>,
513 pub title: Option<String>,
515 pub uri: String,
517 #[serde(rename = "_meta")]
523 pub meta: Option<Meta>,
524}
525
526impl ResourceLink {
527 #[must_use]
529 pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
530 Self {
531 annotations: None,
532 description: None,
533 mime_type: None,
534 name: name.into(),
535 size: None,
536 title: None,
537 uri: uri.into(),
538 meta: None,
539 }
540 }
541
542 #[must_use]
544 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
545 self.annotations = annotations.into_option();
546 self
547 }
548
549 #[must_use]
551 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
552 self.description = description.into_option();
553 self
554 }
555
556 #[must_use]
558 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
559 self.mime_type = mime_type.into_option();
560 self
561 }
562
563 #[must_use]
565 pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
566 self.size = size.into_option();
567 self
568 }
569
570 #[must_use]
572 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
573 self.title = title.into_option();
574 self
575 }
576
577 #[must_use]
583 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
584 self.meta = meta.into_option();
585 self
586 }
587}
588
589#[serde_as]
591#[skip_serializing_none]
592#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
593#[serde(rename_all = "camelCase")]
594#[non_exhaustive]
595pub struct Annotations {
596 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
598 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
599 #[serde(default)]
600 pub audience: Option<Vec<Role>>,
601 pub last_modified: Option<String>,
603 pub priority: Option<f64>,
605 #[serde(rename = "_meta")]
611 pub meta: Option<Meta>,
612}
613
614impl Annotations {
615 #[must_use]
617 pub fn new() -> Self {
618 Self::default()
619 }
620
621 #[must_use]
623 pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
624 self.audience = audience.into_option();
625 self
626 }
627
628 #[must_use]
630 pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
631 self.last_modified = last_modified.into_option();
632 self
633 }
634
635 #[must_use]
637 pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
638 self.priority = priority.into_option();
639 self
640 }
641
642 #[must_use]
648 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
649 self.meta = meta.into_option();
650 self
651 }
652}
653
654#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
656#[serde(rename_all = "camelCase")]
657#[non_exhaustive]
658pub enum Role {
659 Assistant,
661 User,
663 #[serde(untagged)]
669 Other(String),
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675
676 #[test]
677 fn test_text_content_roundtrip() {
678 let content = TextContent::new("hello world");
679 let json = serde_json::to_value(&content).unwrap();
680 let parsed: TextContent = serde_json::from_value(json).unwrap();
681 assert_eq!(content, parsed);
682 }
683
684 #[test]
685 fn test_text_content_omits_optional_fields() {
686 let content = TextContent::new("hello");
687 let json = serde_json::to_value(&content).unwrap();
688 assert!(!json.as_object().unwrap().contains_key("annotations"));
689 assert!(!json.as_object().unwrap().contains_key("meta"));
690 }
691
692 #[test]
693 fn test_text_content_from_string() {
694 let block: ContentBlock = "hello".into();
695 match block {
696 ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
697 _ => panic!("Expected Text variant"),
698 }
699 }
700
701 #[test]
702 fn role_preserves_unknown_variant() {
703 let role: Role = serde_json::from_str("\"critic\"").unwrap();
704 assert_eq!(role, Role::Other("critic".to_string()));
705 assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
706 }
707
708 #[test]
709 fn content_block_preserves_unknown_variant() {
710 let block: ContentBlock = serde_json::from_value(serde_json::json!({
711 "type": "_widget",
712 "title": "Status",
713 "state": {"ok": true}
714 }))
715 .unwrap();
716
717 let ContentBlock::Other(unknown) = block else {
718 panic!("expected unknown content block");
719 };
720
721 assert_eq!(unknown.type_, "_widget");
722 assert_eq!(
723 unknown.fields.get("title"),
724 Some(&serde_json::json!("Status"))
725 );
726 assert_eq!(
727 serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
728 serde_json::json!({
729 "type": "_widget",
730 "title": "Status",
731 "state": {"ok": true}
732 })
733 );
734 }
735
736 #[test]
737 fn content_block_does_not_hide_malformed_known_variant() {
738 assert!(
739 serde_json::from_value::<ContentBlock>(serde_json::json!({
740 "type": "text"
741 }))
742 .is_err()
743 );
744 }
745
746 #[test]
747 fn test_image_content_roundtrip() {
748 let content = ImageContent::new("base64data", "image/png");
749 let json = serde_json::to_value(&content).unwrap();
750 let parsed: ImageContent = serde_json::from_value(json).unwrap();
751 assert_eq!(content, parsed);
752 }
753
754 #[test]
755 fn test_image_content_omits_optional_fields() {
756 let content = ImageContent::new("data", "image/png");
757 let json = serde_json::to_value(&content).unwrap();
758 assert!(!json.as_object().unwrap().contains_key("uri"));
759 assert!(!json.as_object().unwrap().contains_key("annotations"));
760 assert!(!json.as_object().unwrap().contains_key("meta"));
761 }
762
763 #[test]
764 fn test_image_content_with_uri() {
765 let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
766 let json = serde_json::to_value(&content).unwrap();
767 assert_eq!(json["uri"], "https://example.com/image.png");
768 }
769
770 #[test]
771 fn test_audio_content_roundtrip() {
772 let content = AudioContent::new("base64audio", "audio/mp3");
773 let json = serde_json::to_value(&content).unwrap();
774 let parsed: AudioContent = serde_json::from_value(json).unwrap();
775 assert_eq!(content, parsed);
776 }
777
778 #[test]
779 fn test_audio_content_omits_optional_fields() {
780 let content = AudioContent::new("data", "audio/mp3");
781 let json = serde_json::to_value(&content).unwrap();
782 assert!(!json.as_object().unwrap().contains_key("annotations"));
783 assert!(!json.as_object().unwrap().contains_key("meta"));
784 }
785}