use proc_macro2::TokenStream;
use quote::quote;
use syn::Ident;
use crate::{CliFieldInfo, MacroComponents};
use super::structs::{generate_cli_struct, generate_defaults_struct};
pub(crate) fn generate_ortho_impl(
config_ident: &Ident,
components: &MacroComponents,
krate: &TokenStream,
) -> TokenStream {
let MacroComponents {
cli_ident,
load_impl,
prefix_fn,
..
} = components;
let prefix_tokens = prefix_fn.clone().unwrap_or_else(|| quote! {});
quote! {
#load_impl
impl #krate::OrthoConfig for #config_ident {
fn load_from_iter<I, T>(iter: I) -> #krate::OrthoResult<Self>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
#cli_ident::load_from_iter(iter)
}
#prefix_tokens
}
const _: () = {
fn _assert_deser<T: serde::de::DeserializeOwned>() {}
let _ = _assert_deser::<#config_ident>;
};
}
}
fn generate_field_extraction(field: &CliFieldInfo) -> TokenStream {
let field_key = &field.serialized_key;
let arg_id = &field.arg_id;
let move_value = quote! {
if let Some(value) = base_map.remove(#field_key) {
map.insert(#field_key.to_owned(), value);
}
};
if field.is_default_as_absent {
quote! {
if matches.value_source(#arg_id)
== Some(clap::parser::ValueSource::CommandLine)
{
#move_value
}
}
} else {
move_value
}
}
fn generate_prune_nulls_helpers() -> TokenStream {
quote! {
fn prune_nulls_inner(value: &mut serde_json::Value, is_root: bool) {
match value {
serde_json::Value::Object(map) => {
for value in map.values_mut() {
prune_nulls_inner(value, false);
}
map.retain(|_, value| !value.is_null());
if !is_root && map.is_empty() {
*value = serde_json::Value::Null;
}
}
serde_json::Value::Array(values) => {
for value in values.iter_mut() {
prune_nulls_inner(value, false);
}
values.retain(|value| !value.is_null());
}
_ => {}
}
}
fn prune_nulls(value: &mut serde_json::Value) {
prune_nulls_inner(value, true);
}
}
}
fn generate_cli_value_extractor_impl(
config_ident: &Ident,
cli_field_info: &[CliFieldInfo],
krate: &TokenStream,
) -> TokenStream {
let has_default_as_absent = cli_field_info.iter().any(|f| f.is_default_as_absent);
if !has_default_as_absent {
return quote! {};
}
let field_extractions: Vec<TokenStream> = cli_field_info
.iter()
.map(generate_field_extraction)
.collect();
let prune_nulls_helpers = generate_prune_nulls_helpers();
quote! {
#[cfg(feature = "serde_json")]
impl #krate::CliValueExtractor for #config_ident {
fn extract_user_provided(
&self,
matches: &clap::ArgMatches,
) -> #krate::OrthoResult<#krate::serde_json::Value> {
use #krate::OrthoResultExt;
use #krate::serde_json;
#prune_nulls_helpers
let mut base = serde_json::to_value(self).into_ortho()?;
prune_nulls(&mut base);
let mut base_map = match base {
serde_json::Value::Object(m) => m,
other => {
return Err(std::sync::Arc::new(#krate::OrthoError::Validation {
key: String::from("cli"),
message: format!(
"expected parsed CLI values to serialize to an object, got {other:?}",
),
}));
}
};
let mut map = serde_json::Map::new();
#(#field_extractions)*
Ok(serde_json::Value::Object(map))
}
}
#[cfg(not(feature = "serde_json"))]
const _: () = {
compile_error!(
"cli_default_as_absent requires enabling the ortho_config `serde_json` feature"
);
};
}
}
pub(crate) fn generate_trait_implementation(
config_ident: &Ident,
components: &MacroComponents,
krate: &TokenStream,
) -> TokenStream {
let cli_struct = generate_cli_struct(config_ident, components);
let defaults_struct = generate_defaults_struct(config_ident, components);
let ortho_impl = generate_ortho_impl(config_ident, components, krate);
let cli_extractor_impl =
generate_cli_value_extractor_impl(config_ident, &components.cli_field_info, krate);
quote! {
#cli_struct
#defaults_struct
#ortho_impl
#cli_extractor_impl
}
}