Skip to content

Commit

Permalink
Keep the order of the attributes during encoding operations (CoreOffi…
Browse files Browse the repository at this point in the history
…ce#110)

This PR fixes CoreOffice#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 CoreOffice#108
* Improved encapsulation of XMLCoderElement's properties
  • Loading branch information
acecilia authored and Arjun Gupta committed Jun 26, 2020
1 parent 15ce1dc commit 479f187
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 38 deletions.
45 changes: 23 additions & 22 deletions Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
Expand Up @@ -7,26 +7,31 @@

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 = [
("&", "&"),
("<", "&lt;"),
(">", "&gt;"),
("'", "&apos;"),
("\"", "&quot;"),
]

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
Expand All @@ -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<String, Box>()
var elements = self.elements.reduce(storage) { $0.merge(element: $1) }
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/XMLCoder/Auxiliaries/XMLStackParser.swift
Expand Up @@ -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)
}

Expand Down
8 changes: 4 additions & 4 deletions Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift
Expand Up @@ -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() {
Expand All @@ -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() {
Expand All @@ -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() {
Expand All @@ -45,6 +45,6 @@ class XMLElementTests: XCTestCase {
XCTAssertEqual(keyed.key, "foo")
XCTAssertEqual(keyed.value, "bar")
XCTAssertEqual(keyed.elements, [])
XCTAssertEqual(keyed.attributes, [:])
XCTAssertEqual(keyed.attributes, [])
}
}
18 changes: 15 additions & 3 deletions Tests/XMLCoderTests/DynamicNodeDecodingTest.swift
Expand Up @@ -21,14 +21,18 @@ private let overlappingKeys = """
private let libraryXMLYN = """
<?xml version="1.0" encoding="UTF-8"?>
<library count="2">
<book id="123">
<book id="123" author="Jack" gender="novel">
<id>123</id>
<author>Jack</author>
<gender>novel</gender>
<title>Cat in the Hat</title>
<category main="Y"><value>Kids</value></category>
<category main="N"><value>Wildlife</value></category>
</book>
<book id="456">
<book id="456" author="Susan" gender="fantastic">
<id>456</id>
<author>Susan</author>
<gender>fantastic</gender>
<title>1984</title>
<category main="Y"><value>Classics</value></category>
<category main="N"><value>News</value></category>
Expand All @@ -42,6 +46,8 @@ private let libraryXMLYNStrategy = """
<count>2</count>
<book title="Cat in the Hat">
<id>123</id>
<author>Jack</author>
<gender>novel</gender>
<category>
<main>true</main>
<value>Kids</value>
Expand All @@ -53,6 +59,8 @@ private let libraryXMLYNStrategy = """
</book>
<book title="1984">
<id>456</id>
<author>Susan</author>
<gender>fantastic</gender>
<category>
<main>true</main>
<value>Classics</value>
Expand Down Expand Up @@ -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
}
}
Expand Down
30 changes: 25 additions & 5 deletions Tests/XMLCoderTests/DynamicNodeEncodingTest.swift
Expand Up @@ -12,14 +12,18 @@ import XCTest
private let libraryXMLYN = """
<?xml version="1.0" encoding="UTF-8"?>
<library count="2">
<book id="123">
<book id="123" author="Jack" gender="novel">
<id>123</id>
<author>Jack</author>
<gender>novel</gender>
<title>Cat in the Hat</title>
<category main="Y"><value>Kids</value></category>
<category main="N"><value>Wildlife</value></category>
</book>
<book id="456">
<book id="456" author="Susan" gender="fantastic">
<id>456</id>
<author>Susan</author>
<gender>fantastic</gender>
<title>1984</title>
<category main="Y"><value>Classics</value></category>
<category main="N"><value>News</value></category>
Expand All @@ -33,6 +37,8 @@ private let libraryXMLYNStrategy = """
<count>2</count>
<book title="Cat in the Hat">
<id>123</id>
<author>Jack</author>
<gender>novel</gender>
<category>
<main>true</main>
<value>Kids</value>
Expand All @@ -44,6 +50,8 @@ private let libraryXMLYNStrategy = """
</book>
<book title="1984">
<id>456</id>
<author>Susan</author>
<gender>fantastic</gender>
<category>
<main>true</main>
<value>Classics</value>
Expand All @@ -60,8 +68,10 @@ private let libraryXMLTrueFalse = """
<?xml version="1.0" encoding="UTF-8"?>
<library>
<count>2</count>
<book id="123">
<book id="123" author="Jack" gender="novel">
<id>123</id>
<author>Jack</author>
<gender>novel</gender>
<title>Cat in the Hat</title>
<category main="true">
<value>Kids</value>
Expand All @@ -70,8 +80,10 @@ private let libraryXMLTrueFalse = """
<value>Wildlife</value>
</category>
</book>
<book id="456">
<book id="456" author="Susan" gender="fantastic">
<id>456</id>
<author>Susan</author>
<gender>fantastic</gender>
<title>1984</title>
<category main="true">
<value>Classics</value>
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand Down
6 changes: 3 additions & 3 deletions Tests/XMLCoderTests/Minimal/BoxTreeTests.swift
Expand Up @@ -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()
Expand Down

0 comments on commit 479f187

Please sign in to comment.