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;
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, Eq, 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, Eq, 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, Eq, 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, Eq, 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, Eq, 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, Eq, 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, Eq, 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, Eq, 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, Eq, 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, Eq, 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<_>>>")]
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<_>>>")]
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, Deserialize, Serialize, Default)]
801#[serde(rename_all = "camelCase")]
802#[non_exhaustive]
803pub struct Annotations {
804 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
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 PartialEq for Annotations {
839 fn eq(&self, other: &Self) -> bool {
840 let Self {
841 audience,
842 last_modified,
843 priority,
844 meta,
845 } = self;
846 *audience == other.audience
847 && *last_modified == other.last_modified
848 && priority_eq(*priority, other.priority)
849 && *meta == other.meta
850 }
851}
852
853impl Eq for Annotations {}
854
855fn priority_eq(a: Option<f64>, b: Option<f64>) -> bool {
856 match (a, b) {
857 (Some(a), Some(b)) => a.total_cmp(&b).is_eq(),
858 (None, None) => true,
859 _ => false,
860 }
861}
862
863impl Annotations {
864 #[must_use]
866 pub fn new() -> Self {
867 Self::default()
868 }
869
870 #[must_use]
872 pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
873 self.audience = audience.into_option();
874 self
875 }
876
877 #[must_use]
879 pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
880 self.last_modified = last_modified.into_option();
881 self
882 }
883
884 #[must_use]
886 pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
887 self.priority = priority.into_option();
888 self
889 }
890
891 #[must_use]
897 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
898 self.meta = meta.into_option();
899 self
900 }
901}
902
903#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
905#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
906#[serde(rename_all = "camelCase")]
907#[non_exhaustive]
908pub enum Role {
909 Assistant,
911 User,
913 #[serde(untagged)]
919 Other(String),
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925
926 #[test]
927 fn test_text_content_roundtrip() {
928 let content = TextContent::new("hello world");
929 let json = serde_json::to_value(&content).unwrap();
930 let parsed: TextContent = serde_json::from_value(json).unwrap();
931 assert_eq!(content, parsed);
932 }
933
934 #[test]
935 fn test_annotations_equality_is_reflexive() {
936 let nan = Annotations::new().priority(f64::NAN);
937 assert_eq!(nan, nan.clone());
938 assert_eq!(
939 Annotations::new().priority(0.5),
940 Annotations::new().priority(0.5)
941 );
942 assert_ne!(
943 Annotations::new().priority(0.0),
944 Annotations::new().priority(-0.0)
945 );
946 assert_ne!(
947 serde_json::to_string(&Annotations::new().priority(0.0)).unwrap(),
948 serde_json::to_string(&Annotations::new().priority(-0.0)).unwrap()
949 );
950 assert_ne!(
951 Annotations::new().priority(0.5),
952 Annotations::new().priority(1.0)
953 );
954 assert_ne!(nan, Annotations::new().priority(0.5));
955 assert_ne!(Annotations::new().priority(0.5), Annotations::new());
956 assert_ne!(
957 Annotations::new().priority(0.5).audience(vec![Role::User]),
958 Annotations::new().priority(0.5)
959 );
960 }
961
962 #[test]
963 fn test_text_content_omits_optional_fields() {
964 let content = TextContent::new("hello");
965 let json = serde_json::to_value(&content).unwrap();
966 assert!(!json.as_object().unwrap().contains_key("annotations"));
967 assert!(!json.as_object().unwrap().contains_key("meta"));
968 }
969
970 #[test]
971 fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
972 let missing: TextContent = serde_json::from_value(serde_json::json!({
973 "text": "hello"
974 }))
975 .unwrap();
976 assert_eq!(missing.meta, None);
977
978 let malformed: TextContent = serde_json::from_value(serde_json::json!({
979 "text": "hello",
980 "_meta": false
981 }))
982 .unwrap();
983 assert_eq!(malformed.meta, None);
984 }
985
986 #[test]
987 fn test_text_content_from_string() {
988 let block: ContentBlock = "hello".into();
989 match block {
990 ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
991 _ => panic!("Expected Text variant"),
992 }
993 }
994
995 #[test]
996 fn role_preserves_unknown_variant() {
997 let role: Role = serde_json::from_str("\"critic\"").unwrap();
998 assert_eq!(role, Role::Other("critic".to_string()));
999 assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
1000 }
1001
1002 #[test]
1003 fn icon_theme_preserves_unknown_variant() {
1004 let theme: IconTheme = serde_json::from_str("\"contrast\"").unwrap();
1005 assert_eq!(theme, IconTheme::Other("contrast".to_string()));
1006 assert_eq!(serde_json::to_value(&theme).unwrap(), "contrast");
1007 }
1008
1009 #[test]
1010 fn content_block_preserves_unknown_variant() {
1011 let block: ContentBlock = serde_json::from_value(serde_json::json!({
1012 "type": "_widget",
1013 "title": "Status",
1014 "state": {"ok": true}
1015 }))
1016 .unwrap();
1017
1018 let ContentBlock::Other(unknown) = block else {
1019 panic!("expected unknown content block");
1020 };
1021
1022 assert_eq!(unknown.type_, "_widget");
1023 assert_eq!(
1024 unknown.fields.get("title"),
1025 Some(&serde_json::json!("Status"))
1026 );
1027 assert_eq!(
1028 serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
1029 serde_json::json!({
1030 "type": "_widget",
1031 "title": "Status",
1032 "state": {"ok": true}
1033 })
1034 );
1035 }
1036
1037 #[test]
1038 fn content_block_does_not_hide_malformed_known_variant() {
1039 assert!(
1040 serde_json::from_value::<ContentBlock>(serde_json::json!({
1041 "type": "text"
1042 }))
1043 .is_err()
1044 );
1045 }
1046
1047 #[test]
1048 fn test_image_content_roundtrip() {
1049 let content = ImageContent::new("base64data", "image/png");
1050 let json = serde_json::to_value(&content).unwrap();
1051 let parsed: ImageContent = serde_json::from_value(json).unwrap();
1052 assert_eq!(content, parsed);
1053 }
1054
1055 #[test]
1056 fn test_image_content_omits_optional_fields() {
1057 let content = ImageContent::new("data", "image/png");
1058 let json = serde_json::to_value(&content).unwrap();
1059 assert!(!json.as_object().unwrap().contains_key("uri"));
1060 assert!(!json.as_object().unwrap().contains_key("annotations"));
1061 assert!(!json.as_object().unwrap().contains_key("meta"));
1062 }
1063
1064 #[test]
1065 fn test_image_content_with_uri() {
1066 let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
1067 let json = serde_json::to_value(&content).unwrap();
1068 assert_eq!(json["uri"], "https://example.com/image.png");
1069 }
1070
1071 #[test]
1072 fn test_audio_content_roundtrip() {
1073 let content = AudioContent::new("base64audio", "audio/mp3");
1074 let json = serde_json::to_value(&content).unwrap();
1075 let parsed: AudioContent = serde_json::from_value(json).unwrap();
1076 assert_eq!(content, parsed);
1077 }
1078
1079 #[test]
1080 fn test_audio_content_omits_optional_fields() {
1081 let content = AudioContent::new("data", "audio/mp3");
1082 let json = serde_json::to_value(&content).unwrap();
1083 assert!(!json.as_object().unwrap().contains_key("annotations"));
1084 assert!(!json.as_object().unwrap().contains_key("meta"));
1085 }
1086
1087 #[test]
1088 fn resource_link_icons_roundtrip() {
1089 let icon = Icon::new("https://example.com/icon.png")
1090 .mime_type("image/png")
1091 .sizes(vec!["48x48".to_string(), "any".to_string()])
1092 .theme(IconTheme::Dark);
1093 let link = ResourceLink::new("Example", "file:///example.txt").icons(vec![icon]);
1094
1095 let json = serde_json::to_value(&link).unwrap();
1096 assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
1097 assert_eq!(json["icons"][0]["mimeType"], "image/png");
1098 assert_eq!(json["icons"][0]["sizes"][0], "48x48");
1099 assert_eq!(json["icons"][0]["theme"], "dark");
1100
1101 let parsed: ResourceLink = serde_json::from_value(json).unwrap();
1102 assert_eq!(link, parsed);
1103 }
1104
1105 #[cfg(feature = "schemars")]
1106 #[test]
1107 fn annotations_priority_schema_matches_mcp_bounds() {
1108 let schema = schemars::schema_for!(Annotations);
1109 let json = serde_json::to_value(schema).unwrap();
1110
1111 assert_eq!(json["properties"]["priority"]["minimum"], 0);
1112 assert_eq!(json["properties"]["priority"]["maximum"], 1);
1113 assert_eq!(json["properties"]["lastModified"]["format"], "date-time");
1114 }
1115
1116 #[cfg(feature = "schemars")]
1117 #[test]
1118 fn content_schema_uses_standard_string_annotations() {
1119 let image = serde_json::to_value(schemars::schema_for!(ImageContent)).unwrap();
1120 assert_eq!(image["properties"]["data"]["contentEncoding"], "base64");
1121 assert!(image["properties"]["data"].get("format").is_none());
1122 assert_eq!(image["properties"]["uri"]["format"], "uri");
1123
1124 let audio = serde_json::to_value(schemars::schema_for!(AudioContent)).unwrap();
1125 assert_eq!(audio["properties"]["data"]["contentEncoding"], "base64");
1126 assert!(audio["properties"]["data"].get("format").is_none());
1127
1128 let text_resource =
1129 serde_json::to_value(schemars::schema_for!(TextResourceContents)).unwrap();
1130 assert_eq!(text_resource["properties"]["uri"]["format"], "uri");
1131
1132 let blob_resource =
1133 serde_json::to_value(schemars::schema_for!(BlobResourceContents)).unwrap();
1134 assert_eq!(
1135 blob_resource["properties"]["blob"]["contentEncoding"],
1136 "base64"
1137 );
1138 assert!(blob_resource["properties"]["blob"].get("format").is_none());
1139 assert_eq!(blob_resource["properties"]["uri"]["format"], "uri");
1140
1141 let resource_link = serde_json::to_value(schemars::schema_for!(ResourceLink)).unwrap();
1142 assert_eq!(resource_link["properties"]["uri"]["format"], "uri");
1143
1144 let icon = serde_json::to_value(schemars::schema_for!(Icon)).unwrap();
1145 assert_eq!(icon["properties"]["src"]["format"], "uri");
1146 assert_eq!(icon["properties"]["sizes"]["items"]["type"], "string");
1147 assert!(
1148 icon["properties"]["sizes"]["items"]
1149 .get("pattern")
1150 .is_none()
1151 );
1152 }
1153}