ivo 0.4.0

The rust implementation of the ivo schema valitator
Documentation
use std::future::ready;

use crate::async_test_matrix;
use ivo::{
    constant_field, dependent_field, lax_field, virtual_field, IvoContext, IvoInputStruct,
    IvoModel, IvoStruct,
};

mod ignore;
mod ignore_update;
mod post_validate;
mod required;

// [x] ignore
// [x] ignore_update
// [x] on_delete
// [x] on_success
// [x] post_validate
// [x] required

// ignore_update
async fn should_respect_option_to_ignore_updates_with_empty_fields_array() {
    #[derive(Debug, Clone, PartialEq, IvoStruct)]
    struct Data {
        lax: String,
    }

    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct DataInput {
        lax: String,
    }

    let default_value = "default_lax_value";
    const IGNORE_VALUE: &str = "ignore_value";

    let model = IvoModel::<DataInput, Data>::new(
        |f| f.field(lax_field("lax").default(default_value.to_string())),
        |o| {
            o.ignore_update([], |input: PartialDataInput, _, _| {
                ready(input.lax.map(|v| v == IGNORE_VALUE).unwrap_or(false))
            })
        },
    );

    let lax = "lax_value".to_string();

    let data = Data { lax };
    let lax_update = IGNORE_VALUE.to_string();

    let r = model
        .update(
            &data,
            &PartialDataInput {
                lax: Some(lax_update),
            },
            None,
        )
        .await;

    match r {
        Err((e, _, _)) => assert!(e.is_none()),
        _ => unreachable!(),
    }

    let lax_update = "should_not_ignore".to_string();

    let (updates, _, _) = model
        .update(
            &data,
            &PartialDataInput {
                lax: Some(lax_update.clone()),
            },
            None,
        )
        .await
        .ok()
        .unwrap();

    assert_eq!(
        updates,
        PartialData {
            lax: Some(lax_update)
        }
    );
}

async_test_matrix!(should_respect_option_to_ignore_updates_with_empty_fields_array);

// on_delete

async fn should_properly_trigger_on_delete_handlers() {
    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct Data {
        lax: i32,
        lax_1: i32,
    }

    let model: IvoModel<Data> = IvoModel::new(
        |f| {
            f.field(lax_field("lax").default(1234))
                .field(lax_field("lax_1").default(5678))
        },
        |o| {
            o.on_delete(|_, _| {
                if true {
                    panic!("[options.on_delete]: handler triggered");
                }

                ready(())
            })
        },
    );

    model.delete(&Data { lax: 2, lax_1: 3 }, None).await
}

async_test_matrix!(
    "[options.on_delete]: handler triggered",
    should_properly_trigger_on_delete_handlers
);

async fn should_properly_trigger_all_on_delete_handlers() {
    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct Data {
        lax: i32,
        lax_1: i32,
    }

    let model: IvoModel<Data> = IvoModel::new(
        |f| {
            f.field(lax_field("lax").default(1234))
                .field(lax_field("lax_1").default(5678))
        },
        |o| {
            o.on_delete(|_, _| ready(())).on_delete(|_, _| {
                if true {
                    panic!("[options.on_delete]: second handler triggered");
                }

                ready(())
            })
        },
    );

    model.delete(&Data { lax: 2, lax_1: 3 }, None).await
}

async_test_matrix!(
    "[options.on_delete]: second handler triggered",
    should_properly_trigger_all_on_delete_handlers
);

// on_success

#[test]
#[should_panic(
    expected = "[options.on_success]: remove duplicates of \"lax\" in grouped on_success config"
)]
fn should_reject_if_the_fields_array_contains_any_duplicates() {
    #[derive(Debug, Clone, PartialEq, IvoStruct)]
    struct Data {
        lax: i32,
        lax_1: i32,
    }

    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct DataInput {
        lax: i32,
        lax_1: i32,
    }

    let _: IvoModel<DataInput, Data, Option<()>, &'static str> = IvoModel::new(
        |f| {
            f.field(lax_field("lax").default(1234))
                .field(lax_field("lax_1").default(5678))
        },
        |o| o.on_success(["lax", "lax"], |s| s.handle(|_, _| ready(()))),
    );
}

#[test]
#[should_panic(expected = "[options.on_success]: \"invalid_field\" does not exist on your schema")]
fn should_reject_if_the_fields_array_contains_any_string_that_is_not_a_field_on_schema() {
    #[derive(Debug, Clone, PartialEq, IvoStruct)]
    struct Data {
        lax: i32,
        lax_1: i32,
    }

    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct DataInput {
        lax: i32,
        lax_1: i32,
    }

    let _: IvoModel<DataInput, Data, Option<()>, &'static str> = IvoModel::new(
        |f| {
            f.field(lax_field("lax").default(1234))
                .field(lax_field("lax_1").default(5678))
        },
        |o| o.on_success(["lax", "invalid_field"], |s| s.handle(|_, _| ready(()))),
    );
}

