Expand description
A library to generate “microtypes” (A.K.A. “newtypes”). Opinionated in favour of ergonomics over maximum flexibility.
A microtype is a thin wrapper around an underlying type, that helps disambiguate similar uses of the same type
For example, consider the following code from an imaginary e-commerce web backend:
fn handle_order(user_id: String, order_id: String) {
// ...
}
fn main() {
let user_id = retrieve_user_id();
let order_id = retrieve_order_id();
handle_order(order_id, user_id);
}
This code compiles, but has a bug: the order_id
and user_id
are used in the wrong order.
This example is fairly trivial and easy to spot, but the larger a project gets, the harder it
becomes to detect these issues. This becomes especially troublesome if you want to refactor. For
example, if you wanted to swap the order of the arguments, you’d have to make sure you visited
all the calls to this function and swapped the arguments manually. Luckily, we can get the
compiler to help with this.
Microtypes solve this problem. They wrap some inner type, and allow the compiler to distinguish between different uses of the same underlying type. For example, we could rewrite the earlier example as:
// Generate wrappers around String called UserId and OrderId
microtype! {
String {
UserId,
OrderId,
}
}
fn handle_order(user_id: UserId, order_id: OrderId) {
// ...
}
fn main() {
let user_id: OrderId = retrieve_user_id();
let order_id: UserId = retrieve_order_id();
handle_order(order_id, user_id); // Error: incompatible types
}
Excellent, a run-time error has been turned into a compile time error.
§Basic usage
To declare microtypes, use the [‘microtype::microtype’] macro:
microtype! {
// these attributes apply to all microtypes defined in this block
#[derive(Debug, Clone)]
String {
#[derive(PartialEq)]
UserId, // implements Debug, Clone and PartialEq
Username, // only implements Debug and Clone
}
// multiple inner types can be used in a single macro
i64 {
Timestamp
}
// use the `#[secret]` attribute to mark a type as "secret"
#[secret]
String {
Password
}
// use `#[secret(serialize)]` to make a secret type implement serde::Serialize
String {
SessionToken
}
}
fn main() {
let user_id = UserId::new("id".into()); // create new UserId
let string = user_id.into_inner(); // consume UserId, return inner String
let username = Username::new(string); // create new Username
// sometimes you need to explicitly change the type of a value:
let user_id: UserId = username.convert();
// microtypes also optionally implement Deref
let length = user_id.len();
assert_eq!(length, 2);
}
§Secrets
Some types may be considered “sensitive” (for example: passwords, session tokens, etc).
For this purpose, microtypes can be marked as #[secret]
:
microtype! {
#[secret]
String {
Password
}
}
fn main() {
let password = Password::new("secret password".to_string());
assert_eq!(password.expose_secret(), "secret password");
}
Secret microtypes don’t implement Microtype
, instead they implement
SecretMicrotype
, which has a much more restrictive API:
- Mutable and owned access to the inner data is not possible, it is only possible to get a
shared reference to the inner data via
secrecy::ExposeSecret::expose_secret
, which makes accesses easier to audit. - They
#[derive(Debug, Clone)]
(and optionallySerialize
andDeserialize
) but do not support adding extra derive macros.
Internally, they wrap the contained data in secrecy::Secret
, which provides some nice
safety features. In particular:
- The debug representation is redacted. This is can prevent against accidentally leaking
data to logs, but it still has a
Debug
implementation (so you can still#[derive(Debug)]
on structs which contain secret data) - Data is zeroized after use, meaning the underlying data is overwritten with 0s, which
ensures sensitive data exists in memory only for as long as is needed. (Caveat: not all types
have perfect zeroize implementations. Notably
Vec
(andString
) will not be able to zeroize previous allocations) - when using
serde
, secret microtypes do not implementSerialize
, to avoid accidentally leaking secret data
§Serializable Secrets
The fact that secret microtypes do not implement Serialize
can be overly restrictive
sometimes. There are many types (e.g. session tokens) which are sensitive enough to warrant
redacting their debug implementation, but also need to be serialized. For types like this, you
can use #[secret(serialize)]
to make the type implement Serialize
.
microtype! {
#[secret(serialize)]
String {
SessionToken
}
}
#[derive(Serialize)]
struct LoginResponse {
token: SessionToken
}
§Type Hints
Proc-macros are run before type information is available, so can only use the text of the
invocation. Given that, a proc-macro can’t distinguish between the String
type provided by
the standard library and a custom struct String;
. You can use a type hint to mark a microtype
as wrapping a well-known type, to generate more helpful implementations for you:
- If the wrapped type is a
String
, you can use#[string]
to provide a few extra implementations (e.g.FromStr
,From<&str>
,Display
) - If the wrapped type is an integer type, you can use
#[int]
to provide other extra implementations: variousfmt
traits (e.g.UpperHex
, etc), as well as arithmetic traits (Add
,AddAssign
, etc). These are incomplete, please open a PR/issue if there are implementations you rely on that are missing
For example:
microtype! {
#[string]
String {
Email
}
#[int]
i32 {
Num
}
}
fn main() {
let email = Email::from("email");
let num = Num::from(123);
println!("{email}");
println!("display: {num}, hex: {num:x}");
}
§Feature flags
The following feature flags are provided, to help customize the behaviour of the types creates:
serde
- when enabled, any type created will deriveSerialize
andDeserialize
, and will be#[serde(transparent)]
deref_impls
- some people argue that implementingDeref
andDerefMut
on a non-pointer container is unidiomatic. Others prefer the ergonomics of being able to call associated functions more easily. Ifderef_impls
is enabled, microtypes will deref to their inner typestest_impls
- makes secret microtypes easier to work with in test environments by:- making their
Debug
implmentation print their actual value instead of"REDACTED"
- making them derive
PartialEq
- making their
secret
- enables secret microtypes, discussed below:diesel
- if enabled, any attribtes of the form#[diesel(sql_type = ...)]
will be captured, andFromSql
andToSql
implementations will be generated. Note, you will generally also want to#[derive(AsExpression, FromSqlRow)]
Re-exports§
pub use secrecy;
Macros§
- microtype
- Macro to create microtype wrappers
Traits§
- Microtype
- A trait implemented by microtypes
- Secret
Microtype - A trait implemented by secret microtypes