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