rustrails-record 0.1.2

ORM layer (ActiveRecord equivalent)
Documentation
use std::collections::HashMap;

use rustrails_model::{
    errors::{ErrorType, Errors},
    validations::{LengthValidator, NumericalityValidator, PresenceValidator, ValidationSet},
};
use sea_orm::{ConnectionTrait, Schema};
use serde_json::{Value, json};

use crate::{
    Querying, RecordState,
    base::test_support::{TestUser, seed_users, test_user},
    validations::UniquenessValidator,
};
use rustrails_support::{database, runtime};

macro_rules! ignored_tests {
    ($reason:literal => [$($name:ident),+ $(,)?]) => {
        $(
            #[test]
            #[ignore = $reason]
            fn $name() {}
        )+
    };
}

fn run_sync_validation_test(seed: bool, test: impl FnOnce() + Send + 'static) {
    std::thread::spawn(move || {
        let _rt = runtime::init_runtime();
        database::establish("sqlite::memory:").expect("sqlite in-memory connection should succeed");
        runtime::block_on(async {
            let db = database::db();
            let schema = Schema::new(db.get_database_backend());
            db.execute(&schema.create_table_from_entity(test_user::Entity))
                .await
                .expect("test_users table should be created");
            if seed {
                seed_users(&db).await;
            }
        });
        test();
    })
    .join()
    .unwrap();
}

fn run_seeded_sync_validation_test(test: impl FnOnce() + Send + 'static) {
    run_sync_validation_test(true, test);
}

fn run_validations(
    set: &ValidationSet,
    attrs: impl IntoIterator<Item = (&'static str, Value)>,
) -> Errors {
    let attrs = attrs
        .into_iter()
        .map(|(name, value)| (name.to_owned(), value))
        .collect::<HashMap<_, _>>();
    let mut errors = Errors::new();
    let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
    errors
}

fn run_validations_without_attrs(set: &ValidationSet) -> Errors {
    let mut errors = Errors::new();
    let _ = set.validate(&|_| None, &mut errors);
    errors
}

#[test]
fn test_valid_uses_create_context_when_new() {
    run_seeded_sync_validation_test(|| {
        let candidate = TestUser {
            name: "Alice Clone".to_owned(),
            email: "alice@example.com".to_owned(),
            state: RecordState::New,
            ..Default::default()
        };

        let is_unique = UniquenessValidator::new().validate_unique_sync(
            "email",
            &json!("alice@example.com"),
            &candidate,
        );

        assert!(!is_unique);
    });
}

#[test]
fn test_valid_uses_update_context_when_persisted() {
    run_seeded_sync_validation_test(|| {
        let alice = TestUser::find_sync(1).expect("seeded Alice row should exist");

        let is_unique = UniquenessValidator::new().validate_unique_sync(
            "email",
            &json!("alice@example.com"),
            &alice,
        );

        assert!(is_unique);
    });
}

#[test]
fn test_valid_using_special_context() {
    run_seeded_sync_validation_test(|| {
        let candidate = TestUser {
            name: "Alice Clone".to_owned(),
            email: "ALICE@EXAMPLE.COM".to_owned(),
            state: RecordState::New,
            ..Default::default()
        };

        let is_unique = UniquenessValidator::new()
            .case_insensitive()
            .validate_unique_sync("email", &json!("ALICE@EXAMPLE.COM"), &candidate);

        assert!(!is_unique);
    });
}

#[test]
fn test_invalid_using_multiple_contexts() {
    let mut set = ValidationSet::new();
    set.add("name", PresenceValidator::new());
    set.add("name", LengthValidator::new().minimum(3));

    let errors = run_validations(&set, [("name", json!(""))]);

    assert_eq!(errors.on("name").len(), 2);
    assert_eq!(
        errors.messages_for("name"),
        vec!["can't be blank", "is too short (minimum is 3 characters)",]
    );
}

#[test]
fn test_validate() {
    let mut set = ValidationSet::new();
    set.add("name", PresenceValidator::new());

    let missing = run_validations_without_attrs(&set);
    assert_eq!(missing.on("name")[0].error_type, ErrorType::Blank);

    let present = run_validations(&set, [("name", json!("Alice"))]);
    assert!(present.is_empty());
}

ignored_tests!(
    "Rails-specific: validation contexts, bang validation exceptions, and create! error propagation are not implemented by rustrails_model ValidationSet" => [
        test_invalid_record_exception,
        test_validate_with_bang,
        test_validate_with_bang_and_context,
        test_exception_on_create_bang_many,
        test_exception_on_create_bang_with_block,
        test_exception_on_create_bang_many_with_block,
        test_save_without_validation,
    ]
);

ignored_tests!(
    "Rails-specific: acceptance validators and Active Record attribute-method generation are outside the generic ValidationSet API" => [
        test_validates_acceptance_of_with_non_existent_table,
        test_validates_acceptance_of_with_undefined_attribute_methods,
        test_validates_acceptance_of_as_database_column,
        test_acceptance_validator_doesnt_require_db_connection,
    ]
);

ignored_tests!(
    "Rails-specific: Active Record type-casting and custom getter behavior are not modeled by the generic TestUser validation helpers" => [
        test_throw_away_typing,
        test_numericality_validator_wont_be_affected_by_custom_getter,
    ]
);

#[test]
fn test_validators() {
    let validator = UniquenessValidator::new()
        .scope(vec!["account_id".to_owned()])
        .case_insensitive()
        .message("already used");

    assert_eq!(validator.scope, vec!["account_id"]);
    assert!(!validator.case_sensitive);
    assert_eq!(validator.message.as_deref(), Some("already used"));
}

#[test]
fn test_numericality_validation_with_mutation() {
    let mut set = ValidationSet::new();
    set.add("wibble", NumericalityValidator::new().only_integer());

    let mut wibble = "123-4567".to_owned();
    wibble.retain(|ch| ch != '-');

    let errors = run_validations(&set, [("wibble", json!(wibble))]);

    assert!(errors.is_empty());
}

#[test]
#[ignore = "Rails-specific: raw-value numericality semantics are not exposed by the generic ValidationSet API"]
fn test_numericality_validation_checks_against_raw_value() {}