Expand description
§State Validation
state-validation lets you validate an input for a given state. Then, run an action using the validated output.
Ex. You want to remove an admin from UserStorage, given a UserID, you want to retrieve the User who maps onto the UserID and validate they are an existing user whose privilege level is admin.
The state is UserStorage, the input is a UserID, and the valid output is an AdminUser.
Here is our input UserID:
#[derive(Hash, PartialEq, Eq, Clone, Copy)]
struct UserID(usize);Our User which holds its UserID and username:
#[derive(Clone)]
struct User {
id: UserID,
username: String,
}Our state will be UserStorage:
#[derive(Default)]
struct UserStorage {
maps: HashMap<UserID, User>,
}We will create a newtype AdminUser, which only users with admin privilege will be wrapped in.
This will also be the output of our filter which checks if a user is admin:
struct AdminUser(User);Our first filter will check if a User exists given a UserID:
struct UserExists;
impl StateFilter<UserStorage, UserID> for UserExists {
type ValidOutput = User;
type Error = UserDoesNotExistError;
fn filter(state: &UserStorage, user_id: UserID) -> Result<Self::ValidOutput, Self::Error> {
if let Some(user) = state.maps.get(&user_id) {
Ok(user.clone())
} else {
Err(UserDoesNotExistError)
}
}
}Our second filter will check if a User is admin:
struct UserIsAdmin;
impl<State> StateFilter<State, User> for UserIsAdmin {
type ValidOutput = AdminUser;
type Error = UserIsNotAdminError;
fn filter(state: &State, user: User) -> Result<Self::ValidOutput, Self::Error> {
if user.username == "ADMIN" {
Ok(AdminUser(user))
} else {
Err(UserIsNotAdminError)
}
}
}Note: in the above code, we don’t care about the state so it is a generic.
Now, we can finally implement an action that removes the admin from user storage:
struct RemoveAdmin;
impl ValidAction<UserStorage, UserID> for RemoveAdmin {
// To chain filters, use `Condition`.
type Filter = (
// <Input, Filter>
Condition<UserID, UserExists>,
// Previous filter outputs a `User`,
// so next filter can take `User` as input.
Condition<User, UserIsAdmin>,
);
type Output = UserStorage;
fn with_valid_input(
self,
mut state: UserStorage,
// The final output from the filters is an `AdminUser`.
admin_user: <Self::Filter as StateFilter<UserStorage, UserID>>::ValidOutput,
) -> Self::Output {
let _ = state.maps.remove(&admin_user.0.id).unwrap();
state
}
}Now, let’s put it all together. We create the state UserStorage,
and then use Validator::try_new to run our filters. An error is returned if any of the filters fail,
otherwise we get a validator that we can run an action on:
// State setup
let mut user_storage = UserStorage::default();
user_storage.maps.insert(UserID(0), User {
id: UserID(0),
username: "ADMIN".to_string(),
});
// Create validator which will validate the input.
// No error is returned if validation succeeds.
let validator = Validator::try_new(user_storage, UserID(0)).expect("admin user did not exist");
// Execute an action which requires the state and input above.
let user_storage = validator.execute(RemoveAdmin);
assert!(user_storage.maps.is_empty());Another example using UserExists filter, and UserIsAdmin filter in the body of the ValidAction:
enum Privilege {
None,
Admin,
}
struct UpdateUserPrivilege(Privilege);
impl ValidAction<UserStorage, UserID> for UpdateUserPrivilege {
type Filter = UserExists;
type Output = UserStorage;
fn with_valid_input(
self,
mut state: UserStorage,
mut user: <Self::Filter as StateFilter<UserStorage, UserID>>::ValidOutput,
) -> Self::Output {
match self.0 {
Privilege::None => {
if let Ok(AdminUser(mut user)) = UserIsAdmin::filter(&state, user) {
user.username = "NOT_ADMIN".to_string();
let _ = state.maps.insert(user.id, user);
}
}
Privilege::Admin => {
user.username = "ADMIN".to_string();
let _ = state.maps.insert(user.id, user);
}
}
state
}
}
// This `Validator` has its generic `Filter` parameter implicitly changed
// based on what action we call. On a type level, this is a different type
// than the validator in the previous example even though we write the same code
// due to its `Filter` generic parameter being different.
let validator = Validator::try_new(user_storage, UserID(0)).expect("user did not exist");
let user_storage = validator.execute(UpdateUserPrivilege(Privilege::None));
let user = user_storage.maps.get(&UserID(0));
assert_eq!(user.unwrap().username, "NOT_ADMIN");§StateFilterInputConversion & StateFilterInputCombination
Automatic implementations of StateFilterInputConversion and StateFilterInputCombination
are generated with the StateFilterConversion derive macro.
Example:
#[derive(StateFilterConversion)]
struct UserWithUserName {
#[conversion(User)]
user_id: UserID,
username: String,
}Now, UserWithUsername can be broken down into User, UserID, and String.
Take advantage of the newtype pattern to breakdown the input further.
For example, instead of having username as a String, use:
struct Username(String);This way, the compiler can differentiate between a String and a Username.
The StateFilterInputConversion and StateFilterInputCombination traits work together
to allow splitting the input down into its parts and then back together.
The usefulness of this is, if a StateFilter only requires a part of the input,
StateFilterInputConversion can split it down to just that part, and leave the rest in StateFilterInputConversion::Remainder
which will be combined with the output of the filter. And, each consecutive filter can split
whatever input they desire and combine their output with the remainder they did not touch.
Here is an example with manual implementations: Assume we wanted to change the username of a user,
if its UserID and current username matched that of the input.
First, let’s create its input:
struct UserWithUsername {
user_id: UserID,
username: String,
}We want to check if a user with UserID exists, and their current username is username, and then update their username.
We already have a filter that will check if a user exists. It is called: UserExists.
But, UserExists does not take UserWithUsername as an input. It only takes UserID as input.
UserWithUsername does contain a UserID. So, we should be able to retrieve the UserID,
pass it into UserExists, then reconstruct the output of UserExists with the leftover username.
The first part of solving this issue is input conversion into UserID, so we implement StateFilterInputConversion:
struct UsernameForUserID(String);
impl StateFilterInputConversion<UserID> for UserWithUsername {
type Remainder = UsernameForUserID;
fn split_take(self) -> (UserID, Self::Remainder) {
(self.user_id, UsernameForUserID(self.username))
}
}Notice in the above code, within split_take, the first element of the tuple is the input our UserExists filter cares about.
However, for the Remainder, we have a newtype which stores the leftover to combine later with the output of UserExists.
struct UsernameForUser {
user: User,
username: String,
}
impl StateFilterInputCombination<User> for UsernameForUserID {
type Combined = UsernameForUser;
fn combine(self, user: User) -> Self::Combined {
UsernameForUser {
user,
username: self.0,
}
}
}Now, the combination of UserExists’s output (which is a User) and the leftover username,
results in a new struct called UsernameForUser.
Now, we need a new filter that checks if the username of the User is equal to username.
struct UsernameEquals;
impl<State> StateFilter<State, UsernameForUser> for UsernameEquals {
type ValidOutput = User;
type Error = UsernameIsNotEqual;
fn filter(state: &State, value: UsernameForUser) -> Result<Self::ValidOutput, Self::Error> {
if value.user.username == value.username {
Ok(value.user)
} else {
Err(UsernameIsNotEqual)
}
}
}Finally, we can run our action to update the user’s username:
struct UpdateUsername {
new_username: String,
}
impl ValidAction<UserStorage, UserWithUsername> for UpdateUsername {
type Filter = (
Condition<UserID, UserExists>,
Condition<UsernameForUser, UsernameEquals>,
);
type Output = UserStorage;
fn with_valid_input(
self,
mut state: UserStorage,
mut user: <Self::Filter as StateFilter<UserStorage, UserWithUsername>>::ValidOutput,
) -> Self::Output {
user.username = self.new_username;
let _ = state.maps.insert(user.id, user);
state
}
}§Soundness Rules
Validator::try_new takes ownership of the state to disallow consecutive
Validator::execute calls because an action is assumed to mutate the state.
Since an action is assumed to mutate the state, any validators using the same state
cannot be created.
It is up to you to make sure the filters properly validate what they promise.
§Limitations
Currently, the amount of filters that can be chained is eight. The reason for this is because of variadics not being supported as of Rust 2024. Having no more than eight implementations is arbitrary because having more than eight filters is unlikely. There is no reason not to implement more in the future, if more than eight filters are required.
Structs§
Enums§
- State
Filter Eight Chain Error - State
Filter Five Chain Error - State
Filter Four Chain Error - State
Filter Seven Chain Error - State
Filter SixChain Error - State
Filter Three Chain Error - State
Filter TwoChain Error