/// Assert only the listed properties exist on the Emacs AST node and compare their values to the values on the rust AST node. /// /// Example invocation: /// ``` /// if let Some((new_status, new_message)) = compare_properties!( /// emacs, /// rust, /// ( /// EmacsField::Required(":key"), /// |r| Some(r.key), /// compare_property_quoted_string /// ), /// ( /// EmacsField::Required(":value"), /// |r| Some(r.contents), /// compare_property_quoted_string /// ), /// (EmacsField::Required(":pre-blank"), compare_identity, compare_noop) /// )? { /// this_status = new_status; /// message = new_message; /// } /// ``` /// /// Or if the node has no properties aside from :standard-properties, we can still assert that the node has no unexpected properties: /// ``` /// if let Some((new_status, new_message)) = compare_properties!(emacs)? { /// this_status = new_status; /// message = new_message; /// } /// ``` macro_rules! compare_properties { ($source:expr, $emacs:expr, $rust:expr, $(($emacs_field:expr, $rust_value_getter:expr, $compare_fn: expr)),+) => { { let mut new_status = Vec::new(); let children = $emacs.as_list()?; let attributes_child = children .iter() .nth(1) .ok_or("Should have an attributes child.")?; let attributes_map = attributes_child.as_map()?; let mut emacs_keys: BTreeSet<&str> = attributes_map.keys().map(|s| *s).collect(); if emacs_keys.contains(":standard-properties") { emacs_keys.remove(":standard-properties"); } else { new_status.push(ComparePropertiesResult::SelfChange(DiffStatus::Bad, Some(format!( "Emacs token lacks :standard-properties field.", )))); } $( match $emacs_field { EmacsField::Required(name) if emacs_keys.contains(name) => { emacs_keys.remove(name); }, EmacsField::Optional(name) if emacs_keys.contains(name) => { emacs_keys.remove(name); }, EmacsField::Required(name) => { new_status.push(ComparePropertiesResult::SelfChange(DiffStatus::Bad, Some(format!( "Emacs token lacks required field: {}", name )))); }, EmacsField::Optional(_name) => {}, } )+ if !emacs_keys.is_empty() { let unexpected_keys: Vec<&str> = emacs_keys.into_iter().collect(); let unexpected_keys = unexpected_keys.join(", "); new_status.push(ComparePropertiesResult::SelfChange(DiffStatus::Bad, Some(format!( "Emacs token had extra field(s): {}", unexpected_keys )))); } $( let emacs_name = match $emacs_field { EmacsField::Required(name) => { name }, EmacsField::Optional(name) => { name }, }; let result = $compare_fn($source, $emacs, $rust, emacs_name, $rust_value_getter)?; match result { ComparePropertiesResult::SelfChange(DiffStatus::Good, _) => unreachable!("No comparison functions should return SelfChange() when DiffStatus is good."), ComparePropertiesResult::NoChange => {}, result => { new_status.push(result); } } )+ new_status } }; // Default case for when there are no expected properties except for :standard-properties ($emacs:expr) => { { let mut new_status = Vec::new(); let children = $emacs.as_list()?; let attributes_child = children .iter() .nth(1) .ok_or("Should have an attributes child.")?; let attributes_map = attributes_child.as_map()?; let mut emacs_keys: BTreeSet<&str> = attributes_map.keys().map(|s| *s).collect(); if emacs_keys.contains(":standard-properties") { emacs_keys.remove(":standard-properties"); } else { new_status.push(ComparePropertiesResult::SelfChange(DiffStatus::Bad, Some(format!( "Emacs token lacks :standard-properties field.", )))); } if !emacs_keys.is_empty() { let unexpected_keys: Vec<&str> = emacs_keys.into_iter().collect(); let unexpected_keys = unexpected_keys.join(", "); new_status.push(ComparePropertiesResult::SelfChange(DiffStatus::Bad, Some(format!( "Emacs token had extra field(s): {}", unexpected_keys )))); } new_status } }; } pub(crate) use compare_properties;