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),
95 Image(ImageContent),
99 Audio(AudioContent),
103 ResourceLink(ResourceLink),
107 Resource(EmbeddedResource),
113 #[serde(untagged)]
125 Other(OtherContentBlock),
126}
127
128#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
130#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
131#[cfg_attr(feature = "schemars", schemars(inline))]
132#[cfg_attr(feature = "schemars", schemars(transform = other_content_block_schema))]
133#[serde(rename_all = "camelCase")]
134#[non_exhaustive]
135pub struct OtherContentBlock {
136 #[serde(rename = "type")]
142 pub type_: String,
143 #[serde(flatten)]
145 pub fields: BTreeMap<String, serde_json::Value>,
146}
147
148impl OtherContentBlock {
149 #[must_use]
151 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
152 fields.remove("type");
153 Self {
154 type_: type_.into(),
155 fields,
156 }
157 }
158}
159
160impl<'de> Deserialize<'de> for OtherContentBlock {
161 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162 where
163 D: serde::Deserializer<'de>,
164 {
165 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
166 let type_ = fields
167 .remove("type")
168 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
169 let serde_json::Value::String(type_) = type_ else {
170 return Err(serde::de::Error::custom("`type` must be a string"));
171 };
172
173 if is_known_content_block_type(&type_) {
174 return Err(serde::de::Error::custom(format!(
175 "known content block `{type_}` did not match its schema"
176 )));
177 }
178
179 Ok(Self { type_, fields })
180 }
181}
182
183fn is_known_content_block_type(type_: &str) -> bool {
184 matches!(
185 type_,
186 "text" | "image" | "audio" | "resource_link" | "resource"
187 )
188}
189
190#[cfg(feature = "schemars")]
191fn other_content_block_schema(schema: &mut Schema) {
192 super::schema_util::reject_known_string_discriminators(
193 schema,
194 "type",
195 &["text", "image", "audio", "resource_link", "resource"],
196 );
197}
198
199#[serde_as]
201#[skip_serializing_none]
202#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
203#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
204#[non_exhaustive]
205pub struct TextContent {
206 pub text: String,
208 #[serde_as(deserialize_as = "DefaultOnError")]
212 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
213 #[serde(default)]
214 pub annotations: Option<Annotations>,
215 #[serde_as(deserialize_as = "DefaultOnError")]
221 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
222 #[serde(default)]
223 #[serde(rename = "_meta")]
224 pub meta: Option<Meta>,
225}
226
227impl TextContent {
228 #[must_use]
230 pub fn new(text: impl Into<String>) -> Self {
231 Self {
232 annotations: None,
233 text: text.into(),
234 meta: None,
235 }
236 }
237
238 #[must_use]
240 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
241 self.annotations = annotations.into_option();
242 self
243 }
244
245 #[must_use]
251 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
252 self.meta = meta.into_option();
253 self
254 }
255}
256
257impl<T: Into<String>> From<T> for ContentBlock {
258 fn from(value: T) -> Self {
259 Self::Text(TextContent::new(value))
260 }
261}
262
263#[serde_as]
265#[skip_serializing_none]
266#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
267#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
268#[serde(rename_all = "camelCase")]
269#[non_exhaustive]
270pub struct ImageContent {
271 #[cfg_attr(feature = "schemars", schemars(extend("contentEncoding" = "base64")))]
273 pub data: String,
274 pub mime_type: MediaType,
276 #[serde_as(deserialize_as = "DefaultOnError")]
280 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
281 #[cfg_attr(feature = "schemars", schemars(url))]
282 #[serde(default)]
283 pub uri: Option<String>,
284 #[serde_as(deserialize_as = "DefaultOnError")]
288 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
289 #[serde(default)]
290 pub annotations: Option<Annotations>,
291 #[serde_as(deserialize_as = "DefaultOnError")]
297 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
298 #[serde(default)]
299 #[serde(rename = "_meta")]
300 pub meta: Option<Meta>,
301}
302
303impl ImageContent {
304 #[must_use]
306 pub fn new(data: impl Into<String>, mime_type: impl Into<MediaType>) -> Self {
307 Self {
308 annotations: None,
309 data: data.into(),
310 mime_type: mime_type.into(),
311 uri: None,
312 meta: None,
313 }
314 }
315
316 #[must_use]
318 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
319 self.annotations = annotations.into_option();
320 self
321 }
322
323 #[must_use]
325 pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
326 self.uri = uri.into_option();
327 self
328 }
329
330 #[must_use]
336 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
337 self.meta = meta.into_option();
338 self
339 }
340}
341
342#[serde_as]
344#[skip_serializing_none]
345#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
346#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
347#[serde(rename_all = "camelCase")]
348#[non_exhaustive]
349pub struct AudioContent {
350 #[cfg_attr(feature = "schemars", schemars(extend("contentEncoding" = "base64")))]
352 pub data: String,
353 pub mime_type: MediaType,
355 #[serde_as(deserialize_as = "DefaultOnError")]
359 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
360 #[serde(default)]
361 pub annotations: Option<Annotations>,
362 #[serde_as(deserialize_as = "DefaultOnError")]
368 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
369 #[serde(default)]
370 #[serde(rename = "_meta")]
371 pub meta: Option<Meta>,
372}
373
374impl AudioContent {
375 #[must_use]
377 pub fn new(data: impl Into<String>, mime_type: impl Into<MediaType>) -> Self {
378 Self {
379 annotations: None,
380 data: data.into(),
381 mime_type: mime_type.into(),
382 meta: None,
383 }
384 }
385
386 #[must_use]
388 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
389 self.annotations = annotations.into_option();
390 self
391 }
392
393 #[must_use]
399 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
400 self.meta = meta.into_option();
401 self
402 }
403}
404
405#[serde_as]
407#[skip_serializing_none]
408#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
409#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
410#[non_exhaustive]
411pub struct EmbeddedResource {
412 pub resource: EmbeddedResourceResource,
414 #[serde_as(deserialize_as = "DefaultOnError")]
418 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
419 #[serde(default)]
420 pub annotations: Option<Annotations>,
421 #[serde_as(deserialize_as = "DefaultOnError")]
427 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
428 #[serde(default)]
429 #[serde(rename = "_meta")]
430 pub meta: Option<Meta>,
431}
432
433impl EmbeddedResource {
434 #[must_use]
436 pub fn new(resource: EmbeddedResourceResource) -> Self {
437 Self {
438 annotations: None,
439 resource,
440 meta: None,
441 }
442 }
443
444 #[must_use]
446 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
447 self.annotations = annotations.into_option();
448 self
449 }
450
451 #[must_use]
457 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
458 self.meta = meta.into_option();
459 self
460 }
461}
462
463#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
465#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
466#[serde(untagged)]
467#[non_exhaustive]
468pub enum EmbeddedResourceResource {
469 TextResourceContents(TextResourceContents),
471 BlobResourceContents(BlobResourceContents),
473}
474
475#[serde_as]
477#[skip_serializing_none]
478#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
479#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
480#[serde(rename_all = "camelCase")]
481#[non_exhaustive]
482pub struct TextResourceContents {
483 pub text: String,
485 #[cfg_attr(feature = "schemars", schemars(url))]
487 pub uri: String,
488 #[serde_as(deserialize_as = "DefaultOnError")]
492 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
493 #[serde(default)]
494 pub mime_type: Option<MediaType>,
495 #[serde_as(deserialize_as = "DefaultOnError")]
501 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
502 #[serde(default)]
503 #[serde(rename = "_meta")]
504 pub meta: Option<Meta>,
505}
506
507impl TextResourceContents {
508 #[must_use]
510 pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
511 Self {
512 mime_type: None,
513 text: text.into(),
514 uri: uri.into(),
515 meta: None,
516 }
517 }
518
519 #[must_use]
521 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
522 self.mime_type = mime_type.into_option();
523 self
524 }
525
526 #[must_use]
532 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
533 self.meta = meta.into_option();
534 self
535 }
536}
537
538#[serde_as]
540#[skip_serializing_none]
541#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
542#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
543#[serde(rename_all = "camelCase")]
544#[non_exhaustive]
545pub struct BlobResourceContents {
546 #[cfg_attr(feature = "schemars", schemars(extend("contentEncoding" = "base64")))]
548 pub blob: String,
549 #[cfg_attr(feature = "schemars", schemars(url))]
551 pub uri: String,
552 #[serde_as(deserialize_as = "DefaultOnError")]
556 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
557 #[serde(default)]
558 pub mime_type: Option<MediaType>,
559 #[serde_as(deserialize_as = "DefaultOnError")]
565 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
566 #[serde(default)]
567 #[serde(rename = "_meta")]
568 pub meta: Option<Meta>,
569}
570
571impl BlobResourceContents {
572 #[must_use]
574 pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
575 Self {
576 blob: blob.into(),
577 mime_type: None,
578 uri: uri.into(),
579 meta: None,
580 }
581 }
582
583 #[must_use]
585 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
586 self.mime_type = mime_type.into_option();
587 self
588 }
589
590 #[must_use]
596 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
597 self.meta = meta.into_option();
598 self
599 }
600}
601
602#[serde_as]
604#[skip_serializing_none]
605#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
606#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
607#[serde(rename_all = "camelCase")]
608#[non_exhaustive]
609pub struct ResourceLink {
610 pub name: String,
612 #[cfg_attr(feature = "schemars", schemars(url))]
614 pub uri: String,
615 #[serde_as(deserialize_as = "DefaultOnError")]
619 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
620 #[serde(default)]
621 pub title: Option<String>,
622 #[serde_as(deserialize_as = "DefaultOnError")]
626 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
627 #[serde(default)]
628 pub description: Option<String>,
629 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
633 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
634 #[serde(default)]
635 pub icons: Option<Vec<Icon>>,
636 #[serde_as(deserialize_as = "DefaultOnError")]
640 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
641 #[serde(default)]
642 pub mime_type: Option<MediaType>,
643 #[serde_as(deserialize_as = "DefaultOnError")]
647 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
648 #[serde(default)]
649 pub size: Option<u64>,
650 #[serde_as(deserialize_as = "DefaultOnError")]
654 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
655 #[serde(default)]
656 pub annotations: Option<Annotations>,
657 #[serde_as(deserialize_as = "DefaultOnError")]
663 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
664 #[serde(default)]
665 #[serde(rename = "_meta")]
666 pub meta: Option<Meta>,
667}
668
669impl ResourceLink {
670 #[must_use]
672 pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
673 Self {
674 annotations: None,
675 description: None,
676 icons: None,
677 mime_type: None,
678 name: name.into(),
679 size: None,
680 title: None,
681 uri: uri.into(),
682 meta: None,
683 }
684 }
685
686 #[must_use]
688 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
689 self.annotations = annotations.into_option();
690 self
691 }
692
693 #[must_use]
695 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
696 self.description = description.into_option();
697 self
698 }
699
700 #[must_use]
702 pub fn icons(mut self, icons: impl IntoOption<Vec<Icon>>) -> Self {
703 self.icons = icons.into_option();
704 self
705 }
706
707 #[must_use]
709 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
710 self.mime_type = mime_type.into_option();
711 self
712 }
713
714 #[must_use]
716 pub fn size(mut self, size: impl IntoOption<u64>) -> Self {
717 self.size = size.into_option();
718 self
719 }
720
721 #[must_use]
723 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
724 self.title = title.into_option();
725 self
726 }
727
728 #[must_use]
734 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
735 self.meta = meta.into_option();
736 self
737 }
738}
739
740#[serde_as]
742#[skip_serializing_none]
743#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
744#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
745#[serde(rename_all = "camelCase")]
746#[non_exhaustive]
747pub struct Icon {
748 #[cfg_attr(feature = "schemars", schemars(url))]
752 pub src: String,
753 #[serde_as(deserialize_as = "DefaultOnError")]
757 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
758 #[serde(default)]
759 pub mime_type: Option<MediaType>,
760 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
768 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
769 #[serde(default)]
770 pub sizes: Option<Vec<String>>,
771 #[serde_as(deserialize_as = "DefaultOnError")]
775 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
776 #[serde(default)]
777 pub theme: Option<IconTheme>,
778 #[serde_as(deserialize_as = "DefaultOnError")]
784 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
785 #[serde(default)]
786 #[serde(rename = "_meta")]
787 pub meta: Option<Meta>,
788}
789
790impl Icon {
791 #[must_use]
793 pub fn new(src: impl Into<String>) -> Self {
794 Self {
795 src: src.into(),
796 mime_type: None,
797 sizes: None,
798 theme: None,
799 meta: None,
800 }
801 }
802
803 #[must_use]
809 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
810 self.meta = meta.into_option();
811 self
812 }
813
814 #[must_use]
816 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
817 self.mime_type = mime_type.into_option();
818 self
819 }
820
821 #[must_use]
823 pub fn sizes(mut self, sizes: impl IntoOption<Vec<String>>) -> Self {
824 self.sizes = sizes.into_option();
825 self
826 }
827
828 #[must_use]
830 pub fn theme(mut self, theme: impl IntoOption<IconTheme>) -> Self {
831 self.theme = theme.into_option();
832 self
833 }
834}
835
836#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
838#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
839#[serde(rename_all = "camelCase")]
840#[non_exhaustive]
841pub enum IconTheme {
842 Light,
844 Dark,
846 #[serde(untagged)]
852 Other(String),
853}
854
855#[serde_as]
857#[skip_serializing_none]
858#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
859#[derive(Debug, Clone, Deserialize, Serialize, Default)]
860#[serde(rename_all = "camelCase")]
861#[non_exhaustive]
862pub struct Annotations {
863 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
867 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
868 #[serde(default)]
869 pub audience: Option<Vec<Role>>,
870 #[serde_as(deserialize_as = "DefaultOnError")]
876 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
877 #[serde(default)]
878 pub last_modified: Option<String>,
879 #[serde_as(deserialize_as = "DefaultOnError")]
884 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
885 #[cfg_attr(feature = "schemars", schemars(range(min = 0, max = 1)))]
886 #[serde(default)]
887 pub priority: Option<f64>,
888 #[serde_as(deserialize_as = "DefaultOnError")]
894 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
895 #[serde(default)]
896 #[serde(rename = "_meta")]
897 pub meta: Option<Meta>,
898}
899
900impl PartialEq for Annotations {
905 fn eq(&self, other: &Self) -> bool {
906 let Self {
907 audience,
908 last_modified,
909 priority,
910 meta,
911 } = self;
912 *audience == other.audience
913 && *last_modified == other.last_modified
914 && priority_eq(*priority, other.priority)
915 && *meta == other.meta
916 }
917}
918
919impl Eq for Annotations {}
920
921fn priority_eq(a: Option<f64>, b: Option<f64>) -> bool {
922 match (a, b) {
923 (Some(a), Some(b)) => a.total_cmp(&b).is_eq(),
924 (None, None) => true,
925 _ => false,
926 }
927}
928
929impl Annotations {
930 #[must_use]
932 pub fn new() -> Self {
933 Self::default()
934 }
935
936 #[must_use]
938 pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
939 self.audience = audience.into_option();
940 self
941 }
942
943 #[must_use]
945 pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
946 self.last_modified = last_modified.into_option();
947 self
948 }
949
950 #[must_use]
952 pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
953 self.priority = priority.into_option();
954 self
955 }
956
957 #[must_use]
963 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
964 self.meta = meta.into_option();
965 self
966 }
967}
968
969#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
971#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
972#[serde(rename_all = "camelCase")]
973#[non_exhaustive]
974pub enum Role {
975 Assistant,
977 User,
979 #[serde(untagged)]
985 Other(String),
986}
987
988#[cfg(test)]
989mod tests {
990 use super::*;
991
992 #[test]
993 fn test_text_content_roundtrip() {
994 let content = TextContent::new("hello world");
995 let json = serde_json::to_value(&content).unwrap();
996 let parsed: TextContent = serde_json::from_value(json).unwrap();
997 assert_eq!(content, parsed);
998 }
999
1000 #[test]
1001 fn test_annotations_equality_is_reflexive() {
1002 let nan = Annotations::new().priority(f64::NAN);
1003 assert_eq!(nan, nan.clone());
1004 assert_eq!(
1005 Annotations::new().priority(0.5),
1006 Annotations::new().priority(0.5)
1007 );
1008 assert_ne!(
1009 Annotations::new().priority(0.0),
1010 Annotations::new().priority(-0.0)
1011 );
1012 assert_ne!(
1013 serde_json::to_string(&Annotations::new().priority(0.0)).unwrap(),
1014 serde_json::to_string(&Annotations::new().priority(-0.0)).unwrap()
1015 );
1016 assert_ne!(
1017 Annotations::new().priority(0.5),
1018 Annotations::new().priority(1.0)
1019 );
1020 assert_ne!(nan, Annotations::new().priority(0.5));
1021 assert_ne!(Annotations::new().priority(0.5), Annotations::new());
1022 assert_ne!(
1023 Annotations::new().priority(0.5).audience(vec![Role::User]),
1024 Annotations::new().priority(0.5)
1025 );
1026 }
1027
1028 #[test]
1029 fn test_text_content_omits_optional_fields() {
1030 let content = TextContent::new("hello");
1031 let json = serde_json::to_value(&content).unwrap();
1032 assert!(!json.as_object().unwrap().contains_key("annotations"));
1033 assert!(!json.as_object().unwrap().contains_key("meta"));
1034 }
1035
1036 #[test]
1037 fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
1038 let missing: TextContent = serde_json::from_value(serde_json::json!({
1039 "text": "hello"
1040 }))
1041 .unwrap();
1042 assert_eq!(missing.meta, None);
1043
1044 let malformed: TextContent = serde_json::from_value(serde_json::json!({
1045 "text": "hello",
1046 "_meta": false
1047 }))
1048 .unwrap();
1049 assert_eq!(malformed.meta, None);
1050 }
1051
1052 #[test]
1053 fn test_text_content_from_string() {
1054 let block: ContentBlock = "hello".into();
1055 match block {
1056 ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
1057 _ => panic!("Expected Text variant"),
1058 }
1059 }
1060
1061 #[test]
1062 fn role_preserves_unknown_variant() {
1063 let role: Role = serde_json::from_str("\"critic\"").unwrap();
1064 assert_eq!(role, Role::Other("critic".to_string()));
1065 assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
1066 }
1067
1068 #[test]
1069 fn icon_theme_preserves_unknown_variant() {
1070 let theme: IconTheme = serde_json::from_str("\"contrast\"").unwrap();
1071 assert_eq!(theme, IconTheme::Other("contrast".to_string()));
1072 assert_eq!(serde_json::to_value(&theme).unwrap(), "contrast");
1073 }
1074
1075 #[test]
1076 fn content_block_preserves_unknown_variant() {
1077 let block: ContentBlock = serde_json::from_value(serde_json::json!({
1078 "type": "_widget",
1079 "title": "Status",
1080 "state": {"ok": true}
1081 }))
1082 .unwrap();
1083
1084 let ContentBlock::Other(unknown) = block else {
1085 panic!("expected unknown content block");
1086 };
1087
1088 assert_eq!(unknown.type_, "_widget");
1089 assert_eq!(
1090 unknown.fields.get("title"),
1091 Some(&serde_json::json!("Status"))
1092 );
1093 assert_eq!(
1094 serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
1095 serde_json::json!({
1096 "type": "_widget",
1097 "title": "Status",
1098 "state": {"ok": true}
1099 })
1100 );
1101 }
1102
1103 #[test]
1104 fn content_block_does_not_hide_malformed_known_variant() {
1105 assert!(
1106 serde_json::from_value::<ContentBlock>(serde_json::json!({
1107 "type": "text"
1108 }))
1109 .is_err()
1110 );
1111 }
1112
1113 #[test]
1114 fn test_image_content_roundtrip() {
1115 let content = ImageContent::new("base64data", "image/png");
1116 let json = serde_json::to_value(&content).unwrap();
1117 let parsed: ImageContent = serde_json::from_value(json).unwrap();
1118 assert_eq!(content, parsed);
1119 }
1120
1121 #[test]
1122 fn test_image_content_omits_optional_fields() {
1123 let content = ImageContent::new("data", "image/png");
1124 let json = serde_json::to_value(&content).unwrap();
1125 assert!(!json.as_object().unwrap().contains_key("uri"));
1126 assert!(!json.as_object().unwrap().contains_key("annotations"));
1127 assert!(!json.as_object().unwrap().contains_key("meta"));
1128 }
1129
1130 #[test]
1131 fn test_image_content_with_uri() {
1132 let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
1133 let json = serde_json::to_value(&content).unwrap();
1134 assert_eq!(json["uri"], "https://example.com/image.png");
1135 }
1136
1137 #[test]
1138 fn test_audio_content_roundtrip() {
1139 let content = AudioContent::new("base64audio", "audio/mp3");
1140 let json = serde_json::to_value(&content).unwrap();
1141 let parsed: AudioContent = serde_json::from_value(json).unwrap();
1142 assert_eq!(content, parsed);
1143 }
1144
1145 #[test]
1146 fn test_audio_content_omits_optional_fields() {
1147 let content = AudioContent::new("data", "audio/mp3");
1148 let json = serde_json::to_value(&content).unwrap();
1149 assert!(!json.as_object().unwrap().contains_key("annotations"));
1150 assert!(!json.as_object().unwrap().contains_key("meta"));
1151 }
1152
1153 #[test]
1154 fn resource_link_icons_roundtrip() {
1155 let icon = Icon::new("https://example.com/icon.png")
1156 .mime_type("image/png")
1157 .sizes(vec!["48x48".to_string(), "any".to_string()])
1158 .theme(IconTheme::Dark);
1159 let link = ResourceLink::new("Example", "file:///example.txt").icons(vec![icon]);
1160
1161 let json = serde_json::to_value(&link).unwrap();
1162 assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
1163 assert_eq!(json["icons"][0]["mimeType"], "image/png");
1164 assert_eq!(json["icons"][0]["sizes"][0], "48x48");
1165 assert_eq!(json["icons"][0]["theme"], "dark");
1166
1167 let parsed: ResourceLink = serde_json::from_value(json).unwrap();
1168 assert_eq!(link, parsed);
1169 }
1170
1171 #[cfg(feature = "schemars")]
1172 #[test]
1173 fn annotations_priority_schema_matches_mcp_bounds() {
1174 let schema = schemars::schema_for!(Annotations);
1175 let json = serde_json::to_value(schema).unwrap();
1176
1177 assert_eq!(json["properties"]["priority"]["minimum"], 0);
1178 assert_eq!(json["properties"]["priority"]["maximum"], 1);
1179 assert!(json["properties"]["lastModified"].get("format").is_none());
1181 }
1182
1183 #[cfg(feature = "schemars")]
1184 #[test]
1185 fn content_schema_uses_standard_string_annotations() {
1186 let image = serde_json::to_value(schemars::schema_for!(ImageContent)).unwrap();
1187 assert_eq!(image["properties"]["data"]["contentEncoding"], "base64");
1188 assert!(image["properties"]["data"].get("format").is_none());
1189 assert_eq!(image["properties"]["uri"]["format"], "uri");
1190
1191 let audio = serde_json::to_value(schemars::schema_for!(AudioContent)).unwrap();
1192 assert_eq!(audio["properties"]["data"]["contentEncoding"], "base64");
1193 assert!(audio["properties"]["data"].get("format").is_none());
1194
1195 let text_resource =
1196 serde_json::to_value(schemars::schema_for!(TextResourceContents)).unwrap();
1197 assert_eq!(text_resource["properties"]["uri"]["format"], "uri");
1198
1199 let blob_resource =
1200 serde_json::to_value(schemars::schema_for!(BlobResourceContents)).unwrap();
1201 assert_eq!(
1202 blob_resource["properties"]["blob"]["contentEncoding"],
1203 "base64"
1204 );
1205 assert!(blob_resource["properties"]["blob"].get("format").is_none());
1206 assert_eq!(blob_resource["properties"]["uri"]["format"], "uri");
1207
1208 let resource_link = serde_json::to_value(schemars::schema_for!(ResourceLink)).unwrap();
1209 assert_eq!(resource_link["properties"]["uri"]["format"], "uri");
1210
1211 let icon = serde_json::to_value(schemars::schema_for!(Icon)).unwrap();
1212 assert_eq!(icon["properties"]["src"]["format"], "uri");
1213 assert_eq!(icon["properties"]["sizes"]["items"]["type"], "string");
1214 assert!(
1215 icon["properties"]["sizes"]["items"]
1216 .get("pattern")
1217 .is_none()
1218 );
1219 }
1220}