use crate::event::EventKind;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
static PAYLOAD_REGISTRY_OPEN_CACHE: Mutex<Option<Result<(), EventPayloadRegistryError>>> =
Mutex::new(None);
static PAYLOAD_REGISTRY_WARNED: AtomicBool = AtomicBool::new(false);
pub trait EventPayload: Serialize + DeserializeOwned {
const KIND: EventKind;
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum EventPayloadValidation {
#[default]
Warn,
FailFast,
Silent,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EventPayloadKindCollision {
pub category: u8,
pub type_id: u16,
pub first_type_name: &'static str,
pub second_type_name: &'static str,
}
impl EventPayloadKindCollision {
fn from_support(collision: batpak_macros_support::EventKindCollision) -> Self {
#[allow(clippy::cast_possible_truncation)]
let category = (collision.kind_bits >> 12) as u8;
Self {
category,
type_id: collision.kind_bits & 0x0FFF,
first_type_name: collision.first_type_name,
second_type_name: collision.second_type_name,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EventPayloadRegistryError {
collisions: Vec<EventPayloadKindCollision>,
}
impl EventPayloadRegistryError {
pub fn new(collisions: Vec<EventPayloadKindCollision>) -> Self {
assert!(
!collisions.is_empty(),
"EventPayloadRegistryError requires at least one collision"
);
Self { collisions }
}
pub fn collisions(&self) -> &[EventPayloadKindCollision] {
&self.collisions
}
}
impl fmt::Display for EventPayloadRegistryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let first = &self.collisions[0];
write!(
f,
"EventPayload registry contains {} duplicate kind assignment(s); first collision is category=0x{:X} type_id=0x{:03X} between `{}` and `{}`",
self.collisions.len(),
first.category,
first.type_id,
first.first_type_name,
first.second_type_name,
)
}
}
impl std::error::Error for EventPayloadRegistryError {}
pub fn validate_event_payload_registry() -> Result<(), EventPayloadRegistryError> {
let collisions = batpak_macros_support::find_kind_collisions()
.into_iter()
.map(EventPayloadKindCollision::from_support)
.collect::<Vec<_>>();
if collisions.is_empty() {
Ok(())
} else {
Err(EventPayloadRegistryError::new(collisions))
}
}
pub fn revalidate_event_payload_registry() -> Result<(), EventPayloadRegistryError> {
let result = validate_event_payload_registry();
PAYLOAD_REGISTRY_WARNED.store(false, Ordering::SeqCst);
let Ok(mut cached) = PAYLOAD_REGISTRY_OPEN_CACHE.lock() else {
return result;
};
*cached = Some(result.clone());
result
}
pub(crate) fn cached_event_payload_registry_validation() -> Result<(), EventPayloadRegistryError> {
let Ok(mut cached) = PAYLOAD_REGISTRY_OPEN_CACHE.lock() else {
return validate_event_payload_registry();
};
if let Some(result) = cached.as_ref() {
return result.clone();
}
let result = validate_event_payload_registry();
*cached = Some(result.clone());
result
}
pub(crate) fn mark_event_payload_registry_warning_emitted() -> bool {
!PAYLOAD_REGISTRY_WARNED.swap(true, Ordering::SeqCst)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn warning_marker_returns_true_once_until_revalidation_resets_it() {
revalidate_event_payload_registry().expect("test registry is clean");
assert!(
mark_event_payload_registry_warning_emitted(),
"PROPERTY: first open-time collision warning in a process must be emitted"
);
assert!(
!mark_event_payload_registry_warning_emitted(),
"PROPERTY: later open-time checks in the same process must not emit duplicate warnings"
);
revalidate_event_payload_registry().expect("test registry is clean after marker reset");
assert!(
mark_event_payload_registry_warning_emitted(),
"PROPERTY: explicit revalidation must reset the one-shot warning marker"
);
}
}