-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
) This PR removes any remaining use of `NS…Array`/`NS…Dictionary`/`NSNumber`/`NSDecimalNumber`/`NSNull`/… (as storage) from the code-base. It introduces an internal type … ```swift internal enum Box { case null(NullBox) case bool(BoolBox) case decimal(DecimalBox) case signedInteger(SignedIntegerBox) case unsignedInteger(UnsignedIntegerBox) case floatingPoint(FloatingPointBox) case string(StringBox) case array(ArrayBox) case dictionary(DictionaryBox) } ``` … as well as accompanying variant box types, replacing use of `Any`/`NS…`. 👷🏻♀️It improves type-safety by reducing use of `Any` (from 60 down to 19 occurrences) as well as `NSObject` (from 37 down to 1 occurrence). 🏗It further more levels the field for improvements/additions such as [support for order-preserving elements & attributes](#17). 💡Thanks to encapsulation we can now safely change the inner logic of `DictionaryBox` to retain insertion order. **Edit**: We ended up replacing aforementioned `enum Box` with a protocol: ```swift protocol Box { var isNull: Bool { get } var isFragment: Bool { get } func xmlString() -> String? } ``` * Added basic box types * Migrated decoding logic to proper boxes * Migrated encoding logic to proper boxes * Moved auxiliary types into their own dedicated files and group * Replaced use of `.description` with explicit dedicated `.xmlString` * Made box types conform to `Equatable` * Added basic unit tests for box types * Removed manual `Equatable` conformance, changed fragment boxes to structs * Removed redundant use of `internal` on internal types * Simplified logic of `func createElement` instances * Removed `enum Box` in favor of `protocol Box` * Fixed error description for `.typeMismatch` * Shortened type names of `FloatingPointBox`, `SignedIntegerBox` and `UnsignedIntegerBox` * Moved unboxing logic into dedicated Box initializers * Removed last remaining use of explicit `internal` related to this PR * Changed `Unboxed` types of `IntBox` & `UIntBox` to explicit 64bit types * Made `.xmlString` conform to W3.org’s XML-spec * Renamed `.init(string:…)` to `.init(xmlString:…)` * Implemented conformity to encoding strategies * Simplified box unit tests * Added minimalist decoding unit tests for individual box types * Fixed bug related to `Float` encoding and simplified internal encoding/decoding locig
- Loading branch information
1 parent
863b160
commit ad96c5e
Showing
52 changed files
with
3,287 additions
and
1,122 deletions.
There are no files selected for viewing
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// | ||
// String+Extensions.swift | ||
// XMLCoder | ||
// | ||
// Created by Vincent Esche on 12/18/18. | ||
// | ||
|
||
import Foundation | ||
|
||
extension String { | ||
internal func escape(_ characterSet: [(character: String, escapedCharacter: String)]) -> String { | ||
var string = self | ||
|
||
for set in characterSet { | ||
string = string.replacingOccurrences(of: set.character, with: set.escapedCharacter, options: .literal) | ||
} | ||
|
||
return string | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
// | ||
// XMLElement.swift | ||
// XMLCoder | ||
// | ||
// Created by Vincent Esche on 12/18/18. | ||
// | ||
|
||
import Foundation | ||
|
||
internal class _XMLElement { | ||
static let attributesKey = "___ATTRIBUTES" | ||
static let escapedCharacterSet = [("&", "&"), ("<", "<"), (">", ">"), ("'", "'"), ("\"", """)] | ||
|
||
var key: String | ||
var value: String? | ||
var attributes: [String: String] = [:] | ||
var children: [String: [_XMLElement]] = [:] | ||
|
||
internal init(key: String, value: String? = nil, attributes: [String: String] = [:], children: [String: [_XMLElement]] = [:]) { | ||
self.key = key | ||
self.value = value | ||
self.attributes = attributes | ||
self.children = children | ||
} | ||
|
||
static func createRootElement(rootKey: String, object: ArrayBox) -> _XMLElement? { | ||
let element = _XMLElement(key: rootKey) | ||
|
||
_XMLElement.createElement(parentElement: element, key: rootKey, object: object) | ||
|
||
return element | ||
} | ||
|
||
static func createRootElement(rootKey: String, object: DictionaryBox) -> _XMLElement? { | ||
let element = _XMLElement(key: rootKey) | ||
|
||
_XMLElement.modifyElement(element: element, parentElement: nil, key: nil, object: object) | ||
|
||
return element | ||
} | ||
|
||
fileprivate static func modifyElement(element: _XMLElement, parentElement: _XMLElement?, key: String?, object: DictionaryBox) { | ||
let attributesBox = object[_XMLElement.attributesKey] as? DictionaryBox | ||
let uniqueAttributes: [(String, String)]? = attributesBox?.unbox().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 { | ||
_XMLElement.createElement(parentElement: element, key: key, object: box) | ||
} | ||
|
||
if let parentElement = parentElement, let key = key { | ||
parentElement.children[key] = (parentElement.children[key] ?? []) + [element] | ||
} | ||
} | ||
|
||
fileprivate static func createElement(parentElement: _XMLElement, key: String, object: Box) { | ||
switch object { | ||
case let box as ArrayBox: | ||
for box in box.unbox() { | ||
_XMLElement.createElement(parentElement: parentElement, key: key, object: box) | ||
} | ||
case let box as DictionaryBox: | ||
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) | ||
} | ||
|
||
} | ||
|
||
internal func flatten() -> [String: Box] { | ||
var node: [String: Box] = attributes.mapValues { StringBox($0) } | ||
|
||
for childElement in children { | ||
for child in childElement.value { | ||
if let content = child.value { | ||
if let oldContent = node[childElement.key] as? ArrayBox { | ||
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] = ArrayBox([oldContent, StringBox(content)]) | ||
} else { | ||
node[childElement.key] = StringBox(content) | ||
} | ||
} else if !child.children.isEmpty || !child.attributes.isEmpty { | ||
let newValue = child.flatten() | ||
|
||
if let existingValue = node[childElement.key] { | ||
if let array = existingValue as? ArrayBox { | ||
array.append(DictionaryBox(newValue)) | ||
// FIXME: Box is a reference type, so this shouldn't be necessary: | ||
node[childElement.key] = array | ||
} else { | ||
node[childElement.key] = ArrayBox([existingValue, DictionaryBox(newValue)]) | ||
} | ||
} else { | ||
node[childElement.key] = DictionaryBox(newValue) | ||
} | ||
} | ||
} | ||
} | ||
|
||
return node | ||
} | ||
|
||
func toXMLString(with header: XMLHeader? = nil, withCDATA cdata: Bool, formatting: XMLEncoder.OutputFormatting, ignoreEscaping _: Bool = false) -> String { | ||
if let header = header, let headerXML = header.toXML() { | ||
return headerXML + _toXMLString(withCDATA: cdata, formatting: formatting) | ||
} | ||
return _toXMLString(withCDATA: cdata, formatting: formatting) | ||
} | ||
|
||
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) | ||
} | ||
|
||
fileprivate func elementString(for childElement: (key: String, value: [_XMLElement]), at level: Int, cdata: Bool, formatting: XMLEncoder.OutputFormatting, prettyPrinted: Bool) -> String { | ||
var string = "" | ||
for child in childElement.value { | ||
string += child._toXMLString(indented: level + 1, withCDATA: cdata, formatting: formatting) | ||
string += prettyPrinted ? "\n" : "" | ||
} | ||
return string | ||
} | ||
|
||
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) | ||
} | ||
|
||
fileprivate func attributeString(key: String, value: String) -> String { | ||
return " \(key)=\"\(value.escape(_XMLElement.escapedCharacterSet))\"" | ||
} | ||
|
||
fileprivate func formatXMLAttributes(from keyValuePairs: [(key: String, value: String)], into string: inout String) { | ||
for (key, value) in keyValuePairs { | ||
string += attributeString(key: key, value: value) | ||
} | ||
} | ||
|
||
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 formatSortedXMLAttributes(_ string: inout String) { | ||
formatXMLAttributes(from: attributes.sorted(by: { $0.key < $1.key }), into: &string) | ||
} | ||
|
||
fileprivate func formatUnsortedXMLAttributes(_ string: inout String) { | ||
formatXMLAttributes(from: attributes.map { (key: $0, value: $1) }, into: &string) | ||
} | ||
|
||
fileprivate func formatXMLAttributes(_ formatting: XMLEncoder.OutputFormatting, _ string: inout String) { | ||
if #available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) { | ||
if formatting.contains(.sortedKeys) { | ||
formatSortedXMLAttributes(&string) | ||
return | ||
} | ||
formatUnsortedXMLAttributes(&string) | ||
return | ||
} | ||
formatUnsortedXMLAttributes(&string) | ||
} | ||
|
||
fileprivate func formatXMLElements(_ formatting: XMLEncoder.OutputFormatting, _ string: inout String, _ level: Int, _ cdata: Bool, _ prettyPrinted: Bool) { | ||
if #available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) { | ||
if formatting.contains(.sortedKeys) { | ||
formatSortedXMLElements(&string, level, cdata, formatting, prettyPrinted) | ||
return | ||
} | ||
formatUnsortedXMLElements(&string, level, cdata, formatting, prettyPrinted) | ||
return | ||
} | ||
formatUnsortedXMLElements(&string, level, cdata, formatting, prettyPrinted) | ||
} | ||
|
||
fileprivate func _toXMLString(indented level: Int = 0, withCDATA cdata: Bool, formatting: XMLEncoder.OutputFormatting, ignoreEscaping: Bool = false) -> String { | ||
let prettyPrinted = formatting.contains(.prettyPrinted) | ||
let indentation = String(repeating: " ", count: (prettyPrinted ? level : 0) * 4) | ||
var string = indentation | ||
string += "<\(key)" | ||
|
||
formatXMLAttributes(formatting, &string) | ||
|
||
if let value = value { | ||
string += ">" | ||
if !ignoreEscaping { | ||
string += (cdata == true ? "<![CDATA[\(value)]]>" : "\(value.escape(_XMLElement.escapedCharacterSet))") | ||
} else { | ||
string += "\(value)" | ||
} | ||
string += "</\(key)>" | ||
} else if !children.isEmpty { | ||
string += prettyPrinted ? ">\n" : ">" | ||
formatXMLElements(formatting, &string, level, cdata, prettyPrinted) | ||
|
||
string += indentation | ||
string += "</\(key)>" | ||
} else { | ||
string += " />" | ||
} | ||
|
||
return string | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// | ||
// XMLHeader.swift | ||
// XMLCoder | ||
// | ||
// Created by Vincent Esche on 12/18/18. | ||
// | ||
|
||
import Foundation | ||
|
||
public struct XMLHeader { | ||
/// the XML standard that the produced document conforms to. | ||
public let version: Double? | ||
/// the encoding standard used to represent the characters in the produced document. | ||
public let encoding: String? | ||
/// indicates whether a document relies on information from an external source. | ||
public let standalone: String? | ||
|
||
public init(version: Double? = nil, encoding: String? = nil, standalone: String? = nil) { | ||
self.version = version | ||
self.encoding = encoding | ||
self.standalone = standalone | ||
} | ||
|
||
internal func isEmpty() -> Bool { | ||
return version == nil && encoding == nil && standalone == nil | ||
} | ||
|
||
internal func toXML() -> String? { | ||
guard !isEmpty() else { return nil } | ||
|
||
var string = "<?xml " | ||
|
||
if let version = version { | ||
string += "version=\"\(version)\" " | ||
} | ||
|
||
if let encoding = encoding { | ||
string += "encoding=\"\(encoding)\" " | ||
} | ||
|
||
if let standalone = standalone { | ||
string += "standalone=\"\(standalone)\"" | ||
} | ||
|
||
return string.trimmingCharacters(in: .whitespaces) + "?>\n" | ||
} | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// | ||
// XMLStackParser.swift | ||
// XMLCoder | ||
// | ||
// Created by Shawn Moore on 11/14/17. | ||
// Copyright © 2017 Shawn Moore. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
internal class _XMLStackParser: NSObject, XMLParserDelegate { | ||
var root: _XMLElement? | ||
var stack = [_XMLElement]() | ||
var currentNode: _XMLElement? | ||
|
||
var currentElementName: String? | ||
var currentElementData = "" | ||
|
||
static func parse(with data: Data) throws -> [String: Box] { | ||
let parser = _XMLStackParser() | ||
|
||
do { | ||
if let node = try parser.parse(with: data) { | ||
return node.flatten() | ||
} else { | ||
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data could not be parsed into XML.")) | ||
} | ||
} catch { | ||
throw error | ||
} | ||
} | ||
|
||
func parse(with data: Data) throws -> _XMLElement? { | ||
let xmlParser = XMLParser(data: data) | ||
xmlParser.delegate = self | ||
|
||
if xmlParser.parse() { | ||
return root | ||
} else if let error = xmlParser.parserError { | ||
throw error | ||
} else { | ||
return nil | ||
} | ||
} | ||
|
||
func parserDidStartDocument(_: XMLParser) { | ||
root = nil | ||
stack = [_XMLElement]() | ||
} | ||
|
||
func parser(_: XMLParser, didStartElement elementName: String, namespaceURI _: String?, qualifiedName _: String?, attributes attributeDict: [String: String] = [:]) { | ||
let node = _XMLElement(key: elementName) | ||
node.attributes = attributeDict | ||
stack.append(node) | ||
|
||
if let currentNode = currentNode { | ||
if currentNode.children[elementName] != nil { | ||
currentNode.children[elementName]?.append(node) | ||
} else { | ||
currentNode.children[elementName] = [node] | ||
} | ||
} | ||
currentNode = node | ||
} | ||
|
||
func parser(_: XMLParser, didEndElement _: String, namespaceURI _: String?, qualifiedName _: String?) { | ||
if let poppedNode = stack.popLast() { | ||
if let content = poppedNode.value?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) { | ||
if content.isEmpty { | ||
poppedNode.value = nil | ||
} else { | ||
poppedNode.value = content | ||
} | ||
} | ||
|
||
if stack.isEmpty { | ||
root = poppedNode | ||
currentNode = nil | ||
} else { | ||
currentNode = stack.last | ||
} | ||
} | ||
} | ||
|
||
func parser(_: XMLParser, foundCharacters string: String) { | ||
currentNode?.value = (currentNode?.value ?? "") + string | ||
} | ||
|
||
func parser(_: XMLParser, foundCDATA CDATABlock: Data) { | ||
if let string = String(data: CDATABlock, encoding: .utf8) { | ||
currentNode?.value = (currentNode?.value ?? "") + string | ||
} | ||
} | ||
|
||
func parser(_: XMLParser, parseErrorOccurred parseError: Error) { | ||
print(parseError) | ||
} | ||
} |
Oops, something went wrong.