valence_command_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Ident, TokenTree};
3use quote::{format_ident, quote};
4use syn::{parse_macro_input, Attribute, Data, DeriveInput, Error, Expr, Fields, Meta, Result};
5
6#[proc_macro_derive(Command, attributes(command, scopes, paths))]
7pub fn derive_command(input: TokenStream) -> TokenStream {
8    let input = parse_macro_input!(input as DeriveInput);
9    match command(input) {
10        Ok(expansion) => expansion,
11        Err(err) => err.to_compile_error().into(),
12    }
13}
14
15fn command(input: DeriveInput) -> Result<TokenStream> {
16    let input_name = input.ident;
17
18    let outer_scopes = input
19        .attrs
20        .iter()
21        .find_map(|attr| get_lit_list_attr(attr, "scopes"))
22        .unwrap_or_default();
23
24    match input.data {
25        Data::Enum(data_enum) => {
26            let Some(mut alias_paths) = input.attrs.iter().find_map(parse_path) else {
27                return Err(Error::new_spanned(
28                    input_name,
29                    "No paths attribute found for command enum",
30                ));
31            };
32
33            let base_path = alias_paths.remove(0);
34
35            let fields = &data_enum.variants;
36            let mut paths = Vec::new();
37
38            for variant in fields {
39                for attr in &variant.attrs {
40                    if let Some(attr_paths) = parse_path(attr) {
41                        paths.push((attr_paths, variant.fields.clone(), variant.ident.clone()));
42                    }
43                }
44            }
45
46            let mut expanded_nodes = Vec::new();
47
48            for (paths, fields, variant_ident) in paths {
49                expanded_nodes.push({
50                    let processed = process_paths_enum(
51                        &input_name,
52                        paths,
53                        &fields,
54                        variant_ident.clone(),
55                        true,
56                        outer_scopes.clone(),
57                    );
58                    quote! { #processed; }
59                });
60            }
61
62            let base_command_expansion = {
63                let processed = process_paths_enum(
64                    &input_name,
65                    vec![base_path],
66                    &Fields::Unit,
67                    format_ident!("{}Root", input_name), // this is more of placeholder
68                    // (should never be used)
69                    false,
70                    outer_scopes.clone(),
71                ); // this will error if the base path has args
72                let mut expanded_main_command = quote! {
73                    let command_root_node = #processed
74                };
75
76                if !outer_scopes.is_empty() {
77                    expanded_main_command = quote! {
78                        #expanded_main_command
79                            .with_scopes(vec![#(#outer_scopes),*])
80                    }
81                }
82
83                quote! {
84                    #expanded_main_command.id();
85                }
86            };
87
88            let command_alias_expansion = {
89                let mut alias_expansion = quote! {};
90                for path in alias_paths {
91                    let processed = process_paths_enum(
92                        &input_name,
93                        vec![path],
94                        &Fields::Unit,
95                        format_ident!("{}Root", input_name),
96                        false,
97                        outer_scopes.clone(),
98                    );
99
100                    alias_expansion = quote! {
101                        #alias_expansion
102
103                        #processed
104                            .redirect_to(command_root_node)
105                    };
106
107                    if !outer_scopes.is_empty() {
108                        alias_expansion = quote! {
109                            #alias_expansion
110                                .with_scopes(vec![#(#outer_scopes),*])
111                        }
112                    }
113
114                    alias_expansion = quote! {
115                        #alias_expansion;
116                    }
117                }
118
119                alias_expansion
120            };
121
122            let _new_struct = format_ident!("{}Command", input_name);
123
124            Ok(TokenStream::from(quote! {
125
126                impl valence::command::Command for #input_name {
127                    fn assemble_graph(command_graph: &mut valence::command::graph::CommandGraphBuilder<Self>) {
128                        use valence::command::parsers::CommandArg;
129                        #base_command_expansion
130
131                        #command_alias_expansion
132
133                        #(#expanded_nodes)*
134                    }
135                }
136            }))
137        }
138        Data::Struct(x) => {
139            let mut paths = Vec::new();
140
141            for attr in &input.attrs {
142                if let Some(attr_paths) = parse_path(attr) {
143                    paths.push(attr_paths);
144                }
145            }
146
147            let mut expanded_nodes = Vec::new();
148
149            for path in paths {
150                expanded_nodes.push({
151                    let mut processed =
152                        process_paths_struct(&input_name, path, &x.fields, outer_scopes.clone());
153                    // add scopes
154
155                    if !outer_scopes.is_empty() {
156                        processed = quote! {
157                            #processed
158                                .with_scopes(vec![#(#outer_scopes),*])
159                        }
160                    }
161
162                    quote! { #processed; }
163                });
164            }
165
166            Ok(TokenStream::from(quote! {
167
168                impl valence::command::Command for #input_name {
169                    fn assemble_graph(command_graph: &mut valence::command::graph::CommandGraphBuilder<Self>) {
170                        use valence::command::parsers::CommandArg;
171                        #(#expanded_nodes)*
172                    }
173                }
174            }))
175        }
176        Data::Union(x) => Err(Error::new_spanned(
177            x.union_token,
178            "Command enum must be an enum, not a union",
179        )),
180    }
181}
182
183fn process_paths_enum(
184    enum_name: &Ident,
185    paths: Vec<(Vec<CommandArg>, bool)>,
186    fields: &Fields,
187    variant_ident: Ident,
188    executables: bool,
189    outer_scopes: Vec<String>,
190) -> proc_macro2::TokenStream {
191    let mut inner_expansion = quote! {};
192    let mut first = true;
193
194    for path in paths {
195        if !first {
196            inner_expansion = if executables && !path.1 {
197                quote! {
198                        #inner_expansion;
199
200                        command_graph.at(command_root_node)
201                }
202            } else {
203                quote! {
204                    #inner_expansion;
205
206                    command_graph.root()
207                }
208            };
209        } else {
210            inner_expansion = if executables && !path.1 {
211                quote! {
212                    command_graph.at(command_root_node)
213                }
214            } else {
215                quote! {
216                    command_graph.root()
217                }
218            };
219
220            first = false;
221        }
222
223        let path = path.0;
224
225        let mut final_executable = Vec::new();
226        for (i, arg) in path.iter().enumerate() {
227            match arg {
228                CommandArg::Literal(lit) => {
229                    inner_expansion = quote! {
230                        #inner_expansion.literal(#lit)
231                    };
232                    if !outer_scopes.is_empty() {
233                        inner_expansion = quote! {
234                            #inner_expansion.with_scopes(vec![#(#outer_scopes),*])
235                        }
236                    }
237                    if executables && i == path.len() - 1 {
238                        inner_expansion = quote! {
239                            #inner_expansion
240                                .with_executable(|s| #enum_name::#variant_ident{#(#final_executable,)*})
241                        };
242                    }
243                }
244                CommandArg::Required(ident) => {
245                    let field_type = &fields
246                        .iter()
247                        .find(|field| field.ident.as_ref().unwrap() == ident)
248                        .expect("Required arg not found")
249                        .ty;
250                    let ident_string = ident.to_string();
251
252                    inner_expansion = quote! {
253                        #inner_expansion
254                            .argument(#ident_string)
255                            .with_parser::<#field_type>()
256                    };
257
258                    final_executable.push(quote! {
259                        #ident: #field_type::parse_arg(s).unwrap()
260                    });
261
262                    if i == path.len() - 1 {
263                        inner_expansion = quote! {
264                            #inner_expansion
265                                .with_executable(|s| {
266                                    #enum_name::#variant_ident {
267                                        #(#final_executable,)*
268                                    }
269                                })
270                        };
271                    }
272                }
273                CommandArg::Optional(ident) => {
274                    let field_type = &fields
275                        .iter()
276                        .find(|field| field.ident.as_ref().unwrap() == ident)
277                        .expect("Optional arg not found")
278                        .ty;
279                    let so_far_ident = format_ident!("graph_til_{}", ident);
280
281                    // get what is inside the Option<...>
282                    let option_inner = match field_type {
283                        syn::Type::Path(type_path) => {
284                            let path = &type_path.path;
285                            if path.segments.len() != 1 {
286                                return Error::new_spanned(
287                                    path,
288                                    "Option type must be a single path segment",
289                                )
290                                .into_compile_error();
291                            }
292                            let segment = &path.segments.first().unwrap();
293                            if segment.ident != "Option" {
294                                return Error::new_spanned(
295                                    &segment.ident,
296                                    "Option type must be a option",
297                                )
298                                .into_compile_error();
299                            }
300                            match &segment.arguments {
301                                syn::PathArguments::AngleBracketed(angle_bracketed) => {
302                                    if angle_bracketed.args.len() != 1 {
303                                        return Error::new_spanned(
304                                            angle_bracketed,
305                                            "Option type must have a single generic argument",
306                                        )
307                                        .into_compile_error();
308                                    }
309                                    match angle_bracketed.args.first().unwrap() {
310                                        syn::GenericArgument::Type(generic_type) => generic_type,
311                                        _ => {
312                                            return Error::new_spanned(
313                                                angle_bracketed,
314                                                "Option type must have a single generic argument",
315                                            )
316                                            .into_compile_error();
317                                        }
318                                    }
319                                }
320                                _ => {
321                                    return Error::new_spanned(
322                                        segment,
323                                        "Option type must have a single generic argument",
324                                    )
325                                    .into_compile_error();
326                                }
327                            }
328                        }
329                        _ => {
330                            return Error::new_spanned(
331                                field_type,
332                                "Option type must be a single path segment",
333                            )
334                            .into_compile_error();
335                        }
336                    };
337
338                    let ident_string = ident.to_string();
339
340                    // find the ident of all following optional args
341                    let mut next_optional_args = Vec::new();
342                    for next_arg in path.iter().skip(i + 1) {
343                        match next_arg {
344                            CommandArg::Optional(ident) => next_optional_args.push(ident),
345                            _ => {
346                                return Error::new_spanned(
347                                    variant_ident,
348                                    "Only optional args can follow an optional arg",
349                                )
350                                .into_compile_error();
351                            }
352                        }
353                    }
354
355                    inner_expansion = quote! {
356                        let #so_far_ident = {#inner_expansion
357                            .with_executable(|s| {
358                                #enum_name::#variant_ident {
359                                    #(#final_executable,)*
360                                    #ident: None,
361                                    #(#next_optional_args: None,)*
362                                }
363                            })
364                            .id()};
365
366                        command_graph.at(#so_far_ident)
367                            .argument(#ident_string)
368                            .with_parser::<#option_inner>()
369                    };
370
371                    final_executable.push(quote! {
372                        #ident: Some(#option_inner::parse_arg(s).unwrap())
373                    });
374
375                    if i == path.len() - 1 {
376                        inner_expansion = quote! {
377                            #inner_expansion
378                                .with_executable(|s| {
379                                    #enum_name::#variant_ident {
380                                        #(#final_executable,)*
381                                    }
382                                })
383                        };
384                    }
385                }
386            }
387        }
388    }
389    quote!(#inner_expansion)
390}
391
392fn process_paths_struct(
393    struct_name: &Ident,
394    paths: Vec<(Vec<CommandArg>, bool)>,
395    fields: &Fields,
396    outer_scopes: Vec<String>,
397) -> proc_macro2::TokenStream {
398    let mut inner_expansion = quote! {};
399    let mut first = true;
400
401    for path in paths {
402        if !first {
403            inner_expansion = quote! {
404                #inner_expansion;
405
406                command_graph.root()
407            };
408        } else {
409            inner_expansion = quote! {
410                command_graph.root()
411            };
412
413            first = false;
414        }
415
416        let path = path.0;
417
418        let mut final_executable = Vec::new();
419        let mut path_first = true;
420        for (i, arg) in path.iter().enumerate() {
421            match arg {
422                CommandArg::Literal(lit) => {
423                    inner_expansion = quote! {
424                        #inner_expansion.literal(#lit)
425
426                    };
427                    if i == path.len() - 1 {
428                        inner_expansion = quote! {
429                            #inner_expansion
430                                .with_executable(|s| #struct_name{#(#final_executable,)*})
431                        };
432                    }
433
434                    if path_first {
435                        if !outer_scopes.is_empty() {
436                            inner_expansion = quote! {
437                                #inner_expansion.with_scopes(vec![#(#outer_scopes),*])
438                            }
439                        }
440                        path_first = false;
441                    }
442                }
443                CommandArg::Required(ident) => {
444                    let field_type = &fields
445                        .iter()
446                        .find(|field| field.ident.as_ref().unwrap() == ident)
447                        .expect("Required arg not found")
448                        .ty;
449                    let ident_string = ident.to_string();
450
451                    inner_expansion = quote! {
452                        #inner_expansion
453                            .argument(#ident_string)
454                            .with_parser::<#field_type>()
455                    };
456
457                    final_executable.push(quote! {
458                        #ident: #field_type::parse_arg(s).unwrap()
459                    });
460
461                    if i == path.len() - 1 {
462                        inner_expansion = quote! {
463                            #inner_expansion
464                                .with_executable(|s| {
465                                    #struct_name {
466                                        #(#final_executable,)*
467                                    }
468                                })
469                        };
470                    }
471
472                    if path_first {
473                        if !outer_scopes.is_empty() {
474                            inner_expansion = quote! {
475                                #inner_expansion.with_scopes(vec![#(#outer_scopes),*])
476                            };
477                        }
478                        path_first = false;
479                    }
480                }
481                CommandArg::Optional(ident) => {
482                    let field_type = &fields
483                        .iter()
484                        .find(|field| field.ident.as_ref().unwrap() == ident)
485                        .expect("Optional arg not found")
486                        .ty;
487                    let so_far_ident = format_ident!("graph_til_{}", ident);
488
489                    // get what is inside the Option<...>
490                    let option_inner = match field_type {
491                        syn::Type::Path(type_path) => {
492                            let path = &type_path.path;
493                            if path.segments.len() != 1 {
494                                return Error::new_spanned(
495                                    path,
496                                    "Option type must be a single path segment",
497                                )
498                                .into_compile_error();
499                            }
500                            let segment = &path.segments.first().unwrap();
501                            if segment.ident != "Option" {
502                                return Error::new_spanned(
503                                    &segment.ident,
504                                    "Option type must be a option",
505                                )
506                                .into_compile_error();
507                            }
508                            match &segment.arguments {
509                                syn::PathArguments::AngleBracketed(angle_bracketed) => {
510                                    if angle_bracketed.args.len() != 1 {
511                                        return Error::new_spanned(
512                                            angle_bracketed,
513                                            "Option type must have a single generic argument",
514                                        )
515                                        .into_compile_error();
516                                    }
517                                    match angle_bracketed.args.first().unwrap() {
518                                        syn::GenericArgument::Type(generic_type) => generic_type,
519                                        _ => {
520                                            return Error::new_spanned(
521                                                angle_bracketed,
522                                                "Option type must have a single generic argument",
523                                            )
524                                            .into_compile_error();
525                                        }
526                                    }
527                                }
528                                _ => {
529                                    return Error::new_spanned(
530                                        segment,
531                                        "Option type must have a single generic argument",
532                                    )
533                                    .into_compile_error();
534                                }
535                            }
536                        }
537                        _ => {
538                            return Error::new_spanned(
539                                field_type,
540                                "Option type must be a single path segment",
541                            )
542                            .into_compile_error();
543                        }
544                    };
545
546                    let ident_string = ident.to_string();
547
548                    // find the ident of all following optional args
549                    let mut next_optional_args = Vec::new();
550                    for next_arg in path.iter().skip(i + 1) {
551                        match next_arg {
552                            CommandArg::Optional(ident) => next_optional_args.push(ident),
553                            _ => {
554                                return Error::new_spanned(
555                                    struct_name,
556                                    "Only optional args can follow an optional arg",
557                                )
558                                .into_compile_error();
559                            }
560                        }
561                    }
562
563                    inner_expansion = quote! {
564                        let #so_far_ident = {#inner_expansion
565                            .with_executable(|s| {
566                                #struct_name {
567                                    #(#final_executable,)*
568                                    #ident: None,
569                                    #(#next_optional_args: None,)*
570                                }
571                            })
572                            .id()};
573
574                        command_graph.at(#so_far_ident)
575                            .argument(#ident_string)
576                            .with_parser::<#option_inner>()
577                    };
578
579                    final_executable.push(quote! {
580                        #ident: Some(#option_inner::parse_arg(s).unwrap())
581                    });
582
583                    if i == path.len() - 1 {
584                        inner_expansion = quote! {
585                            #inner_expansion
586                                .with_executable(|s| {
587                                    #struct_name {
588                                        #(#final_executable,)*
589                                    }
590                                })
591                        };
592                    }
593
594                    if path_first {
595                        if !outer_scopes.is_empty() {
596                            inner_expansion = quote! {
597                                #inner_expansion.with_scopes(vec![#(#outer_scopes),*])
598                            };
599                        }
600                        path_first = false;
601                    }
602                }
603            }
604        }
605    }
606    quote!(#inner_expansion)
607}
608
609#[derive(Debug)]
610enum CommandArg {
611    Required(Ident),
612    Optional(Ident),
613    Literal(String),
614}
615
616// example input: #[paths = "strawberry {0?}"]
617// example output: [CommandArg::Literal("Strawberry"), CommandArg::Optional(0)]
618fn parse_path(path: &Attribute) -> Option<Vec<(Vec<CommandArg>, bool)>> {
619    let path_strings: Vec<String> = get_lit_list_attr(path, "paths")?;
620
621    let mut paths = Vec::new();
622    // we now have the path as a string eg "strawberry {0?}"
623    // the first word is a literal
624    // the next word is an optional arg with the index 0
625    for path_str in path_strings {
626        let mut args = Vec::new();
627        let at_root = path_str.starts_with("{/}");
628
629        for word in path_str.split_whitespace().skip(usize::from(at_root)) {
630            if word.starts_with('{') && word.ends_with('}') {
631                if word.ends_with("?}") {
632                    args.push(CommandArg::Optional(format_ident!(
633                        "{}",
634                        word[1..word.len() - 2].to_owned()
635                    )));
636                } else {
637                    args.push(CommandArg::Required(format_ident!(
638                        "{}",
639                        word[1..word.len() - 1].to_owned()
640                    )));
641                }
642            } else {
643                args.push(CommandArg::Literal(word.to_owned()));
644            }
645        }
646        paths.push((args, at_root));
647    }
648
649    Some(paths)
650}
651
652fn get_lit_list_attr(attr: &Attribute, ident: &str) -> Option<Vec<String>> {
653    match &attr.meta {
654        Meta::NameValue(key_value) => {
655            if !key_value.path.is_ident(ident) {
656                return None;
657            }
658
659            match &key_value.value {
660                Expr::Lit(lit) => match &lit.lit {
661                    syn::Lit::Str(lit_str) => Some(vec![lit_str.value()]),
662                    _ => None,
663                },
664                _ => None,
665            }
666        }
667        Meta::List(list) => {
668            if !list.path.is_ident(ident) {
669                return None;
670            }
671
672            let mut path_strings = Vec::new();
673            // parse as array with strings
674            let mut comma_next = false;
675            for token in list.tokens.clone() {
676                match token {
677                    TokenTree::Literal(lit) => {
678                        if comma_next {
679                            return None;
680                        }
681                        let lit_str = lit.to_string();
682                        path_strings.push(
683                            lit_str
684                                .strip_prefix('"')
685                                .unwrap()
686                                .strip_suffix('"')
687                                .unwrap()
688                                .to_owned(),
689                        );
690                        comma_next = true;
691                    }
692                    TokenTree::Punct(punct) => {
693                        if punct.as_char() != ',' || !comma_next {
694                            return None;
695                        }
696                        comma_next = false;
697                    }
698                    _ => return None,
699                }
700            }
701            Some(path_strings)
702        }
703        Meta::Path(_) => None,
704    }
705}