Costless typed identifiers backed by UUID, with kind readable in serialized versions

Related tags

Command-line kind
Overview

MIT Latest Version Docs badge

Why kind ?

With kind, you

  • use typed identifiers in Rust, with no overhead over Uuid
  • have the type be human readable and obvious in JSON and any export
  • still use uuid in your database (if enabling the sqlx feature)
  • don't have to add code for that, never explicitly stringify, parse, check types, etc.
  • have no boilerplate to declare types and identifiers
  • have your ids implement Copy, Display, FromStr, Serialize, Deserialize, Eq, Hash, etc.
  • safely deal with both identified objects and new ones of same kind

See also the introduction to Kind motives and operation.

Optional features

  • serde: Serialize and Deserialize implementations for Id, Ided, and the id_enum! enums
  • sqlx: transparent read/write for Id (with uuid columns) and for Ided (with tables having an uuid identifier)
  • jsonschema: JSON schema generation
  • openapi: openapi ID object type for Id

In the current version, the sqlx feature is only complete for postgresql.

Declare a kind of object

You could implement the Identifiable trait, but the easiest solution is to just add attributes to your structs:

use kind::*;

#[derive(Identifiable)]
#[kind(class="Cust")]
pub struct Customer {
    // many fields
}

#[derive(Identifiable)]
#[kind(class="Cont")]
pub struct Contract {
    // many fields
}

Id

A kind::Id is strongly typed to avoid misuse of Rust APIs, especially when functions ask for several ids of different types.

The kind::Id also prevents the misuse of any string based API, such as Rest or GraphQL, by prefixing the internally used ids with a class prefix.

It's costless: the kind is handled by the type system and doesn't clutter the compiled binary

assert_eq!(
    std::mem::size_of::<Id<Customer>>(),
    std::mem::size_of::<uuid::Uuid>(),
);

You can parse the id from eg JSON, or just a string

let id: Id<Customer> = "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
    .parse().unwrap();

The type is checked, so this customer id can't be misused as a contract id

assert!(
    "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
    .parse::<Id<Contract>>()
    .is_err()
);

Note: the public id is parsed and checked in a case insensitive way

assert_eq!(id, "cust_371c35ec-34d9-4315-ab31-7ea8889a419a".parse());
assert_eq!(id, "CUST_371C35EC-34D9-4315-AB31-7EA8889A419A".parse());

Ided

Ided is short for "identified".

Sometimes, you have to deal with raw objects without id, because that's what you receive from your REST api for creation, or because you give it an id only when inserting the row in database.

That's why our raw Customer type has no id. Most API don't deal with just the raw Customer type but with an Ided<Customer>, which is guaranteed to have an id.

An ided can be created from an id and an "entity":

let new_customer = Customer { name: "John".to_string() };
let customer = Ided::new(id, new_customer);
assert_eq!(
    customer.id().to_string(),
    "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
);
assert_eq!(customer.entity().name, "John");

An ided automatically derefs into the entity type, so this is valid too:

assert_eq!(customer.name, "John");

Serde

An Ided object is serialized with the id next to the other fields, without unnecessary nesting.

#[derive(Identifiable, serde::Serialize, serde::Deserialize)]
#[kind(class="Cust")]
pub struct Customer {
    pub name: String,
}

let json = r#"{
    "id": "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a",
    "name": "John"
}"#;

let customer: Ided<Customer> = serde_json::from_str(&json).unwrap();
assert_eq!(
    customer.id().to_string(),
    "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
);
assert_eq!(customer.name, "John");

The id kind is checked, the deserialization below fails because the prefix of the id is wrong:

let json = r#"{
    "id": "Con_371c35ec-34d9-4315-ab31-7ea8889a419a",
    "name": "John"
}"#;
assert!(serde_json::from_str::<Ided<Customer>>(&json).is_err());

sqlx/PostgreSQL

In database, the id is just an uuid. The kind of the id in the database is implicitly given by the query and your DB structure, there's no additional check on reading/writing from rust to the DB and you don't have to change the DB structure when starting to use Kind.

The Id type implements Encode and Decode, so that it can be used transparently in sqlx queries just like any other primary type (arrays of Id are supported too, which is convenient with PostgreSQL).

As for serde, FromRow implementation on Ided is automatically deduced from the implementation on the raw struct.

So you will usually just declare your struct like this to have the Ided loaded from an sqlx::Row containing both the id column and the ones of the raw struct:

