1use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
13
14use derive_more::{Display, From};
15#[cfg(feature = "schemars")]
16use schemars::Schema;
17use serde::{Deserialize, Serialize};
18use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
19
20use super::Meta;
21use crate::{IntoOption, SkipListener};
22
23#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
26#[serde(transparent)]
27#[from(Arc<str>, String, &str, &mut str, Box<str>, Cow<'_, str>)]
28#[non_exhaustive]
29pub struct MediaType(pub Arc<str>);
30
31impl MediaType {
32 #[must_use]
34 pub fn new(media_type: impl Into<Self>) -> Self {
35 media_type.into()
36 }
37}
38
39impl AsRef<str> for MediaType {
40 fn as_ref(&self) -> &str {
41 &self.0
42 }
43}
44
45impl From<&String> for MediaType {
46 fn from(media_type: &String) -> Self {
47 Self(media_type.as_str().into())
48 }
49}
50
51macro_rules! impl_media_type_option_conversion {
52 ($source:ty) => {
53 impl IntoOption<MediaType> for $source {
54 fn into_option(self) -> Option<MediaType> {
55 Some(self.into())
56 }
57 }
58 };
59}
60
61impl_media_type_option_conversion!(Arc<str>);
62impl_media_type_option_conversion!(String);
63impl_media_type_option_conversion!(&str);
64impl_media_type_option_conversion!(&mut str);
65impl_media_type_option_conversion!(&String);
66impl_media_type_option_conversion!(Box<str>);
67impl_media_type_option_conversion!(Cow<'_, str>);
68
69#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
85#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
86#[serde(tag = "type", rename_all = "snake_case")]
87#[non_exhaustive]
88pub enum ContentBlock {
89 Text(TextContent),
94 Image(ImageContent),
98 Audio(AudioContent),
102 ResourceLink(ResourceLink),
106 Resource(EmbeddedResource),
112 #[serde(untagged)]
122 Other(OtherContentBlock),
123}
124
125#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
127#[derive(Debug, Clone, PartialEq, Serialize)]
128#[cfg_attr(feature = "schemars", schemars(inline))]
129#[cfg_attr(feature = "schemars", schemars(transform = other_content_block_schema))]
130#[serde(rename_all = "camelCase")]
131#[non_exhaustive]
132pub struct OtherContentBlock {
133 #[serde(rename = "type")]
139 pub type_: String,
140 #[serde(flatten)]
142 pub fields: BTreeMap<String, serde_json::Value>,
143}
144
145impl OtherContentBlock {
146 #[must_use]
148 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
149 fields.remove("type");
150 Self {
151 type_: type_.into(),
152 fields,
153 }
154 }
155}
156
157impl<'de> Deserialize<'de> for OtherContentBlock {
158 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159 where
160 D: serde::Deserializer<'de>,
161 {
162 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
163 let type_ = fields
164 .remove("type")
165 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
166 let serde_json::Value::String(type_) = type_ else {
167 return Err(serde::de::Error::custom("`type` must be a string"));
168 };
169
170 if is_known_content_block_type(&type_) {
171 return Err(serde::de::Error::custom(format!(
172 "known content block `{type_}` did not match its schema"
173 )));
174 }
175
176 Ok(Self { type_, fields })
177 }
178}
179
180fn is_known_content_block_type(type_: &str) -> bool {
181 matches!(
182 type_,
183 "text" | "image" | "audio" | "resource_link" | "resource"
184 )
185}
186
187#[cfg(feature = "schemars")]
188fn other_content_block_schema(schema: &mut Schema) {
189 super::schema_util::reject_known_string_discriminators(
190 schema,
191 "type",
192 &["text", "image", "audio", "resource_link", "resource"],
193 );
194}
195
196#[serde_as]
198#[skip_serializing_none]
199#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
200#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
201#[non_exhaustive]
202pub struct TextContent {
203 pub text: String,
205 #[serde_as(deserialize_as = "DefaultOnError")]
207 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
208 #[serde(default)]
209 pub annotations: Option<Annotations>,
210 #[serde_as(deserialize_as = "DefaultOnError")]
216 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
217 #[serde(default)]
218 #[serde(rename = "_meta")]
219 pub meta: Option<Meta>,
220}
221
222impl TextContent {
223 #[must_use]
225 pub fn new(text: impl Into<String>) -> Self {
226 Self {
227 annotations: None,
228 text: text.into(),
229 meta: None,
230 }
231 }
232
233 #[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]
246 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
247 self.meta = meta.into_option();
248 self
249 }
250}
251
252impl<T: Into<String>> From<T> for ContentBlock {
253 fn from(value: T) -> Self {
254 Self::Text(TextContent::new(value))
255 }
256}
257
258#[serde_as]
260#[skip_serializing_none]
261#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
262#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
263#[serde(rename_all = "camelCase")]
264#[non_exhaustive]
265pub struct ImageContent {
266 #[cfg_attr(feature = "schemars", schemars(extend("contentEncoding" = "base64")))]
268 pub data: String,
269 pub mime_type: MediaType,
271 #[serde_as(deserialize_as = "DefaultOnError")]
273 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
274 #[cfg_attr(feature = "schemars", schemars(url))]
275 #[serde(default)]
276 pub uri: Option<String>,
277 #[serde_as(deserialize_as = "DefaultOnError")]
279 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
280 #[serde(default)]
281 pub annotations: Option<Annotations>,
282 #[serde_as(deserialize_as = "DefaultOnError")]
288 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
289 #[serde(default)]
290 #[serde(rename = "_meta")]
291 pub meta: Option<Meta>,
292}
293
294impl ImageContent {
295 #[must_use]
297 pub fn new(data: impl Into<String>, mime_type: impl Into<MediaType>) -> Self {
298 Self {
299 annotations: None,
300 data: data.into(),
301 mime_type: mime_type.into(),
302 uri: None,
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]
316 pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
317 self.uri = uri.into_option();
318 self
319 }
320
321 #[must_use]
327 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
328 self.meta = meta.into_option();
329 self
330 }
331}
332
333#[serde_as]
335#[skip_serializing_none]
336#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
337#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
338#[serde(rename_all = "camelCase")]
339#[non_exhaustive]
340pub struct AudioContent {
341 #[cfg_attr(feature = "schemars", schemars(extend("contentEncoding" = "base64")))]
343 pub data: String,
344 pub mime_type: MediaType,
346 #[serde_as(deserialize_as = "DefaultOnError")]
348 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
349 #[serde(default)]
350 pub annotations: Option<Annotations>,
351 #[serde_as(deserialize_as = "DefaultOnError")]
357 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
358 #[serde(default)]
359 #[serde(rename = "_meta")]
360 pub meta: Option<Meta>,
361}
362
363impl AudioContent {
364 #[must_use]
366 pub fn new(data: impl Into<String>, mime_type: impl Into<MediaType>) -> Self {
367 Self {
368 annotations: None,
369 data: data.into(),
370 mime_type: mime_type.into(),
371 meta: None,
372 }
373 }
374
375 #[must_use]
377 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
378 self.annotations = annotations.into_option();
379 self
380 }
381
382 #[must_use]
388 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
389 self.meta = meta.into_option();
390 self
391 }
392}
393
394#[serde_as]
396#[skip_serializing_none]
397#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
398#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
399#[non_exhaustive]
400pub struct EmbeddedResource {
401 pub resource: EmbeddedResourceResource,
403 #[serde_as(deserialize_as = "DefaultOnError")]
405 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
406 #[serde(default)]
407 pub annotations: Option<Annotations>,
408 #[serde_as(deserialize_as = "DefaultOnError")]
414 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
415 #[serde(default)]
416 #[serde(rename = "_meta")]
417 pub meta: Option<Meta>,
418}
419
420impl EmbeddedResource {
421 #[must_use]
423 pub fn new(resource: EmbeddedResourceResource) -> Self {
424 Self {
425 annotations: None,
426 resource,
427 meta: None,
428 }
429 }
430
431 #[must_use]
433 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
434 self.annotations = annotations.into_option();
435 self
436 }
437
438 #[must_use]
444 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
445 self.meta = meta.into_option();
446 self
447 }
448}
449
450#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
452#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
453#[serde(untagged)]
454#[non_exhaustive]
455pub enum EmbeddedResourceResource {
456 TextResourceContents(TextResourceContents),
458 BlobResourceContents(BlobResourceContents),
460}
461
462#[serde_as]
464#[skip_serializing_none]
465#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
466#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
467#[serde(rename_all = "camelCase")]
468#[non_exhaustive]
469pub struct TextResourceContents {
470 pub text: String,
472 #[cfg_attr(feature = "schemars", schemars(url))]
474 pub uri: String,
475 #[serde_as(deserialize_as = "DefaultOnError")]
477 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
478 #[serde(default)]
479 pub mime_type: Option<MediaType>,
480 #[serde_as(deserialize_as = "DefaultOnError")]
486 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
487 #[serde(default)]
488 #[serde(rename = "_meta")]
489 pub meta: Option<Meta>,
490}
491
492impl TextResourceContents {
493 #[must_use]
495 pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
496 Self {
497 mime_type: None,
498 text: text.into(),
499 uri: uri.into(),
500 meta: None,
501 }
502 }
503
504 #[must_use]
506 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
507 self.mime_type = mime_type.into_option();
508 self
509 }
510
511 #[must_use]
517 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
518 self.meta = meta.into_option();
519 self
520 }
521}
522
523#[serde_as]
525#[skip_serializing_none]
526#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
527#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
528#[serde(rename_all = "camelCase")]
529#[non_exhaustive]
530pub struct BlobResourceContents {
531 #[cfg_attr(feature = "schemars", schemars(extend("contentEncoding" = "base64")))]
533 pub blob: String,
534 #[cfg_attr(feature = "schemars", schemars(url))]
536 pub uri: String,
537 #[serde_as(deserialize_as = "DefaultOnError")]
539 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
540 #[serde(default)]
541 pub mime_type: Option<MediaType>,
542 #[serde_as(deserialize_as = "DefaultOnError")]
548 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
549 #[serde(default)]
550 #[serde(rename = "_meta")]
551 pub meta: Option<Meta>,
552}
553
554impl BlobResourceContents {
555 #[must_use]
557 pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
558 Self {
559 blob: blob.into(),
560 mime_type: None,
561 uri: uri.into(),
562 meta: None,
563 }
564 }
565
566 #[must_use]
568 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
569 self.mime_type = mime_type.into_option();
570 self
571 }
572
573 #[must_use]
579 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
580 self.meta = meta.into_option();
581 self
582 }
583}
584
585#[serde_as]
587#[skip_serializing_none]
588#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
589#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
590#[serde(rename_all = "camelCase")]
591#[non_exhaustive]
592pub struct ResourceLink {
593 pub name: String,
595 #[cfg_attr(feature = "schemars", schemars(url))]
597 pub uri: String,
598 #[serde_as(deserialize_as = "DefaultOnError")]
600 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
601 #[serde(default)]
602 pub title: Option<String>,
603 #[serde_as(deserialize_as = "DefaultOnError")]
605 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
606 #[serde(default)]
607 pub description: Option<String>,
608 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
610 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
611 #[serde(default)]
612 pub icons: Option<Vec<Icon>>,
613 #[serde_as(deserialize_as = "DefaultOnError")]
615 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
616 #[serde(default)]
617 pub mime_type: Option<MediaType>,
618 #[serde_as(deserialize_as = "DefaultOnError")]
620 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
621 #[serde(default)]
622 pub size: Option<i64>,
623 #[serde_as(deserialize_as = "DefaultOnError")]
625 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
626 #[serde(default)]
627 pub annotations: Option<Annotations>,
628 #[serde_as(deserialize_as = "DefaultOnError")]
634 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
635 #[serde(default)]
636 #[serde(rename = "_meta")]
637 pub meta: Option<Meta>,
638}
639
640impl ResourceLink {
641 #[must_use]
643 pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
644 Self {
645 annotations: None,
646 description: None,
647 icons: None,
648 mime_type: None,
649 name: name.into(),
650 size: None,
651 title: None,
652 uri: uri.into(),
653 meta: None,
654 }
655 }
656
657 #[must_use]
659 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
660 self.annotations = annotations.into_option();
661 self
662 }
663
664 #[must_use]
666 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
667 self.description = description.into_option();
668 self
669 }
670
671 #[must_use]
673 pub fn icons(mut self, icons: impl IntoOption<Vec<Icon>>) -> Self {
674 self.icons = icons.into_option();
675 self
676 }
677
678 #[must_use]
680 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
681 self.mime_type = mime_type.into_option();
682 self
683 }
684
685 #[must_use]
687 pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
688 self.size = size.into_option();
689 self
690 }
691
692 #[must_use]
694 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
695 self.title = title.into_option();
696 self
697 }
698
699 #[must_use]
705 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
706 self.meta = meta.into_option();
707 self
708 }
709}
710
711#[serde_as]
713#[skip_serializing_none]
714#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
715#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
716#[serde(rename_all = "camelCase")]
717#[non_exhaustive]
718pub struct Icon {
719 #[cfg_attr(feature = "schemars", schemars(url))]
721 pub src: String,
722 #[serde_as(deserialize_as = "DefaultOnError")]
724 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
725 #[serde(default)]
726 pub mime_type: Option<MediaType>,
727 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
733 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
734 #[serde(default)]
735 pub sizes: Option<Vec<String>>,
736 #[serde_as(deserialize_as = "DefaultOnError")]
738 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
739 #[serde(default)]
740 pub theme: Option<IconTheme>,
741}
742
743impl Icon {
744 #[must_use]
746 pub fn new(src: impl Into<String>) -> Self {
747 Self {
748 src: src.into(),
749 mime_type: None,
750 sizes: None,
751 theme: None,
752 }
753 }
754
755 #[must_use]
757 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
758 self.mime_type = mime_type.into_option();
759 self
760 }
761
762 #[must_use]
764 pub fn sizes(mut self, sizes: impl IntoOption<Vec<String>>) -> Self {
765 self.sizes = sizes.into_option();
766 self
767 }
768
769 #[must_use]
771 pub fn theme(mut self, theme: impl IntoOption<IconTheme>) -> Self {
772 self.theme = theme.into_option();
773 self
774 }
775}
776
777#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
779#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
780#[serde(rename_all = "camelCase")]
781#[non_exhaustive]
782pub enum IconTheme {
783 Light,
785 Dark,
787 #[serde(untagged)]
793 Other(String),
794}
795
796#[serde_as]
798#[skip_serializing_none]
799#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
800#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
801#[serde(rename_all = "camelCase")]
802#[non_exhaustive]
803pub struct Annotations {
804 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
806 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
807 #[serde(default)]
808 pub audience: Option<Vec<Role>>,
809 #[serde_as(deserialize_as = "DefaultOnError")]
813 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
814 #[serde(default)]
815 pub last_modified: Option<String>,
816 #[serde_as(deserialize_as = "DefaultOnError")]
818 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
819 #[cfg_attr(feature = "schemars", schemars(range(min = 0, max = 1)))]
820 #[serde(default)]
821 pub priority: Option<f64>,
822 #[serde_as(deserialize_as = "DefaultOnError")]
828 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
829 #[serde(default)]
830 #[serde(rename = "_meta")]
831 pub meta: Option<Meta>,
832}
833
834impl Annotations {
835 #[must_use]
837 pub fn new() -> Self {
838 Self::default()
839 }
840
841 #[must_use]
843 pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
844 self.audience = audience.into_option();
845 self
846 }
847
848 #[must_use]
850 pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
851 self.last_modified = last_modified.into_option();
852 self
853 }
854
855 #[must_use]
857 pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
858 self.priority = priority.into_option();
859 self
860 }
861
862 #[must_use]
868 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
869 self.meta = meta.into_option();
870 self
871 }
872}
873
874#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
876#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
877#[serde(rename_all = "camelCase")]
878#[non_exhaustive]
879pub enum Role {
880 Assistant,
882 User,
884 #[serde(untagged)]
890 Other(String),
891}
892
893#[cfg(test)]
894mod tests {
895 use super::*;
896
897 #[test]
898 fn test_text_content_roundtrip() {
899 let content = TextContent::new("hello world");
900 let json = serde_json::to_value(&content).unwrap();
901 let parsed: TextContent = serde_json::from_value(json).unwrap();
902 assert_eq!(content, parsed);
903 }
904
905 #[test]
906 fn test_text_content_omits_optional_fields() {
907 let content = TextContent::new("hello");
908 let json = serde_json::to_value(&content).unwrap();
909 assert!(!json.as_object().unwrap().contains_key("annotations"));
910 assert!(!json.as_object().unwrap().contains_key("meta"));
911 }
912
913 #[test]
914 fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
915 let missing: TextContent = serde_json::from_value(serde_json::json!({
916 "text": "hello"
917 }))
918 .unwrap();
919 assert_eq!(missing.meta, None);
920
921 let malformed: TextContent = serde_json::from_value(serde_json::json!({
922 "text": "hello",
923 "_meta": false
924 }))
925 .unwrap();
926 assert_eq!(malformed.meta, None);
927 }
928
929 #[test]
930 fn test_text_content_from_string() {
931 let block: ContentBlock = "hello".into();
932 match block {
933 ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
934 _ => panic!("Expected Text variant"),
935 }
936 }
937
938 #[test]
939 fn role_preserves_unknown_variant() {
940 let role: Role = serde_json::from_str("\"critic\"").unwrap();
941 assert_eq!(role, Role::Other("critic".to_string()));
942 assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
943 }
944
945 #[test]
946 fn icon_theme_preserves_unknown_variant() {
947 let theme: IconTheme = serde_json::from_str("\"contrast\"").unwrap();
948 assert_eq!(theme, IconTheme::Other("contrast".to_string()));
949 assert_eq!(serde_json::to_value(&theme).unwrap(), "contrast");
950 }
951
952 #[test]
953 fn content_block_preserves_unknown_variant() {
954 let block: ContentBlock = serde_json::from_value(serde_json::json!({
955 "type": "_widget",
956 "title": "Status",
957 "state": {"ok": true}
958 }))
959 .unwrap();
960
961 let ContentBlock::Other(unknown) = block else {
962 panic!("expected unknown content block");
963 };
964
965 assert_eq!(unknown.type_, "_widget");
966 assert_eq!(
967 unknown.fields.get("title"),
968 Some(&serde_json::json!("Status"))
969 );
970 assert_eq!(
971 serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
972 serde_json::json!({
973 "type": "_widget",
974 "title": "Status",
975 "state": {"ok": true}
976 })
977 );
978 }
979
980 #[test]
981 fn content_block_does_not_hide_malformed_known_variant() {
982 assert!(
983 serde_json::from_value::<ContentBlock>(serde_json::json!({
984 "type": "text"
985 }))
986 .is_err()
987 );
988 }
989
990 #[test]
991 fn test_image_content_roundtrip() {
992 let content = ImageContent::new("base64data", "image/png");
993 let json = serde_json::to_value(&content).unwrap();
994 let parsed: ImageContent = serde_json::from_value(json).unwrap();
995 assert_eq!(content, parsed);
996 }
997
998 #[test]
999 fn test_image_content_omits_optional_fields() {
1000 let content = ImageContent::new("data", "image/png");
1001 let json = serde_json::to_value(&content).unwrap();
1002 assert!(!json.as_object().unwrap().contains_key("uri"));
1003 assert!(!json.as_object().unwrap().contains_key("annotations"));
1004 assert!(!json.as_object().unwrap().contains_key("meta"));
1005 }
1006
1007 #[test]
1008 fn test_image_content_with_uri() {
1009 let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
1010 let json = serde_json::to_value(&content).unwrap();
1011 assert_eq!(json["uri"], "https://example.com/image.png");
1012 }
1013
1014 #[test]
1015 fn test_audio_content_roundtrip() {
1016 let content = AudioContent::new("base64audio", "audio/mp3");
1017 let json = serde_json::to_value(&content).unwrap();
1018 let parsed: AudioContent = serde_json::from_value(json).unwrap();
1019 assert_eq!(content, parsed);
1020 }
1021
1022 #[test]
1023 fn test_audio_content_omits_optional_fields() {
1024 let content = AudioContent::new("data", "audio/mp3");
1025 let json = serde_json::to_value(&content).unwrap();
1026 assert!(!json.as_object().unwrap().contains_key("annotations"));
1027 assert!(!json.as_object().unwrap().contains_key("meta"));
1028 }
1029
1030 #[test]
1031 fn resource_link_icons_roundtrip() {
1032 let icon = Icon::new("https://example.com/icon.png")
1033 .mime_type("image/png")
1034 .sizes(vec!["48x48".to_string(), "any".to_string()])
1035 .theme(IconTheme::Dark);
1036 let link = ResourceLink::new("Example", "file:///example.txt").icons(vec![icon]);
1037
1038 let json = serde_json::to_value(&link).unwrap();
1039 assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
1040 assert_eq!(json["icons"][0]["mimeType"], "image/png");
1041 assert_eq!(json["icons"][0]["sizes"][0], "48x48");
1042 assert_eq!(json["icons"][0]["theme"], "dark");
1043
1044 let parsed: ResourceLink = serde_json::from_value(json).unwrap();
1045 assert_eq!(link, parsed);
1046 }
1047
1048 #[cfg(feature = "schemars")]
1049 #[test]
1050 fn annotations_priority_schema_matches_mcp_bounds() {
1051 let schema = schemars::schema_for!(Annotations);
1052 let json = serde_json::to_value(schema).unwrap();
1053
1054 assert_eq!(json["properties"]["priority"]["minimum"], 0);
1055 assert_eq!(json["properties"]["priority"]["maximum"], 1);
1056 assert_eq!(json["properties"]["lastModified"]["format"], "date-time");
1057 }
1058
1059 #[cfg(feature = "schemars")]
1060 #[test]
1061 fn content_schema_uses_standard_string_annotations() {
1062 let image = serde_json::to_value(schemars::schema_for!(ImageContent)).unwrap();
1063 assert_eq!(image["properties"]["data"]["contentEncoding"], "base64");
1064 assert!(image["properties"]["data"].get("format").is_none());
1065 assert_eq!(image["properties"]["uri"]["format"], "uri");
1066
1067 let audio = serde_json::to_value(schemars::schema_for!(AudioContent)).unwrap();
1068 assert_eq!(audio["properties"]["data"]["contentEncoding"], "base64");
1069 assert!(audio["properties"]["data"].get("format").is_none());
1070
1071 let text_resource =
1072 serde_json::to_value(schemars::schema_for!(TextResourceContents)).unwrap();
1073 assert_eq!(text_resource["properties"]["uri"]["format"], "uri");
1074
1075 let blob_resource =
1076 serde_json::to_value(schemars::schema_for!(BlobResourceContents)).unwrap();
1077 assert_eq!(
1078 blob_resource["properties"]["blob"]["contentEncoding"],
1079 "base64"
1080 );
1081 assert!(blob_resource["properties"]["blob"].get("format").is_none());
1082 assert_eq!(blob_resource["properties"]["uri"]["format"], "uri");
1083
1084 let resource_link = serde_json::to_value(schemars::schema_for!(ResourceLink)).unwrap();
1085 assert_eq!(resource_link["properties"]["uri"]["format"], "uri");
1086
1087 let icon = serde_json::to_value(schemars::schema_for!(Icon)).unwrap();
1088 assert_eq!(icon["properties"]["src"]["format"], "uri");
1089 assert_eq!(icon["properties"]["sizes"]["items"]["type"], "string");
1090 assert!(
1091 icon["properties"]["sizes"]["items"]
1092 .get("pattern")
1093 .is_none()
1094 );
1095 }
1096}