use super::r#override::{
CollectionStrategies, build_collection_logic, build_override_struct,
collect_collection_strategies,
};
use crate::derive::parse::{FieldAttrs, StructAttrs, parse_input};
use anyhow::{Result, anyhow, ensure};
use quote::{ToTokens, quote};
use rstest::{fixture, rstest};
type DemoInput = (Vec<syn::Field>, Vec<FieldAttrs>, StructAttrs);
#[expect(
clippy::needless_pass_by_value,
reason = "helper must take ownership so tests exercise move-only parse semantics"
)]
fn assert_collection_strategy_error(
input: syn::DeriveInput,
expected_error_substring: &str,
context: &str,
) -> Result<()> {
let (_, fields, _, field_attrs) = parse_input(&input)?;
let result = collect_collection_strategies(&fields, &field_attrs);
let Err(err) = result else {
return Err(anyhow!("{context}"));
};
ensure!(
err.to_string().contains(expected_error_substring),
"unexpected error message: {err}",
);
Ok(())
}
fn collect_strategies(input: &syn::DeriveInput) -> Result<CollectionStrategies> {
let (_, fields, _, field_attrs) = parse_input(input)?;
Ok(collect_collection_strategies(&fields, &field_attrs)?)
}
fn assert_collection_skip(
input: &syn::DeriveInput,
check_append: bool,
context: &str,
) -> Result<()> {
let strategies = collect_strategies(input)?;
if check_append {
ensure!(strategies.append.is_empty(), "{context}");
} else {
ensure!(strategies.map_replace.is_empty(), "{context}");
}
Ok(())
}
#[fixture]
fn demo_input() -> Result<DemoInput> {
let input: syn::DeriveInput = syn::parse_quote! {
#[ortho_config(prefix = "CFG_")]
struct Demo {
#[ortho_config(cli_long = "opt", cli_short = 'o', default = 5)]
field1: Option<u32>,
#[ortho_config(merge_strategy = "append")]
field2: Vec<String>,
#[ortho_config(merge_strategy = "replace")]
field3: std::collections::BTreeMap<String, u32>,
}
};
let (_, fields, struct_attrs, field_attrs) = parse_input(&input)?;
Ok((fields, field_attrs, struct_attrs))
}
#[rstest]
fn collect_collection_strategies_selects_collections(demo_input: Result<DemoInput>) -> Result<()> {
let (fields, field_attrs, _) = demo_input?;
let strategies = collect_collection_strategies(&fields, &field_attrs)?;
ensure!(
strategies.append.len() == 1,
"expected single append field, found {}",
strategies.append.len()
);
ensure!(
strategies.map_replace.len() == 1,
"expected single map replace field, found {}",
strategies.map_replace.len()
);
let (map_ident, map_ty) = strategies
.map_replace
.first()
.expect("map replace strategies should contain an entry");
ensure!(
map_ident == "field3",
"expected map field identifier to match, found {map_ident}"
);
ensure!(
map_ty.to_token_stream().to_string() == "std :: collections :: BTreeMap < String , u32 >",
"unexpected map field type: {map_ty:?}",
);
Ok(())
}
#[rstest]
fn build_collection_logic_includes_map_assignment(demo_input: Result<DemoInput>) -> Result<()> {
let (fields, field_attrs, _) = demo_input?;
let strategies = collect_collection_strategies(&fields, &field_attrs)?;
let tokens = build_collection_logic(&strategies, "e!(std::option::Option::<&()>::None));
ensure!(
tokens.post_extract.to_string().contains("cfg . field3"),
"expected generated map reassignment"
);
Ok(())
}
#[rstest]
fn build_collection_logic_preserves_empty_maps(demo_input: Result<DemoInput>) -> Result<()> {
let (fields, field_attrs, _) = demo_input?;
let strategies = collect_collection_strategies(&fields, &field_attrs)?;
let tokens = build_collection_logic(&strategies, "e!(std::option::Option::<&()>::None));
let pre_merge = tokens.pre_merge.to_string();
let guard_count = pre_merge.matches("! v . is_empty ()").count();
let assignment_count = pre_merge.matches("replace . field3 = Some (v").count();
ensure!(
guard_count == 0,
"expected is_empty guard to be removed, found {guard_count}"
);
ensure!(
assignment_count >= 3,
"expected map assignments for each figment source, found {assignment_count}"
);
Ok(())
}
#[rstest]
fn build_override_struct_creates_struct(demo_input: Result<DemoInput>) -> Result<()> {
let (fields, field_attrs, _) = demo_input?;
let strategies = collect_collection_strategies(&fields, &field_attrs)?;
let (ts, init_ts) = build_override_struct(&syn::parse_quote!(Demo), &strategies);
ensure!(
ts.to_string().contains("struct __DemoCollectionOverride"),
"override struct missing expected identifier",
);
ensure!(
init_ts.to_string().contains("__DemoCollectionOverride"),
"override init missing expected struct",
);
Ok(())
}
#[rstest]
#[case::non_collection_append(
syn::parse_quote! {
struct DemoAppendError {
#[ortho_config(merge_strategy = "append")]
field1: u32,
}
},
"merge_strategy is only supported on Vec<_> or BTreeMap<_, _> fields",
"expected strategy validation to fail"
)]
#[case::keyed_vec(
syn::parse_quote! {
struct DemoKeyedVecError {
#[ortho_config(merge_strategy = "keyed")]
items: Vec<String>,
}
},
"keyed merge strategy is not supported for Vec<_> fields",
"expected keyed strategy on Vec to be rejected"
)]
#[case::append_map(
syn::parse_quote! {
struct DemoAppendMapError {
#[ortho_config(merge_strategy = "append")]
settings: std::collections::BTreeMap<String, i32>,
}
},
"append merge strategy is not supported for BTreeMap fields",
"expected append strategy on BTreeMap to be rejected"
)]
fn collect_collection_strategies_rejects_invalid_strategy_usage(
#[case] input: syn::DeriveInput,
#[case] expected_error: &str,
#[case] context: &str,
) -> Result<()> {
assert_collection_strategy_error(input, expected_error, context)
}
#[test]
fn collect_collection_strategies_skips_replace_vec() -> Result<()> {
let input: syn::DeriveInput = syn::parse_quote! {
struct DemoReplaceVec {
#[ortho_config(merge_strategy = "replace")]
values: Vec<String>,
}
};
assert_collection_skip(
&input,
true,
"vector replace strategy should not populate append list",
)
}
#[test]
fn collect_collection_strategies_skips_keyed_map_entry() -> Result<()> {
let input: syn::DeriveInput = syn::parse_quote! {
struct DemoKeyedMap {
#[ortho_config(merge_strategy = "keyed")]
values: std::collections::BTreeMap<String, i32>,
}
};
assert_collection_skip(
&input,
false,
"keyed map strategy should not populate replace list",
)
}