ortho_config_macros 0.9.0

Procedural macros for ortho_config.
Documentation
//! Declarative merge code generation for `#[derive(OrthoConfig)]`.
//!
//! Emits merge-state structs, trait implementations, and helper constructors
//! that layer declarative configuration values while honouring collection
//! strategies such as vector appends and map replacements.

mod collection_tokens;
mod guards;
mod merge_tokens;

use proc_macro2::TokenStream;
use quote::{format_ident, quote};

use crate::derive::build::CollectionStrategies;
use crate::derive::generate::generated_doc;

use collection_tokens::{append_collection_tokens, map_collection_tokens};
use merge_tokens::{FinishCollectionTokens, finish_tokens, merge_layer_tokens};

// Re-export for internal use and tests
pub(crate) use collection_tokens::unique_append_fields;

/// Generate the declarative merge state struct.
///
/// Emits a `#[derive(Default)]` struct containing a backing `value` field,
/// optional append buffers for each unique vector field using the append
/// strategy, and replace buffers (`Option<Value>`) for each `BTreeMap` field
/// using the replace strategy.
///
/// # Examples
///
/// ```rust,ignore
/// use proc_macro2::TokenStream;
/// use quote::quote;
/// use syn::parse_str;
/// use crate::derive::build::CollectionStrategies;
///
/// let krate = quote! { ortho_config };
/// let state_ident = parse_str("__SampleDeclarativeMergeState").unwrap();
/// let config_ident = parse_str("SampleConfig").unwrap();
/// let strategies = CollectionStrategies::default();
/// let tokens = generate_declarative_state_struct(&state_ident, &config_ident, &strategies, &krate);
/// let expected: TokenStream = quote! {
///     #[doc = "Declarative merge state generated by `OrthoConfig` for `SampleConfig`."]
///     #[derive(Default)]
///     struct __SampleDeclarativeMergeState {
///         value: ortho_config::serde_json::Value,
///     }
/// };
/// assert_eq!(tokens.to_string(), expected.to_string());
/// ```
pub(crate) fn generate_declarative_state_struct(
    state_ident: &syn::Ident,
    config_ident: &syn::Ident,
    strategies: &CollectionStrategies,
    krate: &TokenStream,
) -> TokenStream {
    let unique_fields = unique_append_fields(&strategies.append);
    let append_state_fields = unique_fields.iter().map(|(name, _ty)| {
        let state_field_ident = format_ident!("append_{}", name);
        quote! {
            #state_field_ident: Option<Vec<#krate::serde_json::Value>>
        }
    });
    let map_replace_fields = strategies.map_replace.iter().map(|(name, _ty)| {
        let state_field_ident = format_ident!("replace_{}", name);
        quote! {
            #state_field_ident: Option<#krate::serde_json::Value>
        }
    });

    let doc = generated_doc(config_ident, "Declarative merge state");

    quote! {
        #[doc = #doc]
        #[derive(Default)]
        struct #state_ident {
            value: #krate::serde_json::Value,
            #( #append_state_fields, )*
            #( #map_replace_fields, )*
        }
    }
}

/// Generate the `DeclarativeMerge` trait implementation.
///
/// Produces merge logic that accumulates append field contributions into
/// per-field JSON buffers and finalises the state into the concrete
/// configuration type, retaining replace buffers for map strategies along the
/// way.
///
/// # Examples
///
/// ```rust,ignore
/// use proc_macro2::TokenStream;
/// use syn::parse_str;
/// use quote::quote;
/// use crate::derive::build::CollectionStrategies;
///
/// let krate = quote! { ortho_config };
/// let state_ident = parse_str("__SampleDeclarativeMergeState").unwrap();
/// let config_ident = parse_str("SampleConfig").unwrap();
/// let strategies = CollectionStrategies::default();
/// let tokens = generate_declarative_merge_impl(&state_ident, &config_ident, &strategies, &krate);
/// assert!(tokens.to_string().contains("DeclarativeMerge"));
/// ```
// The generated merge implementation mirrors the runtime branching structure of
// the declarative merger. Splitting it further would obscure the emitted code.
pub(crate) fn generate_declarative_merge_impl(
    state_ident: &syn::Ident,
    config_ident: &syn::Ident,
    strategies: &CollectionStrategies,
    krate: &TokenStream,
) -> TokenStream {
    let append_tokens = append_collection_tokens(strategies, krate);
    let map_tokens = map_collection_tokens(strategies);
    let collection_tokens::CollectionTokens {
        merge_logic: append_merge_logic,
        destructured: append_destructured,
        inserts: append_inserts,
    } = append_tokens;
    let collection_tokens::CollectionTokens {
        merge_logic: map_merge_logic,
        destructured: map_destructured,
        inserts: map_inserts,
    } = map_tokens;
    let merge_layer_body =
        merge_layer_tokens(config_ident, krate, &map_merge_logic, &append_merge_logic);
    let finish_body = finish_tokens(
        state_ident,
        krate,
        &FinishCollectionTokens {
            append_destructured: &append_destructured,
            map_destructured: &map_destructured,
            append_inserts: &append_inserts,
            map_inserts: &map_inserts,
        },
    );

    quote! {
        impl #krate::DeclarativeMerge for #state_ident {
            type Output = #config_ident;

            fn merge_layer(&mut self, layer: #krate::MergeLayer<'_>) -> #krate::OrthoResult<()> {
                #merge_layer_body
            }

            fn finish(self) -> #krate::OrthoResult<Self::Output> {
                #finish_body
            }
        }
    }
}

