#![doc(html_root_url = "https://docs.rs/zbus-lockstep-macros/0.5.2")]
type Result<T> = std::result::Result<T, syn::Error>;
use std::{collections::HashMap, path::PathBuf};
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::ParseStream, parse_macro_input, DeriveInput, Ident, LitStr, Token};
#[proc_macro_attribute]
pub fn validate(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as ValidateArgs);
let item = parse_macro_input!(input as DeriveInput);
let item_name = item.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::try_from(xml_string.as_str());
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().as_str() != args.interface.as_ref().unwrap()
{
continue;
}
for signal in interface.signals() {
if args.signal.is_some() && signal.name().as_str() != args.signal.as_ref().unwrap()
{
continue;
}
let xml_signal_name = signal.name();
if args.signal.is_some()
&& xml_signal_name.as_str() == 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.as_str()) {
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_name = item.ident.clone();
let item_name = Ident::new(&item_name.to_string(), proc_macro2::Span::call_site());
let item_plus_validation_test = quote! {
#item
#[cfg(test)]
#[test]
fn #test_name() {
use zvariant::Type;
let xml_file = std::fs::File::open(#xml_file_path).expect("\"#xml_file_path\" expected to be a valid 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_name as Type>::SIGNATURE;
assert_eq!(&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}"),
))
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(ValidateArgs {
xml,
interface,
signal,
})
}
}