macro_rules! define {
{
$(#[$meta:meta])*
$vis:vis $wrapper:ident: $inner:ty;
$(adjust $adjust:expr;)?
$(ensure $ensure:expr;)?
$(validate($err:ty) $validate:expr;)?
$(plugins: [$($plugin:path),+ $(,)?];)?
} => { ... };
{
$(adjust $adjust:expr;)?
} => { ... };
{
$(adjust $adjust:expr;)?
ensure $ensure:expr;
} => { ... };
{
$(adjust $adjust:expr;)?
validate($err:ty) $validate:expr;
} => { ... };
}
Expand description
Convenience macro that creates a
Newtype
wrapper struct that implements Wrapper
.
The macro accepts several arguments (see macro structure for more info). By default, it generates a bare minimum of code:
- The
Newtype
struct; - The implementation of the
Wrapper
for the struct; - The implementation of the
AsRef
;Borrow
,TryFrom
andFrom
traits for the struct.
However, the generated code can be extended in using two methods:
- Attribute macros attached to the type signature (e.g.
#[derive(Debug)]
); - Type plugins specified in the end of the macro.
It is worth noting that the inner value of created Newtype
struct can be
accessed from the code in the same module. To fully protect this value from
being accessed directly, put your type in a separate module.
Macro structure
Table of contents:
Type signature
This is the only required argument of the macro. It specifies the visibiliy and the name of the created struct, as well as it’s inner type. For example, this
prae::define! {
/// An ID of a user.
pub UserID: i64;
}
prae::define! {
/// A name of a user.
Username: String;
}
will expand into this:
/// An ID of a user.
pub struct UserID(i64);
// other impls...
/// A name of a user.
struct Username(String);
// other impls...
You could also use attribute macros on top of your signature if you like. For example, this
prae::define! {
#[derive(Debug, Clone)]
pub Username: String;
}
will expand into this:
#[derive(Debug, Clone)]
pub struct Username(String);
// other impls...
Meaning that your type now implements Debug
and Clone
.
Note: check out
derive_more
for more derive macros.
adjust
closure
This argument specifies a closure that will be executed on every construction and mutation of the wrapper to make sure that it’s value is adjusted properly. For example:
use prae::Wrapper;
prae::define! {
#[derive(Debug)]
pub Text: String;
adjust |text: &mut String| *text = text.trim().to_owned();
}
let mut text = Text::new(" hello world! ").unwrap();
assert_eq!(text.get(), "hello world!");
text.set(" new value\n\n\n").unwrap();
assert_eq!(text.get(), "new value");
ensure
closure
This argument specifies a closure that will be executed on every construction and mutation of the wrapper to make sure that it’s value is valid. For example:
use prae::Wrapper;
prae::define! {
#[derive(Debug)]
pub Text: String;
ensure |text: &String| !text.is_empty();
}
assert!(Text::new("hello world").is_ok());
assert!(Text::new("").is_err());
As you can see, the closure receives a shared reference to the inner value
and returns true
if the value is valid, and false
if it’s not.
This closure is easy to use, but it has a downside: you can’t customize your
error type. The Wrapper::Error
type will always
be a &'static str
with a generic error message:
let err = Text::new("").unwrap_err();
assert_eq!(err.original, "value is invalid");
assert_eq!(
err.to_string(),
"failed to construct type Text from value \"\": value is invalid",
)
If you want more control, use validate
closure
closure described below.
Note:
- this closure can be used together with the
adjust
closure and will be executed after it; - this closure can’t be used together with the
validate
closure.
validate
closure
This closure is similar to the ensure
closure, but uses
custom error specified by the user:
use ::core::fmt;
use prae::Wrapper;
#[derive(Debug)]
pub enum TextError {
Empty,
}
// Required in order for `err.to_string()` to work.
impl fmt::Display for TextError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", match self {
Self::Empty => "text is empty",
})
}
}
prae::define! {
#[derive(Debug)]
pub Text: String;
validate(TextError) |text: &String| {
if text.is_empty() {
Err(TextError::Empty)
} else {
Ok(())
}
};
}
let err = Text::new("").unwrap_err();
assert!(matches!(err.original, TextError::Empty));
assert_eq!(
err.to_string(),
"failed to construct type Text from value \"\": text is empty",
)
As you can see, the closure receives a shared reference to the inner value
and returns Ok(())
if the value is valid, and Err(...)
if it’s not.
Note:
- this closure can be used together with the
adjust
closure and will be executed after it; - this closure can’t be used together with the
ensure
closure.
Plugins
Sometimes attribute macros just dont’t cut it. In this case, you have two options:
- add manual
impl
to your type; - use plugins.
In the context of this macro, plugin is just a macro that takes your type as an input and does something with it.
For example, suppose we want our type to implement
serde::Serialize
and
serde::Deserialize
. We could use attribute
macros:
use serde::{Serialize, Deserialize};
prae::define! {
#[derive(Serialize, Deserialize)]
pub Username: String;
adjust |un| *un = un.trim().to_owned();
ensure |un| !un.is_empty();
}
However, this implementation won’t use our adjust
and ensure
closures
for the type deserialization. This means, that we can create Username
with
invalid data:
// This won't work
assert!(Username::new(" ").is_err());
// But this will
let un: Username = serde_json::from_str("\" \"").unwrap();
assert_eq!(un.get(), " "); // not good
To avoid this, we need to add a custom implementation of
serde::Deserialize
for our type. Since the
implementation is indentical for any type created with this crate, prae
ships with a built-in (under serde
feature) plugin called
impl_serde
. This plugin will implement both
serde::Serialize
and
serde::Deserialize
the right way:
use prae::Wrapper;
use serde::{Serialize, Deserialize};
prae::define! {
#[derive(Debug)]
pub Username: String;
adjust |un| *un = un.trim().to_owned();
ensure |un| !un.is_empty();
plugins: [
prae::impl_serde,
];
}
// This will work
let un: Username = serde_json::from_str("\" qwerty \"").unwrap();
assert_eq!(un.get(), "qwerty");
// But this won't
let err = serde_json::from_str::<Username>("\" \"").unwrap_err();
assert_eq!(err.to_string(), "value is invalid");
You can implement your own plugins and use them for your types - it’s easy.