Skip to main content

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}