diff --git a/CHANGELOG.md b/CHANGELOG.md index ad527d4..43f26b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Currently we are using v0.0.x where every version can and will contain breaking changes. +## [Unreleased] +## Added +- Nothing yet. + +### Changed +- Make `Element` struct members private [#74] + +[#74]: https://github.com/webern/exile/pull/74 + ## [v0.0.2] - 2020-11-15 ### Added - Support for single-quoted attributes [#58] - `exile::load` for loading files [#58] - A lot of work on generating test cases with Java [#67], [#70], [#72] - ### Changed - The `xdoc` `Version` and `Encoding` enums were weird, changed to remove `None` [#59] - Added some mutating functions to `Document`, `Element`, and maybe others @@ -53,7 +61,7 @@ Currently we are using v0.0.x where every version can and will contain breaking [#52]: https://github.com/webern/exile/pull/52 -[Unreleased]: https://github.com/webern/exile/compare/v0.0.1...HEAD +[Unreleased]: https://github.com/webern/exile/compare/v0.0.2...HEAD [v0.0.2]: https://github.com/webern/exile/compare/v0.0.1...v0.0.2 [v0.0.1]: https://github.com/webern/exile/compare/v0.0.0...v0.0.1 [v0.0.0]: https://github.com/webern/exile/releases/tag/v0.0.0 diff --git a/Cargo.toml b/Cargo.toml index 410cb99..11bc621 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ description = "Nacent XML DOM Library" authors = ["Matthew James Briggs "] categories = ["encoding", "parser-implementations", "parsing"] edition = "2018" -exclude = ["tests/"] +exclude = ["bin/", "data/", "target/", "testgen/", "tests/"] keywords = ["xml"] license = "MIT OR Apache-2.0" readme = "README.md" diff --git a/README.md b/README.md index 7b72b8e..ffd657e 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ let xml = r#" let doc = exile::parse(xml).unwrap(); for child in doc.root().children() { - println!("element name: {}", child.name); - if let Some(attribute) = child.attributes.map().get("name") { + println!("element name: {}", child.name()); + if let Some(attribute) = child.attribute("name") { println!("name attribute: {}", attribute); } } @@ -50,19 +50,18 @@ Authoring XML looks like this. ```rust use exile::{Document, Element, Node}; let mut root = Element::from_name("my_root"); -// TODO - improve the interface -root.attributes.mut_map().insert("foo".into(), "bar".into()); +root.add_attribute("foo", "bar"); let mut child = Element::from_name("my_child"); -child.nodes.push(Node::Text("Hello World!".into())); -root.nodes.push(Node::Element(child)); +child.add_text("Hello World!"); +root.add_child(child); let doc = Document::from_root(root); println!("{}", doc.to_string()); ``` -The program above prints: +The above program prints: ```xml Hello World! -``` \ No newline at end of file +``` diff --git a/src/error.rs b/src/error.rs index c7ad87a..9b1fc7f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,6 +6,7 @@ use core::fmt; use std::fmt::{Display, Formatter}; use crate::parser::ParserState; +use crate::xdoc::error::XDocErr; //////////////////////////////////////////////////////////////////////////////////////////////////// // public error type @@ -19,6 +20,8 @@ pub type Result = std::result::Result; pub enum Error { /// A syntax error encountered when parsing an XML document. Parse(ParseError), + /// An error related to the `Document` model. + XdocErr(XDocErr), /// Any other error not related to the syntax of the XML document. Other(OtherError), } @@ -27,6 +30,7 @@ impl Display for crate::error::Error { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Error::Parse(pe) => pe.fmt(f), + Error::XdocErr(xe) => xe.fmt(f), Error::Other(oe) => oe.fmt(f), } } @@ -42,6 +46,13 @@ impl std::error::Error for crate::error::Error { None } } + Error::XdocErr(e) => { + if let Some(s) = &e.source { + Some(s.as_ref()) + } else { + None + } + } Error::Other(e) => { if let Some(s) = &e.source { Some(s.as_ref()) @@ -53,6 +64,12 @@ impl std::error::Error for crate::error::Error { } } +impl From for Error { + fn from(xe: XDocErr) -> Self { + Error::XdocErr(xe) + } +} + //////////////////////////////////////////////////////////////////////////////////////////////////// // public error data //////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/lib.rs b/src/lib.rs index ec3f589..39e1eb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,8 +35,8 @@ let xml = r#" let doc = exile::parse(xml).unwrap(); for child in doc.root().children() { - println!("element name: {}", child.name); - if let Some(attribute) = child.attributes.map().get("name") { + println!("element name: {}", child.name()); + if let Some(attribute) = child.attribute("name") { println!("name attribute: {}", attribute); } } @@ -47,22 +47,22 @@ Authoring XML looks like this. ``` use exile::{Document, Element, Node}; let mut root = Element::from_name("my_root"); -// TODO - improve the interface -root.attributes.mut_map().insert("foo".into(), "bar".into()); +root.add_attribute("foo", "bar"); let mut child = Element::from_name("my_child"); -child.nodes.push(Node::Text("Hello World!".into())); -root.nodes.push(Node::Element(child)); +child.add_text("Hello World!"); +root.add_child(child); let doc = Document::from_root(root); println!("{}", doc.to_string()); ``` -The program above prints: +The above program prints: ```xml Hello World! ``` + */ #![deny(rust_2018_idioms)] @@ -101,13 +101,13 @@ fn simple_document_test() { "#; let doc = parse(xml).unwrap(); let root = doc.root(); - assert_eq!("r", root.name.as_str()); - assert_eq!(1, root.nodes.len()); - let child = root.nodes.first().unwrap(); + assert_eq!("r", root.name()); + assert_eq!(1, root.nodes_len()); + let child = root.first_node().unwrap(); if let Node::Element(element) = child { - assert_eq!("a", element.name.as_str()); - let attribute_value = element.attributes.map().get("b").unwrap(); - assert_eq!("c", attribute_value.as_str()); + assert_eq!("a", element.name()); + let attribute_value = element.attribute("b").unwrap(); + assert_eq!("c", attribute_value); } else { panic!("expected element but found a different type of node") } diff --git a/src/parser/element.rs b/src/parser/element.rs index 23538aa..d48c9cc 100644 --- a/src/parser/element.rs +++ b/src/parser/element.rs @@ -3,7 +3,6 @@ use crate::parser::chars::is_name_start_char; use crate::parser::pi::parse_pi; use crate::parser::string::{parse_string, StringType}; use crate::parser::{parse_name, skip_comment, Iter}; -use crate::xdoc::OrdMap; use crate::{Element, Misc, Node}; pub(crate) fn parse_element(iter: &mut Iter<'_>) -> Result { @@ -24,7 +23,7 @@ pub(crate) fn parse_element(iter: &mut Iter<'_>) -> Result { // now the only valid chars are '>' or the start of an attribute name if iter.is_name_start_char() { - element.attributes = parse_attributes(iter)?; + parse_attributes(iter, &mut element)?; } // check and return early if it is an empty, self-closing tag that had attributes @@ -55,19 +54,14 @@ fn split_element_name(input: &str) -> Result<(&str, &str)> { fn make_named_element(input: &str) -> Result { let split = split_element_name(input)?; - Ok(Element { - namespace: match split.0 { - "" => None, - _ => Some(split.0.to_owned()), - }, - name: split.1.to_string(), - attributes: Default::default(), - nodes: vec![], - }) + let mut element = Element::from_name(split.1); + if !split.0.is_empty() { + element.set_prefix(split.0)? + } + Ok(element) } -fn parse_attributes(iter: &mut Iter<'_>) -> Result { - let mut attributes = OrdMap::new(); +fn parse_attributes(iter: &mut Iter<'_>, element: &mut Element) -> Result<()> { loop { iter.skip_whitespace()?; if iter.is('/') || iter.is('>') { @@ -86,12 +80,12 @@ fn parse_attributes(iter: &mut Iter<'_>) -> Result { iter.advance_or_die()?; let value = parse_attribute_value(iter, string_type)?; expect!(iter, start)?; - attributes.mut_map().insert(key, value); + element.add_attribute(key, value); if !iter.advance() { break; } } - Ok(attributes) + Ok(()) } fn attribute_start_quote(iter: &Iter<'_>) -> Result<(char, StringType)> { @@ -136,12 +130,12 @@ fn parse_children(iter: &mut Iter<'_>, parent: &mut Element) -> Result<()> { // do nothing } LTParse::Some(node) => { - parent.nodes.push(node); + parent.add_node(node); } } } else { let text = parse_text(iter)?; - parent.nodes.push(Node::Text(text)); + parent.add_node(Node::Text(text)); } // some parsing functions may return with the iter pointing to the last thing that was part // of their construct, while others might advance the iter to the next char *after* the diff --git a/src/xdoc/doc.rs b/src/xdoc/doc.rs index cccb4f2..9e8ebee 100644 --- a/src/xdoc/doc.rs +++ b/src/xdoc/doc.rs @@ -1,9 +1,10 @@ -use crate::xdoc::error::Result; -use crate::{Element, Misc, WriteOpts}; use std::borrow::Cow; use std::default::Default; use std::io::{Cursor, Write}; +use crate::xdoc::error::Result; +use crate::{Element, Misc, WriteOpts}; + #[derive(Debug, Clone, Eq, PartialOrd, PartialEq, Hash)] /// Represents the XML Version being used. pub enum Version { @@ -35,9 +36,12 @@ impl Default for Encoding { #[derive(Debug, Clone, Eq, PartialOrd, PartialEq, Hash, Default)] /// The XML declaration at the start of the XML Document. pub struct Declaration { - /// The version of the XML Document. Default is `Version::V10` when `None`. + /// The version of the XML Document. `None` is the same as `Version::V10` except that it is not + /// printed in the XML document. That is, XML defaults to 1.0 in the absence of a declaration. pub version: Option, - /// The encoding of the XML Document. Default is `Encoding::Utf8` when `None`. + /// The encoding of the XML Document. `None` is the same as `Encoding::Utf8` except that it is + /// not printed in the XML document. That is, XML defaults to `UTF-8` in the absence of a + /// declaration. pub encoding: Option, } @@ -266,40 +270,41 @@ macro_rules! map ( #[cfg(test)] mod tests { - use super::*; - use crate::xdoc::OrdMap; - use crate::Node; use std::io::Cursor; + use crate::Node; + + use super::*; + fn assert_ezfile(doc: &Document) { let root = doc.root(); let root_data = root; - assert_eq!(root_data.name, "cats"); - assert_eq!(root_data.namespace, None); - assert_eq!(root_data.attributes.map().len(), 0); - assert_eq!(root_data.nodes.len(), 2); - let bones_element = root_data.nodes.get(0).unwrap(); + assert_eq!("cats", root_data.name()); + assert_eq!(None, root_data.prefix()); + assert_eq!(0, root_data.attributes_len()); + assert_eq!(2, root_data.nodes_len()); + let bones_element = root_data.node(0).unwrap(); if let Node::Element(bones) = bones_element { - assert_eq!(bones.name, "cat"); - assert_eq!(bones.namespace, None); - assert_eq!(bones.attributes.map().len(), 1); - assert_eq!(bones.nodes.len(), 0); - let name = bones.attributes.map().get("name").unwrap(); - assert_eq!(name, "bones"); + assert_eq!("cat", bones.name()); + assert_eq!(None, bones.prefix()); + assert_eq!(1, bones.attributes_len()); + assert_eq!(0, bones.nodes_len()); + let name_attribute_value = bones.attribute("name").unwrap(); + assert_eq!("bones", name_attribute_value); } else { panic!("bones was supposed to be an element but was not"); } - let bishop_element = root_data.nodes.get(1).unwrap(); + let bishop_element = root_data.node(1).unwrap(); if let Node::Element(bishop) = bishop_element { - assert_eq!(bishop.name, "cat"); - assert_eq!(bishop.namespace, None); - assert_eq!(bishop.attributes.map().len(), 1); - let name = bishop.attributes.map().get("name").unwrap(); - assert_eq!(name, "bishop"); + assert_eq!("cat", bishop.name()); + assert_eq!(None, bishop.prefix()); + assert_eq!(1, bishop.attributes_len()); + let name_attribute_value = bishop.attribute("name").unwrap(); + assert_eq!("bishop", name_attribute_value); // assert text data - assert_eq!(bishop.nodes.len(), 1); - if let Node::Text(text) = bishop.nodes.get(0).unwrap() { - assert_eq!(text, "punks"); + assert_eq!(1, bishop.nodes_len()); + if let Node::Text(text) = bishop.node(0).unwrap() { + assert_eq!("punks", text); } else { panic!("Expected to find a text node but it was not there."); } @@ -315,40 +320,20 @@ mod tests { "#; fn create_ezfile() -> Document { - let bones_data = Element { - namespace: None, - name: "cat".into(), - attributes: OrdMap::from(map! { "name".to_string() => "bones".to_string() }), - nodes: Vec::default(), - }; - - let bishop_data = Element { - namespace: None, - name: "cat".into(), - attributes: OrdMap::from(map! { "name".to_string() => "bishop".to_string() }), - nodes: vec![Node::Text("punks".to_string())], - }; - - let bones_element = Node::Element(bones_data); - let bishop_element = Node::Element(bishop_data); - - let cats_data = Element { - namespace: None, - name: "cats".into(), - attributes: Default::default(), - nodes: vec![bones_element, bishop_element], - }; - - Document { - declaration: Declaration { - version: Some(Version::V10), - encoding: Some(Encoding::Utf8), - }, - doctypedecl: None, - prolog_misc: vec![], - root: cats_data, - epilog_misc: vec![], - } + let mut bones = Element::from_name("cat"); + bones.add_attribute("name", "bones"); + let mut bishop = Element::from_name("cat"); + bishop.add_attribute("name", "bishop"); + bishop.add_text("punks"); + let mut cats = Element::from_name("cats"); + cats.add_child(bones); + cats.add_child(bishop); + let mut doc = Document::from_root(cats); + doc.set_declaration(Declaration { + version: Some(Version::V10), + encoding: Some(Encoding::Utf8), + }); + doc } #[test] @@ -365,18 +350,16 @@ mod tests { assert!(result.is_ok()); let data = c.into_inner(); let data_str = std::str::from_utf8(data.as_slice()).unwrap(); - assert_eq!(data_str, EZFILE_STR); + assert_eq!(EZFILE_STR, data_str); } #[test] fn test_escapes() { let expected = r#"&&&<<<'"🍔"'>>>&&&"#; let mut root = Element::default(); - root.name = "root".into(); - root.attributes - .mut_map() - .insert("attr".into(), "<&>\"🍔\"\'\'".into()); - root.nodes.push(Node::Text("&&&<<<'\"🍔\"'>>>&&&".into())); + root.set_name("root"); + root.add_attribute("attr", "<&>\"🍔\"\'\'"); + root.add_text("&&&<<<'\"🍔\"'>>>&&&"); let doc = Document::from_root(root); let mut c = Cursor::new(Vec::new()); diff --git a/src/xdoc/element.rs b/src/xdoc/element.rs index d773958..71b9bec 100644 --- a/src/xdoc/element.rs +++ b/src/xdoc/element.rs @@ -1,28 +1,26 @@ use std::io::Write; -use crate::xdoc::error::{Result, XErr}; +use crate::xdoc::error::{Result, XDocErr}; +use crate::xdoc::ord_map::OrdMap; use crate::xdoc::write_ops::write_attribute_value; -use crate::xdoc::OrdMap; +use crate::xdoc::Name; use crate::{Misc, Node, WriteOpts, PI}; #[derive(Debug, Clone, Eq, PartialOrd, PartialEq, Hash)] /// Represents an Element in an XML Document. pub struct Element { - /// The namespace of this element. e.g. in `foo:bar`, `foo` is the namespace. - pub namespace: Option, - /// The name of this element. e.g. in `foo:bar`, `bar` is the name. - pub name: String, + /// The name of this element, which may contain a prefix part, such as `ns` in `ns:foo`. + name: Name, /// Attributes of this element. - pub attributes: OrdMap, + attributes: OrdMap, /// Children of this element. - pub nodes: Vec, + nodes: Vec, } impl Default for Element { fn default() -> Self { Self { - namespace: None, - name: "element".to_string(), + name: "element".into(), attributes: Default::default(), nodes: vec![], } @@ -33,7 +31,6 @@ impl Element { /// Create a new element using the given name. pub fn from_name>(name: S) -> Self { Element { - namespace: None, name: name.as_ref().into(), attributes: Default::default(), nodes: vec![], @@ -74,20 +71,69 @@ impl Element { self.nodes.push(Node::Element(element)) } - /// The fullname of the element (including both the namespace and the name). - pub fn fullname(&self) -> String { - if let Some(ns) = &self.namespace { - if !ns.is_empty() { - return format!("{}:{}", ns, self.name); - } - } - self.name.clone() + /// Add a node of any kind as a child of this element. + pub fn add_node(&mut self, node: Node) { + self.nodes.push(node) + } + + /// Get the number of nodes (of any kind) that are children of this node. + pub fn nodes_len(&self) -> usize { + self.nodes.len() + } + + /// Get the first child node (of any kind) + pub fn first_node(&self) -> Option<&Node> { + self.nodes.first() + } + + /// Get the child node (of any kind) at `index`. + pub fn node(&self, index: usize) -> Option<&Node> { + self.nodes.get(index) + } + + /// The fullname of the element (including both the namespace alias prefix and the name). For + /// example, if the name of this element is `ns:foo`, this function returns `"ns:foo"`. + /// [`Element::name`] and [`Element:prefix`] give the parsed sections of the fullname. + pub fn fullname(&self) -> &str { + self.name.full() + } + + /// The name of the element without its prefix. For example, if the name of this element is + /// `ns:foo`, `name()` will return `foo`. + pub fn name(&self) -> &str { + self.name.name() + } + + /// The name of the element's namespace alias prefix. For example, if the name of this element + /// is `ns:foo`, `prefix()` will return `Some("ns")`. + pub fn prefix(&self) -> Option<&str> { + self.name.prefix() } - // TODO - fullname? figure out namespace stuff - /// Sets the name of this element. + /// Sets the name of this element without changing the namespace alias prefix. For example, if + /// the name of this element is `ns:foo` then `set_name("bar")` will change the fullname to + /// `ns:bar`. pub fn set_name>(&mut self, name: S) { - self.name = name.as_ref().into() + // TODO check validity of the name, return an error? + self.name.set_name(name) + } + + /// Sets the namespace alias prefix of this element without changing the name. For example, if + /// the name of this element is `ns:foo` then `set_prefix("xyz:) will change the fullname to + /// `xyz:foo`. + pub fn set_prefix>(&mut self, prefix: S) -> Result<()> { + // TODO check validity of the prefix + self.name.set_prefix(prefix); + Ok(()) + } + + /// Sets the fullname of the element. For example, if the name of this element is `ns:foo`, then + /// `set_fullname("xyz:baz")` will set the fullname to `xyz:baz`. `set_fullname("baz")` will + /// eliminate any existing namespace alias prefix and set the fullname to `baz`. + pub fn set_fullname>(&mut self, fullname: S) -> Result<()> { + // TODO check validity of the fullname + self.name.set_full(fullname); + Ok(()) } /// Inserts a key-value pair into the attributes map. @@ -107,10 +153,20 @@ impl Element { .insert(key.as_ref().into(), value.as_ref().into()) } + /// Gets the attribute value at `key`. `None` if an attribute by that name does not exist. + pub fn attribute>(&self, key: S) -> Option<&str> { + self.attributes.map().get(key.as_ref()).map(|s| s.as_str()) + } + + /// Gets the count of attributes. + pub fn attributes_len(&self) -> usize { + self.attributes.map().len() + } + /// Creates a new element as the last child of this element and returns a mut ref to it. pub fn add_new_child(&mut self) -> Result<&mut Element> { self.nodes.push(Node::Element(Element::default())); - let new_node = self.nodes.last_mut().ok_or_else(|| XErr { + let new_node = self.nodes.last_mut().ok_or_else(|| XDocErr { message: "the sky is falling".to_string(), file: "".to_string(), line: 0, @@ -119,7 +175,7 @@ impl Element { if let Node::Element(new_element) = new_node { Ok(new_element) } else { - Err(XErr { + Err(XDocErr { message: "the sky is still falling".to_string(), file: "".to_string(), line: 0, @@ -195,14 +251,7 @@ impl Element { if let Err(e) = write!(writer, "<") { return wrap!(e); } - if let Some(ns) = &self.namespace { - if !ns.is_empty() { - if let Err(e) = write!(writer, "{}:", ns) { - return wrap!(e); - } - } - } - if let Err(e) = write!(writer, "{}", self.name) { + if let Err(e) = write!(writer, "{}", self.fullname()) { return wrap!(e); } @@ -254,7 +303,7 @@ impl Element { if let Err(e) = write!(writer, "") { return wrap!(e); } - // if let Err(e) = opts.newline(writer) { - // return wrap!(e); - // } Ok(()) } @@ -277,7 +323,7 @@ impl Element { if self.name.is_empty() { return raise!("Empty element name."); } - if let Some(ns) = &self.namespace { + if let Some(ns) = self.prefix() { if ns.is_empty() { return raise!("Namespace should not be empty when the option is 'some'."); } diff --git a/src/xdoc/error.rs b/src/xdoc/error.rs index 4ba53ef..70e4da6 100644 --- a/src/xdoc/error.rs +++ b/src/xdoc/error.rs @@ -2,11 +2,11 @@ use std::error::Error; use std::fmt; /// The `Result` type for this library. -pub type Result = std::result::Result; +pub type Result = std::result::Result; /// A generic error type for this library. #[derive(Debug)] -pub struct XErr { +pub struct XDocErr { /// The error message. pub message: String, /// The sourcecode file where the error was raised. @@ -17,7 +17,7 @@ pub struct XErr { pub source: Option>, } -impl fmt::Display for XErr { +impl fmt::Display for XDocErr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(src) = &self.source { write!( @@ -34,7 +34,7 @@ impl fmt::Display for XErr { } } -impl Error for XErr { +impl Error for XDocErr { fn source(&self) -> Option<&(dyn Error + 'static)> { if let Some(src) = &self.source { return Some(src.as_ref()); @@ -45,19 +45,19 @@ impl Error for XErr { macro_rules! wrap { // Base case: - ($err:expr) => (Err($crate::xdoc::error::XErr { + ($err:expr) => (Err($crate::xdoc::error::XDocErr { message: "an error occurred".to_string(), file: file!().to_string(), line: line!() as u64, source: Some($err.into()), })); - ($err:expr, $msg:expr) => (Err($crate::xdoc::error::XErr { + ($err:expr, $msg:expr) => (Err($crate::xdoc::error::XDocErr { message: $msg.to_string(), file: file!().to_string(), line: line!() as u64, source: Some($err.into()), })); - ($err:expr, $fmt:expr, $($arg:expr),+) => (Err($crate::xdoc::error::XErr { + ($err:expr, $fmt:expr, $($arg:expr),+) => (Err($crate::xdoc::error::XDocErr { message: format!($fmt, $($arg),+), file: file!().to_string(), line: line!() as u64, @@ -77,7 +77,7 @@ macro_rules! better_wrap { // a convenience macro for creating a Result::Err macro_rules! raise { // Base case: - ($msg:expr) => (Err($crate::xdoc::error::XErr { + ($msg:expr) => (Err($crate::xdoc::error::XDocErr { message: $msg.to_string(), file: file!().to_string(), line: line!() as u64, diff --git a/src/xdoc/mod.rs b/src/xdoc/mod.rs index f00a90e..6844106 100644 --- a/src/xdoc/mod.rs +++ b/src/xdoc/mod.rs @@ -15,8 +15,8 @@ pub use chars::{contains_whitespace, is_whitespace}; pub use doc::Document; pub use doc::{Declaration, Encoding, Version}; pub use element::Element; +pub(crate) use name::Name; pub use node::{Misc, Node}; -pub use ord_map::OrdMap; pub use pi::PI; pub use write_ops::{Newline, WriteOpts}; @@ -29,8 +29,9 @@ pub mod error; mod chars; mod doc; mod element; +mod name; mod node; -mod ord_map; +pub(crate) mod ord_map; mod pi; mod write_ops; @@ -42,13 +43,7 @@ mod tests { #[test] fn structs_test() { - let mut doc = Document::new(); - doc.set_root(Element { - namespace: None, - name: "root-element".into(), - attributes: Default::default(), - nodes: vec![], - }); + let doc = Document::from_root(Element::from_name("root-element")); let mut c = Cursor::new(Vec::new()); let result = doc.write(&mut c); assert!(result.is_ok()); diff --git a/src/xdoc/name.rs b/src/xdoc/name.rs new file mode 100644 index 0000000..997ace1 --- /dev/null +++ b/src/xdoc/name.rs @@ -0,0 +1,206 @@ +use std::fmt::{Display, Formatter}; +use std::ops::Deref; + +/// Both attributes and elements can have a namespace alias prefix, such as `ns:foo`, where `ns` is +/// the 'prefix' and 'foo' is the 'name'. The `Name` struct provides the convenience of parsing and +/// differentiating the 'prefix' and 'name' parts. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Name { + value: String, +} + +impl Name { + /// Instantiates a new name using `full` as the fullname. For example `new("ns:foo")` creates a + /// new name with prefix `ns` and name `foo`. `new("foo")` creates a new name with no prefix and + /// name `foo`, etc. + pub(crate) fn new>(full: S) -> Self { + Self { value: full.into() } + } + + /// Returns the 'name' part. For example, if the `fullname` is `ns:foo`, this function returns + /// `foo`. + pub(crate) fn name(&self) -> &str { + match self.find() { + None => self.value.as_str(), + Some(pos) => { + let s = self.value.as_str(); + if pos == s.len() - 1 { + "" + } else { + &s[pos + 1..] + } + } + } + } + + /// Returns the 'prefix' part. For example, if the `fullname` is `ns:foo`, this function returns + /// `ns`. + pub(crate) fn prefix(&self) -> Option<&str> { + match self.find() { + None => None, + Some(pos) => { + let s = self.value.as_str(); + Some(&s[..pos]) + } + } + } + + /// Returns both the 'prefix' and the 'name', for example `ns:foo`. + pub(crate) fn full(&self) -> &str { + self.value.as_str() + } + + /// Sets the 'name' part. For example, if the value was `ns::foo`, then + /// `set_name("bar")` would set it to `ns:bar`. + pub(crate) fn set_name>(&mut self, name: S) { + match self.prefix() { + None => self.value = name.as_ref().into(), + Some(prefix) => { + let mut value = String::with_capacity(prefix.len() + 1 + name.as_ref().len()); + value.push_str(prefix); + value.push(':'); + value.push_str(name.as_ref()); + self.value = value; + } + } + } + + /// Sets the 'prefix' part. For example, if the value was `ns::foo`, then + /// `set_prefix("xyz")` would set it to `xyz:foo`. + pub(crate) fn set_prefix>(&mut self, prefix: S) { + let name = self.name(); + let mut value = String::with_capacity(prefix.as_ref().len() + 1 + name.len()); + value.push_str(prefix.as_ref()); + value.push(':'); + value.push_str(name.as_ref()); + self.value = value; + } + + /// Sets the complete value. For example, if the value was `ns::foo`, then + /// `set_full("xyz:bar")` would set it to `xyz:bar`. + pub(crate) fn set_full>(&mut self, full: S) { + self.value = full.into(); + } +} + +impl Display for Name { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.full()) + } +} + +impl AsRef for Name { + fn as_ref(&self) -> &str { + self.full().as_ref() + } +} + +impl AsRef for Name { + fn as_ref(&self) -> &String { + &self.value + } +} + +impl Into for Name { + fn into(self) -> String { + self.value + } +} + +impl From<&str> for Name { + fn from(s: &str) -> Self { + Name::new(s) + } +} + +impl Deref for Name { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl From for Name { + fn from(value: String) -> Self { + Self { value } + } +} + +impl Name { + fn find(&self) -> Option { + self.value.as_str().find(':') + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#[test] +fn ns_foo() { + let name = Name::new("ns:foo"); + assert_eq!("ns", name.prefix().unwrap()); + assert_eq!("foo", name.name()); + assert_eq!("ns:foo", name.full()); +} + +#[test] +fn no_prefix() { + let name = Name::new("nsfoo"); + assert!(name.prefix().is_none()); + assert_eq!("nsfoo", name.name()); + assert_eq!("nsfoo", name.full()); +} + +#[test] +fn weird_colons() { + let name = Name::new(":ns:foo:"); + assert_eq!("", name.prefix().unwrap()); + assert_eq!("ns:foo:", name.name()); + assert_eq!(":ns:foo:", name.full()); +} + +#[test] +fn no_name_test() { + let name = Name::new("foo:"); + assert_eq!("foo", name.prefix().unwrap()); + assert_eq!("", name.name()); + assert_eq!("foo:", name.full()); +} + +#[test] +fn set_prefix() { + let mut name = Name::new("xyz:bones"); + name.set_prefix("cat"); + assert_eq!("cat", name.prefix().unwrap()); + assert_eq!("bones", name.name()); + assert_eq!("cat:bones", name.full()); +} + +#[test] +fn set_name_test() { + let mut name = Name::new("xyz:bones"); + name.set_name("bishop"); + assert_eq!("xyz", name.prefix().unwrap()); + assert_eq!("bishop", name.name()); + assert_eq!("xyz:bishop", name.full()); +} + +#[test] +fn set_full_test() { + let mut name = Name::new("xyz:bones"); + name.set_full("cat:bishop"); + assert_eq!("cat", name.prefix().unwrap()); + assert_eq!("bishop", name.name()); + assert_eq!("cat:bishop", name.full()); +} + +#[test] +fn find_test() { + assert!(Name::new("xyz").find().is_none()); + assert!(Name::new("").find().is_none()); + assert_eq!(0, Name::new(":xyz").find().unwrap()); + assert_eq!(0, Name::new(":x:yz").find().unwrap()); + assert_eq!(1, Name::new("x:yz").find().unwrap()); + assert_eq!(3, Name::new("xyz:").find().unwrap()); + assert_eq!(0, Name::new(":").find().unwrap()); +} diff --git a/src/xdoc/ord_map.rs b/src/xdoc/ord_map.rs index 0247223..f121f67 100644 --- a/src/xdoc/ord_map.rs +++ b/src/xdoc/ord_map.rs @@ -3,22 +3,9 @@ use std::cmp::Ordering; use std::collections::BTreeMap; use std::hash::{Hash, Hasher}; -// TODO - extract key and value types -/// OrdMap implements some conveniences like Clone an PartialEq for maps so that we can compare -/// XML Documents (and do other things). -pub struct OrdMap(BTreeMap); - -impl OrdMap { - /// Create a new, empty OrdMap - pub fn new() -> Self { - OrdMap(BTreeMap::new()) - } - - /// Construct a new OrdMap from a BTreeMap - pub fn from(inner: BTreeMap) -> Self { - OrdMap(inner) - } -} +/// OrdMap implements some conveniences like Clone and PartialEq for BTreeMap so that we can compare +/// XML Documents. +pub(crate) struct OrdMap(BTreeMap); impl Clone for OrdMap { fn clone(&self) -> Self { @@ -58,12 +45,12 @@ impl Eq for OrdMap {} impl OrdMap { /// Return the inner BTreeMap as immutable. - pub fn map(&self) -> &BTreeMap { + pub(crate) fn map(&self) -> &BTreeMap { &self.0 } /// Return the inner BTreeMap as mutable. - pub fn mut_map(&mut self) -> &mut BTreeMap { + pub(crate) fn mut_map(&mut self) -> &mut BTreeMap { &mut self.0 } @@ -147,6 +134,8 @@ impl Hash for OrdMap { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use super::OrdMap; /// Although this test does nothing but explore the behavior of a built-in data structure, it's @@ -154,7 +143,7 @@ mod tests { /// serialize in the order that they were inserted. #[test] fn map_insertion_order() { - let mut a = OrdMap::new(); + let mut a = OrdMap(BTreeMap::new()); a.mut_map().insert("0".to_string(), String::new()); a.mut_map().insert("1".to_string(), String::new()); a.mut_map().insert("2".to_string(), String::new()); @@ -170,13 +159,13 @@ mod tests { /// insertion order. #[test] fn map_equality() { - let mut a = OrdMap::new(); + let mut a = OrdMap(BTreeMap::new()); a.mut_map().insert("0".to_string(), "a".to_string()); a.mut_map().insert("1".to_string(), "b".to_string()); a.mut_map().insert("2".to_string(), "c".to_string()); a.mut_map().insert("3".to_string(), "d".to_string()); a.mut_map().insert("4".to_string(), "e".to_string()); - let mut b = OrdMap::new(); + let mut b = OrdMap(BTreeMap::new()); b.mut_map().insert("4".to_string(), "e".to_string()); b.mut_map().insert("3".to_string(), "d".to_string()); b.mut_map().insert("1".to_string(), "b".to_string());