6 stable releases
2.0.2 | Oct 3, 2023 |
---|---|
2.0.1 | Feb 16, 2023 |
2.0.0 | Sep 7, 2022 |
1.0.2 | Sep 1, 2022 |
1.0.0 | Aug 31, 2022 |
#271 in Build Utils
105KB
2K
SLoC
EnPow
EnPow is a procedural macro crate used to enPower user defined Enums with many methods usually known from the standard library's Result<T, E>
and Option<T>
. It can generate methods like fn is_<variant>(&self) -> bool
or fn unwrap_<variant>(self) -> <inner>
, supporting variants with named or unnamed fields (or none), as well as generics. See the enpow
macro documentation for details on the specific methods supported.
Additionally, this crate allows to extract the data associated with each enum variant into separate structs, allowing for more compact code e.g. when designing an Abstract Syntax Tree. See the extract
macro documentation for more details.
It is also possible to combine both macros when keeping them in the right order: first extract
and then enpow
. Combining both macros avoids generating separate structs for Ref
or Mut
struct variants as further explained in the following use case demonstration. Nevertheless, both macros can be used indenpendently from each other.
Installation
⚠️ The minimum supported Rust toolchain version is Rust 1.58.0
Add the following to your Cargo.toml
to include enpow
as dependency to your project.
[dependencies]
enpow = "~2.0.2"
Alternatively, run following command inside your crate's root directory.
cargo add enpow
Quick Start
Enpow
To generate all available methods for an enum, import the enpow
macro and add #[enpow(All)]
in front of the enum's definition. It is also possible to selectively generate certain methods. For more information, see the following use case demonstration or the documentation for the enpow
macro.
use enpow::enpow;
#[enpow(All)]
pub enum IpAddress {
None,
V4(u8, u8, u8, u8),
V6(String),
Multi {
v4: (u8, u8, u8, u8),
v6: String,
},
}
Some of the generated methods are:
fn v4(self) -> Option<(u8, u8, u8, u8)>
fn is_none(&self) -> bool
fn unwrap_v6(self) -> String
fn expect_multi(self, msg: &str) -> IpAddressMulti
- ...
Besides these methods, also structs are generated as needed, like the following:
pub struct IpAddressMulti {
pub v4: (u8, u8, u8, u8),
pub v6: String,
}
Extract
To extract all variants of an enum into separate types, import the extract
macro and add #[extract(All)]
in front of the enum's definition. It is also possible to select certain variants for extraction. For more information see the following use case demonstration or the documentation for the extract
macro.
use enpow::extract;
#[extract(All)]
pub enum IpAddress {
None,
V4(u8, u8, u8, u8),
V6(String),
Multi {
v4: (u8, u8, u8, u8),
v6: String,
},
}
This will generate the following data types:
pub enum IpAddress {
None(IpAddressNone),
V4(IpAddressV4),
V6(IpAddressV6),
Multi(IpAddressMulti),
}
pub struct IpAddressNone;
pub struct IpAddressV4(pub u8, pub u8, pub u8, pub u8);
pub struct IpAddressV6(pub String);
pub struct IpAddressMulti {
pub v4: (u8, u8, u8, u8),
pub v6: String,
}
Use Case
The following code describes a simple logging system with support for different log levels. We then create an example log and print all errors in it.
/// A log entry
#[derive(Clone)]
pub enum LogEntry<C: ToString + Clone> {
/// A simple note without context
Note(
/// Note's message
String
),
/// A warning with a given context
Warning(
/// Warning's message
String,
/// Context of the warning
C
),
/// An error message with error code and context
Error {
/// Error message
message: String,
/// Context of the error
context: C,
/// Error code
code: i16,
},
}
/// Application log for a certain context type
pub struct Log<C: ToString + Clone> {
/// Log entries
entries: Vec<LogEntry<C>>,
}
impl<C: ToString + Clone> Log<C> {
/// Collects all entries of type `LogEntry::Error` from the log
pub fn get_errors(&self) -> Vec<LogEntry<C>> {
self.entries.iter()
.filter(|entry| match entry {
LogEntry::Error { .. } => true,
_ => false,
})
.cloned()
.collect()
}
}
/// Line number in source
type Line = usize;
// Create a sample log
let log = Log { entries: vec![
LogEntry::Note("All fine 😊".into()),
LogEntry::Warning("There might be an issue here 🤔".into(), 4),
LogEntry::Error {
message: "There _was_ an issue 😖".into(),
context: 4,
code: -1,
},
LogEntry::Error {
message: "Follow up".into(),
context: 12,
code: -7,
},
] };
// Get and print all errors
let errors = log.get_errors();
if !errors.is_empty() {
eprintln!("Failed for the following reasons:");
for error in errors {
match error {
LogEntry::Error { message, context: line, code } => {
eprintln!("Error {code} at {line}: {message}");
}
_ => panic!("Expected to find a LogEntry::Error"),
}
}
}
This code works, but it is a bit wordy having to pattern match against the specific variant of each log entry every time.
Using enpow
Here comes the enpow
macro into play. It can generate some helper methods that should make our code more concise. We specifically make use of the is_<variant>()
(keyword IsVar
) and unwrap_<variant>()
(keyword UnwrapVar
) methods.
use enpow::enpow; // ℹ️
/// A log entry
#[enpow(IsVar, UnwrapVar)] // ℹ️
#[derive(Clone)]
pub enum LogEntry<C: ToString + Clone> {
/// A simple note without context
Note(
/// Note's message
String
),
/// A warning with a given context
Warning(
/// Warning's message
String,
/// Context of the warning
C
),
/// An error message with error code and context
Error {
/// Error message
message: String,
/// Context of the error
context: C,
/// Error code
code: i16,
},
}
/// Application log for a certain context type
pub struct Log<C: ToString + Clone> {
/// Log entries
entries: Vec<LogEntry<C>>,
}
impl<C: ToString + Clone> Log<C> {
/// Collects all entries of type `LogEntry::Error` from the log
pub fn get_errors(&self) -> Vec<LogEntry<C>> {
self.entries.iter()
.filter(|entry| entry.is_error()) // ℹ️
.cloned()
.collect()
}
}
/// Line number in source
type Line = usize;
// Create a sample log
let log = Log { entries: vec![
LogEntry::Note("All fine 😊".into()),
LogEntry::Warning("There might be an issue here 🤔".into(), 4),
LogEntry::Error {
message: "There _was_ an issue 😖".into(),
context: 4,
code: -1,
},
LogEntry::Error {
message: "Follow up".into(),
context: 12,
code: -7,
},
] };
// Get and print all errors
let errors = log.get_errors();
if !errors.is_empty() {
eprintln!("Failed for the following reasons:");
for error in errors {
let error = error.unwrap_error(); // ℹ️
eprintln!("Error {} at {}: {}", error.code, error.context, error.message);
}
}
Even though this code is already more concise, there is still a rough edge. When collecting all the errors, they still are returned as Vec<LogEntry>
with no guarantee by the type system that this collection would actually contain only errors. We could solve this problem by returning from get_errors()
the type LogEntryError<C>
that was already constructed to aid the unwrap_error()
method.
Currently, however, there are multiple (depending on the generated methods, up to four) parts in the code where the same data is defined: in the enum variant LogEntry::Error { .. }
, and in the just mentioned, automatically generated struct LogEntryError<C>
. It might be more desireable to extract the enum variant into this struct and let the variant just point to it, like so: LogEntry::Error(LogEntryError<C>)
. This would especially come in handy when working with methods that need references to the variant's associated data, since up until now, special LogEntryErrorRef<C>
and LogEntryErrorMut<C>
structs need to be generated for that. With the data extracted, it would be possible to just use &LogEntryError<C>
and &mut LogEntryError<C>
.
Using extract
Here, the extract
macro comes into play, which does this automatically for us. We tell the macro to do the extraction for every variant with more than one unnamed field (keyword Unnamed
) or with named fields (keyword Named
). This will automatically change our two affected variants into LogEntry::Warning(LogEntryWarning<C>)
and LogEntry::Error(LogEntryError<C>)
. We can then use the generated type LogEntryError<C>
to guarantee that actually only errors are returned by get_errors()
. Note, how the construction of the log entries changes, even though the enum code was not changed by hand. extract
also automatically implements the From
trait to convert instances of the extracted types into the corresponding enum variants.
Additionally, we make use of the method <variant>_as_ref()
(keyword VarAsRef
) to make collecting all error entries and unwrapping them more concise. To make the cloning of the automatically generated LogEntryError<C>
struct work, we add the inner(derive(Clone))
attribute.
⚠️ Macro Order: When combining both macros,
enpow
must be placed afterextract
to work correctly. If the helper attributeinner
is placed betweenextract
andenpow
, it will only be effective forextract
. If it is placed after bothextract
andenpow
, it will be effective for both macros. Also, the normalderive
macro must be placed afterextract
to work correctly.
use enpow::{enpow, extract}; // ℹ️
/// A log entry
#[extract(Unnamed, Named)] // ℹ️
#[enpow(VarAsRef)] //
#[inner(derive(Clone))] //
#[derive(Clone)]
pub enum LogEntry<C: ToString + Clone> {
/// A simple note without context
Note(
/// Note's message
String
),
/// A warning with a given context
Warning(
/// Warning's message
String,
/// Context of the warning
C
),
/// An error message with error code and context
Error {
/// Error message
message: String,
/// Context of the error
context: C,
/// Error code
code: i16,
},
}
/// Application log for a certain context type
pub struct Log<C: ToString + Clone> {
/// Log entries
entries: Vec<LogEntry<C>>,
}
impl<C: ToString + Clone> Log<C> {
/// Collects all entries of type `LogEntry::Error` from the log
pub fn get_errors(&self) -> Vec<LogEntryError<C>> { // ℹ️
self.entries.iter()
.filter_map(|entry| entry.error_as_ref()) // ℹ️
.cloned()
.collect()
}
}
/// Line number in source
type Line = usize;
// Create a sample log
let log = Log { entries: vec![
LogEntry::Note("All fine 😊".into()),
LogEntry::from(LogEntryWarning( // ℹ️
"There might be an issue here 🤔".into(),
4,
)),
LogEntry::from(LogEntryError { // ℹ️
message: "There _was_ an issue 😖".into(),
context: 4,
code: -1,
}),
LogEntry::from(LogEntryError { // ℹ️
message: "Follow up".into(),
context: 12,
code: -7,
}),
] };
// Get and print all errors
let errors = log.get_errors();
if !errors.is_empty() {
eprintln!("Failed for the following reasons:");
for error in errors {
// ℹ️
eprintln!("Error {} at {}: {}", error.code, error.context, error.message);
}
}
At last, we get rid of the long names of the generated structs by giving defining another pattern naming pattern. For this, the same inner()
helper attribute with the argument type_name
or type_names
is used. Besides, with inner()
it is also possible to change how the variant's name appears in its methods method_name="..."
, and it is possible to add auto derives to a single variant.
use enpow::{enpow, extract};
/// A log entry
#[extract(Unnamed, Named)]
#[enpow(VarAsRef)]
#[inner(type_names=Var, derive(Clone))] // ℹ️
#[derive(Clone)]
pub enum LogEntry<C: ToString + Clone> {
/// A simple note without context
Note(
/// Note's message
String
),
/// A warning with a given context
Warning(
/// Warning's message
String,
/// Context of the warning
C
),
/// An error message with error code and context
Error {
/// Error message
message: String,
/// Context of the error
context: C,
/// Error code
code: i16,
},
}
/// Application log for a certain context type
pub struct Log<C: ToString + Clone> {
/// Log entries
entries: Vec<LogEntry<C>>,
}
impl<C: ToString + Clone> Log<C> {
/// Collects all entries of type `LogEntry::Error` from the log
pub fn get_errors(&self) -> Vec<Error<C>> { // ℹ️
self.entries.iter()
.filter_map(|entry| entry.error_as_ref())
.cloned()
.collect()
}
}
/// Line number in source
type Line = usize;
// Create a sample log
let log = Log { entries: vec![
LogEntry::Note("All fine 😊".into()),
LogEntry::from(Warning( // ℹ️
"There might be an issue here 🤔".into(),
4,
)),
LogEntry::from(Error { // ℹ️
message: "There _was_ an issue 😖".into(),
context: 4,
code: -1,
}),
LogEntry::from(Error { // ℹ️
message: "Follow up".into(),
context: 12,
code: -7,
}),
] };
// Get and print all errors
let errors = log.get_errors();
if !errors.is_empty() {
eprintln!("Failed for the following reasons:");
for error in errors {
eprintln!("Error {} at {}: {}", error.code, error.context, error.message);
}
}
This was just a quick introductory example for understanding the use and usage of this crate. See the macros' documentation for more details.
Inspiration and Alternatives
While the first plan for this crate was limited to simple unwrap
methods and alike, the crate variantly
was a great inspiration to take this idea way further. It can be seen as an alternative to this crate with partially different feature set.
Another alternative with partially different feature set is enum_variant_type
. Its main focus is generating separate types for every enum variant.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as below, without any additional terms or conditions.
License
© 2022 Florian Köhler.
This project is licensed at your option under either of
The SPDX license identifier for this project is MIT OR Apache-2.0
.
Licensing derived from arnavyc/dual-licensed-mit-apache
Dependencies
~1.5MB
~37K SLoC