use crate::analysis::StructAnalysis;
use crate::generation::GenerationConfig;
use crate::utils::identifiers::generate_unique_identifier;
use proc_macro2::TokenStream;
use quote::quote;
#[derive(Debug, Clone)]
pub struct TokenGenerator<'a> {
analysis: &'a StructAnalysis,
config: GenerationConfig,
phantom_data_field_name: String,
}
impl<'a> TokenGenerator<'a> {
pub fn new(analysis: &'a StructAnalysis) -> Self {
let config = GenerationConfig {
const_builder: analysis.struct_attributes().get_const_builder(),
..Default::default()
};
Self {
analysis,
config,
phantom_data_field_name: generate_unique_identifier("_marker"),
}
}
pub fn analysis(&self) -> &StructAnalysis {
self.analysis
}
pub fn config(&self) -> &GenerationConfig {
&self.config
}
pub fn is_const_builder(&self) -> bool {
self.config.const_builder
}
pub fn const_keyword(&self) -> TokenStream {
if self.config.const_builder {
quote! { const }
} else {
quote! {}
}
}
pub fn get_phantom_data_field_name(&self) -> &str {
&self.phantom_data_field_name
}
pub fn impl_generics_tokens(&self) -> TokenStream {
self.analysis.impl_generics_tokens()
}
pub fn type_generics_tokens(&self) -> TokenStream {
self.analysis.type_generics_tokens()
}
pub fn where_clause_tokens(&self) -> TokenStream {
self.analysis.where_clause_tokens()
}
pub fn generate_method_documentation(
&self,
method_name: &str,
description: &str,
additional_info: Option<&str>,
) -> TokenStream {
if !self.config.include_documentation {
return quote! {};
}
let mut doc_lines = vec![format!("{description}.")];
if let Some(info) = additional_info {
doc_lines.push(String::new()); doc_lines.push(info.to_string());
}
if self.config.include_error_guidance {
match method_name {
"builder" => {
doc_lines.push(String::new());
let build_method_name =
self.analysis.struct_attributes().get_build_method_name();
doc_lines.push(format!(
"Create a builder, set required fields, then call `{build_method_name}()`."
));
}
"build" => {
doc_lines.push(String::new());
doc_lines.push("# Panics".to_string());
doc_lines.push(String::new());
doc_lines.push(
"This method is only available after all required fields have been set."
.to_string(),
);
}
_ => {}
}
}
let doc_comments: Vec<TokenStream> = doc_lines
.into_iter()
.map(|line| quote! { #[doc = #line] })
.collect();
quote! { #(#doc_comments)* }
}
pub fn generate_field_documentation(
&self,
field_name: &str,
field_type: &str,
is_required: bool,
context: &str,
) -> TokenStream {
if !self.config.include_documentation {
return quote! {};
}
let requirement = if is_required { "required" } else { "optional" };
let doc_text =
format!("{context} {requirement} field `{field_name}` of type `{field_type}`.");
quote! { #[doc = #doc_text] }
}
pub fn generate_type_path(&self, type_name: &str) -> TokenStream {
if self.config.use_qualified_paths {
match type_name {
"Option" => quote! { ::core::option::Option },
"Vec" => quote! { Vec },
"String" => quote! { String },
"Default" => quote! { ::core::default::Default },
"PhantomData" => quote! { ::core::marker::PhantomData },
_ => {
let ident = syn::parse_str::<syn::Ident>(type_name).unwrap_or_else(|_| {
syn::Ident::new("Unknown", proc_macro2::Span::call_site())
});
quote! { #ident }
}
}
} else {
let ident = syn::parse_str::<syn::Ident>(type_name)
.unwrap_or_else(|_| syn::Ident::new("Unknown", proc_macro2::Span::call_site()));
quote! { #ident }
}
}
pub fn generate_debug_impl(
&self,
type_name: &TokenStream,
type_generics: &TokenStream,
) -> TokenStream {
if !self.config.generate_debug_impls {
return quote! {};
}
let impl_generics = self.impl_generics_tokens();
let where_clause = self.where_clause_tokens();
quote! {
#[automatically_derived]
impl #impl_generics ::core::fmt::Debug for #type_name #type_generics #where_clause {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.debug_struct(stringify!(#type_name)).finish()
}
}
}
}
pub fn generate_phantom_data_field(&self) -> TokenStream {
if !self.analysis.needs_phantom_data() {
return quote! {};
}
let field_types: Vec<&syn::Type> =
self.analysis.all_fields().map(|f| f.field_type()).collect();
let phantom_data_type = crate::utils::generics::generate_phantom_data_type(
field_types.into_iter(),
self.analysis.struct_generics(),
);
if phantom_data_type.is_empty() {
return quote! {};
}
let field_name = self.get_phantom_data_field_name();
let field_ident = syn::parse_str::<syn::Ident>(field_name)
.unwrap_or_else(|_| syn::Ident::new("_marker", proc_macro2::Span::call_site()));
let doc = if self.config.include_documentation {
quote! { #[doc = "PhantomData to track generics and lifetimes from the original struct."] }
} else {
quote! {}
};
quote! {
#doc
#field_ident: #phantom_data_type,
}
}
pub fn generate_phantom_data_init(&self) -> TokenStream {
if !self.analysis.needs_phantom_data() {
return quote! {};
}
let field_name = self.get_phantom_data_field_name();
let field_ident = syn::parse_str::<syn::Ident>(field_name)
.unwrap_or_else(|_| syn::Ident::new("_marker", proc_macro2::Span::call_site()));
let phantom_data_path = self.generate_type_path("PhantomData");
quote! {
#field_ident: #phantom_data_path,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::analyze_struct;
use syn::parse_quote;
#[test]
fn test_token_generator_creation() {
let input = parse_quote!(
struct Example {
name: String,
}
);
let analysis = analyze_struct(&input).unwrap();
let generator = TokenGenerator::new(&analysis);
assert_eq!(generator.analysis().struct_name().to_string(), "Example");
}
#[test]
fn test_token_generator_with_config() {
let input = parse_quote!(
struct Example {
name: String,
}
);
let analysis = analyze_struct(&input).unwrap();
let _config = GenerationConfig::default();
let generator = TokenGenerator::new(&analysis);
assert!(generator.config().include_documentation);
}
#[test]
fn test_generate_method_documentation() {
let input = parse_quote!(
struct Example {
name: String,
}
);
let analysis = analyze_struct(&input).unwrap();
let generator = TokenGenerator::new(&analysis);
let doc =
generator.generate_method_documentation("test_method", "Test method description", None);
let doc_str = doc.to_string();
assert!(doc_str.contains("Test method description"));
}
#[test]
fn test_generate_method_documentation_minimal() {
let input = parse_quote!(
struct Example {
name: String,
}
);
let analysis = analyze_struct(&input).unwrap();
let _config = GenerationConfig::default();
let generator = TokenGenerator::new(&analysis);
let doc =
generator.generate_method_documentation("test_method", "Test method description", None);
assert!(!doc.is_empty());
}
#[test]
fn test_generate_type_path_qualified() {
let input = parse_quote!(
struct Example {
name: String,
}
);
let analysis = analyze_struct(&input).unwrap();
let _config = GenerationConfig {
use_qualified_paths: true,
..Default::default()
};
let generator = TokenGenerator::new(&analysis);
let path = generator.generate_type_path("Option");
assert_eq!(path.to_string(), ":: core :: option :: Option");
}
#[test]
fn test_generate_type_path_unqualified() {
let input = parse_quote!(
struct Example {
name: String,
}
);
let analysis = analyze_struct(&input).unwrap();
let _config = GenerationConfig {
use_qualified_paths: false,
..Default::default()
};
let generator = TokenGenerator::new(&analysis);
let path = generator.generate_type_path("Option");
assert_eq!(path.to_string(), ":: core :: option :: Option");
}
#[test]
fn test_generic_token_generation() {
let input = parse_quote! {
struct Example<T: Clone>
where
T: Send
{
value: T,
}
};
let analysis = analyze_struct(&input).unwrap();
let generator = TokenGenerator::new(&analysis);
let impl_generics = generator.impl_generics_tokens();
let type_generics = generator.type_generics_tokens();
let where_clause = generator.where_clause_tokens();
assert!(!impl_generics.is_empty());
assert!(!type_generics.is_empty());
assert!(!where_clause.is_empty());
}
#[test]
fn test_phantom_data_generation() {
let input = parse_quote! {
struct Example<T> {
value: T,
}
};
let analysis = analyze_struct(&input).unwrap();
let generator = TokenGenerator::new(&analysis);
let phantom_field = generator.generate_phantom_data_field();
let phantom_init = generator.generate_phantom_data_init();
assert!(!phantom_field.is_empty());
assert!(!phantom_init.is_empty());
}
}