Skip to main content

bcp_types/
enums.rs

1use crate::error::TypeError;
2
3// ── Macro for wire-byte enum boilerplate ──────────────────────────────
4//
5// Every enum in this module follows the same pattern: a fixed set of
6// named variants, each mapped to a single wire byte, plus a conversion
7// pair (to_wire_byte / from_wire_byte). This macro eliminates the
8// repetition while keeping each enum's doc comments and derive list
9// explicit at the call site.
10
11macro_rules! wire_enum {
12  (
13    $(#[$meta:meta])*
14    pub enum $name:ident {
15      $( $(#[$vmeta:meta])* $variant:ident = $wire:expr ),+ $(,)?
16    }
17  ) => {
18    $(#[$meta])*
19    pub enum $name {
20      $( $(#[$vmeta])* $variant ),+
21    }
22
23    impl $name {
24      /// Encode this variant as a single wire byte.
25      pub fn to_wire_byte(self) -> u8 {
26        match self {
27          $( Self::$variant => $wire ),+
28        }
29      }
30
31      /// Decode a wire byte into this enum.
32      ///
33      /// Returns `Err(TypeError::InvalidEnumValue)` if the byte
34      /// doesn't match any known variant.
35      pub fn from_wire_byte(value: u8) -> Result<Self, TypeError> {
36        match value {
37          $( $wire => Ok(Self::$variant), )+
38          other => Err(TypeError::InvalidEnumValue {
39            enum_name: stringify!($name),
40            value: other,
41          }),
42        }
43      }
44    }
45  };
46}
47
48// ── Lang ──────────────────────────────────────────────────────────────
49
50/// Programming language identifiers for CODE blocks.
51///
52/// Each variant maps to a single wire byte. The `Unknown` variant (0xFF)
53/// is used when the language is not in the predefined set. For truly
54/// unrecognized bytes from a newer encoder, `Other(u8)` preserves the
55/// raw value for forward compatibility.
56///
57/// ```text
58/// ┌──────┬────────────┐
59/// │ Wire │ Language    │
60/// ├──────┼────────────┤
61/// │ 0x01 │ Rust       │
62/// │ 0x02 │ TypeScript │
63/// │ 0x03 │ JavaScript │
64/// │ 0x04 │ Python     │
65/// │ 0x05 │ Go         │
66/// │ 0x06 │ Java       │
67/// │ 0x07 │ C          │
68/// │ 0x08 │ Cpp        │
69/// │ 0x09 │ Ruby       │
70/// │ 0x0A │ Shell      │
71/// │ 0x0B │ Sql        │
72/// │ 0x0C │ Html       │
73/// │ 0x0D │ Css        │
74/// │ 0x0E │ Json       │
75/// │ 0x0F │ Yaml       │
76/// │ 0x10 │ Toml       │
77/// │ 0x11 │ Markdown   │
78/// │ 0xFF │ Unknown    │
79/// └──────┴────────────┘
80/// ```
81///
82/// `Lang` is special compared to other enums in this module: it has an
83/// `Other(u8)` variant for forward compatibility, so it cannot use the
84/// `wire_enum!` macro and is implemented manually.
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub enum Lang {
87    Rust,
88    TypeScript,
89    JavaScript,
90    Python,
91    Go,
92    Java,
93    C,
94    Cpp,
95    Ruby,
96    Shell,
97    Sql,
98    Html,
99    Css,
100    Json,
101    Yaml,
102    Toml,
103    Markdown,
104    Unknown,
105    /// Forward-compatible catch-all. Preserves the raw wire byte for
106    /// language IDs this version doesn't recognize.
107    Other(u8),
108}
109
110impl Lang {
111    /// Encode this variant as a single wire byte.
112    pub fn to_wire_byte(self) -> u8 {
113        match self {
114            Self::Rust => 0x01,
115            Self::TypeScript => 0x02,
116            Self::JavaScript => 0x03,
117            Self::Python => 0x04,
118            Self::Go => 0x05,
119            Self::Java => 0x06,
120            Self::C => 0x07,
121            Self::Cpp => 0x08,
122            Self::Ruby => 0x09,
123            Self::Shell => 0x0A,
124            Self::Sql => 0x0B,
125            Self::Html => 0x0C,
126            Self::Css => 0x0D,
127            Self::Json => 0x0E,
128            Self::Yaml => 0x0F,
129            Self::Toml => 0x10,
130            Self::Markdown => 0x11,
131            Self::Unknown => 0xFF,
132            Self::Other(id) => id,
133        }
134    }
135
136    /// Decode a wire byte into a [`Lang`].
137    ///
138    /// Known values map to named variants. Unrecognized values become
139    /// `Other(id)` rather than an error, since new languages may be
140    /// added without a spec revision.
141    pub fn from_wire_byte(value: u8) -> Self {
142        match value {
143            0x01 => Self::Rust,
144            0x02 => Self::TypeScript,
145            0x03 => Self::JavaScript,
146            0x04 => Self::Python,
147            0x05 => Self::Go,
148            0x06 => Self::Java,
149            0x07 => Self::C,
150            0x08 => Self::Cpp,
151            0x09 => Self::Ruby,
152            0x0A => Self::Shell,
153            0x0B => Self::Sql,
154            0x0C => Self::Html,
155            0x0D => Self::Css,
156            0x0E => Self::Json,
157            0x0F => Self::Yaml,
158            0x10 => Self::Toml,
159            0x11 => Self::Markdown,
160            0xFF => Self::Unknown,
161            other => Self::Other(other),
162        }
163    }
164}
165
166// ── Role ──────────────────────────────────────────────────────────────
167
168wire_enum! {
169  /// Conversation role for CONVERSATION blocks.
170  ///
171  /// Maps the four standard chat roles to single-byte wire values.
172  /// Unlike [`Lang`], this enum does not have an `Other` variant —
173  /// unrecognized role bytes produce an error, since role semantics
174  /// are fundamental to conversation structure.
175  ///
176  /// ```text
177  /// ┌──────┬───────────┐
178  /// │ Wire │ Role      │
179  /// ├──────┼───────────┤
180  /// │ 0x01 │ System    │
181  /// │ 0x02 │ User      │
182  /// │ 0x03 │ Assistant │
183  /// │ 0x04 │ Tool      │
184  /// └──────┴───────────┘
185  /// ```
186  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
187  pub enum Role {
188    System = 0x01,
189    User = 0x02,
190    Assistant = 0x03,
191    Tool = 0x04,
192  }
193}
194
195// ── Status ────────────────────────────────────────────────────────────
196
197wire_enum! {
198  /// Tool execution status for TOOL_RESULT blocks.
199  ///
200  /// Indicates whether the tool invocation succeeded, failed, or timed out.
201  ///
202  /// ```text
203  /// ┌──────┬─────────┐
204  /// │ Wire │ Status  │
205  /// ├──────┼─────────┤
206  /// │ 0x01 │ Ok      │
207  /// │ 0x02 │ Error   │
208  /// │ 0x03 │ Timeout │
209  /// └──────┴─────────┘
210  /// ```
211  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
212  pub enum Status {
213    Ok = 0x01,
214    Error = 0x02,
215    Timeout = 0x03,
216  }
217}
218
219// ── Priority ──────────────────────────────────────────────────────────
220
221wire_enum! {
222  /// Content priority for ANNOTATION blocks.
223  ///
224  /// Used by the token budget engine to decide which blocks to include,
225  /// summarize, or drop when context space is limited. Ordered from
226  /// highest to lowest urgency.
227  ///
228  /// ```text
229  /// ┌──────┬────────────┐
230  /// │ Wire │ Priority   │
231  /// ├──────┼────────────┤
232  /// │ 0x01 │ Critical   │
233  /// │ 0x02 │ High       │
234  /// │ 0x03 │ Normal     │
235  /// │ 0x04 │ Low        │
236  /// │ 0x05 │ Background │
237  /// └──────┴────────────┘
238  /// ```
239  #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
240  pub enum Priority {
241    Critical = 0x01,
242    High = 0x02,
243    Normal = 0x03,
244    Low = 0x04,
245    Background = 0x05,
246  }
247}
248
249// ── FormatHint ────────────────────────────────────────────────────────
250
251wire_enum! {
252  /// Document format hint for DOCUMENT blocks.
253  ///
254  /// Tells the renderer how to interpret the document body. This is a
255  /// hint, not a guarantee — the body may contain mixed content.
256  ///
257  /// ```text
258  /// ┌──────┬──────────┐
259  /// │ Wire │ Format   │
260  /// ├──────┼──────────┤
261  /// │ 0x01 │ Markdown │
262  /// │ 0x02 │ Plain    │
263  /// │ 0x03 │ Html     │
264  /// └──────┴──────────┘
265  /// ```
266  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
267  pub enum FormatHint {
268    Markdown = 0x01,
269    Plain = 0x02,
270    Html = 0x03,
271  }
272}
273
274// ── DataFormat ────────────────────────────────────────────────────────
275
276wire_enum! {
277  /// Data format for STRUCTURED_DATA blocks.
278  ///
279  /// Identifies the serialization format of the block's content field,
280  /// so the renderer can syntax-highlight or parse it appropriately.
281  ///
282  /// ```text
283  /// ┌──────┬──────┐
284  /// │ Wire │ Fmt  │
285  /// ├──────┼──────┤
286  /// │ 0x01 │ Json │
287  /// │ 0x02 │ Yaml │
288  /// │ 0x03 │ Toml │
289  /// │ 0x04 │ Csv  │
290  /// └──────┴──────┘
291  /// ```
292  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
293  pub enum DataFormat {
294    Json = 0x01,
295    Yaml = 0x02,
296    Toml = 0x03,
297    Csv = 0x04,
298  }
299}
300
301// ── AnnotationKind ────────────────────────────────────────────────────
302
303wire_enum! {
304  /// Annotation kind for ANNOTATION blocks.
305  ///
306  /// Determines how the annotation's `value` field should be interpreted.
307  ///
308  /// ```text
309  /// ┌──────┬──────────┐
310  /// │ Wire │ Kind     │
311  /// ├──────┼──────────┤
312  /// │ 0x01 │ Priority │
313  /// │ 0x02 │ Summary  │
314  /// │ 0x03 │ Tag      │
315  /// └──────┴──────────┘
316  /// ```
317  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
318  pub enum AnnotationKind {
319    Priority = 0x01,
320    Summary = 0x02,
321    Tag = 0x03,
322  }
323}
324
325// ── MediaType ─────────────────────────────────────────────────────────
326
327wire_enum! {
328  /// Image media type for IMAGE blocks.
329  ///
330  /// Identifies the image encoding so the decoder knows how to handle
331  /// the raw bytes in the `data` field.
332  ///
333  /// ```text
334  /// ┌──────┬──────┐
335  /// │ Wire │ Type │
336  /// ├──────┼──────┤
337  /// │ 0x01 │ Png  │
338  /// │ 0x02 │ Jpeg │
339  /// │ 0x03 │ Gif  │
340  /// │ 0x04 │ Svg  │
341  /// │ 0x05 │ Webp │
342  /// └──────┴──────┘
343  /// ```
344  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
345  pub enum MediaType {
346    Png = 0x01,
347    Jpeg = 0x02,
348    Gif = 0x03,
349    Svg = 0x04,
350    Webp = 0x05,
351  }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    // ── Lang tests ────────────────────────────────────────────────────
359
360    #[test]
361    fn lang_all_known_roundtrip() {
362        let cases = [
363            (Lang::Rust, 0x01),
364            (Lang::TypeScript, 0x02),
365            (Lang::JavaScript, 0x03),
366            (Lang::Python, 0x04),
367            (Lang::Go, 0x05),
368            (Lang::Java, 0x06),
369            (Lang::C, 0x07),
370            (Lang::Cpp, 0x08),
371            (Lang::Ruby, 0x09),
372            (Lang::Shell, 0x0A),
373            (Lang::Sql, 0x0B),
374            (Lang::Html, 0x0C),
375            (Lang::Css, 0x0D),
376            (Lang::Json, 0x0E),
377            (Lang::Yaml, 0x0F),
378            (Lang::Toml, 0x10),
379            (Lang::Markdown, 0x11),
380            (Lang::Unknown, 0xFF),
381        ];
382        for (variant, wire) in cases {
383            assert_eq!(variant.to_wire_byte(), wire);
384            assert_eq!(Lang::from_wire_byte(wire), variant);
385        }
386    }
387
388    #[test]
389    fn lang_other_preserved() {
390        let lang = Lang::from_wire_byte(0x42);
391        assert_eq!(lang, Lang::Other(0x42));
392        assert_eq!(lang.to_wire_byte(), 0x42);
393    }
394
395    // ── Role tests ────────────────────────────────────────────────────
396
397    #[test]
398    fn role_roundtrip() {
399        let cases = [
400            (Role::System, 0x01),
401            (Role::User, 0x02),
402            (Role::Assistant, 0x03),
403            (Role::Tool, 0x04),
404        ];
405        for (variant, wire) in cases {
406            assert_eq!(variant.to_wire_byte(), wire);
407            assert_eq!(Role::from_wire_byte(wire).unwrap(), variant);
408        }
409    }
410
411    #[test]
412    fn role_invalid_rejected() {
413        let result = Role::from_wire_byte(0x09);
414        assert!(matches!(
415            result,
416            Err(TypeError::InvalidEnumValue {
417                enum_name: "Role",
418                value: 0x09
419            })
420        ));
421    }
422
423    // ── Status tests ──────────────────────────────────────────────────
424
425    #[test]
426    fn status_roundtrip() {
427        let cases = [
428            (Status::Ok, 0x01),
429            (Status::Error, 0x02),
430            (Status::Timeout, 0x03),
431        ];
432        for (variant, wire) in cases {
433            assert_eq!(variant.to_wire_byte(), wire);
434            assert_eq!(Status::from_wire_byte(wire).unwrap(), variant);
435        }
436    }
437
438    // ── Priority tests ────────────────────────────────────────────────
439
440    #[test]
441    fn priority_roundtrip() {
442        let cases = [
443            (Priority::Critical, 0x01),
444            (Priority::High, 0x02),
445            (Priority::Normal, 0x03),
446            (Priority::Low, 0x04),
447            (Priority::Background, 0x05),
448        ];
449        for (variant, wire) in cases {
450            assert_eq!(variant.to_wire_byte(), wire);
451            assert_eq!(Priority::from_wire_byte(wire).unwrap(), variant);
452        }
453    }
454
455    #[test]
456    fn priority_ordering() {
457        assert!(Priority::Critical < Priority::High);
458        assert!(Priority::High < Priority::Normal);
459        assert!(Priority::Normal < Priority::Low);
460        assert!(Priority::Low < Priority::Background);
461    }
462
463    // ── FormatHint tests ──────────────────────────────────────────────
464
465    #[test]
466    fn format_hint_roundtrip() {
467        let cases = [
468            (FormatHint::Markdown, 0x01),
469            (FormatHint::Plain, 0x02),
470            (FormatHint::Html, 0x03),
471        ];
472        for (variant, wire) in cases {
473            assert_eq!(variant.to_wire_byte(), wire);
474            assert_eq!(FormatHint::from_wire_byte(wire).unwrap(), variant);
475        }
476    }
477
478    // ── DataFormat tests ──────────────────────────────────────────────
479
480    #[test]
481    fn data_format_roundtrip() {
482        let cases = [
483            (DataFormat::Json, 0x01),
484            (DataFormat::Yaml, 0x02),
485            (DataFormat::Toml, 0x03),
486            (DataFormat::Csv, 0x04),
487        ];
488        for (variant, wire) in cases {
489            assert_eq!(variant.to_wire_byte(), wire);
490            assert_eq!(DataFormat::from_wire_byte(wire).unwrap(), variant);
491        }
492    }
493
494    // ── AnnotationKind tests ──────────────────────────────────────────
495
496    #[test]
497    fn annotation_kind_roundtrip() {
498        let cases = [
499            (AnnotationKind::Priority, 0x01),
500            (AnnotationKind::Summary, 0x02),
501            (AnnotationKind::Tag, 0x03),
502        ];
503        for (variant, wire) in cases {
504            assert_eq!(variant.to_wire_byte(), wire);
505            assert_eq!(AnnotationKind::from_wire_byte(wire).unwrap(), variant);
506        }
507    }
508
509    // ── MediaType tests ───────────────────────────────────────────────
510
511    #[test]
512    fn media_type_roundtrip() {
513        let cases = [
514            (MediaType::Png, 0x01),
515            (MediaType::Jpeg, 0x02),
516            (MediaType::Gif, 0x03),
517            (MediaType::Svg, 0x04),
518            (MediaType::Webp, 0x05),
519        ];
520        for (variant, wire) in cases {
521            assert_eq!(variant.to_wire_byte(), wire);
522            assert_eq!(MediaType::from_wire_byte(wire).unwrap(), variant);
523        }
524    }
525}