From 479f18776fb845d564b3109eea887b6b4381b6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cecilia=20Luque?= Date: Sun, 30 Jun 2019 23:00:10 +0200 Subject: [PATCH] Keep the order of the attributes during encoding operations (#110) This PR fixes #108 by replacing the unordered data structure used for storing the attributes (a Swift `Dictionary`) with an ordered data structure: an array of `Attribute` structs. * Added tests to validate attribute order when encoding * Fix #108 * Improved encapsulation of XMLCoderElement's properties --- .../Auxiliaries/XMLCoderElement.swift | 45 ++++++++++--------- .../XMLCoder/Auxiliaries/XMLStackParser.swift | 5 ++- .../Auxiliary/XMLElementTests.swift | 8 ++-- .../DynamicNodeDecodingTest.swift | 18 ++++++-- .../DynamicNodeEncodingTest.swift | 30 ++++++++++--- .../XMLCoderTests/Minimal/BoxTreeTests.swift | 6 +-- 6 files changed, 74 insertions(+), 38 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 737c17bd..acc800a8 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -7,9 +7,14 @@ import Foundation +struct Attribute: Equatable { + let key: String + let value: String +} + struct XMLCoderElement: Equatable { - static let attributesKey = "___ATTRIBUTES" - static let escapedCharacterSet = [ + private static let attributesKey = "___ATTRIBUTES" + private static let escapedCharacterSet = [ ("&", "&"), ("<", "<"), (">", ">"), @@ -17,16 +22,16 @@ struct XMLCoderElement: Equatable { ("\"", """), ] - var key: String - var value: String? - var elements: [XMLCoderElement] = [] - var attributes: [String: String] = [:] + let key: String + private(set) var value: String? + private(set) var elements: [XMLCoderElement] = [] + private(set) var attributes: [Attribute] = [] init( key: String, value: String? = nil, elements: [XMLCoderElement] = [], - attributes: [String: String] = [:] + attributes: [Attribute] = [] ) { self.key = key self.value = value @@ -47,8 +52,8 @@ struct XMLCoderElement: Equatable { } func transformToBoxTree() -> KeyedBox { - let attributes = KeyedStorage(self.attributes.map { key, value in - (key: key, value: StringBox(value) as SimpleBox) + let attributes = KeyedStorage(self.attributes.map { attribute in + (key: attribute.key, value: StringBox(attribute.value) as SimpleBox) }) let storage = KeyedStorage() var elements = self.elements.reduce(storage) { $0.merge(element: $1) } @@ -125,11 +130,11 @@ struct XMLCoderElement: Equatable { } fileprivate func formatXMLAttributes( - from keyValuePairs: [(key: String, value: String)], + from attributes: [Attribute], into string: inout String ) { - for (key, value) in keyValuePairs { - string += attributeString(key: key, value: value) + for attribute in attributes { + string += attributeString(key: attribute.key, value: attribute.value) } } @@ -157,9 +162,7 @@ struct XMLCoderElement: Equatable { } fileprivate func formatUnsortedXMLAttributes(_ string: inout String) { - formatXMLAttributes( - from: attributes.map { (key: $0, value: $1) }, into: &string - ) + formatXMLAttributes(from: attributes, into: &string) } private func formatXMLAttributes( @@ -278,14 +281,12 @@ extension XMLCoderElement { } } - let attributes: [String: String] = Dictionary( - uniqueKeysWithValues: box.attributes.compactMap { key, box in - guard let value = box.xmlString() else { - return nil - } - return (key, value) + let attributes: [Attribute] = box.attributes.compactMap { key, box in + guard let value = box.xmlString() else { + return nil } - ) + return Attribute(key: key, value: value) + } self.init(key: key, elements: elements, attributes: attributes) } diff --git a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift index 6aada60d..7d4aae16 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift @@ -126,7 +126,10 @@ extension XMLStackParser: XMLParserDelegate { namespaceURI: String?, qualifiedName: String?, attributes attributeDict: [String: String] = [:]) { - let element = XMLCoderElement(key: elementName, attributes: attributeDict) + let attributes = attributeDict.map { key, value in + Attribute(key: key, value: value) + } + let element = XMLCoderElement(key: elementName, attributes: attributes) stack.append(element) } diff --git a/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift b/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift index 55b9e9db..f6f1b2ff 100644 --- a/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift +++ b/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift @@ -15,7 +15,7 @@ class XMLElementTests: XCTestCase { XCTAssertEqual(null.key, "foo") XCTAssertNil(null.value) XCTAssertEqual(null.elements, []) - XCTAssertEqual(null.attributes, [:]) + XCTAssertEqual(null.attributes, []) } func testInitUnkeyed() { @@ -24,7 +24,7 @@ class XMLElementTests: XCTestCase { XCTAssertEqual(keyed.key, "foo") XCTAssertNil(keyed.value) XCTAssertEqual(keyed.elements, []) - XCTAssertEqual(keyed.attributes, [:]) + XCTAssertEqual(keyed.attributes, []) } func testInitKeyed() { @@ -36,7 +36,7 @@ class XMLElementTests: XCTestCase { XCTAssertEqual(keyed.key, "foo") XCTAssertNil(keyed.value) XCTAssertEqual(keyed.elements, []) - XCTAssertEqual(keyed.attributes, ["blee": "42"]) + XCTAssertEqual(keyed.attributes, [Attribute(key: "blee", value: "42")]) } func testInitSimple() { @@ -45,6 +45,6 @@ class XMLElementTests: XCTestCase { XCTAssertEqual(keyed.key, "foo") XCTAssertEqual(keyed.value, "bar") XCTAssertEqual(keyed.elements, []) - XCTAssertEqual(keyed.attributes, [:]) + XCTAssertEqual(keyed.attributes, []) } } diff --git a/Tests/XMLCoderTests/DynamicNodeDecodingTest.swift b/Tests/XMLCoderTests/DynamicNodeDecodingTest.swift index 41b3cf49..86e7ce7d 100644 --- a/Tests/XMLCoderTests/DynamicNodeDecodingTest.swift +++ b/Tests/XMLCoderTests/DynamicNodeDecodingTest.swift @@ -21,14 +21,18 @@ private let overlappingKeys = """ private let libraryXMLYN = """ - + 123 + Jack + novel Cat in the Hat Kids Wildlife - + 456 + Susan + fantastic 1984 Classics News @@ -42,6 +46,8 @@ private let libraryXMLYNStrategy = """ 2 123 + Jack + novel
true
Kids @@ -53,6 +59,8 @@ private let libraryXMLYNStrategy = """
456 + Susan + fantastic
true
Classics @@ -109,18 +117,22 @@ private struct Library: Codable, Equatable, DynamicNodeDecoding { private struct Book: Codable, Equatable, DynamicNodeEncoding { let id: UInt + let author: String + let gender: String let title: String let categories: [Category] enum CodingKeys: String, CodingKey { case id + case author + case gender case title case categories = "category" } static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { switch key { - case Book.CodingKeys.id: return .both + case Book.CodingKeys.id, Book.CodingKeys.author, Book.CodingKeys.gender: return .both default: return .element } } diff --git a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift index 586e616a..bf7d4b9e 100644 --- a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift +++ b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift @@ -12,14 +12,18 @@ import XCTest private let libraryXMLYN = """ - + 123 + Jack + novel Cat in the Hat Kids Wildlife - + 456 + Susan + fantastic 1984 Classics News @@ -33,6 +37,8 @@ private let libraryXMLYNStrategy = """ 2 123 + Jack + novel
true
Kids @@ -44,6 +50,8 @@ private let libraryXMLYNStrategy = """
456 + Susan + fantastic
true
Classics @@ -60,8 +68,10 @@ private let libraryXMLTrueFalse = """ 2 - + 123 + Jack + novel Cat in the Hat Kids @@ -70,8 +80,10 @@ private let libraryXMLTrueFalse = """ Wildlife - + 456 + Susan + fantastic 1984 Classics @@ -95,18 +107,22 @@ private struct Library: Codable, Equatable { private struct Book: Codable, Equatable, DynamicNodeEncoding { let id: UInt + let author: String + let gender: String let title: String let categories: [Category] enum CodingKeys: String, CodingKey { case id + case author + case gender case title case categories = "category" } static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { switch key { - case Book.CodingKeys.id: return .both + case Book.CodingKeys.id, Book.CodingKeys.author, Book.CodingKeys.gender: return .both default: return .element } } @@ -135,6 +151,8 @@ final class DynamicNodeEncodingTest: XCTestCase { func testEncode() throws { let book1 = Book( id: 123, + author: "Jack", + gender: "novel", title: "Cat in the Hat", categories: [ Category(main: true, value: "Kids"), @@ -144,6 +162,8 @@ final class DynamicNodeEncodingTest: XCTestCase { let book2 = Book( id: 456, + author: "Susan", + gender: "fantastic", title: "1984", categories: [ Category(main: true, value: "Classics"), diff --git a/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift b/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift index 7daa6e21..350c15c5 100644 --- a/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift +++ b/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift @@ -14,19 +14,19 @@ class BoxTreeTests: XCTestCase { key: "foo", value: "456", elements: [], - attributes: ["id": "123"] + attributes: [Attribute(key: "id", value: "123")] ) let e2 = XMLCoderElement( key: "foo", value: "123", elements: [], - attributes: ["id": "789"] + attributes: [Attribute(key: "id", value: "789")] ) let root = XMLCoderElement( key: "container", value: nil, elements: [e1, e2], - attributes: [:] + attributes: [] ) let boxTree = root.transformToBoxTree()