native-theme-derive 0.6.0

Proc-macro derive for native-theme widget struct pairs
Documentation
// Generation of validate_widget() on the Resolved struct.

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Ident, Type};

use crate::parse::{BorderKind, FieldCategory, FieldMeta, LayerMeta};

/// Generate `validate_widget()` impl block on the Resolved struct.
pub(crate) fn gen_validate(
    opt_name: &Ident,
    fields: &[FieldMeta],
    layer: &LayerMeta,
) -> TokenStream {
    let resolved_name = layer
        .resolved_name
        .clone()
        .unwrap_or_else(|| format_ident!("Resolved{}", opt_name));

    let field_inits = gen_field_inits(fields, layer);

    quote! {
        #[allow(dead_code)]
        #[allow(clippy::ptr_arg)] // Generated code passes Vec to helper functions that push
        impl #resolved_name {
            /// Extract and validate fields from the Option-based source struct.
            /// Generated by `#[derive(ThemeWidget)]` -- field paths use `stringify!()`.
            pub(crate) fn validate_widget(
                source: &#opt_name,
                prefix: &str,
                _dpi: f32,
                missing: &mut Vec<String>,
            ) -> Self {
                Self {
                    #field_inits
                }
            }
        }
    }
}

/// Check if a resolved type's last path segment is "ResolvedWidgetBorder".
fn is_border_type(ty: &syn::Type) -> bool {
    if let syn::Type::Path(type_path) = ty
        && let Some(seg) = type_path.path.segments.last()
    {
        return seg.ident == "ResolvedWidgetBorder";
    }
    false
}

/// Generate field initialization expressions for validate_widget().
fn gen_field_inits(fields: &[FieldMeta], layer: &LayerMeta) -> TokenStream {
    let inits: Vec<TokenStream> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let field_name = ident.to_string();

            match &f.category {
                FieldCategory::Option => {
                    // Determine the fallback sentinel based on the inner type of
                    // `Option<T>`. Supported inner types: Rgba, f32, u16, bool,
                    // Arc<str>. Unknown types produce a compile error so the
                    // caller knows to add a mapping here.
                    let inner = extract_option_inner_ty(&f.ty);
                    let fallback = fallback_for_ty(&inner, &field_name);
                    quote! {
                        #ident: crate::resolve::validate_helpers::require(
                            &source.#ident,
                            &format!("{}.{}", prefix, #field_name),
                            missing,
                            #fallback,
                        ),
                    }
                }
                FieldCategory::SoftOption => {
                    quote! {
                        #ident: source.#ident,
                    }
                }
                FieldCategory::Nested { resolved_ty } => {
                    if is_border_type(resolved_ty) {
                        // Unified border validation: dispatch via BorderKind enum.
                        let kind_variant = match layer.border_kind {
                            BorderKind::Full => quote! { Full },
                            BorderKind::Partial => quote! { Partial },
                            BorderKind::None => quote! { None },
                        };
                        quote! {
                            #ident: crate::resolve::validate_helpers::validate_border(
                                &source.#ident,
                                &format!("{}.{}", prefix, #field_name),
                                crate::resolve::validate_helpers::BorderKind::#kind_variant,
                                missing,
                            ),
                        }
                    } else {
                        // Non-border nested field: use ValidateNested trait dispatch
                        let opt_ty = extract_option_inner_ty(&f.ty);
                        quote! {
                            #ident: <#opt_ty as crate::resolve::validate_helpers::ValidateNested>::validate_nested(
                                &source.#ident,
                                &format!("{}.{}", prefix, #field_name),
                                _dpi,
                                missing,
                            ),
                        }
                    }
                }
            }
        })
        .collect();

    quote! { #(#inits)* }
}

/// Map a field's inner type (`T` in `Option<T>`) to a fallback sentinel
/// expression for `validate_helpers::require`.
///
/// Supported inner types:
/// - `Rgba`  -> `crate::color::Rgba::TRANSPARENT`
/// - `f32`   -> `0.0f32`
/// - `u16`   -> `0u16`
/// - `bool`  -> `false`
/// - `Arc<str>` -> `std::sync::Arc::<str>::from("")`
/// - `DialogButtonOrder` -> `crate::model::DialogButtonOrder::PrimaryRight`
///   (matches the type's own `#[default]` variant)
///
/// Unknown types emit a `compile_error!` that names the offending field so
/// maintainers know to extend this mapping.
fn fallback_for_ty(ty: &Type, field_name: &str) -> TokenStream {
    // Case 1: `syn::Type::Path` – look at the last segment for the short name.
    if let Type::Path(type_path) = ty
        && let Some(seg) = type_path.path.segments.last()
    {
        let name = seg.ident.to_string();
        return match name.as_str() {
            "Rgba" => quote! { crate::color::Rgba::TRANSPARENT },
            "f32" => quote! { 0.0f32 },
            "u16" => quote! { 0u16 },
            "bool" => quote! { false },
            "Arc" => quote! { std::sync::Arc::<str>::from("") },
            "String" => quote! { String::new() },
            "DialogButtonOrder" => {
                quote! { crate::model::DialogButtonOrder::PrimaryRight }
            }
            other => {
                let msg = format!(
                    "ThemeWidget derive: field `{}` has unsupported Option inner type `{}` \
                     for the G1 fallback mapping. Add a branch to \
                     `native-theme-derive::gen_validate::fallback_for_ty` that maps this type \
                     to its zero-value sentinel.",
                    field_name, other
                );
                quote! { compile_error!(#msg) }
            }
        };
    }
    // Non-path types (tuples, arrays, refs, etc.) are not supported.
    let msg = format!(
        "ThemeWidget derive: field `{}` has a non-path Option inner type. \
         Only simple named types (Rgba, f32, u16, bool, Arc<str>) are supported \
         as G1 fallback sentinels.",
        field_name
    );
    quote! { compile_error!(#msg) }
}

/// Extract T from Option<T>. Returns the type as-is if not Option.
fn extract_option_inner_ty(ty: &syn::Type) -> syn::Type {
    if let syn::Type::Path(type_path) = ty
        && let Some(seg) = type_path.path.segments.last()
        && seg.ident == "Option"
        && let syn::PathArguments::AngleBracketed(args) = &seg.arguments
        && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
    {
        return inner.clone();
    }
    ty.clone()
}