use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Attribute, Data, DeriveInput, Field, Fields};
use wheel_rs::str_utils::{split_camel_case, CamelFormat};
fn has_attribute(attrs: &[Attribute], name: &str) -> bool {
attrs.iter().any(|attr| attr.path().is_ident(name))
}
fn generate_field_attrs(field: &Field) -> TokenStream {
let ty = &field.ty;
let has_from = has_attribute(&field.attrs, "from");
let has_builder = has_attribute(&field.attrs, "builder");
let mut attrs = TokenStream::new();
if !has_from {
if let Some(from_attr) = generate_from_attr(ty) {
attrs.extend(from_attr);
}
}
if !has_builder {
if let Some(builder_attr) = generate_builder_attr(field) {
attrs.extend(builder_attr);
}
}
attrs
}
fn generate_from_attr(ty: &syn::Type) -> Option<TokenStream> {
Some(match ty {
syn::Type::Path(type_path) => {
let path_str = type_path.path.segments.last().unwrap().ident.to_string();
if path_str.ends_with("Vo") {
return Some(quote! { #[from(belongs_to_owned(~))] });
}
if is_option_type(ty) {
if let Some(inner_ty) = extract_option_inner_type(type_path) {
if inner_ty.ends_with("Vo") {
return Some(quote! { #[from(belongs_to_owned(~))] });
}
return Some(match inner_ty.as_str() {
"u8" | "u16" | "u32" | "u64" | "u128" => {
quote! { #[from(~.map(|v|v.into()))] }
}
_ => return None,
});
}
}
Some(match path_str.as_str() {
"u8" | "u16" | "u32" | "u64" | "u128" => quote! { #[from(~.into())] },
_ => return None,
})?
}
_ => return None,
})
}
fn extract_option_inner_type(type_path: &syn::TypePath) -> Option<String> {
if let syn::PathArguments::AngleBracketed(args) = &type_path.path.segments.last()?.arguments {
if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
if let syn::Type::Path(inner_path) = inner_ty {
return Some(inner_path.path.segments.last()?.ident.to_string());
}
}
}
None
}
fn generate_builder_attr(field: &Field) -> Option<TokenStream> {
let ty = &field.ty;
if is_option_type(ty) {
return Some(quote! {
#[builder(default, setter(into))]
});
}
None
}
fn map_unsigned_type(ty: &syn::Type) -> TokenStream {
match ty {
syn::Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
let ident_str = segment.ident.to_string();
if ident_str.ends_with("Vo") {
let ex_vo_type = format_ident!("{}", &ident_str.replace("Vo", "ExVo"));
return quote! { #ex_vo_type };
}
if ident_str == "Option" {
if let Some(inner) = extract_option_inner_type(type_path) {
return match inner.as_str() {
"u8" => quote! { Option<U8> },
"u16" => quote! { Option<U16> },
"u32" => quote! { Option<U32> },
"u64" => quote! { Option<U64> },
"u128" => quote! { Option<U128> },
_ => quote! { #ty },
};
}
return quote! { #ty };
}
match ident_str.as_str() {
"u8" => return quote! { U8 },
"u16" => return quote! { U16 },
"u32" => return quote! { U32 },
"u64" => return quote! { U64 },
"u128" => return quote! { U128 },
_ => {}
}
}
}
_ => {}
}
quote! { #ty }
}
fn is_option_type(ty: &syn::Type) -> bool {
match ty {
syn::Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
segment.ident == "Option"
} else {
false
}
}
_ => false,
}
}
fn get_type_name(ty: &syn::Type) -> Option<String> {
match ty {
syn::Type::Path(type_path) => type_path.path.segments.last().map(|s| s.ident.to_string()),
_ => None,
}
}
fn handle_fields(input: &DeriveInput, is_ex: bool) -> Result<TokenStream, TokenStream> {
Ok(match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => {
let processed_fields: Vec<_> = fields_named
.named
.iter()
.filter_map(|field| {
let field_name = &field.ident;
let is_vo_field =
get_type_name(&field.ty).map_or(false, |name| name.ends_with("Vo"));
if is_vo_field && !is_ex {
return None;
}
let field_ty = map_unsigned_type(&field.ty);
let attrs = generate_field_attrs(field);
let original_attrs: Vec<_> = field
.attrs
.iter()
.filter(|attr| {
!attr.path().is_ident("from")
&& !attr.path().is_ident("builder")
&& !attr.path().is_ident("serde")
})
.collect();
Some(quote! {
#(#original_attrs)*
#attrs
pub #field_name: #field_ty,
})
})
.collect();
quote! {
{ #(#processed_fields)* }
}
}
Fields::Unnamed(_) | Fields::Unit => {
return Err(quote! {
compile_error!("VO macro only supports named fields");
});
}
},
_ => {
return Err(quote! {
compile_error!("VO macro can only be used on structs");
});
}
})
}
pub fn vo_macro(input: DeriveInput) -> TokenStream {
let struct_name = &input.ident;
let vis = &input.vis;
let struct_name_str = struct_name.to_string();
if !struct_name_str.ends_with("Vo") {
return syn::Error::new_spanned(struct_name, "Struct name must end with 'Vo'")
.to_compile_error()
.into();
}
let struct_name_split = split_camel_case(&struct_name_str, CamelFormat::Upper);
if struct_name_split.is_err() {
return syn::Error::new_spanned(
struct_name,
"Struct name must be a valid upper camel case",
)
.to_compile_error()
.into();
}
let mut struct_name_split = struct_name_split.unwrap();
struct_name_split.pop();
let module_name = format_ident!("{}", struct_name_split.join("_").to_lowercase());
let ex_struct_name = format_ident!("{}ExVo", struct_name_split.join(""));
let fields = match handle_fields(&input, false) {
Ok(value) => value,
Err(value) => return value,
};
let ex_fields = match handle_fields(&input, true) {
Ok(value) => value,
Err(value) => return value,
};
let expanded = quote! {
use o2o::o2o;
use serde::Serialize;
use serde_with::{serde_as, skip_serializing_none};
use utoipa::ToSchema;
use derive_setters::Setters;
use typed_builder::TypedBuilder;
use sea_orm::DerivePartialModel;
use wheel_rs::serde::{u64_serde, u64_option_serde};
use robotech::dao::{belongs_to_owned, U8, U16, U32, U64, U128};
use crate::mo::#module_name::{Entity, Model, ModelEx};
use crate::vo::*;
#[skip_serializing_none] #[derive(o2o, ToSchema, DerivePartialModel, Debug, Serialize, Clone, Setters, TypedBuilder)]
#[from_owned(Model)]
#[serde(rename_all = "camelCase")]
#[serde_as]
#[builder]
#[sea_orm(entity = "Entity")]
#vis struct #struct_name #fields
#[skip_serializing_none] #[derive(o2o, ToSchema, Debug, Serialize, Clone, Setters, TypedBuilder)]
#[from_owned(ModelEx)]
#[serde(rename_all = "camelCase")]
#[serde_as]
#[builder]
#vis struct #ex_struct_name #ex_fields
};
TokenStream::from(expanded)
}