doido-model 0.0.26

SeaORM re-exports plus connection pooling and model testing helpers for Doido apps.
Documentation
use doido_model::entities::{
    dedupe_model_extension_stubs, ensure_active_model_behavior_in_extensions,
    ensure_model_extension_stubs, entity_has_model_extension, entity_modules, extension_stub,
    model_extension_covers_entity, model_modules, reexported_entity_module, register_entity_module,
    register_model_module, rewrite_generated_imports, write_active_model_behavior_module,
};
use std::fs;
use tempfile::tempdir;

#[test]
fn entity_modules_parses_mod_declarations() {
    let content = r#"pub mod prelude;
pub mod posts;
pub mod users;
pub mod sea_orm_active_enums;
"#;
    assert_eq!(
        entity_modules(content),
        vec!["posts".to_string(), "users".to_string()]
    );
}

#[test]
fn model_modules_skips_entities_registry() {
    let content = "pub mod _entities;\npub mod post;\n// @generated-models\n";
    assert_eq!(model_modules(content), vec!["post".to_string()]);
}

#[test]
fn extension_stub_reexports_entity_table_module() {
    let stub = extension_stub("post", "posts");
    assert!(stub.contains("pub use super::_entities::posts::*;"));
    assert!(stub.contains("impl ActiveModelBehavior for ActiveModel {}"));
    assert!(stub.contains("#![allow(dead_code, unused_imports)]"));
}

#[test]
fn register_model_module_is_idempotent() {
    let base = "pub mod _entities;\n\n// @generated-models\n";
    let once = register_model_module(base, "post");
    assert!(once.contains("pub mod post;"));
    let twice = register_model_module(&once, "post");
    assert_eq!(once, twice);
}

#[test]
fn register_entity_module_is_idempotent() {
    let base = "// @generated-entities\n";
    let once = register_entity_module(base, "posts");
    assert!(once.contains("pub mod posts;"));
    let twice = register_entity_module(&once, "posts");
    assert_eq!(once, twice);
}

#[test]
fn rewrite_generated_imports_replaces_sea_orm_paths() {
    let dir = tempdir().unwrap();
    fs::write(
        dir.path().join("posts.rs"),
        "use sea_orm::entity::prelude::*;\nuse sea_orm::Set;\n",
    )
    .unwrap();
    rewrite_generated_imports(dir.path()).unwrap();
    let content = fs::read_to_string(dir.path().join("posts.rs")).unwrap();
    assert!(content.contains("use doido::model::sea_orm as sea_orm;"));
    assert!(content.contains("use doido::model::sea_orm::entity::prelude::*;"));
    assert!(content.contains("use doido::model::sea_orm::Set;"));
}

#[test]
fn rewrite_generated_imports_adds_alias_for_sea_orm_attributes() {
    let dir = tempdir().unwrap();
    fs::write(
        dir.path().join("storage_blobs.rs"),
        "use sea_orm::entity::prelude::*;\n\n#[sea_orm(table_name = \"storage_blobs\")]\n",
    )
    .unwrap();
    rewrite_generated_imports(dir.path()).unwrap();
    let content = fs::read_to_string(dir.path().join("storage_blobs.rs")).unwrap();
    assert!(content.contains("use doido::model::sea_orm as sea_orm;"));
    assert!(content.contains("use doido::model::sea_orm::entity::prelude::*;"));
    assert!(content.contains("#![allow(dead_code, unused_imports)]"));
}

#[test]
fn rewrite_generated_imports_is_idempotent_for_doido_imports() {
    let dir = tempdir().unwrap();
    fs::write(
        dir.path().join("users.rs"),
        "//! Generated user entity\n\
         #![allow(dead_code)]\n\n\
         use doido::model::sea_orm;\n\
         use doido::model::sea_orm::entity::prelude::*;\n",
    )
    .unwrap();
    rewrite_generated_imports(dir.path()).unwrap();
    rewrite_generated_imports(dir.path()).unwrap();
    let content = fs::read_to_string(dir.path().join("users.rs")).unwrap();
    assert!(
        content
            .find("#![allow(dead_code, unused_imports)]")
            .unwrap()
            < content
                .find("use doido::model::sea_orm as sea_orm")
                .unwrap()
    );
    assert_eq!(
        content
            .matches("use doido::model::sea_orm as sea_orm")
            .count(),
        1
    );
}

