Skip to content

Commit

Permalink
Encode only strings as CDATA (#179)
Browse files Browse the repository at this point in the history
* Encode only strings as CDATA

* Fix parameter renaming error

* Improve line count in XMLEncoder.swift
  • Loading branch information
MaxDesiatov committed May 3, 2020
1 parent a8e6c56 commit 1f0e5de
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 65 deletions.
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)
}
}

0 comments on commit 1f0e5de

Please sign in to comment.