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