Crate microtype

Source
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 optionally Serialize and Deserialize) 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 (and String) will not be able to zeroize previous allocations)
  • when using serde, secret microtypes do not implement Serialize, 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: various fmt 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 derive Serialize and Deserialize, and will be #[serde(transparent)]
  • deref_impls - some people argue that implementing Deref and DerefMut on a non-pointer container is unidiomatic. Others prefer the ergonomics of being able to call associated functions more easily. If deref_impls is enabled, microtypes will deref to their inner types
  • test_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
  • secret - enables secret microtypes, discussed below:
  • diesel - if enabled, any attribtes of the form #[diesel(sql_type = ...)] will be captured, and FromSql and ToSql 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
SecretMicrotype
A trait implemented by secret microtypes