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}