1//! # zbus-lockstep-macros
2//!
3//! This provides the `validate` macro that builds on `zbus-lockstep`.
4#![doc(html_root_url = "https://docs.rs/zbus-lockstep-macros/0.4.4")]
56type Result<T> = std::result::Result<T, syn::Error>;
78use std::{collections::HashMap, path::PathBuf};
910use proc_macro::TokenStream;
11use quote::quote;
12use syn::{parse::ParseStream, parse_macro_input, Ident, ItemStruct, LitStr, Token};
1314/// Validate a struct's type signature against XML signal body type.
15///
16/// Retrieves the signal body type from a (collection of) XML file(s) and compares it to the
17/// struct's type signature.
18///
19/// If the XML file(s) are found in the default location, `xml/` or `XML/` of the crate root,
20/// or provided as environment variable, `LOCKSTEP_XML_PATH`, the macro can be used without
21/// arguments.
22///
23///
24/// # Arguments
25///
26/// `#[validate]` can take three optional arguments:
27///
28/// * `xml`: Path to XML file(s) containing the signal definition.
29/// * `interface`: Interface name of the signal.
30/// * `signal`: Signal name.
31///
32/// `#[validate(xml: <xml_path>, interface: <interface_name>, member: <member_name>)]`
33///
34/// ## `xml_path`
35///
36/// Without an argument, the macro looks for XML file(s) in `xml/` or `XML/` of the crate root.
37/// If the definitions are to be found elsewhere, there are two options:
38///
39/// Use the `xml` argument:
40///
41/// ```ignore
42/// #[validate(xml: "xml")]
43/// #[derive(Type)]
44/// struct RemoveNodeSignal {
45/// name: String,
46/// path: OwnedObjectPath,
47/// }
48/// ```
49///
50///
51/// Alternatively, you can provide the XML directory path as environment variable,
52/// `LOCKSTEP_XML_PATH`, which will override both default and the path argument.
53///
54/// ## `interface`
55///
56/// If more than one signal with the same name is defined in the XML file(s),
57/// the macro will fail and you can provide an interface name to disambiguate.
58///
59/// ```ignore
60/// #[validate(interface: "org.example.Node")]
61/// #[derive(Type)]
62/// struct RemoveNodeSignal {
63/// name: String,
64/// path: OwnedObjectPath,
65/// }
66/// ```
67///
68///
69/// ## `signal`
70///
71/// If a custom signal name is desired, you can be provided using `signal:`.
72///
73/// ```ignore
74/// #[validate(signal: "RemoveNode")]
75/// #[derive(Type)]
76/// struct RemoveNodeSignal {
77/// name: String,
78/// path: OwnedObjectPath,
79/// }
80/// ```
81///
82/// ## Multiple arguments
83///
84/// You can provide multiple arguments with a comma separated list.
85///
86/// # Examples
87///
88/// ```ignore
89/// #[validate(xml: "xml", interface: "org.example.Node", signal: "RemoveNode")]
90/// #[derive(Type)]
91/// struct RemoveNodeSignal {
92/// name: String,
93/// path: OwnedObjectPath,
94/// }
95/// ```
96#[proc_macro_attribute]
97pub fn validate(args: TokenStream, input: TokenStream) -> TokenStream {
98// Parse the macro arguments.
99let args = parse_macro_input!(args as ValidateArgs);
100101// Parse the item struct.
102let item_struct = parse_macro_input!(input as ItemStruct);
103let item_name = item_struct.ident.to_string();
104105let xml_str = args.xml.as_ref().and_then(|p| p.to_str());
106107let xml = match zbus_lockstep::resolve_xml_path(xml_str) {
108Ok(xml) => xml,
109Err(e) => {
110return syn::Error::new(
111 proc_macro2::Span::call_site(),
112format!("Failed to resolve XML path: {e}"),
113 )
114 .to_compile_error()
115 .into();
116 }
117 };
118119// Store each file's XML as a string in a with the XML's file path as key.
120let mut xml_files: HashMap<PathBuf, String> = HashMap::new();
121let read_dir = std::fs::read_dir(xml);
122123// If the path does not exist, the process lacks permissions to read the path,
124 // or the path is not a directory, return an error.
125if let Err(e) = read_dir {
126return syn::Error::new(
127 proc_macro2::Span::call_site(),
128format!("Failed to read XML directory: {e}"),
129 )
130 .to_compile_error()
131 .into();
132 }
133134// Iterate over the directory and store each XML file as a string.
135for entry in read_dir.expect("Failed to read XML directory") {
136let entry = entry.expect("Failed to read XML file");
137138// Skip directories.
139if entry.path().is_dir() {
140continue;
141 }
142143if entry.path().extension().expect("File has no extension.") == "xml" {
144let xml =
145 std::fs::read_to_string(entry.path()).expect("Unable to read XML file to string");
146 xml_files.insert(entry.path().clone(), xml);
147 }
148 }
149150// These are later needed to call `get_signal_body_type`.
151let mut xml_file_path = None;
152let mut interface_name = None;
153let mut signal_name = None;
154155// Iterate over `xml_files` and find the signal that is contained in the struct's name.
156 // Or if `signal_arg` is provided, use that.
157for (path_key, xml_string) in xml_files {
158let node = zbus_xml::Node::try_from(xml_string.as_str());
159160if node.is_err() {
161return syn::Error::new(
162 proc_macro2::Span::call_site(),
163format!(
164"Failed to parse XML file: \"{}\" Err: {}",
165 path_key.to_str().unwrap(),
166 node.err().unwrap()
167 ),
168 )
169 .to_compile_error()
170 .into();
171 }
172173let node = node.unwrap();
174175for interface in node.interfaces() {
176// We were called with an interface argument, so if the interface name does not match,
177 // skip it.
178if args.interface.is_some()
179 && interface.name().as_str() != args.interface.as_ref().unwrap()
180 {
181continue;
182 }
183184for signal in interface.signals() {
185if args.signal.is_some() && signal.name().as_str() != args.signal.as_ref().unwrap()
186 {
187continue;
188 }
189190let xml_signal_name = signal.name();
191192if args.signal.is_some()
193 && xml_signal_name.as_str() == args.signal.as_ref().unwrap()
194 {
195 interface_name = Some(interface.name().to_string());
196 signal_name = Some(xml_signal_name.to_string());
197 xml_file_path = Some(path_key.clone());
198continue;
199 }
200201if item_name.contains(xml_signal_name.as_str()) {
202// If we have found a signal with the same name in an earlier iteration:
203if interface_name.is_some() && signal_name.is_some() {
204return syn::Error::new(
205 proc_macro2::Span::call_site(),
206"Multiple interfaces with the same signal name. Please disambiguate.",
207 )
208 .to_compile_error()
209 .into();
210 }
211 interface_name = Some(interface.name().to_string());
212 signal_name = Some(xml_signal_name.to_string());
213 xml_file_path = Some(path_key.clone());
214 }
215 }
216 }
217 }
218219// Lets be nice and provide a informative compiler error message.
220221 // We searched all XML files and did not find a match.
222if interface_name.is_none() {
223return syn::Error::new(
224 proc_macro2::Span::call_site(),
225format!(
226"No interface matching signal name '{}' found.",
227 args.signal.unwrap_or_else(|| item_name.clone())
228 ),
229 )
230 .to_compile_error()
231 .into();
232 }
233234// If we did find a matching interface we have also set `xml_file_path` and `signal_name`.
235236let interface_name = interface_name.expect("Interface should have been found in search loop.");
237let signal_name = signal_name.expect("Signal should have been found in search loop.");
238239let xml_file_path = xml_file_path.expect("XML file path should be found in search loop.");
240let xml_file_path = xml_file_path
241 .to_str()
242 .expect("XML file path should be valid UTF-8");
243244// Create a block to return the item struct with a uniquely named validation test.
245let test_name = format!("test_{item_name}_type_signature");
246let test_name = Ident::new(&test_name, proc_macro2::Span::call_site());
247248let item_struct_name = item_struct.ident.clone();
249let item_struct_name = Ident::new(
250&item_struct_name.to_string(),
251 proc_macro2::Span::call_site(),
252 );
253254let item_plus_validation_test = quote! {
255 #item_struct
256257#[cfg(test)]
258 #[test]
259fn #test_name() {
260use zvariant::Type;
261262let xml_file = std::fs::File::open(#xml_file_path).expect("\"#xml_file_path\" expected to be a valid file path." );
263let item_signature_from_xml = zbus_lockstep::get_signal_body_type(
264 xml_file,
265 #interface_name,
266 #signal_name,
267None
268).expect("Failed to get signal body type from XML file.");
269let item_signature_from_struct = <#item_struct_name as Type>::signature();
270271assert_eq!(&item_signature_from_xml, &item_signature_from_struct);
272 }
273 };
274275 item_plus_validation_test.into()
276}
277278struct ValidateArgs {
279// Optional path to XML file
280xml: Option<PathBuf>,
281282// Optional interface name
283interface: Option<String>,
284285// Optional signal name
286signal: Option<String>,
287}
288289impl syn::parse::Parse for ValidateArgs {
290fn parse(input: ParseStream) -> Result<Self> {
291let mut xml = None;
292let mut interface = None;
293let mut signal = None;
294295while !input.is_empty() {
296let ident = input.parse::<Ident>()?;
297match ident.to_string().as_str() {
298"xml" => {
299 input.parse::<Token![:]>()?;
300let lit = input.parse::<LitStr>()?;
301 xml = Some(PathBuf::from(lit.value()));
302 }
303"interface" => {
304 input.parse::<Token![:]>()?;
305let lit = input.parse::<LitStr>()?;
306 interface = Some(lit.value());
307 }
308"signal" => {
309 input.parse::<Token![:]>()?;
310let lit = input.parse::<LitStr>()?;
311 signal = Some(lit.value());
312 }
313_ => {
314return Err(syn::Error::new(
315 ident.span(),
316format!("Unexpected argument: {ident}"),
317 ))
318 }
319 }
320321if !input.is_empty() {
322 input.parse::<Token![,]>()?;
323 }
324 }
325326Ok(ValidateArgs {
327 xml,
328 interface,
329 signal,
330 })
331 }
332}