Skip to main content

bcp_types/
summary.rs

1use bcp_wire::varint::{decode_varint, encode_varint};
2
3use crate::error::TypeError;
4
5/// Summary sub-block — a compact UTF-8 description prefixed to the body
6/// when the block's `HAS_SUMMARY` flag is set.
7///
8/// The summary is designed for token-budget-aware rendering: when a block
9/// is too large to include in full, the renderer can substitute the summary
10/// to preserve context without blowing the token budget.
11///
12/// Wire layout (within the block body, before any TLV fields):
13///
14/// ```text
15/// ┌─────────────────────────────────────────────────────┐
16/// │ summary_len  (varint)         — byte length of text │
17/// │ summary_text [summary_len]    — UTF-8 bytes         │
18/// │ ... remaining TLV fields ...                        │
19/// └─────────────────────────────────────────────────────┘
20/// ```
21///
22/// The summary is always the first thing in the body when present. The
23/// decoder checks `BlockFlags::has_summary()` before attempting to read it.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct Summary {
26    pub text: String,
27}
28
29/// Maximum varint size in bytes, used for stack-allocated scratch buffers.
30const MAX_VARINT_LEN: usize = 10;
31
32impl Summary {
33    /// Encode this summary into the front of a body buffer.
34    ///
35    /// Writes `text.len()` as a varint followed by the raw UTF-8 bytes.
36    /// Call this before appending the block's TLV fields.
37    pub fn encode(&self, buf: &mut Vec<u8>) {
38        let mut scratch = [0u8; MAX_VARINT_LEN];
39        let n = encode_varint(self.text.len() as u64, &mut scratch);
40        buf.extend_from_slice(&scratch[..n]);
41        buf.extend_from_slice(self.text.as_bytes());
42    }
43
44    /// Decode a summary from the front of a body buffer.
45    ///
46    /// Returns `(summary, bytes_consumed)`. The caller should slice the
47    /// body past `bytes_consumed` before decoding TLV fields.
48    pub fn decode(buf: &[u8]) -> Result<(Self, usize), TypeError> {
49        let (len, n) = decode_varint(buf)?;
50        let len = len as usize;
51        let text_bytes = buf
52            .get(n..n + len)
53            .ok_or(bcp_wire::WireError::UnexpectedEof { offset: n })?;
54        let text = String::from_utf8_lossy(text_bytes).into_owned();
55        Ok((Self { text }, n + len))
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn roundtrip_summary() {
65        let summary = Summary {
66            text: "This block contains the main entry point.".to_string(),
67        };
68        let mut buf = Vec::new();
69        summary.encode(&mut buf);
70
71        let (decoded, consumed) = Summary::decode(&buf).unwrap();
72        assert_eq!(decoded, summary);
73        assert_eq!(consumed, buf.len());
74    }
75
76    #[test]
77    fn roundtrip_empty_summary() {
78        let summary = Summary {
79            text: String::new(),
80        };
81        let mut buf = Vec::new();
82        summary.encode(&mut buf);
83
84        let (decoded, consumed) = Summary::decode(&buf).unwrap();
85        assert_eq!(decoded, summary);
86        assert_eq!(consumed, buf.len());
87    }
88
89    #[test]
90    fn summary_followed_by_other_data() {
91        let summary = Summary {
92            text: "short".to_string(),
93        };
94        let mut buf = Vec::new();
95        summary.encode(&mut buf);
96        buf.extend_from_slice(b"remaining TLV data");
97
98        let (decoded, consumed) = Summary::decode(&buf).unwrap();
99        assert_eq!(decoded.text, "short");
100        assert_eq!(&buf[consumed..], b"remaining TLV data");
101    }
102}