borrowize 0.1.0

Derive borrowed view structs from owned Rust structs.
Documentation
//! Expansion for `#[derive(View)]`.

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
    Data, DeriveInput, Error, Fields, GenericParam, Generics, Ident, Lifetime, LifetimeParam,
    Result, Visibility, parse_quote, spanned::Spanned,
};

use crate::{
    attrs::{self, FieldOptions, StructOptions},
    mapping::{self, FallbackBorrowMode},
};

const GENERATED_BORROW_LIFETIME: &str = "borrowize";

pub(crate) fn expand(input: &DeriveInput) -> Result<TokenStream> {
    let struct_options = attrs::parse_struct_options(&input.attrs)?;
    let named_fields = named_fields(input)?;
    let borrow_lifetime = generated_borrow_lifetime(&input.generics)?;

    let input_ident = &input.ident;
    let view_ident = view_ident(input_ident, &struct_options);
    let view_visibility = struct_options
        .view_visibility
        .clone()
        .unwrap_or_else(|| input.vis.clone());
    let view_generics = view_generics(&input.generics, borrow_lifetime.clone());

    let mut view_fields = Vec::new();
    let mut generation_fields = Vec::new();

    for field in &named_fields.named {
        let Some(field_ident) = &field.ident else {
            return Err(Error::new(field.span(), "expected a named field"));
        };

        let field_options = attrs::parse_field_options(&field.attrs)?;
        let field_visibility = field_visibility(&field.vis, &struct_options, &field_options);
        let default_mapping = mapping::field_mapping(
            &field.ty,
            &borrow_lifetime,
            parse_quote!(self.#field_ident),
            FallbackBorrowMode::NeedsBorrow,
        );
        let borrowed_type = field_options
            .borrowed_type
            .clone()
            .unwrap_or(default_mapping.borrowed_type);
        let generation_expression = field_options
            .generation_expression
            .clone()
            .unwrap_or(default_mapping.generation_expression);

        view_fields.push(quote! {
            #field_visibility #field_ident: #borrowed_type
        });
        generation_fields.push(quote! {
            #field_ident: #generation_expression
        });
    }

    let method = if struct_options.no_method {
        None
    } else {
        Some(method_tokens(
            input,
            &struct_options,
            &view_ident,
            &generation_fields,
        ))
    };

    Ok(quote! {
        #view_visibility struct #view_ident #view_generics {
            #(#view_fields,)*
        }

        #method
    })
}

fn named_fields(input: &DeriveInput) -> Result<&syn::FieldsNamed> {
    match &input.data {
        Data::Struct(data_struct) => match &data_struct.fields {
            Fields::Named(named_fields) => Ok(named_fields),
            Fields::Unnamed(fields) => Err(Error::new(
                fields.span(),
                "`View` can only be derived for structs with named fields",
            )),
            Fields::Unit => Err(Error::new(
                input.ident.span(),
                "`View` cannot be derived for unit structs",
            )),
        },
        Data::Enum(data_enum) => Err(Error::new(
            data_enum.enum_token.span,
            "`View` cannot be derived for enums",
        )),
        Data::Union(data_union) => Err(Error::new(
            data_union.union_token.span,
            "`View` cannot be derived for unions",
        )),
    }
}

fn view_ident(input_ident: &Ident, options: &StructOptions) -> Ident {
    options
        .view_name
        .clone()
        .unwrap_or_else(|| format_ident!("{input_ident}View"))
}

fn field_visibility(
    source_visibility: &Visibility,
    struct_options: &StructOptions,
    field_options: &FieldOptions,
) -> Visibility {
    field_options
        .visibility
        .clone()
        .or_else(|| struct_options.field_visibility.clone())
        .unwrap_or_else(|| source_visibility.clone())
}

fn method_tokens(
    input: &DeriveInput,
    options: &StructOptions,
    view_ident: &Ident,
    generation_fields: &[TokenStream],
) -> TokenStream {
    let input_ident = &input.ident;
    let method_visibility = options
        .method_visibility
        .clone()
        .unwrap_or_else(|| input.vis.clone());
    let method_ident = options
        .method_name
        .clone()
        .unwrap_or_else(|| Ident::new("view", input_ident.span()));
    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
    let view_type_arguments = view_type_arguments(&input.generics);

    quote! {
        impl #impl_generics #input_ident #type_generics #where_clause {
            #method_visibility fn #method_ident(&self) -> #view_ident<#(#view_type_arguments),*> {
                #view_ident {
                    #(#generation_fields,)*
                }
            }
        }
    }
}

fn view_generics(source_generics: &Generics, borrow_lifetime: Lifetime) -> Generics {
    let mut view_generics = source_generics.clone();
    view_generics.params.insert(
        0,
        GenericParam::Lifetime(LifetimeParam::new(borrow_lifetime)),
    );
    view_generics
}

fn view_type_arguments(source_generics: &Generics) -> Vec<TokenStream> {
    let mut arguments = vec![quote!('_)];

    arguments.extend(
        source_generics
            .params
            .iter()
            .map(|parameter| match parameter {
                GenericParam::Lifetime(lifetime) => {
                    let lifetime = &lifetime.lifetime;
                    quote!(#lifetime)
                }
                GenericParam::Type(type_parameter) => {
                    let ident = &type_parameter.ident;
                    quote!(#ident)
                }
                GenericParam::Const(const_parameter) => {
                    let ident = &const_parameter.ident;
                    quote!(#ident)
                }
            }),
    );

    arguments
}

/// Return the stable generated view lifetime, rejecting user lifetime collisions.
///
/// The generated view type always uses `'borrowize`; `borrowed_type`
/// overrides can refer to that same lifetime directly. A source struct that
/// already defines `'borrowize` would make that contract ambiguous, so the
/// derive fails instead of choosing a fresh name.
fn generated_borrow_lifetime(generics: &Generics) -> Result<Lifetime> {
    if let Some(existing_lifetime) = generics
        .lifetimes()
        .find(|lifetime| lifetime.lifetime.ident == GENERATED_BORROW_LIFETIME)
    {
        return Err(Error::new(
            existing_lifetime.lifetime.span(),
            "`View` reserves the lifetime name `'borrowize` for the generated view borrow",
        ));
    }

    Ok(Lifetime::new("'borrowize", proc_macro2::Span::call_site()))
}