Skip to main content

bcp_types/
extension.rs

1use crate::error::TypeError;
2use crate::fields::{decode_bytes_value, decode_field_header, encode_bytes_field, skip_field};
3
4/// EXTENSION block — user-defined block type.
5///
6/// Provides an escape hatch for custom content that doesn't fit the
7/// 10 built-in block types. Extensions are namespaced to avoid collisions
8/// between different organizations or tools.
9///
10/// Field layout within body:
11///
12/// ```text
13/// ┌──────────┬───────────┬───────────┬───────────────────────────┐
14/// │ Field ID │ Wire Type │ Name      │ Description               │
15/// ├──────────┼───────────┼───────────┼───────────────────────────┤
16/// │ 1        │ Bytes     │ namespace │ Namespace (e.g. "myorg")  │
17/// │ 2        │ Bytes     │ type_name │ Type within namespace     │
18/// │ 3        │ Bytes     │ content   │ Opaque content bytes      │
19/// └──────────┴───────────┴───────────┴───────────────────────────┘
20/// ```
21///
22/// The `content` field is opaque — the BCP decoder does not attempt to
23/// parse it. Only consumers that understand the `namespace/type_name`
24/// pair will interpret the content.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct ExtensionBlock {
27    pub namespace: String,
28    pub type_name: String,
29    pub content: Vec<u8>,
30}
31
32impl ExtensionBlock {
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.namespace.as_bytes());
37        encode_bytes_field(&mut buf, 2, self.type_name.as_bytes());
38        encode_bytes_field(&mut buf, 3, &self.content);
39        buf
40    }
41
42    /// Deserialize an EXTENSION block from a TLV-encoded body.
43    pub fn decode_body(mut buf: &[u8]) -> Result<Self, TypeError> {
44        let mut namespace: Option<String> = None;
45        let mut type_name: Option<String> = None;
46        let mut content: Option<Vec<u8>> = 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                    namespace = Some(String::from_utf8_lossy(data).into_owned());
57                }
58                2 => {
59                    let (data, n) = decode_bytes_value(buf)?;
60                    buf = &buf[n..];
61                    type_name = Some(String::from_utf8_lossy(data).into_owned());
62                }
63                3 => {
64                    let (data, n) = decode_bytes_value(buf)?;
65                    buf = &buf[n..];
66                    content = Some(data.to_vec());
67                }
68                _ => {
69                    let n = skip_field(buf, header.wire_type)?;
70                    buf = &buf[n..];
71                }
72            }
73        }
74
75        Ok(Self {
76            namespace: namespace.ok_or(TypeError::MissingRequiredField { field: "namespace" })?,
77            type_name: type_name.ok_or(TypeError::MissingRequiredField { field: "type_name" })?,
78            content: content.ok_or(TypeError::MissingRequiredField { field: "content" })?,
79        })
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn roundtrip_extension() {
89        let block = ExtensionBlock {
90            namespace: "myorg".to_string(),
91            type_name: "custom_metric".to_string(),
92            content: b"{\"latency_ms\": 42}".to_vec(),
93        };
94        let body = block.encode_body();
95        let decoded = ExtensionBlock::decode_body(&body).unwrap();
96        assert_eq!(decoded, block);
97    }
98
99    #[test]
100    fn roundtrip_empty_content() {
101        let block = ExtensionBlock {
102            namespace: "test".to_string(),
103            type_name: "marker".to_string(),
104            content: vec![],
105        };
106        let body = block.encode_body();
107        let decoded = ExtensionBlock::decode_body(&body).unwrap();
108        assert_eq!(decoded, block);
109    }
110}