Skip to main content

bcp_types/
block.rs

1use bcp_wire::block_frame::BlockFlags;
2
3use crate::annotation::AnnotationBlock;
4use crate::block_type::BlockType;
5use crate::code::CodeBlock;
6use crate::conversation::ConversationBlock;
7use crate::diff::DiffBlock;
8use crate::document::DocumentBlock;
9use crate::embedding_ref::EmbeddingRefBlock;
10use crate::error::TypeError;
11use crate::extension::ExtensionBlock;
12use crate::file_tree::FileTreeBlock;
13use crate::image::ImageBlock;
14use crate::structured_data::StructuredDataBlock;
15use crate::summary::Summary;
16use crate::tool_result::ToolResultBlock;
17
18/// A fully parsed BCP block — the union of all block types with
19/// optional metadata.
20///
21/// This is the primary type that higher-level crates (`bcp-encoder`,
22/// `bcp-decoder`, `bcp-driver`) work with. It combines the block's
23/// type tag, per-block flags, optional summary, and typed content
24/// into a single value.
25///
26/// The `Block` struct sits between the wire layer (`BlockFrame` from
27/// `bcp-wire`) and the application layer. The encoder converts a
28/// `Block` into a `BlockFrame` by calling `content.encode_body()`
29/// and prepending the summary if present. The decoder does the reverse:
30/// it reads a `BlockFrame`, strips the summary if `flags.has_summary()`,
31/// then dispatches to the appropriate `decode_body` method based on
32/// `block_type`.
33#[derive(Clone, Debug, PartialEq)]
34pub struct Block {
35    pub block_type: BlockType,
36    pub flags: BlockFlags,
37    pub summary: Option<Summary>,
38    pub content: BlockContent,
39}
40
41/// The typed content within a block.
42///
43/// Each variant wraps the corresponding block struct from this crate.
44/// The `Unknown` variant preserves unrecognized block types as raw bytes,
45/// enabling forward compatibility: a decoder built against an older spec
46/// can still read (and re-encode) blocks from a newer encoder.
47///
48/// ```text
49/// ┌─────────────────┬────────────────────────┐
50/// │ Variant         │ Block Type Wire ID     │
51/// ├─────────────────┼────────────────────────┤
52/// │ Code            │ 0x01                   │
53/// │ Conversation    │ 0x02                   │
54/// │ FileTree        │ 0x03                   │
55/// │ ToolResult      │ 0x04                   │
56/// │ Document        │ 0x05                   │
57/// │ StructuredData  │ 0x06                   │
58/// │ Diff            │ 0x07                   │
59/// │ Annotation      │ 0x08                   │
60/// │ EmbeddingRef    │ 0x09                   │
61/// │ Image           │ 0x0A                   │
62/// │ Extension       │ 0xFE                   │
63/// │ End             │ 0xFF                   │
64/// │ Unknown         │ any other byte         │
65/// └─────────────────┴────────────────────────┘
66/// ```
67#[derive(Clone, Debug, PartialEq)]
68pub enum BlockContent {
69    Code(CodeBlock),
70    Conversation(ConversationBlock),
71    FileTree(FileTreeBlock),
72    ToolResult(ToolResultBlock),
73    Document(DocumentBlock),
74    StructuredData(StructuredDataBlock),
75    Diff(DiffBlock),
76    Annotation(AnnotationBlock),
77    EmbeddingRef(EmbeddingRefBlock),
78    Image(ImageBlock),
79    Extension(ExtensionBlock),
80    End,
81    /// Raw body bytes for an unrecognized block type.
82    Unknown {
83        type_id: u8,
84        body: Vec<u8>,
85    },
86}
87
88impl BlockContent {
89    /// Encode the typed content into a raw body byte vector.
90    ///
91    /// For `End`, returns an empty vec (END blocks have no body).
92    /// For `Unknown`, returns the preserved raw bytes as-is.
93    pub fn encode_body(&self) -> Vec<u8> {
94        match self {
95            Self::Code(b) => b.encode_body(),
96            Self::Conversation(b) => b.encode_body(),
97            Self::FileTree(b) => b.encode_body(),
98            Self::ToolResult(b) => b.encode_body(),
99            Self::Document(b) => b.encode_body(),
100            Self::StructuredData(b) => b.encode_body(),
101            Self::Diff(b) => b.encode_body(),
102            Self::Annotation(b) => b.encode_body(),
103            Self::EmbeddingRef(b) => b.encode_body(),
104            Self::Image(b) => b.encode_body(),
105            Self::Extension(b) => b.encode_body(),
106            Self::End => Vec::new(),
107            Self::Unknown { body, .. } => body.clone(),
108        }
109    }
110
111    /// Decode typed content from a raw body, dispatching on block type.
112    ///
113    /// The caller is responsible for stripping the summary prefix from
114    /// the body before calling this method (if `flags.has_summary()`).
115    pub fn decode_body(block_type: &BlockType, body: &[u8]) -> Result<Self, TypeError> {
116        match block_type {
117            BlockType::Code => Ok(Self::Code(CodeBlock::decode_body(body)?)),
118            BlockType::Conversation => {
119                Ok(Self::Conversation(ConversationBlock::decode_body(body)?))
120            }
121            BlockType::FileTree => Ok(Self::FileTree(FileTreeBlock::decode_body(body)?)),
122            BlockType::ToolResult => Ok(Self::ToolResult(ToolResultBlock::decode_body(body)?)),
123            BlockType::Document => Ok(Self::Document(DocumentBlock::decode_body(body)?)),
124            BlockType::StructuredData => Ok(Self::StructuredData(
125                StructuredDataBlock::decode_body(body)?,
126            )),
127            BlockType::Diff => Ok(Self::Diff(DiffBlock::decode_body(body)?)),
128            BlockType::Annotation => Ok(Self::Annotation(AnnotationBlock::decode_body(body)?)),
129            BlockType::EmbeddingRef => {
130                Ok(Self::EmbeddingRef(EmbeddingRefBlock::decode_body(body)?))
131            }
132            BlockType::Image => Ok(Self::Image(ImageBlock::decode_body(body)?)),
133            BlockType::Extension => Ok(Self::Extension(ExtensionBlock::decode_body(body)?)),
134            BlockType::End => Ok(Self::End),
135            BlockType::Unknown(id) => Ok(Self::Unknown {
136                type_id: *id,
137                body: body.to_vec(),
138            }),
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::enums::{FormatHint, Lang, Role, Status};
147
148    #[test]
149    fn block_encode_decode_roundtrip() {
150        let block = Block {
151            block_type: BlockType::Code,
152            flags: BlockFlags::NONE,
153            summary: None,
154            content: BlockContent::Code(CodeBlock {
155                lang: Lang::Rust,
156                path: "lib.rs".to_string(),
157                content: b"pub fn hello() {}".to_vec(),
158                line_range: None,
159            }),
160        };
161
162        let body = block.content.encode_body();
163        let decoded = BlockContent::decode_body(&block.block_type, &body).unwrap();
164        assert_eq!(decoded, block.content);
165    }
166
167    #[test]
168    fn block_with_summary() {
169        let summary = Summary {
170            text: "Main entry point".to_string(),
171        };
172        let content = BlockContent::Code(CodeBlock {
173            lang: Lang::Rust,
174            path: "main.rs".to_string(),
175            content: b"fn main() {}".to_vec(),
176            line_range: None,
177        });
178
179        // Encode: summary prefix + body
180        let mut body = Vec::new();
181        summary.encode(&mut body);
182        body.extend_from_slice(&content.encode_body());
183
184        // Decode: strip summary first, then decode content
185        let (decoded_summary, consumed) = Summary::decode(&body).unwrap();
186        let decoded_content =
187            BlockContent::decode_body(&BlockType::Code, &body[consumed..]).unwrap();
188
189        assert_eq!(decoded_summary, summary);
190        assert_eq!(decoded_content, content);
191    }
192
193    #[test]
194    fn unknown_block_type_preserved() {
195        let raw_body = b"arbitrary bytes".to_vec();
196        let block_type = BlockType::Unknown(0x42);
197
198        let content = BlockContent::decode_body(&block_type, &raw_body).unwrap();
199        assert_eq!(
200            content,
201            BlockContent::Unknown {
202                type_id: 0x42,
203                body: raw_body.clone()
204            }
205        );
206
207        // Round-trip: encoding gives back the same bytes
208        assert_eq!(content.encode_body(), raw_body);
209    }
210
211    #[test]
212    fn end_block_empty_body() {
213        let content = BlockContent::decode_body(&BlockType::End, &[]).unwrap();
214        assert_eq!(content, BlockContent::End);
215        assert!(content.encode_body().is_empty());
216    }
217
218    #[test]
219    fn all_block_types_dispatch() {
220        // Verify that decode_body dispatches to the right variant for each type.
221        // We use minimal valid bodies for each.
222
223        let code = CodeBlock {
224            lang: Lang::Python,
225            path: "x.py".to_string(),
226            content: b"pass".to_vec(),
227            line_range: None,
228        };
229        let body = code.encode_body();
230        let result = BlockContent::decode_body(&BlockType::Code, &body).unwrap();
231        assert!(matches!(result, BlockContent::Code(_)));
232
233        let conv = ConversationBlock {
234            role: Role::User,
235            content: b"hi".to_vec(),
236            tool_call_id: None,
237        };
238        let body = conv.encode_body();
239        let result = BlockContent::decode_body(&BlockType::Conversation, &body).unwrap();
240        assert!(matches!(result, BlockContent::Conversation(_)));
241
242        let doc = DocumentBlock {
243            title: "t".to_string(),
244            content: b"c".to_vec(),
245            format_hint: FormatHint::Plain,
246        };
247        let body = doc.encode_body();
248        let result = BlockContent::decode_body(&BlockType::Document, &body).unwrap();
249        assert!(matches!(result, BlockContent::Document(_)));
250
251        let tool = ToolResultBlock {
252            tool_name: "t".to_string(),
253            status: Status::Ok,
254            content: b"ok".to_vec(),
255            schema_hint: None,
256        };
257        let body = tool.encode_body();
258        let result = BlockContent::decode_body(&BlockType::ToolResult, &body).unwrap();
259        assert!(matches!(result, BlockContent::ToolResult(_)));
260    }
261}