hyprrust_macros 0.2.0

A helper crate for the hyprrust crate. Not meant to be used by itself.
Documentation
use core::panic;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{format_ident, quote};
use syn::punctuated::Punctuated;
use syn::{parse_macro_input, ExprAssign, Ident, ItemEnum};
use syn::{Attribute, DeriveInput, Expr, ExprLit, ExprPath, Lit, Meta, Path, Token};

fn parse_expr_to_ident(expr: &Expr) -> &syn::Ident {
    match expr {
        Expr::Path(ExprPath {
            attrs: _,
            qself: _,
            path: Path {
                leading_colon: _,
                segments,
            },
        }) => &segments.last().unwrap().ident,
        _ => panic!("Expected identifier"),
    }
}

fn parse_command(attr: &Attribute) -> (Option<String>, Option<Ident>) {
    if !attr.path().is_ident("command") {
        return (None, None);
    }

    let mut prefix: Option<String> = None;
    let mut argument_type: Option<Ident> = None;

    match &attr.meta {
        Meta::NameValue(name) => {
            if let Expr::Lit(ExprLit {
                attrs: _,
                lit: Lit::Str(lit_str),
            }) = &name.value
            {
                prefix = Some(lit_str.value());
            } else {
                panic!("command expects a string");
            }
        }
        Meta::List(list) => {
            for expr in list
                .parse_args_with(Punctuated::<ExprAssign, Token![,]>::parse_terminated)
                .unwrap()
            {
                match parse_expr_to_ident(&expr.left).to_string().as_str() {
                    "prefix" => {
                        if prefix.is_some() {
                            panic!("Duplicate prefix");
                        }

                        prefix = match &*expr.right {
                            Expr::Lit(ExprLit {
                                attrs: _,
                                lit: Lit::Str(s),
                            }) => Some(s.value()),
                            _ => panic!("Prefix should be a string"),
                        }
                    }
                    "arg_type" => {
                        if argument_type.is_some() {
                            panic!("Duplicate arg_type");
                        }

                        argument_type = Some(parse_expr_to_ident(&expr.right).clone());
                    }
                    _ => {
                        panic!("Unknown option")
                    }
                }
            }
        }
        _ => {
            panic!("command supports only a string value or a list of assignmets")
        }
    }

    (prefix, argument_type)
}

#[proc_macro_derive(HyprlandDataWithArgument, attributes(command))]
pub fn hyprland_data_with_argument_derive(input: TokenStream) -> TokenStream {
    let derive = parse_macro_input!(input as DeriveInput);
    let name = &derive.ident;

    let mut name_str = None;
    let mut arg_type = None;

    for attr in derive.attrs {
        (name_str, arg_type) = parse_command(&attr);
        if name_str.is_some() || arg_type.is_some() {
            break;
        }
    }

    let name_str = match name_str {
        Some(name) => name,
        None => name.to_string().to_lowercase(),
    };

    let arg_type = match arg_type {
        Some(arg_type) => arg_type,
        None => Ident::new("String", Span::call_site()),
    };

    quote! {
        impl HyprlandDataWithArgument for #name {
            type Argument = #arg_type;

            fn get_command(arg: Self::Argument) -> String {
                return format!("{} {}", #name_str, arg.to_argument_string()).to_owned();
            }
        }
    }
    .into()
}

#[proc_macro_derive(HyprlandData, attributes(command))]
pub fn hyprland_data_derive(input: TokenStream) -> TokenStream {
    let derive = parse_macro_input!(input as DeriveInput);
    let name = &derive.ident;

    let mut name_str = None;
    for attr in derive.attrs {
        (name_str, _) = parse_command(&attr);
        if name_str.is_some() {
            break;
        }
    }

    let name_str = match name_str {
        Some(name) => name,
        None => name.to_string().to_lowercase(),
    };

    quote! {
        impl HyprlandData for #name {
            fn get_command() -> &'static str {
                return #name_str;
            }
        }
    }
    .into()
}

#[proc_macro_attribute]
pub fn generate_enum_types(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let enum_ = parse_macro_input!(item as ItemEnum);

    let name = format_ident!("{}Type", enum_.ident.to_string());

    let (types, type_conversion): (Vec<_>, Vec<_>) = enum_
        .variants
        .iter()
        .map(|v| {
            let type_str_name = &v.ident.to_string().to_lowercase();
            let ident = &v.ident;
            (
                ident,
                quote! {
                    #name::#ident => #type_str_name
                },
            )
        })
        .unzip();

    quote! {
        #enum_
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
        pub enum #name {
            #(#types),*
        }
        impl #name {
            pub fn get_name(&self) -> &'static str {
                match self {
                    #(#type_conversion),*
                }
            }
        }
        impl AsRef<#name> for #name {
            fn as_ref(&self) -> &#name {
                &self
            }
        }
    }
    .into()
}