This crate provides a convenient macro that allows you to generate type wrappers that promise to always uphold arbitrary invariants that you specified.

Related tags

Miscellaneous prae
Overview

prae

crates.io version docs.rs crates.io license

This crate provides a convenient macro that allows you to generate type wrappers that promise to always uphold arbitrary invariants that you specified.

Usage

Let's create a Username type. It will be a wrapper around non-empty String:

prae::define!(pub Username: String ensure |u| !u.is_empty());

// We can't create an invalid username.
assert!(Username::new("").is_err());

// But we can create a valid one!
let mut u = Username::new("valid name").unwrap();
assert_eq!(u.get(), "valid name");

// We can mutate it:
assert!(u.try_mutate(|u| *u = "new name".to_owned()).is_ok());
assert_eq!(u.get(), "new name"); // our name has changed!

// But we can't make it invalid:
assert!(u.try_mutate(|u| *u = "".to_owned()).is_err());
assert_eq!(u.get(), "new name"); // our name hasn't changed!

// Let's try this...
assert!(Username::new("  ").is_ok()); // looks kind of invalid though :(

As you can see, the last example treats " " as a valid username, but it's not. We can of course do something like Username::new(s.trim()) every time, but why should we do it ourselves? Let's automate it!

prae::define! {
    pub Username: String
    adjust |u| *u = u.trim().to_string()
    ensure |u| !u.is_empty()
}

let mut u = Username::new(" valid name \n\n").unwrap();
assert_eq!(u.get(), "valid name"); // now we're talking!

// This also works for mutations:
assert!(matches!(u.try_mutate(|u| *u = "   ".to_owned()), Err(prae::ValidationError)));

Now our Username trims provided value automatically.

You might noticed that prae::ValidationError is returned by default when our construction/mutation fails. Altough it's convenient, there are situations when you might want to return a custom error. And prae can help with this:

#[derive(Debug)]
struct UsernameError;

prae::define! {
    pub Username: String
    adjust   |u| *u = u.trim().to_string()
    validate |u| -> Option<UsernameError> {
        if u.is_empty() {
            Some(UsernameError)
        } else {
            None
        }
    }
}

assert!(matches!(Username::new("  "), Err(UsernameError)));

Perfect! Now you can integrate it in your code and don't write .map_err(...) everywhere.

Serde integration

You can enable serde integration with the serde feature. It will implement Serialize and Deserialize traits for the wrappers if the underlying type implements them. The deserialization will automatically fail if it contains invalid value. Here is an example:

