geop_ops_parts_derive/
lib.rs1use proc_macro::TokenStream;
15use quote::quote;
16use syn::{
17 Attribute, Data, DeriveInput, Expr, Fields, LitStr, Meta, parse_macro_input, spanned::Spanned,
18};
19
20fn 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
40fn 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#[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
129fn 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#[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 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 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 pub fn kind(&self) -> &'static str {
248 match self {
249 #(#kind_arms)*
250 }
251 }
252
253 pub fn label(&self) -> &'static str {
255 match self {
256 #(#label_arms)*
257 }
258 }
259
260 pub fn schemas() -> ::std::vec::Vec<::geop_ops_parts::operation::OperationSchema> {
262 ::std::vec![#(#schemas),*]
263 }
264 }
265 #(#froms)*
266 }
267 .into()
268}