entity-derive-impl 0.6.5

Internal proc-macro implementation for entity-derive. Use entity-derive instead.
Documentation
// SPDX-FileCopyrightText: 2025-2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

//! Lifecycle hooks trait generation.
//!
//! Generates a hooks trait for entities with `#[entity(hooks)]`.
//! Hooks provide before/after callbacks for CRUD operations.
//!
//! # ⚠️ Status: trait emitted, invocation is manual (as of 0.8.2)
//!
//! This module emits the `{Entity}Hooks` trait for the user to
//! implement. **The generated repository methods do not call it
//! automatically yet.** The generated `impl {Entity}Repository for
//! sqlx::PgPool` lives in a different crate from any user-provided
//! `impl …Hooks for PgPool`, and Rust's orphan rule prevents the user
//! from wiring the two together after the fact.
//!
//! Until full auto-invocation lands, the supported pattern is:
//!
//! 1. Implement `{Entity}Hooks` on a type you own (typically a wrapper around
//!    `PgPool` or a service struct in your application).
//! 2. Call the hook methods explicitly at your handler / service layer around
//!    the calls into the generated CRUD methods. See
//!    `examples/hooks/src/main.rs` for the wiring pattern.
//!
//! Tracking auto-invocation: [issue #127](https://github.com/RAprogramm/entity-derive/issues/127).
//!
//! # Generated Code
//!
//! For an entity `User`, generates:
//!
//! ```rust,ignore
//! #[async_trait]
//! pub trait UserHooks: Send + Sync {
//!     type Error: std::error::Error + Send + Sync;
//!
//!     async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Self::Error> { Ok(()) }
//!     async fn after_create(&self, entity: &User) -> Result<(), Self::Error> { Ok(()) }
//!     async fn before_update(&self, id: &Uuid, dto: &mut UpdateUserRequest) -> Result<(), Self::Error> { Ok(()) }
//!     async fn after_update(&self, entity: &User) -> Result<(), Self::Error> { Ok(()) }
//!     async fn before_delete(&self, id: &Uuid) -> Result<(), Self::Error> { Ok(()) }
//!     async fn after_delete(&self, id: &Uuid) -> Result<(), Self::Error> { Ok(()) }
//! }
//! ```
//!
//! # Usage (manual wiring)
//!
//! ```rust,ignore
//! struct AppService {
//!     pool: PgPool,
//! }
//!
//! impl UserHooks for AppService {
//!     type Error = AppError;
//!
//!     async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Self::Error> {
//!         dto.email = dto.email.to_lowercase();
//!         Ok(())
//!     }
//!
//!     async fn after_create(&self, user: &User) -> Result<(), Self::Error> {
//!         send_welcome_email(&user.email).await?;
//!         Ok(())
//!     }
//! }
//!
//! // At your handler layer:
//! async fn create_user(svc: &AppService, mut req: CreateUserRequest) -> Result<User, AppError> {
//!     svc.before_create(&mut req).await?;
//!     let user = <PgPool as UserRepository>::create(&svc.pool, req).await?;
//!     svc.after_create(&user).await?;
//!     Ok(user)
//! }
//! ```

use proc_macro2::TokenStream;
use quote::{format_ident, quote};

use super::parse::EntityDef;
use crate::utils::marker;

/// Generates the lifecycle hooks trait for an entity.
///
/// Returns empty `TokenStream` if `hooks` is not enabled.
pub fn generate(entity: &EntityDef) -> TokenStream {
    if !entity.has_hooks() {
        return TokenStream::new();
    }

    let vis = &entity.vis;
    let entity_name = entity.name();
    let hooks_trait = format_ident!("{}Hooks", entity_name);

    let id_type = entity.id_field().ty();

    let create_hooks = generate_create_hooks(entity);
    let update_hooks = generate_update_hooks(entity, id_type);
    let delete_hooks = generate_delete_hooks(id_type, entity.is_soft_delete());
    let command_hooks = generate_command_hooks(entity);

    let marker = marker::generated();

    quote! {
        #marker
        /// Lifecycle hooks for [`#entity_name`].
        ///
        /// Implement this trait to add custom logic before/after CRUD operations.
        /// All methods have default no-op implementations.
        ///
        /// # ⚠️ Invocation is manual
        ///
        /// The generated `Repository` impl on `sqlx::PgPool` does **not**
        /// call these hooks automatically. Implement the trait on a type
        /// you own (e.g. a service struct that wraps the pool) and call
        /// the hook methods explicitly around your repository calls.
        /// See `examples/hooks` and the module docs for the wiring pattern.
        ///
        /// Tracking auto-invocation: <https://github.com/RAprogramm/entity-derive/issues/127>.
        ///
        /// # Error Handling
        ///
        /// If a `before_*` hook returns an error, the caller should abort
        /// the operation. If an `after_*` hook returns an error, the
        /// operation has already completed but the error is propagated.
        #[async_trait::async_trait]
        #vis trait #hooks_trait: Send + Sync {
            /// Error type for hook operations.
            type Error: std::error::Error + Send + Sync;

            #create_hooks
            #update_hooks
            #delete_hooks
            #command_hooks
        }
    }
}

