Skip to content

Commit

Permalink
Ordered encoding (#82)
Browse files Browse the repository at this point in the history
Resolves #17 

* First attempt at ordered encoding
* Fix line length linter warning
* Rename DynamicNodeEncoding function, fix order
  • Loading branch information
MaxDesiatov authored Mar 1, 2019
1 parent 6dbb4ba commit f8616cf
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 65 deletions.
57 changes: 37 additions & 20 deletions Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,24 @@
import Foundation

struct KeyedStorage<Key: Hashable & Comparable, Value> {
struct Iterator: IteratorProtocol {
fileprivate var orderIterator: Order.Iterator
fileprivate var buffer: Buffer
mutating func next() -> (Key, Value)? {
guard
let key = orderIterator.next(),
let value = buffer[key]
else { return nil }

return (key, value)
}
}

typealias Buffer = [Key: Value]
typealias Order = [Key]

fileprivate var buffer: Buffer = [:]
fileprivate var order = Order()
fileprivate var buffer = Buffer()

var isEmpty: Bool {
return buffer.isEmpty
Expand All @@ -24,15 +39,19 @@ struct KeyedStorage<Key: Hashable & Comparable, Value> {
return buffer.keys
}

init(_ buffer: Buffer) {
self.buffer = buffer
init<S>(_ sequence: S) where S: Sequence, S.Element == (Key, Value) {
order = sequence.map { $0.0 }
buffer = Dictionary(uniqueKeysWithValues: sequence)
}

subscript(key: Key) -> Value? {
get {
return buffer[key]
}
set {
if buffer[key] == nil {
order.append(key)
}
buffer[key] = newValue
}
}
Expand All @@ -44,23 +63,25 @@ struct KeyedStorage<Key: Hashable & Comparable, Value> {
func compactMap<T>(_ transform: ((Key, Value)) throws -> T?) rethrows -> [T] {
return try buffer.compactMap(transform)
}
}

extension KeyedStorage: Sequence {
func makeIterator() -> Buffer.Iterator {
return buffer.makeIterator()
}
init() {}
}

extension KeyedStorage: ExpressibleByDictionaryLiteral {
init(dictionaryLiteral elements: (Key, Value)...) {
self.init(Dictionary(uniqueKeysWithValues: elements))
extension KeyedStorage: Sequence {
func makeIterator() -> Iterator {
return Iterator(orderIterator: order.makeIterator(), buffer: buffer)
}
}

extension KeyedStorage: CustomStringConvertible {
var description: String {
return "\(buffer)"
let result = order.compactMap { (key: Key) -> String? in
guard let value = buffer[key] else { return nil }

return "\"\(key)\": \(value)"
}.joined(separator: ", ")

return "[\(result)]"
}
}

Expand All @@ -72,8 +93,8 @@ struct KeyedBox {
typealias Attributes = KeyedStorage<Key, Attribute>
typealias Elements = KeyedStorage<Key, Element>

var elements: Elements = [:]
var attributes: Attributes = [:]
var elements = Elements()
var attributes = Attributes()

func unbox() -> (elements: Elements, attributes: Attributes) {
return (
Expand All @@ -86,14 +107,10 @@ struct KeyedBox {
extension KeyedBox {
init<E, A>(elements: E, attributes: A)
where E: Sequence, E.Element == (Key, Element), A: Sequence, A.Element == (Key, Attribute) {
let elements = Elements(Dictionary(uniqueKeysWithValues: elements))
let attributes = Attributes(Dictionary(uniqueKeysWithValues: attributes))
let elements = Elements(elements)
let attributes = Attributes(attributes)
self.init(elements: elements, attributes: attributes)
}

init(elements: [Key: Element], attributes: [Key: Attribute]) {
self.init(elements: Elements(elements), attributes: Attributes(attributes))
}
}

extension KeyedBox: Box {
Expand Down
13 changes: 8 additions & 5 deletions Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ struct XMLCoderElement: Equatable {
// FIXME: this should be split into separate functions and
// thoroughtly tested
func flatten() -> KeyedBox {
let attributes = self.attributes.mapValues { StringBox($0) }
let attributes = KeyedStorage(self.attributes.mapValues {
StringBox($0) as SimpleBox
}.shuffled())
let storage = KeyedStorage<String, Box>()

var keyedElements = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in
var elements = self.elements.reduce(storage) { result, element in
var result = result
let key = element.key

Expand Down Expand Up @@ -97,10 +100,10 @@ struct XMLCoderElement: Equatable {

// Handle attributed unkeyed value <foo attr="bar">zap</foo>
// Value should be zap. Detect only when no other elements exist
if keyedElements.isEmpty, let value = value {
keyedElements["value"] = StringBox(value)
if elements.isEmpty, let value = value {
elements["value"] = StringBox(value)
}
let keyedBox = KeyedBox(elements: keyedElements, attributes: attributes)
let keyedBox = KeyedBox(elements: elements, attributes: attributes)

return keyedBox
}
Expand Down
10 changes: 5 additions & 5 deletions Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
import Foundation

public protocol DynamicNodeEncoding: Encodable {
static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
}

extension Array: DynamicNodeEncoding where Element: DynamicNodeEncoding {
public static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
return Element.nodeEncoding(forKey: key)
public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
return Element.nodeEncoding(for: key)
}
}

extension DynamicNodeEncoding where Self: Collection, Self.Iterator.Element: DynamicNodeEncoding {
public static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
return Element.nodeEncoding(forKey: key)
public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
return Element.nodeEncoding(for: key)
}
}
2 changes: 1 addition & 1 deletion Sources/XMLCoder/Encoder/XMLEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ open class XMLEncoder {
guard let dynamicType = codableType as? DynamicNodeEncoding.Type else {
return { _ in .default }
}
return dynamicType.nodeEncoding(forKey:)
return dynamicType.nodeEncoding(for:)
}
}

Expand Down
8 changes: 4 additions & 4 deletions Tests/XMLCoderTests/AttributedIntrinsicTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ private struct Foo: Codable, DynamicNodeEncoding {
case value
}

static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.id:
return .attribute
Expand All @@ -42,7 +42,7 @@ private struct FooEmptyKeyed: Codable, DynamicNodeEncoding {
case unkeyedValue = ""
}

static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.id:
return .attribute
Expand Down Expand Up @@ -81,7 +81,7 @@ private struct PreviewImageTime: Codable, Equatable, DynamicNodeEncoding {
case value
}

static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.format:
return .attribute
Expand Down Expand Up @@ -166,7 +166,7 @@ private struct FooNumber: Codable, DynamicNodeEncoding {
case typeValue = ""
}

public static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case FooNumber.CodingKeys.type: return .attribute
default: return .element
Expand Down
4 changes: 2 additions & 2 deletions Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class XMLElementTests: XCTestCase {

func testInitKeyed() {
let keyed = XMLCoderElement(key: "foo", box: KeyedBox(
elements: [:],
attributes: ["baz": NullBox(), "blee": IntBox(42)]
elements: [] as [(String, Box)],
attributes: [("baz", NullBox()), ("blee", IntBox(42))] as [(String, SimpleBox)]
))

XCTAssertEqual(keyed.key, "foo")
Expand Down
10 changes: 5 additions & 5 deletions Tests/XMLCoderTests/BooksTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ private let bookXML = """
<?xml version="1.0" encoding="UTF-8"?>
<book id="bk101">
<author>Gambardella, Matthew</author>
<description>An in-depth look at creating applications
with XML.</description>
<title>XML Developer&apos;s Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<description>An in-depth look at creating applications
with XML.</description>
<publish_date>2000-10-01</publish_date>
<title>XML Developer&apos;s Guide</title>
</book>
""".data(using: .utf8)!

Expand Down Expand Up @@ -170,7 +170,7 @@ private struct Book: Codable, Equatable, DynamicNodeEncoding {
case publishDate = "publish_date"
}

static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.id:
return .attribute
Expand Down Expand Up @@ -200,7 +200,7 @@ final class BooksTest: XCTestCase {
let decoder = XMLDecoder()
let encoder = XMLEncoder()

encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.outputFormatting = [.prettyPrinted]

decoder.dateDecodingStrategy = .formatted(formatter)
encoder.dateEncodingStrategy = .formatted(formatter)
Expand Down
18 changes: 9 additions & 9 deletions Tests/XMLCoderTests/Box/KeyedBoxTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class KeyedBoxTests: XCTestCase {
typealias Boxed = KeyedBox

let box = Boxed(
elements: ["foo": StringBox("bar"), "baz": IntBox(42)],
attributes: ["baz": StringBox("blee")]
elements: [("foo", StringBox("bar")), ("baz", IntBox(42))] as [(String, Box)],
attributes: [("baz", StringBox("blee"))]
)

func testIsNull() {
Expand All @@ -37,11 +37,10 @@ class KeyedBoxTests: XCTestCase {
}

func testDescription() {
// FIXME: once we have an order-preserving storage
// we can check against the full description:
let description = box.description
XCTAssertTrue(description.contains("\"foo\": bar"))
XCTAssertTrue(description.contains("\"baz\": 42"))
XCTAssertEqual(
box.description,
"{attributes: [\"baz\": blee], elements: [\"foo\": bar, \"baz\": 42]}"
)
}

func testSequence() {
Expand All @@ -53,9 +52,10 @@ class KeyedBoxTests: XCTestCase {
}

func testSubscript() {
let elements: [(String, Box)] = [("foo", StringBox("bar")), ("baz", IntBox(42))]
var box = Boxed(
elements: ["foo": StringBox("bar"), "baz": IntBox(42)],
attributes: ["baz": StringBox("blee")]
elements: elements,
attributes: [("baz", StringBox("blee"))]
)
box.elements["bar"] = NullBox()
XCTAssertEqual(box.elements.count, 3)
Expand Down
8 changes: 4 additions & 4 deletions Tests/XMLCoderTests/ClassTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,19 @@ private let xmlData = """
<x>\(str)</x>
</a>
<b>
<y>\(double)</y>
<super>
<x>\(str)</x>
</super>
<y>\(double)</y>
</b>
<c>
<z>\(int)</z>
<super>
<y>\(double)</y>
<super>
<x>\(str)</x>
</super>
<y>\(double)</y>
</super>
<z>\(int)</z>
</c>
</s>
""".data(using: .utf8)!
Expand All @@ -94,7 +94,7 @@ class ClassTests: XCTestCase {
func testEmpty() throws {
let decoder = XMLDecoder()
let encoder = XMLEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.outputFormatting = [.prettyPrinted]

let decoded = try decoder.decode(S.self, from: xmlData)
XCTAssertEqual(decoded.a.x, str)
Expand Down
18 changes: 9 additions & 9 deletions Tests/XMLCoderTests/DynamicNodeEncodingTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,27 @@ private let libraryXMLYN = """
private let libraryXMLTrueFalse = """
<?xml version="1.0" encoding="UTF-8"?>
<library>
<count>2</count>
<book id="123">
<id>123</id>
<title>Cat in the Hat</title>
<category main="true">
<value>Kids</value>
</category>
<category main="false">
<value>Wildlife</value>
</category>
<id>123</id>
<title>Cat in the Hat</title>
</book>
<book id="456">
<id>456</id>
<title>1984</title>
<category main="true">
<value>Classics</value>
</category>
<category main="false">
<value>News</value>
</category>
<id>456</id>
<title>1984</title>
</book>
<count>2</count>
</library>
"""

Expand All @@ -75,7 +75,7 @@ private struct Book: Codable, Equatable, DynamicNodeEncoding {
case categories = "category"
}

static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case Book.CodingKeys.id: return .both
default: return .element
Expand All @@ -92,7 +92,7 @@ private struct Category: Codable, Equatable, DynamicNodeEncoding {
case value
}

static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case Category.CodingKeys.main:
return .attribute
Expand Down Expand Up @@ -120,11 +120,11 @@ final class DynamicNodeEncodingTest: XCTestCase {

let library = Library(count: 2, books: [book1, book2])
let encoder = XMLEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.outputFormatting = [.prettyPrinted]

let header = XMLHeader(version: 1.0, encoding: "UTF-8")
let encoded = try encoder.encode(library, withRootKey: "library", header: header)
let xmlString = String(data: encoded, encoding: .utf8)
let xmlString = String(data: encoded, encoding: .utf8)!
XCTAssertEqual(xmlString, libraryXMLTrueFalse)
}

Expand Down
2 changes: 1 addition & 1 deletion Tests/XMLCoderTests/Minimal/UnkeyedIntTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class UnkeyedIntTests: XCTestCase {
func testInt<T: Codable & IntegerArrayContainer>(_ type: T.Type) throws {
let decoder = XMLDecoder()
let encoder = XMLEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.outputFormatting = [.prettyPrinted]

let xmlString =
"""
Expand Down

0 comments on commit f8616cf

Please sign in to comment.