#![doc(html_root_url = "https://docs.rs/zbus-lockstep-macros/0.2.3")]
type Result<T> = std::result::Result<T, syn::Error>;
use std::{collections::HashMap, path::PathBuf, str::FromStr};
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::ParseStream, parse_macro_input, Ident, ItemStruct, LitStr, Token};
#[proc_macro_attribute]
pub fn validate(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as ValidateArgs);
let item_struct = parse_macro_input!(input as ItemStruct);
let item_name = item_struct.ident.to_string();
let xml_str = args.xml.as_ref().and_then(|p| p.to_str());
let xml = match zbus_lockstep::resolve_xml_path(xml_str) {
Ok(xml) => xml,
Err(e) => {
return syn::Error::new(
proc_macro2::Span::call_site(),
format!("Failed to resolve XML path: {e}"),
)
.to_compile_error()
.into();
}
};
let mut xml_files: HashMap<PathBuf, String> = HashMap::new();
let read_dir = std::fs::read_dir(&xml);
if let Err(e) = read_dir {
return syn::Error::new(
proc_macro2::Span::call_site(),
format!("Failed to read XML directory: {e}"),
)
.to_compile_error()
.into();
}
for entry in read_dir.expect("Failed to read XML directory") {
let entry = entry.expect("Failed to read XML file");
if entry.path().is_dir() {
continue;
}
if entry.path().extension().expect("File has no extension.") == "xml" {
let xml =
std::fs::read_to_string(entry.path()).expect("Unable to read XML file to string");
xml_files.insert(entry.path().clone(), xml);
}
}
let mut xml_file_path = None;
let mut interface_name = None;
let mut signal_name = None;
for (path_key, xml_string) in xml_files {
let node = zbus::xml::Node::from_str(&xml_string);
if node.is_err() {
return syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"Failed to parse XML file: \"{}\" Err: {}",
path_key.to_str().unwrap(),
node.err().unwrap()
),
)
.to_compile_error()
.into();
}
let node = node.unwrap();
for interface in node.interfaces() {
if args.interface.is_some() && interface.name() != args.interface.as_ref().unwrap() {
continue;
}
for signal in interface.signals() {
if args.signal.is_some() && signal.name() != args.signal.as_ref().unwrap() {
continue;
}
let xml_signal_name = signal.name();
if args.signal.is_some() && xml_signal_name == args.signal.as_ref().unwrap() {
interface_name = Some(interface.name().to_string());
signal_name = Some(xml_signal_name.to_string());
xml_file_path = Some(path_key.clone());
continue;
}
if item_name.contains(xml_signal_name) {
if interface_name.is_some() && signal_name.is_some() {
return syn::Error::new(
proc_macro2::Span::call_site(),
"Multiple interfaces with the same signal name. Please disambiguate.",
)
.to_compile_error()
.into();
}
interface_name = Some(interface.name().to_string());
signal_name = Some(xml_signal_name.to_string());
xml_file_path = Some(path_key.clone());
}
}
}
}
if interface_name.is_none() {
return syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"No interface matching signal name '{}' found.",
args.signal.unwrap_or_else(|| item_name.clone())
),
)
.to_compile_error()
.into();
}
let interface_name = interface_name.expect("Interface should have been found in search loop.");
let signal_name = signal_name.expect("Signal should have been found in search loop.");
let xml_file_path = xml_file_path.expect("XML file path should be found in search loop.");
let xml_file_path = xml_file_path
.to_str()
.expect("XML file path should be valid UTF-8");
let test_name = format!("test_{item_name}_type_signature");
let test_name = Ident::new(&test_name, proc_macro2::Span::call_site());
let item_struct_name = item_struct.ident.clone();
let item_struct_name = Ident::new(
&item_struct_name.to_string(),
proc_macro2::Span::call_site(),
);
let item_plus_validation_test = quote! {
#item_struct
#[test]
fn #test_name() {
use zbus::zvariant::{self, Type};
use zbus_lockstep::{signatures_are_eq, assert_eq_signatures};
let xml_file = std::fs::File::open(#xml_file_path).expect(#xml_file_path);
let item_signature_from_xml = zbus_lockstep::get_signal_body_type(
xml_file,
#interface_name,
#signal_name,
None
).expect("Failed to get signal body type from XML file");
let item_signature_from_struct = <#item_struct_name as zvariant::Type>::signature();
assert_eq_signatures!(&item_signature_from_xml, &item_signature_from_struct);
}
};
item_plus_validation_test.into()
}
struct ValidateArgs {
xml: Option<PathBuf>,
interface: Option<String>,
signal: Option<String>,
}
impl syn::parse::Parse for ValidateArgs {
fn parse(input: ParseStream) -> Result<Self> {
let mut xml = None;
let mut interface = None;
let mut signal = None;
while !input.is_empty() {
let ident = input.parse::<Ident>()?;
match ident.to_string().as_str() {
"xml" => {
input.parse::<Token![:]>()?;
let lit = input.parse::<LitStr>()?;
xml = Some(PathBuf::from(lit.value()));
}
"interface" => {
input.parse::<Token![:]>()?;
let lit = input.parse::<LitStr>()?;
interface = Some(lit.value());
}
"signal" => {
input.parse::<Token![:]>()?;
let lit = input.parse::<LitStr>()?;
signal = Some(lit.value());
}
_ => {
return Err(syn::Error::new(
ident.span(),
format!("Unexpected argument: {ident}"),
))
}
}
}
Ok(ValidateArgs {
xml,
interface,
signal,
})
}
}