1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{ItemFn, parse_macro_input};
4
5#[proc_macro_attribute]
22pub fn inspectable(attr: TokenStream, item: TokenStream) -> TokenStream {
23 let input = parse_macro_input!(item as ItemFn);
24 let attrs = parse_attrs(attr);
25
26 let fn_name = &input.sig.ident;
27 let fn_name_str = fn_name.to_string();
28 let schema_fn_name = syn::Ident::new(&format!("{fn_name_str}__schema"), fn_name.span());
29 let is_async = input.sig.asyncness.is_some();
30
31 let description = attrs
32 .description
33 .unwrap_or_else(|| fn_name_str.replace('_', " "));
34
35 let args_info = extract_args(&input.sig);
36 let arg_tokens: Vec<_> = args_info
37 .iter()
38 .map(|(name, type_str, required)| {
39 quote! {
40 victauri_core::registry::CommandArg {
41 name: #name.to_string(),
42 type_name: #type_str.to_string(),
43 required: #required,
44 schema: None,
45 }
46 }
47 })
48 .collect();
49
50 let return_type = extract_return_type(&input.sig);
51
52 let intent_token = match &attrs.intent {
53 Some(i) => quote! { Some(#i.to_string()) },
54 None => quote! { None },
55 };
56
57 let category_token = match &attrs.category {
58 Some(c) => quote! { Some(#c.to_string()) },
59 None => quote! { None },
60 };
61
62 let example_tokens: Vec<_> = attrs
63 .examples
64 .iter()
65 .map(|e| quote! { #e.to_string() })
66 .collect();
67
68 let expanded = quote! {
69 #input
70
71 #[allow(dead_code, non_snake_case)]
72 fn #schema_fn_name() -> victauri_core::registry::CommandInfo {
73 victauri_core::registry::CommandInfo {
74 name: #fn_name_str.to_string(),
75 plugin: None,
76 description: Some(#description.to_string()),
77 args: vec![#(#arg_tokens),*],
78 return_type: Some(#return_type.to_string()),
79 is_async: #is_async,
80 intent: #intent_token,
81 category: #category_token,
82 examples: vec![#(#example_tokens),*],
83 }
84 }
85 };
86
87 TokenStream::from(expanded)
88}
89
90struct InspectableAttrs {
91 description: Option<String>,
92 intent: Option<String>,
93 category: Option<String>,
94 examples: Vec<String>,
95}
96
97fn parse_attrs(attr: TokenStream) -> InspectableAttrs {
98 let mut attrs = InspectableAttrs {
99 description: None,
100 intent: None,
101 category: None,
102 examples: Vec::new(),
103 };
104
105 let parser = syn::meta::parser(|meta| {
106 if meta.path.is_ident("description") {
107 attrs.description = Some(meta.value()?.parse::<syn::LitStr>()?.value());
108 } else if meta.path.is_ident("intent") {
109 attrs.intent = Some(meta.value()?.parse::<syn::LitStr>()?.value());
110 } else if meta.path.is_ident("category") {
111 attrs.category = Some(meta.value()?.parse::<syn::LitStr>()?.value());
112 } else if meta.path.is_ident("example") {
113 attrs
114 .examples
115 .push(meta.value()?.parse::<syn::LitStr>()?.value());
116 } else {
117 return Err(meta.error("unknown #[inspectable] attribute"));
118 }
119 Ok(())
120 });
121
122 syn::parse::Parser::parse(parser, attr).expect("failed to parse #[inspectable] attributes");
123 attrs
124}
125
126fn extract_args(sig: &syn::Signature) -> Vec<(String, String, bool)> {
127 sig.inputs
128 .iter()
129 .filter_map(|arg| {
130 if let syn::FnArg::Typed(pat_type) = arg {
131 let name = match &*pat_type.pat {
132 syn::Pat::Ident(ident) => ident.ident.to_string(),
133 _ => return None,
134 };
135
136 let ty = &*pat_type.ty;
138 let type_str = quote!(#ty).to_string();
139 if type_str.contains("AppHandle")
140 || type_str.contains("State")
141 || type_str.contains("Window")
142 || type_str.contains("Webview")
143 {
144 return None;
145 }
146
147 let is_option = type_str.contains("Option");
148 let type_name = type_str;
149
150 Some((name, type_name, !is_option))
151 } else {
152 None
153 }
154 })
155 .collect()
156}
157
158fn extract_return_type(sig: &syn::Signature) -> String {
159 match &sig.output {
160 syn::ReturnType::Default => "()".to_string(),
161 syn::ReturnType::Type(_, ty) => quote!(#ty).to_string(),
162 }
163}