diff --git a/CHANGELOG.md b/CHANGELOG.md index e72b24d21..22f291627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ### Added -* Node methods: `get_property_no_ns` (alias: `get_attribute_no_ns`) +* Node methods: `get_property_no_ns` (alias: `get_attribute_no_ns`), `get_properties_ns` (alias: `get_attributes_ns`) +* Added implementations of `Hash`, `PartialEq` and `Eq` traits for `Namespace` ## [0.3.3] 2023-17-07 diff --git a/src/c_helpers.rs b/src/c_helpers.rs index 6c7900607..f23199d91 100644 --- a/src/c_helpers.rs +++ b/src/c_helpers.rs @@ -67,6 +67,9 @@ pub fn xmlNextPropertySibling(attr: xmlAttrPtr) -> xmlAttrPtr { pub fn xmlAttrName(attr: xmlAttrPtr) -> *const c_char { unsafe { (*attr).name as *const c_char } } +pub fn xmlAttrNs(attr: xmlAttrPtr) -> xmlNsPtr { + unsafe { (*attr).ns } +} pub fn xmlGetFirstProperty(node: xmlNodePtr) -> xmlAttrPtr { unsafe { (*node).properties } } diff --git a/src/readonly/tree.rs b/src/readonly/tree.rs index f560261db..eb174c9ed 100644 --- a/src/readonly/tree.rs +++ b/src/readonly/tree.rs @@ -298,21 +298,41 @@ impl RoNode { /// Get a copy of the attributes of this node pub fn get_properties(self) -> HashMap { let mut attributes = HashMap::new(); - let mut attr_names = Vec::new(); - unsafe { - let mut current_prop = xmlGetFirstProperty(self.0); - while !current_prop.is_null() { - let name_ptr = xmlAttrName(current_prop); - let c_name_string = CStr::from_ptr(name_ptr); - let name = c_name_string.to_string_lossy().into_owned(); - attr_names.push(name); - current_prop = xmlNextPropertySibling(current_prop); - } - } - for name in attr_names { + let mut current_prop = xmlGetFirstProperty(self.0); + while !current_prop.is_null() { + let name_ptr = xmlAttrName(current_prop); + let c_name_string = unsafe { CStr::from_ptr(name_ptr) }; + let name = c_name_string.to_string_lossy().into_owned(); let value = self.get_property(&name).unwrap_or_default(); attributes.insert(name, value); + current_prop = xmlNextPropertySibling(current_prop); + } + + attributes + } + + /// Get a copy of this node's attributes and their namespaces + pub fn get_properties_ns(self) -> HashMap<(String, Option), String> { + let mut attributes = HashMap::new(); + + let mut current_prop = xmlGetFirstProperty(self.0); + while !current_prop.is_null() { + let name_ptr = xmlAttrName(current_prop); + let c_name_string = unsafe { CStr::from_ptr(name_ptr) }; + let name = c_name_string.to_string_lossy().into_owned(); + let ns_ptr = xmlAttrNs(current_prop); + if ns_ptr.is_null() { + let value = self.get_property_no_ns(&name).unwrap_or_default(); + attributes.insert((name, None), value); + } else { + let ns = Namespace { ns_ptr }; + let value = self + .get_property_ns(&name, &ns.get_href()) + .unwrap_or_default(); + attributes.insert((name, Some(ns)), value); + } + current_prop = xmlNextPropertySibling(current_prop); } attributes @@ -323,6 +343,11 @@ impl RoNode { self.get_properties() } + /// Alias for `get_properties_ns` + pub fn get_attributes_ns(self) -> HashMap<(String, Option), String> { + self.get_properties_ns() + } + /// Check if a property has been defined, without allocating its value pub fn has_property(self, name: &str) -> bool { let c_name = CString::new(name).unwrap(); diff --git a/src/tree/namespace.rs b/src/tree/namespace.rs index 18e6a5832..1e44f41fa 100644 --- a/src/tree/namespace.rs +++ b/src/tree/namespace.rs @@ -2,6 +2,7 @@ //! use std::error::Error; use std::ffi::{CStr, CString}; +use std::hash::{Hash, Hasher}; use std::ptr; use std::str; @@ -16,6 +17,21 @@ pub struct Namespace { pub(crate) ns_ptr: xmlNsPtr, } +impl PartialEq for Namespace { + fn eq(&self, other: &Self) -> bool { + self.get_prefix() == other.get_prefix() && self.get_href() == other.get_href() + } +} + +impl Eq for Namespace {} + +impl Hash for Namespace { + fn hash(&self, state: &mut H) { + self.get_prefix().hash(state); + self.get_href().hash(state); + } +} + impl Namespace { /// Creates a new namespace pub fn new( diff --git a/src/tree/node.rs b/src/tree/node.rs index f3050e715..93306698c 100644 --- a/src/tree/node.rs +++ b/src/tree/node.rs @@ -656,21 +656,41 @@ impl Node { /// Get a copy of the attributes of this node pub fn get_properties(&self) -> HashMap { let mut attributes = HashMap::new(); - let mut attr_names = Vec::new(); - unsafe { - let mut current_prop = xmlGetFirstProperty(self.node_ptr()); - while !current_prop.is_null() { - let name_ptr = xmlAttrName(current_prop); - let c_name_string = CStr::from_ptr(name_ptr); - let name = c_name_string.to_string_lossy().into_owned(); - attr_names.push(name); - current_prop = xmlNextPropertySibling(current_prop); - } - } - for name in attr_names { + let mut current_prop = xmlGetFirstProperty(self.node_ptr()); + while !current_prop.is_null() { + let name_ptr = xmlAttrName(current_prop); + let c_name_string = unsafe { CStr::from_ptr(name_ptr) }; + let name = c_name_string.to_string_lossy().into_owned(); let value = self.get_property(&name).unwrap_or_default(); attributes.insert(name, value); + current_prop = xmlNextPropertySibling(current_prop); + } + + attributes + } + + /// Get a copy of this node's attributes and their namespaces + pub fn get_properties_ns(&self) -> HashMap<(String, Option), String> { + let mut attributes = HashMap::new(); + + let mut current_prop = xmlGetFirstProperty(self.node_ptr()); + while !current_prop.is_null() { + let name_ptr = xmlAttrName(current_prop); + let c_name_string = unsafe { CStr::from_ptr(name_ptr) }; + let name = c_name_string.to_string_lossy().into_owned(); + let ns_ptr = xmlAttrNs(current_prop); + if ns_ptr.is_null() { + let value = self.get_property_no_ns(&name).unwrap_or_default(); + attributes.insert((name, None), value); + } else { + let ns = Namespace { ns_ptr }; + let value = self + .get_property_ns(&name, &ns.get_href()) + .unwrap_or_default(); + attributes.insert((name, Some(ns)), value); + } + current_prop = xmlNextPropertySibling(current_prop); } attributes @@ -681,6 +701,11 @@ impl Node { self.get_properties() } + /// Alias for `get_properties_ns` + pub fn get_attributes_ns(&self) -> HashMap<(String, Option), String> { + self.get_properties_ns() + } + /// Gets the active namespace associated of this node pub fn get_namespace(&self) -> Option { let ns_ptr = xmlNodeNs(self.node_ptr()); diff --git a/tests/resources/file01_ns.xml b/tests/resources/file01_ns.xml new file mode 100644 index 000000000..7c70ebbf0 --- /dev/null +++ b/tests/resources/file01_ns.xml @@ -0,0 +1,11 @@ + + + + some text + + more text + diff --git a/tests/tree_tests.rs b/tests/tree_tests.rs index 1d99bfadc..52fc03d81 100644 --- a/tests/tree_tests.rs +++ b/tests/tree_tests.rs @@ -109,6 +109,133 @@ fn node_attributes_accessor() { assert_eq!(attributes.len(), 0); } +#[test] +fn node_attributes_ns_accessor() { + // Setup + let parser = Parser::default(); + let doc_result = parser.parse_file("tests/resources/file01_ns.xml"); + assert!(doc_result.is_ok()); + let doc = doc_result.unwrap(); + let root = doc.get_root_element().unwrap(); + let mut root_elements = root.get_child_elements(); + let child_opt = root_elements.first_mut(); + assert!(child_opt.is_some()); + let child = child_opt.unwrap(); + + // All attributes + let attributes = child.get_attributes_ns(); + assert_eq!(attributes.len(), 3); + assert_eq!( + attributes.get(&("attribute".to_string(), None)), + Some(&"value1".to_string()) + ); + let namespaces = child.get_namespaces(&doc); + assert_eq!(namespaces.len(), 2); + let foo_ns = namespaces[0].clone(); + let bar_ns = namespaces[1].clone(); + + assert_eq!( + attributes.get(&("attribute".to_string(), Some(foo_ns.clone()))), + Some(&"foo1".to_string()) + ); + assert_eq!( + attributes.get(&("attr".to_string(), Some(bar_ns.clone()))), + Some(&"bar1".to_string()) + ); + + // Has + // TODO include this when `has_attribute_no_ns` is implemented + // assert!(child.has_attribute("attribute")); + + // Get + assert_eq!( + child.get_attribute_no_ns("attribute"), + Some("value1".to_string()) + ); + assert_eq!( + child.get_attribute_ns("attribute", "http://www.example.com/myns"), + Some("foo1".to_string()) + ); + assert_eq!( + child.get_attribute_ns("attr", "http://www.example.com/myns"), + Some("bar1".to_string()) + ); + + // Get as node + // TODO include this when `get_attribute_node_ns` and + // `get_attribute_node_no_ns` are implemented + // let attr_node_opt = child.get_attribute_node("attribute"); + // assert!(attr_node_opt.is_some()); + // let attr_node = attr_node_opt.unwrap(); + // assert_eq!(attr_node.get_name(), "attribute"); + // assert_eq!(attr_node.get_type(), Some(NodeType::AttributeNode)); + + // Set + assert!(child.set_attribute("attribute", "setter_value").is_ok()); + assert_eq!( + child.get_attribute_no_ns("attribute"), + Some("setter_value".to_string()) + ); + assert!(child + .set_attribute_ns("attribute", "foo_value", &foo_ns) + .is_ok()); + assert_eq!( + child.get_attribute_no_ns("attribute"), + Some("setter_value".to_string()) + ); + // Remove + // TODO include this when `remove_attribute_no_ns` is implemented + // assert!(child.remove_attribute("attribute").is_ok()); + // assert_eq!(child.get_attribute("attribute"), None); + // assert_eq!(child.has_attribute("attribute"), false); + // Recount + // let attributes = child.get_attributes_ns(); + // assert_eq!(attributes.len(), 2); +} + +#[test] +fn namespace_partial_eq() { + // Setup + let parser = Parser::default(); + let doc_result = parser.parse_file("tests/resources/file01_ns.xml"); + assert!(doc_result.is_ok()); + let doc = doc_result.unwrap(); + let root = doc.get_root_element().unwrap(); + let mut root_elements = root.get_child_elements(); + let child1_opt = root_elements.first_mut(); + assert!(child1_opt.is_some()); + let child1 = child1_opt.unwrap(); + + // Child 1 namespaces + let namespaces1 = child1.get_namespaces(&doc); + assert_eq!(namespaces1.len(), 2); + let foo_ns1 = namespaces1[0].clone(); + assert_eq!(foo_ns1.get_prefix(), "foo"); + assert_eq!(foo_ns1.get_href(), "http://www.example.com/myns"); + let bar_ns1 = namespaces1[1].clone(); + assert_eq!(bar_ns1.get_prefix(), "bar"); + assert_eq!(bar_ns1.get_href(), "http://www.example.com/myns"); + // The current implementation of PartialEq for Namespace compares the prefix + // and href + assert!(foo_ns1 != bar_ns1); + + // Compare with child2 namespace + let child2_opt = child1.get_next_element_sibling(); + assert!(child2_opt.is_some()); + let child2 = child2_opt.unwrap(); + let attributes2 = child2.get_attributes_ns(); + assert_eq!(attributes2.len(), 2); + let namespaces2 = child2.get_namespaces(&doc); + assert_eq!(namespaces2.len(), 1); + let foo_ns2 = namespaces2[0].clone(); + // The current implementation of PartialEq for Namespace compares the prefix + // and href not the pointer + assert!(foo_ns1 == foo_ns2); + assert_eq!(foo_ns1.get_href(), foo_ns2.get_href()); + assert_eq!(foo_ns1.get_prefix(), foo_ns2.get_prefix()); + assert_ne!(foo_ns1.ns_ptr(), foo_ns2.ns_ptr()); +} + #[test] fn attribute_namespace_accessors() { let mut doc = Document::new().unwrap();