Skip to main content

geop_ops_parts_derive/
lib.rs

1//! Derives that describe `geop_ops_parts` operations to anything that
2//! edits programs — a UI generates its forms from these descriptions
3//! instead of knowing each operation by hand.
4//!
5//! - `#[derive(OperationArgs)]` on an operation's arguments struct: every
6//!   field carries `#[arg(<ArgKind>)]` saying what kind of value it holds
7//!   (and so how it is entered), and its doc comment becomes its
8//!   description.
9//! - `#[derive(Operations)]` on the enum of all operations: every variant
10//!   `Name(NameArgs)` dispatches to the unit struct `Name`, which implements
11//!   `Operation`; its doc comment describes the operation and
12//!   `#[operation(label = "...")]` gives its short name.
13
14use proc_macro::TokenStream;
15use quote::quote;
16use syn::{
17    Attribute, Data, DeriveInput, Expr, Fields, LitStr, Meta, parse_macro_input, spanned::Spanned,
18};
19
20/// The text of `attrs`' doc comments, one line each, with the space
21/// rustdoc puts after `///` removed.
22fn doc_of(attrs: &[Attribute]) -> String {
23    attrs
24        .iter()
25        .filter_map(|a| match &a.meta {
26            Meta::NameValue(nv) if nv.path.is_ident("doc") => match &nv.value {
27                Expr::Lit(syn::ExprLit {
28                    lit: syn::Lit::Str(s),
29                    ..
30                }) => Some(s.value()),
31                _ => None,
32            },
33            _ => None,
34        })
35        .map(|line| line.strip_prefix(' ').unwrap_or(&line).to_string())
36        .collect::<Vec<_>>()
37        .join("\n")
38}
39
40/// `Number { .. }` as `::geop_ops_parts::operation::ArgKind::Number { .. }`:
41/// an argument names a kind by its bare variant, and it must mean that
42/// variant whatever else the module has in scope under the same name.
43fn qualify(kind: Expr) -> syn::Result<Expr> {
44    let prefix: syn::Path = syn::parse_quote!(::geop_ops_parts::operation::ArgKind);
45    let qualified = |path: &syn::Path| -> syn::Result<syn::Path> {
46        let Some(variant) = path.get_ident() else {
47            return Err(syn::Error::new(
48                path.span(),
49                "expected an `ArgKind` variant, e.g. `Solid`",
50            ));
51        };
52        let mut full = prefix.clone();
53        full.segments.push(variant.clone().into());
54        Ok(full)
55    };
56    Ok(match kind {
57        Expr::Path(mut p) => {
58            p.path = qualified(&p.path)?;
59            Expr::Path(p)
60        }
61        Expr::Struct(mut s) => {
62            s.path = qualified(&s.path)?;
63            Expr::Struct(s)
64        }
65        other => {
66            return Err(syn::Error::new(
67                other.span(),
68                "expected an `ArgKind` variant, e.g. `Solid` or `Number { .. }`",
69            ));
70        }
71    })
72}
73
74/// `impl OperationArgs`: the schema of every field, from its
75/// `#[arg(<ArgKind expression>)]` and its doc comment.
76#[proc_macro_derive(OperationArgs, attributes(arg))]
77pub fn derive_operation_args(input: TokenStream) -> TokenStream {
78    let input = parse_macro_input!(input as DeriveInput);
79    let name = &input.ident;
80    let Data::Struct(data) = &input.data else {
81        return syn::Error::new(input.span(), "OperationArgs needs a struct")
82            .to_compile_error()
83            .into();
84    };
85    let Fields::Named(fields) = &data.fields else {
86        return syn::Error::new(input.span(), "OperationArgs needs named fields")
87            .to_compile_error()
88            .into();
89    };
90    let mut schemas = Vec::new();
91    for field in &fields.named {
92        let ident = field.ident.as_ref().expect("named field");
93        let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("arg")) else {
94            return syn::Error::new(
95                field.span(),
96                "every argument needs #[arg(<ArgKind>)] saying what kind of value it holds",
97            )
98            .to_compile_error()
99            .into();
100        };
101        let kind: Expr = match attr.parse_args() {
102            Ok(kind) => kind,
103            Err(e) => return e.to_compile_error().into(),
104        };
105        let kind = match qualify(kind) {
106            Ok(kind) => kind,
107            Err(e) => return e.to_compile_error().into(),
108        };
109        let field_name = ident.to_string();
110        let doc = doc_of(&field.attrs);
111        schemas.push(quote! {
112            ::geop_ops_parts::operation::ArgSchema {
113                name: #field_name,
114                doc: #doc,
115                kind: #kind,
116            }
117        });
118    }
119    quote! {
120        impl ::geop_ops_parts::operation::OperationArgs for #name {
121            fn schema() -> ::std::vec::Vec<::geop_ops_parts::operation::ArgSchema> {
122                ::std::vec![#(#schemas),*]
123            }
124        }
125    }
126    .into()
127}
128
129/// `PascalCase` to `snake_case`, the way serde's `rename_all` does it.
130fn snake_case(name: &str) -> String {
131    let mut out = String::new();
132    for (i, c) in name.chars().enumerate() {
133        if c.is_uppercase() {
134            if i > 0 {
135                out.push('_');
136            }
137            out.extend(c.to_lowercase());
138        } else {
139            out.push(c);
140        }
141    }
142    out
143}
144
145/// Dispatch, conversions and schemas for the enum of every operation.
146#[proc_macro_derive(Operations, attributes(operation))]
147pub fn derive_operations(input: TokenStream) -> TokenStream {
148    let input = parse_macro_input!(input as DeriveInput);
149    let name = &input.ident;
150    let Data::Enum(data) = &input.data else {
151        return syn::Error::new(input.span(), "Operations needs an enum")
152            .to_compile_error()
153            .into();
154    };
155    let mut apply_arms = Vec::new();
156    let mut handle_arms = Vec::new();
157    let mut kind_arms = Vec::new();
158    let mut label_arms = Vec::new();
159    let mut schemas = Vec::new();
160    let mut froms = Vec::new();
161    for variant in &data.variants {
162        let op = &variant.ident;
163        let Fields::Unnamed(fields) = &variant.fields else {
164            return syn::Error::new(variant.span(), "every operation is `Name(NameArgs)`")
165                .to_compile_error()
166                .into();
167        };
168        let Some(args) = fields.unnamed.first().map(|f| &f.ty) else {
169            return syn::Error::new(variant.span(), "every operation is `Name(NameArgs)`")
170                .to_compile_error()
171                .into();
172        };
173        let mut label = op.to_string();
174        for attr in variant
175            .attrs
176            .iter()
177            .filter(|a| a.path().is_ident("operation"))
178        {
179            let parsed = attr.parse_nested_meta(|meta| {
180                if meta.path.is_ident("label") {
181                    label = meta.value()?.parse::<LitStr>()?.value();
182                    Ok(())
183                } else {
184                    Err(meta.error("expected `label = \"...\"`"))
185                }
186            });
187            if let Err(e) = parsed {
188                return e.to_compile_error().into();
189            }
190        }
191        let kind = snake_case(&op.to_string());
192        let doc = doc_of(&variant.attrs);
193        apply_arms.push(quote! {
194            #name::#op(args) => ::geop_ops_parts::operation::Operation::<S>::apply(
195                &#op, part, operation_id, args,
196            ),
197        });
198        handle_arms.push(quote! {
199            #name::#op(args) => ::geop_ops_parts::operation::Operation::<S>::handles(&#op, before, args),
200        });
201        kind_arms.push(quote! { #name::#op(_) => #kind, });
202        label_arms.push(quote! { #name::#op(_) => #label, });
203        schemas.push(quote! {
204            ::geop_ops_parts::operation::OperationSchema {
205                kind: #kind,
206                label: #label,
207                doc: #doc,
208                args: <#args as ::geop_ops_parts::operation::OperationArgs>::schema(),
209            }
210        });
211        froms.push(quote! {
212            impl ::std::convert::From<#args> for #name {
213                fn from(args: #args) -> Self {
214                    #name::#op(args)
215                }
216            }
217        });
218    }
219    quote! {
220        impl #name {
221            /// Applies the operation as the program step `operation_id`, see
222            /// [`::geop_ops_parts::operation::Operation::apply`].
223            pub fn apply<S: ::geop_core_math::scalars::Scalar>(
224                &self,
225                part: ::geop_core_part::Part<S>,
226                operation_id: &str,
227            ) -> ::geop_core_math::geop_error::GeopResult<::geop_core_part::Part<S>> {
228                match self {
229                    #(#apply_arms)*
230                }
231            }
232
233            /// The step's handles, given the part `before` it, see
234            /// [`::geop_ops_parts::operation::Operation::handles`].
235            pub fn handles<S: ::geop_core_math::scalars::Scalar>(
236                &self,
237                before: &::geop_core_part::Part<S>,
238            ) -> ::geop_core_math::geop_error::GeopResult<
239                ::std::vec::Vec<::geop_ops_parts::operation::Handle>,
240            > {
241                match self {
242                    #(#handle_arms)*
243                }
244            }
245
246            /// The operation's kind, as it is serialized: `extrude`.
247            pub fn kind(&self) -> &'static str {
248                match self {
249                    #(#kind_arms)*
250                }
251            }
252
253            /// The operation's short name: `Extrude`.
254            pub fn label(&self) -> &'static str {
255                match self {
256                    #(#label_arms)*
257                }
258            }
259
260            /// A description of every operation and its arguments.
261            pub fn schemas() -> ::std::vec::Vec<::geop_ops_parts::operation::OperationSchema> {
262                ::std::vec![#(#schemas),*]
263            }
264        }
265        #(#froms)*
266    }
267    .into()
268}