use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use syn::{
Expr, Field, GenericArgument, Ident, Meta, PathArguments, Token, Type, parse::Parse, parse2,
punctuated::Punctuated,
};
pub enum AttributeParam {
Flag(Ident),
NameValue {
ident: Ident,
value: Expr,
eq: Token![=],
},
}
impl AttributeParam {
pub fn ident(&self) -> &Ident {
match self {
AttributeParam::Flag(ident) => ident,
AttributeParam::NameValue { ident, .. } => ident,
}
}
}
impl Parse for AttributeParam {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let ident: Ident = input.parse()?;
if input.peek(Token![=]) {
let eq = input.parse()?;
let value = input.parse()?;
Ok(AttributeParam::NameValue { ident, value, eq })
} else {
Ok(AttributeParam::Flag(ident))
}
}
}
impl ToTokens for AttributeParam {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self {
AttributeParam::Flag(ident) => ident.to_tokens(tokens),
AttributeParam::NameValue { ident, value, eq } => {
ident.to_tokens(tokens);
eq.to_tokens(tokens);
value.to_tokens(tokens);
}
}
}
}
#[derive(Default)]
pub struct CongenFieldAttribute {
pub default: Option<CongenDefault>,
pub inner_default: Option<Expr>,
}
pub enum CongenDefault {
UseDefault,
UseRust,
Expr(Expr),
}
impl Parse for CongenFieldAttribute {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args = Punctuated::<AttributeParam, Token![,]>::parse_terminated(input)?;
let mut default = None;
let mut inner_default = None;
for arg in args {
match arg.ident().to_string().as_str() {
"rust_default" => {
if default.is_some() {
return Err(syn::Error::new_spanned(
arg,
"\"default\" should only be specified once in `congen` attribute",
));
}
if !matches!(arg, AttributeParam::Flag(_)) {
return Err(syn::Error::new_spanned(
arg,
"\"rust_default\" takes no arguments",
));
}
default = Some(CongenDefault::UseRust);
}
"default" => {
if default.is_some() {
return Err(syn::Error::new_spanned(
arg,
"\"default\" should only be specified once in `congen` attribute",
));
}
default = Some(match arg {
AttributeParam::Flag(_ident) => CongenDefault::UseDefault,
AttributeParam::NameValue { value, .. } => CongenDefault::Expr(value),
});
}
"inner_default" => {
if inner_default.is_some() {
return Err(syn::Error::new_spanned(
arg,
"\"inner_default\" should only be specified once in `congen` attribute",
));
}
let AttributeParam::NameValue { value: expr, .. } = arg else {
return Err(syn::Error::new_spanned(
arg,
"\"inner_default\" requires expression as argument",
));
};
inner_default = Some(expr);
}
_ => {
return Err(syn::Error::new_spanned(
arg,
"unknown argument to `congen` attribute",
));
}
}
}
Ok(CongenFieldAttribute {
default,
inner_default,
})
}
}
pub struct CongenField {
pub attr: CongenFieldAttribute,
pub field: Field,
pub inner_type: Option<InnerType>,
pub is_option: bool,
}
#[allow(clippy::large_enum_variant)]
pub enum InnerType {
Single(Type),
#[expect(unused)]
Double(Type, Type),
}
impl InnerType {
pub fn field_type(&self) -> &Type {
match self {
InnerType::Single(typ) => typ,
InnerType::Double(_, typ) => typ,
}
}
}
impl CongenField {
pub fn from_field(errors: &mut Vec<syn::Error>, field: Field) -> CongenField {
let attr = field
.attrs
.iter()
.find_map(|attr| match &attr.meta {
Meta::List(meta_list) if meta_list.path.is_ident(&format_ident!("congen")) => {
Some(parse2(meta_list.tokens.clone()))
}
_ => None,
})
.unwrap_or(Ok(CongenFieldAttribute::default()))
.unwrap_or_else(|err| {
errors.push(err);
CongenFieldAttribute::default()
});
let inner_type = get_inner_type(&field.ty).unwrap_or_else(|err| {
errors.push(err);
None
});
let is_option = is_option(&field.ty).unwrap_or_else(|err| {
errors.push(err);
false
});
CongenField {
attr,
field,
inner_type,
is_option,
}
}
pub fn derive_default_constructor(&self) -> TokenStream {
let Some(inner_type) = self.inner_type.as_ref() else {
return quote! { None };
};
let inner_type = inner_type.field_type();
if let Some(inner_default) = self.attr.inner_default.as_ref() {
quote! {
Some(|| {
let inner_default: #inner_type = #inner_default;
Box::new(inner_default)
})
}
} else if let Some(CongenDefault::Expr(expr)) = self.attr.default.as_ref()
&& self.is_option
{
quote! {
Some(|| {
let outer_default: core::option::Option<#inner_type> = #expr;
let inner_default: #inner_type = core::option::Option::expect(outer_default,
concat!("In order to use default of Option<",
stringify!(#inner_type),
"> for inner type default the default must be Some(_)."));
Box::new(inner_default)
})
}
} else {
quote! {
Some(|| {
let inner_default = <#inner_type as congen::internal::CongenInternal>::default();
let inner_default: #inner_type = core::result::Result::expect(inner_default,
concat!(stringify!(#inner_type), " does not implement Configuration::default")
);
Box::new(inner_default)
})
}
}
}
}
fn get_inner_type(ty: &Type) -> Result<Option<InnerType>, syn::Error> {
let Type::Path(path) = ty else {
return Ok(None);
};
if path.qself.is_some() {
return Err(syn::Error::new_spanned(
ty,
"Qself path type not supported in option for congen",
));
}
let path = &path.path;
let Some(last_segment) = path.segments.iter().last() else {
return Err(syn::Error::new_spanned(
ty,
"Empty path. Is this even possible?",
));
};
let PathArguments::AngleBracketed(opt_inner_type_args) = &last_segment.arguments else {
return Ok(None);
};
let mut args = opt_inner_type_args.args.iter().cloned();
let Some(GenericArgument::Type(first_inner)) = args.next() else {
return Ok(None);
};
let Some(GenericArgument::Type(second_inner)) = args.next() else {
return Ok(Some(InnerType::Single(first_inner)));
};
Ok(Some(InnerType::Double(first_inner, second_inner)))
}
fn is_option(ty: &Type) -> Result<bool, syn::Error> {
let Type::Path(path) = ty else {
return Ok(false);
};
if path.qself.is_some() {
return Err(syn::Error::new_spanned(
ty,
"Qself path type not supported in option for congen",
));
}
let path = &path.path;
if path.leading_colon.is_some() {
return Ok(false);
}
let mut segments = path.segments.iter();
let Some(first) = segments.next() else {
return Err(syn::Error::new_spanned(
ty,
"Empty path. Is this even possible?",
));
};
match first.ident.to_string().as_str() {
"Option" => Ok(true),
"std" | "core" => {
let Some(opt) = segments.next() else {
return Ok(false);
};
if opt.ident != "option" {
return Ok(false);
};
let Some(opt) = segments.next() else {
return Ok(false);
};
if opt.ident != "Option" {
return Ok(false);
};
Ok(true)
}
_ => Ok(false),
}
}