kind/
lib.rs

1//! A `kind::Id` is strongly typed to avoid misuse of Rust APIs, especially
2//! when functions ask for several ids of different types.
3//!
4//! The `kind::Id` also prevents the misuse of any string based API, such
5//! as Rest or GraphQL, by prefixing the internally used ids with a class
6//! prefix.
7//!
8//! ```
9//! use kind::*;
10//!
11//! // The structs we want to define Id types for are just annotated. The
12//! // Identifiable trait is derived.
13//!
14//! #[derive(Debug, Identifiable)]
15//! #[kind(class="Cust")]
16//! pub struct Customer {
17//!     // many fields
18//! }
19//!
20//! #[derive(Debug, Identifiable)]
21//! #[kind(class="Cont")]
22//! pub struct Contract {
23//!     // many fields
24//! }
25//!
26//! // Let's start from an id in the database (we use the string representantion
27//! // but kind natively decodes from postgres' Uuid into Id)
28//! let customer_db_id = "371c35ec-34d9-4315-ab31-7ea8889a419a";
29//!
30//! // Now, use it to get our Rust instance of Id:
31//! let customer_id: Id<Customer> = Id::from_db_id(customer_db_id).unwrap();
32//!
33//! // If we communicate (via serde, Display, or directly), we
34//! // use the public id
35//! let customer_public_id = customer_id.public_id();
36//! assert_eq!(&customer_public_id, "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a");
37//!
38//! // When reading an id withtout prefix, from the db, there was
39//! // no type check. It's (almost) OK because we carefully wrote our
40//! // queries. But we need a type check when we read from a public id.
41//! // Let's try to read our public id as a contract id:
42//! let contract_id: Result<Id<Contract>, IdError> = customer_public_id.parse();
43//! assert!(contract_id.is_err());
44//!
45//! // And let's check it's OK as a customer id:
46//! let customer_id: Result<Id<Customer>, IdError> = Id::from_public_id(&customer_public_id);
47//! assert!(customer_id.is_ok());
48//! assert_eq!(customer_id.unwrap().db_id(), "371c35ec-34d9-4315-ab31-7ea8889a419a");
49//!
50//! // The public id is parsed and checked in a case insensitive way
51//! assert_eq!(customer_id, "cust_371c35ec-34d9-4315-ab31-7ea8889a419a".parse());
52//! assert_eq!(customer_id, "CUST_371C35EC-34D9-4315-AB31-7EA8889A419A".parse());
53//!
54//! ```
55
56mod error;
57mod id;
58mod id_class;
59mod ided;
60mod identifiable;
61
62#[cfg(feature = "sqlx")]
63mod postgres;
64
65#[cfg(feature = "serde")]
66mod id_enum;
67#[cfg(feature = "jsonschema")]
68mod jsonschema;
69#[cfg(feature = "openapi")]
70mod openapi;
71#[cfg(feature = "serde")]
72mod serde_serialize;
73
74#[allow(unused_imports)]
75pub use {error::*, id::*, id_class::*, ided::*, identifiable::*, kind_proc::*};
76
77#[allow(unused_imports)]
78#[cfg(feature = "serde")]
79pub use {crate::serde_serialize::*, id_enum::*};
80
81#[allow(unused_imports)]
82#[cfg(feature = "jsonschema")]
83pub use crate::jsonschema::*;
84
85#[allow(unused_imports)]
86#[cfg(feature = "openapi")]
87pub use crate::openapi::*;
88
89#[test]
90fn test_id_ided() {
91    #[derive(Debug, Identifiable)]
92    #[kind(class = "Cust")]
93    pub struct Customer {
94        pub name: String,
95    }
96
97    #[derive(Debug, Identifiable)]
98    #[kind(class = "Cont")]
99    pub struct Contract {
100        // many fields
101    }
102
103    // It's costless: the kind is handled by the type system
104    // and doesn't clutter the compiled binary
105    assert_eq!(
106        std::mem::size_of::<Id<Customer>>(),
107        std::mem::size_of::<uuid::Uuid>(),
108    );
109
110    // You can parse the id from eg JSON, or just a string
111    let id: Id<Customer> = "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a".parse().unwrap();
112
113    // The type is checked, so this id can't be misused as a contract id
114    assert!("Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
115        .parse::<Id<Contract>>()
116        .is_err());
117
118    // The public id is parsed and checked in a case insensitive way
119    assert_eq!(
120        id,
121        "cust_371c35ec-34d9-4315-ab31-7ea8889a419a".parse().unwrap()
122    );
123    assert_eq!(
124        id,
125        "CUST_371C35EC-34D9-4315-AB31-7EA8889A419A".parse().unwrap()
126    );
127
128    // You can build an identified object from just
129    // Here's a new customer:
130    let new_customer = Customer {
131        name: "John".to_string(),
132    };
133    // Give it an id, by wrapping it in an Ided
134    let customer = Ided::new(id, new_customer);
135
136    assert_eq!(customer.name, "John");
137    assert_eq!(
138        customer.id().to_string(),
139        "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
140    );
141}
142
143#[cfg(feature = "serde")]
144#[test]
145fn test_serde() {
146    // deserialize a customer
147    #[derive(Debug, Identifiable, serde::Serialize, serde::Deserialize)]
148    #[kind(class = "Cust")]
149    pub struct Customer {
150        pub name: String,
151    }
152
153    let json = r#"{
154        "id": "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a",
155        "name": "John"
156    }"#;
157
158    let customer: Ided<Customer> = serde_json::from_str(json).unwrap();
159    assert_eq!(customer.entity().name, "John");
160    assert_eq!(
161        customer.id().to_string(),
162        "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
163    );
164
165    // id kind is checked: this one fails because the prefix of the
166    // id is wrong
167    let json = r#"{
168        "id": "Con_371c35ec-34d9-4315-ab31-7ea8889a419a",
169        "name": "John"
170    }"#;
171    assert!(serde_json::from_str::<Ided<Customer>>(json).is_err());
172
173    assert_eq!(
174        serde_json::to_string(&customer).unwrap(),
175        r#"{"id":"Cust_371c35ec-34d9-4315-ab31-7ea8889a419a","name":"John"}"#,
176    );
177}