Skip to content

Commit

Permalink
Add get_properties_ns to Node and RoNode
Browse files Browse the repository at this point in the history
This required implementing the traits Hash, PartialEq and Eq for Namespace
  • Loading branch information
anwaralameddin authored and dginev committed Feb 25, 2024
1 parent 4d70951 commit 2380450
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 25 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions src/c_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
49 changes: 37 additions & 12 deletions src/readonly/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,21 +298,41 @@ impl RoNode {
/// Get a copy of the attributes of this node
pub fn get_properties(self) -> HashMap<String, String> {
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<Namespace>), 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
Expand All @@ -323,6 +343,11 @@ impl RoNode {
self.get_properties()
}

/// Alias for `get_properties_ns`
pub fn get_attributes_ns(self) -> HashMap<(String, Option<Namespace>), 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();
Expand Down
16 changes: 16 additions & 0 deletions src/tree/namespace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
use std::error::Error;
use std::ffi::{CStr, CString};
use std::hash::{Hash, Hasher};
use std::ptr;
use std::str;

Expand All @@ -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<H: Hasher>(&self, state: &mut H) {
self.get_prefix().hash(state);
self.get_href().hash(state);
}
}

impl Namespace {
/// Creates a new namespace
pub fn new(
Expand Down
49 changes: 37 additions & 12 deletions src/tree/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,21 +656,41 @@ impl Node {
/// Get a copy of the attributes of this node
pub fn get_properties(&self) -> HashMap<String, String> {
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<Namespace>), 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
Expand All @@ -681,6 +701,11 @@ impl Node {
self.get_properties()
}

/// Alias for `get_properties_ns`
pub fn get_attributes_ns(&self) -> HashMap<(String, Option<Namespace>), String> {
self.get_properties_ns()
}

/// Gets the active namespace associated of this node
pub fn get_namespace(&self) -> Option<Namespace> {
let ns_ptr = xmlNodeNs(self.node_ptr());
Expand Down
11 changes: 11 additions & 0 deletions tests/resources/file01_ns.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<!--
According to the Namespaces in XML 1.1 (Second Edition) specification, https://www.w3.org/TR/xml-names11/#uniqAttrs, we should not have a node like this
<child xmlns:foo="http://www.example.com/myns" xmlns:bar="http://www.example.com/myns" attribute="value1" foo:attribute="foo1" bar:attribute="bar1">some text</child>
as both `foo` and `bar` have been bound to identical namespace names.
-->
<child xmlns:foo="http://www.example.com/myns" xmlns:bar="http://www.example.com/myns" attribute="value1" foo:attribute="foo1" bar:attr="bar1">some text</child>
<child xmlns:foo="http://www.example.com/myns" attribute="value2" foo:attribute="foo2" />
<child xmlns:bar="http://www.example.com/myns" attribute="value3" bar:attribute="bar3">more text</child>
</root>
127 changes: 127 additions & 0 deletions tests/tree_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 2380450

Please sign in to comment.