6 useful Rust macros that you might not have seen before

Ben
4 min readApr 22, 2019

--

Below are 6 Rust macros that are worth taking a look at to improve your project. Rust macros are a great feature that can reduce code boilerplate and be a time saver for programmers. They also offer flexibility for developers to use metaprogramming to add new features to the language and package them in a way that is easy to integrate into code. They are one of the more powerful features of the language and this led me to search github and cargo to see what was out there. Below are some interest macros that are not as well known

1) Log-derive — Result logging

https://crates.io/crates/log-derive

#[logfn(ok = "TRACE", Err = "Error", fmt = "Failed fetching json: {:?}")]
fn fetch(url: &str) -> reqwest::Result<reqwest::Response> {
reqwest::get(url)
}

Here is a macro to log errors from a function. Add this attribute to a function returning a Result and with a single line you will enable logging of errors. Customise the log level and the message logged attached to both the Err or the Ok enum variants.

The code generated makes use of the log crate https://crates.io/crates/log.

2) Recap — regex parsing

https://crates.io/crates/recap

#[recap(regex = r#"(?x)
(?P<foo>\d+)
\s+
(?P<bar>true|false)
\s+
(?P<baz>\S+)
"#)]
struct LogEntry {
foo: usize,
bar: bool,
baz: String,
}
let entry: LogEntry = "1 true hello".parse()?;

Recap is an easy way to build data from regex strings. Each attribute of the struct matches a group in a regex string that is defined in the attribute tag. The macro then derives a FromStr trait that will populate the struct with data once the parse method is called on a string.

3) Shrinkwraprs — generate distinct types

https://crates.io/crates/shrinkwraprs

#[derive(Shrinkwrap)]
struct Email(String);
let email = Email("chiya+snacks@natsumeya.jp".into());
let is_discriminated_email =
email.contains("+"); // Woohoo, we can use the email like a string!

Shrinkwraprs redefines a datatype as a new distinct type. You can add the Shrinkwrap attribute to inherit all the behaviour of the embedded datatype. Useful if you want to use the power of the type checker to ensure the correctness of your code. Use this crate to create variants of a library type that have the same behaviour and data but are distinct types.

4) Metered

https://crates.io/crates/metered
This macro will automatically generate the following stats on a method

  • HitCount: number of times a piece of code was called
  • ErrorCount: number of errors returned – (works on any expression returning a Result)
  • InFlight: number of requests active
  • ResponseTime: statistics on the duration of an expression
  • Throughput: how many times an expression is called per second.
#[derive(Default, Debug, serde::Serialize)]
pub struct Biz {
metrics: BizMetrics,
}
#[metered(registry = BizMetrics)]
impl Biz {
#[measure([HitCount, Throughput])]
pub fn biz(&self) {
let delay = std::time::Duration::from_millis(rand::random::<u64>() % 200);
std::thread::sleep(delay);
}
}

In the struct you will add a metrics attribute and a new metrics type will be generated by the macro. For example:
metrics: BizMetrics,

Then on the impl the name of the attribute must match the metrics type
#[metered(registry = BizMetrics)]

Retrieve the metrics as serialised yaml:

serde_yaml::to_string(&*biz).unwrap();metrics:
biz:
hit_count: 1000
throughput:
- samples: 20
min: 35
max: 58
mean: 49.75
stdev: 5.146600819958742
90%ile: 55
95%ile: 55
99%ile: 58
99.9%ile: 58
99.99%ile: 58
- ~

5) Derive-new — generate constructors

https://github.com/nrc/derive-new

#[derive(new)]
struct Foo {
x: bool,
#[new(value = "42")]
y: i32,
#[new(default)]
z: Vec<String>,
}
let _ = Foo::new(true);

Derive-new is a simple way to add a constructor to a struct. This method will add an impl fn new(...) -> Self method generated from the structs attributes that will initialise the struct.

6) Snafu — error management

https://crates.io/crates/snafu

This crate has many helper functions for dealing with errors in Rust. Dealing with error can be verbose in Rust and having a library to reduce code and increase readable is something you may be seeking after starting a project. It has functions for generating Fromtraits for Errors to be used with the try operator (?), easy methods for embedding data related to the errors into the error struct, and helpers for generating display error messages. This is a well-designed error crate including much of the functionality a developer will need for error management in Rust.

#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("Could not open config from {}: {}", filename.display(), source))]
OpenConfig { filename: PathBuf, source: std::io::Error },
#[snafu(display("Could not save config to {}: {}", filename.display(), source))]
SaveConfig { filename: PathBuf, source: std::io::Error },
#[snafu(display("The user id {} is invalid", user_id))]
UserIdInvalid { user_id: i32, backtrace: Backtrace },
}
type Result<T, E = Error> = std::result::Result<T, E>;fn log_in_user<P>(config_root: P, user_id: i32) -> Result<bool>
where
P: AsRef<Path>,
{
let config_root = config_root.as_ref();
let filename = &config_root.join("config.toml");
let config = fs::read(filename).context(OpenConfig { filename })?;
// Perform updates to config
fs::write(filename, config).context(SaveConfig { filename })?;
ensure!(user_id == 42, UserIdInvalid { user_id }); Ok(true)
}

The content method is used to embed relevant information into the error.

If the enum has a source field, its context selector will have an implementation of From for a Context. This allows a developer to use the try operator (?) and the type will be converted into a Result enum.

fs::write(filename, config).context(SaveConfig { filename })?

Otherwise, the context selector will have a method fail to create a Result enum.

LoadNextPage { page_number }.fail() 

📝 Read this story later in Journal.

👩‍💻 Wake up every Sunday morning to the week’s most noteworthy stories in Tech waiting in your inbox. Read the Noteworthy in Tech newsletter.

--

--