zbus_lockstep_macros/
lib.rs1#![doc(html_root_url = "https://docs.rs/zbus-lockstep-macros/0.5.1")]
5
6type Result<T> = std::result::Result<T, syn::Error>;
7
8use std::{collections::HashMap, path::PathBuf};
9
10use proc_macro::TokenStream;
11use quote::quote;
12use syn::{parse::ParseStream, parse_macro_input, DeriveInput, Ident, LitStr, Token};
13
14#[proc_macro_attribute]
101pub fn validate(args: TokenStream, input: TokenStream) -> TokenStream {
102 let args = parse_macro_input!(args as ValidateArgs);
104
105 let item = parse_macro_input!(input as DeriveInput);
107 let item_name = item.ident.to_string();
108
109 let xml_str = args.xml.as_ref().and_then(|p| p.to_str());
110
111 let xml = match zbus_lockstep::resolve_xml_path(xml_str) {
112 Ok(xml) => xml,
113 Err(e) => {
114 return syn::Error::new(
115 proc_macro2::Span::call_site(),
116 format!("Failed to resolve XML path: {e}"),
117 )
118 .to_compile_error()
119 .into();
120 }
121 };
122
123 let mut xml_files: HashMap<PathBuf, String> = HashMap::new();
125 let read_dir = std::fs::read_dir(xml);
126
127 if let Err(e) = read_dir {
130 return syn::Error::new(
131 proc_macro2::Span::call_site(),
132 format!("Failed to read XML directory: {e}"),
133 )
134 .to_compile_error()
135 .into();
136 }
137
138 for entry in read_dir.expect("Failed to read XML directory") {
140 let entry = entry.expect("Failed to read XML file");
141
142 if entry.path().is_dir() {
144 continue;
145 }
146
147 if entry.path().extension().expect("File has no extension.") == "xml" {
148 let xml =
149 std::fs::read_to_string(entry.path()).expect("Unable to read XML file to string");
150 xml_files.insert(entry.path().clone(), xml);
151 }
152 }
153
154 let mut xml_file_path = None;
156 let mut interface_name = None;
157 let mut signal_name = None;
158
159 for (path_key, xml_string) in xml_files {
162 let node = zbus_xml::Node::try_from(xml_string.as_str());
163
164 if node.is_err() {
165 return syn::Error::new(
166 proc_macro2::Span::call_site(),
167 format!(
168 "Failed to parse XML file: \"{}\" Err: {}",
169 path_key.to_str().unwrap(),
170 node.err().unwrap()
171 ),
172 )
173 .to_compile_error()
174 .into();
175 }
176
177 let node = node.unwrap();
178
179 for interface in node.interfaces() {
180 if args.interface.is_some()
183 && interface.name().as_str() != args.interface.as_ref().unwrap()
184 {
185 continue;
186 }
187
188 for signal in interface.signals() {
189 if args.signal.is_some() && signal.name().as_str() != args.signal.as_ref().unwrap()
190 {
191 continue;
192 }
193
194 let xml_signal_name = signal.name();
195
196 if args.signal.is_some()
197 && xml_signal_name.as_str() == args.signal.as_ref().unwrap()
198 {
199 interface_name = Some(interface.name().to_string());
200 signal_name = Some(xml_signal_name.to_string());
201 xml_file_path = Some(path_key.clone());
202 continue;
203 }
204
205 if item_name.contains(xml_signal_name.as_str()) {
206 if interface_name.is_some() && signal_name.is_some() {
208 return syn::Error::new(
209 proc_macro2::Span::call_site(),
210 "Multiple interfaces with the same signal name. Please disambiguate.",
211 )
212 .to_compile_error()
213 .into();
214 }
215 interface_name = Some(interface.name().to_string());
216 signal_name = Some(xml_signal_name.to_string());
217 xml_file_path = Some(path_key.clone());
218 }
219 }
220 }
221 }
222
223 if interface_name.is_none() {
227 return syn::Error::new(
228 proc_macro2::Span::call_site(),
229 format!(
230 "No interface matching signal name '{}' found.",
231 args.signal.unwrap_or_else(|| item_name.clone())
232 ),
233 )
234 .to_compile_error()
235 .into();
236 }
237
238 let interface_name = interface_name.expect("Interface should have been found in search loop.");
241 let signal_name = signal_name.expect("Signal should have been found in search loop.");
242
243 let xml_file_path = xml_file_path.expect("XML file path should be found in search loop.");
244 let xml_file_path = xml_file_path
245 .to_str()
246 .expect("XML file path should be valid UTF-8");
247
248 let test_name = format!("test_{item_name}_type_signature");
250 let test_name = Ident::new(&test_name, proc_macro2::Span::call_site());
251
252 let item_name = item.ident.clone();
253 let item_name = Ident::new(&item_name.to_string(), proc_macro2::Span::call_site());
254
255 let item_plus_validation_test = quote! {
256 #item
257
258 #[cfg(test)]
259 #[test]
260 fn #test_name() {
261 use zvariant::Type;
262
263 let xml_file = std::fs::File::open(#xml_file_path).expect("\"#xml_file_path\" expected to be a valid file path." );
264 let item_signature_from_xml = zbus_lockstep::get_signal_body_type(
265 xml_file,
266 #interface_name,
267 #signal_name,
268 None
269 ).expect("Failed to get signal body type from XML file.");
270 let item_signature_from_struct = <#item_name as Type>::SIGNATURE;
271
272 assert_eq!(&item_signature_from_xml, item_signature_from_struct);
273 }
274 };
275
276 item_plus_validation_test.into()
277}
278
279struct ValidateArgs {
280 xml: Option<PathBuf>,
282
283 interface: Option<String>,
285
286 signal: Option<String>,
288}
289
290impl syn::parse::Parse for ValidateArgs {
291 fn parse(input: ParseStream) -> Result<Self> {
292 let mut xml = None;
293 let mut interface = None;
294 let mut signal = None;
295
296 while !input.is_empty() {
297 let ident = input.parse::<Ident>()?;
298 match ident.to_string().as_str() {
299 "xml" => {
300 input.parse::<Token![:]>()?;
301 let lit = input.parse::<LitStr>()?;
302 xml = Some(PathBuf::from(lit.value()));
303 }
304 "interface" => {
305 input.parse::<Token![:]>()?;
306 let lit = input.parse::<LitStr>()?;
307 interface = Some(lit.value());
308 }
309 "signal" => {
310 input.parse::<Token![:]>()?;
311 let lit = input.parse::<LitStr>()?;
312 signal = Some(lit.value());
313 }
314 _ => {
315 return Err(syn::Error::new(
316 ident.span(),
317 format!("Unexpected argument: {ident}"),
318 ))
319 }
320 }
321
322 if !input.is_empty() {
323 input.parse::<Token![,]>()?;
324 }
325 }
326
327 Ok(ValidateArgs {
328 xml,
329 interface,
330 signal,
331 })
332 }
333}