This crate provides a way to define type wrappers (guards) that behave as close as possible to the underlying type, but guarantee to uphold arbitrary invariants at all times.
The name comes from latin praesidio, which means guard.
Basic usage
The simplest way to create a guard is to use the define! macro.
Let's create a Text type. It will be a wrapper around a String with an invariant that the
value is not empty:
use ;
// Here we define our new type.
// `pub` - the visibility of our type;
// `Text` - the name of our type;
// `String` - the underlying type;
// `ensure $closure` - the closure that returns `true` if the value is valid.
define!;
// We can easily create a value of our type. Note that the type of the agrument
// is not `String`. That is because `Text::new(...)` accepts anything
// that is `Into<String>`.
let mut t = new.unwrap;
assert_eq!;
// One way to mutate the value is to call the `mutate` method.
// See docs for `prae::Guard` to learn about other methods.
t.mutate;
assert_eq!;
// Creating an invalid value is not possible.
assert!;
Okay, we're getting there. Right now our type will accept every non-empty string and reject any zero-length string. But does it really protect us from a possible abuse? The following example shows us, that the answer is no:
# use ;
# define!;
// Technically not empty, but makes no sense as a "text" for a human.
assert!;
// Trailing whitespace should not be allowed either.
assert!;
One way to solve this is to tell the user of our type to always trim the string before any
construction/mutation of the Text. But this can go wrong very easily. It would be better if
we could somehow embed this behaviour into our type. And we can!
use ;
define!
// This won't work anymore.
assert!;
// And this will be automatically adjusted.
let t = new.unwrap;
assert_eq!;
That's a lot better!
Custom errors
Both ConstructionError and MutationError are
useful wrappers around some inner error. They provide access to the values that were in play when
the error occured. By default (when you use ensure closure inside the
define!), the inner error is just &'static str with the default error message.
Sometimes, however, we might want to use our own type. In this case, we should use validate
instead of ensure:
use ;
define!
assert!;
assert!;
assert!;
assert!;
Extending our types
If you want to reuse adjustment/validation behaviour of some type in a new type, you should use
extend!. It's just like define!, but it's inner type should be Guard.
use ;
define!
extend!
assert!;
assert!;
assert!;
assert!;
Integration with serde
If you enable serde feature, every Guard will automatically implement
Serialize and
Deserialize if its inner type implements them.
Deserialization will automatically return an error if the data is not invalid.
Here is an example of some API implemented using axum:
use prae::{define, Guard};
use axum::{extract, handler::post, Router};
define! {
Text: String
adjust |t| *t = t.trim().to_owned()
validate |t| !t.is_empty()
}
async fn save_text(extract::Json(text): extract::Json<Text>) {
// Our `text` was automatically validated. We don't need to
// do anything manually at all!
// ...
}
let app = Router::new().route("/texts", post(save_text));
Optimising for performance
If you find yourself in a situation where always adjusting/validating values of your type
is a performance issue, you can opt in to avoid those extra calls using methods under the
unchecked feature gate. Those methods (new_unchecked, mutate_unchecked, etc.) don't
adjust/validate your values at all, making you responsible for the validity of your data.