borrowize 0.3.0

Derive borrowed view structs from owned Rust structs.
Documentation
//! Parsing for `#[borrowize(...)]` derive options.

use syn::{
    Attribute, Error, Expr, Ident, LitStr, Result, Token, Type, Visibility, parse_str,
    spanned::Spanned,
};

pub(crate) fn parse_struct_options(attributes: &[Attribute]) -> Result<StructOptions> {
    let mut options = StructOptions::default();

    for attribute in borrowize_attributes(attributes) {
        attribute.parse_nested_meta(|meta| {
            if meta.path.is_ident("no_method") {
                if meta.input.peek(Token![=]) {
                    return Err(meta.error("`no_method` does not take a value"));
                }

                reject_duplicate_flag(&mut options.no_method, meta.path.span(), "no_method")?;
                return Ok(());
            }

            let value = meta.value()?;
            let literal = value.parse::<LitStr>()?;

            if meta.path.is_ident("view_name") {
                set_once(
                    &mut options.view_name,
                    parse_literal(&literal, "view_name")?,
                    meta.path.span(),
                    "view_name",
                )
            } else if meta.path.is_ident("view_visibility") {
                set_once(
                    &mut options.view_visibility,
                    parse_visibility_literal(&literal, "view_visibility")?,
                    meta.path.span(),
                    "view_visibility",
                )
            } else if meta.path.is_ident("field_visibility") {
                set_once(
                    &mut options.field_visibility,
                    parse_visibility_literal(&literal, "field_visibility")?,
                    meta.path.span(),
                    "field_visibility",
                )
            } else if meta.path.is_ident("method_name") {
                set_once(
                    &mut options.method_name,
                    parse_literal(&literal, "method_name")?,
                    meta.path.span(),
                    "method_name",
                )
            } else if meta.path.is_ident("method_visibility") {
                set_once(
                    &mut options.method_visibility,
                    parse_visibility_literal(&literal, "method_visibility")?,
                    meta.path.span(),
                    "method_visibility",
                )
            } else {
                Err(meta.error(format!(
                    "unsupported borrowize struct option `{}`",
                    option_name(&meta.path)
                )))
            }
        })?;
    }

    if options.no_method
        && let Some(method_name) = &options.method_name
    {
        return Err(Error::new(
            method_name.span(),
            "`no_method` cannot be combined with `method_name`",
        ));
    }

    if options.no_method
        && let Some(method_visibility) = &options.method_visibility
    {
        return Err(Error::new(
            method_visibility.span(),
            "`no_method` cannot be combined with `method_visibility`",
        ));
    }

    Ok(options)
}

pub(crate) fn parse_field_options(attributes: &[Attribute]) -> Result<FieldOptions> {
    let mut options = FieldOptions::default();

    for attribute in borrowize_attributes(attributes) {
        attribute.parse_nested_meta(|meta| {
            let value = meta.value()?;
            let literal = value.parse::<LitStr>()?;

            if meta.path.is_ident("borrowed_type") {
                set_once(
                    &mut options.borrowed_type,
                    parse_literal(&literal, "borrowed_type")?,
                    meta.path.span(),
                    "borrowed_type",
                )
            } else if meta.path.is_ident("generation_expression") {
                set_once(
                    &mut options.generation_expression,
                    parse_literal(&literal, "generation_expression")?,
                    meta.path.span(),
                    "generation_expression",
                )
            } else if meta.path.is_ident("visibility") {
                set_once(
                    &mut options.visibility,
                    parse_visibility_literal(&literal, "visibility")?,
                    meta.path.span(),
                    "visibility",
                )
            } else {
                Err(meta.error(format!(
                    "unsupported borrowize field option `{}`",
                    option_name(&meta.path)
                )))
            }
        })?;
    }

    Ok(options)
}

/// Options accepted on the source struct.
#[derive(Default)]
pub(crate) struct StructOptions {
    /// Override for the generated view type name from `view_name`.
    pub(crate) view_name: Option<Ident>,
    /// Explicit visibility for the generated view type from `view_visibility`.
    pub(crate) view_visibility: Option<Visibility>,
    /// Default visibility for generated view fields from `field_visibility`.
    pub(crate) field_visibility: Option<Visibility>,
    /// Override for the generated inherent method name from `method_name`.
    pub(crate) method_name: Option<Ident>,
    /// Explicit visibility for the generated inherent method from `method_visibility`.
    pub(crate) method_visibility: Option<Visibility>,
    /// Whether `no_method` was supplied, suppressing the inherent method.
    pub(crate) no_method: bool,
}

/// Options accepted on a source struct field.
#[derive(Default)]
pub(crate) struct FieldOptions {
    /// Explicit generated field type from `borrowed_type`.
    ///
    /// The generated borrow lifetime is named `'borrowize`, so overrides may
    /// refer to it directly.
    pub(crate) borrowed_type: Option<Type>,
    /// Explicit generated field initializer from `generation_expression`.
    pub(crate) generation_expression: Option<Expr>,
    /// Explicit visibility for this generated view field from `visibility`.
    pub(crate) visibility: Option<Visibility>,
}

fn borrowize_attributes(attributes: &[Attribute]) -> impl Iterator<Item = &Attribute> {
    attributes
        .iter()
        .filter(|attribute| attribute.path().is_ident("borrowize"))
}

fn parse_literal<T>(literal: &LitStr, option_name: &str) -> Result<T>
where
    T: syn::parse::Parse,
{
    literal.parse().map_err(|error| {
        Error::new(
            literal.span(),
            format!("failed to parse `{option_name}` value: {error}"),
        )
    })
}

fn parse_visibility_literal(literal: &LitStr, option_name: &str) -> Result<Visibility> {
    let value = literal.value();

    parse_str::<Visibility>(&value).map_err(|error| {
        Error::new(
            literal.span(),
            format!(
                "failed to parse `{option_name}` value as Rust visibility; omit the option to inherit visibility: {error}"
            ),
        )
    })
}

/// Store an option value once, reporting a duplicate at the second option span.
fn set_once<T>(
    target: &mut Option<T>,
    value: T,
    span: proc_macro2::Span,
    name: &str,
) -> Result<()> {
    if target.is_some() {
        return Err(Error::new(
            span,
            format!("duplicate borrowize option `{name}`"),
        ));
    }

    *target = Some(value);
    Ok(())
}

/// Store a flag once, reporting a duplicate at the second flag span.
fn reject_duplicate_flag(target: &mut bool, span: proc_macro2::Span, name: &str) -> Result<()> {
    if *target {
        return Err(Error::new(
            span,
            format!("duplicate borrowize option `{name}`"),
        ));
    }

    *target = true;
    Ok(())
}

/// Format a possibly qualified option path for diagnostic messages.
fn option_name(path: &syn::Path) -> String {
    path.segments
        .iter()
        .map(|segment| segment.ident.to_string())
        .collect::<Vec<_>>()
        .join("::")
}