Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement removeWhitespaceElements on XMLDecoder #222

Merged
merged 12 commits into from Jul 30, 2021
6 changes: 6 additions & 0 deletions Sources/XMLCoder/Auxiliaries/String+Extensions.swift
Expand Up @@ -44,3 +44,9 @@ extension StringProtocol {
self = lowercasingFirstLetter()
}
}

extension String {
func isAllWhitespace() -> Bool {
return self.trimmingCharacters(in: .whitespacesAndNewlines) == ""
}
}
7 changes: 7 additions & 0 deletions Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
Expand Up @@ -392,3 +392,10 @@ extension XMLCoderElement {
}
}
}

extension XMLCoderElement {
func isWhitespaceWithNoElements() -> Bool {
let stringValueIsWhitespaceOrNil = stringValue?.isAllWhitespace() ?? true
return self.key == "" && stringValueIsWhitespaceOrNil && self.elements.isEmpty
}
}
37 changes: 32 additions & 5 deletions Sources/XMLCoder/Auxiliaries/XMLStackParser.swift
Expand Up @@ -15,19 +15,23 @@ class XMLStackParser: NSObject {
var root: XMLCoderElement?
private var stack: [XMLCoderElement] = []
private let trimValueWhitespaces: Bool
private let removeWhitespaceElements: Bool

init(trimValueWhitespaces: Bool = true) {
init(trimValueWhitespaces: Bool = true, removeWhitespaceElements: Bool = false) {
self.trimValueWhitespaces = trimValueWhitespaces
self.removeWhitespaceElements = removeWhitespaceElements
super.init()
}

static func parse(
with data: Data,
errorContextLength length: UInt,
shouldProcessNamespaces: Bool,
trimValueWhitespaces: Bool
trimValueWhitespaces: Bool,
removeWhitespaceElements: Bool
) throws -> Box {
let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces)
let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces,
removeWhitespaceElements: removeWhitespaceElements)

let node = try parser.parse(
with: data,
Expand Down Expand Up @@ -141,15 +145,38 @@ extension XMLStackParser: XMLParserDelegate {
return
}

let updatedElement = removeWhitespaceElements ? elementWithFilteredElements(element: element) : element

withCurrentElement { currentElement in
currentElement.append(element: element, forKey: element.key)
currentElement.append(element: updatedElement, forKey: updatedElement.key)
}

if stack.isEmpty {
root = element
root = updatedElement
}
}

func elementWithFilteredElements(element: XMLCoderElement) -> XMLCoderElement {
var hasWhitespaceElements: Bool = false
wooj2 marked this conversation as resolved.
Show resolved Hide resolved
var hasNonWhitespaceElements: Bool = false
var filteredElements: [XMLCoderElement] = []
for ele in element.elements {
if ele.isWhitespaceWithNoElements() {
hasWhitespaceElements = true
} else {
hasNonWhitespaceElements = true
filteredElements.append(ele)
}
}
let updatedElement: XMLCoderElement
if hasWhitespaceElements && hasNonWhitespaceElements {
updatedElement = XMLCoderElement(key: element.key, elements: filteredElements, attributes: element.attributes)
wooj2 marked this conversation as resolved.
Show resolved Hide resolved
} else {
updatedElement = element
}
return updatedElement
}

func parser(_: XMLParser, foundCharacters string: String) {
let processedString = process(string: string)
guard processedString.count > 0, string.count != 0 else {
Expand Down
12 changes: 10 additions & 2 deletions Sources/XMLCoder/Decoder/XMLDecoder.swift
Expand Up @@ -303,6 +303,12 @@ open class XMLDecoder {
*/
open var trimValueWhitespaces: Bool

/** A boolean value that determines whether to remove pure whitespace elements
that have sibling elements that aren't pure whitespace. The default value
is `false`.
*/
open var removeWhitespaceElements: Bool

/// Options set on the top-level encoder to pass down the decoding hierarchy.
struct Options {
let dateDecodingStrategy: DateDecodingStrategy
Expand All @@ -328,8 +334,9 @@ open class XMLDecoder {
// MARK: - Constructing a XML Decoder

/// Initializes `self` with default strategies.
public init(trimValueWhitespaces: Bool = true) {
public init(trimValueWhitespaces: Bool = true, removeWhitespaceElements: Bool = false) {
self.trimValueWhitespaces = trimValueWhitespaces
self.removeWhitespaceElements = removeWhitespaceElements
}

// MARK: - Decoding Values
Expand All @@ -349,7 +356,8 @@ open class XMLDecoder {
with: data,
errorContextLength: errorContextLength,
shouldProcessNamespaces: shouldProcessNamespaces,
trimValueWhitespaces: trimValueWhitespaces
trimValueWhitespaces: trimValueWhitespaces,
removeWhitespaceElements: removeWhitespaceElements
)

let decoder = XMLDecoderImplementation(
Expand Down
35 changes: 35 additions & 0 deletions Tests/XMLCoderTests/Auxiliary/String+ExtensionsTests.swift
Expand Up @@ -41,4 +41,39 @@ class StringExtensionsTests: XCTestCase {
}
XCTAssertEqual(expected, mutated)
}

func testIsAllWhitespace() {
let testString1 = ""
let testString2 = " "

let testString3 = "\n"
let testString4 = "\n "
let testString5 = " \n "
let testString6 = " \n"

let testString7 = "\r"
let testString8 = "\r "
let testString9 = " \r "
let testString10 = " \r"

let testString11 = "\r\n"
let testString12 = "\r\n "
let testString13 = " \r\n "
let testString14 = " \r\n"

XCTAssert(testString1.isAllWhitespace())
XCTAssert(testString2.isAllWhitespace())
XCTAssert(testString3.isAllWhitespace())
XCTAssert(testString4.isAllWhitespace())
XCTAssert(testString5.isAllWhitespace())
XCTAssert(testString6.isAllWhitespace())
XCTAssert(testString7.isAllWhitespace())
XCTAssert(testString8.isAllWhitespace())
XCTAssert(testString9.isAllWhitespace())
XCTAssert(testString10.isAllWhitespace())
XCTAssert(testString11.isAllWhitespace())
XCTAssert(testString12.isAllWhitespace())
XCTAssert(testString13.isAllWhitespace())
XCTAssert(testString14.isAllWhitespace())
}
}
15 changes: 15 additions & 0 deletions Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift
Expand Up @@ -49,4 +49,19 @@ class XMLElementTests: XCTestCase {
XCTAssertEqual(keyed.elements, [element])
XCTAssertEqual(keyed.attributes, [])
}

func testWhitespaceWithNoElements_keyed() {
let keyed = XMLCoderElement(key: "foo", isStringBoxCDATA: false, box: StringBox("bar"))
XCTAssertFalse(keyed.isWhitespaceWithNoElements())
}

func testWhitespaceWithNoElements_whitespace() {
let whitespaceElement1 = XMLCoderElement(stringValue: "\n ")
let whitespaceElement2 = XMLCoderElement(stringValue: "\n")
let whitespaceElement3 = XMLCoderElement(stringValue: " ")

XCTAssert(whitespaceElement1.isWhitespaceWithNoElements())
XCTAssert(whitespaceElement2.isWhitespaceWithNoElements())
XCTAssert(whitespaceElement3.isWhitespaceWithNoElements())
}
}
102 changes: 102 additions & 0 deletions Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift
Expand Up @@ -56,4 +56,106 @@ class XMLStackParserTests: XCTestCase {
shouldProcessNamespaces: false
))
}

func testNestedMembers_removeWhitespaceElements() throws {
let parser = XMLStackParser(trimValueWhitespaces: false, removeWhitespaceElements: true)
let xmlData =
"""
<SomeType>
<nestedStringList>
<member>
<member>foo</member>
<member>bar</member>
</member>
<member>
<member>baz</member>
<member>qux</member>
</member>
</nestedStringList>
</SomeType>
""".data(using: .utf8)!
let root = try! parser.parse(with: xmlData, errorContextLength: 0, shouldProcessNamespaces: false)
wooj2 marked this conversation as resolved.
Show resolved Hide resolved

XCTAssertEqual(root.elements[0].key, "nestedStringList")

XCTAssertEqual(root.elements[0].elements[0].key, "member")
XCTAssertEqual(root.elements[0].elements[0].elements[0].key, "member")
XCTAssertEqual(root.elements[0].elements[0].elements[0].elements[0].stringValue, "foo")
XCTAssertEqual(root.elements[0].elements[0].elements[1].elements[0].stringValue, "bar")

XCTAssertEqual(root.elements[0].elements[1].key, "member")
XCTAssertEqual(root.elements[0].elements[1].elements[0].key, "member")
XCTAssertEqual(root.elements[0].elements[1].elements[0].elements[0].stringValue, "baz")
XCTAssertEqual(root.elements[0].elements[1].elements[1].elements[0].stringValue, "qux")
}

func testNestedMembers() throws {
let parser = XMLStackParser(trimValueWhitespaces: false, removeWhitespaceElements: false)
let xmlData =
"""
<SomeType>
<nestedStringList>
<member>
<member>foo</member>
<member>bar</member>
</member>
<member>
<member>baz</member>
<member>qux</member>
</member>
</nestedStringList>
</SomeType>
""".data(using: .utf8)!
let root = try! parser.parse(with: xmlData, errorContextLength: 0, shouldProcessNamespaces: false)
wooj2 marked this conversation as resolved.
Show resolved Hide resolved

XCTAssertEqual(root.elements[0].key, "")
XCTAssertEqual(root.elements[0].stringValue, "\n ")

XCTAssertEqual(root.elements[1].key, "nestedStringList")
XCTAssertEqual(root.elements[1].elements[0].key, "")
XCTAssertEqual(root.elements[1].elements[0].stringValue, "\n ")
XCTAssertEqual(root.elements[1].elements[1].key, "member")
XCTAssertEqual(root.elements[1].elements[1].elements[0].stringValue, "\n ")

XCTAssertEqual(root.elements[1].elements[1].elements[1].key, "member")
XCTAssertEqual(root.elements[1].elements[1].elements[1].elements[0].stringValue, "foo")
XCTAssertEqual(root.elements[1].elements[1].elements[3].key, "member")
XCTAssertEqual(root.elements[1].elements[1].elements[3].elements[0].stringValue, "bar")

XCTAssertEqual(root.elements[1].elements[3].elements[1].key, "member")
XCTAssertEqual(root.elements[1].elements[3].elements[1].elements[0].stringValue, "baz")
XCTAssertEqual(root.elements[1].elements[3].elements[3].key, "member")
XCTAssertEqual(root.elements[1].elements[3].elements[3].elements[0].stringValue, "qux")
}

func testEscapableCharacters_removeWhitespaceElements() {
wooj2 marked this conversation as resolved.
Show resolved Hide resolved
let parser = XMLStackParser(trimValueWhitespaces: false, removeWhitespaceElements: true)
let xmlData =
"""
<SomeType>
<strValue>escaped data: &amp;lt;&#xD;&#10;</strValue>
</SomeType>
""".data(using: .utf8)!
let root = try! parser.parse(with: xmlData, errorContextLength: 0, shouldProcessNamespaces: false)
wooj2 marked this conversation as resolved.
Show resolved Hide resolved

XCTAssertEqual(root.key, "SomeType")
XCTAssertEqual(root.elements[0].key, "strValue")
XCTAssertEqual(root.elements[0].elements[0].stringValue, "escaped data: &lt;\r\n")
}

func testEscapableCharacters() {
wooj2 marked this conversation as resolved.
Show resolved Hide resolved
let parser = XMLStackParser(trimValueWhitespaces: false, removeWhitespaceElements: false)
let xmlData =
"""
<SomeType>
<strValue>escaped data: &amp;lt;&#xD;&#10;</strValue>
</SomeType>
""".data(using: .utf8)!
let root = try! parser.parse(with: xmlData, errorContextLength: 0, shouldProcessNamespaces: false)
wooj2 marked this conversation as resolved.
Show resolved Hide resolved
XCTAssertEqual(root.key, "SomeType")
XCTAssertEqual(root.elements[0].key, "")
XCTAssertEqual(root.elements[0].stringValue, "\n ")
XCTAssertEqual(root.elements[1].elements[0].stringValue, "escaped data: &lt;\r\n")
XCTAssertEqual(root.elements[2].stringValue, "\n")
}
wooj2 marked this conversation as resolved.
Show resolved Hide resolved
}