From f59dcbe4f342b098e8071ef11bcabc7a86a8a984 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Fri, 21 Dec 2018 02:45:10 +0100 Subject: [PATCH 1/5] Clean up `XMLElement`; Move attributes into separate dictionary # Conflicts: # Sources/XMLCoder/Auxiliaries/XMLStackParser.swift --- Sources/XMLCoder/Auxiliaries/XMLElement.swift | 73 ++++++------ .../XMLCoder/Auxiliaries/XMLStackParser.swift | 4 +- Sources/XMLCoder/Box/KeyedBox.swift | 105 +++++++++++------- Sources/XMLCoder/Decoder/XMLDecoder.swift | 4 +- .../Decoder/XMLKeyedDecodingContainer.swift | 55 ++++++--- Sources/XMLCoder/Encoder/XMLEncoder.swift | 2 +- .../Encoder/XMLKeyedEncodingContainer.swift | 18 +-- .../Encoder/XMLReferencingEncoder.swift | 2 +- .../Auxiliary/XMLStackParserTests.swift | 2 +- Tests/XMLCoderTests/Box/FloatBoxTests.swift | 5 - Tests/XMLCoderTests/Box/KeyedBoxTests.swift | 26 ++--- Tests/XMLCoderTests/Box/UnkeyedBoxTests.swift | 4 - 12 files changed, 162 insertions(+), 138 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/XMLElement.swift b/Sources/XMLCoder/Auxiliaries/XMLElement.swift index a7753d6f..6ee36ac6 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLElement.swift @@ -14,13 +14,13 @@ class _XMLElement { var key: String var value: String? var attributes: [String: String] = [:] - var children: [String: [_XMLElement]] = [:] + var elements: [String: [_XMLElement]] = [:] - init(key: String, value: String? = nil, attributes: [String: String] = [:], children: [String: [_XMLElement]] = [:]) { + init(key: String, value: String? = nil, attributes: [String: String] = [:], elements: [String: [_XMLElement]] = [:]) { self.key = key self.value = value self.attributes = attributes - self.children = children + self.elements = elements } static func createRootElement(rootKey: String, object: UnkeyedBox) -> _XMLElement? { @@ -40,20 +40,17 @@ class _XMLElement { } fileprivate static func modifyElement(element: _XMLElement, parentElement: _XMLElement?, key: String?, object: KeyedBox) { - let attributesBox = object[_XMLElement.attributesKey] as? KeyedBox - let uniqueAttributes: [(String, String)]? = attributesBox?.unbox().compactMap { key, box in + let uniqueAttributes: [(String, String)]? = object.attributes.compactMap { key, box in return box.xmlString().map { (key, $0) } } element.attributes = uniqueAttributes.map { Dictionary(uniqueKeysWithValues: $0) } ?? [:] - let objects = object.filter { key, _value in key != _XMLElement.attributesKey } - - for (key, box) in objects { + for (key, box) in object.elements { _XMLElement.createElement(parentElement: element, key: key, object: box) } if let parentElement = parentElement, let key = key { - parentElement.children[key] = (parentElement.children[key] ?? []) + [element] + parentElement.elements[key] = (parentElement.elements[key] ?? []) + [element] } } @@ -67,9 +64,8 @@ class _XMLElement { modifyElement(element: _XMLElement(key: key), parentElement: parentElement, key: key, object: box) case _: let element = _XMLElement(key: key, value: object.xmlString()) - parentElement.children[key, default: []].append(element) + parentElement.elements[key, default: []].append(element) } - } func append(value string: String) { @@ -78,40 +74,41 @@ class _XMLElement { self.value = value } - func flatten() -> [String: Box] { - var node: [String: Box] = attributes.mapValues { StringBox($0) } + func flatten() -> KeyedBox { + let attributes = self.attributes.mapValues { StringBox($0) } + var elements: [String: Box] = [:] - for childElement in children { - for child in childElement.value { + for element in self.elements { + for child in element.value { if let content = child.value { - if let oldContent = node[childElement.key] as? UnkeyedBox { + if let oldContent = elements[element.key] as? UnkeyedBox { oldContent.append(StringBox(content)) // FIXME: Box is a reference type, so this shouldn't be necessary: - node[childElement.key] = oldContent - } else if let oldContent = node[childElement.key] { - node[childElement.key] = UnkeyedBox([oldContent, StringBox(content)]) + elements[element.key] = oldContent + } else if let oldContent = elements[element.key] { + elements[element.key] = UnkeyedBox([oldContent, StringBox(content)]) } else { - node[childElement.key] = StringBox(content) + elements[element.key] = StringBox(content) } - } else if !child.children.isEmpty || !child.attributes.isEmpty { + } else if !child.elements.isEmpty || !child.attributes.isEmpty { let newValue = child.flatten() - if let existingValue = node[childElement.key] { + if let existingValue = elements[element.key] { if let unkeyed = existingValue as? UnkeyedBox { - unkeyed.append(KeyedBox(newValue)) + unkeyed.append(newValue) // FIXME: Box is a reference type, so this shouldn't be necessary: - node[childElement.key] = unkeyed + elements[element.key] = unkeyed } else { - node[childElement.key] = UnkeyedBox([existingValue, KeyedBox(newValue)]) + elements[element.key] = UnkeyedBox([existingValue, newValue]) } } else { - node[childElement.key] = KeyedBox(newValue) + elements[element.key] = newValue } } } } - return node + return KeyedBox(elements: elements, attributes: attributes) } func toXMLString(with header: XMLHeader? = nil, withCDATA cdata: Bool, formatting: XMLEncoder.OutputFormatting, ignoreEscaping _: Bool = false) -> String { @@ -122,12 +119,12 @@ class _XMLElement { } fileprivate func formatUnsortedXMLElements(_ string: inout String, _ level: Int, _ cdata: Bool, _ formatting: XMLEncoder.OutputFormatting, _ prettyPrinted: Bool) { - formatXMLElements(from: children.map { (key: $0, value: $1) }, into: &string, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted) + formatXMLElements(from: elements.map { (key: $0, value: $1) }, into: &string, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted) } - fileprivate func elementString(for childElement: (key: String, value: [_XMLElement]), at level: Int, cdata: Bool, formatting: XMLEncoder.OutputFormatting, prettyPrinted: Bool) -> String { + fileprivate func elementString(for element: (key: String, value: [_XMLElement]), at level: Int, cdata: Bool, formatting: XMLEncoder.OutputFormatting, prettyPrinted: Bool) -> String { var string = "" - for child in childElement.value { + for child in element.value { string += child._toXMLString(indented: level + 1, withCDATA: cdata, formatting: formatting) string += prettyPrinted ? "\n" : "" } @@ -135,7 +132,7 @@ class _XMLElement { } fileprivate func formatSortedXMLElements(_ string: inout String, _ level: Int, _ cdata: Bool, _ formatting: XMLEncoder.OutputFormatting, _ prettyPrinted: Bool) { - formatXMLElements(from: children.sorted { $0.key < $1.key }, into: &string, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted) + formatXMLElements(from: elements.sorted { $0.key < $1.key }, into: &string, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted) } fileprivate func attributeString(key: String, value: String) -> String { @@ -148,9 +145,9 @@ class _XMLElement { } } - fileprivate func formatXMLElements(from children: [(key: String, value: [_XMLElement])], into string: inout String, at level: Int, cdata: Bool, formatting: XMLEncoder.OutputFormatting, prettyPrinted: Bool) { - for childElement in children { - string += elementString(for: childElement, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted) + fileprivate func formatXMLElements(from elements: [(key: String, value: [_XMLElement])], into string: inout String, at level: Int, cdata: Bool, formatting: XMLEncoder.OutputFormatting, prettyPrinted: Bool) { + for element in elements { + string += elementString(for: element, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted) } } @@ -202,7 +199,7 @@ class _XMLElement { string += "\(value)" } string += "" - } else if !children.isEmpty { + } else if !elements.isEmpty { string += prettyPrinted ? ">\n" : ">" formatXMLElements(formatting, &string, level, cdata, prettyPrinted) @@ -222,9 +219,9 @@ extension _XMLElement: Equatable { lhs.key == rhs.key, lhs.value == rhs.value, lhs.attributes == rhs.attributes, - lhs.children == rhs.children - else { - return false + lhs.elements == rhs.elements + else { + return false } return true } diff --git a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift index ec366800..4ae22677 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift @@ -16,7 +16,7 @@ class _XMLStackParser: NSObject { var currentElementName: String? = nil var currentElementData = "" - static func parse(with data: Data) throws -> [String: Box] { + static func parse(with data: Data) throws -> KeyedBox { let parser = _XMLStackParser() guard let node = try parser.parse(with: data) else { @@ -55,7 +55,7 @@ extension _XMLStackParser: XMLParserDelegate { stack.append(node) - currentNode?.children[elementName, default: []].append(node) + currentNode?.elements[elementName, default: []].append(node) currentNode = node } diff --git a/Sources/XMLCoder/Box/KeyedBox.swift b/Sources/XMLCoder/Box/KeyedBox.swift index ffe88790..ec2a2da1 100644 --- a/Sources/XMLCoder/Box/KeyedBox.swift +++ b/Sources/XMLCoder/Box/KeyedBox.swift @@ -7,59 +7,96 @@ import Foundation -// Minimalist implementation of an order-preserving keyed box: -class KeyedBox { - typealias Key = String - typealias Value = Box - - typealias Unboxed = [Key: Value] +struct KeyedStorage { + typealias Buffer = [Key: Value] - private(set) var unboxed: Unboxed + fileprivate var buffer: Buffer = [:] var count: Int { - return self.unboxed.count + return self.buffer.count + } + + var keys: Buffer.Keys { + return self.buffer.keys } - var keys: Unboxed.Keys { - return self.unboxed.keys + init(_ buffer: Buffer) { + self.buffer = buffer } subscript(key: Key) -> Value? { get { - return self.unboxed[key] + return self.buffer[key] } set { - self.unboxed[key] = newValue + self.buffer[key] = newValue } } - init(_ unboxed: Unboxed = [:]) { - self.unboxed = unboxed + func filter(_ isIncluded: (Key, Value) throws -> Bool) rethrows -> [(Key, Value)] { + return try self.buffer.filter(isIncluded) } - convenience init(_ keysAndValues: S, uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows where S : Sequence, S.Element == (Key, Value) { - let unboxed = try Dictionary(keysAndValues, uniquingKeysWith: combine) - self.init(unboxed) + func map(_ transform: (Key, Value) throws -> T) rethrows -> [T] { + return try self.buffer.map(transform) } - func unbox() -> Unboxed { - return self.unboxed + func compactMap(_ transform: ((Key, Value)) throws -> T?) rethrows -> [T] { + return try self.buffer.compactMap(transform) } - func filter(_ isIncluded: (Key, Value) throws -> Bool) rethrows -> [(Key, Value)] { - return try self.unboxed.filter(isIncluded) + func mapValues(_ transform: (Value) throws -> Value) rethrows -> KeyedStorage { + return KeyedStorage(try self.buffer.mapValues(transform)) } +} + +extension KeyedStorage: Sequence { + typealias Iterator = Buffer.Iterator - func map(_ transform: (Key, Value) throws -> T) rethrows -> [T] { - return try self.unboxed.map(transform) + func makeIterator() -> Iterator { + return self.buffer.makeIterator() } +} + +extension KeyedStorage: ExpressibleByDictionaryLiteral { + init(dictionaryLiteral elements: (Key, Value)...) { + self.init(Dictionary(uniqueKeysWithValues: elements)) + } +} + +class KeyedBox { + typealias Key = String + typealias Attribute = SimpleBox + typealias Element = Box + + typealias Attributes = KeyedStorage + typealias Elements = KeyedStorage - func compactMap(_ transform: ((Key, Value)) throws -> T?) rethrows -> [T] { - return try self.unboxed.compactMap(transform) + var attributes: Attributes = [:] + var elements: Elements = [:] + + init() { + self.attributes = [:] + self.elements = [:] + } + + init(elements: E, attributes: A) + where E: Sequence, E.Element == (Key, Element), A: Sequence, A.Element == (Key, Attribute) + { + self.elements = Elements(Dictionary(uniqueKeysWithValues: elements)) + self.attributes = Attributes(Dictionary(uniqueKeysWithValues: attributes)) + } + + init(elements: [Key: Element], attributes: [Key: Attribute]) { + self.elements = Elements(elements) + self.attributes = Attributes(attributes) } - func mapValues(_ transform: (Value) throws -> Value) rethrows -> KeyedBox { - return KeyedBox(try self.unboxed.mapValues(transform)) + func unbox() -> (elements: Elements, attributes: Attributes) { + return ( + elements: self.elements, + attributes: self.attributes + ) } } @@ -72,17 +109,3 @@ extension KeyedBox: Box { return nil } } - -extension KeyedBox: Sequence { - typealias Iterator = Unboxed.Iterator - - func makeIterator() -> Iterator { - return self.unboxed.makeIterator() - } -} - -extension KeyedBox: CustomStringConvertible { - var description: String { - return self.unboxed.description - } -} diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index df7ed380..f7589ade 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -245,7 +245,7 @@ open class XMLDecoder { open func decode(_ type: T.Type, from data: Data) throws -> T { let topLevel: Box do { - topLevel = KeyedBox(try _XMLStackParser.parse(with: data)) + topLevel = try _XMLStackParser.parse(with: data) } catch { throw DecodingError.dataCorrupted(DecodingError.Context( codingPath: [], @@ -576,7 +576,7 @@ extension _XMLDecoder { } } - internal func unbox(_ box: Box) throws -> URL? { + func unbox(_ box: Box) throws -> URL? { guard !box.isNull else { return nil } guard let string = (box as? StringBox)?.unbox() else { return nil } diff --git a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift index 871c347a..2c900cd5 100644 --- a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift @@ -44,17 +44,29 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol case .convertFromSnakeCase: // Convert the snake case keys in the container to camel case. // If we hit a duplicate key after conversion, then we'll use the first one we saw. Effectively an undefined behavior with dictionaries. - self.container = KeyedBox(container.map { - key, value in (XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase(key), value) - }, uniquingKeysWith: { first, _ in first }) + let attributes = container.attributes.map { key, value in + (XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase(key), value) + } + let elements = container.elements.map { key, value in + (XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase(key), value) + } + self.container = KeyedBox(elements: elements, attributes: attributes) case .convertFromCapitalized: - self.container = KeyedBox(container.map { - key, value in (XMLDecoder.KeyDecodingStrategy._convertFromCapitalized(key), value) - }, uniquingKeysWith: { first, _ in first }) + let attributes = container.attributes.map { key, value in + (XMLDecoder.KeyDecodingStrategy._convertFromCapitalized(key), value) + } + let elements = container.elements.map { key, value in + (XMLDecoder.KeyDecodingStrategy._convertFromCapitalized(key), value) + } + self.container = KeyedBox(elements: elements, attributes: attributes) case let .custom(converter): - self.container = KeyedBox(container.map { - key, value in (converter(decoder.codingPath + [_XMLKey(stringValue: key, intValue: nil)]).stringValue, value) - }, uniquingKeysWith: { first, _ in first }) + let attributes = container.attributes.map { key, value in + (converter(decoder.codingPath + [_XMLKey(stringValue: key, intValue: nil)]).stringValue, value) + } + let elements = container.elements.map { key, value in + (converter(decoder.codingPath + [_XMLKey(stringValue: key, intValue: nil)]).stringValue, value) + } + self.container = KeyedBox(elements: elements, attributes: attributes) } codingPath = decoder.codingPath } @@ -62,11 +74,14 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol // MARK: - KeyedDecodingContainerProtocol Methods public var allKeys: [Key] { - return container.keys.compactMap { Key(stringValue: $0) } + let attributeKeys = Array(container.attributes.keys.compactMap { Key(stringValue: $0) }) + let elementKeys = Array(container.elements.keys.compactMap { Key(stringValue: $0) }) + return attributeKeys + elementKeys } public func contains(_ key: Key) -> Bool { - return container[key.stringValue] != nil + let keyString = key.stringValue + return (container.attributes[keyString] != nil) || (container.elements[keyString] != nil) } private func _errorDescription(of key: CodingKey) -> String { @@ -87,7 +102,10 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol } public func decodeNil(forKey key: Key) throws -> Bool { - if let entry = container[key.stringValue] { + let keyString = key.stringValue + if let entry = container.attributes[keyString] { + return entry.isNull + } else if let entry = container.elements[key.stringValue] { return entry.isNull } else { return true @@ -173,7 +191,10 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol } public func decode(_ type: T.Type, forKey key: Key) throws -> T { - if type is AnyEmptySequence.Type && container[key.stringValue] == nil { + let attributeNotFound = (container.attributes[key.stringValue] == nil) + let elementNotFound = (container.elements[key.stringValue] == nil) + + if type is AnyEmptySequence.Type && attributeNotFound && elementNotFound { return (type as! AnyEmptySequence.Type).init() as! T } @@ -205,7 +226,7 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol forKey key: Key, decode: (_XMLDecoder, Box) throws -> T? ) throws -> T { - guard let entry = container[key.stringValue] else { + guard let entry = container.elements[key.stringValue] ?? container.attributes[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context( codingPath: decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key))." @@ -229,7 +250,7 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol decoder.codingPath.append(key) defer { decoder.codingPath.removeLast() } - guard let value = self.container[key.stringValue] else { + guard let value = self.container.elements[key.stringValue] ?? self.container.attributes[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context( codingPath: codingPath, debugDescription: "Cannot get \(KeyedDecodingContainer.self) -- no value found for key \"\(key.stringValue)\"" @@ -248,7 +269,7 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol decoder.codingPath.append(key) defer { decoder.codingPath.removeLast() } - guard let value = self.container[key.stringValue] else { + guard let value = container.elements[key.stringValue] ?? container.attributes[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context( codingPath: codingPath, debugDescription: "Cannot get UnkeyedDecodingContainer -- no value found for key \"\(key.stringValue)\"" @@ -266,7 +287,7 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol decoder.codingPath.append(key) defer { decoder.codingPath.removeLast() } - let box: Box = container[key.stringValue] ?? NullBox() + let box: Box = container.elements[key.stringValue] ?? container.attributes[key.stringValue] ?? NullBox() return _XMLDecoder(referencing: box, at: decoder.codingPath, options: decoder.options) } diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index ef851ce9..215cabfb 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -551,7 +551,7 @@ extension _XMLEncoder { return URLBox(value) } - internal func box(_ value: T) throws -> Box { + func box(_ value: T) throws -> Box { if T.self == Date.self || T.self == NSDate.self { return try box(value as! Date) } else if T.self == Data.self || T.self == NSData.self { diff --git a/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift b/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift index 1f63587c..7e54683e 100644 --- a/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift +++ b/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift @@ -47,7 +47,7 @@ struct _XMLKeyedEncodingContainer : KeyedEncodingContainerProtoco // MARK: - KeyedEncodingContainerProtocol Methods public mutating func encodeNil(forKey key: Key) throws { - self.container[_converted(key).stringValue] = NullBox() + self.container.elements[_converted(key).stringValue] = NullBox() } public mutating func encode(_ value: Bool, forKey key: Key) throws { @@ -173,27 +173,21 @@ struct _XMLKeyedEncodingContainer : KeyedEncodingContainerProtoco let box = try encode(self.encoder, value) switch strategy(key) { case .attribute: - guard box is SimpleBox else { + guard let attribute = box as? SimpleBox else { throw EncodingError.invalidValue(value, EncodingError.Context( codingPath: [], debugDescription: "Complex values cannot be encoded as attributes." )) } - if let attributesContainer = self.container[_XMLElement.attributesKey] as? KeyedBox { - attributesContainer[_converted(key).stringValue] = box - } else { - let attributesContainer = KeyedBox() - attributesContainer[_converted(key).stringValue] = box - self.container[_XMLElement.attributesKey] = attributesContainer - } + self.container.attributes[_converted(key).stringValue] = attribute case .element: - self.container[_converted(key).stringValue] = box + self.container.elements[_converted(key).stringValue] = box } } public mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { let keyed = KeyedBox() - self.container[_converted(key).stringValue] = keyed + self.container.elements[_converted(key).stringValue] = keyed self.codingPath.append(key) defer { self.codingPath.removeLast() } @@ -204,7 +198,7 @@ struct _XMLKeyedEncodingContainer : KeyedEncodingContainerProtoco public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { let unkeyed = UnkeyedBox() - self.container[_converted(key).stringValue] = unkeyed + self.container.elements[_converted(key).stringValue] = unkeyed self.codingPath.append(key) defer { self.codingPath.removeLast() } diff --git a/Sources/XMLCoder/Encoder/XMLReferencingEncoder.swift b/Sources/XMLCoder/Encoder/XMLReferencingEncoder.swift index c6dca8d2..c73a72b8 100644 --- a/Sources/XMLCoder/Encoder/XMLReferencingEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLReferencingEncoder.swift @@ -93,7 +93,7 @@ class _XMLReferencingEncoder: _XMLEncoder { case let .unkeyed(unkeyed, index): unkeyed.insert(box, at: index) case let .keyed(keyed, key): - keyed[key] = box + keyed.elements[key] = box } } } diff --git a/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift b/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift index b82fc6b3..f75defa7 100644 --- a/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift +++ b/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift @@ -19,7 +19,7 @@ class XMLStackParserTests: XCTestCase { let expected = _XMLElement( key: "container", - children: [ + elements: [ "value": [ _XMLElement( key: "value", diff --git a/Tests/XMLCoderTests/Box/FloatBoxTests.swift b/Tests/XMLCoderTests/Box/FloatBoxTests.swift index ad066451..68c895fe 100644 --- a/Tests/XMLCoderTests/Box/FloatBoxTests.swift +++ b/Tests/XMLCoderTests/Box/FloatBoxTests.swift @@ -42,11 +42,6 @@ class FloatBoxTests: XCTestCase { } } - func testDescription() { - let box = FloatBox(4.2) - XCTAssertEqual(box.description, "4.2") - } - func testValidValues() { let values: [String] = [ "-3E2", diff --git a/Tests/XMLCoderTests/Box/KeyedBoxTests.swift b/Tests/XMLCoderTests/Box/KeyedBoxTests.swift index 9a0e3089..bedd369c 100644 --- a/Tests/XMLCoderTests/Box/KeyedBoxTests.swift +++ b/Tests/XMLCoderTests/Box/KeyedBoxTests.swift @@ -9,25 +9,23 @@ import XCTest @testable import XMLCoder class KeyedBoxTests: XCTestCase { - lazy var box = KeyedBox(["foo": StringBox("bar"), "baz": IntBox(42)]) + lazy var box = KeyedBox( + elements: ["foo": StringBox("bar"), "baz": IntBox(42)], + attributes: ["baz": StringBox("blee")] + ) func testUnbox() { - let unboxed = box.unbox() - XCTAssertEqual(unboxed.count, 2) - XCTAssertEqual(unboxed["foo"] as? StringBox, StringBox("bar")) - XCTAssertEqual(unboxed["baz"] as? IntBox, IntBox(42)) + let (elements, attributes) = box.unbox() + + XCTAssertEqual(elements.count, 2) + XCTAssertEqual(elements["foo"] as? StringBox, StringBox("bar")) + XCTAssertEqual(elements["baz"] as? IntBox, IntBox(42)) + + XCTAssertEqual(attributes.count, 1) + XCTAssertEqual(attributes["baz"] as? StringBox, StringBox("blee")) } func testXMLString() { XCTAssertEqual(box.xmlString(), nil) } - - func testDescription() { - // Element order is undefined, hence we just check for infixes: - XCTAssertTrue(box.description.contains("\"baz\": 42")) - XCTAssertTrue(box.description.contains("\"foo\": bar")) - XCTAssertTrue(box.description.contains(", ")) - XCTAssertTrue(box.description.hasPrefix("[")) - XCTAssertTrue(box.description.hasSuffix("]")) - } } diff --git a/Tests/XMLCoderTests/Box/UnkeyedBoxTests.swift b/Tests/XMLCoderTests/Box/UnkeyedBoxTests.swift index 30fb4361..83871c8d 100644 --- a/Tests/XMLCoderTests/Box/UnkeyedBoxTests.swift +++ b/Tests/XMLCoderTests/Box/UnkeyedBoxTests.swift @@ -21,8 +21,4 @@ class UnkeyedBoxTests: XCTestCase { func testXMLString() { XCTAssertEqual(box.xmlString(), nil) } - - func testDescription() { - XCTAssertEqual(box.description, "[foo, 42]") - } } From 8fb8aab7faaf7c057f26bd690fc678c4a632ba1a Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Fri, 21 Dec 2018 10:36:55 +0100 Subject: [PATCH 2/5] Removed obscure use of `static func` in `XMLElement` in favor of simple initializers and instance methods --- Sources/XMLCoder/Auxiliaries/XMLElement.swift | 82 ++++++++++++------- Sources/XMLCoder/Box/KeyedBox.swift | 10 ++- Sources/XMLCoder/Box/UnkeyedBox.swift | 4 +- Sources/XMLCoder/Encoder/XMLEncoder.swift | 4 +- 4 files changed, 64 insertions(+), 36 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/XMLElement.swift b/Sources/XMLCoder/Auxiliaries/XMLElement.swift index 6ee36ac6..5ab0e8e2 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLElement.swift @@ -23,48 +23,61 @@ class _XMLElement { self.elements = elements } - static func createRootElement(rootKey: String, object: UnkeyedBox) -> _XMLElement? { - let element = _XMLElement(key: rootKey) + convenience init(key: String, box: UnkeyedBox) { + self.init(key: key) - _XMLElement.createElement(parentElement: element, key: rootKey, object: object) - - return element + self.elements[key] = box.map { box in + _XMLElement(key: key, box: box) + } } - static func createRootElement(rootKey: String, object: KeyedBox) -> _XMLElement? { - let element = _XMLElement(key: rootKey) - - _XMLElement.modifyElement(element: element, parentElement: nil, key: nil, object: object) + convenience init(key: String, box: KeyedBox) { + self.init(key: key) - return element - } - - fileprivate static func modifyElement(element: _XMLElement, parentElement: _XMLElement?, key: String?, object: KeyedBox) { - let uniqueAttributes: [(String, String)]? = object.attributes.compactMap { key, box in - return box.xmlString().map { (key, $0) } - } - element.attributes = uniqueAttributes.map { Dictionary(uniqueKeysWithValues: $0) } ?? [:] + self.attributes = Dictionary(uniqueKeysWithValues: box.attributes.compactMap { key, box in + guard let value = box.xmlString() else { + return nil + } + return (key, value) + }) - for (key, box) in object.elements { - _XMLElement.createElement(parentElement: element, key: key, object: box) + let elementsByKey: [(String, [_XMLElement])] = box.elements.map { key, box in + switch box { + case let unkeyedBox as UnkeyedBox: + // This basically injects the unkeyed children directly into self: + let elements = unkeyedBox.map { _XMLElement(key: key, box: $0) } + return (key, elements) + case let keyedBox as KeyedBox: + let elements = [_XMLElement(key: key, box: keyedBox)] + return (key, elements) + case let simpleBox as SimpleBox: + let elements = [_XMLElement(key: key, box: simpleBox)] + return (key, elements) + case _: + preconditionFailure("Unclassified box.") + } } - if let parentElement = parentElement, let key = key { - parentElement.elements[key] = (parentElement.elements[key] ?? []) + [element] + self.elements = Dictionary(elementsByKey) { existingElements, newElements in + existingElements + newElements } } - fileprivate static func createElement(parentElement: _XMLElement, key: String, object: Box) { - switch object { - case let box as UnkeyedBox: - for box in box.unbox() { - _XMLElement.createElement(parentElement: parentElement, key: key, object: box) - } - case let box as KeyedBox: - modifyElement(element: _XMLElement(key: key), parentElement: parentElement, key: key, object: box) + convenience init(key: String, box: SimpleBox) { + self.init(key: key) + self.value = box.xmlString() + } + + convenience init(key: String, box: Box) { + switch box { + case let unkeyedBox as UnkeyedBox: + self.init(key: key, box: unkeyedBox) + case let keyedBox as KeyedBox: + self.init(key: key, box: keyedBox) + case let simpleBox as SimpleBox: + self.init(key: key, box: simpleBox) case _: - let element = _XMLElement(key: key, value: object.xmlString()) - parentElement.elements[key, default: []].append(element) + preconditionFailure("Unclassified box.") } } @@ -108,6 +121,13 @@ class _XMLElement { } } + + func append(value string: String) { + var value = self.value ?? "" + value += string.trimmingCharacters(in: .whitespacesAndNewlines) + self.value = value + } + return KeyedBox(elements: elements, attributes: attributes) } diff --git a/Sources/XMLCoder/Box/KeyedBox.swift b/Sources/XMLCoder/Box/KeyedBox.swift index ec2a2da1..1018180e 100644 --- a/Sources/XMLCoder/Box/KeyedBox.swift +++ b/Sources/XMLCoder/Box/KeyedBox.swift @@ -45,8 +45,16 @@ struct KeyedStorage { return try self.buffer.compactMap(transform) } + func mapValues(_ transform: (Value) throws -> T) rethrows -> [Key: T] { + return try self.buffer.mapValues(transform) + } + + func mapValues(_ transform: (Value) throws -> T) rethrows -> [(Key, T)] { + return Array(try self.mapValues(transform)) + } + func mapValues(_ transform: (Value) throws -> Value) rethrows -> KeyedStorage { - return KeyedStorage(try self.buffer.mapValues(transform)) + return KeyedStorage(try self.mapValues(transform)) } } diff --git a/Sources/XMLCoder/Box/UnkeyedBox.swift b/Sources/XMLCoder/Box/UnkeyedBox.swift index 83013757..d8ca4f1a 100644 --- a/Sources/XMLCoder/Box/UnkeyedBox.swift +++ b/Sources/XMLCoder/Box/UnkeyedBox.swift @@ -47,11 +47,11 @@ class UnkeyedBox { return try self.unboxed.filter(isIncluded) } - func map(_ transform: (Any) throws -> T) rethrows -> [T] { + func map(_ transform: (Element) throws -> T) rethrows -> [T] { return try self.unboxed.map(transform) } - func compactMap(_ transform: (Any) throws -> T?) rethrows -> [T] { + func compactMap(_ transform: (Element) throws -> T?) rethrows -> [T] { return try self.unboxed.compactMap(transform) } } diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index 215cabfb..3bf36dbb 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -270,9 +270,9 @@ open class XMLEncoder { let elementOrNone: _XMLElement? if let keyed = topLevel as? KeyedBox { - elementOrNone = _XMLElement.createRootElement(rootKey: rootKey, object: keyed) + elementOrNone = _XMLElement(key: rootKey, box: keyed) } else if let unkeyed = topLevel as? UnkeyedBox { - elementOrNone = _XMLElement.createRootElement(rootKey: rootKey, object: unkeyed) + elementOrNone = _XMLElement(key: rootKey, box: unkeyed) } else { fatalError("Unrecognized top-level element.") } From 462618fb724150c092b385964d51119a085ab9dd Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Fri, 21 Dec 2018 11:30:24 +0100 Subject: [PATCH 3/5] Completely removed mutating methods from `XMLElement`! --- Sources/XMLCoder/Auxiliaries/XMLElement.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/XMLElement.swift b/Sources/XMLCoder/Auxiliaries/XMLElement.swift index 5ab0e8e2..ed8c8372 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLElement.swift @@ -120,13 +120,6 @@ class _XMLElement { } } } - - - func append(value string: String) { - var value = self.value ?? "" - value += string.trimmingCharacters(in: .whitespacesAndNewlines) - self.value = value - } return KeyedBox(elements: elements, attributes: attributes) } From 80c5b790c629ee31c0502448b8001a7919421964 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Fri, 21 Dec 2018 15:17:05 +0100 Subject: [PATCH 4/5] Turned `_XMLElement` from a `class` into a `struct`! --- Sources/XMLCoder/Auxiliaries/XMLElement.swift | 16 +++--- .../XMLCoder/Auxiliaries/XMLStackParser.swift | 52 +++++++++---------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/XMLElement.swift b/Sources/XMLCoder/Auxiliaries/XMLElement.swift index ed8c8372..74983d3a 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLElement.swift @@ -7,7 +7,7 @@ import Foundation -class _XMLElement { +struct _XMLElement { static let attributesKey = "___ATTRIBUTES" static let escapedCharacterSet = [("&", "&"), ("<", "<"), (">", ">"), ("'", "'"), ("\"", """)] @@ -23,7 +23,7 @@ class _XMLElement { self.elements = elements } - convenience init(key: String, box: UnkeyedBox) { + init(key: String, box: UnkeyedBox) { self.init(key: key) self.elements[key] = box.map { box in @@ -31,7 +31,7 @@ class _XMLElement { } } - convenience init(key: String, box: KeyedBox) { + init(key: String, box: KeyedBox) { self.init(key: key) self.attributes = Dictionary(uniqueKeysWithValues: box.attributes.compactMap { key, box in @@ -63,12 +63,12 @@ class _XMLElement { } } - convenience init(key: String, box: SimpleBox) { + init(key: String, box: SimpleBox) { self.init(key: key) self.value = box.xmlString() } - convenience init(key: String, box: Box) { + init(key: String, box: Box) { switch box { case let unkeyedBox as UnkeyedBox: self.init(key: key, box: unkeyedBox) @@ -81,12 +81,16 @@ class _XMLElement { } } - func append(value string: String) { + mutating func append(value string: String) { var value = self.value ?? "" value += string.trimmingCharacters(in: .whitespacesAndNewlines) self.value = value } + mutating func append(element: _XMLElement, forKey key: String) { + self.elements[key, default: []].append(element) + } + func flatten() -> KeyedBox { let attributes = self.attributes.mapValues { StringBox($0) } var elements: [String: Box] = [:] diff --git a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift index 4ae22677..a9bd2098 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift @@ -8,13 +8,13 @@ import Foundation +struct XMLElementContext { + +} + class _XMLStackParser: NSObject { var root: _XMLElement? = nil - var stack: [_XMLElement] = [] - var currentNode: _XMLElement? = nil - - var currentElementName: String? = nil - var currentElementData = "" + private var stack: [_XMLElement] = [] static func parse(with data: Data) throws -> KeyedBox { let parser = _XMLStackParser() @@ -42,6 +42,13 @@ class _XMLStackParser: NSObject { return root } + + func withCurrentElement(_ body: (inout _XMLElement) throws -> ()) rethrows { + guard !stack.isEmpty else { + return + } + try body(&stack[stack.count - 1]) + } } extension _XMLStackParser: XMLParserDelegate { @@ -51,48 +58,41 @@ extension _XMLStackParser: XMLParserDelegate { } func parser(_: XMLParser, didStartElement elementName: String, namespaceURI _: String?, qualifiedName _: String?, attributes attributeDict: [String: String] = [:]) { - let node = _XMLElement(key: elementName, attributes: attributeDict) - - stack.append(node) - - currentNode?.elements[elementName, default: []].append(node) - - currentNode = node + let element = _XMLElement(key: elementName, attributes: attributeDict) + stack.append(element) } func parser(_: XMLParser, didEndElement _: String, namespaceURI _: String?, qualifiedName _: String?) { - guard let poppedNode = stack.popLast() else { + guard var element = stack.popLast() else { return } - if let value = poppedNode.value { - poppedNode.value = value.isEmpty ? nil : value + if let value = element.value { + element.value = value.isEmpty ? nil : value + } + + withCurrentElement { currentElement in + currentElement.append(element: element, forKey: element.key) } if stack.isEmpty { - root = poppedNode + root = element } - - currentNode = stack.last } func parser(_: XMLParser, foundCharacters string: String) { - guard let currentNode = currentNode else { - return + withCurrentElement { currentElement in + currentElement.append(value: string) } - - currentNode.append(value: string) } func parser(_: XMLParser, foundCDATA CDATABlock: Data) { guard let string = String(data: CDATABlock, encoding: .utf8) else { return } - guard let currentNode = currentNode else { - return + withCurrentElement { currentElement in + currentElement.append(value: string) } - - currentNode.append(value: string) } func parser(_: XMLParser, parseErrorOccurred parseError: Error) { From 7412933cfd8ebb83bf1039ae6799dcccf0a1386e Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Fri, 21 Dec 2018 15:34:55 +0100 Subject: [PATCH 5/5] Simplified `func flatten()` --- Sources/XMLCoder/Auxiliaries/XMLElement.swift | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/XMLElement.swift b/Sources/XMLCoder/Auxiliaries/XMLElement.swift index 74983d3a..de789c6d 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLElement.swift @@ -40,7 +40,7 @@ struct _XMLElement { } return (key, value) }) - + let elementsByKey: [(String, [_XMLElement])] = box.elements.map { key, box in switch box { case let unkeyedBox as UnkeyedBox: @@ -53,8 +53,8 @@ struct _XMLElement { case let simpleBox as SimpleBox: let elements = [_XMLElement(key: key, box: simpleBox)] return (key, elements) - case _: - preconditionFailure("Unclassified box.") + case let box: + preconditionFailure("Unclassified box: \(type(of: box))") } } @@ -76,8 +76,8 @@ struct _XMLElement { self.init(key: key, box: keyedBox) case let simpleBox as SimpleBox: self.init(key: key, box: simpleBox) - case _: - preconditionFailure("Unclassified box.") + case let box: + preconditionFailure("Unclassified box: \(type(of: box))") } } @@ -93,38 +93,38 @@ struct _XMLElement { func flatten() -> KeyedBox { let attributes = self.attributes.mapValues { StringBox($0) } - var elements: [String: Box] = [:] - for element in self.elements { - for child in element.value { + var elements: [String: Box] = [:] + for (key, value) in self.elements { + for child in value { if let content = child.value { - if let oldContent = elements[element.key] as? UnkeyedBox { - oldContent.append(StringBox(content)) - // FIXME: Box is a reference type, so this shouldn't be necessary: - elements[element.key] = oldContent - } else if let oldContent = elements[element.key] { - elements[element.key] = UnkeyedBox([oldContent, StringBox(content)]) - } else { - elements[element.key] = StringBox(content) + switch elements[key] { + case let unkeyedBox as UnkeyedBox: + var boxes = unkeyedBox.unbox() + boxes.append(StringBox(content)) + elements[key] = UnkeyedBox(boxes) + case let keyedBox as StringBox: + elements[key] = UnkeyedBox([keyedBox, StringBox(content)]) + case _: + elements[key] = StringBox(content) } } else if !child.elements.isEmpty || !child.attributes.isEmpty { - let newValue = child.flatten() - - if let existingValue = elements[element.key] { - if let unkeyed = existingValue as? UnkeyedBox { - unkeyed.append(newValue) - // FIXME: Box is a reference type, so this shouldn't be necessary: - elements[element.key] = unkeyed + let content = child.flatten() + if let existingValue = elements[key] { + if let unkeyedBox = existingValue as? UnkeyedBox { + var boxes = unkeyedBox.unbox() + boxes.append(content) + elements[key] = UnkeyedBox(boxes) } else { - elements[element.key] = UnkeyedBox([existingValue, newValue]) + elements[key] = UnkeyedBox([existingValue, content]) } } else { - elements[element.key] = newValue + elements[key] = content } } } } - + return KeyedBox(elements: elements, attributes: attributes) }