Crates.io | typetrait |
lib.rs | typetrait |
version | 0.1.1 |
source | src |
created_at | 2021-12-25 19:45:44.876091 |
updated_at | 2021-12-28 16:09:31.311237 |
description | Helper macro to generate types for typestate programming |
homepage | |
repository | |
max_upload_size | |
id | 503056 |
size | 12,974 |
Helper macro for generating boilerplate for more type safe APIs.
For example, consider an API that receives user input:
struct Data {
email: String,
age: u8,
}
fn get_data() -> Data {
todo!()
}
We might want to make sure that we have validated that data.email
contains a valid email address before we use it.
This is possible by making Data
generic over "whether or not it has been validated":
// generates a trait called Status, and types Validated and Unvalidated that implement Status
union! {
pub Status = Validated | Unvalidated
}
struct Data<T: Status> {
_marker: std::marker::PhantomData<T>,
email: String,
age: u8,
}
With this setup, we can now prevent unvalidated data from being used at compile time:
// data received from user input is considered unvalidated
fn get_data() -> Data<Unvalidated> {
todo!()
}
// only validated data should be used by the rest of the application
fn handle_data(data: Data<Validated>) {
todo!()
}
// convert unvalidated data to validated data (by validating it!)
fn validate(Data { email, age, .. }: Data<Unvalidated>) -> Result<Data<Validated>, &'static str> {
if !is_valid_email(&email) {
return Err("invalid email");
}
Ok(Data {
email,
age,
_marker: std::marker::PhantomData,
})
}
This API is now significantly harder to misuse. Instead of requiring the programmer to keep track of which data is validated and which isn't, this work is offloaded to the compiler.
This pattern is widely applicable, for example:
Validated
or Unvalidated
SessionToken<Validated>
could be required to make a particular database requestEnabled
or Disabled
, as well as Input
or Output
(see https://docs.rust-embedded.org/book/design-patterns/hal/gpio.html for a more thorough example)Initialized
/Uninitialized
/Disposed
)Each trait generated by union!
is "sealed", meaning it cannot be implemented outside of the module they are defined in. They generate a private module with a supertrait which is implemented for only the types given in the macro.
Each type is an empty enum (i.e. an enum with no variants) and so cannot be instantiated. Note that these types are almost exclusively used with PhantomData
, since they only ever exist as concrete values for type parameters, but never as values themselves (there are no possible values of an empty enum).