borrowize 0.2.0

Derive borrowed view structs from owned Rust structs.
Documentation
//! Type and expression mapping for borrowed view fields.

use nom::{
    Err as NomErr, IResult, Input, Parser, branch::alt, combinator::all_consuming,
    combinator::value, error::Error as NomError, error::ErrorKind,
};
use syn::{Expr, GenericArgument, Lifetime, Path, PathArguments, PathSegment, Type, parse_quote};

use crate::path_segment_input::PathSegmentInput;

/// Whether the fallback expression for an unknown type still needs a borrow.
#[derive(Clone, Copy)]
pub(crate) enum FallbackBorrowMode {
    /// The expression still needs `&expr`, as with direct source field access.
    NeedsBorrow,
    /// The expression is already an immutable borrowed reference.
    AlreadyBorrowed,
    /// The expression is a binding from matching on `&self`.
    MatchedBorrow,
}

pub(crate) struct FieldMapping {
    pub(crate) borrowed_type: Type,
    pub(crate) generation_expression: Expr,
}

pub(crate) fn field_mapping(
    source_type: &Type,
    borrow_lifetime: &Lifetime,
    source_expression: Expr,
    mode: FallbackBorrowMode,
) -> FieldMapping {
    let type_shape = TypeShape::classify(source_type);

    FieldMapping {
        borrowed_type: type_shape.borrowed_type(borrow_lifetime),
        generation_expression: type_shape.generation_expression(source_expression, mode),
    }
}

