Skip to main content

ownable_std_macros/
lib.rs

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/// Adds `Transfer { to: Addr }` to an `ExecuteMsg` enum.
164#[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/// Adds `Lock {}` to an `ExecuteMsg` enum.
171#[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/// Adds `Consume {}` to an `ExecuteMsg` enum.
186#[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/// Adds `Attach { attachments: Vec<AttachmentInput> }` to an `ExecuteMsg` enum.
201#[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/// Adds `Close {}` to an `ExecuteMsg` enum.
208#[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/// Adds `GetMetadata {}` to a `QueryMsg` enum.
215#[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/// Adds `GetInfo {}` to a `QueryMsg` enum.
230#[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/// Adds `GetWidgetState {}` to a `QueryMsg` enum.
237#[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/// Adds `IsLocked {}` to a `QueryMsg` enum.
252#[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/// Adds `IsConsumed {}` to a `QueryMsg` enum.
267#[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/// Adds `GetAttachments {}` to a `QueryMsg` enum.
274#[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/// Adds `IsClosed {}` to a `QueryMsg` enum.
281#[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/// Adds `IsConsumerOf { issuer: Addr, consumable_type: String }` to a `QueryMsg` enum.
288#[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/// Adds default ownables fields to an `InstantiateMsg` struct:
295/// InstantiateMsg {
296///     pub ownable_id: String,
297///     pub package: String,
298///     pub nft: Option<NFT>,
299///     pub ownable_type: Option<String>,
300///     pub network_id: u8,
301/// }
302#[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}