1use core::fmt;
40use core::marker::PhantomData;
41use core::num::NonZeroU64;
42use core::str::FromStr;
43
44#[cfg(not(feature = "std"))]
45use alloc::string::String;
46
47#[cfg(feature = "serde")]
48use serde::{Deserialize, Serialize};
49
50use crate::domain::IdDomain;
51use crate::error::IdParseError;
52
53pub struct Id<D: IdDomain> {
106 value: NonZeroU64,
107 _marker: PhantomData<D>,
108}
109
110impl<D: IdDomain> fmt::Debug for Id<D> {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 write!(f, "{}({})", D::DOMAIN_NAME, self.value)
116 }
117}
118
119impl<D: IdDomain> Copy for Id<D> {}
120
121impl<D: IdDomain> Clone for Id<D> {
122 fn clone(&self) -> Self {
123 *self
124 }
125}
126
127impl<D: IdDomain> PartialEq for Id<D> {
128 fn eq(&self, other: &Self) -> bool {
129 self.value == other.value
130 }
131}
132
133impl<D: IdDomain> Eq for Id<D> {}
134
135impl<D: IdDomain> PartialOrd for Id<D> {
136 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
137 Some(self.cmp(other))
138 }
139}
140
141impl<D: IdDomain> Ord for Id<D> {
142 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
143 self.value.cmp(&other.value)
144 }
145}
146
147impl<D: IdDomain> core::hash::Hash for Id<D> {
148 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
149 self.value.hash(state);
150 }
151}
152
153impl<D: IdDomain> Id<D> {
154 #[inline]
174 #[must_use]
175 pub const fn new(value: u64) -> Option<Self> {
176 match NonZeroU64::new(value) {
177 Some(nz) => Some(Self {
178 value: nz,
179 _marker: PhantomData,
180 }),
181 None => None,
182 }
183 }
184
185 #[inline]
187 #[must_use]
188 pub const fn from_non_zero(value: NonZeroU64) -> Self {
189 Self {
190 value,
191 _marker: PhantomData,
192 }
193 }
194
195 #[inline]
197 #[must_use]
198 pub const fn get(&self) -> u64 {
199 self.value.get()
200 }
201
202 #[inline]
204 #[must_use]
205 pub const fn non_zero(&self) -> NonZeroU64 {
206 self.value
207 }
208
209 #[inline]
211 #[must_use]
212 pub fn domain(&self) -> &'static str {
213 D::DOMAIN_NAME
214 }
215}
216
217impl<D: IdDomain> fmt::Display for Id<D> {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 write!(f, "{}", self.value)
227 }
228}
229
230impl<D: IdDomain> FromStr for Id<D> {
231 type Err = IdParseError;
232
233 fn from_str(s: &str) -> Result<Self, Self::Err> {
234 let value = s.parse::<NonZeroU64>()?;
235 Ok(Self::from_non_zero(value))
236 }
237}
238
239impl<D: IdDomain> From<NonZeroU64> for Id<D> {
240 #[inline]
241 fn from(value: NonZeroU64) -> Self {
242 Self::from_non_zero(value)
243 }
244}
245
246impl<D: IdDomain> From<Id<D>> for NonZeroU64 {
247 #[inline]
248 fn from(id: Id<D>) -> Self {
249 id.value
250 }
251}
252
253impl<D: IdDomain> From<Id<D>> for u64 {
254 #[inline]
255 fn from(id: Id<D>) -> Self {
256 id.value.get()
257 }
258}
259
260impl<D: IdDomain> TryFrom<u64> for Id<D> {
261 type Error = IdParseError;
262
263 fn try_from(value: u64) -> Result<Self, Self::Error> {
264 Self::new(value).ok_or(IdParseError::Zero)
265 }
266}
267
268impl<D: IdDomain> TryFrom<&str> for Id<D> {
269 type Error = IdParseError;
270
271 fn try_from(s: &str) -> Result<Self, Self::Error> {
272 s.parse()
273 }
274}
275
276impl<D: IdDomain> TryFrom<String> for Id<D> {
277 type Error = IdParseError;
278
279 fn try_from(s: String) -> Result<Self, Self::Error> {
280 s.parse()
281 }
282}
283
284#[cfg(feature = "serde")]
289impl<D: IdDomain> Serialize for Id<D> {
290 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
291 self.value.serialize(serializer)
292 }
293}
294
295#[cfg(feature = "serde")]
296impl<'de, D: IdDomain> Deserialize<'de> for Id<D> {
297 fn deserialize<De: serde::Deserializer<'de>>(deserializer: De) -> Result<Self, De::Error> {
298 let value = NonZeroU64::deserialize(deserializer)?;
299 Ok(Self::from_non_zero(value))
300 }
301}
302
303#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[cfg(not(feature = "std"))]
312 use alloc::{format, string::ToString};
313
314 #[derive(Debug)]
315 struct TestDomain;
316 impl crate::Domain for TestDomain {
317 const DOMAIN_NAME: &'static str = "test";
318 }
319 impl IdDomain for TestDomain {}
320
321 type TestId = Id<TestDomain>;
322
323 #[test]
324 fn new_stores_nonzero_value() {
325 let id = TestId::new(42).unwrap();
326 assert_eq!(id.get(), 42);
327 }
328
329 #[test]
330 fn zero_returns_none() {
331 assert!(TestId::new(0).is_none());
332 }
333
334 #[test]
335 fn try_from_u64_zero_is_error() {
336 let result = TestId::try_from(0u64);
337 assert!(result.is_err());
338 assert!(matches!(result.unwrap_err(), IdParseError::Zero));
339 }
340
341 #[test]
342 fn try_from_u64_nonzero_succeeds() {
343 let id = TestId::try_from(42u64).unwrap();
344 assert_eq!(id.get(), 42);
345 }
346
347 #[test]
348 fn from_non_zero_roundtrips() {
349 let nz = NonZeroU64::new(7).unwrap();
350 let id = TestId::from_non_zero(nz);
351 assert_eq!(id.get(), 7);
352 assert_eq!(id.non_zero(), nz);
353 }
354
355 #[test]
356 fn debug_shows_domain_and_value() {
357 let id = TestId::new(42).unwrap();
358 assert_eq!(format!("{id:?}"), "test(42)");
359 }
360
361 #[test]
362 fn domain_returns_name() {
363 let id = TestId::new(1).unwrap();
364 assert_eq!(id.domain(), "test");
365 }
366
367 #[test]
368 fn display_shows_numeric_value() {
369 let id = TestId::new(12345).unwrap();
370 assert_eq!(id.to_string(), "12345");
371 }
372
373 #[test]
374 fn parse_valid_string() {
375 let id: TestId = "42".parse().unwrap();
376 assert_eq!(id.get(), 42);
377 }
378
379 #[test]
380 fn parse_zero_string_is_error() {
381 let result: Result<TestId, _> = "0".parse();
382 assert!(result.is_err());
383 }
384
385 #[test]
386 fn parse_non_numeric_string_is_error() {
387 let result: Result<TestId, _> = "not_a_number".parse();
388 assert!(result.is_err());
389 }
390
391 #[test]
392 fn into_from_non_zero_u64_roundtrips() {
393 let nz = NonZeroU64::new(100).unwrap();
394 let id: TestId = nz.into();
395 assert_eq!(id.get(), 100);
396 }
397
398 #[test]
399 fn into_non_zero_u64_preserves_value() {
400 let id = TestId::new(99).unwrap();
401 let nz: NonZeroU64 = id.into();
402 assert_eq!(nz.get(), 99);
403 }
404
405 #[test]
406 fn into_u64_preserves_value() {
407 let id = TestId::new(99).unwrap();
408 let value: u64 = id.into();
409 assert_eq!(value, 99);
410 }
411
412 #[test]
413 fn try_from_str_succeeds() {
414 let id = TestId::try_from("7").unwrap();
415 assert_eq!(id.get(), 7);
416 }
417
418 #[test]
419 fn try_from_string_succeeds() {
420 let id = TestId::try_from(String::from("123")).unwrap();
421 assert_eq!(id.get(), 123);
422 }
423
424 #[test]
425 fn id_is_copy() {
426 let id1 = TestId::new(5).unwrap();
427 let id2 = id1; assert_eq!(id1, id2); }
430
431 #[test]
432 fn ordering_follows_numeric_value() {
433 let a = TestId::new(1).unwrap();
434 let b = TestId::new(2).unwrap();
435 assert!(a < b);
436 }
437
438 #[cfg(feature = "std")]
439 #[test]
440 fn equal_ids_produce_same_hash() {
441 use core::hash::{Hash, Hasher};
442 let id1 = TestId::new(42).unwrap();
443 let id2 = TestId::new(42).unwrap();
444
445 let hash = |id: &TestId| {
446 let mut hasher = std::collections::hash_map::DefaultHasher::new();
447 id.hash(&mut hasher);
448 hasher.finish()
449 };
450
451 assert_eq!(hash(&id1), hash(&id2));
452 }
453
454 #[test]
455 fn max_u64_is_valid_id() {
456 let id = TestId::new(u64::MAX).unwrap();
457 assert_eq!(id.get(), u64::MAX);
458 }
459
460 #[test]
461 fn option_id_has_no_size_overhead() {
462 assert_eq!(
463 core::mem::size_of::<Option<TestId>>(),
464 core::mem::size_of::<TestId>()
465 );
466 }
467
468 #[cfg(feature = "serde")]
469 #[test]
470 fn serde_roundtrip_preserves_id() {
471 let id = TestId::new(42).unwrap();
472 let json = serde_json::to_string(&id).unwrap();
473 assert_eq!(json, "42");
474 let deserialized: TestId = serde_json::from_str(&json).unwrap();
475 assert_eq!(id, deserialized);
476 }
477
478 #[cfg(feature = "serde")]
479 #[test]
480 fn serde_rejects_zero() {
481 let result: Result<TestId, _> = serde_json::from_str("0");
482 assert!(result.is_err());
483 }
484
485 #[test]
486 fn different_domains_are_distinct_types() {
487 #[derive(Debug)]
488 struct DomainA;
489 impl crate::Domain for DomainA {
490 const DOMAIN_NAME: &'static str = "a";
491 }
492 impl IdDomain for DomainA {}
493
494 #[derive(Debug)]
495 struct DomainB;
496 impl crate::Domain for DomainB {
497 const DOMAIN_NAME: &'static str = "b";
498 }
499 impl IdDomain for DomainB {}
500
501 let _a: Id<DomainA> = Id::new(1).unwrap();
502 let _b: Id<DomainB> = Id::new(1).unwrap();
503
504 }
507}