4 releases
0.0.4 | Nov 23, 2022 |
---|---|
0.0.3 | Nov 22, 2022 |
0.0.2 | Nov 21, 2022 |
0.0.1 | Nov 18, 2022 |
#2049 in Procedural macros
20KB
299 lines
Experimental proc-macro generating partially inhabited enums from a template enum and valid morphisms between those enums. The goal is to define an enum with all possible variants once and generate the partial enums to constrain different APIs to different variant subsets, without having to redefine new enums for each API. Generated morphisms can then be used to convert between those different enums and easily compose APIs.
lib.rs
:
A proc-macro for generating partial enums from a template enum. This partial enum contains the same number of variants as the template but can disable a subset of these variants at compile time. The goal is used specialize enum with finer-grained variant set for each API.
This is useful for handling errors. A common pattern is to define an enum with all possible errors and use this for the entire API surface. Albeit simple, this representation can fail to represent exact error scenarii by allowing errors that can not happen.
Take an API responsible for decoding messages from a socket.
enum Error {
Connect(ConnectError),
Read(ReadError),
Decode(DecodeError),
}
fn connect() -> Result<Socket, Error> {
Ok(Socket)
}
fn read(sock: &mut Socket) -> Result<Bytes, Error> {
Ok(Bytes)
}
fn decode(bytes: Bytes) -> Result<Message, Error> {
Err(Error::Decode(DecodeError))
}
The same error enum is used all over the place and exposes variants that do
not match the API: decode
returns a DecodeError
but nothing prevents
from returning a ConnectError
. For such low-level API, we could substitute
Error
by their matching error like ConnectError
for connect
. The
downside is that composing with such functions forces us to redefine custom
enums:
enum NextMessageError {
Read(ReadError),
Decode(DecodeError),
}
impl From<ReadError> for NextMessageError {
fn from(err: ReadError) -> Self {
NextMessageError::Read(err)
}
}
impl From<DecodeError> for NextMessageError {
fn from(err: DecodeError) -> Self {
NextMessageError::Decode(err)
}
}
fn read(sock: &mut Socket) -> Result<Bytes, ReadError> {
Ok(Bytes)
}
fn decode(bytes: Bytes) -> Result<Message, DecodeError> {
Err(DecodeError)
}
fn next_message(sock: &mut Socket) -> Result<Message, NextMessageError> {
let payload = read(sock)?;
let message = decode(payload)?;
Ok(message)
}
This proc-macro intend to ease the composition of APIs that does not share the exact same errors by generating a new generic enum where each variant can be disabled one by one. We can then redefine our API like so:
#[derive(partial_enum::Enum)]
enum Error {
Connect(ConnectError),
Read(ReadError),
Decode(DecodeError),
}
use partial::Error as E;
fn connect() -> Result<Socket, E<ConnectError, !, !>> {
Ok(Socket)
}
fn read(sock: &mut Socket) -> Result<Bytes, E<!, ReadError, !>> {
Ok(Bytes)
}
fn decode(bytes: Bytes) -> Result<Message, E<!, !, DecodeError>> {
Err(DecodeError)?
}
fn next_message(sock: &mut Socket) -> Result<Message, E<!, ReadError, DecodeError>> {
let payload = read(sock)?;
let message = decode(payload)?;
Ok(message)
}
Notice that the next_message
implementation is unaltered and the signature
clearly states that only ReadError
and DecodeError
can be returned. The
callee would never be able to match on Error::Connect
. The decode
implementation
uses the ?
operator to convert DecodeError
to the partial enum. By using the
nightly feature exhaustive_patterns
, the match statement does not even
need to write the disabled variants.
#![feature(exhaustive_patterns)]
fn read_one_message() -> Result<Message, Error> {
let mut socket = connect()?;
match next_message(&mut socket) {
Ok(msg) => Ok(msg),
Err(E::Read(_)) => {
// Retry...
next_message(&mut socket).map_err(Error::from)
}
Err(E::Decode(err)) => Err(Error::Decode(err)),
}
}
Rust version
By default, the empty placeholder is the unit type ()
. The generated code
is compatible with the stable compiler. When the never
feature is enabled,
the never type !
is used instead. This requires a nightly compiler and the
nightly feature #![feature(never_type)]
.
Dependencies
~1.5MB
~35K SLoC