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.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/// 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/// ```rust
89/// use zvariant::OwnedObjectPath;
90/// use zbus_lockstep_macros::validate;
91/// use zvariant::Type;
92///
93/// #[validate(xml: "xml", interface: "org.example.Node", signal: "RemoveNode")]
94/// #[derive(Type)]
95/// struct RemoveNodeSignal {
96///    name: String,
97///    path: OwnedObjectPath,
98/// }
99/// ```
100#[proc_macro_attribute]
101pub fn validate(args: TokenStream, input: TokenStream) -> TokenStream {
102    // Parse the macro arguments.
103    let args = parse_macro_input!(args as ValidateArgs);
104
105    // Parse the item struct.
106    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    // Store each file's XML as a string in a with the XML's file path as key.
124    let mut xml_files: HashMap<PathBuf, String> = HashMap::new();
125    let read_dir = std::fs::read_dir(xml);
126
127    // If the path does not exist, the process lacks permissions to read the path,
128    // or the path is not a directory, return an error.
129    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    // Iterate over the directory and store each XML file as a string.
139    for entry in read_dir.expect("Failed to read XML directory") {
140        let entry = entry.expect("Failed to read XML file");
141
142        // Skip directories.
143        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    // These are later needed to call `get_signal_body_type`.
155    let mut xml_file_path = None;
156    let mut interface_name = None;
157    let mut signal_name = None;
158
159    // Iterate over `xml_files` and find the signal that is contained in the struct's name.
160    // Or if `signal_arg` is provided, use that.
161    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            // We were called with an interface argument, so if the interface name does not match,
181            // skip it.
182            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 we have found a signal with the same name in an earlier iteration:
207                    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    // Lets be nice and provide a informative compiler error message.
224
225    // We searched all XML files and did not find a match.
226    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    // If we did find a matching interface we have also set `xml_file_path` and `signal_name`.
239
240    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    // Create a block to return the item struct with a uniquely named validation test.
249    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    // Optional path to XML file
281    xml: Option<PathBuf>,
282
283    // Optional interface name
284    interface: Option<String>,
285
286    // Optional signal name
287    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}