flaron-sdk 1.0.0

Official Rust SDK for writing Flaron edge flares - WebAssembly modules that run on the Flaron CDN edge runtime.
Documentation
//! Unique-ID generators provided by the host.
//!
//! All generators run in the host's native crypto + time stack so they are
//! collision-resistant across edges and don't pull a randomness crate into
//! the flare's `.wasm`.

use crate::{ffi, mem};

/// Generate a UUID v4 (random).
///
/// Returns the canonical string form (`xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`).
pub fn uuid_v4() -> Option<String> {
    uuid_with_version("v4")
}

/// Generate a UUID v7 (time-ordered).
///
/// V7 UUIDs are k-sortable by creation time - better than v4 as a database
/// primary key.
pub fn uuid_v7() -> Option<String> {
    uuid_with_version("v7")
}

fn uuid_with_version(version: &str) -> Option<String> {
    let args = serde_json::json!({ "version": version });
    let args_str = args.to_string();
    let (args_ptr, args_len) = mem::host_arg_str(&args_str);
    let result = unsafe { ffi::id_uuid(args_ptr, args_len) };
    // SAFETY: host writes a UTF-8 UUID string into the bump arena.
    unsafe { mem::read_packed_string(result) }
}

/// Generate a [ULID][ulid] - 26-character Crockford-base32 timestamp-prefixed
/// identifier.
///
/// [ulid]: https://github.com/ulid/spec
pub fn ulid() -> Option<String> {
    let result = unsafe { ffi::id_ulid() };
    // SAFETY: host writes a UTF-8 ULID string into the bump arena.
    unsafe { mem::read_packed_string(result) }
}

/// Generate a [Nanoid][nanoid] of the requested length.
///
/// Pass `0` to use the host's default length (21).
///
/// [nanoid]: https://github.com/ai/nanoid
pub fn nanoid(length: u32) -> Option<String> {
    let result = unsafe { ffi::id_nanoid(length as i32) };
    // SAFETY: host writes a UTF-8 nanoid string into the bump arena.
    unsafe { mem::read_packed_string(result) }
}

/// Generate a [KSUID][ksuid] - 27-character base62 sortable identifier.
///
/// [ksuid]: https://github.com/segmentio/ksuid
pub fn ksuid() -> Option<String> {
    let result = unsafe { ffi::id_ksuid() };
    // SAFETY: host writes a UTF-8 KSUID string into the bump arena.
    unsafe { mem::read_packed_string(result) }
}

/// Generate a Snowflake ID (decimal string).
///
/// This calls the rate-limited `snowflake_id` host function which counts
/// against `max_snowflake_per_invocation` (default 100). Use this in
/// preference to the unmetered variant.
pub fn snowflake() -> Option<String> {
    let result = unsafe { ffi::snowflake_id() };
    // SAFETY: host writes a UTF-8 snowflake decimal string into the arena.
    unsafe { mem::read_packed_string(result) }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ffi::test_host;

    #[test]
    fn uuid_v4_passes_version_in_args() {
        test_host::reset();
        test_host::with_mock(|m| {
            m.id_uuid_response = Some("550e8400-e29b-41d4-a716-446655440000".into());
        });
        let id = uuid_v4().unwrap();
        assert_eq!(id, "550e8400-e29b-41d4-a716-446655440000");

        let captured = test_host::read_mock(|m| m.last_id_uuid_args.clone()).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&captured).unwrap();
        assert_eq!(parsed["version"], "v4");
    }

    #[test]
    fn uuid_v7_passes_version() {
        test_host::reset();
        test_host::with_mock(|m| {
            m.id_uuid_response = Some("018e1234-5678-7abc-9def-0123456789ab".into());
        });
        uuid_v7().unwrap();
        let captured = test_host::read_mock(|m| m.last_id_uuid_args.clone()).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&captured).unwrap();
        assert_eq!(parsed["version"], "v7");
    }

    #[test]
    fn uuid_none_when_host_silent() {
        test_host::reset();
        assert!(uuid_v4().is_none());
    }

    #[test]
    fn ulid_returns_host_value() {
        test_host::reset();
        test_host::with_mock(|m| {
            m.id_ulid_response = Some("01F8MECHZX3TBDSZ7XRADM79XE".into());
        });
        assert_eq!(ulid().as_deref(), Some("01F8MECHZX3TBDSZ7XRADM79XE"));
    }

    #[test]
    fn nanoid_passes_length() {
        test_host::reset();
        test_host::with_mock(|m| {
            m.id_nanoid_response = Some("V1StGXR8_Z5jdHi6B-myT".into());
        });
        let id = nanoid(21).unwrap();
        assert_eq!(id.len(), 21);
        assert_eq!(test_host::read_mock(|m| m.last_nanoid_length), Some(21));
    }

    #[test]
    fn nanoid_zero_length_passes_zero() {
        test_host::reset();
        test_host::with_mock(|m| {
            m.id_nanoid_response = Some("default".into());
        });
        nanoid(0).unwrap();
        assert_eq!(test_host::read_mock(|m| m.last_nanoid_length), Some(0));
    }

    #[test]
    fn ksuid_returns_host_value() {
        test_host::reset();
        test_host::with_mock(|m| {
            m.id_ksuid_response = Some("0o5Fs0EELR0fUjHjbCnEtdUwQe3".into());
        });
        assert_eq!(ksuid().as_deref(), Some("0o5Fs0EELR0fUjHjbCnEtdUwQe3"));
    }

    #[test]
    fn snowflake_returns_host_value() {
        test_host::reset();
        test_host::with_mock(|m| {
            m.snowflake_id_response = Some("1234567890123456789".into());
        });
        assert_eq!(snowflake().as_deref(), Some("1234567890123456789"));
    }

    #[test]
    fn snowflake_none_when_host_silent() {
        test_host::reset();
        assert!(snowflake().is_none());
    }
}