#![forbid(unsafe_code)]
use core::num::NonZeroU64;
use postcard::experimental::max_size::MaxSize;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Id(NonZeroU64);
impl Id {
#[must_use]
pub const fn try_new(raw: u64) -> Option<Self> {
match NonZeroU64::new(raw) {
Some(nz) => Some(Self(nz)),
None => None,
}
}
#[must_use]
pub const fn from_nonzero(nz: NonZeroU64) -> Self {
Self(nz)
}
#[inline]
#[must_use]
pub const fn get(self) -> u64 {
self.0.get()
}
#[must_use]
pub const fn as_nonzero(self) -> NonZeroU64 {
self.0
}
#[inline]
#[must_use]
pub const fn to_be_bytes(self) -> [u8; 8] {
self.0.get().to_be_bytes()
}
#[inline]
#[must_use]
pub fn from_be_bytes(bytes: &[u8]) -> Option<Self> {
let arr: [u8; 8] = bytes.try_into().ok()?;
Self::try_new(u64::from_be_bytes(arr))
}
}
impl MaxSize for Id {
const POSTCARD_MAX_SIZE: usize = 10;
}
pub fn bump_next_id<F>(next_id: &mut u64, collection: F) -> Result<Id>
where
F: FnOnce() -> String,
{
if *next_id == 0 {
return Err(Error::IdSpaceExhausted {
collection: collection(),
});
}
let issued = *next_id;
*next_id = next_id.checked_add(1).unwrap_or(0);
Id::try_new(issued).ok_or_else(|| Error::IdSpaceExhausted {
collection: collection(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_new_rejects_zero() {
assert!(Id::try_new(0).is_none());
assert_eq!(Id::try_new(1).map(Id::get), Some(1));
assert_eq!(Id::try_new(u64::MAX).map(Id::get), Some(u64::MAX));
}
#[test]
fn be_bytes_round_trip() {
let id = Id::try_new(0x0102_0304_0506_0708).expect("non-zero");
assert_eq!(
id.to_be_bytes(),
[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
);
assert_eq!(Id::from_be_bytes(&id.to_be_bytes()), Some(id));
assert_eq!(Id::from_be_bytes(&[0u8; 8]), None);
assert_eq!(Id::from_be_bytes(&[0u8; 7]), None);
}
#[test]
fn serde_round_trip_via_postcard() {
let id = Id::try_new(42).expect("non-zero");
let bytes = postcard::to_allocvec(&id).expect("encode");
let back: Id = postcard::from_bytes(&bytes).expect("decode");
assert_eq!(back, id);
}
#[test]
fn serde_rejects_zero_on_decode() {
let bytes = [0u8];
let result = postcard::from_bytes::<Id>(&bytes);
assert!(result.is_err(), "zero must be rejected; got {result:?}");
}
#[test]
fn postcard_max_size_constant() {
assert_eq!(Id::POSTCARD_MAX_SIZE, 10);
}
#[test]
fn bump_allocator_advances() {
let mut next = 1u64;
let id1 = bump_next_id(&mut next, || "test".to_owned()).expect("bump 1");
assert_eq!(id1.get(), 1);
assert_eq!(next, 2);
let id2 = bump_next_id(&mut next, || "test".to_owned()).expect("bump 2");
assert_eq!(id2.get(), 2);
assert_eq!(next, 3);
}
#[test]
fn bump_allocator_detects_wraparound() {
let mut next = u64::MAX;
let id = bump_next_id(&mut next, || "wrap".to_owned()).expect("last id");
assert_eq!(id.get(), u64::MAX);
let err = bump_next_id(&mut next, || "wrap".to_owned()).expect_err("wraparound");
match err {
Error::IdSpaceExhausted { collection } => assert_eq!(collection, "wrap"),
other => panic!("expected IdSpaceExhausted, got {other:?}"),
}
}
#[test]
fn bump_allocator_detects_zero_watermark() {
let mut next = 0u64;
let err = bump_next_id(&mut next, || "zerowm".to_owned()).expect_err("zero watermark");
match err {
Error::IdSpaceExhausted { collection } => assert_eq!(collection, "zerowm"),
other => panic!("expected IdSpaceExhausted, got {other:?}"),
}
}
#[test]
fn bump_allocator_error_field_preserves_user_supplied_name() {
let mut next = 0u64;
let user_input = String::from("dynamically built name");
let err = bump_next_id(&mut next, || user_input.clone()).expect_err("error");
match err {
Error::IdSpaceExhausted { collection } => {
assert_eq!(collection, "dynamically built name");
}
other => panic!("expected IdSpaceExhausted, got {other:?}"),
}
}
}