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