bomboni_request_derive 0.3.0

Internal request derive macros for Bomboni library.
Documentation
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use std::collections::BTreeSet;

use crate::parse::{
    field_type_info::FieldTypeInfo,
    options::{FieldExtract, FieldExtractStep, ParseFieldOptions},
};

use super::options::ParseConvert;

pub fn expand_field_extract(
    extract: &FieldExtract,
    field_clone_set: &BTreeSet<String>,
    field_type_info: Option<&FieldTypeInfo>,
    field_path_wrapper: Option<&TokenStream>,
    borrow: bool,
) -> (TokenStream, TokenStream, String) {
    let mut extract_impl = quote!();
    let mut get_impl = quote!();
    let mut field_path = String::new();

    let mut source_steps = extract.steps.iter().enumerate().peekable();
    if let Some((_, FieldExtractStep::Field(field_name))) = source_steps.peek() {
        source_steps.next();

        field_path.clone_from(field_name);
        let source_ident = if borrow {
            quote! { &source }
        } else {
            quote! { source }
        };
        let field_ident = format_ident!("{field_name}");

        // Check if this field will be unwrapped later (has Unwrap step)
        let will_unwrap = extract
            .steps
            .iter()
            .any(|step| matches!(step, FieldExtractStep::Unwrap));

        if field_clone_set.contains(&field_path) || will_unwrap {
            extract_impl.extend(quote! {
                // TODO: Avoid cloning in some cases
                let target = #source_ident.#field_ident.clone();
            });
        } else {
            extract_impl.extend(quote! {
                let target = #source_ident.#field_ident;
            });
        }
    } else {
        extract_impl.extend(if borrow {
            quote! {
                let target = &source;
            }
        } else {
            quote! {
                let target = source;
            }
        });
    }

    let last_unwrap_step = extract
        .steps
        .iter()
        .rposition(|step| matches!(step, FieldExtractStep::Unwrap));
    let mut target_option = if field_type_info
        .and_then(|field_type_info| field_type_info.container_ident.as_deref())
        == Some("Option")
    {
        Some(())
    } else {
        None
    };

    for (i, step) in source_steps {
        let block_impl = if let Some(last_unwrap_step) = last_unwrap_step {
            if last_unwrap_step < i {
                &mut get_impl
            } else {
                &mut extract_impl
            }
        } else {
            &mut extract_impl
        };

        match step {
            FieldExtractStep::Field(field_name) => {
                field_path = if field_path.is_empty() {
                    field_name.clone()
                } else {
                    format!("{field_path}.{field_name}")
                };
                let field_ident = format_ident!("{field_name}");

                if field_clone_set.contains(&field_path) {
                    block_impl.extend(quote! {
                        // TODO: Avoid cloning in some cases
                        let target = target.#field_ident.clone();
                    });
                } else {
                    block_impl.extend(quote! {
                        let target = target.#field_ident;
                    });
                }
            }
            FieldExtractStep::Unwrap => {
                if !(last_unwrap_step == Some(i) && target_option.take().is_some()) {
                    let error_path = make_field_error_path(&field_path, field_path_wrapper);
                    block_impl.extend(quote! {
                        let target = target.ok_or_else(|| {
                            RequestError::path(
                                #error_path,
                                CommonError::RequiredFieldMissing,
                            )
                        })?;
                    });
                }
            }
            FieldExtractStep::UnwrapOr(expr) => {
                block_impl.extend(quote! {
                    let target = target.unwrap_or_else(|| #expr);
                });
            }
            FieldExtractStep::UnwrapOrDefault => {
                block_impl.extend(quote! {
                    let target = target.unwrap_or_default();
                });
            }
            FieldExtractStep::Unbox => {
                block_impl.extend(quote! {
                    let target = *target;
                });
            }
            FieldExtractStep::StringFilterEmpty => {
                block_impl.extend(quote! {
                    let target = if target.is_empty() {
                        None
                    } else {
                        Some(target)
                    };
                });
            }
            FieldExtractStep::EnumerationFilterUnspecified => {
                block_impl.extend(quote! {
                    let target = if target == 0 {
                        None
                    } else {
                        Some(target)
                    };
                });
            }
        }
    }

    (extract_impl, get_impl, field_path)
}

pub fn make_field_error_path(field_path: &str, before: Option<&TokenStream>) -> TokenStream {
    let mut error_path = before.map_or_else(
        || {
            assert!(
                !field_path.is_empty(),
                "expected extract plan to begin with a field path"
            );
            quote!()
        },
        |before| quote!(#before,),
    );
    for part in field_path.split('.').filter(|part| !part.is_empty()) {
        error_path.extend(quote! {
            PathErrorStep::Field(#part.into()),
        });
    }
    quote!([#error_path])
}

pub fn parse_field_source_extract(source: &str) -> Option<FieldExtract> {
    let mut steps = Vec::new();

    for part in source.split('.') {
        if let Some(stripped) = part.strip_suffix('?') {
            steps.push(FieldExtractStep::Field(stripped.to_string()));
            steps.push(FieldExtractStep::Unwrap);
        } else {
            steps.push(FieldExtractStep::Field(part.to_string()));
        }
    }

    if steps.is_empty() {
        None
    } else {
        Some(FieldExtract { steps })
    }
}

pub fn expand_parse_field_type(
    field_options: &ParseFieldOptions,
    field_type_info: &FieldTypeInfo,
    field_error_path: TokenStream,
    get_impl: TokenStream,
) -> TokenStream {
    let mut parse_impl = quote!();

    if let Some(regex) = field_options.regex.as_ref() {
        parse_impl.extend(quote! {
            static REGEX: ::std::sync::OnceLock<::regex::Regex> = ::std::sync::OnceLock::new();
            let re = REGEX.get_or_init(|| ::regex::Regex::new(#regex).unwrap());
        });
    }

    let inner_wrap_err = match field_type_info.container_ident.as_deref() {
        Some("Vec") => {
            quote!(.insert_path([PathErrorStep::Index(i)], 1))
        }
        Some("HashMap" | "BTreeMap") => {
            quote!(.insert_path([PathErrorStep::Key(key.to_string())], 1))
        }
        _ => quote!(),
    };

    if let Some(try_from) = field_options.try_from.as_ref() {
        parse_impl.extend(quote! {
            let target = TryFrom::<#try_from>::try_from(target)
                .map_err(|_| RequestError::path(#field_error_path, CommonError::FailedConvertValue) #inner_wrap_err)?;
        });
    } else if let Some(ParseConvert { parse, module, .. }) = field_options.convert.as_ref() {
        let convert_impl = parse
            .as_ref()
            .map(ToTokens::to_token_stream)
            .or_else(|| module.as_ref().map(|module| quote!(#module::parse)))
            .unwrap();
        parse_impl.extend(quote! {
            let target = #convert_impl(target)
                .map_err(|err: RequestError| err.wrap_path(#field_error_path) #inner_wrap_err)?;
        });
    } else if field_options.enumeration {
        if !field_options.unspecified {
            parse_impl = quote! {
                if target == 0 {
                    return Err(RequestError::path(
                        #field_error_path,
                        CommonError::RequiredFieldMissing,
                    ) #inner_wrap_err);
                }
            };
        }
        if !field_options.keep_primitive {
            parse_impl.extend(quote! {
                let target = target.try_into()
                    .map_err(|_| RequestError::path(#field_error_path, CommonError::InvalidEnumValue) #inner_wrap_err)?;
            });
        }
    } else if field_options.timestamp {
        parse_impl.extend(quote! {
            let target = target.try_into()
                .map_err(|_| RequestError::path(#field_error_path, CommonError::InvalidDateTime) #inner_wrap_err)?;
        });
    } else if let Some(primitive_ident) = field_type_info.primitive_ident.as_ref() {
        if field_options.wrapper {
            // TODO: Verify that these `.value`s don't require `.unwrap_or_default()`
            if matches!(
                primitive_ident.as_str(),
                "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize" | "usize"
            ) {
                let target_type_ident = format_ident!("{}", primitive_ident);
                parse_impl = quote! {
                    let target = target.value as #target_type_ident;
                };
            } else {
                parse_impl = quote! {
                    let target = target.value;
                };
            }
        }

        if primitive_ident == "String" {
            // Only validate non-empty string if the field is not wrapped in Option
            // and is not marked as unspecified
            if !field_options.unspecified && field_type_info.container_ident.is_none() {
                parse_impl.extend(quote! {
                    if target.is_empty() {
                        return Err(RequestError::path(
                            #field_error_path,
                            CommonError::RequiredFieldMissing,
                        ) #inner_wrap_err);
                    }
                });
            }
            if let Some(regex) = field_options.regex.as_ref() {
                parse_impl.extend(quote! {
                    if !re.is_match(&target) {
                        return Err(RequestError::path(
                            #field_error_path,
                            CommonError::InvalidStringFormat {
                                expected: #regex.into(),
                            },
                        ) #inner_wrap_err);
                    }
                });
            }
        } else if field_type_info.primitive_message && !field_options.keep_primitive {
            parse_impl.extend(quote! {
                let target = target.parse_into()
                    .map_err(|err: RequestError| err.wrap_path(#field_error_path) #inner_wrap_err)?;
            });
        }
    }

    if field_type_info.generic_param.is_some() {
        parse_impl.extend(quote! {
            let target = target.parse_into()
                .map_err(|err: RequestError| err.wrap_path(#field_error_path) #inner_wrap_err)?;
        });
    }

    if let Some(container_ident) = field_type_info.container_ident.as_ref() {
        match container_ident.as_str() {
            "Option" => {
                return quote! {
                    let target = if let Some(target) = target {
                        #get_impl
                        #parse_impl
                        Some(target)
                    } else {
                        None
                    };
                };
            }
            "Box" => {
                return quote! {
                    let target = {
                        #get_impl
                        #parse_impl
                        Box::new(target)
                    };
                };
            }
            "Vec" => {
                return quote! {
                    #get_impl
                    let mut v = Vec::new();
                    for (i, target) in target.into_iter().enumerate() {
                        v.push({
                            #parse_impl
                            target
                        });
                    }
                    let target = v;
                };
            }
            "HashMap" | "BTreeMap" => {
                let container_ident = if container_ident == "HashMap" {
                    quote! { HashMap }
                } else {
                    quote! { BTreeMap }
                };
                let parse_kv_impl = if parse_impl.is_empty() {
                    quote!()
                } else {
                    quote! {
                        let target = {
                            #parse_impl
                            target
                        };
                    }
                };
                return if parse_kv_impl.is_empty() {
                    quote! {
                        #get_impl
                    }
                } else {
                    quote! {
                        #get_impl
                        let mut m = #container_ident::new();
                        for (key, target) in target.into_iter() {
                            #parse_kv_impl
                            m.insert(key, target);
                        }
                        let target = m;
                    }
                };
            }
            _ => unreachable!(),
        }
    }

    quote! {
        #get_impl
        #parse_impl
    }
}