11 releases (4 breaking)

0.4.2 Jun 24, 2024
0.4.1 Jun 24, 2024
0.3.2 Apr 13, 2024
0.2.1 Apr 9, 2024
0.0.1 Dec 28, 2023

#253 in Text processing

Apache-2.0 OR MIT

41KB
698 lines

Container Image Reference

Rust Version crates.io Dependency Status

A library for using and handling Typed Object IDs.

About

An Object ID (OID) is a base32hex (which is base32 with the extended hex alphabet; see RFC4648 for details) encoded UUID. The UUID is either v4 (random) or v7 (based on UNIX Epoch; see [draft RFC4122v17] for details) - this library further qualifies the OID with a "type" which is a short alphanumeric prefix separated from the OID by a -. This library refers to Typed OIDs as TOIDs which are distinct from the OID counterparts which lack the "type" prefix.

For example EXA-4GKFGPRVND4QT3PDR90PDKF66O, by convention the prefix is three ASCII characters, however that is not a hard constraint of TOIDs in general.

The Pitch

TOIDs allow a "human readable subject line" in the form of the prefix, where actual data is a UUIDv7. This means while debugging or reviewing a system it's trivial to determine if an incorrect TOID was passed in a particular location by looking at the prefix. This isn't achievable with bare UUIDs or GUIDs due to their lacking of any typed identifiers.

In other words, without using TOIDs it's far too easy to incorrectly swap say a UserID with an OrderID if they are both just simple GUIDs. Where as a TOID would make the mistake more easily identifiable or even programmatically impossible.

Base32hex encoding the UUID also allows compressing the data into a smaller and more familiar format for humans, akin to a commit hash. Using the "extended hex encoding" adds the additional property that the encodings do not lose their sort order when compared bitwise.

Finally, using a UUIDv7 enables index locality when used as database entries.

The Anti-Pitch

The downside to TOIDs is a layer of indirection when handling IDs and values, it's not immediately obvious that the TOIDs are a prefixed UUIDv7. Additionally, the prefixes themselves must be controlled in some manner including migrated on changes which adds a layer of complexity at the application layer.

There is also additional processing overhead compared to a bare UUID in order to encode/decode as well as handling the appending and removing the prefixes.

However, we believe the drawbacks pale in comparison to the benefits derived from the format.

Example

use typed_oid::{error::*, Oid, OidStr, OidPrefix};
use uuid::Uuid;
use anyhow::Result;

fn main() -> Result<()> {
    // TOIDs come in two flavors, Oid<T> and `OidStr`.

    // A Oid<T> is a TOID using a Rust types as the type, e.g. a true Typed OID
    // These are less ergonomic, but more type-safe.
    run_oid()?;

    // A `OidStr` is a TOID using a bare string as the type, e.g. "Stringly Typed OID"
    // These easier to use, but less type-safe.
    run_oidstr()?;

    Ok(())
}

fn run_oidstr() -> Result<()> {
    // OIDs can be created with a given prefix alone
    #[cfg(feature = "uuid_v4")]
    {
        let oid = OidStr::new_v4("EXA")?;
        println!("OidStr from UUIDv4: {oid}");
    }
    #[cfg(feature = "uuid_v7")]
    {
        let oid = OidStr::new_v7_now("EXA")?;
        println!("OidStr from UUIDv7: {oid}");
    }
    // OIDs can also be created from the raw parts
    let oid = OidStr::try_with_uuid("EXA", "b3cfdafa-3fec-41e2-82bf-ff881131abf1")?;
    println!("OidStr from UUID: {oid}");

    // OIDs can be parsed from strings, however the "value" must be a valid
    // base32hex (no pad) encoded UUID
    let oid: OidStr = "EXA-4GKFGPRVND4QT3PDR90PDKF66O".parse()?;
    println!("OidStr from string: {oid}");

    // One can retrieve the various parts of the OID if needed
    println!("Components of {oid}:");
    println!("\tPrefix: {}", oid.prefix());
    println!("\tValue: {}", oid.value());
    println!("\tUUID: {}", oid.uuid());

    Ok(())
}

fn run_oid() -> Result<()> {
    // In order for a struct to be used as a type it must implement typed_oid::OidPrefix
    #[derive(Debug)]
    struct EXA;
    impl OidPrefix for EXA {}

    // We can create a new OID by generating a random UUID
    #[cfg(feature = "uuid_v4")]
    {
        let oid: Oid<EXA> = Oid::new_v4();
        println!("Oid<EXA> with new UUIDv4: {oid}");
    }
    #[cfg(feature = "uuid_v7")]
    {
        let oid: Oid<EXA> = Oid::new_v7_now();
        println!("Oid<EXA> with new UUIDv7: {oid}");
    }
    // Or by giving a UUID
    let oid: Oid<EXA> = Oid::try_with_uuid("b3cfdafa-3fec-41e2-82bf-ff881131abf1")?;
    println!("Oid<EXA> with new UUID: {oid}");

    // We can go the other direction and parse a string to a Oid<EXA>
    let oid: Oid<EXA> = "EXA-4GKFGPRVND4QT3PDR90PDKF66O".parse()?;
    println!("Oid<EXA> with from string: {oid}");

    // One can retrieve the various parts of the OID if needed
    println!("Components of {oid}:");
    println!("\tPrefix: {}", oid.prefix());
    println!("\tValue: {}", oid.value());
    println!("\tUUID: {}", oid.uuid());

    // However, if we change the prefix to something that doesn't match our EXA type
    // we get an error even if the UUID is valid
    let res = "FAIL-4GKFGPRVND4QT3PDR90PDKF66O".parse::<Oid<EXA>>();
    assert!(res.is_err());
    assert_eq!(res.unwrap_err(), Error::InvalidPrefix { valid_until: 0 });

    Ok(())
}

Minimum Supported Rust Version (MSRV)

The MSRV depends on which crate features are enabled:

Feature MSRV
uuid_4 1.60.0
uuid_7 1.60.0
serde 1.60.0
surrealdb 1.75.0

License

This project is dual licensed under the terms of either the Apache License, Version 2.0, LICENSE-APACHE or MIT LICENSE-MIT at your option.

typed-oid was originally forked from seaplane-oid v0.4.0 which was licensed under the Apache License, Version 2.0.

Dependencies

~2–37MB
~586K SLoC