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 | |
6 | type Result<T> = std::result::Result<T, syn::Error>; |
7 | |
8 | use std::{collections::HashMap, path::PathBuf}; |
9 | |
10 | use proc_macro::TokenStream; |
11 | use quote::quote; |
12 | use 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 ] |
97 | pub 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 | |
278 | struct 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 | |
289 | impl 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 | |