l10n_impl 0.1.3

Proc macros for l10n.
Documentation
use crate::ast::MessageArgs;
use proc_macro2::Span;
use syn::parse::ParseStream;
use syn::spanned::Spanned;
use syn::{
    Attribute, Data, DataEnum, DataStruct, DeriveInput, Error, Fields, Ident, Index, Lifetime,
    LitStr, Member, Meta, Result, Token, Type,
};

pub enum Input<'a> {
    Struct(Struct<'a>),
    Enum(Enum<'a>),
}

pub struct Struct<'a> {
    pub derive_input: &'a DeriveInput,
    pub fields: Vec<Field<'a>>,
    pub l10n_attribute: L10nAttribute<'a>,
}

pub struct Enum<'a> {
    pub derive_input: &'a DeriveInput,
    pub variants: Vec<Variant<'a>>,
    pub l10n_attribute: L10nAttribute<'a>,
}

pub struct Variant<'a> {
    pub variant_input: &'a syn::Variant,
    pub fields: Vec<Field<'a>>,
    pub l10n_attribute: L10nAttribute<'a>,
}

#[derive(Clone)]
pub struct Field<'a> {
    pub member: Member,
    pub ty: &'a Type,
    pub from: Option<&'a Attribute>,
}

// TODO: I would like to remove Default, either it's a L10nAttribute as a whole
// or it's not.
#[derive(Default)]
pub struct L10nAttribute<'a> {
    pub attribute: Option<&'a Attribute>,
    pub transparent: Option<Span>,
    pub self_lifetime: Option<Lifetime>,
    pub first_literal: Option<LitStr>,
    pub second_literal: Option<LitStr>,
    pub arguments: MessageArgs,
    pub closing_span: Option<Span>,
}

impl<'a> Input<'a> {
    pub fn from_syn(derive_input: &'a DeriveInput) -> Result<Self> {
        match &derive_input.data {
            Data::Struct(data) => Struct::from_syn(derive_input, data).map(Input::Struct),
            Data::Enum(data) => Enum::from_syn(derive_input, data).map(Input::Enum),
            Data::Union(_) => Err(Error::new_spanned(derive_input, "union is not supported")),
        }
    }
}

impl<'a> Struct<'a> {
    fn from_syn(derive_input: &'a DeriveInput, data: &'a DataStruct) -> Result<Self> {
        let l10n_attribute = parse_l10n_attribute(&derive_input.attrs)?.ok_or_else(|| {
            Error::new_spanned(derive_input, r#"missing #[l10n_message("...")] attribute"#)
        })?;
        let fields = Field::multiple_from_syn(&data.fields)?;
        Ok(Struct {
            derive_input,
            fields,
            l10n_attribute,
        })
    }
}

impl<'a> Enum<'a> {
    fn from_syn(derive_input: &'a DeriveInput, data: &'a DataEnum) -> Result<Self> {
        let l10n_attribute = parse_l10n_attribute(&derive_input.attrs)?.unwrap_or_default();

        let variants = data
            .variants
            .iter()
            .map(Variant::from_syn)
            .collect::<Result<_>>()?;

        Ok(Enum {
            derive_input,
            variants,
            l10n_attribute,
        })
    }
}

impl<'a> Variant<'a> {
    fn from_syn(variant_input: &'a syn::Variant) -> Result<Self> {
        let l10n_attribute = parse_l10n_attribute(&variant_input.attrs)?.unwrap_or_default();
        let fields = Field::multiple_from_syn(&variant_input.fields)?;
        Ok(Variant {
            variant_input,
            fields,
            l10n_attribute,
        })
    }
}

fn parse_l10n_attribute(attrs: &[Attribute]) -> Result<Option<L10nAttribute<'_>>> {
    let mut l10n_attribute: Option<L10nAttribute> = None;
    for attr in attrs {
        if attr.path().is_ident("l10n_message") {
            if l10n_attribute.is_some() {
                return Err(Error::new_spanned(
                    attr,
                    "only one #[l10n_message(...)] attribute is allowed",
                ));
            }
            l10n_attribute = Some(_parse_l10n_attribute(attr)?);
        }
    }
    Ok(l10n_attribute)
}

fn _parse_l10n_attribute(attr: &Attribute) -> Result<L10nAttribute<'_>> {
    syn::custom_keyword!(transparent);

    let Meta::List(list) = &attr.meta else {
        unimplemented!();
        // return attr.meta.span()
    };

    let closing_span = list.delimiter.span().close();

    let mut l10n_attribute = L10nAttribute {
        attribute: Some(attr),
        transparent: None,
        self_lifetime: None,
        first_literal: None,
        second_literal: None,
        arguments: Default::default(),
        closing_span: Some(closing_span),
    };

    attr.parse_args_with(|input: ParseStream| {
        l10n_attribute.transparent = input
            .parse::<Option<transparent>>()
            .map(|r| r.map(|kw| kw.span()))?;
        if l10n_attribute.transparent.is_some() {
            return Ok(l10n_attribute);
        }

        l10n_attribute.self_lifetime = input.parse()?;
        if input.is_empty() {
            return Ok(l10n_attribute);
        } else if l10n_attribute.self_lifetime.is_some() {
            input.parse::<Token![,]>()?;
        }

        if !peek_argument(input) {
            l10n_attribute.first_literal = input.parse()?;
            if input.is_empty() {
                return Ok(l10n_attribute);
            } else if l10n_attribute.first_literal.is_some() {
                input.parse::<Token![,]>()?;
            }
        }

        if !peek_argument(input) {
            l10n_attribute.second_literal = input.parse()?;
            if input.is_empty() {
                return Ok(l10n_attribute);
            } else if l10n_attribute.second_literal.is_some() {
                input.parse::<Token![,]>()?;
            }
        }

        if !input.is_empty() {
            l10n_attribute.arguments = input.parse()?;
        }

        Ok(l10n_attribute)
    })
}

fn peek_argument(input: ParseStream) -> bool {
    (input.peek(LitStr) && input.peek2(Token![=]))
        || input.peek(Ident)
        || (input.peek(Token![*]) && input.peek2(Ident))
}

impl<'a> Field<'a> {
    fn multiple_from_syn(fields: &'a Fields) -> Result<Vec<Self>> {
        fields
            .iter()
            .enumerate()
            .map(|(i, field)| Field::from_syn(i, field))
            .collect()
    }

    fn from_syn(i: usize, field_input: &'a syn::Field) -> Result<Self> {
        Ok(Field {
            member: field_input
                .ident
                .clone()
                .map(Member::Named)
                .unwrap_or_else(|| {
                    Member::Unnamed(Index {
                        index: i as u32,
                        span: field_input.ty.span(),
                    })
                }),
            ty: &field_input.ty,
            from: field_input
                .attrs
                .iter()
                .find(|attr| attr.path().is_ident("l10n_from")),
        })
    }
}