Skip to main content

bcp_types/
document.rs

1use crate::enums::FormatHint;
2use crate::error::TypeError;
3use crate::fields::{
4    decode_bytes_value, decode_field_header, decode_varint_value, encode_bytes_field,
5    encode_varint_field, skip_field,
6};
7
8/// DOCUMENT block — represents prose or documentation content.
9///
10/// Used for READMEs, design docs, comments, or any non-code textual
11/// content that provides context to the LLM. The `format_hint` tells
12/// the renderer how to interpret the body (markdown, plain text, HTML).
13///
14/// Field layout within body:
15///
16/// ```text
17/// ┌──────────┬───────────┬─────────────┬──────────────────────────┐
18/// │ Field ID │ Wire Type │ Name        │ Description              │
19/// ├──────────┼───────────┼─────────────┼──────────────────────────┤
20/// │ 1        │ Bytes     │ title       │ Document title           │
21/// │ 2        │ Bytes     │ content     │ Document body            │
22/// │ 3        │ Varint    │ format_hint │ FormatHint enum byte     │
23/// └──────────┴───────────┴─────────────┴──────────────────────────┘
24/// ```
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct DocumentBlock {
27    pub title: String,
28    pub content: Vec<u8>,
29    pub format_hint: FormatHint,
30}
31
32impl DocumentBlock {
33    /// Serialize this block's fields into a TLV-encoded body.
34    pub fn encode_body(&self) -> Vec<u8> {
35        let mut buf = Vec::new();
36        encode_bytes_field(&mut buf, 1, self.title.as_bytes());
37        encode_bytes_field(&mut buf, 2, &self.content);
38        encode_varint_field(&mut buf, 3, u64::from(self.format_hint.to_wire_byte()));
39        buf
40    }
41
42    /// Deserialize a DOCUMENT block from a TLV-encoded body.
43    pub fn decode_body(mut buf: &[u8]) -> Result<Self, TypeError> {
44        let mut title: Option<String> = None;
45        let mut content: Option<Vec<u8>> = None;
46        let mut format_hint: Option<FormatHint> = None;
47
48        while !buf.is_empty() {
49            let (header, n) = decode_field_header(buf)?;
50            buf = &buf[n..];
51
52            match header.field_id {
53                1 => {
54                    let (data, n) = decode_bytes_value(buf)?;
55                    buf = &buf[n..];
56                    title = Some(String::from_utf8_lossy(data).into_owned());
57                }
58                2 => {
59                    let (data, n) = decode_bytes_value(buf)?;
60                    buf = &buf[n..];
61                    content = Some(data.to_vec());
62                }
63                3 => {
64                    let (v, n) = decode_varint_value(buf)?;
65                    buf = &buf[n..];
66                    format_hint = Some(FormatHint::from_wire_byte(v as u8)?);
67                }
68                _ => {
69                    let n = skip_field(buf, header.wire_type)?;
70                    buf = &buf[n..];
71                }
72            }
73        }
74
75        Ok(Self {
76            title: title.ok_or(TypeError::MissingRequiredField { field: "title" })?,
77            content: content.ok_or(TypeError::MissingRequiredField { field: "content" })?,
78            format_hint: format_hint.ok_or(TypeError::MissingRequiredField {
79                field: "format_hint",
80            })?,
81        })
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn roundtrip_markdown_doc() {
91        let block = DocumentBlock {
92            title: "README".to_string(),
93            content: b"# Hello\n\nThis is a test.".to_vec(),
94            format_hint: FormatHint::Markdown,
95        };
96        let body = block.encode_body();
97        let decoded = DocumentBlock::decode_body(&body).unwrap();
98        assert_eq!(decoded, block);
99    }
100
101    #[test]
102    fn roundtrip_plain_text() {
103        let block = DocumentBlock {
104            title: "notes.txt".to_string(),
105            content: b"Just plain text.".to_vec(),
106            format_hint: FormatHint::Plain,
107        };
108        let body = block.encode_body();
109        let decoded = DocumentBlock::decode_body(&body).unwrap();
110        assert_eq!(decoded, block);
111    }
112}