#[cfg(test)]
mod tokens;
#[cfg(test)]
use quote::{format_ident, quote};
use syn::{Ident, Type};
#[cfg(test)]
use self::tokens::{build_post_extract_tokens, build_pre_merge_tokens};
use crate::derive::parse::{FieldAttrs, MergeStrategy, btree_map_inner, vec_inner};
#[derive(Default)]
pub(crate) struct CollectionStrategies {
pub append: Vec<(Ident, Type)>,
pub map_replace: Vec<(Ident, Type)>,
}
fn process_vec_field(
field: &syn::Field,
name: Ident,
vec_ty: &Type,
attrs: &FieldAttrs,
) -> syn::Result<Option<(Ident, Type)>> {
let strategy = attrs.merge_strategy.unwrap_or(MergeStrategy::Append);
match strategy {
MergeStrategy::Append => Ok(Some((name, (*vec_ty).clone()))),
MergeStrategy::Replace => Ok(None),
MergeStrategy::Keyed => Err(syn::Error::new_spanned(
field,
"keyed merge strategy is not supported for Vec<_> fields",
)),
}
}
fn process_btree_map_field(
field: &syn::Field,
name: Ident,
field_ty: &Type,
attrs: &FieldAttrs,
) -> syn::Result<Option<(Ident, Type)>> {
let strategy = attrs.merge_strategy.unwrap_or(MergeStrategy::Keyed);
match strategy {
MergeStrategy::Append => Err(syn::Error::new_spanned(
field,
"append merge strategy is not supported for BTreeMap fields",
)),
MergeStrategy::Replace => Ok(Some((name, field_ty.clone()))),
MergeStrategy::Keyed => Ok(None),
}
}
fn validate_non_collection_field(field: &syn::Field, attrs: &FieldAttrs) -> syn::Result<()> {
if attrs.merge_strategy.is_some() {
return Err(syn::Error::new_spanned(
field,
"merge_strategy is only supported on Vec<_> or BTreeMap<_, _> fields",
));
}
Ok(())
}
pub(crate) fn collect_collection_strategies(
fields: &[syn::Field],
field_attrs: &[FieldAttrs],
) -> syn::Result<CollectionStrategies> {
let mut strategies = CollectionStrategies::default();
for (field, attrs) in fields.iter().zip(field_attrs) {
if attrs.is_subcommand {
continue;
}
let Some(name) = field.ident.clone() else {
return Err(syn::Error::new_spanned(
field,
"unnamed (tuple) fields do not support merge strategies",
));
};
if let Some(vec_ty) = vec_inner(&field.ty) {
let Some((vec_name, ty)) = process_vec_field(field, name.clone(), vec_ty, attrs)?
else {
continue;
};
strategies.append.push((vec_name, ty));
continue;
}
if btree_map_inner(&field.ty).is_some() {
let Some((map_name, ty)) =
process_btree_map_field(field, name.clone(), &field.ty, attrs)?
else {
continue;
};
strategies.map_replace.push((map_name, ty));
continue;
}
validate_non_collection_field(field, attrs)?;
}
Ok(strategies)
}
#[cfg(test)]
pub(crate) fn build_override_struct(
base: &Ident,
strategies: &CollectionStrategies,
) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
let ident = format_ident!("__{}CollectionOverride", base);
let vec_fields = strategies.append.iter().map(|(name, ty)| {
quote! {
#[serde(skip_serializing_if = "Option::is_none")]
pub #name: Option<Vec<#ty>>
}
});
let map_fields = strategies.map_replace.iter().map(|(name, _ty)| {
quote! {
#[serde(skip_serializing_if = "Option::is_none")]
pub #name: Option<ortho_config::serde_json::Value>
}
});
let struct_fields = vec_fields.chain(map_fields);
let init_vec = strategies
.append
.iter()
.map(|(name, _)| quote! { #name: None });
let init_map = strategies
.map_replace
.iter()
.map(|(name, _)| quote! { #name: None });
let init = init_vec.chain(init_map);
let ts = quote! {
#[derive(serde::Serialize)]
struct #ident {
#( #struct_fields, )*
}
};
let init_ts = quote! { #ident { #( #init, )* } };
(ts, init_ts)
}
#[cfg(test)]
pub(crate) struct CollectionLogicTokens {
pub pre_merge: proc_macro2::TokenStream,
pub post_extract: proc_macro2::TokenStream,
}
#[cfg(test)]
pub(crate) fn build_collection_logic(
strategies: &CollectionStrategies,
cli_binding: &proc_macro2::TokenStream,
) -> CollectionLogicTokens {
if strategies.append.is_empty() && strategies.map_replace.is_empty() {
return CollectionLogicTokens {
pre_merge: quote! {},
post_extract: quote! {},
};
}
let pre_merge = build_pre_merge_tokens(strategies, cli_binding);
let post_extract = build_post_extract_tokens(strategies);
CollectionLogicTokens {
pre_merge,
post_extract,
}
}