hephae_plugins/
lib.rs

1#![allow(internal_features)]
2#![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))]
3#![doc = include_str!("../README.md")]
4#![cfg_attr(doc, deny(missing_docs))]
5
6extern crate proc_macro;
7
8use hephae_macros::{
9    Manifest,
10    proc_macro2::TokenStream,
11    quote::{ToTokens, quote, quote_spanned},
12    syn,
13    syn::{
14        Error, Ident, Token, Type, parenthesized,
15        parse::{Parse, ParseStream},
16        spanned::Spanned,
17    },
18};
19
20struct Syntax {
21    #[cfg(feature = "atlas")]
22    atlas: Option<TokenStream>,
23    #[cfg(feature = "locale")]
24    locale: Option<TokenStream>,
25    render: Option<TokenStream>,
26    #[cfg(feature = "text")]
27    text: Option<TokenStream>,
28    #[cfg(feature = "ui")]
29    ui: Option<TokenStream>,
30}
31
32impl Parse for Syntax {
33    fn parse(input: ParseStream) -> syn::Result<Self> {
34        fn no_data(id: &str, input: ParseStream) -> syn::Result<()> {
35            if input.peek(Token![:]) {
36                Err(input.error(format!("`{id}` doesn't accept type arguments")))
37            } else {
38                Ok(())
39            }
40        }
41
42        fn no_redefine(
43            id: &str,
44            dst: &mut Option<TokenStream>,
45            src: impl FnOnce() -> syn::Result<TokenStream>,
46            tokens: impl ToTokens,
47        ) -> syn::Result<()> {
48            match dst {
49                Some(..) => Err(Error::new_spanned(tokens, format!("`{id}` defined multiple times"))),
50                None => {
51                    *dst = Some(src()?);
52                    Ok(())
53                }
54            }
55        }
56
57        #[allow(unused)]
58        fn unsupported(id: &str, tokens: impl ToTokens) -> Error {
59            Error::new_spanned(
60                tokens,
61                format!("`{id}` unsupported; have you enabled the feature in `Cargo.toml`?"),
62            )
63        }
64
65        let manifest = Manifest::get();
66
67        #[cfg(feature = "atlas")]
68        let mut atlas = None;
69        #[cfg(feature = "locale")]
70        let mut locale = None;
71        let mut render = None;
72        #[cfg(feature = "text")]
73        let mut text = None;
74        #[cfg(feature = "ui")]
75        let mut ui = None;
76
77        while !input.is_empty() {
78            if let Some(token) = input.parse::<Option<Token![..]>>()? {
79                if !input.is_empty() {
80                    return Err(input.error("no more tokens are allowed after `..`"))
81                }
82
83                #[cfg(feature = "atlas")]
84                if let (true, Ok(atlas_crate)) = (atlas.is_none(), manifest.resolve("hephae", "atlas", token)) {
85                    atlas = Some(quote_spanned! { token.span() => #atlas_crate::AtlasPlugin::default() })
86                }
87                #[cfg(feature = "locale")]
88                if let (true, Ok(locale_crate)) = (locale.is_none(), manifest.resolve("hephae", "locale", token)) {
89                    locale = Some(quote_spanned! { token.span() => #locale_crate::LocalePlugin::<(), ()>::default() })
90                }
91                if let (true, Ok(render_crate)) = (render.is_none(), manifest.resolve("hephae", "render", token)) {
92                    render = Some(quote_spanned! { token.span() => #render_crate::RendererPlugin::<(), ()>::default() })
93                }
94                #[cfg(feature = "text")]
95                if let (true, Ok(text_crate)) = (text.is_none(), manifest.resolve("hephae", "text", token)) {
96                    text = Some(quote_spanned! { token.span() => #text_crate::TextPlugin::default() })
97                }
98                #[cfg(feature = "ui")]
99                if let (true, Ok(ui_crate)) = (ui.is_none(), manifest.resolve("hephae", "ui", token)) {
100                    ui = Some(quote_spanned! { token.span() => #ui_crate::UiPlugin::<(), ()>::default() })
101                }
102
103                break
104            } else {
105                let id = input.parse::<Ident>()?;
106                match &*id.to_string() {
107                    #[cfg(feature = "atlas")]
108                    "atlas" => {
109                        no_data("atlas", input)?;
110                        no_redefine(
111                            "atlas",
112                            &mut atlas,
113                            || {
114                                let atlas_crate = manifest.resolve("hephae", "atlas", &id)?;
115                                Ok(quote_spanned! { id.span() => #atlas_crate::AtlasPlugin::default() })
116                            },
117                            &id,
118                        )?
119                    }
120                    #[cfg(not(feature = "atlas"))]
121                    "atlas" => return Err(unsupported("atlas", id)),
122                    #[cfg(feature = "locale")]
123                    "locale" => no_redefine(
124                        "locale",
125                        &mut locale,
126                        || {
127                            let locale_crate = manifest.resolve("hephae", "locale", &id)?;
128                            if input.parse::<Option<Token![:]>>()?.is_some() {
129                                let data;
130                                parenthesized!(data in input);
131
132                                let arg = data.parse::<Type>()?;
133                                data.parse::<Token![,]>()?;
134                                let target = data.parse::<Type>()?;
135                                data.parse::<Option<Token![,]>>()?;
136
137                                if !data.is_empty() {
138                                    return Err(data.error("expected end of tuple"))
139                                }
140
141                                Ok(quote_spanned! { id.span() => #locale_crate::LocalePlugin::<#arg, #target>::default() })
142                            } else {
143                                Ok(quote_spanned! { id.span() => #locale_crate::LocalePlugin::<(), ()>::default() })
144                            }
145                        },
146                        &id,
147                    )?,
148                    #[cfg(not(feature = "locale"))]
149                    "locale" => return Err(unsupported("locale", id)),
150                    "render" => no_redefine(
151                        "render",
152                        &mut render,
153                        || {
154                            let render_crate = manifest.resolve("hephae", "render", &id)?;
155                            if input.parse::<Option<Token![:]>>()?.is_some() {
156                                let data;
157                                parenthesized!(data in input);
158
159                                let vertex = data.parse::<Type>()?;
160                                data.parse::<Token![,]>()?;
161                                let drawer = data.parse::<Type>()?;
162                                data.parse::<Option<Token![,]>>()?;
163
164                                if !data.is_empty() {
165                                    return Err(data.error("expected end of tuple"))
166                                }
167
168                                Ok(
169                                    quote_spanned! { id.span() => #render_crate::RendererPlugin::<#vertex, #drawer>::default() },
170                                )
171                            } else {
172                                Ok(quote_spanned! { id.span() => #render_crate::RendererPlugin::<(), ()>::default() })
173                            }
174                        },
175                        &id,
176                    )?,
177                    #[cfg(feature = "text")]
178                    "text" => {
179                        no_data("text", input)?;
180                        no_redefine(
181                            "text",
182                            &mut text,
183                            || {
184                                let text_crate = manifest.resolve("hephae", "text", &id)?;
185                                Ok(quote_spanned! { id.span() => #text_crate::TextPlugin::default() })
186                            },
187                            &id,
188                        )?
189                    }
190                    #[cfg(not(feature = "text"))]
191                    "text" => return Err(unsupported("text", id)),
192                    #[cfg(feature = "ui")]
193                    "ui" => no_redefine(
194                        "ui",
195                        &mut ui,
196                        || {
197                            let ui_crate = manifest.resolve("hephae", "ui", &id)?;
198                            if input.parse::<Option<Token![:]>>()?.is_some() {
199                                let data;
200                                parenthesized!(data in input);
201
202                                let measure = data.parse::<Type>()?;
203                                data.parse::<Token![,]>()?;
204                                let root = data.parse::<Type>()?;
205                                data.parse::<Option<Token![,]>>()?;
206
207                                if !data.is_empty() {
208                                    return Err(data.error("expected end of tuple"))
209                                }
210
211                                Ok(quote_spanned! { id.span() => #ui_crate::UiPlugin::<#measure, #root>::default() })
212                            } else {
213                                Ok(quote_spanned! { id.span() => #ui_crate::UiPlugin::<(), ()>::default() })
214                            }
215                        },
216                        &id,
217                    )?,
218                    #[cfg(not(feature = "ui"))]
219                    "ui" => return Err(unsupported("ui", id)),
220                    other => return Err(Error::new_spanned(id, format!("unknown plugin `{other}`"))),
221                }
222            }
223
224            if input.is_empty() {
225                break
226            } else {
227                input.parse::<Token![,]>()?;
228            }
229        }
230
231        Ok(Self {
232            #[cfg(feature = "atlas")]
233            atlas,
234            #[cfg(feature = "locale")]
235            locale,
236            render,
237            #[cfg(feature = "text")]
238            text,
239            #[cfg(feature = "ui")]
240            ui,
241        })
242    }
243}
244
245/// The `hephae! { ... }` procedural macro for specifying Hephae plugins.
246///
247/// ```rust,no_run
248/// # use hephae_plugins::hephae;
249///
250/// // You can define these plugin attributes in any order, as long as you only define it either zero or one times.
251/// hephae! {
252///     // `atlas`: Requires the `"atlas"` feature, and does not accept any type arguments.
253///     atlas,
254///     // `locale`: Requires the `"locale"` feature, optionally accepts `(ArgConf, TargetConf)` type arguments.
255///     locale: ((MyLocaleArg1, MyLocaleArg2, ..), (MyTarget1, MyTarget2, ..)),
256///     // `render`: Always available, optionally accepts `(VertexConf, DrawerConf)` type arguments.
257///     render: ((MyVertex1, MyVertex2, ..), (MyDrawer1, MyDrawer2, ..)),
258///     // `text`: Requires the `"text"` feature, and does not accept any type arguments.
259///     text,
260///     // `ui`: Requires the `"ui"` feature, optionally accepts `(MeasureConf, RootConf)` type arguments.
261///     ui: ((MyMeasurer1, MyMeasurer2, ..), (MyRoot1, MyRoot2, ..)),
262///     // You can also tell the macro to include every Hephae features via the feature flags with their default settings.
263///     // Note that this `..` syntax must appear at the very last.
264///     ..
265/// }
266/// ```
267#[proc_macro]
268pub fn hephae(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
269    fn parse_inner(input: TokenStream) -> syn::Result<TokenStream> {
270        let span = input.span();
271        let Syntax {
272            atlas,
273            locale,
274            render,
275            text,
276            ui,
277        } = syn::parse2(input)?;
278
279        let mut plugins = Vec::with_capacity(5);
280
281        if let Some(render) = render {
282            plugins.push(render)
283        }
284        #[cfg(feature = "atlas")]
285        if let Some(atlas) = atlas {
286            plugins.push(atlas)
287        }
288        #[cfg(feature = "locale")]
289        if let Some(locale) = locale {
290            plugins.push(locale)
291        }
292        #[cfg(feature = "text")]
293        if let Some(text) = text {
294            plugins.push(text)
295        }
296        #[cfg(feature = "ui")]
297        if let Some(ui) = ui {
298            plugins.push(ui)
299        }
300
301        match &*plugins {
302            [] => Err(Error::new(
303                span,
304                "at least one plugin must be specified, or `..` use all defaults",
305            )),
306            [data] => Ok(quote! { #data }),
307            data => Ok(quote! { (#(#data),*) }),
308        }
309    }
310
311    parse_inner(input.into()).unwrap_or_else(Error::into_compile_error).into()
312}