tnid/
lib.rs

1//! # TNID
2//!
3//! **T**ype-checked, **N**amed **ID**s that are fully compatible with UUIDs.
4//!
5//! TNID ("Typed Named ID", pronounced "tee-nid") embeds a human-readable name directly into
6//! a UUID-compatible 128-bit identifier. Each TNID carries a 1-4 character type name (like
7//! "user" or "post") that's validated at compile time, preventing you from accidentally
8//! mixing up IDs for different entity types.
9//!
10//! # Quick Start
11//!
12//! ```rust
13//! use tnid::{Tnid, TnidName, NameStr};
14//!
15//! // Define a name type for your entity
16//! struct User;
17//! impl TnidName for User {
18//!     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
19//! }
20//!
21//! // Generate IDs
22//! let id = Tnid::<User>::new_v0();  // Time-ordered (like UUIDv7)
23//!
24//! // Display as a TNID string
25//! println!("{}", id);  // e.g., "user.Br2flcNDfF6LYICnT"
26//!
27//! // Or as a standard UUID for database storage
28//! let uuid_str = id.to_uuid_string_cased(false);
29//! // e.g., "cab1952a-f09d-86d9-928e-96ea03dc6af3"
30//! ```
31//!
32//! # Why TNIDs?
33//!
34//! TNIDs solve several common problems with plain UUIDs:
35//!
36//! - **Type Safety**: `Tnid<User>` and `Tnid<Post>` are different types. You can't accidentally
37//!   pass a post ID where a user ID is expected.
38//! - **Human Readable**: The TNID string format (`user.Br2flcNDfF6LYICnT`) tells you at a
39//!   glance what kind of entity the ID refers to.
40//! - **UUID Compatible**: Every TNID is a valid UUIDv8, so it works with existing UUID columns
41//!   in databases and UUID-based APIs.
42//!
43//! # TNID Variants
44//!
45//! Like UUID versions, TNIDs come in different variants:
46//!
47//! - **V0** ([`Tnid::new_v0`]) - Time-ordered with millisecond precision (like UUIDv7).
48//!   Use when you want IDs to sort chronologically.
49//! - **V1** ([`Tnid::new_v1`]) - High-entropy random (like UUIDv4).
50//!   Use when you want maximum randomness without time information.
51//!
52//! See [`TnidVariant`] for details.
53//!
54//! # String Representations
55//!
56//! TNIDs support two string formats:
57//!
58//! - **TNID format** (`user.Br2flcNDfF6LYICnT`): Human-readable, sortable, unambiguous
59//! - **UUID format** (`cab1952a-f09d-86d9-928e-96ea03dc6af3`): Standard UUID hex for compatibility
60//!
61//! ```rust
62//! # use tnid::{Tnid, TnidName, NameStr};
63//! # struct User;
64//! # impl TnidName for User {
65//! #     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
66//! # }
67//! let id = Tnid::<User>::new_v1();
68//!
69//! // TNID string (for APIs, logs, user-facing contexts)
70//! let tnid_str = id.as_tnid_string();
71//!
72//! // UUID string (for databases, UUID-based systems)
73//! let uuid_str = id.to_uuid_string_cased(false);
74//!
75//! // Parse back
76//! let from_tnid = Tnid::<User>::parse_tnid_string(&tnid_str);
77//! let from_uuid = Tnid::<User>::parse_uuid_string(&uuid_str);
78//! ```
79//!
80//! # Feature Flags
81//!
82//! | Feature | Default | Description |
83//! |---------|---------|-------------|
84//! | `time` | ✓ | Enables [`Tnid::new_v0`] with automatic timestamps |
85//! | `rand` | ✓ | Enables [`Tnid::new_v0`] and [`Tnid::new_v1`] with automatic randomness |
86//! | `encryption` | | Enables [`Tnid::encrypt_v0_to_v1`] and [`Tnid::decrypt_v1_to_v0`] for hiding timestamps |
87//! | `uuid` | | Integration with the [`uuid`](https://docs.rs/uuid) crate |
88//!
89//! Without the default features, you can still create TNIDs using explicit components:
90//! - [`Tnid::new_v0_with_parts`] - Provide your own timestamp and random bits
91//! - [`Tnid::new_v1_with_random`] - Provide your own random bits
92//!
93//! # Reliability
94//!
95//! This crate is designed to never panic in normal use. All functions that could potentially
96//! panic are fuzz tested with randomized inputs to verify they don't. Extensive unit tests
97//! verify correctness of encoding, sorting, and round-trip conversions.
98
99#![cfg_attr(docsrs, feature(doc_cfg))]
100#![deny(unsafe_code)]
101#![deny(clippy::unwrap_used)]
102#![deny(clippy::indexing_slicing)]
103#![deny(rustdoc::broken_intra_doc_links)]
104#![warn(missing_docs)]
105
106use std::marker::PhantomData;
107
108mod data_encoding;
109#[cfg(feature = "encryption")]
110pub mod encryption;
111mod name_encoding;
112mod tnid_variant;
113mod utils;
114#[cfg(feature = "uuid")]
115mod uuid;
116mod uuidlike;
117mod v0;
118mod v1;
119
120pub use name_encoding::NameStr;
121pub use tnid_variant::TnidVariant;
122pub use uuidlike::UUIDLike;
123
124/// Intended to be used on empty structs to create type checked TNID names.
125///
126/// ```rust
127/// # use tnid::TnidName;
128/// # use tnid::Tnid;
129/// # use tnid::NameStr;
130///
131/// struct ExampleName;
132/// impl TnidName for ExampleName {
133///     const ID_NAME: NameStr<'static> = NameStr::new_const("exna");
134/// }
135///
136/// # let _ = Tnid::<ExampleName>::new_v0();
137/// ```
138///
139/// [`NameStr::new_const`] validates the name at compile time and is the only way to create
140/// a `NameStr<'static>`, ensuring all [`Tnid`] names are valid.
141/// ```rust,compile_fail
142/// # use tnid::TnidName;
143/// # use tnid::Tnid;
144/// # use tnid::NameStr;
145///
146/// struct InvalidName;
147/// impl TnidName for InvalidName {
148///     const ID_NAME: NameStr<'static> = NameStr::new_const("2long");
149/// }
150///
151/// # let _ = Tnid::<InvalidName>::new_v0();
152/// ```
153pub trait TnidName {
154    /// Must be overridden with the name of your ID
155    const ID_NAME: NameStr<'static>;
156}
157
158/// A type-safe TNID parameterized by name.
159///
160/// The type parameter uses the [`TnidName`] trait to enforce compile-time checking of names.
161/// `Tnid<User>` and `Tnid<Post>` are distinct types that cannot be mixed.
162///
163/// All validation happens at construction time, so any `Tnid<Name>` instance is guaranteed
164/// to be valid. If you need to work with potentially invalid 128-bit values, use [`UUIDLike`]
165/// for inspection without validation.
166#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)]
167pub struct Tnid<Name: TnidName> {
168    id_name: PhantomData<Name>,
169    id: u128,
170}
171
172impl<Name: TnidName> Copy for Tnid<Name> {}
173
174impl<Name: TnidName> Clone for Tnid<Name> {
175    fn clone(&self) -> Self {
176        *self
177    }
178}
179
180impl<Name: TnidName> Tnid<Name> {
181    /// Returns the name associated with this TNID type.
182    ///
183    /// The name comes from the [`TnidName`] implementation for this type and is
184    /// used in the TNID string representation (`<name>.<data>`).
185    ///
186    /// # Examples
187    ///
188    /// ```rust
189    /// use tnid::{Tnid, TnidName, NameStr};
190    ///
191    /// struct User;
192    /// impl TnidName for User {
193    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
194    /// }
195    ///
196    /// let id = Tnid::<User>::new_v0();
197    /// assert_eq!(id.name(), "user");
198    /// ```
199    pub fn name(&self) -> &'static str {
200        Name::ID_NAME.as_str()
201    }
202
203    /// Returns the hex representation of the name field (20 bits as 5 hex characters).
204    ///
205    /// The ASCII representation of a name (like "test") is different from the hex
206    /// representation of those bits when viewing a TNID in hex format. This method shows
207    /// what the name looks like as hex, which is how it appears in TNID hex strings.
208    ///
209    /// This is useful for understanding what the name portion looks like in the hex
210    /// representation without needing a specific TNID instance.
211    ///
212    /// # Examples
213    ///
214    /// ```rust
215    /// use tnid::{Tnid, TnidName, NameStr};
216    ///
217    /// struct Test;
218    /// impl TnidName for Test {
219    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("test");
220    /// }
221    ///
222    /// // Check what "test" looks like in hex (any TNID instance works)
223    /// let id = Tnid::<Test>::new_v1();
224    /// assert_eq!(id.name_hex(), "cab19");
225    /// ```
226    pub fn name_hex(&self) -> String {
227        let hex = format!("{:05x}", self.id >> 108);
228
229        debug_assert_eq!(hex.len(), 5);
230
231        hex
232    }
233
234    /// Returns the raw 128-bit UUIDv8-compatible representation of this TNID.
235    ///
236    /// This returns the complete bit representation including the name, UUID version/variant
237    /// bits, TNID variant, and all data bits.
238    ///
239    /// # Endianness
240    ///
241    /// The UUID specification dictates that [UUIDs are stored in big-endian](https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-format) byte order.
242    /// When storing or transmitting this `u128` value as bytes, you may need to convert
243    /// to big-endian format using methods like [`u128::to_be_bytes()`] since `u128` uses
244    /// the platform's native endianness.
245    ///
246    /// # Examples
247    ///
248    /// ```rust
249    /// use tnid::{Tnid, TnidName, NameStr};
250    ///
251    /// struct User;
252    /// impl TnidName for User {
253    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
254    /// }
255    ///
256    /// let id = Tnid::<User>::new_v0();
257    /// let as_u128 = id.as_u128();
258    ///
259    /// // Convert to big-endian bytes for storage/transmission
260    /// let bytes = as_u128.to_be_bytes();
261    /// ```
262    pub fn as_u128(&self) -> u128 {
263        self.id
264    }
265
266    /// Generates a new time-ordered TNID (alias for [`Self::new_v0`]).
267    ///
268    /// This variant is sortable by creation time, similar to UUIDv7. TNIDs created earlier
269    /// will sort before those created later in all representations (u128, UUID hex, TNID string).
270    ///
271    /// Use this when you need time-based sorting, similar to choosing UUIDv7 over UUIDv4.
272    pub fn new_time_ordered() -> Self {
273        Self::new_v0()
274    }
275
276    /// Generates a new v0 TNID.
277    ///
278    /// This variant is time-ordered with millisecond precision, similar to UUIDv7.
279    /// TNIDs created earlier will sort before those created later in all representations
280    /// (u128, UUID hex, and TNID string). The remaining bits are filled with random data.
281    ///
282    /// Use this when you need time-based sorting and want IDs to be roughly chronological,
283    /// similar to choosing UUIDv7 over UUIDv4.
284    #[cfg(feature = "time")]
285    pub fn new_v0() -> Self {
286        Self::new_v0_with_time(time::OffsetDateTime::now_utc())
287    }
288
289    /// Generates a new TNID with maximum randomness (alias for [`Self::new_v1`]).
290    ///
291    /// This variant maximizes entropy with 100 bits of random data, similar to UUIDv4
292    /// but with slightly less entropy due to the 20-bit name field. This is almost
293    /// certainly sufficient for most use cases.
294    ///
295    /// Use this when you don't need time-based sorting and want maximum randomness,
296    /// similar to choosing UUIDv4 over UUIDv7.
297    ///
298    /// # Examples
299    ///
300    /// ```rust
301    /// use tnid::{Tnid, TnidName, NameStr};
302    ///
303    /// struct User;
304    /// impl TnidName for User {
305    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
306    /// }
307    ///
308    /// let id = Tnid::<User>::new_high_entropy();
309    /// ```
310    #[cfg(feature = "rand")]
311    pub fn new_high_entropy() -> Self {
312        Self::new_v1()
313    }
314
315    /// Generates a new v1 TNID.
316    ///
317    /// This variant maximizes entropy with 100 bits of random data, similar to UUIDv4.
318    /// This is almost certainly sufficient for most use cases.
319    ///
320    /// Use this when you don't need time-based sorting and want maximum randomness,
321    /// similar to choosing UUIDv4 over UUIDv7.
322    #[cfg(feature = "rand")]
323    pub fn new_v1() -> Self {
324        Self::new_v1_with_random(rand::random())
325    }
326
327    /// Generates a new high-entropy TNID (v1) from explicit random bits.
328    ///
329    /// This creates a v1 TNID without requiring the `rand` feature dependency,
330    /// allowing you to provide your own randomness source. This is useful in
331    /// environments where the `rand` library may not be suitable (embedded systems,
332    /// WASM, or when you need a custom random source).
333    ///
334    /// # Parameters
335    ///
336    /// - `random_bits`: Random bits for the TNID. Only 100 bits are used, but
337    ///   accepting a `u128` makes it easier to provide randomness.
338    ///
339    /// # Examples
340    ///
341    /// ```rust
342    /// use tnid::{Tnid, TnidName, NameStr};
343    ///
344    /// struct User;
345    /// impl TnidName for User {
346    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
347    /// }
348    ///
349    /// // Provide your own randomness
350    /// let random_bits = 0x0123456789ABCDEF0123456789ABCDEF;
351    ///
352    /// let id = Tnid::<User>::new_v1_with_random(random_bits);
353    /// ```
354    pub fn new_v1_with_random(random_bits: u128) -> Self {
355        let id_name = Name::ID_NAME;
356
357        let id = v1::make_from_parts(id_name, random_bits);
358
359        Self {
360            id_name: PhantomData,
361            id,
362        }
363    }
364
365    /// Generates a new time-ordered TNID (v0) with a specific timestamp.
366    ///
367    /// This creates the same time-sortable TNID as [`Self::new_v0`], but allows you to
368    /// provide a specific timestamp instead of using the current time. The timestamp is
369    /// converted to milliseconds since the Unix epoch and encoded into the TNID.
370    ///
371    /// TNIDs created with earlier timestamps will sort before those with later timestamps
372    /// in all representations (u128, UUID hex, and TNID string).
373    ///
374    /// # Examples
375    ///
376    /// ```rust
377    /// use tnid::{Tnid, TnidName, NameStr};
378    /// use time::OffsetDateTime;
379    ///
380    /// struct User;
381    /// impl TnidName for User {
382    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
383    /// }
384    ///
385    /// let timestamp = OffsetDateTime::now_utc();
386    /// let id = Tnid::<User>::new_v0_with_time(timestamp);
387    /// ```
388    ///
389    /// Demonstrating time-based sorting:
390    /// ```rust
391    /// use tnid::{Tnid, TnidName, NameStr};
392    /// use time::{OffsetDateTime, Duration};
393    ///
394    /// struct User;
395    /// impl TnidName for User {
396    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
397    /// }
398    ///
399    /// let now = OffsetDateTime::now_utc();
400    /// let earlier = now - Duration::hours(1);
401    /// let later = now + Duration::hours(1);
402    ///
403    /// let id1 = Tnid::<User>::new_v0_with_time(earlier);
404    /// let id2 = Tnid::<User>::new_v0_with_time(now);
405    /// let id3 = Tnid::<User>::new_v0_with_time(later);
406    ///
407    /// // Earlier times sort before later times
408    /// assert!(id1.as_u128() < id2.as_u128());
409    /// assert!(id2.as_u128() < id3.as_u128());
410    /// ```
411    #[cfg(all(feature = "rand", feature = "time"))]
412    pub fn new_v0_with_time(time: time::OffsetDateTime) -> Self {
413        let id_name = Name::ID_NAME;
414
415        let epoch_millis = (time.unix_timestamp_nanos() / 1000 / 1000) as u64;
416
417        let random_bits: u64 = rand::random();
418
419        let id = v0::make_from_parts(id_name, epoch_millis, random_bits);
420
421        Self {
422            id_name: PhantomData,
423            id,
424        }
425    }
426
427    /// Generates a new time-ordered TNID (v0) from explicit components.
428    ///
429    /// This creates a v0 TNID without requiring the `time` or `rand` feature dependencies,
430    /// allowing you to provide your own timestamp and randomness sources. This is useful
431    /// in environments where those libraries may not be suitable (embedded systems, WASM,
432    /// or when you need custom time/random sources).
433    ///
434    /// # Parameters
435    ///
436    /// - `epoch_millis`: Milliseconds since the Unix epoch (January 1, 1970 UTC)
437    /// - `random`: Random bits for the TNID (57 bits will be used)
438    ///
439    /// # Examples
440    ///
441    /// ```rust
442    /// use tnid::{Tnid, TnidName, NameStr};
443    ///
444    /// struct User;
445    /// impl TnidName for User {
446    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
447    /// }
448    ///
449    /// // Provide your own timestamp and randomness
450    /// let timestamp_ms = 1750118400000;
451    /// let random_bits = 42;
452    ///
453    /// let id = Tnid::<User>::new_v0_with_parts(timestamp_ms, random_bits);
454    /// ```
455    pub fn new_v0_with_parts(epoch_millis: u64, random: u64) -> Self {
456        Self {
457            id_name: PhantomData,
458            id: v0::make_from_parts(Name::ID_NAME, epoch_millis, random),
459        }
460    }
461
462    /// Returns the TNID string representation.
463    ///
464    /// This representation has several advantages over the UUID hex format:
465    /// - **Unambiguous**: Unlike UUID hex strings which are case-insensitive, TNID strings
466    ///   are case-sensitive with exactly one valid representation
467    /// - **Sortable**: For v0 TNIDs, the string representation maintains time-ordering
468    /// - **Human-readable name**: The name prefix makes it easy to identify the ID type
469    ///
470    /// The format is `<name>.<encoded-data>`, where the data is encoded using the TNID
471    /// Data Encoding that preserves these sortability and uniqueness properties.
472    ///
473    /// # Examples
474    ///
475    /// ```rust
476    /// use tnid::{Tnid, TnidName, NameStr};
477    ///
478    /// struct User;
479    /// impl TnidName for User {
480    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
481    /// }
482    ///
483    /// let id = Tnid::<User>::new_v0();
484    /// let tnid_string = id.as_tnid_string();
485    ///
486    /// // Format: <name>.<encoded-data>
487    /// // Example: "user.Br2flcNDfF6LYICnT"
488    /// assert!(tnid_string.starts_with("user."));
489    /// ```
490    pub fn as_tnid_string(&self) -> String {
491        format!(
492            "{}.{}",
493            self.name(),
494            data_encoding::id_data_to_string(self.id)
495        )
496    }
497
498    /// Returns the TNID variant.
499    ///
500    /// # Examples
501    ///
502    /// ```rust
503    /// use tnid::{Tnid, TnidName, NameStr, TnidVariant};
504    ///
505    /// struct User;
506    /// impl TnidName for User {
507    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
508    /// }
509    ///
510    /// let id_v0 = Tnid::<User>::new_v0();
511    /// assert_eq!(id_v0.variant(), TnidVariant::V0);
512    ///
513    /// let id_v1 = Tnid::<User>::new_v1();
514    /// assert_eq!(id_v1.variant(), TnidVariant::V1);
515    /// ```
516    pub fn variant(&self) -> TnidVariant {
517        let variant_bits = (self.id >> 60) as u8;
518
519        TnidVariant::from_u8(variant_bits)
520    }
521
522    /// Converts the TNID to UUID hex string format.
523    ///
524    /// This is useful for UUID compatibility and interoperability with systems that expect
525    /// standard UUID format, or any other case where you need the common UUID hex representation.
526    ///
527    /// # Parameters
528    ///
529    /// - `uppercase`: If `true`, uses uppercase hex digits (A-F). If `false`, uses lowercase (a-f).
530    ///
531    /// # Examples
532    ///
533    /// ```rust
534    /// use tnid::{Tnid, TnidName, NameStr};
535    ///
536    /// struct User;
537    /// impl TnidName for User {
538    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
539    /// }
540    ///
541    /// let id = Tnid::<User>::new_v1();
542    ///
543    /// let uuid_lower = id.to_uuid_string_cased(false);
544    /// // "cab1952a-f09d-86d9-928e-96ea03dc6af3"
545    ///
546    /// let uuid_upper = id.to_uuid_string_cased(true);
547    /// // "CAB1952A-F09D-86D9-928E-96EA03DC6AF3"
548    /// ```
549    pub fn to_uuid_string_cased(&self, uppercase: bool) -> String {
550        utils::u128_to_uuid_string(self.id, uppercase)
551    }
552
553    /// Parses a TNID from UUID hex string format.
554    ///
555    /// This is the inverse of [`Self::to_uuid_string_cased`].
556    ///
557    /// The parser accepts both uppercase and lowercase hex digits (A-F or a-f).
558    ///
559    /// Returns `None` if:
560    /// - The string is not valid UUID format
561    /// - The UUID is not a valid TNID (wrong version/variant bits or name mismatch)
562    ///
563    /// For inspecting why a UUID might not be a valid TNID, see [`UUIDLike`].
564    ///
565    /// # Examples
566    ///
567    /// ```rust
568    /// use tnid::{Tnid, TnidName, NameStr};
569    ///
570    /// struct User;
571    /// impl TnidName for User {
572    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
573    /// }
574    ///
575    /// // Create a TNID and convert to UUID string
576    /// let original = Tnid::<User>::new_v1();
577    /// let uuid_string = original.to_uuid_string_cased(false);
578    ///
579    /// // Parse it back
580    /// let parsed = Tnid::<User>::parse_uuid_string(&uuid_string);
581    /// assert!(parsed.is_some());
582    /// assert_eq!(parsed.unwrap().as_u128(), original.as_u128());
583    ///
584    /// // Also accepts uppercase
585    /// let uuid_upper = original.to_uuid_string_cased(true);
586    /// let parsed_upper = Tnid::<User>::parse_uuid_string(&uuid_upper);
587    /// assert!(parsed_upper.is_some());
588    ///
589    /// // Invalid: not a valid UUID format
590    /// assert!(Tnid::<User>::parse_uuid_string("not-a-uuid").is_none());
591    /// ```
592    pub fn parse_uuid_string(uuid_string: &str) -> Option<Self> {
593        let id = UUIDLike::parse_uuid_string(uuid_string)?.as_u128();
594
595        Self::from_u128(id)
596    }
597
598    /// Parses a TNID from its string representation.
599    ///
600    /// This is the inverse of [`Self::as_tnid_string`]. See that method for details
601    /// on the TNID string format.
602    ///
603    /// Returns `None` if the string is invalid. Validation includes:
604    /// - Correct format (`<name>.<encoded-data>`)
605    /// - Name matches the expected name for this TNID type
606    /// - Valid TNID Data Encoding
607    /// - Correct UUIDv8 version and variant bits
608    ///
609    /// If you need to inspect non-compliant IDs or understand why parsing failed,
610    /// consider using [`UUIDLike`] which provides lower-level access.
611    ///
612    /// # Examples
613    ///
614    /// ```rust
615    /// use tnid::{Tnid, TnidName, NameStr};
616    ///
617    /// struct User;
618    /// impl TnidName for User {
619    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
620    /// }
621    ///
622    /// // Successful parsing
623    /// let parsed = Tnid::<User>::parse_tnid_string("user.Br2flcNDfF6LYICnT");
624    /// assert!(parsed.is_some());
625    ///
626    /// // Failed parsing - wrong name
627    /// assert!(Tnid::<User>::parse_tnid_string("post.Br2flcNDfF6LYICnT").is_none());
628    ///
629    /// // Failed parsing - invalid format
630    /// assert!(Tnid::<User>::parse_tnid_string("not-a-tnid").is_none());
631    /// ```
632    pub fn parse_tnid_string(tnid_string: &str) -> Option<Self> {
633        // Split on dot separator
634        let (name, data_str) = tnid_string.split_once('.')?;
635
636        // Validate name matches expected name
637        if name != Name::ID_NAME.as_str() {
638            return None;
639        }
640
641        // Decode data string to compact 102 bits
642        let compact_data = data_encoding::string_to_id_data(data_str)?;
643
644        // Expand to proper bit positions
645        let data_bits = data_encoding::expand_data_bits(compact_data);
646
647        // Get name bits
648        let name_bits = name_encoding::name_mask(Name::ID_NAME);
649
650        // Combine: name + UUID metadata + data
651        let id = name_bits | utils::UUID_V8_MASK | data_bits;
652
653        // Validate and construct (this checks UUID bits and name encoding)
654        Self::from_u128(id)
655    }
656
657    /// Creates a TNID from a raw 128-bit value.
658    ///
659    /// This is the inverse of [`Self::as_u128`] and is useful for loading TNIDs from
660    /// databases that store UUIDs as u128/binary, interoperating with UUID-based systems,
661    /// or deserializing.
662    ///
663    /// Returns `None` if the value is not a valid TNID. Validation includes:
664    /// - Correct UUIDv8 version and variant bits
665    /// - Name encoding matches the expected name for this TNID type
666    ///
667    /// # Endianness
668    ///
669    /// When loading from bytes, you'll almost certainly want to parse a `[u8; 16]` to a
670    /// `u128` using big-endian byte order with [`u128::from_be_bytes()`], as per the
671    /// UUID specification.
672    pub fn from_u128(id: u128) -> Option<Self> {
673        // check UUIDv8 version and variant bits
674        if (id & utils::UUID_V8_MASK) != utils::UUID_V8_MASK {
675            return None;
676        }
677
678        // check name encoding matches expected name
679        let name_bits_mask = 0xFFFFF_u128 << 108; // top 20 bits
680        let actual_name_bits = id & name_bits_mask;
681        let expected_name_bits = name_encoding::name_mask(Name::ID_NAME);
682        if actual_name_bits != expected_name_bits {
683            return None;
684        }
685
686        Some(Self {
687            id,
688            id_name: PhantomData,
689        })
690    }
691
692    /// Encrypts a V0 TNID to a V1 TNID, hiding timestamp information.
693    ///
694    /// V0 TNIDs contain a timestamp (like UUIDv7), which may leak information when exposed
695    /// publicly. This method encrypts the data bits to produce a valid V1 TNID that hides
696    /// the timestamp while remaining decryptable with [`Self::decrypt_v1_to_v0`].
697    ///
698    /// See the [`encryption`] module for more details on why and how this works.
699    ///
700    /// # Parameters
701    ///
702    /// - `secret`: 128-bit (16 bytes) encryption key
703    ///
704    /// # Returns
705    ///
706    /// - `Ok(encrypted)` for V0 input (encrypts and converts to V1)
707    /// - `Ok(self)` for V1 input (already encrypted, returns unchanged)
708    /// - `Err(())` for V2/V3 input (unsupported variants)
709    ///
710    /// # Example
711    ///
712    /// ```rust
713    /// use tnid::{Tnid, TnidName, NameStr, TNIDVariant};
714    ///
715    /// struct User;
716    /// impl TnidName for User {
717    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
718    /// }
719    ///
720    /// let secret = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
721    ///
722    /// let original = Tnid::<User>::new_v0();
723    /// let encrypted = original.encrypt_v0_to_v1(secret).unwrap();
724    /// assert_eq!(encrypted.variant(), TnidVariant::V1);
725    ///
726    /// let decrypted = encrypted.decrypt_v1_to_v0(secret).unwrap();
727    /// assert_eq!(decrypted.as_u128(), original.as_u128());
728    /// ```
729    #[cfg(feature = "encryption")]
730    pub fn encrypt_v0_to_v1(&self, key: impl Into<encryption::EncryptionKey>) -> Result<Self, ()> {
731        match self.variant() {
732            TnidVariant::V0 => {}
733            TnidVariant::V1 => return Ok(*self),
734            TnidVariant::V2 => return Err(()),
735            TnidVariant::V3 => return Err(()),
736        }
737
738        // Extract only the secret data bits (100 bits, excludes TNID variant)
739        let secret_data = encryption::extract_secret_data_bits(self.id);
740
741        // Encrypt the secret data
742        let encrypted_data = encryption::encrypt(secret_data, &key.into());
743
744        // Expand back to proper bit positions
745        let expanded = encryption::expand_secret_data_bits(encrypted_data);
746
747        // Preserve name and UUID metadata, replace data bits with encrypted version
748        let id = (self.id & !encryption::COMPLETE_SECRET_DATA_MASK) | expanded;
749
750        // Change variant from V0 to V1
751        let id = utils::change_variant(id, TnidVariant::V1);
752
753        Ok(Self {
754            id_name: PhantomData,
755            id,
756        })
757    }
758
759    /// Decrypts a V1 TNID back to a V0 TNID, recovering timestamp information.
760    ///
761    /// This is the inverse of [`Self::encrypt_v0_to_v1`]. See the [`encryption`] module
762    /// for more details.
763    ///
764    /// # Parameters
765    ///
766    /// - `secret`: 128-bit (16 bytes) encryption key (must match the key used for encryption)
767    ///
768    /// # Returns
769    ///
770    /// - `Ok(decrypted)` for V1 input (decrypts and converts to V0)
771    /// - `Ok(self)` for V0 input (already decrypted, returns unchanged)
772    /// - `Err(())` for V2/V3 input (unsupported variants)
773    ///
774    /// # Example
775    ///
776    /// ```rust
777    /// use tnid::{Tnid, TnidName, NameStr, TNIDVariant};
778    ///
779    /// struct User;
780    /// impl TnidName for User {
781    ///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
782    /// }
783    ///
784    /// let secret = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
785    ///
786    /// let original = Tnid::<User>::new_v0();
787    /// let encrypted = original.encrypt_v0_to_v1(secret).unwrap();
788    ///
789    /// let decrypted = encrypted.decrypt_v1_to_v0(secret).unwrap();
790    /// assert_eq!(decrypted.variant(), TnidVariant::V0);
791    /// assert_eq!(decrypted.as_u128(), original.as_u128());
792    /// ```
793    #[cfg(feature = "encryption")]
794    pub fn decrypt_v1_to_v0(&self, key: impl Into<encryption::EncryptionKey>) -> Result<Self, ()> {
795        match self.variant() {
796            TnidVariant::V0 => return Ok(*self),
797            TnidVariant::V1 => {}
798            TnidVariant::V2 => return Err(()),
799            TnidVariant::V3 => return Err(()),
800        }
801
802        // Extract only the secret data bits (100 bits, excludes TNID variant)
803        let encrypted_data = encryption::extract_secret_data_bits(self.id);
804
805        // Decrypt the secret data
806        let decrypted_data = encryption::decrypt(encrypted_data, &key.into());
807
808        // Expand back to proper bit positions
809        let expanded = encryption::expand_secret_data_bits(decrypted_data);
810
811        // Preserve name and UUID metadata, replace data bits with decrypted version
812        let id = (self.id & !encryption::COMPLETE_SECRET_DATA_MASK) | expanded;
813
814        // Change variant from V1 to V0
815        let id = utils::change_variant(id, TnidVariant::V0);
816
817        Ok(Self {
818            id_name: PhantomData,
819            id,
820        })
821    }
822}
823
824impl<Name: TnidName> std::fmt::Display for Tnid<Name> {
825    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
826        write!(f, "{}", self.as_tnid_string())
827    }
828}
829
830impl<Name: TnidName> std::fmt::Debug for Tnid<Name> {
831    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
832        write!(f, "{}", self.as_tnid_string())
833    }
834}
835
836#[cfg(test)]
837mod tests {
838    use super::*;
839
840    struct TestId;
841    impl TnidName for TestId {
842        const ID_NAME: NameStr<'static> = NameStr::new_const("test");
843    }
844
845    #[test]
846    fn variant0_is_k_sortable() {
847        use time::Duration;
848
849        let mut test_time = time::OffsetDateTime::now_utc();
850        let mut last_id: Tnid<TestId> = Tnid::new_v0_with_time(test_time);
851
852        for _ in 1..10_000 {
853            test_time += Duration::milliseconds(1);
854            let id: Tnid<TestId> = Tnid::new_v0_with_time(test_time);
855
856            assert!(last_id.as_u128() < id.as_u128());
857            assert!(last_id.as_tnid_string() < id.as_tnid_string());
858
859            last_id = id;
860        }
861    }
862
863    #[test]
864    fn tnid_variant_returns_v0() {
865        let id: Tnid<TestId> = Tnid::new_v0();
866        assert_eq!(id.variant(), TnidVariant::V0);
867    }
868
869    #[cfg(feature = "encryption")]
870    #[test]
871    fn encryption_bidirectional() {
872        let secret = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
873
874        let original: Tnid<TestId> = Tnid::new_v0();
875        assert_eq!(original.variant(), TnidVariant::V0);
876
877        let encrypted = original.encrypt_v0_to_v1(secret).unwrap();
878        assert_eq!(encrypted.variant(), TnidVariant::V1);
879
880        dbg!(encrypted, original);
881
882        let decrypted = encrypted.decrypt_v1_to_v0(secret).unwrap();
883        assert_eq!(decrypted.variant(), TnidVariant::V0);
884
885        assert_eq!(decrypted.as_u128(), original.as_u128());
886    }
887
888    #[test]
889    fn parse_tnid_string_roundtrip() {
890        let original: Tnid<TestId> = Tnid::new_v0();
891        let tnid_string = original.as_tnid_string();
892        let parsed = Tnid::<TestId>::parse_tnid_string(&tnid_string).unwrap();
893        assert_eq!(parsed.as_u128(), original.as_u128());
894    }
895
896    #[test]
897    fn parse_tnid_string_invalid_name() {
898        let result = Tnid::<TestId>::parse_tnid_string("wrong.abc123xyz");
899        assert!(result.is_none());
900    }
901
902    #[test]
903    fn parse_tnid_string_no_separator() {
904        let result = Tnid::<TestId>::parse_tnid_string("testabc123xyz");
905        assert!(result.is_none());
906    }
907}