Skip to main content

triblespace_core/inline/encodings/
genid.rs

1use crate::inline::Encodes;
2use crate::id::ExclusiveId;
3use crate::id::Id;
4use crate::id::NilUuidError;
5use crate::id::OwnedId;
6use crate::id::RawId;
7use crate::id_hex;
8use crate::macros::entity;
9use crate::metadata;
10use crate::metadata::MetaDescribe;
11use crate::trible::Fragment;
12use crate::inline::IntoInline;
13use crate::inline::TryFromInline;
14use crate::inline::TryToInline;
15use crate::inline::Inline;
16use crate::inline::InlineEncoding;
17use crate::inline::INLINE_LEN;
18
19use std::convert::TryInto;
20
21use hex::FromHex;
22use hex::FromHexError;
23
24#[cfg(feature = "proptest")]
25use proptest::prelude::RngCore;
26
27/// A inline encoding for an abstract 128-bit identifier.
28/// This identifier is generated with high entropy and is suitable for use as a unique identifier.
29///
30/// See the [crate::id] module documentation for a discussion on the role of this identifier.
31pub struct GenId;
32
33impl MetaDescribe for GenId {
34    fn describe() -> Fragment {
35        let id: Id = id_hex!("B08EE1D45EB081E8C47618178AFE0D81");
36        #[allow(unused_mut)]
37        let mut tribles = entity! {
38            ExclusiveId::force_ref(&id) @
39                metadata::name: "genid",
40                metadata::description: "Opaque 128-bit identifier stored in the lower 16 bytes; the upper 16 bytes are zero. The value is intended to be high-entropy and stable over time.\n\nUse for entity ids, references, or user-assigned identifiers when the bytes do not carry meaning. If you want content-derived identifiers or deduplication, use a Hash schema instead.\n\nGenId does not imply ordering or integrity. If you need deterministic ids across systems, derive them from agreed inputs (for example by wrapping the inputs in `entity!{}` and taking its `root()`, or by hashing them directly).",
41                metadata::tag: metadata::KIND_INLINE_ENCODING,
42        };
43
44        #[cfg(feature = "wasm")]
45        {
46            tribles += entity! { ExclusiveId::force_ref(&id) @
47                metadata::value_formatter: wasm_formatter::GENID_WASM,
48            };
49        }
50        tribles
51    }
52}
53
54#[cfg(feature = "wasm")]
55mod wasm_formatter {
56    use core::fmt::Write;
57
58    use triblespace_core_macros::value_formatter;
59
60    #[value_formatter]
61    pub(crate) fn genid(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
62        const TABLE: &[u8; 16] = b"0123456789ABCDEF";
63
64        let prefix_ok = raw[..16].iter().all(|&b| b == 0);
65        let bytes = if prefix_ok { &raw[16..] } else { &raw[..] };
66        for &byte in bytes {
67            let hi = (byte >> 4) as usize;
68            let lo = (byte & 0x0F) as usize;
69            out.write_char(TABLE[hi] as char).map_err(|_| 1u32)?;
70            out.write_char(TABLE[lo] as char).map_err(|_| 1u32)?;
71        }
72        Ok(())
73    }
74}
75impl InlineEncoding for GenId {
76    type ValidationError = ();
77    type Encoding = Self;
78    fn validate(value: Inline<Self>) -> Result<Inline<Self>, Self::ValidationError> {
79        if value.raw[0..16] == [0; 16] {
80            Ok(value)
81        } else {
82            Err(())
83        }
84    }
85}
86
87/// Error returned when extracting an identifier from a [`Inline<GenId>`].
88#[derive(Debug, Copy, Clone, PartialEq, Eq)]
89pub enum IdParseError {
90    /// The identifier is nil (all zeros), which is reserved.
91    IsNil,
92    /// The upper 16 bytes are not zero, violating the GenId layout.
93    BadFormat,
94}
95
96//RawId
97impl<'a> TryFromInline<'a, GenId> for &'a RawId {
98    type Error = IdParseError;
99
100    fn try_from_inline(value: &'a Inline<GenId>) -> Result<Self, Self::Error> {
101        if value.raw[0..16] != [0; 16] {
102            return Err(IdParseError::BadFormat);
103        }
104        Ok(value.raw[16..32].try_into().unwrap())
105    }
106}
107
108impl TryFromInline<'_, GenId> for RawId {
109    type Error = IdParseError;
110
111    fn try_from_inline(value: &Inline<GenId>) -> Result<Self, Self::Error> {
112        let r: Result<&RawId, IdParseError> = value.try_from_inline();
113        r.copied()
114    }
115}
116
117impl Encodes<RawId> for GenId
118{
119    type Output = Inline<GenId>;
120    fn encode(source: RawId) -> Inline<GenId> {
121        let mut data = [0; INLINE_LEN];
122        data[16..32].copy_from_slice(&source[..]);
123        Inline::new(data)
124    }
125}
126
127impl Encodes<&RawId> for GenId {
128    type Output = Inline<GenId>;
129    fn encode(source: &RawId) -> Inline<GenId> {
130        <GenId as Encodes<RawId>>::encode(*source)
131    }
132}
133
134//Id
135impl<'a> TryFromInline<'a, GenId> for &'a Id {
136    type Error = IdParseError;
137
138    fn try_from_inline(value: &'a Inline<GenId>) -> Result<Self, Self::Error> {
139        if value.raw[0..16] != [0; 16] {
140            return Err(IdParseError::BadFormat);
141        }
142        if let Some(id) = Id::as_transmute_raw(value.raw[16..32].try_into().unwrap()) {
143            Ok(id)
144        } else {
145            Err(IdParseError::IsNil)
146        }
147    }
148}
149
150impl TryFromInline<'_, GenId> for Id {
151    type Error = IdParseError;
152
153    fn try_from_inline(value: &Inline<GenId>) -> Result<Self, Self::Error> {
154        let r: Result<&Id, IdParseError> = value.try_from_inline();
155        r.copied()
156    }
157}
158
159impl Encodes<&Id> for GenId
160{
161    type Output = Inline<GenId>;
162    fn encode(source: &Id) -> Inline<GenId> {
163        let mut data = [0; INLINE_LEN];
164        data[16..32].copy_from_slice(&source[..]);
165        Inline::new(data)
166    }
167}
168
169impl Encodes<Id> for GenId
170{
171    type Output = Inline<GenId>;
172    fn encode(source: Id) -> Inline<GenId> {
173        (&source).to_inline()
174    }
175}
176
177impl TryFromInline<'_, GenId> for uuid::Uuid {
178    type Error = IdParseError;
179
180    fn try_from_inline(value: &Inline<GenId>) -> Result<Self, Self::Error> {
181        if value.raw[0..16] != [0; 16] {
182            return Err(IdParseError::BadFormat);
183        }
184        let bytes: [u8; 16] = value.raw[16..32].try_into().unwrap();
185        Ok(uuid::Uuid::from_bytes(bytes))
186    }
187}
188
189/// Error returned when extracting an [`ExclusiveId`] from a [`Inline<GenId>`].
190#[derive(Debug, Copy, Clone, PartialEq, Eq)]
191pub enum ExclusiveIdError {
192    /// The raw bytes could not be interpreted as an identifier.
193    FailedParse(IdParseError),
194    /// The identifier is valid but could not be exclusively acquired
195    /// (another holder already owns it).
196    FailedAcquire(),
197}
198
199impl From<IdParseError> for ExclusiveIdError {
200    fn from(e: IdParseError) -> Self {
201        ExclusiveIdError::FailedParse(e)
202    }
203}
204
205impl<'a> TryFromInline<'a, GenId> for ExclusiveId {
206    type Error = ExclusiveIdError;
207
208    fn try_from_inline(value: &'a Inline<GenId>) -> Result<Self, Self::Error> {
209        let id: Id = value.try_from_inline()?;
210        id.acquire().ok_or(ExclusiveIdError::FailedAcquire())
211    }
212}
213
214impl Encodes<ExclusiveId> for GenId
215{
216    type Output = Inline<GenId>;
217    fn encode(source: ExclusiveId) -> Inline<GenId> {
218        source.id.to_inline()
219    }
220}
221
222impl Encodes<&ExclusiveId> for GenId
223{
224    type Output = Inline<GenId>;
225    fn encode(source: &ExclusiveId) -> Inline<GenId> {
226        source.id.to_inline()
227    }
228}
229
230impl TryFromInline<'_, GenId> for String {
231    type Error = IdParseError;
232
233    fn try_from_inline(v: &'_ Inline<GenId>) -> Result<Self, Self::Error> {
234        let id: Id = v.try_from_inline()?;
235        let mut s = String::new();
236        s.push_str("genid:");
237        s.push_str(&hex::encode(id));
238        Ok(s)
239    }
240}
241
242impl Encodes<OwnedId<'_>> for GenId
243{
244    type Output = Inline<GenId>;
245    fn encode(source: OwnedId<'_>) -> Inline<GenId> {
246        source.id.to_inline()
247    }
248}
249
250impl Encodes<&OwnedId<'_>> for GenId
251{
252    type Output = Inline<GenId>;
253    fn encode(source: &OwnedId<'_>) -> Inline<GenId> {
254        source.id.to_inline()
255    }
256}
257
258/// Error returned when packing a string into a [`Inline<GenId>`].
259///
260/// The expected format is `"genid:<32 hex chars>"`.
261#[derive(Debug, Clone, Copy, PartialEq)]
262pub enum PackIdError {
263    /// The string does not start with `"genid:"`.
264    BadProtocol,
265    /// The hex portion could not be decoded.
266    BadHex(FromHexError),
267}
268
269impl From<FromHexError> for PackIdError {
270    fn from(value: FromHexError) -> Self {
271        PackIdError::BadHex(value)
272    }
273}
274
275impl TryToInline<GenId> for &str {
276    type Error = PackIdError;
277
278    fn try_to_inline(self) -> Result<Inline<GenId>, Self::Error> {
279        let protocol = "genid:";
280        if !self.starts_with(protocol) {
281            return Err(PackIdError::BadProtocol);
282        }
283        let id = RawId::from_hex(&self[protocol.len()..])?;
284        Ok(id.to_inline())
285    }
286}
287
288impl TryToInline<GenId> for uuid::Uuid {
289    type Error = NilUuidError;
290
291    fn try_to_inline(self) -> Result<Inline<GenId>, Self::Error> {
292        let mut data = [0; INLINE_LEN];
293        data[16..32].copy_from_slice(self.as_bytes());
294        Ok(Inline::new(data))
295    }
296}
297
298impl TryToInline<GenId> for &uuid::Uuid {
299    type Error = NilUuidError;
300
301    fn try_to_inline(self) -> Result<Inline<GenId>, Self::Error> {
302        (*self).try_to_inline()
303    }
304}
305
306#[cfg(feature = "proptest")]
307/// Proptest value tree for a random [`GenId`]. Does not shrink.
308pub struct IdValueTree(RawId);
309
310#[cfg(feature = "proptest")]
311/// Proptest strategy that generates random 128-bit identifiers.
312#[derive(Debug)]
313pub struct RandomGenId();
314#[cfg(feature = "proptest")]
315impl proptest::strategy::Strategy for RandomGenId {
316    type Tree = IdValueTree;
317    type Value = RawId;
318
319    fn new_tree(
320        &self,
321        runner: &mut proptest::prelude::prop::test_runner::TestRunner,
322    ) -> proptest::prelude::prop::strategy::NewTree<Self> {
323        let rng = runner.rng();
324        let mut id = [0; 16];
325        rng.fill_bytes(&mut id[..]);
326
327        Ok(IdValueTree(id))
328    }
329}
330
331#[cfg(feature = "proptest")]
332impl proptest::strategy::ValueTree for IdValueTree {
333    type Value = RawId;
334
335    fn simplify(&mut self) -> bool {
336        false
337    }
338    fn complicate(&mut self) -> bool {
339        false
340    }
341    fn current(&self) -> RawId {
342        self.0
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::GenId;
349    use crate::id::rngid;
350    use crate::inline::TryFromInline;
351    use crate::inline::TryToInline;
352    use crate::inline::InlineEncoding;
353
354    #[test]
355    fn unique() {
356        assert!(rngid() != rngid());
357    }
358
359    #[test]
360    fn uuid_nil_round_trip() {
361        let uuid = uuid::Uuid::nil();
362        let value = uuid.try_to_inline().expect("uuid packing should succeed");
363        GenId::validate(value).expect("schema validation");
364        let round_trip = uuid::Uuid::try_from_inline(&value).expect("uuid unpacking should succeed");
365        assert_eq!(uuid, round_trip);
366    }
367}