maxep преди 5 години
родител
ревизия
fe273999a0

+ 7 - 1
Package.swift

@@ -84,7 +84,13 @@ let package = Package(
 
         .target(
             name: "XML",
-            dependencies: []
+            dependencies: [],
+            exclude: ["LICENSE"]
+        ),
+        .testTarget(
+            name: "XMLTests",
+            dependencies: ["XML"],
+            resources: [.process("Resources")]
         ),
 
         // MARK: KeePass Cryptographic Libraries

+ 14 - 18
Sources/XML/Document.swift

@@ -20,12 +20,10 @@ open class Document: Element {
 
     /// Root (the first child element) element of XML Document **(Empty element with error if not exists)**.
     open var root: Element {
-        guard let rootElement = children.first else {
-            let errorElement = Element(name: "Error")
-            errorElement.error = XMLError.rootElementMissing
-            return errorElement
-        }
-        return rootElement
+        if let element = children.first { return element }
+        let element = Element(name: "Error")
+        element.error = XMLError.rootElementMissing
+        return element
     }
 
     public let options: Options
@@ -40,18 +38,18 @@ open class Document: Element {
 
          - returns: Initialized XML Document object.
      */
-    public init(root: Element? = nil, options: Options = Options()) {
+    public init(root: Element? = nil, options: Options = .init()) {
         self.options = options
 
-        let documentName = String(describing: Document.self)
-        super.init(name: documentName)
+        let name = String(describing: Document.self)
+        super.init(name: name)
 
         // document has no parent element
         parent = nil
 
         // add root element to document (if any)
-        if let rootElement = root {
-            addChild(rootElement)
+        if let root = root {
+            addChild(root)
         }
     }
 
@@ -63,7 +61,7 @@ open class Document: Element {
 
          - returns: Initialized XML Document object containing parsed data. Throws error if data could not be parsed.
      */
-    public convenience init(xml: Data, options: Options = Options()) throws {
+    public convenience init(xml: Data, options: Options = .init()) throws {
         self.init(options: options)
         try load(xml)
     }
@@ -78,7 +76,7 @@ open class Document: Element {
          - returns: Initialized XML Document object containing parsed data. Throws error if data could not be parsed.
      */
     public convenience init(xml: String,
-                            encoding: String.Encoding = String.Encoding.utf8,
+                            encoding: String.Encoding = .utf8,
                             options: Options = Options()) throws {
         guard let data = xml.data(using: encoding) else { throw XMLError.parsingFailed }
         try self.init(xml: data, options: options)
@@ -94,16 +92,14 @@ open class Document: Element {
      */
     open func load(_ data: Data) throws {
         children.removeAll(keepingCapacity: false)
-        let xmlParser = Parser(document: self, data: data)
-        try xmlParser.parse()
+        let parser = Parser(document: self, data: data)
+        try parser.parse()
     }
 
     // MARK: - Override
 
     /// Override of `xml` property of `Element` - it just inserts XML Document header at the beginning.
     override open var xml: String {
-        var xml = "\(options.documentHeader.xmlString)\n"
-        xml += root.xml
-        return xml
+        "\(options.documentHeader.xml)\n\(root.xml)"
     }
 }

+ 17 - 12
Sources/XML/Element.swift

@@ -72,6 +72,11 @@ open class Element {
         self[member]
     }
 
+    public subscript<T>(dynamicMember member: String) -> T? where T: LosslessStringConvertible {
+        guard let string = self[member].value, let value = T(string) else { return nil }
+        return value
+    }
+
     open func get<T>() throws -> T where T: LosslessStringConvertible {
         guard let string = value, let value = T(string) else { throw XMLError.valueConversionFailed }
         return value
@@ -82,22 +87,22 @@ open class Element {
         return formatter.date(from: value)
     }
 
-    open func date(formatter: ISO8601DateFormatter = ISO8601DateFormatter()) -> Date? {
+    open func date(formatter: ISO8601DateFormatter = .init()) -> Date? {
         guard let value = value else { return nil }
         return formatter.date(from: value)
     }
 
     /// Returns all of the elements with equal name as `self` **(nil if not exists)**.
-    open var all: [Element]? { return parent?.children.filter { $0.name == self.name } }
+    open var all: [Element]? { parent?.children.filter { $0.name == self.name } }
 
     /// Returns the first element with equal name as `self` **(nil if not exists)**.
-    open var first: Element? { return all?.first }
+    open var first: Element? { all?.first }
 
     /// Returns the last element with equal name as `self` **(nil if not exists)**.
-    open var last: Element? { return all?.last }
+    open var last: Element? { all?.last }
 
     /// Returns number of all elements with equal name as `self`.
-    open var count: Int { return all?.count ?? 0 }
+    open var count: Int { all?.count ?? 0 }
 
     /**
          Returns all elements with given value.
@@ -107,7 +112,7 @@ open class Element {
          - returns: Optional Array of found XML elements.
      */
     open func all(withValue value: String) -> [Element]? {
-        return all?.compactMap { $0.value == value ? $0 : nil }
+        all?.compactMap { $0.value == value ? $0 : nil }
     }
 
     /**
@@ -118,7 +123,7 @@ open class Element {
          - returns: Optional Array of found XML elements.
      */
     open func all(containingAttributeKeys keys: [String]) -> [Element]? {
-        return all?.compactMap { element in
+        all?.compactMap { element in
             keys.reduce(true) { result, key in
                 result && Array(element.attributes.keys).contains(key)
             } ? element : nil
@@ -173,11 +178,11 @@ open class Element {
 
          - returns: Optional Element.
      */
-    open func firstDescendant(where predicate: (Element) -> Bool) -> Element? {
+    open func firstDescendant(where predicate: (Element) throws -> Bool) rethrows -> Element? {
         for child in children {
-            if predicate(child) {
+            if try predicate(child) {
                 return child
-            } else if let descendant = child.firstDescendant(where: predicate) {
+            } else if let descendant = try child.firstDescendant(where: predicate) {
                 return descendant
             }
         }
@@ -191,8 +196,8 @@ open class Element {
 
          - returns: Bool.
      */
-    open func hasDescendant(where predicate: (Element) -> Bool) -> Bool {
-        return firstDescendant(where: predicate) != nil
+    open func hasDescendant(where predicate: (Element) throws -> Bool) rethrows -> Bool {
+        try firstDescendant(where: predicate) != nil
     }
 
     // MARK: - XML Write

+ 19 - 0
Sources/XML/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2014-2020 Marko Tadić <tadija@me.com> http://tadija.net
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 1 - 1
Sources/XML/Options.swift

@@ -24,7 +24,7 @@ public struct Options {
         public var standalone = "no"
 
         /// XML Document header
-        public var xmlString: String {
+        public var xml: String {
             return "<?xml version=\"\(version)\" encoding=\"\(encoding)\" standalone=\"\(standalone)\"?>"
         }
     }

+ 15 - 0
Tests/XMLTests/Resources/example.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<animals>
+    <cats>
+        <cat breed="Siberian" color="lightgray">Tinna</cat>
+        <cat breed="Domestic" color="darkgray">Rose</cat>
+        <cat breed="Domestic" color="yellow">Caesar</cat>
+        <cat></cat>
+    </cats>
+    <dogs>
+        <dog breed="Bull Terrier" color="white">Villy</dog>
+        <dog breed="Bull Terrier" color="white" gender="male">Spot</dog>
+        <dog breed="Golden Retriever" color="yellow">Betty</dog>
+        <dog breed="Miniature Schnauzer" color="black" gender="female">Kika</dog>
+    </dogs>
+</animals>

+ 300 - 0
Tests/XMLTests/Resources/plant_catalog.xml

@@ -0,0 +1,300 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<!-- slightly modified http://www.w3schools.com/xml/plant_catalog.xml -->
+<CATALOG>
+	<PLANT>
+		<COMMON>Bloodroot</COMMON>\n\t
+		<BOTANICAL>Sanguinaria canadensis</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>2.44</PRICE>
+		<AVAILABILITY>031599</AVAILABILITY>
+        <TRUESTRING>true</TRUESTRING>
+        <FALSESTRING>false</FALSESTRING>
+        <TRUESTRING2>True</TRUESTRING2>
+        <FALSESTRING2>False</FALSESTRING2>
+        <TRUEINT>1</TRUEINT>
+        <FALSEINT>0</FALSEINT>
+        <ELEMENTWITHOUTVALUE></ELEMENTWITHOUTVALUE>
+        <EMPTYELEMENT />
+	</PLANT>
+	<PLANT>
+		<COMMON>Columbine</COMMON>
+		<BOTANICAL>Aquilegia canadensis</BOTANICAL>
+		<ZONE>3</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>9.37</PRICE>
+		<AVAILABILITY>030699</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Marsh Marigold</COMMON>
+		<BOTANICAL>Caltha palustris</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Mostly Sunny</LIGHT>
+		<PRICE>6.81</PRICE>
+		<AVAILABILITY>051799</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Cowslip</COMMON>
+		<BOTANICAL>Caltha palustris</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>9.90</PRICE>
+		<AVAILABILITY>030699</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Dutchman's-Breeches</COMMON>
+		<BOTANICAL>Dicentra cucullaria</BOTANICAL>
+		<ZONE>3</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>6.44</PRICE>
+		<AVAILABILITY>012099</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Ginger, Wild</COMMON>
+		<BOTANICAL>Asarum canadense</BOTANICAL>
+		<ZONE>3</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>9.03</PRICE>
+		<AVAILABILITY>041899</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Hepatica</COMMON>
+		<BOTANICAL>Hepatica americana</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>4.45</PRICE>
+		<AVAILABILITY>012699</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Liverleaf</COMMON>
+		<BOTANICAL>Hepatica americana</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>3.99</PRICE>
+		<AVAILABILITY>010299</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Jack-In-The-Pulpit</COMMON>
+		<BOTANICAL>Arisaema triphyllum</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>3.23</PRICE>
+		<AVAILABILITY>020199</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Mayapple</COMMON>
+		<BOTANICAL>Podophyllum peltatum</BOTANICAL>
+		<ZONE>3</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>2.98</PRICE>
+		<AVAILABILITY>060599</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Phlox, Woodland</COMMON>
+		<BOTANICAL>Phlox divaricata</BOTANICAL>
+		<ZONE>3</ZONE>
+		<LIGHT>Sun or Shade</LIGHT>
+		<PRICE>2.80</PRICE>
+		<AVAILABILITY>012299</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Phlox, Blue</COMMON>
+		<BOTANICAL>Phlox divaricata</BOTANICAL>
+		<ZONE>3</ZONE>
+		<LIGHT>Sun or Shade</LIGHT>
+		<PRICE>5.59</PRICE>
+		<AVAILABILITY>021699</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Spring-Beauty</COMMON>
+		<BOTANICAL>Claytonia Virginica</BOTANICAL>
+		<ZONE>7</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>6.59</PRICE>
+		<AVAILABILITY>020199</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Trillium</COMMON>
+		<BOTANICAL>Trillium grandiflorum</BOTANICAL>
+		<ZONE>5</ZONE>
+		<LIGHT>Sun or Shade</LIGHT>
+		<PRICE>3.90</PRICE>
+		<AVAILABILITY>042999</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Wake Robin</COMMON>
+		<BOTANICAL>Trillium grandiflorum</BOTANICAL>
+		<ZONE>5</ZONE>
+		<LIGHT>Sun or Shade</LIGHT>
+		<PRICE>3.20</PRICE>
+		<AVAILABILITY>022199</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Violet, Dog-Tooth</COMMON>
+		<BOTANICAL>Erythronium americanum</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>9.04</PRICE>
+		<AVAILABILITY>020199</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Trout Lily</COMMON>
+		<BOTANICAL>Erythronium americanum</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>6.94</PRICE>
+		<AVAILABILITY>032499</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Adder's-Tongue</COMMON>
+		<BOTANICAL>Erythronium americanum</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>9.58</PRICE>
+		<AVAILABILITY>041399</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Anemone</COMMON>
+		<BOTANICAL>Anemone blanda</BOTANICAL>
+		<ZONE>6</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>8.86</PRICE>
+		<AVAILABILITY>122698</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Grecian Windflower</COMMON>
+		<BOTANICAL>Anemone blanda</BOTANICAL>
+		<ZONE>6</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>9.16</PRICE>
+		<AVAILABILITY>071099</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Bee Balm</COMMON>
+		<BOTANICAL>Monarda didyma</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>4.59</PRICE>
+		<AVAILABILITY>050399</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Bergamot</COMMON>
+		<BOTANICAL>Monarda didyma</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>7.16</PRICE>
+		<AVAILABILITY>042799</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Black-Eyed Susan</COMMON>
+		<BOTANICAL>Rudbeckia hirta</BOTANICAL>
+		<ZONE>Annual</ZONE>
+		<LIGHT>Sunny</LIGHT>
+		<PRICE>9.80</PRICE>
+		<AVAILABILITY>061899</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Buttercup</COMMON>
+		<BOTANICAL>Ranunculus</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>2.57</PRICE>
+		<AVAILABILITY>061099</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Crowfoot</COMMON>
+		<BOTANICAL>Ranunculus</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>9.34</PRICE>
+		<AVAILABILITY>040399</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Butterfly Weed</COMMON>
+		<BOTANICAL>Asclepias tuberosa</BOTANICAL>
+		<ZONE>Annual</ZONE>
+		<LIGHT>Sunny</LIGHT>
+		<PRICE>2.78</PRICE>
+		<AVAILABILITY>063099</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Cinquefoil</COMMON>
+		<BOTANICAL>Potentilla</BOTANICAL>
+		<ZONE>Annual</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>7.06</PRICE>
+		<AVAILABILITY>052599</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Primrose</COMMON>
+		<BOTANICAL>Oenothera</BOTANICAL>
+		<ZONE>3 - 5</ZONE>
+		<LIGHT>Sunny</LIGHT>
+		<PRICE>6.56</PRICE>
+		<AVAILABILITY>013099</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Gentian</COMMON>
+		<BOTANICAL>Gentiana</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Sun or Shade</LIGHT>
+		<PRICE>7.81</PRICE>
+		<AVAILABILITY>051899</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Blue Gentian</COMMON>
+		<BOTANICAL>Gentiana</BOTANICAL>
+		<ZONE>4</ZONE>
+		<LIGHT>Sun or Shade</LIGHT>
+		<PRICE>8.56</PRICE>
+		<AVAILABILITY>050299</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Jacob's Ladder</COMMON>
+		<BOTANICAL>Polemonium caeruleum</BOTANICAL>
+		<ZONE>Annual</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>9.26</PRICE>
+		<AVAILABILITY>022199</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Greek Valerian</COMMON>
+		<BOTANICAL>Polemonium caeruleum</BOTANICAL>
+		<ZONE>Annual</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>4.36</PRICE>
+		<AVAILABILITY>071499</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>California Poppy</COMMON>
+		<BOTANICAL>Eschscholzia californica</BOTANICAL>
+		<ZONE>Annual</ZONE>
+		<LIGHT>Sun</LIGHT>
+		<PRICE>7.89</PRICE>
+		<AVAILABILITY>032799</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Shooting Star</COMMON>
+		<BOTANICAL>Dodecatheon</BOTANICAL>
+		<ZONE>Annual</ZONE>
+		<LIGHT>Mostly Shady</LIGHT>
+		<PRICE>8.60</PRICE>
+		<AVAILABILITY>051399</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Snakeroot</COMMON>
+		<BOTANICAL>Cimicifuga</BOTANICAL>
+		<ZONE>Annual</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>5.63</PRICE>
+		<AVAILABILITY>071199</AVAILABILITY>
+	</PLANT>
+	<PLANT>
+		<COMMON>Cardinal Flower</COMMON>
+		<BOTANICAL>Lobelia cardinalis</BOTANICAL>
+		<ZONE>2</ZONE>
+		<LIGHT>Shade</LIGHT>
+		<PRICE>3.02</PRICE>
+		<AVAILABILITY>022299</AVAILABILITY>
+	</PLANT>
+</CATALOG>

+ 5 - 0
Tests/XMLTests/Resources/whitespace_examples.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<paragraph>
+    <text>Hello, </text>
+    <text>how are you?</text>
+</paragraph>

+ 437 - 0
Tests/XMLTests/XMLTests.swift

@@ -0,0 +1,437 @@
+/**
+ *  https://github.com/tadija/AEXML
+ *  Copyright © Marko Tadić 2014-2020
+ *  Licensed under the MIT license
+ */
+
+import Foundation
+#if canImport(FoundationXML)
+import FoundationXML
+#endif
+
+import XCTest
+@testable import XML
+
+class XMLTests: XCTestCase {
+    
+    // MARK: - Properties
+    
+    var exampleDocument = Document()
+    var plantsDocument = Document()
+    
+    // MARK: - Helpers
+    
+    func URLForResource(fileName: String, withExtension ext: String) -> URL {
+        guard let url = Bundle.module.url(forResource: fileName, withExtension: ext) else {
+            fatalError("can't find resource named: '\(fileName)'")
+        }
+        return url
+    }
+    
+    func xmlDocumentFromURL(url: URL) -> Document {
+        var xmlDocument = Document()
+        
+        do {
+            let data = try Data.init(contentsOf: url)
+            xmlDocument = try Document(xml: data)
+        } catch {
+            print(error)
+        }
+        
+        return xmlDocument
+    }
+    
+    func readXMLFromFile(filename: String) -> Document {
+        let url = URLForResource(fileName: filename, withExtension: "xml")
+        return xmlDocumentFromURL(url: url)
+    }
+    
+    // MARK: - Setup & Teardown
+    
+    override func setUp() {
+        super.setUp()
+        
+        // create some sample xml documents
+        exampleDocument = readXMLFromFile(filename: "example")
+        plantsDocument = readXMLFromFile(filename: "plant_catalog")
+    }
+    
+    override func tearDown() {
+        // reset sample xml document
+        exampleDocument = Document()
+        plantsDocument = Document()
+        
+        super.tearDown()
+    }
+    
+    // MARK: - XML Document
+    
+    func testXMLDocumentManualDataLoading() {
+        do {
+            let url = URLForResource(fileName: "example", withExtension: "xml")
+            let data = try Data.init(contentsOf: url)
+            
+            let testDocument = Document()
+            try testDocument.load(data)
+            XCTAssertEqual(testDocument.root.name, "animals", "Should be able to find root element.")
+        } catch {
+            XCTFail("Should be able to load XML Document with given Data.")
+        }
+    }
+    
+    func testXMLDocumentInitFromString() {
+        do {
+            let testDocument = try Document(xml: exampleDocument.xml)
+            XCTAssertEqual(testDocument.xml, exampleDocument.xml)
+        } catch {
+            XCTFail("Should be able to initialize XML Document from XML String.")
+        }
+    }
+    
+    func testXMLOptions() {
+        do {
+            var options = Options()
+            options.documentHeader.version = 2.0
+            options.documentHeader.encoding = "utf-16"
+            options.documentHeader.standalone = "yes"
+            
+            let testDocument = try Document(xml: "<foo><bar>hello</bar></foo>", options: options)
+            XCTAssertEqual(testDocument.xml, "<?xml version=\"2.0\" encoding=\"utf-16\" standalone=\"yes\"?>\n<foo>\n\t<bar>hello</bar>\n</foo>")
+            XCTAssertEqual(try testDocument.root.bar.first?.get(), "hello")
+        } catch {
+            XCTFail("Should be able to initialize XML Document with custom Options.")
+        }
+    }
+    
+    func testXMLParser() {
+        do {
+            let testDocument = Document()
+            let url = URLForResource(fileName: "example", withExtension: "xml")
+            let data = try Data.init(contentsOf: url)
+            
+            let parser = Parser(document: testDocument, data: data)
+            try parser.parse()
+            
+            XCTAssertEqual(testDocument.root.name, "animals", "Should be able to find root element.")
+        } catch {
+            XCTFail("Should be able to parse XML Data into XML Document without throwing error.")
+        }
+    }
+    
+    func testXMLParserTrimsWhitespace() {
+        let result = whitespaceResult(shouldTrimWhitespace: true)
+        XCTAssertEqual(result, "Hello,")
+    }
+    
+    func testXMLParserWithoutTrimmingWhitespace() {
+        let result = whitespaceResult(shouldTrimWhitespace: false)
+        XCTAssertEqual(result, "Hello, ")
+    }
+    
+    private func whitespaceResult(shouldTrimWhitespace: Bool) -> String? {
+        do {
+            var options = Options()
+            options.parserSettings.shouldTrimWhitespace = shouldTrimWhitespace
+            
+            let testDocument = Document(options: options)
+            let url = URLForResource(fileName: "whitespace_examples", withExtension: "xml")
+            let data = try Data.init(contentsOf: url)
+            
+            let parser = Parser(document: testDocument, data: data)
+            try parser.parse()
+            
+            return testDocument.root.text.first?.value
+        } catch {
+            XCTFail("Should be able to parse XML without throwing an error")
+        }
+        return nil
+    }
+    
+    func testXMLParserError() {
+        do {
+            let testDocument = Document()
+            let testData = Data()
+            let parser = Parser(document: testDocument, data: testData)
+            try parser.parse()
+        } catch {
+            XCTAssertEqual(error.localizedDescription, XMLError.parsingFailed.localizedDescription)
+        }
+    }
+    
+    // MARK: - XML Read
+    
+    func testRootElement() {
+        XCTAssertEqual(exampleDocument.root.name, "animals", "Should be able to find root element.")
+        
+        let documentWithoutRootElement = Document()
+        let rootElement = documentWithoutRootElement.root
+        XCTAssertEqual(rootElement.error, XMLError.rootElementMissing, "Should have RootElementMissing error.")
+    }
+    
+    func testParentElement() {
+        XCTAssertEqual(exampleDocument.root.cats.parent?.name, "animals", "Should be able to find parent element.")
+    }
+    
+    func testChildrenElements() {
+        var count = 0
+        for _ in exampleDocument.root.cats.children {
+            count += 1
+        }
+        XCTAssertEqual(count, 4, "Should be able to iterate children elements")
+    }
+    
+    func testName() {
+        let secondChildElementName = exampleDocument.root.children[1].name
+        XCTAssertEqual(secondChildElementName, "dogs", "Should be able to return element name.")
+    }
+    
+    func testAttributes() {
+        let firstCatAttributes = exampleDocument.root.cats.cat.attributes
+        XCTAssertEqual(firstCatAttributes["breed"], "Siberian", "Should be able to return attribute value.")
+    }
+    
+    func testValue() {
+        let firstPlant = plantsDocument.root.PLANT
+        XCTAssertEqual(firstPlant.COMMON, "Bloodroot", "Should be able to return element value as optional string.")
+        XCTAssertNil(firstPlant.ELEMENTWITHOUTVALUE.value, "Should be able to have nil value.")
+        XCTAssertNil(firstPlant.EMPTYELEMENT.value, "Should be able to have nil value.")
+    }
+    
+    func testStringValue() {
+        let firstPlant = plantsDocument.root.PLANT
+        XCTAssertEqual(firstPlant.COMMON.value, "Bloodroot", "Should be able to return element value as string.")
+        XCTAssertNil(firstPlant.ELEMENTWITHOUTVALUE.value, "Should be able to return nil if element has no value.")
+        XCTAssertNil(firstPlant.EMPTYELEMENT.value, "Should be able to return nil if element has no value.")
+    }
+    
+    func testBoolValue() {
+        XCTAssertEqual(plantsDocument.root.PLANT.TRUESTRING, true, "Should be able to cast element value as Bool.")
+        XCTAssertEqual(plantsDocument.root.PLANT.FALSESTRING, false, "Should be able to cast element value as Bool.")
+//        XCTAssertEqual(plantsDocument.root.PLANT.TRUESTRING2, true, "Should be able to cast element value as Bool.")
+//        XCTAssertEqual(plantsDocument.root.PLANT.FALSESTRING2, false, "Should be able to cast element value as Bool.")
+//        XCTAssertEqual(plantsDocument.root.PLANT.TRUEINT, true, "Should be able to cast element value as Bool.")
+//        XCTAssertEqual(plantsDocument.root.PLANT.FALSEINT, false, "Should be able to cast element value as Bool.")
+        XCTAssertNil(plantsDocument.root.ELEMENTWITHOUTVALUE as Bool?, "Should be able to return nil if value can't be represented as Bool.")
+    }
+    
+    func testIntValue() throws {
+        XCTAssertEqual(plantsDocument.root.PLANT.ZONE, 4, "Should be able to cast element value as Integer.")
+        XCTAssertNil(plantsDocument.root.PLANT.ELEMENTWITHOUTVALUE as Int?, "Should be able to return nil if value can't be represented as Integer.")
+    }
+    
+    func testDoubleValue() throws {
+        XCTAssertEqual(plantsDocument.root.PLANT.PRICE, 2.44, "Should be able to cast element value as Double.")
+        XCTAssertNil(plantsDocument.root.PLANT.ELEMENTWITHOUTVALUE as Double?, "Should be able to return nil if value can't be represented as Double.")
+    }
+
+    func testNotExistingElement() throws {
+        // non-optional
+        XCTAssertNotNil(exampleDocument.root.ducks.duck.error, "Should contain error inside element which does not exist.")
+        XCTAssertEqual(exampleDocument.root.ducks.duck.error, XMLError.elementNotFound, "Should have ElementNotFound error.")
+        XCTAssertNil(exampleDocument.root.ducks.duck.value, "Should have empty value.")
+        XCTAssertNil(exampleDocument.root.ducks.duck.first, "Should not be able to find ducks here.")
+    }
+
+    func testAllElements() {
+        var count = 0
+        if let cats = exampleDocument.root.cats.cat.all {
+            for cat in cats {
+                XCTAssertNotNil(cat.parent, "Each child element should have its parent element.")
+                count += 1
+            }
+        }
+        XCTAssertEqual(count, 4, "Should be able to iterate all elements")
+    }
+    
+    func testFirstElement() throws {
+        let catElement = exampleDocument.root.cats.cat
+        let firstCatExpectedValue = "Tinna"
+        
+        // non-optional
+        XCTAssertEqual(try catElement.get(), firstCatExpectedValue, "Should be able to find the first element as non-optional.")
+        
+        // optional
+        if let cat = catElement.first {
+            XCTAssertEqual(try cat.get(), firstCatExpectedValue, "Should be able to find the first element as optional.")
+        } else {
+            XCTFail("Should be able to find the first element.")
+        }
+    }
+    
+    func testLastElement() throws {
+        if let dog = exampleDocument.root.dogs.dog.last {
+            XCTAssertEqual(try dog.get(), "Kika", "Should be able to find the last element.")
+        } else {
+            XCTFail("Should be able to find the last element.")
+        }
+    }
+    
+    func testCountElements() {
+        let dogsCount = exampleDocument.root.dogs.dog.count
+        XCTAssertEqual(dogsCount, 4, "Should be able to count elements.")
+    }
+    
+    func testAllWithValue() {
+        let cats = exampleDocument.root.cats
+        cats.addChild(name: "cat", value: "Tinna")
+        
+        var count = 0
+        if let tinnas = cats["cat"].all(withValue: "Tinna") {
+            for _ in tinnas {
+                count += 1
+            }
+        }
+        XCTAssertEqual(count, 2, "Should be able to return elements with given value.")
+    }
+    
+    func testAllWithAttributes() {
+        var count = 0
+        if let bulls = exampleDocument.root.dogs.dog.all(withAttributes: ["color" : "white"]) {
+            for _ in bulls {
+                count += 1
+            }
+        }
+        XCTAssertEqual(count, 2, "Should be able to return elements with given attributes.")
+    }
+    
+    func testAllContainingAttributes() {
+        var count = 0
+        if let bulls = exampleDocument.root.dogs.dog.all(containingAttributeKeys: ["gender"]) {
+            for _ in bulls {
+                count += 1
+            }
+        }
+        XCTAssertEqual(count, 2, "Should be able to return elements with given attribute keys.")
+    }
+    
+    func testAllDescendantsWherePredicate() {
+        let children = exampleDocument.allDescendants { $0.attributes["color"] == "yellow" }
+        
+        XCTAssertEqual(children.count, 2, "Should be able to return elements matching predicate.")
+    }
+    
+    func testFirstDescendantWherePredicate() {
+        let descendant = plantsDocument.root.firstDescendant { $0.hasDescendant { $0.name == "LIGHT" && $0.value == "Sunny" } }
+        let plantName = descendant?["COMMON"].value
+        
+        XCTAssertEqual(plantName, "Black-Eyed Susan", "Should be able to find first child satisfying predicate.")
+    }
+    
+    func testHasDescendantWherePredicate() throws {
+        let hasDescendant = plantsDocument.hasDescendant { $0.name == "AVAILABILITY" && $0.value == "030699" }
+        XCTAssert(hasDescendant, "Should be able to determine that document has a child satisfying predicate.")
+    }
+    
+    func testSpecialCharacterTrimRead() {
+        let expected = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<elements>\n\t<string name=\"this_and_that\">This &amp; that</string>\n</elements>"
+        
+        let readerDocument = try! Document(xml: expected)
+        let readerXml = readerDocument.xml
+        XCTAssertEqual(readerXml, expected, "Should be able to print XML formatted string.")
+    }
+    
+    // MARK: - XML Write
+    
+    func testAddChild() throws {
+        let ducks = exampleDocument.root.addChild(name: "ducks")
+        ducks.addChild(name: "duck", value: "Donald")
+        ducks.addChild(name: "duck", value: "Daisy")
+        ducks.addChild(name: "duck", value: "Scrooge")
+        
+        let animalsCount = exampleDocument.root.children.count
+        XCTAssertEqual(animalsCount, 3, "Should be able to add child elements to an element.")
+        XCTAssertEqual(try exampleDocument.root["ducks"]["duck"].last?.get(), "Scrooge", "Should be able to iterate ducks now.")
+    }
+    
+    func testAddChildWithAttributes() throws {
+        let cats = exampleDocument.root.cats
+        let dogs = exampleDocument.root.dogs
+        
+        cats.addChild(name: "cat", value: "Garfield", attributes: ["breed" : "tabby", "color" : "orange"])
+        dogs.addChild(name: "dog", value: "Snoopy", attributes: ["breed" : "beagle", "color" : "white"])
+        
+        let catsCount = cats["cat"].count
+        let dogsCount = dogs["dog"].count
+        
+        let lastCat = cats["cat"].last!
+        let penultDog = dogs.children[3]
+        
+        XCTAssertEqual(catsCount, 5, "Should be able to add child element with attributes to an element.")
+        XCTAssertEqual(dogsCount, 5, "Should be able to add child element with attributes to an element.")
+        
+        XCTAssertEqual(lastCat.attributes["color"], "orange", "Should be able to get attribute value from added element.")
+        XCTAssertEqual(try penultDog.get(), "Kika", "Should be able to add child with attributes without overwrites existing elements. (Github Issue #28)")
+    }
+    
+    func testAddChildren() {
+        let animals: [Element] = [
+            Element(name: "dinosaurs"),
+            Element(name: "birds"),
+            Element(name: "bugs"),
+        ]
+        exampleDocument.root.addChildren(animals)
+        
+        let animalsCount = exampleDocument.root.children.count
+        XCTAssertEqual(animalsCount, 5, "Should be able to add children elements to an element.")
+    }
+    
+    func testAddAttributes() {
+        let firstCat = exampleDocument.root.cats.cat
+
+        firstCat.attributes["funny"] = "true"
+        firstCat.attributes["speed"] = "fast"
+        firstCat.attributes["years"] = "7"
+        
+        XCTAssertEqual(firstCat.attributes.count, 5, "Should be able to add attributes to an element.")
+        XCTAssertEqual(Int(firstCat.attributes["years"]!), 7, "Should be able to get any attribute value now.")
+    }
+    
+    func testRemoveChild() throws {
+        let cats = exampleDocument.root.cats
+        let lastCat = cats["cat"].last!
+        let duplicateCat = cats.addChild(name: "cat", value: "Tinna", attributes: ["breed" : "Siberian", "color" : "lightgray"])
+        
+        lastCat.removeFromParent()
+        duplicateCat.removeFromParent()
+        
+        let catsCount = cats["cat"].count
+        let firstCat = cats["cat"]
+        XCTAssertEqual(catsCount, 3, "Should be able to remove element from parent.")
+        XCTAssertEqual(try firstCat.get(), "Tinna", "Should be able to remove the exact element from parent.")
+    }
+    
+    func testXMLEscapedString() {
+        let string = "&<>'\""
+        let escapedString = string.xmlEscaped
+        XCTAssertEqual(escapedString, "&amp;&lt;&gt;&apos;&quot;")
+    }
+    
+    func testXMLString() {
+        let testDocument = Document()
+        let children = testDocument.addChild(name: "children")
+        children.addChild(name: "child", value: "value", attributes: ["attribute" : "attributeValue<&>"])
+        children.addChild(name: "child")
+        children.addChild(name: "child", value: "&<>'\"")
+        
+        XCTAssertEqual(testDocument.xml, "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<children>\n\t<child attribute=\"attributeValue&lt;&amp;&gt;\">value</child>\n\t<child />\n\t<child>&amp;&lt;&gt;&apos;&quot;</child>\n</children>", "Should be able to print XML formatted string.")
+        
+        XCTAssertEqual(testDocument.xmlCompact, "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?><children><child attribute=\"attributeValue&lt;&amp;&gt;\">value</child><child /><child>&amp;&lt;&gt;&apos;&quot;</child></children>", "Should be able to print compact XML string.")
+        
+        XCTAssertEqual(testDocument.xmlSpaces, "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<children>\n    <child attribute=\"attributeValue&lt;&amp;&gt;\">value</child>\n    <child />\n    <child>&amp;&lt;&gt;&apos;&quot;</child>\n</children>", "Should be able to print XML formatted string.")
+    }
+    
+    // MARK: - XML Parse Performance
+    
+    func testReadXMLPerformance() {
+        self.measure() {
+            _ = self.readXMLFromFile(filename: "plant_catalog")
+        }
+    }
+    
+    func testWriteXMLPerformance() {
+        self.measure() {
+            _ = self.plantsDocument.xml
+        }
+    }
+    
+}