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), false,
70 outer_scopes.clone(),
71 ); 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 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 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 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 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 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
616fn 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 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 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}