prosto_derive 0.11.26

Procedural macros for proto_rs, Rust as a first-class citizen in the protobuf ecosystem
Documentation
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::Data;
use syn::DeriveInput;
use syn::Error;
use syn::Fields;
use syn::ItemEnum;
use syn::ItemStruct;

use crate::emit_proto::generate_complex_enum_proto;
use crate::emit_proto::generate_simple_enum_proto;
use crate::emit_proto::generate_struct_proto;
use crate::parse::UnifiedProtoConfig;
use crate::schema::SchemaTokens;
use crate::schema::assoc_proto_ident_const;
use crate::schema::schema_tokens_for_complex_enum;
use crate::schema::schema_tokens_for_simple_enum;

pub(crate) fn build_validate_with_ext_impl(config: &UnifiedProtoConfig) -> TokenStream2 {
    let validate_with_ext_tokens: proc_macro2::TokenStream;
    #[cfg(feature = "tonic")]
    {
        validate_with_ext_tokens = {
            let Some(validator_fn) = &config.validator_with_ext else {
                // no validator => generate nothing (or generate const false, your choice)
                return quote! {};
            };

            let validator_path: syn::Path = syn::parse_str(validator_fn).expect("invalid validator_with_ext function path");

            quote! {
                const VALIDATE_WITH_EXT: bool = true;

                #[inline]
                fn validate_with_ext(
                    value: &mut Self,
                    ext: &::tonic::Extensions,
                ) -> Result<(), ::proto_rs::DecodeError> {
                    #validator_path(value, ext)
                }
            }
        };
    }

    #[cfg(not(feature = "tonic"))]
    {
        validate_with_ext_tokens = quote! {
            const VALIDATE_WITH_EXT: bool = false;
        };
    }
    validate_with_ext_tokens
}

fn build_validator_const(type_tokens: TokenStream2) -> TokenStream2 {
    quote! {
        #[cfg(feature = "build-schemas")]
        const _: () = <#type_tokens as ::proto_rs::schemas::ProtoIdentifiable>::_VALIDATOR;
    }
}

