1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4 parse::Nothing,
5 parse_macro_input,
6 Data::{Enum, Struct},
7 DataEnum, DataStruct, DeriveInput, FieldsNamed, Variant,
8};
9
10fn variants_from(
11 tokens: proc_macro2::TokenStream,
12) -> syn::Result<syn::punctuated::Punctuated<Variant, syn::token::Comma>> {
13 let default_ast: DeriveInput = syn::parse2(tokens)?;
14 match default_ast.data {
15 Enum(DataEnum { variants, .. }) => Ok(variants),
16 _ => panic!("only enums can provide variants"),
17 }
18}
19
20fn extend_enum_with_variants_impl(
21 input: proc_macro2::TokenStream,
22 default_variants: proc_macro2::TokenStream,
23) -> proc_macro2::TokenStream {
24 let mut input_ast: DeriveInput = syn::parse2(input).expect("parse input enum");
25 let variants = variants_from(default_variants).expect("parse default variants");
26 let input_variants_data = match &mut input_ast.data {
27 Enum(DataEnum { variants, .. }) => variants,
28 _ => panic!("only enums can accept variants"),
29 };
30 input_variants_data.extend(variants);
31 quote! { #input_ast }
32}
33
34fn ownables_attach_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
35 extend_enum_with_variants_impl(
36 input,
37 quote! {
38 enum ExecuteMsg {
39 Attach { attachments: Vec<AttachmentInput> },
40 }
41 },
42 )
43 .into()
44}
45
46fn ownables_close_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
47 extend_enum_with_variants_impl(
48 input,
49 quote! {
50 enum ExecuteMsg {
51 Close {},
52 }
53 },
54 )
55 .into()
56}
57
58fn ownables_query_attachments_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
59 extend_enum_with_variants_impl(
60 input,
61 quote! {
62 enum QueryMsg {
63 GetAttachments {},
64 }
65 },
66 )
67 .into()
68}
69
70fn ownables_query_closed_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
71 extend_enum_with_variants_impl(
72 input,
73 quote! {
74 enum QueryMsg {
75 IsClosed {},
76 }
77 },
78 )
79 .into()
80}
81
82fn ownables_transfer_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
83 extend_enum_with_variants_impl(
84 input,
85 quote! {
86 enum ExecuteMsg {
87 Transfer {to: Addr},
88 }
89 },
90 )
91 .into()
92}
93
94fn ownables_query_info_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
95 extend_enum_with_variants_impl(
96 input,
97 quote! {
98 enum QueryMsg {
99 GetInfo {},
100 }
101 },
102 )
103 .into()
104}
105
106fn ownables_query_consumed_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
107 extend_enum_with_variants_impl(
108 input,
109 quote! {
110 enum QueryMsg {
111 IsConsumed {},
112 }
113 },
114 )
115 .into()
116}
117
118fn ownables_query_consumer_of_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
119 extend_enum_with_variants_impl(
120 input,
121 quote! {
122 enum QueryMsg {
123 IsConsumerOf {
124 issuer: Addr,
125 consumable_type: String,
126 },
127 }
128 },
129 )
130 .into()
131}
132
133fn ownables_instantiate_msg_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
134 let default_instantiate_fields = quote! {
135 struct InstantiateMsg {
136 pub ownable_id: String,
137 pub package: String,
138 pub nft: Option<NFT>,
139 pub ownable_type: Option<String>,
140 pub network_id: u32,
141 }
142 };
143
144 let default_ast: DeriveInput = syn::parse2(default_instantiate_fields).expect("parse default struct");
145 let default_fields = match default_ast.data {
146 Struct(DataStruct { fields, .. }) => fields,
147 _ => panic!("only structs can accept fields"),
148 };
149
150 let mut input_ast: DeriveInput = syn::parse2(input).expect("parse input struct");
151 let input_fields_data = match &mut input_ast.data {
152 Struct(DataStruct { fields, .. }) => fields,
153 _ => panic!("only structs can accept fields"),
154 };
155
156 if let syn::Fields::Named(FieldsNamed { named, .. }) = input_fields_data {
157 named.extend(default_fields);
158 }
159
160 quote! { #input_ast }
161}
162
163#[proc_macro_attribute]
165pub fn ownables_transfer(metadata: TokenStream, input: TokenStream) -> TokenStream {
166 parse_macro_input!(metadata as Nothing);
167 ownables_transfer_impl(input.into()).into()
168}
169
170#[proc_macro_attribute]
172pub fn ownables_lock(metadata: TokenStream, input: TokenStream) -> TokenStream {
173 parse_macro_input!(metadata as Nothing);
174 extend_enum_with_variants_impl(
175 input.into(),
176 quote! {
177 enum ExecuteMsg {
178 Lock {},
179 }
180 },
181 )
182 .into()
183}
184
185#[proc_macro_attribute]
187pub fn ownables_consume(metadata: TokenStream, input: TokenStream) -> TokenStream {
188 parse_macro_input!(metadata as Nothing);
189 extend_enum_with_variants_impl(
190 input.into(),
191 quote! {
192 enum ExecuteMsg {
193 Consume {},
194 }
195 },
196 )
197 .into()
198}
199
200#[proc_macro_attribute]
202pub fn ownables_attach(metadata: TokenStream, input: TokenStream) -> TokenStream {
203 parse_macro_input!(metadata as Nothing);
204 ownables_attach_impl(input.into()).into()
205}
206
207#[proc_macro_attribute]
209pub fn ownables_close(metadata: TokenStream, input: TokenStream) -> TokenStream {
210 parse_macro_input!(metadata as Nothing);
211 ownables_close_impl(input.into()).into()
212}
213
214#[proc_macro_attribute]
216pub fn ownables_query_metadata(metadata: TokenStream, input: TokenStream) -> TokenStream {
217 parse_macro_input!(metadata as Nothing);
218 extend_enum_with_variants_impl(
219 input.into(),
220 quote! {
221 enum QueryMsg {
222 GetMetadata {},
223 }
224 },
225 )
226 .into()
227}
228
229#[proc_macro_attribute]
231pub fn ownables_query_info(metadata: TokenStream, input: TokenStream) -> TokenStream {
232 parse_macro_input!(metadata as Nothing);
233 ownables_query_info_impl(input.into()).into()
234}
235
236#[proc_macro_attribute]
238pub fn ownables_query_widget_state(metadata: TokenStream, input: TokenStream) -> TokenStream {
239 parse_macro_input!(metadata as Nothing);
240 extend_enum_with_variants_impl(
241 input.into(),
242 quote! {
243 enum QueryMsg {
244 GetWidgetState {},
245 }
246 },
247 )
248 .into()
249}
250
251#[proc_macro_attribute]
253pub fn ownables_query_locked(metadata: TokenStream, input: TokenStream) -> TokenStream {
254 parse_macro_input!(metadata as Nothing);
255 extend_enum_with_variants_impl(
256 input.into(),
257 quote! {
258 enum QueryMsg {
259 IsLocked {},
260 }
261 },
262 )
263 .into()
264}
265
266#[proc_macro_attribute]
268pub fn ownables_query_consumed(metadata: TokenStream, input: TokenStream) -> TokenStream {
269 parse_macro_input!(metadata as Nothing);
270 ownables_query_consumed_impl(input.into()).into()
271}
272
273#[proc_macro_attribute]
275pub fn ownables_query_attachments(metadata: TokenStream, input: TokenStream) -> TokenStream {
276 parse_macro_input!(metadata as Nothing);
277 ownables_query_attachments_impl(input.into()).into()
278}
279
280#[proc_macro_attribute]
282pub fn ownables_query_closed(metadata: TokenStream, input: TokenStream) -> TokenStream {
283 parse_macro_input!(metadata as Nothing);
284 ownables_query_closed_impl(input.into()).into()
285}
286
287#[proc_macro_attribute]
289pub fn ownables_query_consumer_of(metadata: TokenStream, input: TokenStream) -> TokenStream {
290 parse_macro_input!(metadata as Nothing);
291 ownables_query_consumer_of_impl(input.into()).into()
292}
293
294#[proc_macro_attribute]
303pub fn ownables_instantiate_msg(metadata: TokenStream, input: TokenStream) -> TokenStream {
304 parse_macro_input!(metadata as Nothing);
305 ownables_instantiate_msg_impl(input.into()).into()
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use quote::ToTokens;
312 use syn::{Data, Fields};
313
314 fn parse_enum(input: proc_macro2::TokenStream) -> syn::DataEnum {
315 let parsed: DeriveInput = syn::parse2(input).expect("parse enum");
316 match parsed.data {
317 Data::Enum(data) => data,
318 _ => panic!("expected enum"),
319 }
320 }
321
322 fn find_variant<'a>(data: &'a syn::DataEnum, name: &str) -> &'a Variant {
323 data.variants
324 .iter()
325 .find(|variant| variant.ident == name)
326 .unwrap_or_else(|| panic!("missing variant {name}"))
327 }
328
329 fn parse_struct(input: proc_macro2::TokenStream) -> syn::DataStruct {
330 let parsed: DeriveInput = syn::parse2(input).expect("parse struct");
331 match parsed.data {
332 Data::Struct(data) => data,
333 _ => panic!("expected struct"),
334 }
335 }
336
337 #[test]
338 fn ownables_attach_injects_attachment_input_vector() {
339 let data = parse_enum(ownables_attach_impl(quote! {
340 enum ExecuteMsg {
341 Existing {},
342 }
343 }));
344 let variant = find_variant(&data, "Attach");
345
346 match &variant.fields {
347 Fields::Named(fields) => {
348 assert_eq!(fields.named.len(), 1);
349 let field = &fields.named[0];
350 assert_eq!(field.ident.as_ref().expect("named field"), "attachments");
351 assert_eq!(
352 field.ty.to_token_stream().to_string(),
353 "Vec < AttachmentInput >"
354 );
355 }
356 _ => panic!("Attach should have named fields"),
357 }
358 }
359
360 #[test]
361 fn ownables_close_injects_close_variant() {
362 let data = parse_enum(ownables_close_impl(quote! {
363 enum ExecuteMsg {
364 Existing {},
365 }
366 }));
367 let variant = find_variant(&data, "Close");
368
369 match &variant.fields {
370 Fields::Named(fields) => assert!(fields.named.is_empty()),
371 _ => panic!("Close should use named fields"),
372 }
373 }
374
375 #[test]
376 fn ownables_query_attachments_injects_get_attachments_variant() {
377 let data = parse_enum(ownables_query_attachments_impl(quote! {
378 enum QueryMsg {
379 Existing {},
380 }
381 }));
382 let variant = find_variant(&data, "GetAttachments");
383
384 match &variant.fields {
385 Fields::Named(fields) => assert!(fields.named.is_empty()),
386 _ => panic!("GetAttachments should use named fields"),
387 }
388 }
389
390 #[test]
391 fn ownables_query_closed_injects_is_closed_variant() {
392 let data = parse_enum(ownables_query_closed_impl(quote! {
393 enum QueryMsg {
394 Existing {},
395 }
396 }));
397 let variant = find_variant(&data, "IsClosed");
398
399 match &variant.fields {
400 Fields::Named(fields) => assert!(fields.named.is_empty()),
401 _ => panic!("IsClosed should use named fields"),
402 }
403 }
404
405 #[test]
406 fn ownables_transfer_still_injects_addr_field() {
407 let data = parse_enum(ownables_transfer_impl(quote! {
408 enum ExecuteMsg {
409 Existing {},
410 }
411 }));
412 let variant = find_variant(&data, "Transfer");
413
414 match &variant.fields {
415 Fields::Named(fields) => {
416 assert_eq!(fields.named.len(), 1);
417 let field = &fields.named[0];
418 assert_eq!(field.ident.as_ref().expect("named field"), "to");
419 assert_eq!(field.ty.to_token_stream().to_string(), "Addr");
420 }
421 _ => panic!("Transfer should have named fields"),
422 }
423 }
424
425 #[test]
426 fn ownables_query_info_still_injects_get_info_variant() {
427 let data = parse_enum(ownables_query_info_impl(quote! {
428 enum QueryMsg {
429 Existing {},
430 }
431 }));
432 let variant = find_variant(&data, "GetInfo");
433
434 match &variant.fields {
435 Fields::Named(fields) => assert!(fields.named.is_empty()),
436 _ => panic!("GetInfo should use named fields"),
437 }
438 }
439
440 #[test]
441 fn ownables_query_consumed_still_injects_is_consumed_variant() {
442 let data = parse_enum(ownables_query_consumed_impl(quote! {
443 enum QueryMsg {
444 Existing {},
445 }
446 }));
447 let variant = find_variant(&data, "IsConsumed");
448
449 match &variant.fields {
450 Fields::Named(fields) => assert!(fields.named.is_empty()),
451 _ => panic!("IsConsumed should use named fields"),
452 }
453 }
454
455 #[test]
456 fn ownables_query_consumer_of_still_injects_expected_fields() {
457 let data = parse_enum(ownables_query_consumer_of_impl(quote! {
458 enum QueryMsg {
459 Existing {},
460 }
461 }));
462 let variant = find_variant(&data, "IsConsumerOf");
463
464 match &variant.fields {
465 Fields::Named(fields) => {
466 assert_eq!(fields.named.len(), 2);
467 assert_eq!(fields.named[0].ident.as_ref().expect("issuer"), "issuer");
468 assert_eq!(
469 fields.named[0].ty.to_token_stream().to_string(),
470 "Addr"
471 );
472 assert_eq!(
473 fields.named[1].ident.as_ref().expect("consumable_type"),
474 "consumable_type"
475 );
476 assert_eq!(
477 fields.named[1].ty.to_token_stream().to_string(),
478 "String"
479 );
480 }
481 _ => panic!("IsConsumerOf should use named fields"),
482 }
483 }
484
485 #[test]
486 fn ownables_instantiate_msg_still_injects_standard_fields() {
487 let data = parse_struct(ownables_instantiate_msg_impl(quote! {
488 struct InstantiateMsg {
489 pub name: String,
490 }
491 }));
492
493 match data.fields {
494 Fields::Named(fields) => {
495 let names: Vec<String> = fields
496 .named
497 .iter()
498 .map(|field| field.ident.as_ref().expect("named field").to_string())
499 .collect();
500 for required in [
501 "name",
502 "ownable_id",
503 "ownable_type",
504 "network_id",
505 "package",
506 "nft",
507 ] {
508 assert!(names.iter().any(|name| name == required), "missing {required}");
509 }
510 }
511 _ => panic!("InstantiateMsg should use named fields"),
512 }
513 }
514}