/// Generate before/after hooks for create operation.
fn generate_create_hooks(entity: &EntityDef) -> TokenStream {
    if entity.create_fields().is_empty() {
        return TokenStream::new();
    }

    let entity_name = entity.name();
    let create_dto = entity.ident_with("Create", "Request");

    quote! {
        /// Called before entity creation.
        ///
        /// Use for validation, normalization, or rejecting invalid data.
        /// Modify `dto` to transform input before persistence.
        async fn before_create(&self, dto: &mut #create_dto) -> Result<(), Self::Error> {
            let _ = dto;
            Ok(())
        }

        /// Called after entity creation.
        ///
        /// Use for sending notifications, updating caches, or audit logging.
        async fn after_create(&self, entity: &#entity_name) -> Result<(), Self::Error> {
            let _ = entity;
            Ok(())
        }
    }
}

/// Generate before/after hooks for update operation.
fn generate_update_hooks(entity: &EntityDef, id_type: &syn::Type) -> TokenStream {
    if entity.update_fields().is_empty() {
        return TokenStream::new();
    }

    let entity_name = entity.name();
    let update_dto = entity.ident_with("Update", "Request");

    quote! {
        /// Called before entity update.
        ///
        /// Use for validation or rejecting invalid updates.
        /// Modify `dto` to transform input before persistence.
        async fn before_update(
            &self,
            id: &#id_type,
            dto: &mut #update_dto
        ) -> Result<(), Self::Error> {
            let _ = (id, dto);
            Ok(())
        }

        /// Called after entity update.
        ///
        /// Use for cache invalidation, notifications, or audit logging.
        async fn after_update(&self, entity: &#entity_name) -> Result<(), Self::Error> {
            let _ = entity;
            Ok(())
        }
    }
}

/// Generate before/after hooks for delete operations.
fn generate_delete_hooks(id_type: &syn::Type, soft_delete: bool) -> TokenStream {
    let soft_delete_hooks = if soft_delete {
        quote! {
            /// Called before hard delete (permanent removal).
            ///
            /// Use to check if hard delete is allowed.
            async fn before_hard_delete(&self, id: &#id_type) -> Result<(), Self::Error> {
                let _ = id;
                Ok(())
            }

            /// Called after hard delete (permanent removal).
            async fn after_hard_delete(&self, id: &#id_type) -> Result<(), Self::Error> {
                let _ = id;
                Ok(())
            }

            /// Called before restore from soft-delete.
            async fn before_restore(&self, id: &#id_type) -> Result<(), Self::Error> {
                let _ = id;
                Ok(())
            }

            /// Called after restore from soft-delete.
            async fn after_restore(&self, id: &#id_type) -> Result<(), Self::Error> {
                let _ = id;
                Ok(())
            }
        }
    } else {
        TokenStream::new()
    };

    quote! {
        /// Called before entity deletion.
        ///
        /// Use to check if deletion is allowed or perform cleanup.
        async fn before_delete(&self, id: &#id_type) -> Result<(), Self::Error> {
            let _ = id;
            Ok(())
        }

        /// Called after entity deletion.
        ///
        /// Use for cascade cleanup, notifications, or audit logging.
        async fn after_delete(&self, id: &#id_type) -> Result<(), Self::Error> {
            let _ = id;
            Ok(())
        }

        #soft_delete_hooks
    }
}

/// Generate before/after hooks for command execution.
fn generate_command_hooks(entity: &EntityDef) -> TokenStream {
    if !entity.has_commands() || entity.command_defs().is_empty() {
        return TokenStream::new();
    }

    let entity_name = entity.name();
    let command_enum = format_ident!("{}Command", entity_name);
    let result_enum = format_ident!("{}CommandResult", entity_name);

    quote! {
        /// Called before any command execution.
        ///
        /// Use for authorization, validation, or audit logging.
        /// Returning an error aborts the command.
        async fn before_command(&self, cmd: &#command_enum) -> Result<(), Self::Error> {
            let _ = cmd;
            Ok(())
        }

        /// Called after successful command execution.
        ///
        /// Use for notifications, cache updates, or audit logging.
        async fn after_command(
            &self,
            cmd: &#command_enum,
            result: &#result_enum
        ) -> Result<(), Self::Error> {
            let _ = (cmd, result);
            Ok(())
        }
    }
}