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