batpak_macros_support/lib.rs
1//! Runtime support types for `#[derive(EventPayload)]`.
2//!
3//! This crate is an implementation detail of the batpak macro surface.
4//! Its public items are re-exported from `batpak::__private`; do not
5//! depend on it directly.
6
7/// Re-exported so generated code can reference `batpak::__private::inventory`
8/// without requiring users to add `inventory` to their own Cargo.toml.
9pub extern crate inventory;
10
11/// Registration item emitted once per `#[derive(EventPayload)]` type.
12///
13/// `inventory::submit!` calls that produce these items are emitted
14/// unconditionally by generated code. That keeps dependency-crate payloads
15/// visible to a downstream binary's explicit registry validator instead of
16/// losing them behind each dependency crate's own `cfg(test)` boundary.
17pub struct EventPayloadRegistration {
18 /// Packed (category << 12 | type_id) as stored in EventKind.
19 pub kind_bits: u16,
20 /// `std::any::type_name::<T>()` for the derived type.
21 pub type_name: &'static str,
22}
23
24inventory::collect!(EventPayloadRegistration);
25
26/// One duplicate `EventKind` registration discovered in the current binary.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub struct EventKindCollision {
29 /// Packed `(category << 12) | type_id` representation.
30 pub kind_bits: u16,
31 /// First registered type name for `kind_bits`.
32 pub first_type_name: &'static str,
33 /// Second registered type name for `kind_bits`.
34 pub second_type_name: &'static str,
35}
36
37/// Return all `EventPayload` kind collisions visible in the current binary.
38pub fn find_kind_collisions() -> Vec<EventKindCollision> {
39 let mut seen: std::collections::HashMap<u16, &'static str> = std::collections::HashMap::new();
40 let mut collisions = Vec::new();
41 for item in inventory::iter::<EventPayloadRegistration>() {
42 if let Some(prior) = seen.insert(item.kind_bits, item.type_name) {
43 collisions.push(EventKindCollision {
44 kind_bits: item.kind_bits,
45 first_type_name: prior,
46 second_type_name: item.type_name,
47 });
48 }
49 }
50 collisions
51}
52
53/// Panic if the current binary has duplicate `EventPayload` kind registrations.
54pub fn assert_no_kind_collisions() {
55 if let Some(collision) = find_kind_collisions().into_iter().next() {
56 panic!(
57 "batpak EventKind collision detected.\n\
58 Types `{prior}` and `{ty}` share the same (category, type_id) \
59 pair (kind_bits = 0x{bits:04X}).\n\
60 Each EventPayload type must have a unique kind within a binary.",
61 prior = collision.first_type_name,
62 ty = collision.second_type_name,
63 bits = collision.kind_bits,
64 );
65 }
66}
67
68/// Scan all `EventPayload` registrations in the current binary for kind collisions.
69///
70/// Called by the `#[test]` function generated by `#[derive(EventPayload)]`.
71/// Detection scope is per-binary: two separate binaries can reuse the same
72/// `(category, type_id)` without triggering a panic here.
73///
74/// Safe to call in non-test binaries. Prefer [`find_kind_collisions`] for a
75/// structured result.
76pub fn scan_for_kind_collisions() {
77 assert_no_kind_collisions();
78}
79
80// ─── Validation ───────────────────────────────────────────────────────────────
81
82/// Minimum valid category value (inclusive).
83pub const CATEGORY_MIN: u8 = 1;
84/// Maximum valid category value (inclusive, 4 bits).
85pub const CATEGORY_MAX: u8 = 15;
86/// Maximum valid type_id value (inclusive, 12 bits).
87pub const TYPE_ID_MAX: u16 = 0x0FFF;
88
89/// Validate an EventKind category value.
90///
91/// Valid range: 0x1–0xF, excluding 0xD (reserved for effects).
92pub fn validate_category(cat: u8) -> Result<(), &'static str> {
93 match cat {
94 0x0 => Err("category 0x0 is reserved for system events"),
95 0xD => Err("category 0xD is reserved for effect events"),
96 1..=15 => Ok(()),
97 _ => Err("category must fit in 4 bits (0x1–0xF, excluding 0x0 and 0xD)"),
98 }
99}
100
101/// Validate an EventKind type_id value.
102///
103/// Valid range: 0x000–0xFFF (12 bits).
104pub fn validate_type_id(tid: u16) -> Result<(), &'static str> {
105 if tid > TYPE_ID_MAX {
106 Err("type_id must fit in 12 bits (0x000–0xFFF)")
107 } else {
108 Ok(())
109 }
110}