mod api;
#[cfg(feature = "commands")]
mod commands;
mod dto;
#[cfg(feature = "events")]
mod events;
#[cfg(feature = "hooks")]
mod hooks;
mod insertable;
mod mappers;
#[cfg(feature = "migrations")]
mod migrations;
#[cfg(feature = "aggregate_root")]
pub mod new_entity;
pub mod parse;
mod policy;
#[cfg(feature = "projections")]
mod projection;
mod query;
mod repository;
mod row;
mod sql;
mod streams;
#[cfg(feature = "transactions")]
mod transaction;
use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
use self::parse::EntityDef;
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match EntityDef::from_derive_input(&input) {
Ok(entity) => generate(entity),
Err(err) => err.write_errors().into()
}
}
fn generate(entity: EntityDef) -> TokenStream {
let dto = dto::generate(&entity);
let query_struct = query::generate(&entity);
let policy = policy::generate(&entity);
let streams = streams::generate(&entity);
let api = api::generate(&entity);
let repository = repository::generate(&entity);
let row = row::generate(&entity);
let insertable = insertable::generate(&entity);
let mappers = mappers::generate(&entity);
let sql = sql::generate(&entity);
#[cfg(feature = "events")]
let events = events::generate(&entity);
#[cfg(not(feature = "events"))]
let events = guard_disabled_attribute(&entity, "events", entity.has_events());
#[cfg(feature = "hooks")]
let hooks = hooks::generate(&entity);
#[cfg(not(feature = "hooks"))]
let hooks = guard_disabled_attribute(&entity, "hooks", entity.has_hooks());
#[cfg(feature = "commands")]
let commands = commands::generate(&entity);
#[cfg(not(feature = "commands"))]
let commands = guard_disabled_attribute(&entity, "commands", entity.has_commands());
#[cfg(feature = "transactions")]
let transaction = transaction::generate(&entity);
#[cfg(not(feature = "transactions"))]
let transaction = guard_disabled_attribute(&entity, "transactions", entity.has_transactions());
#[cfg(feature = "aggregate_root")]
let new_entity = new_entity::generate(&entity);
#[cfg(not(feature = "aggregate_root"))]
let new_entity =
guard_disabled_attribute(&entity, "aggregate_root", entity.is_aggregate_root());
#[cfg(feature = "migrations")]
let migrations = migrations::generate(&entity);
#[cfg(not(feature = "migrations"))]
let migrations = guard_disabled_attribute(&entity, "migrations", entity.migrations);
#[cfg(feature = "projections")]
let projections = projection::generate(&entity);
#[cfg(not(feature = "projections"))]
let projections =
guard_disabled_attribute(&entity, "projections", !entity.projections.is_empty());
let expanded = quote! {
#dto
#projections
#query_struct
#events
#hooks
#commands
#policy
#streams
#transaction
#api
#repository
#row
#insertable
#mappers
#new_entity
#sql
#migrations
};
expanded.into()
}
#[allow(dead_code)]
fn guard_disabled_attribute(
entity: &EntityDef,
feature_name: &str,
is_requested: bool
) -> proc_macro2::TokenStream {
if !is_requested {
return proc_macro2::TokenStream::new();
}
let entity_name = entity.name();
let msg = format!(
"entity `{entity_name}` uses an attribute that requires the `{feature_name}` feature of \
`entity-derive`, but it is currently disabled. Enable it by adding \
`features = [\"{feature_name}\"]` to your `entity-derive` dependency, or remove the \
corresponding `#[entity(...)]` / `#[command(...)]` / `#[projection(...)]` attribute."
);
quote! { ::core::compile_error!(#msg); }
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::*;
fn parse_minimal_entity() -> EntityDef {
let input: syn::DeriveInput = parse_quote! {
#[entity(table = "users")]
pub struct User {
#[id]
pub id: ::uuid::Uuid
}
};
EntityDef::from_derive_input(&input).expect("minimal entity must parse")
}
#[test]
fn guard_returns_empty_when_attribute_not_requested() {
let entity = parse_minimal_entity();
let tokens = guard_disabled_attribute(&entity, "commands", false);
assert!(
tokens.is_empty(),
"no compile_error must be emitted when the attribute is absent, got: {tokens}"
);
}
#[test]
fn guard_emits_compile_error_when_attribute_requested_without_feature() {
let entity = parse_minimal_entity();
let tokens = guard_disabled_attribute(&entity, "commands", true).to_string();
assert!(
tokens.contains("compile_error"),
"must emit compile_error! token, got: {tokens}"
);
assert!(
tokens.contains("commands"),
"diagnostic must name the missing feature, got: {tokens}"
);
assert!(
tokens.contains("features = "),
"diagnostic must show the user how to enable, got: {tokens}"
);
}
#[test]
fn guard_includes_entity_name_in_diagnostic() {
let entity = parse_minimal_entity();
let tokens = guard_disabled_attribute(&entity, "hooks", true).to_string();
assert!(
tokens.contains("User"),
"diagnostic must name the offending entity, got: {tokens}"
);
}
#[test]
fn guard_message_references_correct_feature_name() {
let entity = parse_minimal_entity();
for feature in [
"events",
"commands",
"hooks",
"transactions",
"aggregate_root",
"migrations",
"projections"
] {
let tokens = guard_disabled_attribute(&entity, feature, true).to_string();
assert!(
tokens.contains(feature),
"diagnostic for `{feature}` must mention it, got: {tokens}"
);
}
}
}