lumen-engine-macros 0.2.2

Procedural macros for Lumen engine node metadata.
Documentation
use quote::{format_ident, quote};
use syn::{
    Attribute, Data, DeriveInput, Fields, Ident, LitBool, LitFloat, LitInt, LitStr, Path, Result,
    Token, Type, Variant, punctuated::Punctuated, spanned::Spanned,
};

pub(crate) fn expand_delegate(input: DeriveInput) -> Result<proc_macro2::TokenStream> {
    match &input.data {
        Data::Struct(data) => structure::expand_struct_delegate(&input, &data.fields),
        Data::Enum(data) => enums::expand_enum_delegate(&input, &data.variants),
        Data::Union(_) => Err(syn::Error::new(
            input.ident.span(),
            "Delegate derive does not support unions",
        )),
    }
}

mod enums;
mod structure;

struct DelegateField {
    ident: Ident,
    ty: Type,
    property: PropertyAttr,
}

struct PropertyAttr {
    id: LitStr,
    name: LitStr,
    kind: Option<Ident>,
    enum_type: Option<Path>,
    constraints: PropertyConstraintsAttr,
    docs: Vec<String>,
}

#[derive(Default)]
struct PropertyConstraintsAttr {
    min: Option<f64>,
    max: Option<f64>,
    step: Option<f64>,
    format: Option<LitStr>,
    multiline: bool,
    recommended_rows: Option<u32>,
    role: Option<LitStr>,
}

struct DelegateAttr {
    name: Ident,
    kind: Option<Ident>,
}

fn parse_delegate_attr(input: &DeriveInput) -> Result<DelegateAttr> {
    let mut name = None;
    let mut kind = None;
    if let Some(attr) = optional_attr(&input.attrs, "delegate") {
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("name") {
                name = Some(meta.value()?.parse::<Ident>()?);
            } else if meta.path.is_ident("kind") {
                kind = Some(ident_value(&meta)?);
            } else {
                return Err(meta.error("unknown delegate attribute key"));
            }
            Ok(())
        })?;
    }
    Ok(DelegateAttr {
        name: name.unwrap_or_else(|| format_ident!("{}Delegate", input.ident)),
        kind,
    })
}

fn parse_meta_attr(attrs: &[Attribute], field: &Ident) -> Result<PropertyAttr> {
    let attr = optional_attr(attrs, "meta")
        .ok_or_else(|| syn::Error::new(field.span(), "Delegate fields require #[meta]"))?;
    let mut id = None;
    let mut name = None;
    let mut kind = None;
    let mut enum_type = None;
    let mut constraints = PropertyConstraintsAttr::default();

    attr.parse_nested_meta(|meta| {
        if meta.path.is_ident("id") {
            id = Some(meta.value()?.parse()?);
        } else if meta.path.is_ident("name") {
            name = Some(meta.value()?.parse()?);
        } else if meta.path.is_ident("kind") {
            kind = Some(ident_value(&meta)?);
        } else if meta.path.is_ident("enum_type") {
            enum_type = Some(meta.value()?.parse()?);
        } else if meta.path.is_ident("min") {
            constraints.min = Some(float_value(&meta)?);
        } else if meta.path.is_ident("max") {
            constraints.max = Some(float_value(&meta)?);
        } else if meta.path.is_ident("step") {
            constraints.step = Some(float_value(&meta)?);
        } else if meta.path.is_ident("format") {
            constraints.format = Some(meta.value()?.parse()?);
        } else if meta.path.is_ident("multiline") {
            constraints.multiline = optional_bool(&meta)?;
        } else if meta.path.is_ident("recommended_rows") {
            constraints.recommended_rows = Some(meta.value()?.parse::<LitInt>()?.base10_parse()?);
        } else if meta.path.is_ident("role") {
            constraints.role = Some(meta.value()?.parse()?);
        } else {
            return Err(meta.error("unknown meta attribute key"));
        }
        Ok(())
    })?;

    Ok(PropertyAttr {
        id: id.unwrap_or_else(|| LitStr::new(&field.to_string(), field.span())),
        name: name
            .unwrap_or_else(|| LitStr::new(&title_from_ident(&field.to_string()), field.span())),
        kind,
        enum_type,
        constraints,
        docs: doc_lines(attrs),
    })
}

