use core::fmt;
use core::marker::PhantomData;
use core::num::NonZeroU64;
use core::str::FromStr;
#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::domain::IdDomain;
use crate::error::IdParseError;
pub struct Id<D: IdDomain> {
value: NonZeroU64,
_marker: PhantomData<D>,
}
impl<D: IdDomain> fmt::Debug for Id<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", D::DOMAIN_NAME, self.value)
}
}
impl<D: IdDomain> Copy for Id<D> {}
impl<D: IdDomain> Clone for Id<D> {
fn clone(&self) -> Self {
*self
}
}
impl<D: IdDomain> PartialEq for Id<D> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl<D: IdDomain> Eq for Id<D> {}
impl<D: IdDomain> PartialOrd for Id<D> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<D: IdDomain> Ord for Id<D> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.value.cmp(&other.value)
}
}
impl<D: IdDomain> core::hash::Hash for Id<D> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.value.hash(state);
}
}
impl<D: IdDomain> Id<D> {
#[inline]
#[must_use]
pub const fn new(value: u64) -> Option<Self> {
match NonZeroU64::new(value) {
Some(nz) => Some(Self {
value: nz,
_marker: PhantomData,
}),
None => None,
}
}
#[inline]
#[must_use]
pub const fn from_non_zero(value: NonZeroU64) -> Self {
Self {
value,
_marker: PhantomData,
}
}
#[inline]
#[must_use]
pub const fn get(&self) -> u64 {
self.value.get()
}
#[inline]
#[must_use]
pub const fn non_zero(&self) -> NonZeroU64 {
self.value
}
#[inline]
#[must_use]
pub fn domain(&self) -> &'static str {
D::DOMAIN_NAME
}
}
impl<D: IdDomain> fmt::Display for Id<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}
impl<D: IdDomain> FromStr for Id<D> {
type Err = IdParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value = s.parse::<NonZeroU64>()?;
Ok(Self::from_non_zero(value))
}
}
impl<D: IdDomain> From<NonZeroU64> for Id<D> {
#[inline]
fn from(value: NonZeroU64) -> Self {
Self::from_non_zero(value)
}
}
impl<D: IdDomain> From<Id<D>> for NonZeroU64 {
#[inline]
fn from(id: Id<D>) -> Self {
id.value
}
}
impl<D: IdDomain> From<Id<D>> for u64 {
#[inline]
fn from(id: Id<D>) -> Self {
id.value.get()
}
}
impl<D: IdDomain> TryFrom<u64> for Id<D> {
type Error = IdParseError;
fn try_from(value: u64) -> Result<Self, Self::Error> {
Self::new(value).ok_or(IdParseError::Zero)
}
}
impl<D: IdDomain> TryFrom<&str> for Id<D> {
type Error = IdParseError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl<D: IdDomain> TryFrom<String> for Id<D> {
type Error = IdParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
#[cfg(feature = "serde")]
impl<D: IdDomain> Serialize for Id<D> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.value.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de, D: IdDomain> Deserialize<'de> for Id<D> {
fn deserialize<De: serde::Deserializer<'de>>(deserializer: De) -> Result<Self, De::Error> {
let value = NonZeroU64::deserialize(deserializer)?;
Ok(Self::from_non_zero(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::{format, string::ToString};
#[derive(Debug)]
struct TestDomain;
impl crate::Domain for TestDomain {
const DOMAIN_NAME: &'static str = "test";
}
impl IdDomain for TestDomain {}
type TestId = Id<TestDomain>;
#[test]
fn new_stores_nonzero_value() {
let id = TestId::new(42).unwrap();
assert_eq!(id.get(), 42);
}
#[test]
fn zero_returns_none() {
assert!(TestId::new(0).is_none());
}
#[test]
fn try_from_u64_zero_is_error() {
let result = TestId::try_from(0u64);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), IdParseError::Zero));
}
#[test]
fn try_from_u64_nonzero_succeeds() {
let id = TestId::try_from(42u64).unwrap();
assert_eq!(id.get(), 42);
}
#[test]
fn from_non_zero_roundtrips() {
let nz = NonZeroU64::new(7).unwrap();
let id = TestId::from_non_zero(nz);
assert_eq!(id.get(), 7);
assert_eq!(id.non_zero(), nz);
}
#[test]
fn debug_shows_domain_and_value() {
let id = TestId::new(42).unwrap();
assert_eq!(format!("{id:?}"), "test(42)");
}
#[test]
fn domain_returns_name() {
let id = TestId::new(1).unwrap();
assert_eq!(id.domain(), "test");
}
#[test]
fn display_shows_numeric_value() {
let id = TestId::new(12345).unwrap();
assert_eq!(id.to_string(), "12345");
}
#[test]
fn parse_valid_string() {
let id: TestId = "42".parse().unwrap();
assert_eq!(id.get(), 42);
}
#[test]
fn parse_zero_string_is_error() {
let result: Result<TestId, _> = "0".parse();
assert!(result.is_err());
}
#[test]
fn parse_non_numeric_string_is_error() {
let result: Result<TestId, _> = "not_a_number".parse();
assert!(result.is_err());
}
#[test]
fn into_from_non_zero_u64_roundtrips() {
let nz = NonZeroU64::new(100).unwrap();
let id: TestId = nz.into();
assert_eq!(id.get(), 100);
}
#[test]
fn into_non_zero_u64_preserves_value() {
let id = TestId::new(99).unwrap();
let nz: NonZeroU64 = id.into();
assert_eq!(nz.get(), 99);
}
#[test]
fn into_u64_preserves_value() {
let id = TestId::new(99).unwrap();
let value: u64 = id.into();
assert_eq!(value, 99);
}
#[test]
fn try_from_str_succeeds() {
let id = TestId::try_from("7").unwrap();
assert_eq!(id.get(), 7);
}
#[test]
fn try_from_string_succeeds() {
let id = TestId::try_from(String::from("123")).unwrap();
assert_eq!(id.get(), 123);
}
#[test]
fn id_is_copy() {
let id1 = TestId::new(5).unwrap();
let id2 = id1; assert_eq!(id1, id2); }
#[test]
fn ordering_follows_numeric_value() {
let a = TestId::new(1).unwrap();
let b = TestId::new(2).unwrap();
assert!(a < b);
}
#[cfg(feature = "std")]
#[test]
fn equal_ids_produce_same_hash() {
use core::hash::{Hash, Hasher};
let id1 = TestId::new(42).unwrap();
let id2 = TestId::new(42).unwrap();
let hash = |id: &TestId| {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish()
};
assert_eq!(hash(&id1), hash(&id2));
}
#[test]
fn max_u64_is_valid_id() {
let id = TestId::new(u64::MAX).unwrap();
assert_eq!(id.get(), u64::MAX);
}
#[test]
fn option_id_has_no_size_overhead() {
assert_eq!(
core::mem::size_of::<Option<TestId>>(),
core::mem::size_of::<TestId>()
);
}
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip_preserves_id() {
let id = TestId::new(42).unwrap();
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "42");
let deserialized: TestId = serde_json::from_str(&json).unwrap();
assert_eq!(id, deserialized);
}
#[cfg(feature = "serde")]
#[test]
fn serde_rejects_zero() {
let result: Result<TestId, _> = serde_json::from_str("0");
assert!(result.is_err());
}
#[test]
fn different_domains_are_distinct_types() {
#[derive(Debug)]
struct DomainA;
impl crate::Domain for DomainA {
const DOMAIN_NAME: &'static str = "a";
}
impl IdDomain for DomainA {}
#[derive(Debug)]
struct DomainB;
impl crate::Domain for DomainB {
const DOMAIN_NAME: &'static str = "b";
}
impl IdDomain for DomainB {}
let _a: Id<DomainA> = Id::new(1).unwrap();
let _b: Id<DomainB> = Id::new(1).unwrap();
}
}