use proc_macro2::TokenStream;
use quote::quote;
use syn::DeriveInput;
use crate::derive::parse::clap_variant_name;
fn variant_has_matches(variant: &syn::Variant) -> syn::Result<bool> {
let mut has_matches = false;
for attr in &variant.attrs {
if !attr.path().is_ident("ortho_subcommand") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("with_matches") {
has_matches = true;
return Ok(());
}
Err(meta.error("unsupported ortho_subcommand option"))
})?;
}
Ok(has_matches)
}
fn validate_tuple_variant(variant_ident: &syn::Ident, fields: &syn::Fields) -> syn::Result<()> {
match fields {
syn::Fields::Unnamed(unnamed_fields) if unnamed_fields.unnamed.len() == 1 => Ok(()),
syn::Fields::Named(_) => Err(syn::Error::new_spanned(
variant_ident,
"named-field variants are not supported; use tuple variants like Variant(Args)",
)),
syn::Fields::Unnamed(_) => Err(syn::Error::new_spanned(
variant_ident,
"tuple variants must contain exactly one field",
)),
syn::Fields::Unit => Err(syn::Error::new_spanned(
variant_ident,
"unit variants are not supported; use Variant(Args)",
)),
}
}
fn parse_crate_path(attrs: &[syn::Attribute]) -> syn::Result<Option<syn::Path>> {
let mut crate_path = None;
for attr in attrs {
if !attr.path().is_ident("ortho_config") {
continue;
}
attr.parse_nested_meta(|meta| {
if !meta.path.is_ident("crate") {
return Err(meta.error("unsupported ortho_config option on enum"));
}
if crate_path.is_some() {
return Err(meta.error("duplicate `crate` attribute"));
}
crate_path = Some(crate::derive::parse::lit_crate_path(&meta)?);
Ok(())
})?;
}
Ok(crate_path)
}
fn merge_expr(has_matches: bool, selected_label: &syn::LitStr, krate: &TokenStream) -> TokenStream {
if has_matches {
quote! {
{
let subcommand_matches = matches
.subcommand()
.map(|(_, subcommand_matches)| subcommand_matches)
.ok_or_else(|| {
#krate::SelectedSubcommandMergeError::MissingSubcommandMatches {
selected: #selected_label,
}
})?;
#krate::SubcmdConfigMerge::load_and_merge_with_matches(&args, subcommand_matches)
.map_err(#krate::SelectedSubcommandMergeError::from)?
}
}
} else {
quote! {
#krate::SubcmdConfigMerge::load_and_merge(&args)
.map_err(#krate::SelectedSubcommandMergeError::from)?
}
}
}
fn build_arm(variant_ident: &syn::Ident, merge_expr: &TokenStream) -> TokenStream {
quote! {
Self::#variant_ident(args) => {
let merged = #merge_expr;
Ok(Self::#variant_ident(merged))
}
}
}
pub(crate) fn derive_selected_subcommand_merge(input: DeriveInput) -> syn::Result<TokenStream> {
let crate_path = parse_crate_path(&input.attrs)?;
let krate = crate::derive::crate_path::resolve(crate_path.as_ref());
let ident = input.ident;
let generics = input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let syn::Data::Enum(enum_data) = input.data else {
return Err(syn::Error::new_spanned(
ident,
"SelectedSubcommandMerge can only be derived for enums",
));
};
let mut arms = Vec::new();
for variant in enum_data.variants {
let has_matches = variant_has_matches(&variant)?;
let selected_label = clap_variant_name(&variant)?
.unwrap_or_else(|| syn::LitStr::new(&variant.ident.to_string(), variant.ident.span()));
let variant_ident = variant.ident;
validate_tuple_variant(&variant_ident, &variant.fields)?;
let merge_tokens = merge_expr(has_matches, &selected_label, &krate);
arms.push(build_arm(&variant_ident, &merge_tokens));
}
Ok(quote! {
impl #impl_generics #krate::SelectedSubcommandMerge for #ident #ty_generics #where_clause {
fn load_and_merge_selected(
self,
matches: &clap::ArgMatches,
) -> std::result::Result<Self, #krate::SelectedSubcommandMergeError> {
match self {
#(#arms)*
}
}
}
})
}