Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encode only strings as CDATA #179

Merged
merged 3 commits into from May 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
12 changes: 12 additions & 0 deletions .vscode/tasks.json
@@ -0,0 +1,12 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "swift test",
"type": "shell",
"command": "swift test"
}
]
}
80 changes: 42 additions & 38 deletions Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
Expand Up @@ -122,26 +122,23 @@ struct XMLCoderElement: Equatable {
}

func toXMLString(with header: XMLHeader? = nil,
withCDATA cdata: Bool,
formatting: XMLEncoder.OutputFormatting) -> String {
if let header = header, let headerXML = header.toXML() {
return headerXML + _toXMLString(withCDATA: cdata, formatting: formatting)
return headerXML + _toXMLString(formatting: formatting)
}
return _toXMLString(withCDATA: cdata, formatting: formatting)
return _toXMLString(formatting: formatting)
}

private func formatUnsortedXMLElements(
_ string: inout String,
_ level: Int,
_ cdata: Bool,
_ formatting: XMLEncoder.OutputFormatting,
_ prettyPrinted: Bool
) {
formatXMLElements(
from: elements,
into: &string,
at: level,
cdata: cdata,
formatting: formatting,
prettyPrinted: prettyPrinted
)
Expand All @@ -150,12 +147,11 @@ struct XMLCoderElement: Equatable {
fileprivate func elementString(
for element: XMLCoderElement,
at level: Int,
cdata: Bool,
formatting: XMLEncoder.OutputFormatting,
prettyPrinted: Bool
) -> String {
if let stringValue = element.stringValue {
if element.isCDATANode || cdata {
if element.isCDATANode {
return "<![CDATA[\(stringValue)]]>"
} else {
return stringValue.escape(XMLCoderElement.escapedCharacterSet)
Expand All @@ -164,7 +160,7 @@ struct XMLCoderElement: Equatable {

var string = ""
string += element._toXMLString(
indented: level + 1, withCDATA: cdata, formatting: formatting
indented: level + 1, formatting: formatting
)
string += prettyPrinted ? "\n" : ""
return string
Expand All @@ -173,14 +169,12 @@ struct XMLCoderElement: Equatable {
fileprivate func formatSortedXMLElements(
_ string: inout String,
_ level: Int,
_ cdata: Bool,
_ formatting: XMLEncoder.OutputFormatting,
_ prettyPrinted: Bool
) {
formatXMLElements(from: elements.sorted { $0.key < $1.key },
into: &string,
at: level,
cdata: cdata,
formatting: formatting,
prettyPrinted: prettyPrinted)
}
Expand All @@ -202,14 +196,12 @@ struct XMLCoderElement: Equatable {
from elements: [XMLCoderElement],
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 && !containsTextNodes)
}
Expand Down Expand Up @@ -240,23 +232,21 @@ struct XMLCoderElement: Equatable {
_ formatting: XMLEncoder.OutputFormatting,
_ string: inout String,
_ level: Int,
_ cdata: Bool,
_ prettyPrinted: Bool
) {
if formatting.contains(.sortedKeys) {
formatSortedXMLElements(
&string, level, cdata, formatting, prettyPrinted
&string, level, formatting, prettyPrinted
)
return
}
formatUnsortedXMLElements(
&string, level, cdata, formatting, prettyPrinted
&string, level, formatting, prettyPrinted
)
}

private func _toXMLString(
indented level: Int = 0,
withCDATA cdata: Bool,
formatting: XMLEncoder.OutputFormatting
) -> String {
let prettyPrinted = formatting.contains(.prettyPrinted)
Expand All @@ -276,7 +266,7 @@ struct XMLCoderElement: Equatable {
if !key.isEmpty {
string += prettyPrintElements ? ">\n" : ">"
}
formatXMLElements(formatting, &string, level, cdata, prettyPrintElements)
formatXMLElements(formatting, &string, level, prettyPrintElements)

if prettyPrintElements { string += indentation }
if !key.isEmpty {
Expand All @@ -293,23 +283,35 @@ struct XMLCoderElement: Equatable {
// MARK: - Convenience Initializers

extension XMLCoderElement {
init(key: String, box: UnkeyedBox, attributes: [Attribute] = []) {
init(key: String, isStringBoxCDATA isCDATA: Bool, box: UnkeyedBox, attributes: [Attribute] = []) {
if let containsChoice = box as? [ChoiceBox] {
self.init(
key: key,
elements: containsChoice.map { XMLCoderElement(key: $0.key, box: $0.element) },
elements: containsChoice.map {
XMLCoderElement(key: $0.key, isStringBoxCDATA: isCDATA, box: $0.element)
},
attributes: attributes
)
} else {
self.init(key: key, elements: box.map { XMLCoderElement(key: key, box: $0) }, attributes: attributes)
self.init(
key: key,
elements: box.map { XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0) },
attributes: attributes
)
}
}

init(key: String, box: ChoiceBox, attributes: [Attribute] = []) {
self.init(key: key, elements: [XMLCoderElement(key: box.key, box: box.element)], attributes: attributes)
init(key: String, isStringBoxCDATA: Bool, box: ChoiceBox, attributes: [Attribute] = []) {
self.init(
key: key,
elements: [
XMLCoderElement(key: box.key, isStringBoxCDATA: isStringBoxCDATA, box: box.element),
],
attributes: attributes
)
}

init(key: String, box: KeyedBox, attributes: [Attribute] = []) {
init(key: String, isStringBoxCDATA isCDATA: Bool, box: KeyedBox, attributes: [Attribute] = []) {
var elements: [XMLCoderElement] = []

for (key, box) in box.elements {
Expand All @@ -321,20 +323,20 @@ extension XMLCoderElement {
case let sharedUnkeyedBox as SharedBox<UnkeyedBox>:
let box = sharedUnkeyedBox.unboxed
elements.append(contentsOf: box.map {
XMLCoderElement(key: key, box: $0)
XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0)
})
case let unkeyedBox as UnkeyedBox:
// This basically injects the unkeyed children directly into self:
elements.append(contentsOf: unkeyedBox.map {
XMLCoderElement(key: key, box: $0)
XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0)
})
case let sharedKeyedBox as SharedBox<KeyedBox>:
let box = sharedKeyedBox.unboxed
elements.append(XMLCoderElement(key: key, box: box))
elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: box))
case let keyedBox as KeyedBox:
elements.append(XMLCoderElement(key: key, box: keyedBox))
elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: keyedBox))
case let simpleBox as SimpleBox:
elements.append(XMLCoderElement(key: key, box: simpleBox))
elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: simpleBox))
default:
fail()
}
Expand All @@ -350,30 +352,32 @@ extension XMLCoderElement {
self.init(key: key, elements: elements, attributes: attributes)
}

init(key: String, box: SimpleBox) {
if let value = box.xmlString {
init(key: String, isStringBoxCDATA: Bool, box: SimpleBox) {
if isStringBoxCDATA, let stringBox = box as? StringBox {
self.init(key: key, cdataValue: stringBox.unboxed)
} else if let value = box.xmlString {
self.init(key: key, stringValue: value)
} else {
self.init(key: key)
}
}

init(key: String, box: Box, attributes: [Attribute] = []) {
init(key: String, isStringBoxCDATA isCDATA: Bool, box: Box, attributes: [Attribute] = []) {
switch box {
case let sharedUnkeyedBox as SharedBox<UnkeyedBox>:
self.init(key: key, box: sharedUnkeyedBox.unboxed, attributes: attributes)
self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedUnkeyedBox.unboxed, attributes: attributes)
case let sharedKeyedBox as SharedBox<KeyedBox>:
self.init(key: key, box: sharedKeyedBox.unboxed, attributes: attributes)
self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedKeyedBox.unboxed, attributes: attributes)
case let sharedChoiceBox as SharedBox<ChoiceBox>:
self.init(key: key, box: sharedChoiceBox.unboxed, attributes: attributes)
self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedChoiceBox.unboxed, attributes: attributes)
case let unkeyedBox as UnkeyedBox:
self.init(key: key, box: unkeyedBox, attributes: attributes)
self.init(key: key, isStringBoxCDATA: isCDATA, box: unkeyedBox, attributes: attributes)
case let keyedBox as KeyedBox:
self.init(key: key, box: keyedBox, attributes: attributes)
self.init(key: key, isStringBoxCDATA: isCDATA, box: keyedBox, attributes: attributes)
case let choiceBox as ChoiceBox:
self.init(key: key, box: choiceBox, attributes: attributes)
self.init(key: key, isStringBoxCDATA: isCDATA, box: choiceBox, attributes: attributes)
case let simpleBox as SimpleBox:
self.init(key: key, box: simpleBox)
self.init(key: key, isStringBoxCDATA: isCDATA, box: simpleBox)
case let box:
preconditionFailure("Unclassified box: \(type(of: box))")
}
Expand Down
35 changes: 23 additions & 12 deletions Sources/XMLCoder/Encoder/XMLEncoder.swift
Expand Up @@ -71,7 +71,7 @@ open class XMLEncoder {
/// Defer to `String` for choosing an encoding. This is the default strategy.
case deferredToString

/// Encoded the `String` as a CData-encoded string.
/// Encode the `String` as a CData-encoded string.
case cdata
}

Expand Down Expand Up @@ -329,10 +329,7 @@ open class XMLEncoder {
withRootKey rootKey: String? = nil,
rootAttributes: [String: String]? = nil,
header: XMLHeader? = nil) throws -> Data {
let encoder = XMLEncoderImplementation(
options: options,
nodeEncodings: []
)
let encoder = XMLEncoderImplementation(options: options, nodeEncodings: [])
encoder.nodeEncodings.append(options.nodeEncodingStrategy.nodeEncodings(forType: T.self, with: encoder))

let topLevel = try encoder.box(value)
Expand All @@ -342,12 +339,29 @@ open class XMLEncoder {

let rootKey = rootKey ?? "\(T.self)".convert(for: keyEncodingStrategy)

let isStringBoxCDATA = stringEncodingStrategy == .cdata

if let keyedBox = topLevel as? KeyedBox {
elementOrNone = XMLCoderElement(key: rootKey, box: keyedBox, attributes: attributes)
elementOrNone = XMLCoderElement(
key: rootKey,
isStringBoxCDATA: isStringBoxCDATA,
box: keyedBox,
attributes: attributes
)
} else if let unkeyedBox = topLevel as? UnkeyedBox {
elementOrNone = XMLCoderElement(key: rootKey, box: unkeyedBox, attributes: attributes)
elementOrNone = XMLCoderElement(
key: rootKey,
isStringBoxCDATA: isStringBoxCDATA,
box: unkeyedBox,
attributes: attributes
)
} else if let choiceBox = topLevel as? ChoiceBox {
elementOrNone = XMLCoderElement(key: rootKey, box: choiceBox, attributes: attributes)
elementOrNone = XMLCoderElement(
key: rootKey,
isStringBoxCDATA: isStringBoxCDATA,
box: choiceBox,
attributes: attributes
)
} else {
fatalError("Unrecognized top-level element of type: \(type(of: topLevel))")
}
Expand All @@ -359,10 +373,7 @@ open class XMLEncoder {
))
}

let withCDATA = stringEncodingStrategy != .deferredToString
return element.toXMLString(with: header,
withCDATA: withCDATA,
formatting: outputFormatting)
return element.toXMLString(with: header, formatting: outputFormatting)
.data(using: .utf8, allowLossyConversion: true)!
}
}
Expand Down
6 changes: 3 additions & 3 deletions Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift
Expand Up @@ -19,7 +19,7 @@ class XMLElementTests: XCTestCase {
}

func testInitUnkeyed() {
let keyed = XMLCoderElement(key: "foo", box: UnkeyedBox())
let keyed = XMLCoderElement(key: "foo", isStringBoxCDATA: false, box: UnkeyedBox())

XCTAssertEqual(keyed.key, "foo")
XCTAssertNil(keyed.stringValue)
Expand All @@ -28,7 +28,7 @@ class XMLElementTests: XCTestCase {
}

func testInitKeyed() {
let keyed = XMLCoderElement(key: "foo", box: KeyedBox(
let keyed = XMLCoderElement(key: "foo", isStringBoxCDATA: false, box: KeyedBox(
elements: [] as [(String, Box)],
attributes: [("baz", NullBox()), ("blee", IntBox(42))] as [(String, SimpleBox)]
))
Expand All @@ -40,7 +40,7 @@ class XMLElementTests: XCTestCase {
}

func testInitSimple() {
let keyed = XMLCoderElement(key: "foo", box: StringBox("bar"))
let keyed = XMLCoderElement(key: "foo", isStringBoxCDATA: false, box: StringBox("bar"))
let element = XMLCoderElement(stringValue: "bar")

XCTAssertEqual(keyed.key, "foo")
Expand Down
48 changes: 36 additions & 12 deletions Tests/XMLCoderTests/CDATATest.swift
Expand Up @@ -5,24 +5,48 @@
import XCTest
import XMLCoder

private struct Container: Codable, Equatable {
let value: Int
let data: String
}
final class CDATATest: XCTestCase {
private struct Container: Codable, Equatable {
let value: Int
let data: String
}

private let xml =
"""
<container>
<value>42</value>
<data><![CDATA[lorem ipsum]]></data>
</container>
""".data(using: .utf8)!
private let xml =
"""
<container>
<value>42</value>
<data><![CDATA[lorem ipsum]]></data>
</container>
""".data(using: .utf8)!

final class CDATATest: XCTestCase {
func testXML() throws {
let decoder = XMLDecoder()
let result = try decoder.decode(Container.self, from: xml)

XCTAssertEqual(result, Container(value: 42, data: "lorem ipsum"))
}

private struct CData: Codable {
let string: String
let int: Int
let bool: Bool
}

private let expectedCData =
"""
<CData>
<string><![CDATA[string]]></string>
<int>123</int>
<bool>true</bool>
</CData>
"""

func testCDataTypes() throws {
let example = CData(string: "string", int: 123, bool: true)
let xmlEncoder = XMLEncoder()
xmlEncoder.stringEncodingStrategy = .cdata
xmlEncoder.outputFormatting = .prettyPrinted
let encoded = try xmlEncoder.encode(example)
XCTAssertEqual(String(data: encoded, encoding: .utf8), expectedCData)
}
}