1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{FnArg, ItemFn, PatType, Receiver, ReturnType, Type, parse_macro_input};
4
5#[proc_macro_attribute]
6pub fn cli(attr: TokenStream, item: TokenStream) -> TokenStream {
7 let attr_tokens: proc_macro2::TokenStream = attr.into();
8
9 if !attr_tokens.is_empty() {
10 return syn::Error::new_spanned(attr_tokens, "cli attribute does not take any arguments")
11 .into_compile_error()
12 .into();
13 }
14
15 let input_fn = parse_macro_input!(item as ItemFn);
16
17 if input_fn.sig.asyncness.is_some() {
18 return syn::Error::new_spanned(&input_fn.sig, "async functions are not supported")
19 .into_compile_error()
20 .into();
21 }
22
23 let mut inputs = input_fn.sig.inputs.iter();
24 match inputs.next() {
25 Some(FnArg::Receiver(Receiver {
26 reference: Some(_),
27 mutability: _,
28 attrs,
29 ..
30 })) if attrs.is_empty() => {}
31 Some(FnArg::Receiver(_)) => {
32 return syn::Error::new_spanned(
33 &input_fn.sig,
34 "cli strategy methods must use an attribute-free &self receiver",
35 )
36 .into_compile_error()
37 .into();
38 }
39 _ => {
40 return syn::Error::new_spanned(
41 &input_fn.sig,
42 "cli strategy functions must match CommandStrategy::execute with an &self receiver and options, arguments, and subcommands arguments",
43 )
44 .into_compile_error()
45 .into();
46 }
47 }
48
49 let options_pat = match inputs.next() {
50 Some(FnArg::Typed(PatType { pat, ty, .. })) => {
51 match ty.as_ref() {
52 Type::Path(path)
53 if path
54 .path
55 .segments
56 .last()
57 .is_some_and(|segment| segment.ident == "Vec") => {}
58 _ => {
59 return syn::Error::new_spanned(
60 ty,
61 "cli strategy functions must accept a Vec<String> options argument",
62 )
63 .into_compile_error()
64 .into();
65 }
66 }
67
68 pat
69 }
70 _ => {
71 return syn::Error::new_spanned(
72 &input_fn.sig,
73 "cli strategy functions must accept an options Vec<String> argument",
74 )
75 .into_compile_error()
76 .into();
77 }
78 };
79
80 let arguments_pat = match inputs.next() {
81 Some(FnArg::Typed(PatType { pat, ty, .. })) => {
82 match ty.as_ref() {
83 Type::Path(path)
84 if path
85 .path
86 .segments
87 .last()
88 .is_some_and(|segment| segment.ident == "HashMap") => {}
89 _ => {
90 return syn::Error::new_spanned(
91 ty,
92 "cli strategy functions must accept a HashMap<String, String> arguments argument",
93 )
94 .into_compile_error()
95 .into();
96 }
97 }
98
99 pat
100 }
101 _ => {
102 return syn::Error::new_spanned(
103 &input_fn.sig,
104 "cli strategy functions must accept an arguments HashMap<String, String> argument",
105 )
106 .into_compile_error()
107 .into();
108 }
109 };
110
111 let subcommands_pat = match inputs.next() {
112 Some(FnArg::Typed(PatType { pat, ty, .. })) => {
113 if inputs.next().is_some() {
114 return syn::Error::new_spanned(
115 &input_fn.sig,
116 "cli strategy functions must accept exactly three parsed invocation arguments",
117 )
118 .into_compile_error()
119 .into();
120 }
121
122 match ty.as_ref() {
123 Type::Path(path)
124 if path
125 .path
126 .segments
127 .last()
128 .is_some_and(|segment| segment.ident == "Vec") => {}
129 _ => {
130 return syn::Error::new_spanned(
131 ty,
132 "cli strategy functions must accept a Vec<String> subcommands argument",
133 )
134 .into_compile_error()
135 .into();
136 }
137 }
138
139 pat
140 }
141 _ => {
142 return syn::Error::new_spanned(
143 &input_fn.sig,
144 "cli strategy functions must accept a subcommands Vec<String> argument",
145 )
146 .into_compile_error()
147 .into();
148 }
149 };
150
151 match &input_fn.sig.output {
152 ReturnType::Type(_, ty) => match ty.as_ref() {
153 Type::Path(path)
154 if path.path.segments.len() == 1 && path.path.segments[0].ident == "Result" => {}
155 _ => {
156 return syn::Error::new_spanned(
157 ty,
158 "cli strategy functions must return Result<(), cmdkit::StrategyError>",
159 )
160 .into_compile_error()
161 .into();
162 }
163 },
164 ReturnType::Default => {
165 return syn::Error::new_spanned(
166 &input_fn.sig,
167 "cli strategy functions must return Result<(), cmdkit::StrategyError>",
168 )
169 .into_compile_error()
170 .into();
171 }
172 }
173
174 let fn_ident = &input_fn.sig.ident;
175 let vis = &input_fn.vis;
176 let strategy_ident = format_ident!("{}", to_pascal(&fn_ident.to_string()));
177 let factory_ident = format_ident!("{}_strategy", fn_ident);
178 let attrs = &input_fn.attrs;
179 let body = &input_fn.block;
180
181 let expanded = quote! {
182 #(#attrs)*
183 #vis struct #strategy_ident;
184
185 impl #strategy_ident {
186 #vis fn new() -> Self {
187 Self
188 }
189 }
190
191 impl ::cmdkit::CommandStrategy for #strategy_ident {
192 fn execute(
193 &self,
194 #options_pat: Vec<String>,
195 #arguments_pat: ::std::collections::HashMap<String, String>,
196 #subcommands_pat: Vec<String>,
197 ) -> Result<(), ::cmdkit::StrategyError> {
198 #body
199 }
200 }
201
202 #vis fn #factory_ident() -> #strategy_ident {
203 #strategy_ident::new()
204 }
205 };
206
207 expanded.into()
208}
209
210fn to_pascal(s: &str) -> String {
211 let mut out = String::new();
212 for part in s.split('_') {
213 if part.is_empty() {
214 continue;
215 }
216 let mut chars = part.chars();
217 if let Some(first) = chars.next() {
218 out.extend(first.to_uppercase());
219 out.push_str(chars.as_str());
220 }
221 }
222 out
223}