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. Nevertheless, both macros can be used indenpendently from each other.
Add the following to your Cargo.toml
to include enpow
as dependency to your project.
toml
[dependencies]
enpow = "~2.0.0"
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.
```rust /// A log entry
pub enum LogEntry
/// Application log for a certain context type
pub struct Log
implLogEntry::Error
from the log
pub fn get_errors(&self) -> Vec
/// 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.geterrors(); if !errors.isempty() { 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.
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.
```rust use enpow::enpow; // ℹ️
/// A log entry
pub enum LogEntry
}
/// Application log for a certain context type
pub struct Log
implLogEntry::Error
from the log
pub fn geterrors(&self) -> Vec
/// Line number in source type Line = usize;
// Create a sample log let log = Log { entries: vec![ // ✂ unchanged
] };
// Get and print all errors let errors = log.geterrors(); if !errors.isempty() { 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>
.
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.
```rust use enpow::{enpow, extract}; // ℹ️
/// A log entry
pub enum LogEntry
}
/// Application log for a certain context type
pub struct Log
implLogEntry::Error
from the log
pub fn geterrors(&self) -> Vec
/// 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.geterrors(); if !errors.isempty() { 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.
```rust use enpow::{enpow, extract};
/// A log entry
pub enum LogEntry
}
/// Application log for a certain context type
pub struct Log
implLogEntry::Error
from the log
pub fn geterrors(&self) -> Vec
/// 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.geterrors(); if !errors.isempty() { 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.
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.
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.
© 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