use std::num::NonZeroU32;
use serde::{Deserialize, Serialize};
macro_rules! define_id {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(transparent)]
pub struct $name(NonZeroU32);
impl $name {
#[must_use]
pub const fn new(raw: u32) -> Option<Self> {
match NonZeroU32::new(raw) {
Some(v) => Some(Self(v)),
None => None,
}
}
#[must_use]
pub fn from_index(raw: usize) -> Self {
let clamped: u32 = u32::try_from(raw).unwrap_or(u32::MAX - 1);
let one_based = clamped.saturating_add(1);
NonZeroU32::new(one_based).map_or(Self(NonZeroU32::MIN), Self)
}
#[must_use]
pub const fn get(self) -> u32 {
self.0.get()
}
#[must_use]
pub const fn index(self) -> usize {
(self.0.get() - 1) as usize
}
}
};
}
define_id! {
ComponentId
}
define_id! {
ModuleId
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
#[test]
fn test_should_round_trip_component_id_from_index() {
let id = ComponentId::from_index(0);
assert_eq!(id.get(), 1, "1-based id");
assert_eq!(id.index(), 0, "0-based index");
}
#[test]
fn test_should_reject_zero_in_component_id_new() {
assert!(ComponentId::new(0).is_none(), "zero is not a valid id");
assert_eq!(ComponentId::new(1).map(ComponentId::get), Some(1));
}
#[test]
fn test_should_keep_option_niche_optimisation() {
assert_eq!(
std::mem::size_of::<Option<ComponentId>>(),
std::mem::size_of::<ComponentId>()
);
}
#[test]
fn test_should_saturate_component_id_from_giant_index() {
let id = ComponentId::from_index(usize::MAX);
assert_eq!(id.get(), u32::MAX);
}
#[test]
fn test_should_serde_round_trip_component_id() {
let id = ComponentId::from_index(41);
let s = serde_json::to_string(&id).unwrap();
assert_eq!(s, "42");
let back: ComponentId = serde_json::from_str(&s).unwrap();
assert_eq!(back, id);
}
}