zbus_lockstep_macros/
lib.rs1#![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#[proc_macro_attribute]
97pub fn validate(args: TokenStream, input: TokenStream) -> TokenStream {
98 let args = parse_macro_input!(args as ValidateArgs);
100
101 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 let mut xml_files: HashMap<PathBuf, String> = HashMap::new();
121 let read_dir = std::fs::read_dir(xml);
122
123 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 for entry in read_dir.expect("Failed to read XML directory") {
136 let entry = entry.expect("Failed to read XML file");
137
138 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 let mut xml_file_path = None;
152 let mut interface_name = None;
153 let mut signal_name = None;
154
155 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 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 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 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 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 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 xml: Option<PathBuf>,
281
282 interface: Option<String>,
284
285 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}