Skip to main content

armour_core/
fuid.rs

1use std::{
2    cell::Cell,
3    fmt::Debug,
4    marker::PhantomData,
5    str::FromStr,
6    sync::atomic::{AtomicU8, Ordering},
7    time::{Duration, SystemTime},
8};
9
10use bytemuck::{Pod, TransparentWrapper, Zeroable};
11use rapira::{Rapira, RapiraError};
12use rend::u64_be;
13use serde::{Deserialize, Deserializer, Serialize, Serializer};
14use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
15
16use crate::{
17    IdStr, Typ,
18    enc::IdHasher,
19    error::ArmourError,
20    get_type::GetType,
21    key_part::{KeyPart, MILLISECOND_BITS},
22    key_type::KeyType,
23    num_ops::g8bits,
24};
25
26type Result<T, E = ArmourError> = core::result::Result<T, E>;
27
28/// different from the unix timestamp epoch in milliseconds
29///
30/// 2023-04-09 0:00:00 UTC
31const TS_DIFF: u64 = 0x187_6350_0000;
32
33const TS_BITS: u32 = 40;
34const TS_MAX: u64 = (1 << TS_BITS) - 1;
35// 64 - 40 = 24
36const TS_SHIFT: u32 = u64::BITS - TS_BITS;
37
38// 2^8 = 256
39pub(crate) const SHARD_BITS: u32 = 8;
40// 255
41pub const SHARD_ID_MAX: u64 = (1 << SHARD_BITS) - 1;
42
43/// 16 bits
44const SEQ_BITS: u32 = u64::BITS - TS_BITS - SHARD_BITS;
45/// 65_535 max sequence
46const SEQ_MAX: u64 = (1 << SEQ_BITS) - 1;
47
48// 2^3 = 8
49const SHARD_INST_BITS: u32 = 3;
50// 7
51pub const SHARD_INSTANCE_ID_MAX: u64 = (1 << SHARD_INST_BITS) - 1;
52// 5 + 16 = 21
53const SHARD_INSTANCE_SHIFT: u32 = SHARD_THREAD_BITS + SEQ_BITS;
54
55// 2^5 = 32
56pub const SHARD_THREAD_BITS: u32 = 5;
57// 31
58pub const SHARD_THREAD_ID_MAX: u64 = (1 << SHARD_THREAD_BITS) - 1;
59
60#[derive(Debug, Clone, Copy)]
61struct SeqForMs {
62    /// timestamp in milliseconds
63    ts: u64,
64    seq: u64,
65    count: u64,
66}
67
68impl SeqForMs {
69    #[cfg(feature = "std")]
70    #[inline]
71    fn with_ts_and_rand_seq(ts: u64) -> Self {
72        use rand::{RngExt, rng};
73
74        let seq = rng().random_range(0..=SEQ_MAX);
75
76        SeqForMs { ts, seq, count: 0 }
77    }
78
79    #[cold]
80    fn wait_inc(self, ts: u64, duration: Duration) -> Self {
81        tracing::info!(?duration, "wait_inc");
82        let nanos = duration.subsec_nanos() % 1_000_000;
83        let wait_nanos = 1_000_000 - nanos;
84        std::thread::sleep(Duration::from_nanos(wait_nanos as u64));
85
86        SeqForMs::with_ts_and_rand_seq(ts + 1)
87    }
88
89    #[inline]
90    fn increment(mut self, ts: u64, duration: Duration) -> Self {
91        if self.count == SEQ_MAX {
92            self.wait_inc(ts, duration)
93        } else {
94            if self.seq == SEQ_MAX {
95                self.seq = 0;
96            } else {
97                self.seq += 1;
98            }
99            self.count += 1;
100            self
101        }
102    }
103}
104
105static THREAD_SEQ: AtomicU8 = AtomicU8::new(0);
106
107thread_local! {
108    static SEQ_CHECK: Cell<SeqForMs> = Cell::new(SeqForMs::with_ts_and_rand_seq(0));
109    static THREAD_ID: Cell<u8> = Cell::new(THREAD_SEQ.fetch_add(1, Ordering::Relaxed));
110}
111
112/// - 64bit id [--timestamp(40)--|--shard_id(8)--|--seq_id(16)--]
113/// - 40 bit - timestamp (in milliseconds) started from 2023-04-09 0:00:00 UTC, 34 years available (from 2023 to 2057)
114/// - 8 bit - shard_id ([datacenter_id / instance_id] with thread_id), 256 values
115/// - 16 bit - random sequence id (unique for thread / per thread), 65_536 values
116/// - Blowfish for encryption
117/// - ZBASE32 for encoding into 13 length string (example: "ifs1gp9dw8hdw")
118/// - create ~ 50ns (~ 21k in 1ms or 21m in 1s) in 1 thread
119///
120/// Notes:
121/// - checks timestamp and shard_id when deserialized from string
122/// - Fuid is not inlined for InlineArray
123#[derive(Hash, IntoBytes, FromBytes, Immutable, KnownLayout, TransparentWrapper)]
124#[cfg_attr(
125    feature = "rkyv",
126    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
127)]
128#[cfg_attr(feature = "bitcode", derive(bitcode::Encode, bitcode::Decode))]
129#[repr(transparent)]
130#[transparent(u64_be)]
131pub struct Fuid<H>(pub u64_be, PhantomData<H>);
132
133unsafe impl<T> Zeroable for Fuid<T> {}
134unsafe impl<T: 'static> Pod for Fuid<T> {}
135
136impl<T> Clone for Fuid<T> {
137    fn clone(&self) -> Self {
138        *self
139    }
140}
141
142impl<T> Copy for Fuid<T> {}
143
144impl<H> Fuid<H> {
145    pub fn from_thread() -> Self {
146        let thread_id = THREAD_ID.get();
147        Self::with_shard(thread_id as u64)
148    }
149
150    /// - 3 bit - instance id (datacenter id / shard id) (max 7)
151    /// - 5 bit - thread id (max 31)
152    /// - instance_id (datacenter) < 8 and thread_id (worker) < 32
153    #[cfg(feature = "std")]
154    #[inline]
155    pub fn with_inst_thread(instance_id: u64, thread_id: u64) -> Self {
156        assert!(instance_id <= SHARD_INSTANCE_ID_MAX);
157        assert!(thread_id <= SHARD_THREAD_ID_MAX);
158
159        let shard_id = (instance_id << SHARD_THREAD_BITS) | thread_id;
160        Self::with_shard(shard_id)
161    }
162
163    /// Create a new Fuid.
164    /// - shard_id < 256
165    #[cfg(feature = "std")]
166    pub fn with_shard(shard_id: u64) -> Self {
167        assert!(shard_id <= SHARD_ID_MAX);
168
169        let now = SystemTime::now();
170        // in ms
171        #[allow(clippy::unwrap_used)]
172        let duration = now.duration_since(SystemTime::UNIX_EPOCH).unwrap();
173        let timestamp = duration.as_millis() as u64;
174
175        let new_epoch_ts = timestamp - TS_DIFF;
176
177        let id = new_epoch_ts << TS_SHIFT;
178
179        let id = id | (shard_id << SEQ_BITS);
180
181        let mut seq_check = SEQ_CHECK.get();
182
183        if seq_check.ts == timestamp {
184            seq_check = seq_check.increment(timestamp, duration);
185        } else {
186            seq_check = SeqForMs::with_ts_and_rand_seq(timestamp);
187        }
188        SEQ_CHECK.set(seq_check);
189
190        let id = id | seq_check.seq;
191
192        Self(u64_be::from_native(id), PhantomData)
193    }
194
195    /// unix timestamp in milliseconds
196    #[inline]
197    pub fn timestamp(&self) -> u64 {
198        let ts = self.0.to_native() >> TS_SHIFT;
199        ts + TS_DIFF
200    }
201
202    pub fn date(&self) -> time::OffsetDateTime {
203        let ts = self.timestamp();
204        let ts = ts as i128;
205        let ts = ts * 1_000_000;
206        let dt = time::OffsetDateTime::from_unix_timestamp_nanos(ts);
207        dt.expect("invalid timestamp")
208    }
209
210    /// get LE u64
211    #[inline]
212    pub fn get(&self) -> u64 {
213        self.0.to_native()
214    }
215
216    #[inline]
217    pub fn to_be_bytes(self) -> [u8; 8] {
218        zerocopy::transmute!(self)
219    }
220
221    #[inline]
222    pub fn from_be_bytes(bytes: [u8; 8]) -> Self {
223        zerocopy::transmute!(bytes)
224    }
225
226    /// to BE bytes
227    #[inline]
228    pub fn to_bytes(self) -> [u8; 8] {
229        zerocopy::transmute!(self)
230    }
231
232    /// from BE bytes
233    #[inline]
234    pub fn from_bytes(bytes: [u8; 8]) -> Self {
235        zerocopy::transmute!(bytes)
236    }
237
238    /// to BE u64
239    #[inline]
240    pub fn to_u64(self) -> u64 {
241        zerocopy::transmute!(self)
242    }
243
244    /// from BE u64
245    #[inline]
246    pub fn from_u64(bytes: u64) -> Self {
247        zerocopy::transmute!(bytes)
248    }
249
250    #[inline]
251    pub fn to_le_bytes(self) -> [u8; 8] {
252        self.0.to_native().to_le_bytes()
253    }
254
255    #[inline]
256    pub fn instance_id(&self) -> u64 {
257        let ts = self.0.to_native() >> SHARD_INSTANCE_SHIFT;
258        ts & SHARD_INSTANCE_ID_MAX
259    }
260
261    #[inline]
262    pub fn thread_id(&self) -> u64 {
263        let ts = self.0.to_native() >> SEQ_BITS;
264        ts & SHARD_THREAD_ID_MAX
265    }
266
267    #[inline]
268    pub fn shard_id(&self) -> u64 {
269        let ts = self.0.to_native() >> SEQ_BITS;
270        ts & SHARD_ID_MAX
271    }
272
273    #[inline]
274    fn seq(&self) -> u64 {
275        let ts = self.0.to_native();
276        ts & SEQ_MAX
277    }
278
279    /// id: {id:#x}; {id:#b}; {dt}-{instance_id}-{thread_id}-{seq}
280    #[cfg(feature = "std")]
281    pub fn format<W>(&self, w: &mut W) -> core::result::Result<(), core::fmt::Error>
282    where
283        W: std::fmt::Write,
284    {
285        let id = self.0.to_native();
286        let dt = self.date();
287        let instance_id = self.instance_id();
288        let thread_id = self.thread_id();
289        let seq = self.seq();
290        write!(
291            w,
292            "id: {id:#x}; {id:#b}; {dt}-{instance_id}-{thread_id}-{seq}"
293        )
294    }
295
296    /// return prefix for date [--timestamp(5)--|0x00(1)|0x0000(2)]
297    /// ms - unix timestamp in milliseconds
298    pub fn date_prefix(ms: u64) -> [u8; 8] {
299        let ts = (ms - TS_DIFF) << TS_SHIFT;
300        ts.to_be_bytes()
301    }
302
303    pub fn check(&self) -> core::result::Result<(), ArmourError> {
304        let ts = self.0.to_native() >> TS_SHIFT;
305        if ts > TS_MAX {
306            return Err(ArmourError::IdDecodeError);
307        }
308        Ok(())
309    }
310
311    /// only for iterator with excluded bounds
312    pub fn increment(mut self) -> Self {
313        self.0 += 1;
314        self
315    }
316
317    /// group_id for this Fuid (12 days one group)
318    #[inline]
319    pub fn group_id(&self) -> u32 {
320        g8bits(self.0.to_native(), MILLISECOND_BITS)
321    }
322}
323
324impl<H: IdHasher> Fuid<H> {
325    pub fn ser(self) -> IdStr {
326        let u: u64 = zerocopy::transmute!(self);
327        H::ser(u)
328    }
329
330    pub fn deser(id: &str) -> Result<Self> {
331        let id = H::deser(id)?;
332        Ok(zerocopy::transmute!(id))
333    }
334}
335
336impl Fuid<()> {
337    pub fn ser(self) -> IdStr {
338        let u: [u8; 8] = zerocopy::transmute!(self);
339        crate::enc::encode(&u)
340    }
341
342    pub fn deser(id: &str) -> Result<Self> {
343        let id = crate::enc::decode(id)?;
344        Ok(zerocopy::transmute!(id))
345    }
346}
347
348impl<T> PartialOrd for Fuid<T> {
349    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
350        Some(self.cmp(other))
351    }
352}
353
354impl<T> Ord for Fuid<T> {
355    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
356        self.0.cmp(&other.0)
357    }
358}
359
360impl<T> PartialEq for Fuid<T> {
361    fn eq(&self, other: &Self) -> bool {
362        self.0 == other.0
363    }
364}
365
366impl<T> Eq for Fuid<T> {}
367
368impl<T> PartialEq<[u8; 8]> for Fuid<T> {
369    fn eq(&self, other: &[u8; 8]) -> bool {
370        let bytes: &[u8; 8] = zerocopy::transmute_ref!(self);
371        bytes == other
372    }
373}
374
375impl<T> PartialEq<Fuid<T>> for [u8; 8] {
376    fn eq(&self, other: &Fuid<T>) -> bool {
377        let bytes: &[u8; 8] = zerocopy::transmute_ref!(other);
378        bytes == self
379    }
380}
381
382impl<T> PartialEq<Fuid<T>> for Option<Fuid<T>> {
383    fn eq(&self, other: &Fuid<T>) -> bool {
384        match self {
385            Some(id) => id == other,
386            None => false,
387        }
388    }
389}
390
391impl<T> PartialEq<Option<Fuid<T>>> for Fuid<T> {
392    fn eq(&self, other: &Option<Fuid<T>>) -> bool {
393        match other {
394            Some(id) => self == id,
395            None => false,
396        }
397    }
398}
399
400impl<T> AsRef<Self> for Fuid<T> {
401    fn as_ref(&self) -> &Self {
402        self
403    }
404}
405
406impl<T> AsRef<[u8; 8]> for Fuid<T> {
407    fn as_ref(&self) -> &[u8; 8] {
408        zerocopy::transmute_ref!(self)
409    }
410}
411
412/// only check if non-zero, not check valid byte mask
413impl<H: IdHasher> Rapira for Fuid<H> {
414    const STATIC_SIZE: Option<usize> = Some(8);
415    const MIN_SIZE: usize = 8;
416
417    #[inline]
418    fn size(&self) -> usize {
419        8
420    }
421
422    #[inline]
423    fn check_bytes(slice: &mut &[u8]) -> rapira::Result<()> {
424        let bytes: &[u8] = slice.get(..8).ok_or(RapiraError::SliceLen)?;
425
426        const ZERO_BYTES: [u8; 8] = [0u8; 8];
427
428        if bytes == ZERO_BYTES {
429            return Err(RapiraError::NonZero);
430        }
431
432        *slice = unsafe { slice.get_unchecked(8..) };
433        Ok(())
434    }
435
436    #[inline]
437    fn from_slice(slice: &mut &[u8]) -> rapira::Result<Self>
438    where
439        Self: Sized,
440    {
441        let bytes = <[u8; 8]>::from_slice(slice)?;
442        let id = Self::from_bytes(bytes);
443        Ok(id)
444    }
445
446    #[inline]
447    fn convert_to_bytes(&self, slice: &mut [u8], cursor: &mut usize) {
448        let bytes: &[u8; 8] = zerocopy::transmute_ref!(self);
449        bytes.convert_to_bytes(slice, cursor);
450    }
451
452    #[inline]
453    fn convert_to_bytes_ctx(
454        &self,
455        slice: &mut [u8],
456        cursor: &mut usize,
457        flags: rapira::RapiraFlags,
458    ) {
459        if flags.has(crate::ID_ENC_FLAG) {
460            let u: u64 = zerocopy::transmute!(*self);
461            let id = H::encrypt(u);
462            id.convert_to_bytes(slice, cursor);
463        } else {
464            self.convert_to_bytes(slice, cursor)
465        }
466    }
467
468    #[inline]
469    fn from_slice_ctx(slice: &mut &[u8], flags: rapira::RapiraFlags) -> rapira::Result<Self>
470    where
471        Self: Sized,
472    {
473        if flags.has(crate::ID_ENC_FLAG) {
474            let id = u64::from_slice(slice)?;
475            let id = H::decrypt(id);
476            Ok(zerocopy::transmute!(id))
477        } else {
478            Self::from_slice(slice)
479        }
480    }
481}
482
483impl Rapira for Fuid<()> {
484    const STATIC_SIZE: Option<usize> = Some(8);
485    const MIN_SIZE: usize = 8;
486
487    #[inline]
488    fn size(&self) -> usize {
489        8
490    }
491
492    #[inline]
493    fn check_bytes(slice: &mut &[u8]) -> rapira::Result<()> {
494        let bytes: &[u8] = slice.get(..8).ok_or(RapiraError::SliceLen)?;
495
496        const ZERO_BYTES: [u8; 8] = [0u8; 8];
497
498        if bytes == ZERO_BYTES {
499            return Err(RapiraError::NonZero);
500        }
501
502        *slice = unsafe { slice.get_unchecked(8..) };
503        Ok(())
504    }
505
506    #[inline]
507    fn from_slice(slice: &mut &[u8]) -> rapira::Result<Self>
508    where
509        Self: Sized,
510    {
511        let bytes = <[u8; 8]>::from_slice(slice)?;
512        let id = Self::from_bytes(bytes);
513        Ok(id)
514    }
515
516    #[inline]
517    fn convert_to_bytes(&self, slice: &mut [u8], cursor: &mut usize) {
518        let bytes: &[u8; 8] = zerocopy::transmute_ref!(self);
519        bytes.convert_to_bytes(slice, cursor);
520    }
521}
522
523impl<H> GetType for Fuid<H> {
524    const TYPE: Typ = Typ::Fuid;
525}
526
527impl<H> KeyPart for Fuid<H> {
528    const TY: KeyType = KeyType::Fuid;
529    const PREFIX_BITS: u32 = MILLISECOND_BITS;
530}
531
532impl<H: IdHasher> std::fmt::Display for Fuid<H> {
533    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
534        write!(f, "{}", self.ser())
535    }
536}
537
538impl std::fmt::Display for Fuid<()> {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        write!(f, "{}", self.ser())
541    }
542}
543
544impl<H> Debug for Fuid<H> {
545    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
546        let id = self.0.to_native();
547        let id = format!("{id:#X}");
548        f.debug_tuple("Fuid").field(&id).finish()
549    }
550}
551
552impl<T: IdHasher> TryFrom<&str> for Fuid<T> {
553    type Error = ArmourError;
554    fn try_from(val: &str) -> Result<Self> {
555        Self::deser(val)
556    }
557}
558
559impl TryFrom<&str> for Fuid<()> {
560    type Error = ArmourError;
561    fn try_from(val: &str) -> Result<Self> {
562        Self::deser(val)
563    }
564}
565
566impl<T: IdHasher> FromStr for Fuid<T> {
567    type Err = ArmourError;
568    fn from_str(s: &str) -> Result<Self> {
569        Self::deser(s)
570    }
571}
572
573impl FromStr for Fuid<()> {
574    type Err = ArmourError;
575    fn from_str(s: &str) -> Result<Self> {
576        Self::deser(s)
577    }
578}
579
580#[cfg(feature = "std")]
581impl<T: IdHasher> Serialize for Fuid<T> {
582    fn serialize<S: Serializer>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error> {
583        let s = self.ser();
584        serializer.serialize_str(&s)
585    }
586}
587
588#[cfg(feature = "std")]
589impl<'de, T: IdHasher> Deserialize<'de> for Fuid<T> {
590    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
591    where
592        D: Deserializer<'de>,
593    {
594        use serde::de::Error;
595
596        let s: &str = Deserialize::deserialize(deserializer)?;
597        let a = Fuid::<T>::deser(s).map_err(|err| {
598            tracing::error!("id value error: {err}");
599            D::Error::custom("id value error")
600        })?;
601        Ok(a)
602    }
603}
604
605#[cfg(feature = "std")]
606impl Serialize for Fuid<()> {
607    fn serialize<S: Serializer>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error> {
608        let s = self.ser();
609        serializer.serialize_str(&s)
610    }
611}
612
613#[cfg(feature = "std")]
614impl<'de> Deserialize<'de> for Fuid<()> {
615    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
616    where
617        D: Deserializer<'de>,
618    {
619        use serde::de::Error;
620
621        let s: &str = Deserialize::deserialize(deserializer)?;
622        let a = Fuid::<()>::deser(s).map_err(|err| {
623            tracing::error!("id value error: {err}");
624            D::Error::custom("id value error")
625        })?;
626        Ok(a)
627    }
628}
629
630#[cfg(feature = "ts-rs")]
631impl<H> ts_rs::TS for Fuid<H> {
632    type WithoutGenerics = Fuid<()>;
633    type OptionInnerType = Self;
634    fn name(_: &ts_rs::Config) -> String {
635        "Fuid".to_owned()
636    }
637    fn decl_concrete(c: &ts_rs::Config) -> String {
638        format!("type Fuid = {};", Self::inline(c))
639    }
640    fn decl(c: &ts_rs::Config) -> String {
641        let inline = <Fuid<()> as ::ts_rs::TS>::inline(c);
642        format!("type Fuid = {inline};")
643    }
644    fn inline(_: &ts_rs::Config) -> String {
645        "string".to_owned()
646    }
647    fn inline_flattened(c: &ts_rs::Config) -> String {
648        panic!("{} cannot be flattened", Self::name(c))
649    }
650    fn output_path() -> Option<std::path::PathBuf> {
651        Some(std::path::PathBuf::from("fuid.ts"))
652    }
653}
654
655#[cfg(feature = "facet")]
656unsafe impl<'facet, H: 'static> facet::Facet<'facet> for Fuid<H> {
657    const SHAPE: &'static facet::Shape = &const {
658        const VTABLE: facet::VTableDirect = facet::vtable_direct!(Fuid<()> =>
659            Debug,
660            Hash,
661            PartialEq,
662            PartialOrd,
663            Ord,
664        );
665
666        facet::ShapeBuilder::for_sized::<Fuid<H>>("Fuid")
667            .ty(facet::Type::User(facet::UserType::Struct(facet::StructType {
668                repr: facet::Repr::transparent(),
669                kind: facet::StructKind::TupleStruct,
670                fields: &const {
671                    [facet::FieldBuilder::new("0", facet::shape_of::<u64>, 0).build()]
672                },
673            })))
674            .inner(<u64 as facet::Facet>::SHAPE)
675            .def(facet::Def::Scalar)
676            .vtable_direct(&VTABLE)
677            .eq()
678            .copy()
679            .send()
680            .sync()
681            .build()
682    };
683}
684
685#[cfg(feature = "fake")]
686impl<H, T> fake::Dummy<T> for Fuid<H> {
687    fn dummy_with_rng<R: rand::Rng + ?Sized>(_: &T, _: &mut R) -> Self {
688        Fuid::from_thread()
689    }
690}
691
692// ---------------------------------------------------------------------------
693// OptFuid
694// ---------------------------------------------------------------------------
695
696/// Optional [`Fuid`] with the same 8-byte representation.
697///
698/// Zero (`[0u8; 8]`) is interpreted as `None`. Use in zerocopy-compatible
699/// structs where `Option<Fuid>` would break alignment (16+ bytes).
700///
701/// ```ignore
702/// #[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
703/// #[repr(C)]
704/// struct Follower {
705///     follow: Fuid<H>,       // 8 bytes
706///     follower: OptFuid<H>,  // 8 bytes, zero = None
707/// }
708/// ```
709#[derive(IntoBytes, FromBytes, Immutable, KnownLayout, TransparentWrapper)]
710#[cfg_attr(
711    feature = "rkyv",
712    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
713)]
714#[cfg_attr(feature = "bitcode", derive(bitcode::Encode, bitcode::Decode))]
715#[repr(transparent)]
716#[transparent(u64_be)]
717pub struct OptFuid<H>(u64_be, PhantomData<H>);
718
719unsafe impl<T> Zeroable for OptFuid<T> {}
720unsafe impl<T: 'static> Pod for OptFuid<T> {}
721
722impl<T> Clone for OptFuid<T> {
723    fn clone(&self) -> Self {
724        *self
725    }
726}
727
728impl<T> Copy for OptFuid<T> {}
729
730impl<H> OptFuid<H> {
731    // SAFETY: OptFuid is Zeroable — all-zero bytes is valid and means None.
732    pub const NONE: Self = unsafe { core::mem::zeroed() };
733
734    #[inline]
735    pub fn some(id: Fuid<H>) -> Self {
736        Self(id.0, PhantomData)
737    }
738
739    #[inline]
740    pub fn get(self) -> Option<Fuid<H>> {
741        if self.0.to_native() == 0 {
742            None
743        } else {
744            Some(Fuid(self.0, PhantomData))
745        }
746    }
747
748    #[inline]
749    pub fn is_some(self) -> bool {
750        self.0.to_native() != 0
751    }
752
753    #[inline]
754    pub fn is_none(self) -> bool {
755        self.0.to_native() == 0
756    }
757
758    /// get raw LE u64 (0 for None)
759    #[inline]
760    pub fn get_raw(self) -> u64 {
761        self.0.to_native()
762    }
763
764    /// to BE bytes
765    #[inline]
766    pub fn to_bytes(self) -> [u8; 8] {
767        zerocopy::transmute!(self)
768    }
769
770    /// from BE bytes
771    #[inline]
772    pub fn from_bytes(bytes: [u8; 8]) -> Self {
773        zerocopy::transmute!(bytes)
774    }
775
776    #[inline]
777    pub fn to_be_bytes(self) -> [u8; 8] {
778        zerocopy::transmute!(self)
779    }
780
781    #[inline]
782    pub fn from_be_bytes(bytes: [u8; 8]) -> Self {
783        zerocopy::transmute!(bytes)
784    }
785}
786
787impl<T> Default for OptFuid<T> {
788    fn default() -> Self {
789        Self::NONE
790    }
791}
792
793impl<H> From<Fuid<H>> for OptFuid<H> {
794    fn from(id: Fuid<H>) -> Self {
795        Self::some(id)
796    }
797}
798
799impl<H> From<Option<Fuid<H>>> for OptFuid<H> {
800    fn from(opt: Option<Fuid<H>>) -> Self {
801        match opt {
802            Some(id) => Self::some(id),
803            None => Self::NONE,
804        }
805    }
806}
807
808impl<H> From<OptFuid<H>> for Option<Fuid<H>> {
809    fn from(opt: OptFuid<H>) -> Self {
810        opt.get()
811    }
812}
813
814impl<T> PartialEq for OptFuid<T> {
815    fn eq(&self, other: &Self) -> bool {
816        self.0 == other.0
817    }
818}
819
820impl<T> Eq for OptFuid<T> {}
821
822impl<T> PartialOrd for OptFuid<T> {
823    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
824        Some(self.cmp(other))
825    }
826}
827
828impl<T> Ord for OptFuid<T> {
829    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
830        self.0.cmp(&other.0)
831    }
832}
833
834impl<T> core::hash::Hash for OptFuid<T> {
835    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
836        self.0.hash(state);
837    }
838}
839
840impl<T> PartialEq<Fuid<T>> for OptFuid<T> {
841    fn eq(&self, other: &Fuid<T>) -> bool {
842        self.0 == other.0
843    }
844}
845
846impl<T> PartialEq<OptFuid<T>> for Fuid<T> {
847    fn eq(&self, other: &OptFuid<T>) -> bool {
848        self.0 == other.0
849    }
850}
851
852impl<T> AsRef<[u8; 8]> for OptFuid<T> {
853    fn as_ref(&self) -> &[u8; 8] {
854        zerocopy::transmute_ref!(self)
855    }
856}
857
858impl<H> Debug for OptFuid<H> {
859    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
860        match self.0.to_native() {
861            0 => write!(f, "OptFuid(None)"),
862            id => {
863                let id = format!("{id:#X}");
864                f.debug_tuple("OptFuid").field(&id).finish()
865            }
866        }
867    }
868}
869
870impl<H: IdHasher> Rapira for OptFuid<H> {
871    const STATIC_SIZE: Option<usize> = Some(8);
872    const MIN_SIZE: usize = 8;
873
874    #[inline]
875    fn size(&self) -> usize {
876        8
877    }
878
879    #[inline]
880    fn check_bytes(slice: &mut &[u8]) -> rapira::Result<()> {
881        if slice.len() < 8 {
882            return Err(RapiraError::SliceLen);
883        }
884        *slice = unsafe { slice.get_unchecked(8..) };
885        Ok(())
886    }
887
888    #[inline]
889    fn from_slice(slice: &mut &[u8]) -> rapira::Result<Self>
890    where
891        Self: Sized,
892    {
893        let bytes = <[u8; 8]>::from_slice(slice)?;
894        Ok(Self::from_bytes(bytes))
895    }
896
897    #[inline]
898    fn convert_to_bytes(&self, slice: &mut [u8], cursor: &mut usize) {
899        let bytes: &[u8; 8] = zerocopy::transmute_ref!(self);
900        bytes.convert_to_bytes(slice, cursor);
901    }
902
903    #[inline]
904    fn convert_to_bytes_ctx(
905        &self,
906        slice: &mut [u8],
907        cursor: &mut usize,
908        flags: rapira::RapiraFlags,
909    ) {
910        if flags.has(crate::ID_ENC_FLAG) && self.is_some() {
911            let u: u64 = zerocopy::transmute!(*self);
912            let id = H::encrypt(u);
913            id.convert_to_bytes(slice, cursor);
914        } else {
915            self.convert_to_bytes(slice, cursor)
916        }
917    }
918
919    #[inline]
920    fn from_slice_ctx(slice: &mut &[u8], flags: rapira::RapiraFlags) -> rapira::Result<Self>
921    where
922        Self: Sized,
923    {
924        if flags.has(crate::ID_ENC_FLAG) {
925            let bytes = <[u8; 8]>::from_slice(slice)?;
926            if bytes == [0u8; 8] {
927                Ok(Self::NONE)
928            } else {
929                let id = u64::from_le_bytes(bytes);
930                let id = H::decrypt(id);
931                Ok(zerocopy::transmute!(id))
932            }
933        } else {
934            Self::from_slice(slice)
935        }
936    }
937}
938
939impl Rapira for OptFuid<()> {
940    const STATIC_SIZE: Option<usize> = Some(8);
941    const MIN_SIZE: usize = 8;
942
943    #[inline]
944    fn size(&self) -> usize {
945        8
946    }
947
948    #[inline]
949    fn check_bytes(slice: &mut &[u8]) -> rapira::Result<()> {
950        if slice.len() < 8 {
951            return Err(RapiraError::SliceLen);
952        }
953        *slice = unsafe { slice.get_unchecked(8..) };
954        Ok(())
955    }
956
957    #[inline]
958    fn from_slice(slice: &mut &[u8]) -> rapira::Result<Self>
959    where
960        Self: Sized,
961    {
962        let bytes = <[u8; 8]>::from_slice(slice)?;
963        Ok(Self::from_bytes(bytes))
964    }
965
966    #[inline]
967    fn convert_to_bytes(&self, slice: &mut [u8], cursor: &mut usize) {
968        let bytes: &[u8; 8] = zerocopy::transmute_ref!(self);
969        bytes.convert_to_bytes(slice, cursor);
970    }
971}
972
973impl<H> GetType for OptFuid<H> {
974    const TYPE: Typ = Typ::Fuid;
975}
976
977#[cfg(feature = "std")]
978impl<H: IdHasher> Serialize for OptFuid<H> {
979    fn serialize<S: Serializer>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error> {
980        match self.get() {
981            Some(id) => serializer.serialize_str(&id.ser()),
982            None => serializer.serialize_none(),
983        }
984    }
985}
986
987#[cfg(feature = "std")]
988impl<'de, H: IdHasher> Deserialize<'de> for OptFuid<H> {
989    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
990    where
991        D: Deserializer<'de>,
992    {
993        use serde::de::Error;
994        let s: Option<&str> = Deserialize::deserialize(deserializer)?;
995        match s {
996            Some(s) => {
997                let id = Fuid::<H>::deser(s).map_err(|err| {
998                    tracing::error!("id value error: {err}");
999                    D::Error::custom("id value error")
1000                })?;
1001                Ok(Self::some(id))
1002            }
1003            None => Ok(Self::NONE),
1004        }
1005    }
1006}
1007
1008#[cfg(feature = "std")]
1009impl Serialize for OptFuid<()> {
1010    fn serialize<S: Serializer>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error> {
1011        match self.get() {
1012            Some(id) => serializer.serialize_str(&id.ser()),
1013            None => serializer.serialize_none(),
1014        }
1015    }
1016}
1017
1018#[cfg(feature = "std")]
1019impl<'de> Deserialize<'de> for OptFuid<()> {
1020    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
1021    where
1022        D: Deserializer<'de>,
1023    {
1024        use serde::de::Error;
1025        let s: Option<&str> = Deserialize::deserialize(deserializer)?;
1026        match s {
1027            Some(s) => {
1028                let id = Fuid::<()>::deser(s).map_err(|err| {
1029                    tracing::error!("id value error: {err}");
1030                    D::Error::custom("id value error")
1031                })?;
1032                Ok(Self::some(id))
1033            }
1034            None => Ok(Self::NONE),
1035        }
1036    }
1037}
1038
1039#[cfg(feature = "ts-rs")]
1040impl<H> ts_rs::TS for OptFuid<H> {
1041    type WithoutGenerics = OptFuid<()>;
1042    type OptionInnerType = Self;
1043    fn name(_: &ts_rs::Config) -> String {
1044        "OptFuid".to_owned()
1045    }
1046    fn decl_concrete(c: &ts_rs::Config) -> String {
1047        format!("type OptFuid = {};", Self::inline(c))
1048    }
1049    fn decl(c: &ts_rs::Config) -> String {
1050        let inline = <OptFuid<()> as ::ts_rs::TS>::inline(c);
1051        format!("type OptFuid = {inline};")
1052    }
1053    fn inline(_: &ts_rs::Config) -> String {
1054        "string | null".to_owned()
1055    }
1056    fn inline_flattened(c: &ts_rs::Config) -> String {
1057        panic!("{} cannot be flattened", Self::name(c))
1058    }
1059    fn output_path() -> Option<std::path::PathBuf> {
1060        Some(std::path::PathBuf::from("opt_fuid.ts"))
1061    }
1062}
1063
1064#[cfg(feature = "facet")]
1065unsafe impl<'facet, H: 'static> facet::Facet<'facet> for OptFuid<H> {
1066    const SHAPE: &'static facet::Shape = &const {
1067        const VTABLE: facet::VTableDirect = facet::vtable_direct!(OptFuid<()> =>
1068            Debug,
1069            Hash,
1070            PartialEq,
1071            PartialOrd,
1072            Ord,
1073        );
1074
1075        facet::ShapeBuilder::for_sized::<OptFuid<H>>("OptFuid")
1076            .ty(facet::Type::User(facet::UserType::Struct(facet::StructType {
1077                repr: facet::Repr::transparent(),
1078                kind: facet::StructKind::TupleStruct,
1079                fields: &const {
1080                    [facet::FieldBuilder::new("0", facet::shape_of::<u64>, 0).build()]
1081                },
1082            })))
1083            .inner(<u64 as facet::Facet>::SHAPE)
1084            .def(facet::Def::Scalar)
1085            .vtable_direct(&VTABLE)
1086            .eq()
1087            .copy()
1088            .send()
1089            .sync()
1090            .build()
1091    };
1092}
1093
1094#[cfg(feature = "fake")]
1095impl<H, T> fake::Dummy<T> for OptFuid<H> {
1096    fn dummy_with_rng<R: rand::Rng + ?Sized>(_: &T, _: &mut R) -> Self {
1097        Self::some(Fuid::from_thread())
1098    }
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103    use super::*;
1104    use crate::enc::Cipher;
1105
1106    #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
1107    pub struct Hasher;
1108
1109    impl IdHasher for Hasher {
1110        const HASHER: Cipher = Cipher::new(
1111            "_mKbKGF2IrkGvIJvl97HuCgWjgt6QRZ7Ye8DHBQ2anvyi18BdMz8uN6Ej3YJApooY6qDu0obqq4",
1112        );
1113    }
1114
1115    #[test]
1116    fn test_fuid_creation() {
1117        let fuid = Fuid::<Hasher>::with_inst_thread(1, 2);
1118        assert_eq!(fuid.instance_id(), 1);
1119        assert_eq!(fuid.thread_id(), 2);
1120
1121        let fuid = Fuid::<Hasher>::with_shard(55);
1122        assert_eq!(fuid.shard_id(), 55);
1123    }
1124
1125    #[test]
1126    fn test_fuid_ser_deser() {
1127        let fuid = Fuid::<Hasher>::with_shard(111);
1128        let id_str = fuid.ser();
1129        #[allow(clippy::unwrap_used)]
1130        let deserialized_fuid = Fuid::<Hasher>::deser(&id_str).unwrap();
1131        assert_eq!(fuid, deserialized_fuid);
1132    }
1133
1134    // -- OptFuid tests --
1135
1136    #[test]
1137    fn test_opt_fuid_none() {
1138        let opt = OptFuid::<Hasher>::NONE;
1139        assert!(opt.is_none());
1140        assert!(!opt.is_some());
1141        assert_eq!(opt.get(), None);
1142        assert_eq!(opt.to_bytes(), [0u8; 8]);
1143    }
1144
1145    #[test]
1146    fn test_opt_fuid_some() {
1147        let fuid = Fuid::<Hasher>::with_shard(1);
1148        let opt = OptFuid::some(fuid);
1149        assert!(opt.is_some());
1150        assert!(!opt.is_none());
1151        assert_eq!(opt.get(), Some(fuid));
1152    }
1153
1154    #[test]
1155    fn test_opt_fuid_conversions() {
1156        let fuid = Fuid::<Hasher>::with_shard(1);
1157
1158        let opt: OptFuid<Hasher> = fuid.into();
1159        assert_eq!(opt.get(), Some(fuid));
1160
1161        let opt: OptFuid<Hasher> = None.into();
1162        assert!(opt.is_none());
1163
1164        let opt: OptFuid<Hasher> = Some(fuid).into();
1165        let back: Option<Fuid<Hasher>> = opt.into();
1166        assert_eq!(back, Some(fuid));
1167    }
1168
1169    #[test]
1170    fn test_opt_fuid_size() {
1171        assert_eq!(size_of::<OptFuid<Hasher>>(), 8);
1172        assert_eq!(size_of::<OptFuid<Hasher>>(), size_of::<Fuid<Hasher>>());
1173    }
1174
1175    #[test]
1176    fn test_opt_fuid_default() {
1177        let opt = OptFuid::<Hasher>::default();
1178        assert!(opt.is_none());
1179    }
1180
1181    #[test]
1182    fn test_opt_fuid_eq_with_fuid() {
1183        let fuid = Fuid::<Hasher>::with_shard(1);
1184        let opt = OptFuid::some(fuid);
1185        assert_eq!(opt, fuid);
1186        assert_eq!(fuid, opt);
1187    }
1188
1189    // -- Rapira _ctx round-trip tests --
1190
1191    #[test]
1192    fn test_fuid_ctx_roundtrip() {
1193        let id = Fuid::<Hasher>::with_shard(1);
1194        let flags = rapira::RapiraFlags::new(crate::ID_ENC_FLAG);
1195
1196        let encrypted = rapira::serialize_ctx(&id, flags);
1197        let plain = rapira::serialize(&id);
1198        assert_ne!(encrypted, plain);
1199
1200        let decoded: Fuid<Hasher> = rapira::deserialize_ctx(&encrypted, flags).unwrap();
1201        assert_eq!(decoded, id);
1202    }
1203
1204    #[test]
1205    fn test_fuid_ctx_no_flag_same_as_plain() {
1206        let id = Fuid::<Hasher>::with_shard(1);
1207        let plain = rapira::serialize(&id);
1208        let ctx_none = rapira::serialize_ctx(&id, rapira::RapiraFlags::NONE);
1209        assert_eq!(plain, ctx_none);
1210    }
1211
1212    #[test]
1213    fn test_opt_fuid_ctx_none_roundtrip() {
1214        let opt = OptFuid::<Hasher>::NONE;
1215        let flags = rapira::RapiraFlags::new(crate::ID_ENC_FLAG);
1216        let bytes = rapira::serialize_ctx(&opt, flags);
1217        assert_eq!(bytes, [0u8; 8]);
1218        let decoded: OptFuid<Hasher> = rapira::deserialize_ctx(&bytes, flags).unwrap();
1219        assert!(decoded.is_none());
1220    }
1221
1222    #[test]
1223    fn test_opt_fuid_ctx_some_roundtrip() {
1224        let id = Fuid::<Hasher>::with_shard(1);
1225        let opt = OptFuid::some(id);
1226        let flags = rapira::RapiraFlags::new(crate::ID_ENC_FLAG);
1227
1228        let encrypted = rapira::serialize_ctx(&opt, flags);
1229        let plain = rapira::serialize(&opt);
1230        assert_ne!(encrypted, plain);
1231
1232        let decoded: OptFuid<Hasher> = rapira::deserialize_ctx(&encrypted, flags).unwrap();
1233        assert_eq!(decoded.get(), Some(id));
1234    }
1235}