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")]
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, Ident, ItemStruct, LitStr, Token};
13
14/// 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.
99 let args = parse_macro_input!(args as ValidateArgs);
100
101 // Parse the item struct.
102 let item_struct = parse_macro_input!(input as ItemStruct);
103 let item_name = item_struct.ident.to_string();
104
105 let xml_str = args.xml.as_ref().and_then(|p| p.to_str());
106
107 let xml = match zbus_lockstep::resolve_xml_path(xml_str) {
108 Ok(xml) => xml,
109 Err(e) => {
110 return syn::Error::new(
111 proc_macro2::Span::call_site(),
112 format!("Failed to resolve XML path: {e}"),
113 )
114 .to_compile_error()
115 .into();
116 }
117 };
118
119 // Store each file's XML as a string in a with the XML's file path as key.
120 let mut xml_files: HashMap<PathBuf, String> = HashMap::new();
121 let read_dir = std::fs::read_dir(xml);
122
123 // 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.
125 if let Err(e) = read_dir {
126 return syn::Error::new(
127 proc_macro2::Span::call_site(),
128 format!("Failed to read XML directory: {e}"),
129 )
130 .to_compile_error()
131 .into();
132 }
133
134 // Iterate over the directory and store each XML file as a string.
135 for entry in read_dir.expect("Failed to read XML directory") {
136 let entry = entry.expect("Failed to read XML file");
137
138 // Skip directories.
139 if entry.path().is_dir() {
140 continue;
141 }
142
143 if entry.path().extension().expect("File has no extension.") == "xml" {
144 let 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 }
149
150 // These are later needed to call `get_signal_body_type`.
151 let mut xml_file_path = None;
152 let mut interface_name = None;
153 let mut signal_name = None;
154
155 // 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.
157 for (path_key, xml_string) in xml_files {
158 let node = zbus_xml::Node::try_from(xml_string.as_str());
159
160 if node.is_err() {
161 return syn::Error::new(
162 proc_macro2::Span::call_site(),
163 format!(
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 }
172
173 let node = node.unwrap();
174
175 for interface in node.interfaces() {
176 // We were called with an interface argument, so if the interface name does not match,
177 // skip it.
178 if args.interface.is_some()
179 && interface.name().as_str() != args.interface.as_ref().unwrap()
180 {
181 continue;
182 }
183
184 for signal in interface.signals() {
185 if args.signal.is_some() && signal.name().as_str() != args.signal.as_ref().unwrap()
186 {
187 continue;
188 }
189
190 let xml_signal_name = signal.name();
191
192 if 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());
198 continue;
199 }
200
201 if item_name.contains(xml_signal_name.as_str()) {
202 // If we have found a signal with the same name in an earlier iteration:
203 if interface_name.is_some() && signal_name.is_some() {
204 return 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 }
218
219 // Lets be nice and provide a informative compiler error message.
220
221 // We searched all XML files and did not find a match.
222 if interface_name.is_none() {
223 return syn::Error::new(
224 proc_macro2::Span::call_site(),
225 format!(
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 }
233
234 // If we did find a matching interface we have also set `xml_file_path` and `signal_name`.
235
236 let interface_name = interface_name.expect("Interface should have been found in search loop.");
237 let signal_name = signal_name.expect("Signal should have been found in search loop.");
238
239 let xml_file_path = xml_file_path.expect("XML file path should be found in search loop.");
240 let xml_file_path = xml_file_path
241 .to_str()
242 .expect("XML file path should be valid UTF-8");
243
244 // Create a block to return the item struct with a uniquely named validation test.
245 let test_name = format!("test_{item_name}_type_signature");
246 let test_name = Ident::new(&test_name, proc_macro2::Span::call_site());
247
248 let item_struct_name = item_struct.ident.clone();
249 let item_struct_name = Ident::new(
250 &item_struct_name.to_string(),
251 proc_macro2::Span::call_site(),
252 );
253
254 let item_plus_validation_test = quote! {
255 #item_struct
256
257 #[cfg(test)]
258 #[test]
259 fn #test_name() {
260 use zvariant::Type;
261
262 let xml_file = std::fs::File::open(#xml_file_path).expect("\"#xml_file_path\" expected to be a valid file path." );
263 let item_signature_from_xml = zbus_lockstep::get_signal_body_type(
264 xml_file,
265 #interface_name,
266 #signal_name,
267 None
268 ).expect("Failed to get signal body type from XML file.");
269 let item_signature_from_struct = <#item_struct_name as Type>::signature();
270
271 assert_eq!(&item_signature_from_xml, &item_signature_from_struct);
272 }
273 };
274
275 item_plus_validation_test.into()
276}
277
278struct ValidateArgs {
279 // Optional path to XML file
280 xml: Option<PathBuf>,
281
282 // Optional interface name
283 interface: Option<String>,
284
285 // Optional signal name
286 signal: Option<String>,
287}
288
289impl syn::parse::Parse for ValidateArgs {
290 fn parse(input: ParseStream) -> Result<Self> {
291 let mut xml = None;
292 let mut interface = None;
293 let mut signal = None;
294
295 while !input.is_empty() {
296 let ident = input.parse::<Ident>()?;
297 match ident.to_string().as_str() {
298 "xml" => {
299 input.parse::<Token![:]>()?;
300 let lit = input.parse::<LitStr>()?;
301 xml = Some(PathBuf::from(lit.value()));
302 }
303 "interface" => {
304 input.parse::<Token![:]>()?;
305 let lit = input.parse::<LitStr>()?;
306 interface = Some(lit.value());
307 }
308 "signal" => {
309 input.parse::<Token![:]>()?;
310 let lit = input.parse::<LitStr>()?;
311 signal = Some(lit.value());
312 }
313 _ => {
314 return Err(syn::Error::new(
315 ident.span(),
316 format!("Unexpected argument: {ident}"),
317 ))
318 }
319 }
320
321 if !input.is_empty() {
322 input.parse::<Token![,]>()?;
323 }
324 }
325
326 Ok(ValidateArgs {
327 xml,
328 interface,
329 signal,
330 })
331 }
332}
333