fn concrete_type_tokens(
    type_ident: &syn::Ident,
    generics: &syn::Generics,
    substitutions: &std::collections::BTreeMap<String, syn::Type>,
) -> Option<TokenStream2> {
    if generics.lifetimes().next().is_some() || generics.const_params().next().is_some() {
        return None;
    }
    let type_params: Vec<_> = generics.type_params().collect();
    if type_params.is_empty() {
        return Some(quote! { #type_ident });
    }
    let mut args = Vec::with_capacity(type_params.len());
    for param in type_params {
        let ty = substitutions.get(&param.ident.to_string())?;
        args.push(ty);
    }
    Some(quote! { #type_ident <#(#args),*> })
}

mod complex_enums;
mod enums;
mod generic_bounds;
mod structs;
mod unified_field_handler;

use complex_enums::generate_complex_enum_impl;
use enums::generate_simple_enum_impl;
use structs::generate_struct_impl;

pub fn proto_message_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
    let item_ts: TokenStream2 = item.clone().into();
    let input: DeriveInput = syn::parse2(item_ts.clone()).expect("proto_message expects a type definition");

    let type_ident = input.ident.to_string();
    let mut config = UnifiedProtoConfig::from_attributes(attr, &type_ident, &input.attrs, &input.data, input.generics.clone());
    let proto_names = config.proto_message_names(&type_ident);
    let generic_params: Vec<syn::Ident> = input.generics.type_params().map(|param| param.ident.clone()).collect();
    if config.transparent && config.proto_path().is_some() {
        return Error::new_spanned(&input.ident, "transparent proto_message types must not be written to .proto files")
            .to_compile_error()
            .into();
    }

    // Get generic type variants (concrete type combinations)
    let generic_variants = match config.generic_type_variants(&input.generics) {
        Ok(variants) => variants,
        Err(err) => return err.to_compile_error().into(),
    };

    let tokens = match input.data {
        Data::Struct(ref data) => {
            let mut schema_tokens_col = quote! {};
            let mut inventory_tokens_col = quote! {};
            let mut validator_tokens_col = quote! {};

            // Iterate over each proto name (from suns configuration)
            for proto_name in &proto_names {
                // For each proto name, iterate over generic variants
                for variant in &generic_variants {
                    let message_name = if variant.suffix.is_empty() {
                        proto_name.clone()
                    } else {
                        format!("{}{}", proto_name, variant.suffix)
                    };

                    // Apply generic substitutions to fields
                    let fields = if variant.substitutions.is_empty() {
                        data.fields.clone()
                    } else {
                        crate::generic_substitutions::apply_generic_substitutions_fields(&data.fields, &variant.substitutions)
                    };

                    let proto = generate_struct_proto(&message_name, &fields, &generic_params);
                    // Use _concrete version if we have substitutions
                    let schema_tokens = if variant.substitutions.is_empty() {
                        crate::schema::schema_tokens_for_struct(&input.ident, &message_name, &fields, &config, &message_name)
                    } else {
                        crate::schema::schema_tokens_for_struct_concrete(&input.ident, &message_name, &fields, &config, &message_name)
                    };
                    // Only emit proto file entry for concrete variants (not base generic type)
                    // Base generic type (empty substitutions) is only for Rust client schema
                    let has_type_params = !input.generics.type_params().collect::<Vec<_>>().is_empty();
                    if !has_type_params || !variant.substitutions.is_empty() {
                        config.register_and_emit_proto(&proto);
                    }
                    let SchemaTokens { schema, inventory_submit } = schema_tokens;
                    schema_tokens_col = quote! { #schema #schema_tokens_col};
                    inventory_tokens_col = quote! { #inventory_submit #inventory_tokens_col};
                    if (!has_type_params || !variant.substitutions.is_empty())
                        && let Some(type_tokens) = concrete_type_tokens(&input.ident, &input.generics, &variant.substitutions)
                    {
                        let validator_const = build_validator_const(type_tokens);
                        validator_tokens_col = quote! { #validator_tokens_col #validator_const };
                    }
                }
            }

            let item_struct: ItemStruct = syn::parse2(item_ts).expect("failed to parse struct");
            let type_tokens = generate_struct_impl(&input, &item_struct, data, &config);
            quote! {#type_tokens #schema_tokens_col #inventory_tokens_col #validator_tokens_col}
        }
        Data::Enum(ref data) => {
            let is_simple_enum = data.variants.iter().all(|variant| matches!(variant.fields, Fields::Unit));
            let mut schema_tokens_col = quote! {};
            let mut inventory_tokens_col = quote! {};
            let mut validator_tokens_col = quote! {};

            // Iterate over each proto name (from suns configuration)
            for proto_name in &proto_names {
                // For each proto name, iterate over generic variants
                for variant in &generic_variants {
                    let message_name = if variant.suffix.is_empty() {
                        proto_name.clone()
                    } else {
                        format!("{}{}", proto_name, variant.suffix)
                    };

                    // Apply generic substitutions to enum data
                    let enum_data = if variant.substitutions.is_empty() {
                        data.clone()
                    } else {
                        crate::generic_substitutions::apply_generic_substitutions_enum(data, &variant.substitutions)
                    };

                    let proto = if is_simple_enum {
                        generate_simple_enum_proto(&message_name, &enum_data)
                    } else {
                        generate_complex_enum_proto(&message_name, &enum_data, &generic_params)
                    };
                    // Use _concrete version if we have substitutions
                    let schema_tokens = if variant.substitutions.is_empty() {
                        if is_simple_enum {
                            schema_tokens_for_simple_enum(&input.ident, &message_name, &enum_data, &config, &message_name)
                        } else {
                            schema_tokens_for_complex_enum(&input.ident, &message_name, &enum_data, &config, &message_name)
                        }
                    } else {
                        if is_simple_enum {
                            crate::schema::schema_tokens_for_simple_enum_concrete(
                                &input.ident,
                                &message_name,
                                &enum_data,
                                &config,
                                &message_name,
                            )
                        } else {
                            crate::schema::schema_tokens_for_complex_enum_concrete(
                                &input.ident,
                                &message_name,
                                &enum_data,
                                &config,
                                &message_name,
                            )
                        }
                    };
                    // Only emit proto file entry for concrete variants (not base generic type)
                    // Base generic type (empty substitutions) is only for Rust client schema
                    let has_type_params = !input.generics.type_params().collect::<Vec<_>>().is_empty();
                    if !has_type_params || !variant.substitutions.is_empty() {
                        config.register_and_emit_proto(&proto);
                    }
                    let SchemaTokens { schema, inventory_submit } = schema_tokens;
                    schema_tokens_col = quote! { #schema #schema_tokens_col};
                    inventory_tokens_col = quote! { #inventory_submit #inventory_tokens_col};
                    if (!has_type_params || !variant.substitutions.is_empty())
                        && let Some(type_tokens) = concrete_type_tokens(&input.ident, &input.generics, &variant.substitutions)
                    {
                        let validator_const = build_validator_const(type_tokens);
                        validator_tokens_col = quote! { #validator_tokens_col #validator_const };
                    }
                }
            }

            let item_enum: ItemEnum = syn::parse2(item_ts).expect("failed to parse enum");
            let type_tokens = if is_simple_enum {
                generate_simple_enum_impl(&input, &item_enum, data, &config)
            } else {
                match generate_complex_enum_impl(&input, &item_enum, data, &config) {
                    Ok(tokens) => tokens,
                    Err(err) => return err.to_compile_error().into(),
                }
            };
            quote! {#type_tokens #schema_tokens_col #inventory_tokens_col #validator_tokens_col}
        }
        Data::Union(_) => Error::new_spanned(&input.ident, "proto_message cannot be used on unions").to_compile_error(),
    };

    // Generate ProtoIdentifiable using the base type name (for rust client generation)
    // Concrete variant schemas are registered separately with inventory for proto file generation
    let proto_ident_const = assoc_proto_ident_const(&config, &input.ident, &input.generics, &proto_names);

    let proto_imports = config.imports_mat;
    quote! {
        #proto_imports
        #tokens
        #proto_ident_const
    }
    .into()
}