Skip to main content

geop_ops_derive/
lib.rs

1//! `#[derive(Operations)]`: a set of `geop_ops` operations as one enum.
2//!
3//! On an enum whose every variant is `Name(NameArgs)`, with `Name` a unit
4//! struct implementing `geop_ops::Operation` with `Args = NameArgs`, it
5//! implements `geop_ops::Operations` — dispatching every method to `Name` —
6//! and `From<NameArgs>` for the enum. A variant's doc comment describes the
7//! operation, and `#[operation(label = "...")]` gives its short name if that
8//! is not the variant's.
9
10use proc_macro::TokenStream;
11use quote::quote;
12use syn::{
13    Attribute, Data, DeriveInput, Expr, Fields, LitStr, Meta, parse_macro_input, spanned::Spanned,
14};
15
16/// The text of `attrs`' doc comments, one line each, with the space
17/// rustdoc puts after `///` removed.
18fn doc_of(attrs: &[Attribute]) -> String {
19    attrs
20        .iter()
21        .filter_map(|a| match &a.meta {
22            Meta::NameValue(nv) if nv.path.is_ident("doc") => match &nv.value {
23                Expr::Lit(syn::ExprLit {
24                    lit: syn::Lit::Str(s),
25                    ..
26                }) => Some(s.value()),
27                _ => None,
28            },
29            _ => None,
30        })
31        .map(|line| line.strip_prefix(' ').unwrap_or(&line).to_string())
32        .collect::<Vec<_>>()
33        .join("\n")
34}
35
36/// `PascalCase` to `snake_case`, the way serde's `rename_all` does it.
37fn snake_case(name: &str) -> String {
38    let mut out = String::new();
39    for (i, c) in name.chars().enumerate() {
40        if c.is_uppercase() {
41            if i > 0 {
42                out.push('_');
43            }
44            out.extend(c.to_lowercase());
45        } else {
46            out.push(c);
47        }
48    }
49    out
50}
51
52/// `impl Operations` and the conversions for the enum of a set of
53/// operations.
54#[proc_macro_derive(Operations, attributes(operation))]
55pub fn derive_operations(input: TokenStream) -> TokenStream {
56    let input = parse_macro_input!(input as DeriveInput);
57    let name = &input.ident;
58    let Data::Enum(data) = &input.data else {
59        return syn::Error::new(input.span(), "Operations needs an enum")
60            .to_compile_error()
61            .into();
62    };
63    let mut infos = Vec::new();
64    let mut new_arms = Vec::new();
65    let mut apply_arms = Vec::new();
66    let mut session_arms = Vec::new();
67    let mut form_arms = Vec::new();
68    let mut set_arms = Vec::new();
69    let mut event_arms = Vec::new();
70    let mut kind_arms = Vec::new();
71    let mut label_arms = Vec::new();
72    let mut froms = Vec::new();
73    for variant in &data.variants {
74        let op = &variant.ident;
75        let Some(args) = (match &variant.fields {
76            Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
77                fields.unnamed.first().map(|f| &f.ty)
78            }
79            _ => None,
80        }) else {
81            return syn::Error::new(variant.span(), "every operation is `Name(NameArgs)`")
82                .to_compile_error()
83                .into();
84        };
85        let mut label = op.to_string();
86        for attr in variant
87            .attrs
88            .iter()
89            .filter(|a| a.path().is_ident("operation"))
90        {
91            let parsed = attr.parse_nested_meta(|meta| {
92                if meta.path.is_ident("label") {
93                    label = meta.value()?.parse::<LitStr>()?.value();
94                    Ok(())
95                } else {
96                    Err(meta.error("expected `label = \"...\"`"))
97                }
98            });
99            if let Err(e) = parsed {
100                return e.to_compile_error().into();
101            }
102        }
103        let kind = snake_case(&op.to_string());
104        let doc = doc_of(&variant.attrs);
105        let operation = quote!(::geop_ops::operation::Operation);
106        infos.push(quote! {
107            ::geop_ops::operation::OperationInfo { kind: #kind, label: #label, doc: #doc }
108        });
109        new_arms.push(quote! {
110            #kind => ::std::result::Result::Ok(#name::#op(#operation::new_args(&#op, before))),
111        });
112        apply_arms.push(quote! {
113            #name::#op(args) => #operation::apply(&#op, part, operation_id, args),
114        });
115        let session = quote!(<#op as #operation>::Session);
116        let expect = quote!(.expect("a session made for a step of the same operation"));
117        session_arms.push(quote! {
118            #name::#op(_) => ::std::boxed::Box::new(<#session as ::std::default::Default>::default()),
119        });
120        form_arms.push(quote! {
121            #name::#op(args) => #operation::form(
122                &#op, before, args, session.downcast_ref::<#session>()#expect, selection,
123            ).erase(),
124        });
125        set_arms.push(quote! {
126            #name::#op(args) => #operation::set(
127                &#op, before, args, session.downcast_mut::<#session>()#expect, selection, key,
128                value,
129            ),
130        });
131        event_arms.push(quote! {
132            #name::#op(args) => #operation::event(
133                &#op, before, args, session.downcast_mut::<#session>()#expect, selection, event,
134            ),
135        });
136        kind_arms.push(quote! { #name::#op(_) => #kind, });
137        label_arms.push(quote! { #name::#op(_) => #label, });
138        froms.push(quote! {
139            impl ::std::convert::From<#args> for #name {
140                fn from(args: #args) -> Self {
141                    #name::#op(args)
142                }
143            }
144        });
145    }
146    let private = quote!(::geop_ops::__private);
147    quote! {
148        impl ::geop_ops::operation::Operations for #name {
149            fn infos() -> ::std::vec::Vec<::geop_ops::operation::OperationInfo> {
150                ::std::vec![#(#infos),*]
151            }
152
153            fn new_step<S: #private::Scalar>(
154                kind: &str,
155                before: &::geop_ops::Part<S>,
156            ) -> #private::GeopResult<Self> {
157                match kind {
158                    #(#new_arms)*
159                    other => ::std::result::Result::Err(#private::GeopError::new(
160                        ::std::format!("there is no operation {other:?}"),
161                    )),
162                }
163            }
164
165            fn apply<S: #private::Scalar>(
166                &self,
167                part: ::geop_ops::Part<S>,
168                operation_id: &str,
169            ) -> #private::GeopResult<::geop_ops::Part<S>> {
170                match self {
171                    #(#apply_arms)*
172                }
173            }
174
175            fn new_session(&self) -> ::std::boxed::Box<dyn ::std::any::Any> {
176                match self {
177                    #(#session_arms)*
178                }
179            }
180
181            fn form<'a, S: #private::Scalar>(
182                &self,
183                before: &'a ::geop_ops::Part<S>,
184                session: &dyn ::std::any::Any,
185                selection: &[::std::string::String],
186            ) -> ::geop_ops::ui::Form<'a, S> {
187                match self {
188                    #(#form_arms)*
189                }
190            }
191
192            fn set<S: #private::Scalar>(
193                &mut self,
194                before: &::geop_ops::Part<S>,
195                session: &mut dyn ::std::any::Any,
196                selection: &mut ::std::vec::Vec<::std::string::String>,
197                key: &str,
198                value: ::geop_ops::ui::Value,
199            ) {
200                match self {
201                    #(#set_arms)*
202                }
203            }
204
205            fn event<S: #private::Scalar>(
206                &mut self,
207                before: &::geop_ops::Part<S>,
208                session: &mut dyn ::std::any::Any,
209                selection: &mut ::std::vec::Vec<::std::string::String>,
210                event: &::geop_ops::ui::CanvasEvent<S>,
211            ) {
212                match self {
213                    #(#event_arms)*
214                }
215            }
216
217            fn kind(&self) -> &'static str {
218                match self {
219                    #(#kind_arms)*
220                }
221            }
222
223            fn label(&self) -> &'static str {
224                match self {
225                    #(#label_arms)*
226                }
227            }
228        }
229        #(#froms)*
230    }
231    .into()
232}