azure_data_cosmos_macros 0.2.0

Procedural macros for the Azure Cosmos DB SDK for Rust.
Documentation
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#![doc = include_str!("../README.md")]
#![warn(missing_docs)]

//! Procedural macros for the Azure Cosmos DB SDK hierarchical configuration model.

mod builder;
mod env;
mod parse;
mod view;

use parse::OptionsInput;
use syn::{parse_macro_input, DeriveInput};

type Result<T> = ::std::result::Result<T, syn::Error>;

/// Derives layered configuration boilerplate for Cosmos DB option group structs.
///
/// Generates a View struct, a Builder type, a `Default` implementation, and,
/// if any fields have `#[option(env = "...")]`, `from_env()`/`from_env_vars()`
/// constructors for loading from environment variables.
///
/// # Struct-Level Attributes
///
/// - `#[options(layers(runtime, account, operation))]` — declares which layers
///   this option group participates in.
/// - `#[options(env_only)]` — generates only the `from_env()`/`from_env_vars()`
///   constructors (no View, Builder, or `Default`), so an existing type — such
///   as a hand-written builder — can double as its own environment-variable
///   source. Mutually exclusive with `layers(...)`; requires at least one
///   `#[option(env = "...")]` field.
///
/// # Field-Level Attributes
///
/// - `#[option(env = "AZURE_COSMOS_...")]` — enables environment variable loading.
/// - `#[option(env = "AZURE_COSMOS_...", overridable)]` — additionally recognizes a
///   `{ENV}_OVERRIDE` kill-switch variable that takes precedence over **every**
///   layer (including operation). Generates `from_env_override()` and a
///   top-priority `env_override` layer on the View (constructed via
///   `new_with_override`). Requires `env`.
/// - `#[option(merge = "extend")]` — uses additive merge instead of shadow semantics.
/// - `#[option(nested)]` — delegates resolution to a child View.
/// - `#[option(env = "...", parser = path::to::fn)]` — parses the env var with a
///   custom `fn(&str) -> Option<T>` (where `T` is the field's inner type)
///   instead of `FromStr`, supporting types like `Duration` read from a
///   millisecond count. A `None` result is logged and ignored. Requires `env`.
///
/// # Example
///
/// ```ignore
/// #[derive(CosmosOptions)]
/// #[options(layers(runtime, account, operation))]
/// pub struct RequestOptions {
///     #[option(env = "AZURE_COSMOS_CONSISTENCY_LEVEL")]
///     pub consistency_level: Option<ConsistencyLevel>,
///
///     pub throughput_bucket: Option<usize>,
///
///     #[option(merge = "extend")]
///     pub custom_headers: Option<HashMap<HeaderName, HeaderValue>>,
/// }
/// ```
#[proc_macro_derive(CosmosOptions, attributes(options, option))]
pub fn derive_cosmos_options(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    match derive_cosmos_options_impl(ast) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn derive_cosmos_options_impl(ast: DeriveInput) -> Result<proc_macro2::TokenStream> {
    let input = OptionsInput::from_derive_input(&ast)?;

    // Env-only mode: the struct is purely an environment-variable source (e.g.
    // an existing builder type reused to read env vars into). Emit only the
    // `from_env`/`from_env_vars` constructors; skip the View, Builder, and
    // `Default` generation so the macro can decorate a hand-written type
    // without colliding with its existing `#[derive(Default)]` or builder
    // methods.
    if input.env_only {
        let env_tokens = env::generate_from_env(&input)?;
        return Ok(quote::quote! {
            #[doc(hidden)]
            const _: () = {
                #env_tokens
            };
        });
    }

    let view_tokens = view::generate_view(&input)?;
    let builder_tokens = builder::generate_builder(&input)?;
    let env_tokens = env::generate_from_env(&input)?;
    let default_tokens = generate_default(&input)?;

    // View and Builder structs must be at module scope so they're visible to callers.
    // Trait impls go in a const block for scope isolation.
    let gen = quote::quote! {
        #view_tokens
        #builder_tokens

        #[doc(hidden)]
        const _: () = {
            #env_tokens
            #default_tokens
        };
    };

    Ok(gen)
}

fn generate_default(input: &OptionsInput) -> Result<proc_macro2::TokenStream> {
    let name = &input.name;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
    let fields = input.fields.iter().map(|f| {
        let field_name = &f.ident;
        quote::quote! { #field_name: None }
    });

    Ok(quote::quote! {
        #[automatically_derived]
        impl #impl_generics Default for #name #ty_generics #where_clause {
            fn default() -> Self {
                Self {
                    #(#fields),*
                }
            }
        }
    })
}