ivo 0.1.0

The rust implementation of the ivo schema valitator
Documentation

Rust Implementation

This is the documentation of the Rust implementation of ivo.

Installation

$ cargo add ivo

How to use

ivo expects you to define your data model with structs that implement IvoInputStruct (required for input structs) and IvoStruct and this can be done via their respective derive macros as shown below.

use chrono::{DateTime, Utc};
use ivo::{IvoInputStruct, IvoStruct};

#[derive(Clone, PartailEq, IvoInputStruct)]
struct UserInput {
    email: Option<String>,
    phone_number: Option<String>,
    username: String,
}

type Timestamp = DateTime<Utc>;

#[derive(Clone, PartailEq, IvoStruct)]
struct User {
    id: String,
    created_at: Timestamp,
    email: Option<String>,
    phone_number: Option<String>,
    updated_at: Option<Timestamp>,
    username: String,
    username_last_updated_at: Option<Timestamp>,
}

IvoStruct

Deriving IvoStruct on User generates a struct called PartialUser together some helper methods for User and PartialUser.

  • User gets three helper methods with the following signatures:

    impl IvoStruct for User {
        fn append_updates(&mut self, updates: &Self::Partial);
    
        // and
        fn clone_with_updates(&self, updates: &Self::Partial) -> Self;
    }
    
    impl From<User> for PartialUser {
        fn from(value: User) -> PartialUser;
    }
    
  • PartialUser has the signature:

    struct PartialUser {
      id: Option<String>,
      created_at: Option<Timestamp>,
      email: Option<String>,
      phone_number: Option<Option<String>>,
      updated_at: Option<Option<Timestamp>>,
      username: Option<String>,
      username_last_updated_at: Option<Option<Timestamp>>,
    }
    
    impl PartialUser {
      // the constructor
      fn new() -> Self;
    
      // you also get two types of builder methods for each field
      fn set_id(&mut self, value: String) -> &mut Self;
      fn with_id(mut self, value: String) -> Self;
    
      // ... more builder methods for the other fields
    
      fn set_username_last_updated_at(&mut self, value: Option<Timestamp>) -> &mut Self;
      fn with_username_last_updated_at(mut self, value: Option<Timestamp>) -> Self;
    
      // you also get a method to unset (or set value to None) for each field
      fn unset_id(&mut self) -> &mut Self;
    
      // converts PartialUser to Some(Self) if at least one field is_some, otherwise none
      fn into_option(self) -> Option<Self>;
    
      // returns true if every field in PartialUser is_none, otherwise false
      fn is_empty(&self) -> bool;
    }
    
  • The #[ivo(...)] attribute can be used to customize PartialStructs and their fields.

    #[derive(Clone, PartailEq, IvoInputStruct)]
    #[ivo(derive(Serialize, Deserialize))]
    struct UserInput {
        email: Option<String>,
        #[ivo(serde(skip_serializing_if = "Option::is_none"))]
        phone_number: Option<String>,
        username: String,
    }
    
    #[derive(Serialize, Deserialize)] // 👈 because it was provided above
    struct PartialUserInput {
        email: Option<Option<String>>,
        #[serde(skip_serializing_if = "Option::is_none")] // 👈 because it was provided above
        phone_number: Option<Option<String>>,
        username: Option<String>,
    }
    

IvoInputStruct

Deriving IvoInputStruct on UserInput automatically implements IvoStruct and generates two structs: PartialUserInput and UserInputErrors.

  • UserInputErrors is used to return errors from post-validators and grouped required resolvers and has the signature:

    struct UserInputErrors {
      email: Option<Option<String>>,
      phone_number: Option<Option<String>>,
      username: Option<String>,
    }
    
    impl UserInputErrors {
      // the constructor
      fn new() -> Self;
    
      // you also get two types of builder methods for each field
      fn set_email(&mut self, reason: &str, metadata: Option<IvoErrorSanitizer::Metadata>) -> &mut Self;
      fn with_email(mut self, reason: &str, metadata: Option<IvoErrorSanitizer::Metadata>) -> Self;
      // ... more builder methods for the other fields
    
      // you also get a method to unset (or set value to None) for each field
      fn unset_email(&mut self) -> &mut Self;
    
      // converts UserInputErrors to Some(Self) if at least one field is_some, otherwise none
      fn into_option(self) -> Option<Self>;
    
      // returns true if every field in UserInputErrors is_none, otherwise false
      fn is_empty(&self) -> bool;
    }
    

Fields

Below are links to examples on how to properly configure schema fields.

Constant Fields

Dependent Fields

Lax Fields

Required Fields

Virtual Fields

Timestamps

Schema options

Ignore (Grouped)

  • [With lax fields]: pay attention to the should_properly_handle_grouped_ignore_rule & should_properly_handle_grouped_ignore_update_rule test funtions here
  • [With virtual fields]: pay attention to the should_properly_handle_grouped_ignore_rule, should_properly_handle_grouped_ignore_rule_with_alias & should_properly_handle_grouped_ignore_rule_with_alias_same_as_dependent test funtions here

Ignore update (Grouped)

  • [For the entire domain entity]: pay attention to the should_respect_option_to_ignore_updates_with_empty_fields_array test funtion here
  • [With lax fields]: pay attention to the should_properly_handle_grouped_ignore_update_rule test funtion here
  • [With required fields]: pay attention to the should_properly_handle_grouped_ignore_update_rule test funtion here

Required (Grouped)

  • [With lax fields]: pay attention to the should_properly_handle_grouped_required_errors test funtion here
  • [With virtual fields]: pay attention to the should_properly_handle_grouped_required_errors, should_properly_handle_grouped_required_errors_with_alias & should_properly_handle_grouped_required_errors_with_alias_same_as_dependent test funtions here

On Success (Grouped)

On Delete

Pay attention to the should_properly_trigger_on_delete_handlers & should_properly_trigger_all_on_delete_handlers test funtions here

Post-validate

  • [With lax fields]: pay attention to the should_respect_post_validation_config & should_respect_updated_values_returned_from_pre_validator_in_post_validation_config test funtions here
  • [With required fields]: pay attention to the should_respect_post_validation_config & should_respect_updated_values_returned_from_pre_validator_in_post_validation_config test funtions here
  • [With virtual fields]: pay attention to the should_respect_post_validation_config, should_respect_post_validation_config_with_alias, should_respect_post_validation_config_with_alias_same_as_dependent, should_respect_updated_values_returned_from_pre_validator_in_post_validation_config, should_respect_updated_values_returned_from_pre_validator_in_post_validation_config_with_alias & should_respect_updated_values_returned_from_pre_validator_in_post_validation_config_with_alias_same_as_dependent test funtions here

Custom Context Options

Custom ErrorSanitizer

The default payload returned for unsuccessful operations has the following signature:

In Rust:

type DefaultFieldErrorMetadata = ();

struct FieldError<Metadata: Clone = DefaultFieldErrorMetadata> {
  pub reason: String,
  pub metadata: Option<Metadata>,
}

type IvoErrorPayload<Metadata: Clone> = HashMap<String, FieldError<Metadata>>;

In order to customize this payload, you just need to provide an implementation of the IvoErrorSanitizer trait that suits. Here is an example of how it can be done.