use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
punctuated::Punctuated,
token::Comma,
Attribute, Expr, GenericArgument, Ident, ItemStruct, Path, PathArguments, Result as SynResult,
Token, Type,
};
const TX_CST: &str = "tx_cst";
fn extract_inject_expr(attrs: &[Attribute]) -> SynResult<Option<Expr>> {
for attr in attrs {
if attr.path().is_ident(TX_CST) {
let expr: Expr = attr.parse_args()?;
return Ok(Some(expr));
}
}
Ok(None)
}
#[derive(Debug, Clone, PartialEq)]
enum ScopeAttr {
Singleton,
Prototype,
}
#[derive(Debug)]
struct CompAttr {
scope: ScopeAttr,
has_init: bool,
conf: Option<Option<String>>,
}
fn parse_component_attr(attr_tokens: TokenStream) -> SynResult<CompAttr> {
if attr_tokens.is_empty() {
return Ok(CompAttr {
scope: ScopeAttr::Singleton,
has_init: false,
conf: None,
});
}
struct AttrArgs {
scope: ScopeAttr,
has_init: bool,
conf: Option<Option<String>>,
}
impl Parse for AttrArgs {
fn parse(input: ParseStream) -> SynResult<Self> {
let mut scope = ScopeAttr::Singleton;
let mut has_init = false;
let mut conf = None;
loop {
if input.is_empty() {
break;
}
let key: Ident = input.parse()?;
if key == "scope" {
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
let value: Expr = input.parse()?;
let ident_str = match &value {
Expr::Path(p) => p
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default(),
_ => value.to_token_stream().to_string(),
};
scope = match ident_str.as_str() {
"Singleton" => ScopeAttr::Singleton,
"Prototype" => ScopeAttr::Prototype,
other => {
return Err(syn::Error::new_spanned(
&value,
format!(
"未知的 scope `{}`,只支持 Singleton 或 Prototype",
other
),
))
}
};
} else {
scope = ScopeAttr::Prototype;
}
} else if key == "init" {
has_init = true;
} else if key == "conf" {
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
let value: Expr = input.parse()?;
let key_str = match &value {
Expr::Lit(lit) => {
if let syn::Lit::Str(s) = &lit.lit {
s.value()
} else {
return Err(syn::Error::new_spanned(
&value,
"conf 的值必须是字符串字面量",
));
}
}
_ => {
return Err(syn::Error::new_spanned(
&value,
"conf 的值必须是字符串字面量",
));
}
};
conf = Some(Some(key_str));
} else {
conf = Some(None);
}
} else {
return Err(syn::Error::new_spanned(
key,
"#[tx_comp] 只支持 scope 和 init 参数,\
例如:#[tx_comp(scope = Prototype, init)]",
));
}
if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
} else {
break;
}
}
Ok(AttrArgs { scope, has_init, conf })
}
}
let args: AttrArgs = syn::parse(attr_tokens)?;
Ok(CompAttr {
scope: args.scope,
has_init: args.has_init,
conf: args.conf,
})
}
#[proc_macro_attribute]
pub fn tx_comp(attr: TokenStream, item: TokenStream) -> TokenStream {
let comp_attr = match parse_component_attr(attr) {
Ok(s) => s,
Err(e) => return e.to_compile_error().into(),
};
let input = parse_macro_input!(item as ItemStruct);
match component_impl(comp_attr, input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
#[proc_macro_attribute]
pub fn tx_cst(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
fn component_impl(comp_attr: CompAttr, input: ItemStruct) -> SynResult<TokenStream2> {
let struct_name = &input.ident;
let vis = &input.vis;
enum FieldKind {
Inject { ty: Type },
Custom { expr: Expr },
}
let mut fields_info: Vec<(syn::Ident, FieldKind)> = Vec::new();
let mut clean_fields = input.fields.clone();
for field in &mut clean_fields {
field.attrs.retain(|a| !a.path().is_ident(TX_CST));
}
for field in &input.fields {
let ident = field
.ident
.as_ref()
.ok_or_else(|| syn::Error::new_spanned(&field.ty, "#[tx_comp] 只支持具名字段"))?;
let inject_expr = extract_inject_expr(&field.attrs)?;
let kind = if let Some(expr) = inject_expr {
FieldKind::Custom { expr }
} else {
FieldKind::Inject {
ty: field.ty.clone(),
}
};
fields_info.push((ident.clone(), kind));
}
let build_fields: Vec<TokenStream2> = fields_info
.iter()
.map(|(fname, kind)| {
match kind {
FieldKind::Inject { ty } => {
let inject_ty = strip_arc(ty);
quote! {
#fname: ctx.inject::<#inject_ty>()
}
}
FieldKind::Custom { expr } => {
quote! { #fname: #expr }
}
}
})
.collect();
let mut dep_type_ids: Vec<TokenStream2> = fields_info
.iter()
.filter_map(|(_, kind)| {
match kind {
FieldKind::Inject { ty } => {
let inject_ty = strip_arc(ty);
Some(quote! { || ::std::any::TypeId::of::<#inject_ty>() })
}
FieldKind::Custom { .. } => None, }
})
.collect();
let scope_const = match &comp_attr.scope {
ScopeAttr::Singleton => quote! { ::tx_di_core::Scope::Singleton },
ScopeAttr::Prototype => quote! { ::tx_di_core::Scope::Prototype },
};
let meta_ident = format_ident!(
"__DI_META_{}",
camel_to_screaming_snake(&struct_name.to_string())
);
let clean_input = ItemStruct {
fields: clean_fields,
..input.clone()
};
let comp_init_impl = if comp_attr.has_init {
quote! {}
} else {
quote! {
impl ::tx_di_core::CompInit for #struct_name {}
}
};
let conf_build_code = if let Some(conf_option) = &comp_attr.conf {
let config_key = if let Some(custom_key) = conf_option {
quote! { #custom_key }
} else {
let snake_name = camel_to_snake(&struct_name.to_string());
quote! { #snake_name }
};
dep_type_ids = vec![];
Some(quote! {
fn build(ctx: &mut ::tx_di_core::BuildContext) -> Self {
let app_config = ctx.inject::<::tx_di_core::AppAllConfig>();
if let Some(value) = app_config.get_value(#config_key) {
<Self as ::serde::Deserialize>::deserialize(value.clone())
.unwrap_or_else(|e| {
eprintln!("[di] 警告:配置 '{}' 解析失败: {},使用 serde 默认值", #config_key, e);
let empty_table = ::tx_di_core::Value::Table(::tx_di_core::map::Map::new());
<Self as ::serde::Deserialize>::deserialize(empty_table)
.expect("Failed to deserialize with serde defaults")
})
} else {
eprintln!("[di] 配置 '{}' 不存在,使用 serde 默认值", #config_key);
let empty_table = ::tx_di_core::Value::Table(::tx_di_core::map::Map::new());
<Self as ::serde::Deserialize>::deserialize(empty_table)
.expect("Failed to deserialize with serde defaults")
}
}
})
} else {
None
};
let build_impl = if let Some(conf_code) = conf_build_code {
conf_code
} else {
quote! {
fn build(ctx: &mut ::tx_di_core::BuildContext) -> Self {
Self {
#( #build_fields ),*
}
}
}
};
let output = quote! {
#clean_input
# comp_init_impl
impl ::tx_di_core::ComponentDescriptor for #struct_name {
const DEP_IDS: &'static [fn() -> ::std::any::TypeId] = &[
#( #dep_type_ids ),*
];
const SCOPE: ::tx_di_core::Scope = #scope_const;
#build_impl
}
#[::tx_di_core::linkme::distributed_slice(::tx_di_core::COMPONENT_REGISTRY)]
#[linkme(crate = ::tx_di_core::linkme)]
#[allow(non_upper_case_globals)]
#vis static #meta_ident: ::tx_di_core::ComponentMeta = ::tx_di_core::ComponentMeta {
type_id: || ::std::any::TypeId::of::<#struct_name>(),
deps: &[ #( #dep_type_ids ),* ],
name: ::std::stringify!(#struct_name),
scope: #scope_const,
factory_fn: Some(|ctx: &mut ::tx_di_core::BuildContext| {
::std::boxed::Box::new(
<#struct_name as ::tx_di_core::ComponentDescriptor>::build(ctx)
)
}),
init_sort_fn: <#struct_name as ::tx_di_core::CompInit>::init_sort,
init_fn: Some(<#struct_name as ::tx_di_core::CompInit>::init),
async_init_fn: Some(<#struct_name as ::tx_di_core::CompInit>::async_init),
};
};
Ok(output)
}
struct AppInput {
module_name: Ident,
components: Vec<Path>,
}
impl Parse for AppInput {
fn parse(input: ParseStream) -> SynResult<Self> {
let module_name: Ident = input.parse()?;
let components = if input.peek(syn::token::Bracket) {
let content;
syn::bracketed!(content in input);
if content.is_empty() {
vec![]
} else {
let components: Punctuated<Path, Comma> =
content.parse_terminated(Path::parse, Token![,])?;
components.into_iter().collect()
}
} else {
vec![]
};
Ok(AppInput {
module_name,
components,
})
}
}
#[deprecated(
since = "0.2.0",
note = "请使用 BuildContext::new() 代替,支持配置文件或自动扫描"
)]
#[proc_macro]
pub fn app(input: TokenStream) -> TokenStream {
let AppInput {
module_name,
components,
} = parse_macro_input!(input as AppInput);
match app_impl(module_name, components) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn app_impl(module_name: Ident, components: Vec<Path>) -> SynResult<TokenStream2> {
let fn_name = {
let snake = camel_to_snake(&module_name.to_string());
format_ident!("build_{}", snake, span = Span::call_site())
};
let component_count = components.len();
let build_stmts: Vec<TokenStream2> = components
.iter()
.map(|ty| {
quote! {
ctx.register_factory::<#ty>(
<#ty as ::tx_di_core::ComponentDescriptor>::SCOPE,
|ctx: &mut ::tx_di_core::BuildContext| {
::std::boxed::Box::new(
<#ty as ::tx_di_core::ComponentDescriptor>::build(ctx)
)
},
);
}
})
.collect();
let output = quote! {
#[allow(non_snake_case, dead_code)]
#[deprecated(since = "0.2.0", note = "请使用 BuildContext::new() 代替")]
pub fn #fn_name() -> ::tx_di_core::BuildContext {
let mut ctx = ::tx_di_core::BuildContext::new(None);
if #component_count > 0 {
}
#( #build_stmts )*
ctx
}
};
Ok(output)
}
fn strip_arc(ty: &Type) -> TokenStream2 {
let path = match ty {
Type::Path(tp) => &tp.path,
_ => return quote! { #ty },
};
let segs = &path.segments;
if segs.len() == 1 && segs[0].ident == "Arc" {
if let PathArguments::AngleBracketed(ab) = &segs[0].arguments {
if ab.args.len() == 1 {
if let GenericArgument::Type(inner) = &ab.args[0] {
return quote! { #inner };
}
}
}
}
quote! { #ty }
}
fn camel_to_snake(s: &str) -> String {
let mut result = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() && i != 0 {
result.push('_');
}
result.push(ch.to_lowercase().next().unwrap());
}
result
}
fn camel_to_screaming_snake(s: &str) -> String {
camel_to_snake(s).to_uppercase()
}