Skip to main content

bcp_types/
code.rs

1use crate::enums::Lang;
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/// CODE block — represents a source code file or fragment.
9///
10/// This is the most common block type in practice: every source file,
11/// snippet, or code region in a context pack becomes a CODE block.
12///
13/// Field layout within body:
14///
15/// ```text
16/// ┌──────────┬───────────┬────────────┬────────────────────────────┐
17/// │ Field ID │ Wire Type │ Name       │ Description                │
18/// ├──────────┼───────────┼────────────┼────────────────────────────┤
19/// │ 1        │ Varint    │ lang       │ Language enum byte         │
20/// │ 2        │ Bytes     │ path       │ UTF-8 file path            │
21/// │ 3        │ Bytes     │ content    │ Raw source code bytes      │
22/// │ 4        │ Varint    │ line_start │ Start line (optional)      │
23/// │ 5        │ Varint    │ line_end   │ End line (optional)        │
24/// └──────────┴───────────┴────────────┴────────────────────────────┘
25/// ```
26///
27/// Fields 4 and 5 are optional — they are only encoded when `line_range`
28/// is `Some`. This lets you represent either a full file or a specific
29/// line range within it.
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct CodeBlock {
32    pub lang: Lang,
33    pub path: String,
34    pub content: Vec<u8>,
35    /// Optional line range `(start, end)` for code fragments.
36    /// Both values are 1-indexed and inclusive.
37    pub line_range: Option<(u32, u32)>,
38}
39
40impl CodeBlock {
41    /// Serialize this block's fields into a TLV-encoded body.
42    pub fn encode_body(&self) -> Vec<u8> {
43        let mut buf = Vec::new();
44        encode_varint_field(&mut buf, 1, u64::from(self.lang.to_wire_byte()));
45        encode_bytes_field(&mut buf, 2, self.path.as_bytes());
46        encode_bytes_field(&mut buf, 3, &self.content);
47        if let Some((start, end)) = self.line_range {
48            encode_varint_field(&mut buf, 4, u64::from(start));
49            encode_varint_field(&mut buf, 5, u64::from(end));
50        }
51        buf
52    }
53
54    /// Deserialize a CODE block from a TLV-encoded body.
55    ///
56    /// Unknown field IDs are silently skipped for forward compatibility.
57    pub fn decode_body(mut buf: &[u8]) -> Result<Self, TypeError> {
58        let mut lang: Option<Lang> = None;
59        let mut path: Option<String> = None;
60        let mut content: Option<Vec<u8>> = None;
61        let mut line_start: Option<u32> = None;
62        let mut line_end: Option<u32> = None;
63
64        while !buf.is_empty() {
65            let (header, n) = decode_field_header(buf)?;
66            buf = &buf[n..];
67
68            match header.field_id {
69                1 => {
70                    let (v, n) = decode_varint_value(buf)?;
71                    buf = &buf[n..];
72                    lang = Some(Lang::from_wire_byte(v as u8));
73                }
74                2 => {
75                    let (data, n) = decode_bytes_value(buf)?;
76                    buf = &buf[n..];
77                    path = Some(String::from_utf8_lossy(data).into_owned());
78                }
79                3 => {
80                    let (data, n) = decode_bytes_value(buf)?;
81                    buf = &buf[n..];
82                    content = Some(data.to_vec());
83                }
84                4 => {
85                    let (v, n) = decode_varint_value(buf)?;
86                    buf = &buf[n..];
87                    line_start = Some(v as u32);
88                }
89                5 => {
90                    let (v, n) = decode_varint_value(buf)?;
91                    buf = &buf[n..];
92                    line_end = Some(v as u32);
93                }
94                _ => {
95                    let n = skip_field(buf, header.wire_type)?;
96                    buf = &buf[n..];
97                }
98            }
99        }
100
101        Ok(Self {
102            lang: lang.ok_or(TypeError::MissingRequiredField { field: "lang" })?,
103            path: path.ok_or(TypeError::MissingRequiredField { field: "path" })?,
104            content: content.ok_or(TypeError::MissingRequiredField { field: "content" })?,
105            line_range: match (line_start, line_end) {
106                (Some(s), Some(e)) => Some((s, e)),
107                _ => None,
108            },
109        })
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn roundtrip_full_file() {
119        let block = CodeBlock {
120            lang: Lang::Rust,
121            path: "src/main.rs".to_string(),
122            content: b"fn main() {}".to_vec(),
123            line_range: None,
124        };
125        let body = block.encode_body();
126        let decoded = CodeBlock::decode_body(&body).unwrap();
127        assert_eq!(decoded, block);
128    }
129
130    #[test]
131    fn roundtrip_with_line_range() {
132        let block = CodeBlock {
133            lang: Lang::TypeScript,
134            path: "src/index.ts".to_string(),
135            content: b"console.log('hello');".to_vec(),
136            line_range: Some((10, 25)),
137        };
138        let body = block.encode_body();
139        let decoded = CodeBlock::decode_body(&body).unwrap();
140        assert_eq!(decoded, block);
141    }
142
143    #[test]
144    fn roundtrip_unknown_language() {
145        let block = CodeBlock {
146            lang: Lang::Other(0x42),
147            path: "script.xyz".to_string(),
148            content: b"custom code".to_vec(),
149            line_range: None,
150        };
151        let body = block.encode_body();
152        let decoded = CodeBlock::decode_body(&body).unwrap();
153        assert_eq!(decoded.lang, Lang::Other(0x42));
154    }
155
156    #[test]
157    fn missing_content_field() {
158        // Encode only lang and path, no content
159        let mut buf = Vec::new();
160        encode_varint_field(&mut buf, 1, 0x01);
161        encode_bytes_field(&mut buf, 2, b"test.rs");
162
163        let result = CodeBlock::decode_body(&buf);
164        assert!(matches!(
165            result,
166            Err(TypeError::MissingRequiredField { field: "content" })
167        ));
168    }
169}