obzenflow-idkit

Crates.ioobzenflow-idkit
lib.rsobzenflow-idkit
version0.2.0
created_at2026-01-17 16:47:28.376389+00
updated_at2026-01-17 16:47:28.376389+00
descriptionPhantom-typed ULID identifiers for ObzenFlow - type-safe IDs for native and WASM
homepagehttps://github.com/ObzenFlow/obzenflow-idkit
repositoryhttps://github.com/ObzenFlow/obzenflow-idkit
max_upload_size
id2050763
size52,098
Kevin Webber (rocketpages)

documentation

https://docs.rs/obzenflow-idkit

README

ObzenFlow IDKit

ObzenFlow IDKit provides phantom-typed ULIDs for ObzenFlow (native server + Leptos wasm). Extracted from the main ObzenFlow project to provide type-safe IDs across the ecosystem.

  • Type-only by default (no RNG dependencies).
  • App crates enable gen to generate IDs (server + wasm).
  • Optional serde support (serializes as a ULID string).
  • Re-exports ulid::Ulid for interop.

Why this exists

In a full-stack Rust app (native server + Leptos in the browser), you want one ID type that round-trips cleanly between back end, shared crates, and the frontend.

ULIDs are a good fit: they're 128-bit IDs (timestamp + randomness) that serialize to a compact, human-friendly string (useful for URLs and JSON). The server can generate them from OS entropy, but browser wasm can't access an OS RNG directly; it needs to go through Web APIs like window.crypto.getRandomValues (and Node-based wasm tests need globalThis.crypto).

This crate keeps that complexity out of shared code: it's type-only by default, while app crates opt into generation via gen and configure the RNG backend per target. See "Targets & RNG backends" below for specifics.

ObzenFlow pattern

  • Define marker types in domain crates (struct User; type UserId = Id<User>;); this crate stays generic.
  • Shared crates should not enable gen (often features = ["serde"] only).
  • App crates enable gen when they need to generate IDs (server or Leptos).
  • Don't use Id::default() unless gen is enabled (apps only).
  • Versioning follows SemVer; prior to 1.0, minor releases may include breaking changes.

Install

Server (native)

[dependencies]
obzenflow-idkit = { version = "0.2", features = ["gen", "serde"] }

Leptos app (wasm, browser)

[dependencies]
obzenflow-idkit = { version = "0.2", features = ["gen", "serde"] }
# Route RNG to window.crypto.getRandomValues
getrandom = { version = "0.2", features = ["js"] }

Shared crates (e.g., topology)

[dependencies]
obzenflow-idkit = { version = "0.2", features = ["serde"] } # no "gen" here

Node-based wasm tests: set globalThis.crypto = require('node:crypto').webcrypto.

Quick start

use obzenflow_idkit::{Id, Ulid};

pub struct User;
pub type UserId = Id<User>;

// Parse / round-trip (shared crates can do this without `gen`)
let from_wire: UserId = "01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap();
let ulid: Ulid = from_wire.as_ulid();

// Generate (apps with `features = ["gen"]`)
#[cfg(feature = "gen")]
let generated = UserId::new();

Common helpers: from_ulid, as_ulid, to_bytes/from_bytes, timestamp_ms().

If you don't need phantom typing, you can generate raw ULIDs with new_ulid() / new_ulid_string() (requires gen).

Testing (no RNG)

Keep cargo test trivial in shared crates by synthesizing IDs:

#[cfg(test)]
mod test_ids {
    use std::sync::atomic::{AtomicU128, Ordering};
    use obzenflow_idkit::Id;

    pub struct TestKind;
    static CTR: AtomicU128 = AtomicU128::new(0);

    pub fn next_id() -> Id<TestKind> {
        Id::from_bytes(CTR.fetch_add(1, Ordering::Relaxed).to_be_bytes())
    }
}

Use next_id() in tests, or provide constructors that accept IDs explicitly.

Targets & RNG backends

  • Native: OS entropy via getrandom (apps enable gen).
  • Browser wasm: add getrandom = { features = ["js"] } in the app; Id::new() uses crypto.getRandomValues.
  • Node (wasm tests): set globalThis.crypto as above.

CI guardrails (recommended)

  • Build both targets for shared crates:

    cargo build -p your_shared_crate
    cargo build -p your_shared_crate --target wasm32-unknown-unknown
    
  • Fail if any shared crate calls Id::new/new_ulid/new_ulid_string outside tests:

    rg -n "(Id::new|new_ulid|new_ulid_string)\\s*\\(" crates/your_shared_crate -g '!**/*test*' && exit 1
    

Project policies

  • Security: SECURITY.md
  • Code of Conduct: CODE_OF_CONDUCT.md
  • Trademarks: TRADEMARKS.md

License

Dual-licensed under MIT OR Apache-2.0.

Commit count: 10

cargo fmt