Time and IDs

TypeID

TypeIDs such as user_01h455vb4pex5vsknk084sn02q: a type prefix and a UUIDv7 in base32, made, read back to their UUID and time, and checked against the type a URL expects.

Version 1.0.0 · 4 functions · 2 crates

#ids#database#urls#api

Functions

What use typeid.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use typeid_new, typeid_from_uuid from typeid (Quillo 0.6).

typeid_new(prefix: str) → stror an error

A new TypeID for prefix ("user", "invoice_line"; "" for none), its UUIDv7 taken now: ids of one prefix sort by the time they were made.

typeid_from_uuid(prefix: str, uuid: str) → stror an error

The TypeID of an existing UUID, with prefix: to show database UUIDs as TypeIDs.

typeid_parse(id: str) → anyor an error

A TypeID read: { prefix, suffix, uuid, version, time }; time is when a UUIDv7 was made (ISO 8601, UTC), else null. An error value for an id that is not one.

typeid_valid(id: str, prefix: str) → bool

Whether id is a valid TypeID whose prefix is prefix ("" to take any prefix): for ids in URLs, so /users/order_... is refused.

Get it

$ quillo install typeid               # typeid.qwer into this folder$ quillo install typeid --example     # and its example, typeid.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/typeid.qwer$ curl -O https://quillolang.org/plugins/files/typeid.qwe

Then, in your program, all of it or only what you call:

main.qwe
use typeid.qweruse typeid_new, typeid_from_uuid from typeid      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
uuid = { version = "1", features = ["v7"] }chrono = { version = "0.4", default-features = false, features = ["std"] }

Example

typeid.qwe
// Ids that say what they are: user_..., invoice_..., sorted by when they were made.////   quillo run typeid.qwe      then open http://localhost:8080/idsuse "typeid.qwer"route GET /ids {    let user = typeid_new("user")?    {        user: user,        read: typeid_parse(user)?,        // The example from the TypeID spec.        spec: typeid_from_uuid("prefix", "0110c853-1d09-52d8-d73e-1194e95b5f19")?,        spec_back: typeid_parse("prefix_01h455vb4pex5vsknk084sn02q")?.uuid,        nil: typeid_from_uuid("", "00000000-0000-0000-0000-000000000000")?,        user_is_user: typeid_valid(user, "user"),        user_is_invoice: typeid_valid(user, "invoice"),        too_big: typeid_parse("user_8zzzzzzzzzzzzzzzzzzzzzzzzz").error,    }}// /users/<typeid>: a TypeID of another type, or no TypeID at all, is refused.route GET /users/:id {    if !typeid_valid(id, "user") { reply 404 { error: "not a user id", id: id } }    { user: id, made: typeid_parse(id)?.time }}
$ quillo run typeid.qwe$ curl 'localhost:8080/ids'

Source

typeid.qwer
//! TypeIDs: type-safe, sortable ids such as `user_2x4y6z8a0b1c2d3e4f5g6h7j8k`, a type prefix and a UUIDv7 in base32, made, read back to their UUID and time, and checked.//! version: 1.0.0//!//! [dependencies]//! uuid = { version = "1", features = ["v7"] }//! chrono = { version = "0.4", default-features = false, features = ["std"] }use serde_json::{json, Value};const ALPHABET: &[u8; 32] = b"0123456789abcdefghjkmnpqrstvwxyz";fn encode(n: u128) -> String {    (0..26).map(|i| ALPHABET[((n >> (5 * (25 - i))) & 31) as usize] as char).collect()}fn decode(s: &str) -> Result<u128, String> {    if s.len() != 26 {        return Err(format!("the part after the prefix is 26 characters, not {}", s.len()));    }    let mut n: u128 = 0;    for (i, c) in s.bytes().enumerate() {        let v = ALPHABET.iter().position(|&a| a == c).ok_or_else(|| format!("`{}` is not a TypeID character (0-9 and a-z without i, l, o, u)", c as char))?;        if i == 0 && v > 7 {            return Err("the first character after the prefix is 0 to 7: the id is larger than 128 bits".into());        }        n = (n << 5) | v as u128;    }    Ok(n)}fn check_prefix(prefix: &str) -> Result<(), String> {    if prefix.len() > 63 {        return Err("a TypeID prefix is at most 63 characters".into());    }    if !prefix.bytes().all(|b| b.is_ascii_lowercase() || b == b'_') {        return Err(format!("the prefix `{}` may hold only lower-case a-z and _", prefix));    }    if prefix.starts_with('_') || prefix.ends_with('_') {        return Err(format!("the prefix `{}` may not start or end with _", prefix));    }    Ok(())}fn join(prefix: &str, n: u128) -> String {    if prefix.is_empty() { encode(n) } else { format!("{}_{}", prefix, encode(n)) }}fn split(id: &str) -> Result<(String, u128), String> {    let (prefix, suffix) = match id.rfind('_') {        Some(i) => {            if i == 0 {                return Err("a TypeID with an empty prefix has no `_`".into());            }            (&id[..i], &id[i + 1..])        }        None => ("", id),    };    check_prefix(prefix)?;    Ok((prefix.to_string(), decode(suffix)?))}/// A new TypeID for `prefix` (`"user"`, `"invoice_line"`; `""` for none), its UUIDv7 taken now: ids of one prefix sort by the time they were made.pub fn typeid_new(prefix: &str) -> Result<String, String> {    check_prefix(prefix)?;    Ok(join(prefix, uuid::Uuid::now_v7().as_u128()))}/// The TypeID of an existing UUID, with `prefix`: to show database UUIDs as TypeIDs.pub fn typeid_from_uuid(prefix: &str, uuid: &str) -> Result<String, String> {    check_prefix(prefix)?;    let u = uuid::Uuid::parse_str(uuid.trim()).map_err(|e| format!("`{}` is not a UUID: {}", uuid, e))?;    Ok(join(prefix, u.as_u128()))}/// A TypeID read: `{ prefix, suffix, uuid, version, time }`; `time` is when a UUIDv7 was made (ISO 8601, UTC), else null. An error value for an id that is not one.pub fn typeid_parse(id: &str) -> Result<Value, String> {    let (prefix, n) = split(id)?;    let u = uuid::Uuid::from_u128(n);    let time = if u.get_version_num() == 7 {        let ms = (n >> 80) as i64;        chrono::DateTime::from_timestamp_millis(ms).map(|t| t.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())    } else {        None    };    Ok(json!({        "prefix": prefix,        "suffix": encode(n),        "uuid": u.hyphenated().to_string(),        "version": u.get_version_num(),        "time": time,    }))}/// Whether `id` is a valid TypeID whose prefix is `prefix` (`""` to take any prefix): for ids in URLs, so `/users/order_...` is refused.pub fn typeid_valid(id: &str, prefix: &str) -> bool {    match split(id) {        Ok((p, _)) => prefix.is_empty() || p == prefix,        Err(_) => false,    }}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On TypeID: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.