use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use crate::utils::*;
pub fn derive_service(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let struct_name_str = struct_name.to_string();
let service_name = get_attribute_value(&input.attrs, "service", "name")
.unwrap_or_else(|| struct_name_str.to_lowercase());
let features = get_service_features(&input.attrs);
let service_trait = generate_service_trait(struct_name, &features);
let service_impl = generate_service_implementation(struct_name, &features);
let graphql_resolvers = if features.contains(&"graphql".to_string()) {
generate_graphql_resolvers(struct_name)
} else {
quote! {}
};
let error_types = generate_service_error_types(struct_name);
let config_types = generate_service_config_types(struct_name, &service_name);
let expanded = quote! {
#error_types
#config_types
#service_trait
#service_impl
#graphql_resolvers
};
TokenStream::from(expanded)
}
fn get_service_features(attrs: &[syn::Attribute]) -> Vec<String> {
let mut features = Vec::new();
for attr in attrs {
if attr.path().is_ident("service") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("features") {
if let Ok(lit_str) = meta.value()?.parse::<syn::LitStr>() {
let feature_string = lit_str.value();
let feature_list: Vec<&str> = feature_string.split(',').collect();
for feature in feature_list {
features.push(feature.trim().to_string());
}
}
}
Ok(())
});
}
}
features
}
fn generate_service_trait(struct_name: &syn::Ident, features: &[String]) -> TokenStream2 {
let trait_name = syn::Ident::new(&format!("{}ServiceTrait", struct_name), proc_macro2::Span::call_site());
let result_type = syn::Ident::new(&format!("{}Result", struct_name), proc_macro2::Span::call_site());
let crud_methods = generate_crud_methods(struct_name, &result_type);
let feature_methods = generate_feature_methods(features, &result_type);
quote! {
#[async_trait::async_trait]
pub trait #trait_name: Send + Sync {
#crud_methods
#feature_methods
}
}
}
fn generate_crud_methods(struct_name: &syn::Ident, result_type: &syn::Ident) -> TokenStream2 {
let entity_name = struct_name.to_string().replace("Service", "");
let entity_ident = syn::Ident::new(&entity_name, proc_macro2::Span::call_site());
let create_input = syn::Ident::new(&format!("Create{}Input", entity_name), proc_macro2::Span::call_site());
let update_input = syn::Ident::new(&format!("Update{}Input", entity_name), proc_macro2::Span::call_site());
quote! {
async fn create(&self, product: &str, input: #create_input) -> #result_type<#entity_ident>;
async fn get_by_id(&self, id: uuid::Uuid, product: &str) -> #result_type<Option<#entity_ident>>;
async fn update(&self, id: uuid::Uuid, product: &str, input: #update_input) -> #result_type<#entity_ident>;
async fn delete(&self, id: uuid::Uuid, product: &str) -> #result_type<bool>;
async fn list(&self, product: &str, limit: i64, offset: i64) -> #result_type<Vec<#entity_ident>>;
async fn count(&self, product: &str) -> #result_type<i64>;
async fn exists(&self, id: uuid::Uuid, product: &str) -> #result_type<bool>;
}
}
fn generate_feature_methods(features: &[String], result_type: &syn::Ident) -> TokenStream2 {
let mut methods = Vec::new();
if features.contains(&"brazilian".to_string()) {
methods.push(quote! {
async fn validate_cpf(&self, cpf: &str) -> #result_type<bool>;
async fn validate_cep(&self, cep: &str) -> #result_type<bool>;
});
}
if features.contains(&"payments".to_string()) {
methods.push(quote! {
async fn process_payment(&self, entity_id: uuid::Uuid, payment_method: PaymentMethod) -> #result_type<PaymentResult>;
async fn generate_pix_payment(&self, entity_id: uuid::Uuid) -> #result_type<PixPaymentData>;
async fn generate_boleto_payment(&self, entity_id: uuid::Uuid) -> #result_type<BoletoPaymentData>;
});
}
if features.contains(&"notifications".to_string()) {
methods.push(quote! {
async fn send_notification(&self, entity_id: uuid::Uuid, notification_type: NotificationType) -> #result_type<()>;
});
}
quote! {
#(#methods)*
}
}
fn generate_service_implementation(struct_name: &syn::Ident, features: &[String]) -> TokenStream2 {
let trait_name = syn::Ident::new(&format!("{}ServiceTrait", struct_name), proc_macro2::Span::call_site());
let result_type = syn::Ident::new(&format!("{}Result", struct_name), proc_macro2::Span::call_site());
let error_type = syn::Ident::new(&format!("{}Error", struct_name), proc_macro2::Span::call_site());
let config_type = syn::Ident::new(&format!("{}Config", struct_name), proc_macro2::Span::call_site());
let entity_name = struct_name.to_string().replace("Service", "");
let entity_ident = syn::Ident::new(&entity_name, proc_macro2::Span::call_site());
let repository_trait = syn::Ident::new(&format!("{}RepositoryTrait", entity_name), proc_macro2::Span::call_site());
let create_input = syn::Ident::new(&format!("Create{}Input", entity_name), proc_macro2::Span::call_site());
let update_input = syn::Ident::new(&format!("Update{}Input", entity_name), proc_macro2::Span::call_site());
let dependency_fields = generate_dependency_fields(features);
let constructor_params = generate_constructor_params(features);
let constructor_assigns = generate_constructor_assigns(features);
quote! {
pub struct #struct_name {
repository: std::sync::Arc<dyn #repository_trait>,
config: #config_type,
#dependency_fields
}
impl #struct_name {
pub fn new(
repository: std::sync::Arc<dyn #repository_trait>,
config: #config_type,
#constructor_params
) -> Self {
Self {
repository,
config,
#constructor_assigns
}
}
}
#[async_trait::async_trait]
impl #trait_name for #struct_name {
async fn create(&self, product: &str, input: #create_input) -> #result_type<#entity_ident> {
input.validate().map_err(#error_type::Validation)?;
let entity = #entity_ident::new(product.to_string(), );
let saved_entity = self.repository.create(&entity).await?;
Ok(saved_entity)
}
async fn get_by_id(&self, id: uuid::Uuid, product: &str) -> #result_type<Option<#entity_ident>> {
self.repository.find_by_id(id, product).await
}
async fn update(&self, id: uuid::Uuid, product: &str, input: #update_input) -> #result_type<#entity_ident> {
let mut entity = self.repository.find_by_id(id, product)
.await?
.ok_or_else(|| #error_type::NotFound(format!("Entity not found: {}", id)))?;
let updated_entity = self.repository.update(&entity).await?;
Ok(updated_entity)
}
async fn delete(&self, id: uuid::Uuid, product: &str) -> #result_type<bool> {
self.repository.delete(id, product).await
}
async fn list(&self, product: &str, limit: i64, offset: i64) -> #result_type<Vec<#entity_ident>> {
self.repository.list_by_product(product, limit, offset).await
}
async fn count(&self, product: &str) -> #result_type<i64> {
self.repository.count_by_product(product).await
}
async fn exists(&self, id: uuid::Uuid, product: &str) -> #result_type<bool> {
self.repository.exists(id, product).await
}
}
}
}
fn generate_dependency_fields(features: &[String]) -> TokenStream2 {
let mut fields = Vec::new();
if features.contains(&"cache".to_string()) {
fields.push(quote! { cache: std::sync::Arc<dyn CacheServiceTrait>, });
}
if features.contains(&"payments".to_string()) {
fields.push(quote! { payment_service: std::sync::Arc<dyn PaymentServiceTrait>, });
}
if features.contains(&"notifications".to_string()) {
fields.push(quote! { notification_service: std::sync::Arc<dyn NotificationServiceTrait>, });
}
quote! { #(#fields)* }
}
fn generate_constructor_params(features: &[String]) -> TokenStream2 {
let mut params = Vec::new();
if features.contains(&"cache".to_string()) {
params.push(quote! { cache: std::sync::Arc<dyn CacheServiceTrait>, });
}
if features.contains(&"payments".to_string()) {
params.push(quote! { payment_service: std::sync::Arc<dyn PaymentServiceTrait>, });
}
if features.contains(&"notifications".to_string()) {
params.push(quote! { notification_service: std::sync::Arc<dyn NotificationServiceTrait>, });
}
quote! { #(#params)* }
}
fn generate_constructor_assigns(features: &[String]) -> TokenStream2 {
let mut assigns = Vec::new();
if features.contains(&"cache".to_string()) {
assigns.push(quote! { cache, });
}
if features.contains(&"payments".to_string()) {
assigns.push(quote! { payment_service, });
}
if features.contains(&"notifications".to_string()) {
assigns.push(quote! { notification_service, });
}
quote! { #(#assigns)* }
}
fn generate_graphql_resolvers(struct_name: &syn::Ident) -> TokenStream2 {
let entity_name = struct_name.to_string().replace("Service", "");
let query_name = syn::Ident::new(&format!("{}Query", entity_name), proc_macro2::Span::call_site());
let mutation_name = syn::Ident::new(&format!("{}Mutation", entity_name), proc_macro2::Span::call_site());
let service_trait = syn::Ident::new(&format!("{}ServiceTrait", struct_name), proc_macro2::Span::call_site());
quote! {
pub struct #query_name;
#[async_graphql::Object]
impl #query_name {
async fn get_by_id(
&self,
ctx: &async_graphql::Context<'_>,
id: uuid::Uuid,
) -> async_graphql::Result<Option<crate::models::#entity_name>> {
let service = ctx.data::<std::sync::Arc<dyn #service_trait>>()?;
let product = ctx.data::<String>()?;
let result = service.get_by_id(id, product).await?;
Ok(result)
}
async fn list(
&self,
ctx: &async_graphql::Context<'_>,
limit: Option<i32>,
offset: Option<i32>,
) -> async_graphql::Result<Vec<crate::models::#entity_name>> {
let service = ctx.data::<std::sync::Arc<dyn #service_trait>>()?;
let product = ctx.data::<String>()?;
let limit = limit.unwrap_or(50) as i64;
let offset = offset.unwrap_or(0) as i64;
let result = service.list(product, limit, offset).await?;
Ok(result)
}
}
pub struct #mutation_name;
#[async_graphql::Object]
impl #mutation_name {
async fn create(
&self,
ctx: &async_graphql::Context<'_>,
input: crate::api::CreateInput,
) -> async_graphql::Result<crate::models::Entity> {
let service = ctx.data::<std::sync::Arc<dyn #service_trait>>()?;
let product = ctx.data::<String>()?;
let result = service.create(product, input.into()).await?;
Ok(result)
}
async fn update(
&self,
ctx: &async_graphql::Context<'_>,
id: uuid::Uuid,
input: crate::api::UpdateInput,
) -> async_graphql::Result<crate::models::Entity> {
let service = ctx.data::<std::sync::Arc<dyn #service_trait>>()?;
let product = ctx.data::<String>()?;
let result = service.update(id, product, input.into()).await?;
Ok(result)
}
async fn delete(
&self,
ctx: &async_graphql::Context<'_>,
id: uuid::Uuid,
) -> async_graphql::Result<bool> {
let service = ctx.data::<std::sync::Arc<dyn #service_trait>>()?;
let product = ctx.data::<String>()?;
let result = service.delete(id, product).await?;
Ok(result)
}
}
}
}
fn generate_service_error_types(struct_name: &syn::Ident) -> TokenStream2 {
let error_name = syn::Ident::new(&format!("{}Error", struct_name), proc_macro2::Span::call_site());
let result_name = syn::Ident::new(&format!("{}Result", struct_name), proc_macro2::Span::call_site());
quote! {
#[derive(Debug, thiserror::Error)]
pub enum #error_name {
#[error("Not found: {0}")]
NotFound(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Cache error: {0}")]
Cache(String),
#[error("Internal error: {0}")]
Internal(String),
}
pub type #result_name<T> = Result<T, #error_name>;
}
}
fn generate_service_config_types(struct_name: &syn::Ident, service_name: &str) -> TokenStream2 {
let config_name = syn::Ident::new(&format!("{}Config", struct_name), proc_macro2::Span::call_site());
quote! {
#[derive(Debug, Clone, serde::Deserialize)]
pub struct #config_name {
pub service_name: String,
pub database: DatabaseConfig,
pub cache: CacheConfig,
pub features: std::collections::HashMap<String, bool>,
}
impl Default for #config_name {
fn default() -> Self {
Self {
service_name: #service_name.to_string(),
database: DatabaseConfig::default(),
cache: CacheConfig::default(),
features: std::collections::HashMap::new(),
}
}
}
impl #config_name {
pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self::default())
}
pub fn is_feature_enabled(&self, feature: &str) -> bool {
self.features.get(feature).copied().unwrap_or(false)
}
}
}
}