#[derive(Clone, Copy)]
enum TypeShape<'a> {
    Option(&'a Type),
    String,
    Vec(&'a Type),
    Array(&'a Type),
    Box(&'a Type),
    PathBuf,
    ImmutableReference(&'a Type),
    Fallback(&'a Type),
}

impl<'a> TypeShape<'a> {
    fn classify(source_type: &'a Type) -> Self {
        match source_type {
            Type::Array(array) => Self::Array(array.elem.as_ref()),
            Type::Reference(reference) if reference.mutability.is_none() => {
                Self::ImmutableReference(source_type)
            }
            Type::Path(type_path) if type_path.qself.is_none() => {
                let Some(path_kind) = path_kind(&type_path.path) else {
                    return Self::Fallback(source_type);
                };

                match path_kind {
                    PathKind::Option => single_type_argument(&type_path.path)
                        .map_or(Self::Fallback(source_type), Self::Option),
                    PathKind::String => Self::String,
                    PathKind::Vec => single_type_argument(&type_path.path)
                        .map_or(Self::Fallback(source_type), Self::Vec),
                    PathKind::Box => single_type_argument(&type_path.path)
                        .map_or(Self::Fallback(source_type), Self::Box),
                    PathKind::PathBuf => Self::PathBuf,
                }
            }
            _ => Self::Fallback(source_type),
        }
    }

    fn borrowed_type(self, borrow_lifetime: &Lifetime) -> Type {
        match self {
            Self::Option(inner_type) => {
                let inner_borrowed_type = Self::classify(inner_type).borrowed_type(borrow_lifetime);
                parse_quote!(::core::option::Option<#inner_borrowed_type>)
            }
            Self::String => parse_quote!(&#borrow_lifetime str),
            Self::Vec(inner_type) | Self::Array(inner_type) => {
                parse_quote!(&#borrow_lifetime [#inner_type])
            }
            Self::Box(inner_type) => parse_quote!(&#borrow_lifetime #inner_type),
            Self::PathBuf => parse_quote!(&#borrow_lifetime ::std::path::Path),
            Self::ImmutableReference(source_type) => source_type.clone(),
            Self::Fallback(source_type) => parse_quote!(&#borrow_lifetime #source_type),
        }
    }

    fn generation_expression(self, source_expression: Expr, mode: FallbackBorrowMode) -> Expr {
        match self {
            Self::Option(inner_type) => {
                let inner_mode = match mode {
                    FallbackBorrowMode::MatchedBorrow => FallbackBorrowMode::MatchedBorrow,
                    FallbackBorrowMode::NeedsBorrow | FallbackBorrowMode::AlreadyBorrowed => {
                        FallbackBorrowMode::AlreadyBorrowed
                    }
                };
                let inner_expression = Self::classify(inner_type)
                    .generation_expression(parse_quote!(value), inner_mode);
                parse_quote!(#source_expression.as_ref().map(|value| #inner_expression))
            }
            Self::String => parse_quote!(#source_expression.as_str()),
            Self::Vec(_) => parse_quote!(#source_expression.as_slice()),
            Self::Array(_) => parse_quote!(&#source_expression[..]),
            Self::Box(_) => parse_quote!(#source_expression.as_ref()),
            Self::PathBuf => parse_quote!(#source_expression.as_path()),
            Self::ImmutableReference(_) => match mode {
                FallbackBorrowMode::MatchedBorrow => parse_quote!(*#source_expression),
                FallbackBorrowMode::NeedsBorrow | FallbackBorrowMode::AlreadyBorrowed => {
                    source_expression
                }
            },
            Self::Fallback(_) => match mode {
                FallbackBorrowMode::NeedsBorrow => parse_quote!(&#source_expression),
                FallbackBorrowMode::AlreadyBorrowed | FallbackBorrowMode::MatchedBorrow => {
                    source_expression
                }
            },
        }
    }
}

fn single_type_argument(path: &Path) -> Option<&Type> {
    let last_segment = path.segments.last()?;
    let PathArguments::AngleBracketed(arguments) = &last_segment.arguments else {
        return None;
    };

    let mut type_arguments = arguments.args.iter().filter_map(|argument| match argument {
        GenericArgument::Type(type_argument) => Some(type_argument),
        _ => None,
    });
    let type_argument = type_arguments.next()?;

    if type_arguments.next().is_some() {
        return None;
    }

    Some(type_argument)
}

#[derive(Clone, Copy)]
enum PathKind {
    Option,
    String,
    Vec,
    Box,
    PathBuf,
}

fn path_kind(path: &Path) -> Option<PathKind> {
    parse_path_kind(PathSegmentInput::new(path))
        .ok()
        .map(|(_remaining, path_kind)| path_kind)
}

type SegmentParseResult<'a, Output> = IResult<PathSegmentInput<'a>, Output>;

/// Parse recognised owned container/value paths from already-tokenised path
/// segments. The bare terminal path is accepted by the first branch for each
/// kind; later branches cover the qualified standard-library spellings.
fn parse_path_kind(input: PathSegmentInput<'_>) -> SegmentParseResult<'_, PathKind> {
    all_consuming(alt((
        value(PathKind::Option, segment("Option")),
        value(
            PathKind::Option,
            (segment("std"), segment("option"), segment("Option")),
        ),
        value(
            PathKind::Option,
            (segment("core"), segment("option"), segment("Option")),
        ),
        value(PathKind::String, segment("String")),
        value(
            PathKind::String,
            (segment("std"), segment("string"), segment("String")),
        ),
        value(
            PathKind::String,
            (segment("alloc"), segment("string"), segment("String")),
        ),
        value(PathKind::Vec, segment("Vec")),
        value(
            PathKind::Vec,
            (segment("std"), segment("vec"), segment("Vec")),
        ),
        value(
            PathKind::Vec,
            (segment("alloc"), segment("vec"), segment("Vec")),
        ),
        value(PathKind::Box, segment("Box")),
        value(
            PathKind::Box,
            (segment("std"), segment("boxed"), segment("Box")),
        ),
        value(
            PathKind::Box,
            (segment("alloc"), segment("boxed"), segment("Box")),
        ),
        value(PathKind::PathBuf, segment("PathBuf")),
        value(
            PathKind::PathBuf,
            (segment("std"), segment("path"), segment("PathBuf")),
        ),
    )))
    .parse(input)
}

fn segment<'a>(
    expected_segment: &'static str,
) -> impl Parser<PathSegmentInput<'a>, Output = &'a PathSegment, Error = NomError<PathSegmentInput<'a>>>
{
    move |input: PathSegmentInput<'a>| {
        let Some(segment) = input.iter_elements().next() else {
            return Err(NomErr::Error(NomError::new(input, ErrorKind::Tag)));
        };

        if segment.ident == expected_segment {
            Ok((input.take_from(1), segment))
        } else {
            Err(NomErr::Error(NomError::new(input, ErrorKind::Tag)))
        }
    }
}