fn property_def_tokens(property: &PropertyAttr, ty: &Type) -> proc_macro2::TokenStream {
    let id = &property.id;
    let name = &property.name;
    let kind = match &property.kind {
        Some(kind) => property_kind_tokens(kind),
        None => quote!(<#ty as ::lumen_engine::node::NodeParamType>::property_kind()),
    };
    let enum_def = match &property.enum_type {
        Some(enum_type) => quote!(Some(<#enum_type as ::lumen_engine::node::NodeEnum>::enum_def())),
        None => quote!(<#ty as ::lumen_engine::node::NodeParamType>::enum_def()),
    };
    let description = doc_string(&property.docs);
    let constraints = property_constraints_tokens(&property.constraints);
    quote! {
        ::lumen_engine::node::PropertyDef {
            id: #id,
            expected: #kind,
            #[cfg(any(feature = "json", feature = "metadata"))]
            enum_def: #enum_def,
            #[cfg(feature = "metadata")]
            name: #name,
            #[cfg(feature = "metadata")]
            description: #description,
            #[cfg(feature = "metadata")]
            constraints: #constraints,
        }
    }
}

fn property_constraints_tokens(constraints: &PropertyConstraintsAttr) -> proc_macro2::TokenStream {
    let min = option_f64_tokens(constraints.min);
    let max = option_f64_tokens(constraints.max);
    let step = option_f64_tokens(constraints.step);
    let format = option_lit_str_tokens(&constraints.format);
    let multiline = constraints.multiline;
    let recommended_rows = match constraints.recommended_rows {
        Some(value) => quote!(Some(#value)),
        None => quote!(None),
    };
    let role = option_lit_str_tokens(&constraints.role);
    quote! {
        ::lumen_engine::node::PropertyConstraints {
            min: #min,
            max: #max,
            step: #step,
            format: #format,
            multiline: #multiline,
            recommended_rows: #recommended_rows,
            role: #role,
        }
    }
}

fn named_fields(fields: &Fields) -> Result<&Punctuated<syn::Field, Token![,]>> {
    match fields {
        Fields::Named(fields) => Ok(&fields.named),
        _ => Err(syn::Error::new(
            fields.span(),
            "Delegate derive requires named struct fields",
        )),
    }
}

fn doc_lines(attrs: &[Attribute]) -> Vec<String> {
    attrs
        .iter()
        .filter(|attr| attr.path().is_ident("doc"))
        .filter_map(|attr| match &attr.meta {
            syn::Meta::NameValue(name_value) => match &name_value.value {
                syn::Expr::Lit(expr_lit) => match &expr_lit.lit {
                    syn::Lit::Str(lit) => Some(lit.value().trim().to_string()),
                    _ => None,
                },
                _ => None,
            },
            _ => None,
        })
        .collect()
}

fn doc_string(lines: &[String]) -> String {
    lines.join("\n")
}

fn optional_attr<'a>(attrs: &'a [Attribute], name: &str) -> Option<&'a Attribute> {
    attrs.iter().find(|attr| attr.path().is_ident(name))
}

fn ident_value(meta: &syn::meta::ParseNestedMeta<'_>) -> Result<Ident> {
    let value = meta.value()?;
    if value.peek(LitStr) {
        let lit: LitStr = value.parse()?;
        Ok(Ident::new(&lit.value(), lit.span()))
    } else {
        value.parse()
    }
}

fn optional_bool(meta: &syn::meta::ParseNestedMeta<'_>) -> Result<bool> {
    if meta.input.peek(Token![=]) {
        Ok(meta.value()?.parse::<LitBool>()?.value)
    } else {
        Ok(true)
    }
}

fn float_value(meta: &syn::meta::ParseNestedMeta<'_>) -> Result<f64> {
    let value = meta.value()?;
    if value.peek(LitFloat) {
        value.parse::<LitFloat>()?.base10_parse()
    } else {
        value
            .parse::<LitInt>()?
            .base10_parse::<i64>()
            .map(|value| value as f64)
    }
}

fn option_f64_tokens(value: Option<f64>) -> proc_macro2::TokenStream {
    match value {
        Some(value) => quote!(Some(#value)),
        None => quote!(None),
    }
}

fn option_lit_str_tokens(value: &Option<LitStr>) -> proc_macro2::TokenStream {
    match value {
        Some(value) => quote!(Some(#value)),
        None => quote!(None),
    }
}

fn property_kind_tokens(kind: &Ident) -> proc_macro2::TokenStream {
    match kind.to_string().as_str() {
        "float" | "Float" => quote!(::lumen_engine::node::PropertyKind::Float),
        "int" | "Int" => quote!(::lumen_engine::node::PropertyKind::Int),
        "bool" | "Bool" => quote!(::lumen_engine::node::PropertyKind::Bool),
        "string" | "String" => quote!(::lumen_engine::node::PropertyKind::String),
        "color" | "Color" => quote!(::lumen_engine::node::PropertyKind::Color),
        "vec2" | "Vec2" => quote!(::lumen_engine::node::PropertyKind::Vec2),
        "enum" | "Enum" => quote!(::lumen_engine::node::PropertyKind::Enum),
        "paint" | "Paint" => quote!(::lumen_engine::node::PropertyKind::Paint),
        _ => {
            let span = kind.span();
            quote::quote_spanned!(span=> compile_error!("unknown param kind"))
        }
    }
}

fn to_snake_case(value: &str) -> String {
    let mut out = String::new();
    for (index, ch) in value.chars().enumerate() {
        if ch.is_uppercase() {
            if index > 0 {
                out.push('_');
            }
            out.extend(ch.to_lowercase());
        } else {
            out.push(ch);
        }
    }
    out
}

fn title_from_ident(value: &str) -> String {
    let mut out = String::new();
    for (index, part) in to_snake_case(value).split('_').enumerate() {
        if index > 0 {
            out.push(' ');
        }
        let mut chars = part.chars();
        if let Some(first) = chars.next() {
            out.extend(first.to_uppercase());
            out.push_str(chars.as_str());
        }
    }
    out
}