1use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
13
14use derive_more::{Display, From};
15use schemars::{JsonSchema, Schema};
16use serde::{Deserialize, Serialize};
17use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
18
19use super::Meta;
20use crate::{IntoOption, SkipListener};
21
22#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
24#[serde(transparent)]
25#[from(Arc<str>, String, &str, &mut str, Box<str>, Cow<'_, str>)]
26#[non_exhaustive]
27pub struct MediaType(pub Arc<str>);
28
29impl MediaType {
30 #[must_use]
32 pub fn new(media_type: impl Into<Self>) -> Self {
33 media_type.into()
34 }
35}
36
37impl AsRef<str> for MediaType {
38 fn as_ref(&self) -> &str {
39 &self.0
40 }
41}
42
43impl From<&String> for MediaType {
44 fn from(media_type: &String) -> Self {
45 Self(media_type.as_str().into())
46 }
47}
48
49macro_rules! impl_media_type_option_conversion {
50 ($source:ty) => {
51 impl IntoOption<MediaType> for $source {
52 fn into_option(self) -> Option<MediaType> {
53 Some(self.into())
54 }
55 }
56 };
57}
58
59impl_media_type_option_conversion!(Arc<str>);
60impl_media_type_option_conversion!(String);
61impl_media_type_option_conversion!(&str);
62impl_media_type_option_conversion!(&mut str);
63impl_media_type_option_conversion!(&String);
64impl_media_type_option_conversion!(Box<str>);
65impl_media_type_option_conversion!(Cow<'_, str>);
66
67#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
83#[serde(tag = "type", rename_all = "snake_case")]
84#[non_exhaustive]
85pub enum ContentBlock {
86 Text(TextContent),
91 Image(ImageContent),
95 Audio(AudioContent),
99 ResourceLink(ResourceLink),
103 Resource(EmbeddedResource),
109 #[serde(untagged)]
119 Other(OtherContentBlock),
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
124#[schemars(inline)]
125#[schemars(transform = other_content_block_schema)]
126#[serde(rename_all = "camelCase")]
127#[non_exhaustive]
128pub struct OtherContentBlock {
129 #[serde(rename = "type")]
135 pub type_: String,
136 #[serde(flatten)]
138 pub fields: BTreeMap<String, serde_json::Value>,
139}
140
141impl OtherContentBlock {
142 #[must_use]
144 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
145 fields.remove("type");
146 Self {
147 type_: type_.into(),
148 fields,
149 }
150 }
151}
152
153impl<'de> Deserialize<'de> for OtherContentBlock {
154 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
155 where
156 D: serde::Deserializer<'de>,
157 {
158 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
159 let type_ = fields
160 .remove("type")
161 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
162 let serde_json::Value::String(type_) = type_ else {
163 return Err(serde::de::Error::custom("`type` must be a string"));
164 };
165
166 if is_known_content_block_type(&type_) {
167 return Err(serde::de::Error::custom(format!(
168 "known content block `{type_}` did not match its schema"
169 )));
170 }
171
172 Ok(Self { type_, fields })
173 }
174}
175
176fn is_known_content_block_type(type_: &str) -> bool {
177 matches!(
178 type_,
179 "text" | "image" | "audio" | "resource_link" | "resource"
180 )
181}
182
183fn other_content_block_schema(schema: &mut Schema) {
184 super::schema_util::reject_known_string_discriminators(
185 schema,
186 "type",
187 &["text", "image", "audio", "resource_link", "resource"],
188 );
189}
190
191#[serde_as]
193#[skip_serializing_none]
194#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
195#[non_exhaustive]
196pub struct TextContent {
197 pub text: String,
199 #[serde_as(deserialize_as = "DefaultOnError")]
201 #[schemars(extend("x-deserialize-default-on-error" = true))]
202 #[serde(default)]
203 pub annotations: Option<Annotations>,
204 #[serde_as(deserialize_as = "DefaultOnError")]
210 #[schemars(extend("x-deserialize-default-on-error" = true))]
211 #[serde(default)]
212 #[serde(rename = "_meta")]
213 pub meta: Option<Meta>,
214}
215
216impl TextContent {
217 #[must_use]
219 pub fn new(text: impl Into<String>) -> Self {
220 Self {
221 annotations: None,
222 text: text.into(),
223 meta: None,
224 }
225 }
226
227 #[must_use]
229 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
230 self.annotations = annotations.into_option();
231 self
232 }
233
234 #[must_use]
240 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
241 self.meta = meta.into_option();
242 self
243 }
244}
245
246impl<T: Into<String>> From<T> for ContentBlock {
247 fn from(value: T) -> Self {
248 Self::Text(TextContent::new(value))
249 }
250}
251
252#[serde_as]
254#[skip_serializing_none]
255#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
256#[serde(rename_all = "camelCase")]
257#[non_exhaustive]
258pub struct ImageContent {
259 #[schemars(extend("contentEncoding" = "base64"))]
261 pub data: String,
262 pub mime_type: MediaType,
264 #[serde_as(deserialize_as = "DefaultOnError")]
266 #[schemars(extend("x-deserialize-default-on-error" = true))]
267 #[schemars(url)]
268 #[serde(default)]
269 pub uri: Option<String>,
270 #[serde_as(deserialize_as = "DefaultOnError")]
272 #[schemars(extend("x-deserialize-default-on-error" = true))]
273 #[serde(default)]
274 pub annotations: Option<Annotations>,
275 #[serde_as(deserialize_as = "DefaultOnError")]
281 #[schemars(extend("x-deserialize-default-on-error" = true))]
282 #[serde(default)]
283 #[serde(rename = "_meta")]
284 pub meta: Option<Meta>,
285}
286
287impl ImageContent {
288 #[must_use]
290 pub fn new(data: impl Into<String>, mime_type: impl Into<MediaType>) -> Self {
291 Self {
292 annotations: None,
293 data: data.into(),
294 mime_type: mime_type.into(),
295 uri: None,
296 meta: None,
297 }
298 }
299
300 #[must_use]
302 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
303 self.annotations = annotations.into_option();
304 self
305 }
306
307 #[must_use]
309 pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
310 self.uri = uri.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#[serde(rename_all = "camelCase")]
331#[non_exhaustive]
332pub struct AudioContent {
333 #[schemars(extend("contentEncoding" = "base64"))]
335 pub data: String,
336 pub mime_type: MediaType,
338 #[serde_as(deserialize_as = "DefaultOnError")]
340 #[schemars(extend("x-deserialize-default-on-error" = true))]
341 #[serde(default)]
342 pub annotations: Option<Annotations>,
343 #[serde_as(deserialize_as = "DefaultOnError")]
349 #[schemars(extend("x-deserialize-default-on-error" = true))]
350 #[serde(default)]
351 #[serde(rename = "_meta")]
352 pub meta: Option<Meta>,
353}
354
355impl AudioContent {
356 #[must_use]
358 pub fn new(data: impl Into<String>, mime_type: impl Into<MediaType>) -> Self {
359 Self {
360 annotations: None,
361 data: data.into(),
362 mime_type: mime_type.into(),
363 meta: None,
364 }
365 }
366
367 #[must_use]
369 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
370 self.annotations = annotations.into_option();
371 self
372 }
373
374 #[must_use]
380 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
381 self.meta = meta.into_option();
382 self
383 }
384}
385
386#[serde_as]
388#[skip_serializing_none]
389#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
390#[non_exhaustive]
391pub struct EmbeddedResource {
392 pub resource: EmbeddedResourceResource,
394 #[serde_as(deserialize_as = "DefaultOnError")]
396 #[schemars(extend("x-deserialize-default-on-error" = true))]
397 #[serde(default)]
398 pub annotations: Option<Annotations>,
399 #[serde_as(deserialize_as = "DefaultOnError")]
405 #[schemars(extend("x-deserialize-default-on-error" = true))]
406 #[serde(default)]
407 #[serde(rename = "_meta")]
408 pub meta: Option<Meta>,
409}
410
411impl EmbeddedResource {
412 #[must_use]
414 pub fn new(resource: EmbeddedResourceResource) -> Self {
415 Self {
416 annotations: None,
417 resource,
418 meta: None,
419 }
420 }
421
422 #[must_use]
424 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
425 self.annotations = annotations.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#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
443#[serde(untagged)]
444#[non_exhaustive]
445pub enum EmbeddedResourceResource {
446 TextResourceContents(TextResourceContents),
448 BlobResourceContents(BlobResourceContents),
450}
451
452#[serde_as]
454#[skip_serializing_none]
455#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
456#[serde(rename_all = "camelCase")]
457#[non_exhaustive]
458pub struct TextResourceContents {
459 pub text: String,
461 #[schemars(url)]
463 pub uri: String,
464 #[serde_as(deserialize_as = "DefaultOnError")]
466 #[schemars(extend("x-deserialize-default-on-error" = true))]
467 #[serde(default)]
468 pub mime_type: Option<MediaType>,
469 #[serde_as(deserialize_as = "DefaultOnError")]
475 #[schemars(extend("x-deserialize-default-on-error" = true))]
476 #[serde(default)]
477 #[serde(rename = "_meta")]
478 pub meta: Option<Meta>,
479}
480
481impl TextResourceContents {
482 #[must_use]
484 pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
485 Self {
486 mime_type: None,
487 text: text.into(),
488 uri: uri.into(),
489 meta: None,
490 }
491 }
492
493 #[must_use]
495 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
496 self.mime_type = mime_type.into_option();
497 self
498 }
499
500 #[must_use]
506 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
507 self.meta = meta.into_option();
508 self
509 }
510}
511
512#[serde_as]
514#[skip_serializing_none]
515#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
516#[serde(rename_all = "camelCase")]
517#[non_exhaustive]
518pub struct BlobResourceContents {
519 #[schemars(extend("contentEncoding" = "base64"))]
521 pub blob: String,
522 #[schemars(url)]
524 pub uri: String,
525 #[serde_as(deserialize_as = "DefaultOnError")]
527 #[schemars(extend("x-deserialize-default-on-error" = true))]
528 #[serde(default)]
529 pub mime_type: Option<MediaType>,
530 #[serde_as(deserialize_as = "DefaultOnError")]
536 #[schemars(extend("x-deserialize-default-on-error" = true))]
537 #[serde(default)]
538 #[serde(rename = "_meta")]
539 pub meta: Option<Meta>,
540}
541
542impl BlobResourceContents {
543 #[must_use]
545 pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
546 Self {
547 blob: blob.into(),
548 mime_type: None,
549 uri: uri.into(),
550 meta: None,
551 }
552 }
553
554 #[must_use]
556 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
557 self.mime_type = mime_type.into_option();
558 self
559 }
560
561 #[must_use]
567 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
568 self.meta = meta.into_option();
569 self
570 }
571}
572
573#[serde_as]
575#[skip_serializing_none]
576#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
577#[serde(rename_all = "camelCase")]
578#[non_exhaustive]
579pub struct ResourceLink {
580 pub name: String,
582 #[schemars(url)]
584 pub uri: String,
585 #[serde_as(deserialize_as = "DefaultOnError")]
587 #[schemars(extend("x-deserialize-default-on-error" = true))]
588 #[serde(default)]
589 pub title: Option<String>,
590 #[serde_as(deserialize_as = "DefaultOnError")]
592 #[schemars(extend("x-deserialize-default-on-error" = true))]
593 #[serde(default)]
594 pub description: Option<String>,
595 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
597 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
598 #[serde(default)]
599 pub icons: Option<Vec<Icon>>,
600 #[serde_as(deserialize_as = "DefaultOnError")]
602 #[schemars(extend("x-deserialize-default-on-error" = true))]
603 #[serde(default)]
604 pub mime_type: Option<MediaType>,
605 #[serde_as(deserialize_as = "DefaultOnError")]
607 #[schemars(extend("x-deserialize-default-on-error" = true))]
608 #[serde(default)]
609 pub size: Option<i64>,
610 #[serde_as(deserialize_as = "DefaultOnError")]
612 #[schemars(extend("x-deserialize-default-on-error" = true))]
613 #[serde(default)]
614 pub annotations: Option<Annotations>,
615 #[serde_as(deserialize_as = "DefaultOnError")]
621 #[schemars(extend("x-deserialize-default-on-error" = true))]
622 #[serde(default)]
623 #[serde(rename = "_meta")]
624 pub meta: Option<Meta>,
625}
626
627impl ResourceLink {
628 #[must_use]
630 pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
631 Self {
632 annotations: None,
633 description: None,
634 icons: None,
635 mime_type: None,
636 name: name.into(),
637 size: None,
638 title: None,
639 uri: uri.into(),
640 meta: None,
641 }
642 }
643
644 #[must_use]
646 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
647 self.annotations = annotations.into_option();
648 self
649 }
650
651 #[must_use]
653 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
654 self.description = description.into_option();
655 self
656 }
657
658 #[must_use]
660 pub fn icons(mut self, icons: impl IntoOption<Vec<Icon>>) -> Self {
661 self.icons = icons.into_option();
662 self
663 }
664
665 #[must_use]
667 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
668 self.mime_type = mime_type.into_option();
669 self
670 }
671
672 #[must_use]
674 pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
675 self.size = size.into_option();
676 self
677 }
678
679 #[must_use]
681 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
682 self.title = title.into_option();
683 self
684 }
685
686 #[must_use]
692 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
693 self.meta = meta.into_option();
694 self
695 }
696}
697
698#[serde_as]
700#[skip_serializing_none]
701#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
702#[serde(rename_all = "camelCase")]
703#[non_exhaustive]
704pub struct Icon {
705 #[schemars(url)]
707 pub src: String,
708 #[serde_as(deserialize_as = "DefaultOnError")]
710 #[schemars(extend("x-deserialize-default-on-error" = true))]
711 #[serde(default)]
712 pub mime_type: Option<MediaType>,
713 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
719 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
720 #[serde(default)]
721 pub sizes: Option<Vec<String>>,
722 #[serde_as(deserialize_as = "DefaultOnError")]
724 #[schemars(extend("x-deserialize-default-on-error" = true))]
725 #[serde(default)]
726 pub theme: Option<IconTheme>,
727}
728
729impl Icon {
730 #[must_use]
732 pub fn new(src: impl Into<String>) -> Self {
733 Self {
734 src: src.into(),
735 mime_type: None,
736 sizes: None,
737 theme: None,
738 }
739 }
740
741 #[must_use]
743 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
744 self.mime_type = mime_type.into_option();
745 self
746 }
747
748 #[must_use]
750 pub fn sizes(mut self, sizes: impl IntoOption<Vec<String>>) -> Self {
751 self.sizes = sizes.into_option();
752 self
753 }
754
755 #[must_use]
757 pub fn theme(mut self, theme: impl IntoOption<IconTheme>) -> Self {
758 self.theme = theme.into_option();
759 self
760 }
761}
762
763#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
765#[serde(rename_all = "camelCase")]
766#[non_exhaustive]
767pub enum IconTheme {
768 Light,
770 Dark,
772 #[serde(untagged)]
778 Other(String),
779}
780
781#[serde_as]
783#[skip_serializing_none]
784#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
785#[serde(rename_all = "camelCase")]
786#[non_exhaustive]
787pub struct Annotations {
788 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
790 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
791 #[serde(default)]
792 pub audience: Option<Vec<Role>>,
793 #[serde_as(deserialize_as = "DefaultOnError")]
797 #[schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time"))]
798 #[serde(default)]
799 pub last_modified: Option<String>,
800 #[serde_as(deserialize_as = "DefaultOnError")]
802 #[schemars(extend("x-deserialize-default-on-error" = true))]
803 #[schemars(range(min = 0, max = 1))]
804 #[serde(default)]
805 pub priority: Option<f64>,
806 #[serde_as(deserialize_as = "DefaultOnError")]
812 #[schemars(extend("x-deserialize-default-on-error" = true))]
813 #[serde(default)]
814 #[serde(rename = "_meta")]
815 pub meta: Option<Meta>,
816}
817
818impl Annotations {
819 #[must_use]
821 pub fn new() -> Self {
822 Self::default()
823 }
824
825 #[must_use]
827 pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
828 self.audience = audience.into_option();
829 self
830 }
831
832 #[must_use]
834 pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
835 self.last_modified = last_modified.into_option();
836 self
837 }
838
839 #[must_use]
841 pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
842 self.priority = priority.into_option();
843 self
844 }
845
846 #[must_use]
852 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
853 self.meta = meta.into_option();
854 self
855 }
856}
857
858#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
860#[serde(rename_all = "camelCase")]
861#[non_exhaustive]
862pub enum Role {
863 Assistant,
865 User,
867 #[serde(untagged)]
873 Other(String),
874}
875
876#[cfg(test)]
877mod tests {
878 use super::*;
879
880 #[test]
881 fn test_text_content_roundtrip() {
882 let content = TextContent::new("hello world");
883 let json = serde_json::to_value(&content).unwrap();
884 let parsed: TextContent = serde_json::from_value(json).unwrap();
885 assert_eq!(content, parsed);
886 }
887
888 #[test]
889 fn test_text_content_omits_optional_fields() {
890 let content = TextContent::new("hello");
891 let json = serde_json::to_value(&content).unwrap();
892 assert!(!json.as_object().unwrap().contains_key("annotations"));
893 assert!(!json.as_object().unwrap().contains_key("meta"));
894 }
895
896 #[test]
897 fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
898 let missing: TextContent = serde_json::from_value(serde_json::json!({
899 "text": "hello"
900 }))
901 .unwrap();
902 assert_eq!(missing.meta, None);
903
904 let malformed: TextContent = serde_json::from_value(serde_json::json!({
905 "text": "hello",
906 "_meta": false
907 }))
908 .unwrap();
909 assert_eq!(malformed.meta, None);
910 }
911
912 #[test]
913 fn test_text_content_from_string() {
914 let block: ContentBlock = "hello".into();
915 match block {
916 ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
917 _ => panic!("Expected Text variant"),
918 }
919 }
920
921 #[test]
922 fn role_preserves_unknown_variant() {
923 let role: Role = serde_json::from_str("\"critic\"").unwrap();
924 assert_eq!(role, Role::Other("critic".to_string()));
925 assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
926 }
927
928 #[test]
929 fn icon_theme_preserves_unknown_variant() {
930 let theme: IconTheme = serde_json::from_str("\"contrast\"").unwrap();
931 assert_eq!(theme, IconTheme::Other("contrast".to_string()));
932 assert_eq!(serde_json::to_value(&theme).unwrap(), "contrast");
933 }
934
935 #[test]
936 fn content_block_preserves_unknown_variant() {
937 let block: ContentBlock = serde_json::from_value(serde_json::json!({
938 "type": "_widget",
939 "title": "Status",
940 "state": {"ok": true}
941 }))
942 .unwrap();
943
944 let ContentBlock::Other(unknown) = block else {
945 panic!("expected unknown content block");
946 };
947
948 assert_eq!(unknown.type_, "_widget");
949 assert_eq!(
950 unknown.fields.get("title"),
951 Some(&serde_json::json!("Status"))
952 );
953 assert_eq!(
954 serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
955 serde_json::json!({
956 "type": "_widget",
957 "title": "Status",
958 "state": {"ok": true}
959 })
960 );
961 }
962
963 #[test]
964 fn content_block_does_not_hide_malformed_known_variant() {
965 assert!(
966 serde_json::from_value::<ContentBlock>(serde_json::json!({
967 "type": "text"
968 }))
969 .is_err()
970 );
971 }
972
973 #[test]
974 fn test_image_content_roundtrip() {
975 let content = ImageContent::new("base64data", "image/png");
976 let json = serde_json::to_value(&content).unwrap();
977 let parsed: ImageContent = serde_json::from_value(json).unwrap();
978 assert_eq!(content, parsed);
979 }
980
981 #[test]
982 fn test_image_content_omits_optional_fields() {
983 let content = ImageContent::new("data", "image/png");
984 let json = serde_json::to_value(&content).unwrap();
985 assert!(!json.as_object().unwrap().contains_key("uri"));
986 assert!(!json.as_object().unwrap().contains_key("annotations"));
987 assert!(!json.as_object().unwrap().contains_key("meta"));
988 }
989
990 #[test]
991 fn test_image_content_with_uri() {
992 let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
993 let json = serde_json::to_value(&content).unwrap();
994 assert_eq!(json["uri"], "https://example.com/image.png");
995 }
996
997 #[test]
998 fn test_audio_content_roundtrip() {
999 let content = AudioContent::new("base64audio", "audio/mp3");
1000 let json = serde_json::to_value(&content).unwrap();
1001 let parsed: AudioContent = serde_json::from_value(json).unwrap();
1002 assert_eq!(content, parsed);
1003 }
1004
1005 #[test]
1006 fn test_audio_content_omits_optional_fields() {
1007 let content = AudioContent::new("data", "audio/mp3");
1008 let json = serde_json::to_value(&content).unwrap();
1009 assert!(!json.as_object().unwrap().contains_key("annotations"));
1010 assert!(!json.as_object().unwrap().contains_key("meta"));
1011 }
1012
1013 #[test]
1014 fn resource_link_icons_roundtrip() {
1015 let icon = Icon::new("https://example.com/icon.png")
1016 .mime_type("image/png")
1017 .sizes(vec!["48x48".to_string(), "any".to_string()])
1018 .theme(IconTheme::Dark);
1019 let link = ResourceLink::new("Example", "file:///example.txt").icons(vec![icon]);
1020
1021 let json = serde_json::to_value(&link).unwrap();
1022 assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
1023 assert_eq!(json["icons"][0]["mimeType"], "image/png");
1024 assert_eq!(json["icons"][0]["sizes"][0], "48x48");
1025 assert_eq!(json["icons"][0]["theme"], "dark");
1026
1027 let parsed: ResourceLink = serde_json::from_value(json).unwrap();
1028 assert_eq!(link, parsed);
1029 }
1030
1031 #[test]
1032 fn annotations_priority_schema_matches_mcp_bounds() {
1033 let schema = schemars::schema_for!(Annotations);
1034 let json = serde_json::to_value(schema).unwrap();
1035
1036 assert_eq!(json["properties"]["priority"]["minimum"], 0);
1037 assert_eq!(json["properties"]["priority"]["maximum"], 1);
1038 assert_eq!(json["properties"]["lastModified"]["format"], "date-time");
1039 }
1040
1041 #[test]
1042 fn content_schema_uses_standard_string_annotations() {
1043 let image = serde_json::to_value(schemars::schema_for!(ImageContent)).unwrap();
1044 assert_eq!(image["properties"]["data"]["contentEncoding"], "base64");
1045 assert!(image["properties"]["data"].get("format").is_none());
1046 assert_eq!(image["properties"]["uri"]["format"], "uri");
1047
1048 let audio = serde_json::to_value(schemars::schema_for!(AudioContent)).unwrap();
1049 assert_eq!(audio["properties"]["data"]["contentEncoding"], "base64");
1050 assert!(audio["properties"]["data"].get("format").is_none());
1051
1052 let text_resource =
1053 serde_json::to_value(schemars::schema_for!(TextResourceContents)).unwrap();
1054 assert_eq!(text_resource["properties"]["uri"]["format"], "uri");
1055
1056 let blob_resource =
1057 serde_json::to_value(schemars::schema_for!(BlobResourceContents)).unwrap();
1058 assert_eq!(
1059 blob_resource["properties"]["blob"]["contentEncoding"],
1060 "base64"
1061 );
1062 assert!(blob_resource["properties"]["blob"].get("format").is_none());
1063 assert_eq!(blob_resource["properties"]["uri"]["format"], "uri");
1064
1065 let resource_link = serde_json::to_value(schemars::schema_for!(ResourceLink)).unwrap();
1066 assert_eq!(resource_link["properties"]["uri"]["format"], "uri");
1067
1068 let icon = serde_json::to_value(schemars::schema_for!(Icon)).unwrap();
1069 assert_eq!(icon["properties"]["src"]["format"], "uri");
1070 assert_eq!(icon["properties"]["sizes"]["items"]["type"], "string");
1071 assert!(
1072 icon["properties"]["sizes"]["items"]
1073 .get("pattern")
1074 .is_none()
1075 );
1076 }
1077}