zbus_lockstep_macros/
lib.rs

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}