#[test]
#[should_panic(
    expected = "[options.on_success]: \"alias\" is an alias; use \"virtual_field\" instead"
)]
fn should_reject_if_an_alias_with_foreign_name_is_provided_to_the_fields_array() {
    #[derive(Debug, Clone, PartialEq, IvoStruct)]
    struct Data {
        lax: i32,
        lax_1: i32,
        dependent: i32,
    }

    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct DataInput {
        lax: i32,
        lax_1: i32,
        alias: i32,
    }

    let _: IvoModel<DataInput, Data, Option<()>, &'static str> = IvoModel::new(
        |f| {
            f.field(
                dependent_field("dependent", ["lax", "virtual_field"])
                    .default(1)
                    .resolve(|_, _| ready(2)),
            )
            .field(lax_field("lax").default(1234))
            .field(lax_field("lax_1").default(5678))
            .field(
                virtual_field("virtual_field")
                    .alias("alias")
                    .validate(|_, _, _| ready(Ok(Some(1)))),
            )
        },
        |o| o.on_success(["lax", "lax_1", "alias"], |s| s.handle(|_, _| ready(()))),
    );
}

#[test]
#[should_panic(
    expected = "[options.on_success]: timestamps are not allowed in on_success. remove \"created_at\""
)]
fn should_reject_if_created_at_timestamp_with_default_name_is_provided_to_the_fields_array() {
    #[derive(Debug, Clone, PartialEq, IvoStruct)]
    struct Data {
        created_at: i32,
        lax: i32,
        lax_1: i32,
    }

    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct DataInput {
        lax: i32,
        lax_1: i32,
    }

    let _: IvoModel<DataInput, Data, Option<()>, i32> = IvoModel::new(
        |f| {
            f.field(lax_field("lax").default(1234))
                .field(lax_field("lax_1").default(5678))
                .timestamps(|t| t.resolve(|| 1234).created_at(None))
        },
        |o| {
            o.on_success(["lax", "lax_1", "created_at"], |s| {
                s.handle(|_, _| ready(()))
            })
        },
    );
}

#[test]
#[should_panic(
    expected = "[options.on_success]: timestamps are not allowed in on_success. remove \"custom_created_at\""
)]
fn should_reject_if_created_at_timestamp_with_custom_name_is_provided_to_the_fields_array() {
    #[derive(Debug, Clone, PartialEq, IvoStruct)]
    struct Data {
        custom_created_at: i32,
        lax: i32,
        lax_1: i32,
    }

    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct DataInput {
        lax: i32,
        lax_1: i32,
    }

    let _: IvoModel<DataInput, Data, Option<()>, i32> = IvoModel::new(
        |f| {
            f.field(lax_field("lax").default(1234))
                .field(lax_field("lax_1").default(5678))
                .timestamps(|t| t.resolve(|| 1234).created_at(Some("custom_created_at")))
        },
        |o| {
            o.on_success(["lax", "lax_1", "custom_created_at"], |s| {
                s.handle(|_, _| ready(()))
            })
        },
    );
}

#[test]
#[should_panic(
    expected = "[options.on_success]: timestamps are not allowed in on_success. remove \"updated_at\""
)]
fn should_reject_if_updated_at_timestamp_with_default_name_is_provided_to_the_fields_array() {
    #[derive(Debug, Clone, PartialEq, IvoStruct)]
    struct Data {
        updated_at: i32,
        lax: i32,
        lax_1: i32,
    }

    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct DataInput {
        lax: i32,
        lax_1: i32,
    }

    let _: IvoModel<DataInput, Data, Option<()>, i32> = IvoModel::new(
        |f| {
            f.field(lax_field("lax").default(1234))
                .field(lax_field("lax_1").default(5678))
                .timestamps(|t| t.resolve(|| 1234).updated_at(None))
        },
        |o| {
            o.on_success(["lax", "lax_1", "updated_at"], |s| {
                s.handle(|_, _| ready(()))
            })
        },
    );
}

#[test]
#[should_panic(
    expected = "[options.on_success]: timestamps are not allowed in on_success. remove \"custom_updated_at\""
)]
fn should_reject_if_updated_at_timestamp_with_custom_name_is_provided_to_the_fields_array() {
    #[derive(Debug, Clone, PartialEq, IvoStruct)]
    struct Data {
        custom_updated_at: i32,
        lax: i32,
        lax_1: i32,
    }

    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct DataInput {
        lax: i32,
        lax_1: i32,
    }

    let _: IvoModel<DataInput, Data, Option<()>, i32> = IvoModel::new(
        |f| {
            f.field(lax_field("lax").default(1234))
                .field(lax_field("lax_1").default(5678))
                .timestamps(|t| t.resolve(|| 1234).updated_at(Some("custom_updated_at")))
        },
        |o| {
            o.on_success(["lax", "lax_1", "custom_updated_at"], |s| {
                s.handle(|_, _| ready(()))
            })
        },
    );
}

#[test]
fn should_allow_constant_and_dependents_in_fields_array() {
    #[derive(Debug, Clone, PartialEq, IvoStruct)]
    struct Data {
        id: i32,
        dependent: i32,
        lax: i32,
    }

    #[derive(Debug, Clone, PartialEq, IvoInputStruct)]
    struct DataInput {
        lax: i32,
    }

    let _: IvoModel<DataInput, Data, Option<()>, i32> = IvoModel::new(
        |f| {
            f.field(constant_field("id").value(1234))
                .field(dependent_field("dependent", ["lax"]).default(1).resolve(
                    |ctx: IvoContext<DataInput, Data>, _| {
                        ready(ctx.values().dependent.unwrap() + 1)
                    },
                ))
                .field(lax_field("lax").default(5678))
        },
        |o| o.on_success(["id", "dependent"], |s| s.handle(|_, _| ready(()))),
    );
}