#[derive(Identifiable, sqlx::FromRow)]
#[kind(class="Cust")]
pub struct Customer {
    pub name: String,
}

JSON schema

If you are generating JSON schema for your objects using schemars crate, you can enable jsonschema feature, and we will generate definition for the Id object and any Ided object:

#[derive(JsonSchema, Identifiable)]
#[kind(class="Cust")]
pub struct Customer {
    pub name: String
}

fn main() {
    let schema = schema_for!(Ided<Customer>);
    println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}

will print out

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Customer_ided",
  "description": "Identified version of Customer",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "string"
    },
    "name": {
      "type": "string"
    }
  }
}

Open API

Open APi support (gated behind openapi feature flag) is currently extremely rudimentary. So far the only supported feature is defining the schema-level Id object that can be referenced by other schemas.

Example for including the Id into generated schema:

pub struct ApiDoc;
impl utoipa::OpenApi for ApiDoc {
    fn openapi() -> utoipa::openapi::OpenApi {
        let mut components = utoipa::openapi::ComponentsBuilder::new();
        let (kind_name, kind_schema) = kind::openapi_schema();
        components = components.schema(kind_name, kind_schema);
        //extra components and paths
        let mut openapi = utoipa::openapi::OpenApiBuilder::new()
            .components(Some(components.build()))
            .build();
        openapi
    }
}
You might also like...
Fully-typed, async, reusable state management and synchronization for Dioxus 🧬

dioxus-query 🦀 ⚡ Fully-typed, async, reusable state management and synchronization for Dioxus 🧬. Inspired by TanStack Query. ⚠️ Work in progress ⚠️

Fully-typed global state management with a topics subscription system for Dioxus 🧬

dioxus-radio 📡🦀 ⚠️ Work in progress !! ⚠️ . Fully-typed global state management with a topics subscription system for Dioxus 🧬. Who is this for You

Use LLMs to generate strongly-typed values

Magic Instantiate Quickstart use openai_magic_instantiate::*; #[derive(MagicInstantiate)] struct Person { // Descriptions can help the LLM unders

A fast uuid generator in Python using Rust

ruuid A fast UUID generator for Python built using Rust. Its a simple wrapper on top of Rust's UUID crate. How to use? Installation: pip3 install ruui

Generate unique, yet sortable identifiers

ulid-lite About An implementation of the ULID ("Universally Unique Lexicographically Sortable Identifier") standard. A ULID is 128-bit compatible with

Lightweight compile-time UUID parser.

compiled-uuid Anywhere you're building Uuids from a string literal, you should use uuid. Motivation If you want to use a fixed Uuid throughout your pr

Base 32 + 64 encoding and decoding identifiers + bytes in rust, quickly

fast32 Base32 and base64 encoding in Rust. Primarily for integer (u64, u128) and UUID identifiers (behind feature uuid), as well as arbitrary byte arr

A new kind of terminal
A new kind of terminal

notty - not a typewriter notty is a virtual terminal like xterm, gnome-vte, sh, or rxvt. Unlike these programs, notty is not intended to emulate a DEC

Simple library to host lv2 plugins. Is not meant to support any kind of GUI.

lv2-host-minimal Simple library to host lv2 plugins. Is not meant to support any kind of GUI. Host fx plugins (audio in, audio out) Set parameters Hos

Inkwell - It's a New Kind of Wrapper for Exposing LLVM (Safely)

Inkwell(s) It's a New Kind of Wrapper for Exposing LLVM (Safely) Inkwell aims to help you pen your own programming languages by safely wrapping llvm-s

Make a PDF file by writing kind of like HTML and CSS.
Make a PDF file by writing kind of like HTML and CSS.

markup-pdf-rs The Rust library for making a PDF files by writing kind of like HTML and CSS. Inspired by Satori and React-pdf. This library makes a PDF

Api testing tool made with rust to use for api developement (Kind of Tui)
Api testing tool made with rust to use for api developement (Kind of Tui)

Api testing tool made with rust to use for api developement (Kind of Tui) This Rust project provides a simple yet powerful tool for making HTTP reques

Text Expression Runner – Readable and easy to use text expressions
Text Expression Runner – Readable and easy to use text expressions

ter - Text Expression Runner ter is a cli to run text expressions and perform basic text operations such as filtering, ignoring and replacing on the c

Generate easy to remember sentences that acts as human readable UUIDs 🥳

