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 and From 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.