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}