(r#"{ "username": " " }"#).unwrap_err(); assert_eq!(e.to_string(), "username is empty at line 1 column 20"); // And here we get a nice adjusted value. let u = serde_json::from_str::(r#"{ "username": " john doe " }"#).unwrap(); assert_eq!(u.username.get(), "john doe"); ">
use serde::{Deserialize, Serialize};

prae::define! {
    Username: String
    adjust   |u| *u = u.trim().to_string()
    validate |u| -> Option<&'static str> {
        if u.is_empty() {
            Some("username is empty")
        } else {
            None
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
struct User {
    username: Username,
}

// Serialization works as expected.
let u = User {
    username: Username::new("  john doe  ").unwrap(),
};
let j = serde_json::to_string(&u).unwrap();
assert_eq!(j, r#"{"username":"john doe"}"#)

// Deserialization with invalid data fails.
let e = serde_json::from_str::(r#"{ "username": "  " }"#).unwrap_err();
assert_eq!(e.to_string(), "username is empty at line 1 column 20");

// And here we get a nice adjusted value.
let u = serde_json::from_str::(r#"{ "username": "  john doe  " }"#).unwrap();
assert_eq!(u.username.get(), "john doe");

Drawbacks

Although proc macros are very powerful, they aren't free. In this case, you have to pull up additional dependencies such as syn and quote, and expect a slightly slower compile times.

Credits

This crate was highly inspired by the tightness crate. It's basically just a fork of tightness with a slightly different philosophy. See this issue for details.

Comments
  • Val err details

    Val err details

    I've added more detail to the ValidationError.

    • ValidationError::source_type field stores the name of the source type
    • ValidationError::message stores an error message.
    • ValidationError::new::<T>(message) fills in the source_type name based on T and stores the message.
    • I've removed PartialEq (you should always pair PartialEq and Eq) as it was only being used for equality comparisons in the integration tests. Using assert_matches! allows you to avoid equality tests as the pattern matching system does not use equality tests.
    opened by bheylin 21
  • Allow extending existing `define!`

    Allow extending existing `define!`

    Some draft:

    prae::define! {
        pub Text: String
        adjust |t| *t = t.trim().to_owned()
        ensure |t| !t.is_empty()
    }
    prae::extend! {
        pub CapitalizedText: Text
        ensure |t| t.chars().nth(0).unwrap().is_uppercase()
    }
    
    opened by teenjuna 21
  • Removed unnecessary unsafe fns.

    Removed unnecessary unsafe fns.

    • add warn unsafe_code lint.

    Is there a use-case for having the unsafe functions? The use of unsafe keyword here is stretching it's intended use. Using unsafe is intended to turn off memory safety checks. But prae is using it to mean "turn off data integrity checks"

    opened by bheylin 9
  • Maybe consider the Smart Pointer Pattern?

    Maybe consider the Smart Pointer Pattern?

    Current impl adds get and set for the type. Maybe we can add an impl of Deref and DerefMut to allow for easier use of the type if we expect no error.

    Also, the original set method could be renamed to try_set to reflect the fact that it could fail.

    opened by VoilaNeighbor 4
  • Introduce `ConstructionError<G>` and `MutationError<G>`

    Introduce `ConstructionError` and `MutationError`

    This branch introduces two errors:

    • ConstructionError<G> which will be returned from Guarded::new
    • MutationError<G> which will be returned from Guarded::try_mutate
    opened by teenjuna 4
  • Add ability to document generated types

    Add ability to document generated types

    Possible syntax 1:

    prae::define! {
        /// This is documentation for the `Username`.
        pub Username: String
        ensure |u| !u.is_empty()
    }
    

    Possible syntax 2:

    /// This is documentation for the `Username`.
    prae::define! {
        pub Username: String
        ensure |u| !u.is_empty()
    }
    

    I prefer the second one. The documentation for UsernameGuard must link to the Username.

    opened by teenjuna 2
  • Add `Guard::set`

    Add `Guard::set`

    Instead of doing something like this:

    t.try_mutate(|t| *t = "new text".to_owned());
    

    We should be able to do this:

    t.set("new text");
    
    opened by teenjuna 1
  • Validate fn returns Result instead of Option.

    Validate fn returns Result instead of Option.

    • It is idiomatic Rust to return a Result for a fn that can fail with Error.
    • This means that any fn written to be a validation fn for a prae::Guarded type will play well with other fns in the Rust ecosystem.
    opened by bheylin 1
  • Optimize generated code

    Optimize generated code

    By this I mean the look of generated code. Suppose we have following code:

    prae::define! {
        Username: String
        ensure |u| !u.is_empty()
    }
    

    The validate method of the generated Guard will look like this:

    fn validate(v: &Self::Target) -> Option<&'static str> {
        let f: fn(&Self::Target) -> bool = |u| !u.is_empty();
        if f(v) { None } else { Some("provided value is invalid") }
    }
    

    In this particular case, since the body of the closure is an syn::Expr that compiles (meaning it returns bool), we actually can just use it directly:

    fn validate(v: &Self::Target) -> Option<&'static str> {
        if !v.is_empty() { None } else { Some("provided value is invalid") }
    }
    

    This is tricky, though. In this case we just magically renamed u to v. This is not easy.

    opened by teenjuna 1
  • Improve documentation

    Improve documentation

    The documentation should be improved. Here are some ideas:

    • Use cargo-readme to keep the documentation and README in sync (right now they aren't).
    • Don't populate root doc with a ton of examples. Move most of the into the documentation of define! macro.
    • Add examples to the methods of Guarded.
    opened by teenjuna 1
  • Add optional support for `serde`

    Add optional support for `serde`

    Add optional features serde, which will add implementations for Serialize and Deserialize for wrappers that wrap types that already implement Serialize and Deserialize. The Deserialize must fail for values that don't hold invariants of the given type.

    opened by teenjuna 1
  • Add support for generic wrappers

    Add support for generic wrappers

    Not sure if it's possible. It should look something like this:

    prae::define! {
        pub NonEmptyVec<T>: Vec<T>;
        ensure |v| !v.is_empty();
    }
    
    opened by teenjuna 0
  • Add support for anonymous structs and enums

    Add support for anonymous structs and enums

    Example for structs:

    define! {
        pub User: struct {
            pub name: String,
        }
        ensure |u| !u.name.is_empty()
    }
    

    Example for enums:

    define! {
        pub Error: enum {
            WithMessage(String),
            WithoutMessage,
        }
        ensure |e| {
            if let Error::WithMessage(msg) = e {
                !msg.is_empty()
            } else {
                true
            }
        }
    }
    
    opened by teenjuna 0
Owner
null
📦 Crate Protocol allows anyone to create, manage, and trade a tokenized basket of assets, which we refer to as a Crate.

?? Crate Protocol Crate Protocol allows anyone to create, manage, and trade a tokenized basket of assets, which we refer to as a Crate. A Crate is alw

Crate Protocol 63 Oct 31, 2022
High-level PortMidi bindings and wrappers for Rust

High-level PortMidi bindings and wrappers for Rust

Philippe Delrieu 69 Dec 1, 2022
Runit service management wrappers

void-svtools Basic wrappers for managing services for runit,

Isaac Hung 1 Aug 3, 2022
◉ Arbitrary Protocols on top of Bitcoin (Bitcoin NFTs/Ordinals & Bitcoin Identities/Usernames)

◉ arb arb is a command-line wallet, index, and explorer interface that implements the arb protocol, which enables arbitrary protocols on top of Bitcoi

Ty Vazum 4 May 15, 2023
This crate defines a single macro that is a brainfunct compile-time interpreter.

Compile Protection This crate defines a single macro that is a brainfunct compile-time interpreter. One example is as follows #![recursion_limit = "18

John Marsden 7 Nov 29, 2021
Example of structuring a proc macro crate for testability

testing-proc-macros Example of structuring a proc macro crate for testability. See accompanying blog post for details. License Licensed under either o

Ferrous Systems 12 Dec 11, 2022
A Rust proc-macro crate which derives functions to compile and parse back enums and structs to and from a bytecode representation

Bytecode A simple way to derive bytecode for you Enums and Structs. What is this This is a crate that provides a proc macro which will derive bytecode

null 4 Sep 3, 2022
Tiny crate that allows to wait for a stop signal across multiple threads

Tiny crate that allows to wait for a stop signal across multiple threads. Helpful mostly in server applications that run indefinitely and need a signal for graceful shutdowns.

Dominik Nakamura 5 Dec 16, 2022
Rust crate to generate, manipulate and traverse trees.

SOCAREL Rust crate to generate, manipulate and traverse trees. It provides iterators for eight different traversal algorithms. Add and remove nodes in

Andreu 8 Nov 14, 2021
The nightly_crimes!{} macro commits horrible crimes to allow you to enable nightly features on the stable compiler.

The nightly_crimes!{} macro commits horrible crimes to allow you to enable nightly features on the stable compiler.

Mara Bos 151 Dec 16, 2022
CFD is a tool that allows you to check one or more domains to see if they are protected by CloudFlare or not.

CFD is a tool that allows you to check one or more domains to see if they are protected by CloudFlare or not. The check is carried out based on five criteria: 3 headers in the HTTP response, IP, and SSL certificate issuer. The check result can be displayed on the screen or saved to a file.

Airat Galiullin 13 Apr 7, 2023
A Rust framework for building context-sensitive type conversion.

Xylem is a stateful type conversion framework for Rust.

Jonathan Chan Kwan Yin 4 May 11, 2022
A fusion of OTP lib/dialyzer + lib/compiler for regular Erlang with type inference

Typed ERLC The Problem I have a dream, that one day there will be an Erlang compiler, which will generate high quality type-correct code from deduced

Dmytro Lytovchenko 35 Sep 5, 2022
Use explicit container types with Scrypto! Leverage the Rust compiler's type checking to increase security and productivity when developing Radix blueprints.

Scrypto Static Types Use explicit container types with Scrypto! Leverage the Rust compiler's type checking to increase security and productivity when

null 7 Aug 5, 2022
A bidirectional type checker

A bidirectional type inference system loosely based on Complete and Easy Bidirectional Typechecking for Higher-Rank Polymorphism.

Samuel Ainsworth 35 Sep 22, 2022
The never type (the true one!) in stable Rust.

::never-say-never The ! type. In stable Rust. Since 1.14.0. Better than an enum Never {} definition would be, since an instance of type ! automagicall

Daniel Henry-Mantilla 17 Jan 3, 2023
sblade or switchblade it's a multitool in one capable of doing simple analysis with any type of data, attempting to speed up ethical hacking activities

sblade or switchblade it's a multitool in one capable of doing simple analysis with any type of data, attempting to speed up ethical hacking activities

Gabriel Correia 1 Dec 27, 2022
Simplistic complier~virtual-machine that transforms AST into a Rust function, with basic type checking

rast-jit-vm rast-jit-vm is a simplistic, proof-of-concept~ish compiler / virtual machine that transforms syntax tree into a Rust function, type-checki

Patryk Wychowaniec 4 Oct 23, 2022
A stupid macro that compiles and executes Rust and spits the output directly into your Rust code

inline-rust This is a stupid macro inspired by inline-python that compiles and executes Rust and spits the output directly into your Rust code. There

William 19 Nov 29, 2022