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))]
750 pub src: String,
751 #[serde_as(deserialize_as = "DefaultOnError")]
755 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
756 #[serde(default)]
757 pub mime_type: Option<MediaType>,
758 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
766 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
767 #[serde(default)]
768 pub sizes: Option<Vec<String>>,
769 #[serde_as(deserialize_as = "DefaultOnError")]
773 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
774 #[serde(default)]
775 pub theme: Option<IconTheme>,
776 #[serde_as(deserialize_as = "DefaultOnError")]
782 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
783 #[serde(default)]
784 #[serde(rename = "_meta")]
785 pub meta: Option<Meta>,
786}
787
788impl Icon {
789 #[must_use]
791 pub fn new(src: impl Into<String>) -> Self {
792 Self {
793 src: src.into(),
794 mime_type: None,
795 sizes: None,
796 theme: None,
797 meta: None,
798 }
799 }
800
801 #[must_use]
807 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
808 self.meta = meta.into_option();
809 self
810 }
811
812 #[must_use]
814 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
815 self.mime_type = mime_type.into_option();
816 self
817 }
818
819 #[must_use]
821 pub fn sizes(mut self, sizes: impl IntoOption<Vec<String>>) -> Self {
822 self.sizes = sizes.into_option();
823 self
824 }
825
826 #[must_use]
828 pub fn theme(mut self, theme: impl IntoOption<IconTheme>) -> Self {
829 self.theme = theme.into_option();
830 self
831 }
832}
833
834#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
836#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
837#[serde(rename_all = "camelCase")]
838#[non_exhaustive]
839pub enum IconTheme {
840 Light,
842 Dark,
844 #[serde(untagged)]
850 Other(String),
851}
852
853#[serde_as]
855#[skip_serializing_none]
856#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
857#[derive(Debug, Clone, Deserialize, Serialize, Default)]
858#[serde(rename_all = "camelCase")]
859#[non_exhaustive]
860pub struct Annotations {
861 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
865 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
866 #[serde(default)]
867 pub audience: Option<Vec<Role>>,
868 #[serde_as(deserialize_as = "DefaultOnError")]
874 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
875 #[serde(default)]
876 pub last_modified: Option<String>,
877 #[serde_as(deserialize_as = "DefaultOnError")]
881 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
882 #[cfg_attr(feature = "schemars", schemars(range(min = 0, max = 1)))]
883 #[serde(default)]
884 pub priority: Option<f64>,
885 #[serde_as(deserialize_as = "DefaultOnError")]
891 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
892 #[serde(default)]
893 #[serde(rename = "_meta")]
894 pub meta: Option<Meta>,
895}
896
897impl PartialEq for Annotations {
902 fn eq(&self, other: &Self) -> bool {
903 let Self {
904 audience,
905 last_modified,
906 priority,
907 meta,
908 } = self;
909 *audience == other.audience
910 && *last_modified == other.last_modified
911 && priority_eq(*priority, other.priority)
912 && *meta == other.meta
913 }
914}
915
916impl Eq for Annotations {}
917
918fn priority_eq(a: Option<f64>, b: Option<f64>) -> bool {
919 match (a, b) {
920 (Some(a), Some(b)) => a.total_cmp(&b).is_eq(),
921 (None, None) => true,
922 _ => false,
923 }
924}
925
926impl Annotations {
927 #[must_use]
929 pub fn new() -> Self {
930 Self::default()
931 }
932
933 #[must_use]
935 pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
936 self.audience = audience.into_option();
937 self
938 }
939
940 #[must_use]
942 pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
943 self.last_modified = last_modified.into_option();
944 self
945 }
946
947 #[must_use]
949 pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
950 self.priority = priority.into_option();
951 self
952 }
953
954 #[must_use]
960 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
961 self.meta = meta.into_option();
962 self
963 }
964}
965
966#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
968#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
969#[serde(rename_all = "camelCase")]
970#[non_exhaustive]
971pub enum Role {
972 Assistant,
974 User,
976 #[serde(untagged)]
982 Other(String),
983}
984
985#[cfg(test)]
986mod tests {
987 use super::*;
988
989 #[test]
990 fn test_text_content_roundtrip() {
991 let content = TextContent::new("hello world");
992 let json = serde_json::to_value(&content).unwrap();
993 let parsed: TextContent = serde_json::from_value(json).unwrap();
994 assert_eq!(content, parsed);
995 }
996
997 #[test]
998 fn test_annotations_equality_is_reflexive() {
999 let nan = Annotations::new().priority(f64::NAN);
1000 assert_eq!(nan, nan.clone());
1001 assert_eq!(
1002 Annotations::new().priority(0.5),
1003 Annotations::new().priority(0.5)
1004 );
1005 assert_ne!(
1006 Annotations::new().priority(0.0),
1007 Annotations::new().priority(-0.0)
1008 );
1009 assert_ne!(
1010 serde_json::to_string(&Annotations::new().priority(0.0)).unwrap(),
1011 serde_json::to_string(&Annotations::new().priority(-0.0)).unwrap()
1012 );
1013 assert_ne!(
1014 Annotations::new().priority(0.5),
1015 Annotations::new().priority(1.0)
1016 );
1017 assert_ne!(nan, Annotations::new().priority(0.5));
1018 assert_ne!(Annotations::new().priority(0.5), Annotations::new());
1019 assert_ne!(
1020 Annotations::new().priority(0.5).audience(vec![Role::User]),
1021 Annotations::new().priority(0.5)
1022 );
1023 }
1024
1025 #[test]
1026 fn test_text_content_omits_optional_fields() {
1027 let content = TextContent::new("hello");
1028 let json = serde_json::to_value(&content).unwrap();
1029 assert!(!json.as_object().unwrap().contains_key("annotations"));
1030 assert!(!json.as_object().unwrap().contains_key("meta"));
1031 }
1032
1033 #[test]
1034 fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
1035 let missing: TextContent = serde_json::from_value(serde_json::json!({
1036 "text": "hello"
1037 }))
1038 .unwrap();
1039 assert_eq!(missing.meta, None);
1040
1041 let malformed: TextContent = serde_json::from_value(serde_json::json!({
1042 "text": "hello",
1043 "_meta": false
1044 }))
1045 .unwrap();
1046 assert_eq!(malformed.meta, None);
1047 }
1048
1049 #[test]
1050 fn test_text_content_from_string() {
1051 let block: ContentBlock = "hello".into();
1052 match block {
1053 ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
1054 _ => panic!("Expected Text variant"),
1055 }
1056 }
1057
1058 #[test]
1059 fn role_preserves_unknown_variant() {
1060 let role: Role = serde_json::from_str("\"critic\"").unwrap();
1061 assert_eq!(role, Role::Other("critic".to_string()));
1062 assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
1063 }
1064
1065 #[test]
1066 fn icon_theme_preserves_unknown_variant() {
1067 let theme: IconTheme = serde_json::from_str("\"contrast\"").unwrap();
1068 assert_eq!(theme, IconTheme::Other("contrast".to_string()));
1069 assert_eq!(serde_json::to_value(&theme).unwrap(), "contrast");
1070 }
1071
1072 #[test]
1073 fn content_block_preserves_unknown_variant() {
1074 let block: ContentBlock = serde_json::from_value(serde_json::json!({
1075 "type": "_widget",
1076 "title": "Status",
1077 "state": {"ok": true}
1078 }))
1079 .unwrap();
1080
1081 let ContentBlock::Other(unknown) = block else {
1082 panic!("expected unknown content block");
1083 };
1084
1085 assert_eq!(unknown.type_, "_widget");
1086 assert_eq!(
1087 unknown.fields.get("title"),
1088 Some(&serde_json::json!("Status"))
1089 );
1090 assert_eq!(
1091 serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
1092 serde_json::json!({
1093 "type": "_widget",
1094 "title": "Status",
1095 "state": {"ok": true}
1096 })
1097 );
1098 }
1099
1100 #[test]
1101 fn content_block_does_not_hide_malformed_known_variant() {
1102 assert!(
1103 serde_json::from_value::<ContentBlock>(serde_json::json!({
1104 "type": "text"
1105 }))
1106 .is_err()
1107 );
1108 }
1109
1110 #[test]
1111 fn test_image_content_roundtrip() {
1112 let content = ImageContent::new("base64data", "image/png");
1113 let json = serde_json::to_value(&content).unwrap();
1114 let parsed: ImageContent = serde_json::from_value(json).unwrap();
1115 assert_eq!(content, parsed);
1116 }
1117
1118 #[test]
1119 fn test_image_content_omits_optional_fields() {
1120 let content = ImageContent::new("data", "image/png");
1121 let json = serde_json::to_value(&content).unwrap();
1122 assert!(!json.as_object().unwrap().contains_key("uri"));
1123 assert!(!json.as_object().unwrap().contains_key("annotations"));
1124 assert!(!json.as_object().unwrap().contains_key("meta"));
1125 }
1126
1127 #[test]
1128 fn test_image_content_with_uri() {
1129 let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
1130 let json = serde_json::to_value(&content).unwrap();
1131 assert_eq!(json["uri"], "https://example.com/image.png");
1132 }
1133
1134 #[test]
1135 fn test_audio_content_roundtrip() {
1136 let content = AudioContent::new("base64audio", "audio/mp3");
1137 let json = serde_json::to_value(&content).unwrap();
1138 let parsed: AudioContent = serde_json::from_value(json).unwrap();
1139 assert_eq!(content, parsed);
1140 }
1141
1142 #[test]
1143 fn test_audio_content_omits_optional_fields() {
1144 let content = AudioContent::new("data", "audio/mp3");
1145 let json = serde_json::to_value(&content).unwrap();
1146 assert!(!json.as_object().unwrap().contains_key("annotations"));
1147 assert!(!json.as_object().unwrap().contains_key("meta"));
1148 }
1149
1150 #[test]
1151 fn resource_link_icons_roundtrip() {
1152 let icon = Icon::new("https://example.com/icon.png")
1153 .mime_type("image/png")
1154 .sizes(vec!["48x48".to_string(), "any".to_string()])
1155 .theme(IconTheme::Dark);
1156 let link = ResourceLink::new("Example", "file:///example.txt").icons(vec![icon]);
1157
1158 let json = serde_json::to_value(&link).unwrap();
1159 assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
1160 assert_eq!(json["icons"][0]["mimeType"], "image/png");
1161 assert_eq!(json["icons"][0]["sizes"][0], "48x48");
1162 assert_eq!(json["icons"][0]["theme"], "dark");
1163
1164 let parsed: ResourceLink = serde_json::from_value(json).unwrap();
1165 assert_eq!(link, parsed);
1166 }
1167
1168 #[cfg(feature = "schemars")]
1169 #[test]
1170 fn annotations_priority_schema_matches_mcp_bounds() {
1171 let schema = schemars::schema_for!(Annotations);
1172 let json = serde_json::to_value(schema).unwrap();
1173
1174 assert_eq!(json["properties"]["priority"]["minimum"], 0);
1175 assert_eq!(json["properties"]["priority"]["maximum"], 1);
1176 assert_eq!(json["properties"]["lastModified"]["format"], "date-time");
1177 }
1178
1179 #[cfg(feature = "schemars")]
1180 #[test]
1181 fn content_schema_uses_standard_string_annotations() {
1182 let image = serde_json::to_value(schemars::schema_for!(ImageContent)).unwrap();
1183 assert_eq!(image["properties"]["data"]["contentEncoding"], "base64");
1184 assert!(image["properties"]["data"].get("format").is_none());
1185 assert_eq!(image["properties"]["uri"]["format"], "uri");
1186
1187 let audio = serde_json::to_value(schemars::schema_for!(AudioContent)).unwrap();
1188 assert_eq!(audio["properties"]["data"]["contentEncoding"], "base64");
1189 assert!(audio["properties"]["data"].get("format").is_none());
1190
1191 let text_resource =
1192 serde_json::to_value(schemars::schema_for!(TextResourceContents)).unwrap();
1193 assert_eq!(text_resource["properties"]["uri"]["format"], "uri");
1194
1195 let blob_resource =
1196 serde_json::to_value(schemars::schema_for!(BlobResourceContents)).unwrap();
1197 assert_eq!(
1198 blob_resource["properties"]["blob"]["contentEncoding"],
1199 "base64"
1200 );
1201 assert!(blob_resource["properties"]["blob"].get("format").is_none());
1202 assert_eq!(blob_resource["properties"]["uri"]["format"], "uri");
1203
1204 let resource_link = serde_json::to_value(schemars::schema_for!(ResourceLink)).unwrap();
1205 assert_eq!(resource_link["properties"]["uri"]["format"], "uri");
1206
1207 let icon = serde_json::to_value(schemars::schema_for!(Icon)).unwrap();
1208 assert_eq!(icon["properties"]["src"]["format"], "uri");
1209 assert_eq!(icon["properties"]["sizes"]["items"]["type"], "string");
1210 assert!(
1211 icon["properties"]["sizes"]["items"]
1212 .get("pattern")
1213 .is_none()
1214 );
1215 }
1216}