ezmenu_macros/
lib.rs

1//! This crate is a derive procedural macro for `ezmenu` crate.
2//! It should not be used directly. You must use the [`ezmenu`](https://docs.rs/ezmenu) crate.
3
4mod struct_field;
5mod struct_impl;
6
7mod utils;
8
9extern crate proc_macro as pm;
10
11use crate::struct_field::{FieldFormatting, FieldMenuInit};
12use crate::struct_impl::MenuInit;
13pub(crate) use utils::*;
14
15use proc_macro2::TokenStream;
16use proc_macro_error::{abort, abort_call_site, proc_macro_error};
17use quote::quote;
18use syn::{
19    parse_macro_input, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, FieldsNamed,
20    Ident,
21};
22
23// // TODO: parser attribute
24// #[proc_macro_attribute]
25// #[proc_macro_error]
26// pub fn parser(_attr: pm::TokenStream, _ts: pm::TokenStream) -> pm::TokenStream {
27//     pm::TokenStream::new()
28// }
29
30#[cfg(feature = "parsed")]
31#[proc_macro_attribute]
32#[proc_macro_error]
33pub fn parsed(_attr: pm::TokenStream, ts: pm::TokenStream) -> pm::TokenStream {
34    let input = parse_macro_input!(ts as DeriveInput);
35    if let Data::Enum(e) = &input.data {
36        build_parsed_enum(&input, e)
37    } else {
38        abort!(
39            input,
40            "ezmenu_derive::ezmenu::parsed macro attribute only works on unit enums."
41        )
42    }
43    .into()
44}
45
46fn build_parsed_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream {
47    let ident = &input.ident;
48
49    let inputs = data
50        .variants
51        .iter()
52        .map(|var| var.ident.to_string().to_lowercase());
53    let outputs = data.variants.iter().map(|var| &var.ident);
54
55    quote! {
56        #input
57        impl ::std::str::FromStr for #ident {
58            type Err = ::ezmenu::lib::MenuError;
59
60            fn from_str(s: &str) -> Result<Self, Self::Err> {
61                match s.to_lowercase().as_str() {
62                    #(#inputs => Ok(Self::#outputs),)*
63                    _ => Err(::ezmenu::lib::MenuError::Other(
64                        // necessary to provide error because default value can be provided
65                        Box::new(format!("unrecognized input for `{}`", s))))
66                }
67            }
68        }
69    }
70}
71
72#[cfg(feature = "derive")]
73#[proc_macro_derive(Menu, attributes(menu))]
74#[proc_macro_error]
75pub fn build_menu(ts: pm::TokenStream) -> pm::TokenStream {
76    let input = parse_macro_input!(ts as DeriveInput);
77    match input.data {
78        Data::Enum(_e) => todo!("derive on enum soon"),
79        Data::Struct(DataStruct {
80            fields: Fields::Named(fields),
81            ..
82        }) => build_struct(input.ident, input.attrs, fields),
83        _ => abort_call_site!("Menu derive supports only non-tuple structs and unit-like enums."),
84    }
85    .into()
86}
87
88fn def_init(menu_desc: MenuInit) -> TokenStream {
89    let fields = &menu_desc.fields;
90    let fields_instance = fields.clone().into_iter().map(|f| f.kind);
91    quote! {
92        pub fn from_menu_safe() -> ::ezmenu::lib::MenuResult<Self> {
93            use ::ezmenu::lib::Menu;
94            let mut menu = ::ezmenu::lib::ValueMenu::from([#(#fields),*])
95                #menu_desc;
96            Ok(Self {#(
97                #fields_instance
98            )*})
99        }
100
101        pub fn from_menu() -> Self {
102            Self::from_menu_safe()
103                .expect("An error occurred while processing menu")
104        }
105    }
106}
107
108fn build_struct(name: Ident, attrs: Vec<Attribute>, fields: FieldsNamed) -> TokenStream {
109    // optional menu attr of the struct
110    let struct_attr = get_meta_attr(attrs);
111    // fields of the struct mapped to menu fields description
112    let fields = fields.named.into_iter().map(FieldMenuInit::from).collect();
113
114    let init = def_init(MenuInit::new(struct_attr, fields));
115
116    quote! {
117        impl #name {
118            #init
119        }
120    }
121}