Skip to main content

spaceterm_proto/
bundle.rs

1//! MIME bundles: the alternative representations carried by a block.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8// ============================================================================
9// Constants
10// ============================================================================
11
12/// The mandatory fallback representation every well-formed bundle should carry.
13pub const TEXT_PLAIN: &str = "text/plain";
14
15// ============================================================================
16// Data Structures
17// ============================================================================
18
19/// A set of alternative representations of one block, keyed by MIME type. The
20/// terminal renders the richest type it supports and falls back toward
21/// [`TEXT_PLAIN`]. Values are JSON: a string for text and image payloads, an
22/// object for structured specs (e.g. Vega-Lite).
23#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
24pub struct MimeBundle {
25    #[serde(default, skip_serializing_if = "BlockMeta::is_empty")]
26    pub meta: BlockMeta,
27    pub mime: BTreeMap<String, Value>,
28}
29
30/// Optional presentation hints attached to a bundle.
31#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
32pub struct BlockMeta {
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub height_hint: Option<u16>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub title: Option<String>,
37}
38
39// ============================================================================
40// MimeBundle
41// ============================================================================
42
43impl MimeBundle {
44    /// An empty bundle with no representations and no metadata.
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Add or replace the representation for one MIME type.
50    pub fn insert(&mut self, mime: impl Into<String>, value: Value) {
51        self.mime.insert(mime.into(), value);
52    }
53
54    /// The representation for a MIME type, if present.
55    pub fn get(&self, mime: &str) -> Option<&Value> {
56        self.mime.get(mime)
57    }
58
59    /// The mandatory `text/plain` fallback, if the tool supplied one.
60    pub fn text_plain(&self) -> Option<&str> {
61        self.get(TEXT_PLAIN).and_then(Value::as_str)
62    }
63}
64
65// ============================================================================
66// BlockMeta
67// ============================================================================
68
69impl BlockMeta {
70    /// Whether every hint is absent, so the field can be omitted on the wire.
71    pub fn is_empty(&self) -> bool {
72        self.height_hint.is_none() && self.title.is_none()
73    }
74}
75
76// ============================================================================
77// Tests
78// ============================================================================
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_text_plain_returns_string_fallback() {
86        let mut bundle = MimeBundle::new();
87        bundle.insert(TEXT_PLAIN, Value::from("score: 0.92"));
88        bundle.insert("image/svg+xml", Value::from("<svg/>"));
89        assert_eq!(bundle.text_plain(), Some("score: 0.92"));
90    }
91
92    #[test]
93    fn test_text_plain_absent_when_only_rich_types_present() {
94        let mut bundle = MimeBundle::new();
95        bundle.insert("image/svg+xml", Value::from("<svg/>"));
96        assert_eq!(bundle.text_plain(), None);
97    }
98
99    #[test]
100    fn test_non_string_text_plain_is_not_returned_as_str() {
101        let mut bundle = MimeBundle::new();
102        bundle.insert(TEXT_PLAIN, Value::from(42));
103        assert_eq!(bundle.text_plain(), None);
104    }
105}