use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Field, ItemStruct, Lit, Meta};
pub struct SetterField {
pub field_name: syn::Ident,
pub field_type: syn::Type,
pub is_rename: bool, pub command_name_override: Option<String>, }
pub fn collect_setter_fields(input: &ItemStruct) -> Vec<SetterField> {
let mut setters = Vec::new();
if let syn::Fields::Named(fields) = &input.fields {
for field in &fields.named {
let field_name = field.ident.clone().unwrap();
let field_type = field.ty.clone();
for attr in &field.attrs {
if attr.path().is_ident("myko_rename") {
let command_name_override = parse_string_arg(attr);
setters.push(SetterField {
field_name,
field_type,
is_rename: true,
command_name_override,
});
break;
} else if attr.path().is_ident("myko_setter") {
let command_name_override = parse_string_arg(attr);
setters.push(SetterField {
field_name,
field_type,
is_rename: false,
command_name_override,
});
break;
}
}
}
}
setters
}
fn parse_string_arg(attr: &syn::Attribute) -> Option<String> {
if let Meta::List(meta_list) = &attr.meta
&& let Ok(Lit::Str(lit_str)) = meta_list.parse_args::<Lit>()
{
return Some(lit_str.value());
}
None
}
pub fn strip_setter_attrs(field: &mut Field) {
field.attrs.retain(|attr| {
!attr.path().is_ident("myko_rename") && !attr.path().is_ident("myko_setter")
});
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(c) => c.to_uppercase().chain(chars).collect(),
None => String::new(),
}
})
.collect()
}
pub fn generate_setter_commands(entity_name: &str, setters: &[SetterField]) -> TokenStream {
let entity_ident = format_ident!("{}", entity_name);
let id_type_ident = format_ident!("{}Id", entity_name);
let get_by_id_ident = format_ident!("Get{}ById", entity_name);
let krate = crate::myko_path();
let commands: Vec<TokenStream> = setters
.iter()
.map(|setter| {
let field_name = &setter.field_name;
let field_type = &setter.field_type;
let (command_name, param_name) = if setter.is_rename {
let cmd_name = setter
.command_name_override
.as_ref()
.map(|s| format_ident!("{}", s))
.unwrap_or_else(|| format_ident!("Rename{}", entity_name));
(cmd_name, format_ident!("name"))
} else {
let cmd_name = setter
.command_name_override
.as_ref()
.map(|s| format_ident!("{}", s))
.unwrap_or_else(|| {
let field_pascal = to_pascal_case(&field_name.to_string());
format_ident!("Set{}{}", entity_name, field_pascal)
});
(cmd_name, field_name.clone())
};
let field_assignment = if setter.is_rename {
quote! { #field_name: self.name.to_string() }
} else {
let type_str = quote!(#field_type).to_string();
if type_str.contains("Vec") && type_str.contains("String") {
quote! { #field_name: self.#param_name.iter().map(|s| s.to_string()).collect() }
} else if type_str.contains("String") && !type_str.contains("Option") {
quote! { #field_name: self.#param_name.to_string() }
} else if type_str.contains("Option") && type_str.contains("String") {
quote! { #field_name: self.#param_name.as_ref().map(|s| s.to_string()) }
} else {
quote! { #field_name: self.#param_name.clone() }
}
};
let param_type = {
let type_str = quote!(#field_type).to_string();
if type_str.contains("Vec < String >") || type_str.contains("Vec<String>") {
quote! { Vec<std::sync::Arc<str>> }
} else if type_str == "String" {
quote! { std::sync::Arc<str> }
} else if type_str.contains("Option < String >") || type_str.contains("Option<String>") {
quote! { Option<std::sync::Arc<str>> }
} else {
quote! { #field_type }
}
};
let param_field = if setter.is_rename {
quote! { pub name: std::sync::Arc<str> }
} else {
quote! { pub #param_name: #param_type }
};
quote! {
#[#krate::myko_command]
pub struct #command_name {
pub id: #id_type_ident,
#param_field,
}
impl #krate::command::CommandHandler for #command_name {
fn execute(
self,
ctx: #krate::prelude::CommandContext,
) -> Result<(), #krate::prelude::CommandError> {
let report = #get_by_id_ident { id: self.id.clone() };
let entity = ctx.exec_report(report)?.ok_or_else(|| {
#krate::prelude::CommandError {
tx: ctx.tx().to_string(),
command_id: ctx.command_id.to_string(),
message: format!("{} {} not found", stringify!(#entity_ident), self.id),
}
})?;
let updated = #entity_ident {
#field_assignment,
..entity.as_ref().clone()
};
ctx.emit_set(&updated)?;
Ok(())
}
}
}
})
.collect();
quote! {
#(#commands)*
}
}