uuid-readable-rs Easy to remember unique sentences acting as UUID Generate easy to remember sentences that acts as human readable UUIDs. Built on UUID

Fluent assertion library for Rust with readable messages.

Assertor Assertor makes test assertions and failure messages more human-readable. Assertor is heavily affected by Java Truth in terms of API design an

📦 Transpile readable code into DiamondFire templates

Welcome to fudge 👋 📦 Transpile readable code into DiamondFire templates Author 👤 BumpyBill & DADp Show your support Give a ⭐️ if this project helpe

🦀 Rust library for printing human readable, relative time differences

🦀 Rust library for printing human readable, relative time differences

🤯 A brainf*ck-style programming language, but readable

🤯 Brainease Brainease is a brainf*ck-style programming language, but readable. $ cargo install brainease # brainease -f examples/hello.bz save 'H

Display file sizes in human-readable units

hsize Display file sizes in human-readable units $ hsize 1000 1000000 5000000 1.00 KB 1.00 MB 5.00 MB $ hsize -p 5 1048576 12345678 1.04858 MB 12.345

Comments
  • version 1

    version 1

    We've used it in prod for more than one year with zero problem. It's feature complete and there's no pending request. There's no reason not to call it a V1.

    opened by Canop 0
  • Test and deploy actions

    Test and deploy actions

    • Check cargo fmt
    • Run tests
    • when release/** tag is pushed, tries to deploy crates to crates.io
      • versions MUST be updated manually

    also updated deps, included kind_proc into workspace

    opened by msdinit 0
  • Rename the derive from

    Rename the derive from "Kind" to "Identifiable"

    The purpose of the derive macro is to implement the Identifiable trait.

    In such case, Rust's convention is to give it the same name as the trait. It would also help understand what the derive call does and why.

    This PR rename the derive.

    opened by Canop 0
Releases(release/v1.0.0)
Owner
Wingback
Wingback
Api testing tool made with rust to use for api developement (Kind of Tui)

Api testing tool made with rust to use for api developement (Kind of Tui) This Rust project provides a simple yet powerful tool for making HTTP reques

Kythonlk 3 Feb 14, 2024
Display file sizes in human-readable units

hsize Display file sizes in human-readable units $ hsize 1000 1000000 5000000 1.00 KB 1.00 MB 5.00 MB $ hsize -p 5 1048576 12345678 1.04858 MB 12.345

Ryan 2 Nov 21, 2022
Nvm - Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions

Node Version Manager Table of Contents Intro About Installing and Updating Install & Update Script Additional Notes Troubleshooting on Linux Troublesh

nvm.sh 63.8k Jan 7, 2023
httm prints the size, date and corresponding locations of available unique versions of files residing on ZFS snapshots

httm prints the size, date and corresponding locations of available unique versions of files residing on ZFS snapshots, as well as allowing their interactive viewing and restoration.

null 837 Dec 30, 2022
A command line tool for easily generating multiple versions of a configuration file from a single template

MultiConf A command line tool for easily generating multiple versions of a configuration file from a single template. Why? I'm a big fan of the i3 win

Ian Clarke 4 Dec 10, 2022
🗄️ A simple (and safe!) to consume history of Client and Studio deployment versions.

??️ Roblox Version Archive A simple (and safe!) to consume history of Client and Studio deployment versions. About Parsing Roblox's DeployHistory form

Brooke Rhodes 4 Dec 28, 2022
Web-based tool that allows browsing and comparing symbol and type information of Microsoft Windows binaries across different versions of the OS.

WinDiff About WinDiff is an open-source web-based tool that allows browsing and comparing symbol and type information of Microsoft Windows binaries ac

Erwan Grelet 208 Jun 15, 2023
Log-structured, transactional virtual block device backed by S3

mvps Log-structured, transactional virtual block device compatible with the NBD protocol. mvps stands for "multi-versioned page store". MVPS can store

Heyang Zhou 3 Dec 3, 2023
A statically typed language that can deeply improve the Python ecosystem

The Erg Programming Language This is the main source code repository for Erg. This contains the compiler and documentation. 日本語 | 简体中文 | 繁體中文 Erg can

The Erg Programming Language 2.1k Jan 1, 2023
dwarf is a typed, interpreted, language that shares syntax with Rust.

The dwarf Programming Language dwarf is a programming language based heavily upon, and implemented in, Rust. The language is interpreted (and slow) wi

Keith Star 17 Jul 12, 2023