/// Generate the public `merge_from_layers` constructor.
///
/// Emits an inherent implementation that constructs the declarative state,
/// folds each layer into it, and calls `finish` to produce the configuration
/// value.
///
/// When `has_post_merge_hook` is `true`, the generated function also builds a
/// `PostMergeContext` and invokes `PostMergeHook::post_merge` after finishing.
///
/// # Examples
///
/// ```rust,ignore
/// use syn::parse_str;
/// use quote::quote;
///
/// let krate = quote! { ortho_config };
/// let state_ident = parse_str("__SampleDeclarativeMergeState").unwrap();
/// let config_ident = parse_str("SampleConfig").unwrap();
/// let tokens = generate_declarative_merge_from_layers_fn(
///     &state_ident, &config_ident, false, &krate,
/// );
/// assert!(tokens.to_string().contains("merge_from_layers"));
/// ```
pub(crate) fn generate_declarative_merge_from_layers_fn(
    state_ident: &syn::Ident,
    config_ident: &syn::Ident,
    has_post_merge_hook: bool,
    krate: &TokenStream,
) -> TokenStream {
    let ctx_init = if has_post_merge_hook {
        quote! {
            let mut ctx = #krate::PostMergeContext::new(Self::prefix());
        }
    } else {
        quote! {}
    };

    let ctx_per_layer = if has_post_merge_hook {
        quote! {
            if let Some(path) = layer.path() {
                ctx.with_file(path.to_owned());
            }
            if layer.provenance() == #krate::MergeProvenance::Cli {
                ctx.with_cli_input();
            }
        }
    } else {
        quote! {}
    };

    let post_merge = if has_post_merge_hook {
        quote! {
            let mut result = #krate::DeclarativeMerge::finish(state)?;
            #krate::PostMergeHook::post_merge(&mut result, &ctx)?;
            Ok(result)
        }
    } else {
        quote! {
            #krate::DeclarativeMerge::finish(state)
        }
    };

    let merge_body = quote! {
        let mut state = #state_ident::default();
        #ctx_init
        for layer in layers {
            #ctx_per_layer
            #krate::DeclarativeMerge::merge_layer(&mut state, layer)?;
        }
        #post_merge
    };

    quote! {
        impl #config_ident {
            /// Merge the configuration struct from declarative layers.
            ///
            /// See the
            /// [declarative merging design](https://github.com/leynos/ortho-config/blob/main/docs/design.md#43-declarative-configuration-merging)
            /// for background and trade-offs.
            ///
            /// # Feature Requirements
            ///
            /// This method requires the `serde_json` feature (enabled by default).
            ///
            /// # Examples
            ///
            /// ```rust,ignore
            /// use ortho_config::{MergeComposer, OrthoConfig};
            /// use serde::{Deserialize, Serialize};
            /// use serde_json::json;
            ///
            /// #[derive(Debug, Deserialize, Serialize, OrthoConfig)]
            /// #[ortho_config(prefix = "APP")]
            /// struct AppConfig {
            ///     #[ortho_config(default = 8080)]
            ///     port: u16,
            /// }
            ///
            /// let mut composer = MergeComposer::new();
            /// composer.push_defaults(json!({"port": 8080}));
            /// composer.push_environment(json!({"port": 9090}));
            ///
            /// let config = AppConfig::merge_from_layers(composer.layers())
            ///     .expect("layers merge successfully");
            /// assert_eq!(config.port, 9090);
            /// ```
            pub fn merge_from_layers<'a, I>(layers: I) -> #krate::OrthoResult<Self>
            where
                I: IntoIterator<Item = #krate::MergeLayer<'a>>,
            {
                #merge_body
            }
        }
    }
}

/// Compose the complete declarative merge implementation.
///
/// Combines the state struct, `DeclarativeMerge` trait implementation, and
/// inherent constructor into a single token stream.
///
/// When `has_post_merge_hook` is `true`, the generated `merge_from_layers`
/// builds a `PostMergeContext` and invokes `PostMergeHook::post_merge` after
/// finishing.
///
/// # Feature Requirements
///
/// The generated code references `ortho_config::declarative::*`, which is
/// feature-gated behind `serde_json` in the `ortho_config` crate. If a
/// downstream crate disables `serde_json`, compilation will fail with clear
/// "module not found" errors pointing to the missing declarative module.
///
/// # Examples
///
/// ```rust,ignore
/// use syn::parse_str;
/// use quote::quote;
/// use crate::derive::build::CollectionStrategies;
///
/// let krate = quote! { ortho_config };
/// let config_ident = parse_str("SampleConfig").unwrap();
/// let strategies = CollectionStrategies::default();
/// let tokens = generate_declarative_impl(&config_ident, &strategies, false, &krate);
/// assert!(tokens.to_string().contains("DeclarativeMerge"));
/// ```
pub(crate) fn generate_declarative_impl(
    config_ident: &syn::Ident,
    strategies: &CollectionStrategies,
    has_post_merge_hook: bool,
    krate: &TokenStream,
) -> TokenStream {
    let state_ident = format_ident!("__{}DeclarativeMergeState", config_ident);
    let state_struct =
        generate_declarative_state_struct(&state_ident, config_ident, strategies, krate);
    let merge_impl = generate_declarative_merge_impl(&state_ident, config_ident, strategies, krate);
    let merge_fn = generate_declarative_merge_from_layers_fn(
        &state_ident,
        config_ident,
        has_post_merge_hook,
        krate,
    );

    quote! {
        #state_struct
        #merge_impl
        #merge_fn
    }
}

#[cfg(test)]
mod tests;