#[test]
fn rewrite_generated_imports_strips_active_model_behavior_impl() {
    let dir = tempdir().unwrap();
    fs::write(
        dir.path().join("posts.rs"),
        "use sea_orm::entity::prelude::*;\n\n\
         #[derive(DeriveEntityModel)]\n\
         pub struct Model {}\n\n\
         impl ActiveModelBehavior for ActiveModel {}\n",
    )
    .unwrap();
    rewrite_generated_imports(dir.path()).unwrap();
    let content = fs::read_to_string(dir.path().join("posts.rs")).unwrap();
    assert!(!content.contains("impl ActiveModelBehavior for ActiveModel {}"));
}

#[test]
fn ensure_model_extension_stubs_creates_missing_stubs() {
    let dir = tempdir().unwrap();
    let entities = dir.path().join("_entities");
    let models = dir.path().join("models");
    fs::create_dir_all(&entities).unwrap();
    fs::create_dir_all(&models).unwrap();
    fs::write(
        entities.join("mod.rs"),
        "pub mod storage_blobs;\n// @generated-entities\n",
    )
    .unwrap();
    fs::write(
        models.join("mod.rs"),
        "pub mod _entities;\n// @generated-models\n",
    )
    .unwrap();

    ensure_model_extension_stubs(&entities, &models).unwrap();

    let stub = fs::read_to_string(models.join("storage_blob.rs")).unwrap();
    assert!(stub.contains("pub use super::_entities::storage_blobs::*;"));
    assert!(stub.contains("impl ActiveModelBehavior for ActiveModel {}"));
    let models_mod = fs::read_to_string(models.join("mod.rs")).unwrap();
    assert!(models_mod.contains("pub mod storage_blob;"));
}

#[test]
fn ensure_active_model_behavior_in_extensions_patches_reexports() {
    let dir = tempdir().unwrap();
    let models = dir.path().join("models");
    fs::create_dir_all(&models).unwrap();
    fs::write(
        models.join("user.rs"),
        "pub use super::_entities::users::*;\n",
    )
    .unwrap();

    ensure_active_model_behavior_in_extensions(&models).unwrap();

    let content = fs::read_to_string(models.join("user.rs")).unwrap();
    assert!(content.contains("impl ActiveModelBehavior for ActiveModel {}"));
}

#[test]
fn write_active_model_behavior_module_covers_orphan_entities() {
    let dir = tempdir().unwrap();
    let entities = dir.path().join("_entities");
    let models = dir.path().join("models");
    fs::create_dir_all(&entities).unwrap();
    fs::create_dir_all(&models).unwrap();
    fs::write(
        entities.join("mod.rs"),
        "pub mod posts;\n// @generated-entities\n",
    )
    .unwrap();
    fs::write(
        models.join("post.rs"),
        "#![allow(dead_code)]\n// inline tutorial model\n",
    )
    .unwrap();

    write_active_model_behavior_module(&entities, &models).unwrap();

    let behavior = fs::read_to_string(entities.join("active_model_behavior.rs")).unwrap();
    assert!(behavior.contains("impl ActiveModelBehavior for super::posts::ActiveModel {}"));
    let entities_mod = fs::read_to_string(entities.join("mod.rs")).unwrap();
    assert!(entities_mod.contains("pub mod active_model_behavior;"));
}

#[test]
fn model_extension_covers_entity_when_reexport_and_impl_present() {
    let content =
        "pub use super::_entities::users::*;\nimpl ActiveModelBehavior for ActiveModel {}\n";
    assert!(model_extension_covers_entity(content, "users"));
    assert!(!model_extension_covers_entity(
        "pub use super::_entities::users::*;\n",
        "users"
    ));
}

#[test]
fn ensure_model_extension_stubs_skips_when_entity_already_reexported() {
    let dir = tempdir().unwrap();
    let entities = dir.path().join("_entities");
    let models = dir.path().join("models");
    fs::create_dir_all(&entities).unwrap();
    fs::create_dir_all(&models).unwrap();
    fs::write(entities.join("mod.rs"), "pub mod skus;\n").unwrap();
    fs::write(
        models.join("mod.rs"),
        "pub mod _entities;\npub mod sku;\n// @generated-models\n",
    )
    .unwrap();
    fs::write(
        models.join("sku.rs"),
        "pub use super::_entities::skus::*;\nimpl ActiveModelBehavior for ActiveModel {}\n",
    )
    .unwrap();

    ensure_model_extension_stubs(&entities, &models).unwrap();

    assert!(!models.join("skus.rs").exists());
    assert!(entity_has_model_extension(&models, "skus"));
}

#[test]
fn dedupe_model_extension_stubs_removes_plural_duplicate() {
    let dir = tempdir().unwrap();
    let models = dir.path().join("models");
    fs::create_dir_all(&models).unwrap();
    fs::write(
        models.join("mod.rs"),
        "pub mod _entities;\npub mod sku;\npub mod skus;\n// @generated-models\n",
    )
    .unwrap();
    fs::write(
        models.join("sku.rs"),
        "pub use super::_entities::skus::*;\n",
    )
    .unwrap();
    fs::write(
        models.join("skus.rs"),
        "pub use super::_entities::skus::*;\n",
    )
    .unwrap();

    dedupe_model_extension_stubs(&models).unwrap();

    assert!(models.join("sku.rs").exists());
    assert!(!models.join("skus.rs").exists());
    let models_mod = fs::read_to_string(models.join("mod.rs")).unwrap();
    assert!(models_mod.contains("pub mod sku;"));
    assert!(!models_mod.contains("pub mod skus;"));
}

#[test]
fn reexported_entity_module_parses_pub_use_line() {
    let content = "pub use super::_entities::skus::*;\n";
    assert_eq!(reexported_entity_module(content), Some("skus".to_string()));
}

#[test]
fn ensure_model_extension_stubs_skips_existing_model_modules() {
    let dir = tempdir().unwrap();
    let entities = dir.path().join("_entities");
    let models = dir.path().join("models");
    fs::create_dir_all(&entities).unwrap();
    fs::create_dir_all(&models).unwrap();
    fs::write(entities.join("mod.rs"), "pub mod users;\n").unwrap();
    fs::write(
        models.join("mod.rs"),
        "pub mod _entities;\npub mod user;\n// @generated-models\n",
    )
    .unwrap();
    fs::write(models.join("user.rs"), "// custom user model\n").unwrap();

    ensure_model_extension_stubs(&entities, &models).unwrap();

    assert_eq!(
        fs::read_to_string(models.join("user.rs")).unwrap(),
        "// custom user model\n"
    );
    assert!(!models.join("users.rs").exists());
}

#[test]
fn rewrite_generated_imports_adds_lint_allows_to_prelude() {
    let dir = tempdir().unwrap();
    fs::write(
        dir.path().join("prelude.rs"),
        "pub use super::posts::Entity as Posts;\n",
    )
    .unwrap();
    rewrite_generated_imports(dir.path()).unwrap();
    let content = fs::read_to_string(dir.path().join("prelude.rs")).unwrap();
    assert!(content.contains("#![allow(unused_imports)]"));
}

#[test]
fn reexported_entity_module_skips_doc_comments_before_pub_use() {
    let content = "//! Model extensions\n\
                   #![allow(dead_code)]\n\n\
                   pub use super::_entities::skus::*;\n";
    assert_eq!(reexported_entity_module(content), Some("skus".to_string()));
}