diff --git a/README.md b/README.md
index 75eeccdc..654855e6 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Encoder & Decoder for XML using Swift's `Codable` protocols.
This package is a fork of the original
[ShawnMoore/XMLParsing](https://github.com/ShawnMoore/XMLParsing)
-with more options and tests added.
+with more features and improved test coverage.
## Example
@@ -39,9 +39,131 @@ let note = try? XMLDecoder().decode(Note.self, from: data)
let returnData = try? XMLEncoder().encode(note, withRootKey: "note")
```
+## Advanced features
+
+### Dynamic node coding
+
+XMLCoder provides two helper protocols that allow you to customize whether
+nodes are encoded as attributes or elements: `DynamicNodeEncoding` and
+`DynamicNodeDecoding`.
+
+The declarations of the protocols are very simple:
+
+```swift
+protocol DynamicNodeEncoding: Encodable {
+ static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
+}
+
+protocol DynamicNodeDecoding: Decodable {
+ static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
+}
+```
+
+The values returned by corresponding `static` functions look like this:
+
+```swift
+public enum NodeDecoding {
+ // decodes a value from an attribute
+ case attribute
+
+ // decodes a value from an element
+ case element
+
+ // the default, attempts to decode as an element first,
+ // otherwise reads from an attribute
+ case elementOrAttribute
+}
+
+enum NodeEncoding {
+ // encodes a value in an attribute
+ case attribute
+
+ // the default, encodes a value in an element
+ case element
+
+ // encodes a value in both attribute and element
+ case both
+}
+```
+
+Add conformance to an appropriate protocol for types you'd like to customize.
+Accordingly, this example code:
+
+```swift
+private struct Book: Codable, Equatable, DynamicNodeEncoding {
+ let id: UInt
+ let title: String
+ let categories: [Category]
+
+ private enum CodingKeys: String, CodingKey {
+ case id
+ case title
+ case categories = "category"
+ }
+
+ static func nodeEncoding(forKey key: CodingKey)
+ -> XMLEncoder.NodeEncoding {
+ switch key {
+ case Book.CodingKeys.id: return .both
+ default: return .element
+ }
+ }
+}
+```
+
+works for this XML:
+
+```xml
+
+ 123
+ Cat in the Hat
+ Kids
+ Wildlife
+
+```
+
+### Value coding key intrinsic
+
+Suppose that you need to decode an XML that looks similar to this:
+
+```xml
+
+456
+```
+
+By default you'd be able to decode `foo` as an element, but then it's not
+possible to decode the `id` attribute. `XMLCoder` handles certain `CodingKey`
+values in a special way to allow proper coding for this XML. Just add a coding
+key with `stringValue` that equals `"value"` or `""` (empty string). What
+follows is an example type declaration that encodes the XML above, but special
+handling of coding keys with those values works for both encoding and decoding.
+
+```swift
+struct Foo: Codable, DynamicNodeEncoding {
+ let id: String
+ let value: String
+
+ enum CodingKeys: String, CodingKey {
+ case id
+ case value
+ // case value = "" would also work
+ }
+
+ static func nodeEncoding(forKey key: CodingKey)
+ -> XMLEncoder.NodeEncoding {
+ switch key {
+ case CodingKeys.id:
+ return .attribute
+ default:
+ return .element
+ }
+ }
+}
+```
+
## Installation
-## Requirements
+### Requirements
- Xcode 10
- Swift 4.2
diff --git a/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift
new file mode 100644
index 00000000..748cdfeb
--- /dev/null
+++ b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift
@@ -0,0 +1,10 @@
+//
+// DynamicNodeDecoding.swift
+// XMLCoder
+//
+// Created by Max Desiatov on 01/03/2019.
+//
+
+public protocol DynamicNodeDecoding: Decodable {
+ static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
+}
diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift
index 762c42be..7da6f2ca 100644
--- a/Sources/XMLCoder/Decoder/XMLDecoder.swift
+++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift
@@ -232,7 +232,7 @@ open class XMLDecoder {
/// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys
- /// A node's decoding tyoe
+ /// A node's decoding type
public enum NodeDecoding {
case attribute
case element
@@ -256,7 +256,10 @@ open class XMLDecoder {
) -> ((CodingKey) -> NodeDecoding) {
switch self {
case .deferredToDecoder:
- return { _ in .elementOrAttribute }
+ guard let dynamicType = codableType as? DynamicNodeDecoding.Type else {
+ return { _ in .elementOrAttribute }
+ }
+ return dynamicType.nodeDecoding(for:)
case let .custom(closure):
return closure(codableType, decoder)
}
diff --git a/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift b/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift
index 8ba34b5c..a1219b18 100644
--- a/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift
+++ b/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift
@@ -5,8 +5,6 @@
// Created by Joseph Mattiello on 1/24/19.
//
-import Foundation
-
public protocol DynamicNodeEncoding: Encodable {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
}
diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift
index 6f1e5e39..ff386289 100644
--- a/Sources/XMLCoder/Encoder/XMLEncoder.swift
+++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift
@@ -33,7 +33,7 @@ open class XMLEncoder {
public static let sortedKeys = OutputFormatting(rawValue: 1 << 1)
}
- /// A node's encoding tyoe
+ /// A node's encoding type
public enum NodeEncoding {
case attribute
case element
@@ -216,7 +216,7 @@ open class XMLEncoder {
@available(*, deprecated, renamed: "NodeEncodingStrategy")
public typealias NodeEncodingStrategies = NodeEncodingStrategy
- public typealias XMLNodeEncoderClosure = ((CodingKey) -> XMLEncoder.NodeEncoding)
+ public typealias XMLNodeEncoderClosure = ((CodingKey) -> NodeEncoding)
public typealias XMLEncodingClosure = (Encodable.Type, Encoder) -> XMLNodeEncoderClosure
/// Set of strategies to use for encoding of nodes.
@@ -227,8 +227,10 @@ open class XMLEncoder {
/// Return a closure computing the desired node encoding for the value by its coding key.
case custom(XMLEncodingClosure)
- func nodeEncodings(forType codableType: Encodable.Type,
- with encoder: Encoder) -> ((CodingKey) -> XMLEncoder.NodeEncoding) {
+ func nodeEncodings(
+ forType codableType: Encodable.Type,
+ with encoder: Encoder
+ ) -> ((CodingKey) -> NodeEncoding) {
return encoderClosure(codableType, encoder)
}
diff --git a/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift b/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift
index a10a4fe2..c9deedfd 100644
--- a/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift
+++ b/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift
@@ -12,7 +12,13 @@ extension XMLEncoderImplementation: SingleValueEncodingContainer {
// MARK: - SingleValueEncodingContainer Methods
func assertCanEncodeNewValue() {
- precondition(canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.")
+ precondition(
+ canEncodeNewValue,
+ """
+ Attempt to encode value through single value container when \
+ previously value already encoded.
+ """
+ )
}
public func encodeNil() throws {
diff --git a/Tests/XMLCoderTests/DynamicNodeDecodingTest.swift b/Tests/XMLCoderTests/DynamicNodeDecodingTest.swift
new file mode 100644
index 00000000..41b3cf49
--- /dev/null
+++ b/Tests/XMLCoderTests/DynamicNodeDecodingTest.swift
@@ -0,0 +1,214 @@
+//
+// DynamicNodeEncodingTest.swift
+// XMLCoderTests
+//
+// Created by Joseph Mattiello on 1/23/19.
+//
+
+import Foundation
+import XCTest
+@testable import XMLCoder
+
+private let overlappingKeys = """
+
+
+
+ StringValue
+
+
+""".data(using: .utf8)!
+
+private let libraryXMLYN = """
+
+
+
+ 123
+ Cat in the Hat
+ Kids
+ Wildlife
+
+
+ 456
+ 1984
+ Classics
+ News
+
+
+""".data(using: .utf8)!
+
+private let libraryXMLYNStrategy = """
+
+
+ 2
+
+ 123
+
+ true
+ Kids
+
+
+ false
+ Wildlife
+
+
+
+ 456
+
+ true
+ Classics
+
+
+ false
+ News
+
+
+
+""".data(using: .utf8)!
+
+private struct TestStruct: Codable, Equatable, DynamicNodeDecoding {
+ let attribute: Int
+ let element: String
+
+ private enum CodingKeys: CodingKey {
+ case attribute
+ case element
+
+ public var stringValue: String {
+ return "key"
+ }
+ }
+
+ static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
+ switch key {
+ case CodingKeys.attribute:
+ return .attribute
+ default:
+ return .element
+ }
+ }
+}
+
+private struct Library: Codable, Equatable, DynamicNodeDecoding {
+ let count: Int
+ let books: [Book]
+
+ enum CodingKeys: String, CodingKey {
+ case count
+ case books = "book"
+ }
+
+ static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
+ switch key {
+ case CodingKeys.count:
+ return .attribute
+ default:
+ return .element
+ }
+ }
+}
+
+private struct Book: Codable, Equatable, DynamicNodeEncoding {
+ let id: UInt
+ let title: String
+ let categories: [Category]
+
+ enum CodingKeys: String, CodingKey {
+ case id
+ case title
+ case categories = "category"
+ }
+
+ static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
+ switch key {
+ case Book.CodingKeys.id: return .both
+ default: return .element
+ }
+ }
+}
+
+private struct Category: Codable, Equatable, DynamicNodeEncoding {
+ let main: Bool
+ let value: String
+
+ private enum CodingKeys: String, CodingKey {
+ case main
+ case value
+ }
+
+ static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
+ switch key {
+ case Category.CodingKeys.main:
+ return .attribute
+ default:
+ return .element
+ }
+ }
+}
+
+final class DynamicNodeDecodingTest: XCTestCase {
+ func testDecode() throws {
+ let decoder = XMLDecoder()
+ decoder.errorContextLength = 10
+
+ let library = try decoder.decode(Library.self, from: libraryXMLYN)
+ XCTAssertEqual(library.books.count, 2)
+ XCTAssertEqual(library.count, 2)
+
+ let book1 = library.books[0]
+ XCTAssertEqual(book1.id, 123)
+ XCTAssertEqual(book1.title, "Cat in the Hat")
+
+ let book1Categories = book1.categories
+ XCTAssertEqual(book1Categories.count, 2)
+ XCTAssertEqual(book1Categories[0].value, "Kids")
+ XCTAssertTrue(book1Categories[0].main)
+ XCTAssertEqual(book1Categories[1].value, "Wildlife")
+ XCTAssertFalse(book1Categories[1].main)
+
+ let book2 = library.books[1]
+ XCTAssertEqual(book2.id, 456)
+ XCTAssertEqual(book2.title, "1984")
+
+ let book2Categories = book2.categories
+ XCTAssertEqual(book2Categories.count, 2)
+ XCTAssertEqual(book2Categories[0].value, "Classics")
+ XCTAssertTrue(book2Categories[0].main)
+ XCTAssertEqual(book2Categories[1].value, "News")
+ XCTAssertFalse(book2Categories[1].main)
+ }
+
+ func testStrategyPriority() throws {
+ let decoder = XMLDecoder()
+ decoder.errorContextLength = 10
+
+ decoder.nodeDecodingStrategy = .custom { type, _ in
+ { key in
+ guard
+ type == Book.self &&
+ key.stringValue == Book.CodingKeys.title.stringValue
+ else {
+ return .element
+ }
+
+ return .attribute
+ }
+ }
+
+ let library = try decoder.decode(Library.self, from: libraryXMLYNStrategy)
+ XCTAssertEqual(library.count, 2)
+ }
+
+ func testOverlappingKeys() throws {
+ let decoder = XMLDecoder()
+ decoder.errorContextLength = 10
+
+ let test = try decoder.decode(TestStruct.self, from: overlappingKeys)
+ XCTAssertEqual(test, TestStruct(attribute: 123, element: "StringValue"))
+ }
+
+ static var allTests = [
+ ("testDecode", testDecode),
+ ("testStrategyPriority", testStrategyPriority),
+ ("testOverlappingKeys", testOverlappingKeys),
+ ]
+}
diff --git a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift
index d28d9a29..586e616a 100644
--- a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift
+++ b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift
@@ -19,7 +19,7 @@ private let libraryXMLYN = """
Wildlife
- 789
+ 456
1984
Classics
News
@@ -27,6 +27,35 @@ private let libraryXMLYN = """
""".data(using: .utf8)!
+private let libraryXMLYNStrategy = """
+
+
+ 2
+
+ 123
+
+ true
+ Kids
+
+
+ false
+ Wildlife
+
+
+
+ 456
+
+ true
+ Classics
+
+
+ false
+ News
+
+
+
+"""
+
private let libraryXMLTrueFalse = """
@@ -69,7 +98,7 @@ private struct Book: Codable, Equatable, DynamicNodeEncoding {
let title: String
let categories: [Category]
- private enum CodingKeys: String, CodingKey {
+ enum CodingKeys: String, CodingKey {
case id
case title
case categories = "category"
@@ -152,7 +181,7 @@ final class DynamicNodeEncodingTest: XCTestCase {
XCTAssertFalse(book1Categories[1].main)
let book2 = library.books[1]
- // XCTAssertEqual(book2.id, 456)
+ XCTAssertEqual(book2.id, 456)
XCTAssertEqual(book2.title, "1984")
let book2Categories = book2.categories
@@ -186,7 +215,7 @@ final class DynamicNodeEncodingTest: XCTestCase {
XCTAssertFalse(book1Categories[1].main)
let book2 = library.books[1]
- // XCTAssertEqual(book2.id, 456)
+ XCTAssertEqual(book2.id, 456)
XCTAssertEqual(book2.title, "1984")
let book2Categories = book2.categories
@@ -203,6 +232,32 @@ final class DynamicNodeEncodingTest: XCTestCase {
XCTAssertEqual(library, library2)
}
+ func testStrategyPriority() throws {
+ let decoder = XMLDecoder()
+ decoder.errorContextLength = 10
+
+ let encoder = XMLEncoder()
+ encoder.outputFormatting = [.prettyPrinted]
+ encoder.nodeEncodingStrategy = .custom { type, _ in
+ { key in
+ guard
+ type == [Book].self &&
+ key.stringValue == Book.CodingKeys.title.stringValue
+ else {
+ return .element
+ }
+
+ return .attribute
+ }
+ }
+
+ let library = try decoder.decode(Library.self, from: libraryXMLYN)
+ let data = try encoder.encode(library, withRootKey: "library",
+ header: XMLHeader(version: 1.0,
+ encoding: "UTF-8"))
+ XCTAssertEqual(String(data: data, encoding: .utf8)!, libraryXMLYNStrategy)
+ }
+
static var allTests = [
("testEncode", testEncode),
("testDecode", testDecode),
diff --git a/Tests/XMLCoderTests/SingleChildTests.swift b/Tests/XMLCoderTests/SingleChildTests.swift
index 2848b0dc..8a9dab59 100644
--- a/Tests/XMLCoderTests/SingleChildTests.swift
+++ b/Tests/XMLCoderTests/SingleChildTests.swift
@@ -12,7 +12,7 @@ struct ProudParent: Codable, Equatable {
var myChildAge: [Int]
}
-final class Test: XCTestCase {
+final class SingleChildTest: XCTestCase {
func testEncoder() throws {
let encoder = XMLEncoder()
diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj
index df9822e2..ac061ed8 100644
--- a/XMLCoder.xcodeproj/project.pbxproj
+++ b/XMLCoder.xcodeproj/project.pbxproj
@@ -85,7 +85,9 @@
BF9457F621CBB6BC005ACFDE /* KeyedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9457EB21CBB6BC005ACFDE /* KeyedTests.swift */; };
BF9457F721CBB6BC005ACFDE /* DataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9457EC21CBB6BC005ACFDE /* DataTests.swift */; };
D14D8A8621F1D6B300B0D31A /* SingleChildTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */; };
+ D158F12F2229892C0032B449 /* DynamicNodeDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D158F12E2229892C0032B449 /* DynamicNodeDecoding.swift */; };
D162674321F9B2AF0056D1D8 /* OptionalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D162674121F9B2850056D1D8 /* OptionalTests.swift */; };
+ D1761D1F2247F04500F53CEF /* DynamicNodeDecodingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1761D1E2247F04500F53CEF /* DynamicNodeDecodingTest.swift */; };
D1CB1EF521EA9599009CAF02 /* RJITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_37 /* RJITest.swift */; };
D1CFC8242226B13F00B03222 /* NamespaceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CFC8222226AFB400B03222 /* NamespaceTest.swift */; };
D1E0C85321D8E65E0042A261 /* ErrorContextTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E0C85121D8E6540042A261 /* ErrorContextTest.swift */; };
@@ -200,7 +202,9 @@
D11979B621F5CD2500A9C574 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
D11979B721F5EA5400A9C574 /* XMLCoder.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = XMLCoder.podspec; sourceTree = ""; };
D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleChildTests.swift; sourceTree = ""; };
+ D158F12E2229892C0032B449 /* DynamicNodeDecoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNodeDecoding.swift; sourceTree = ""; };
D162674121F9B2850056D1D8 /* OptionalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTests.swift; sourceTree = ""; };
+ D1761D1E2247F04500F53CEF /* DynamicNodeDecodingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNodeDecodingTest.swift; sourceTree = ""; };
D1CFC8222226AFB400B03222 /* NamespaceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamespaceTest.swift; sourceTree = ""; };
D1E0C85121D8E6540042A261 /* ErrorContextTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorContextTest.swift; sourceTree = ""; };
D1E0C85421D91EBF0042A261 /* Metatypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metatypes.swift; sourceTree = ""; };
@@ -387,6 +391,7 @@
D1FC040421C7EF8200065B43 /* RJISample.swift */,
B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */,
B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */,
+ D1761D1E2247F04500F53CEF /* DynamicNodeDecodingTest.swift */,
A61DCCD621DF8DB300C0A19D /* ClassTests.swift */,
D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */,
D1CFC8222226AFB400B03222 /* NamespaceTest.swift */,
@@ -448,6 +453,7 @@
OBJ_12 /* XMLDecodingStorage.swift */,
OBJ_13 /* XMLKeyedDecodingContainer.swift */,
OBJ_14 /* XMLUnkeyedDecodingContainer.swift */,
+ D158F12E2229892C0032B449 /* DynamicNodeDecoding.swift */,
);
path = Decoder;
sourceTree = "";
@@ -594,6 +600,7 @@
BF9457DA21CBB5D2005ACFDE /* DataBox.swift in Sources */,
BF9457AB21CBB498005ACFDE /* DecimalBox.swift in Sources */,
OBJ_56 /* XMLKeyedEncodingContainer.swift in Sources */,
+ D158F12F2229892C0032B449 /* DynamicNodeDecoding.swift in Sources */,
D1E0C85521D91EBF0042A261 /* Metatypes.swift in Sources */,
OBJ_57 /* XMLReferencingEncoder.swift in Sources */,
BF9457BC21CBB4DB005ACFDE /* XMLCoderElement.swift in Sources */,
@@ -634,6 +641,7 @@
D1CFC8242226B13F00B03222 /* NamespaceTest.swift in Sources */,
BF9457D021CBB516005ACFDE /* UIntBoxTests.swift in Sources */,
OBJ_80 /* BooksTest.swift in Sources */,
+ D1761D1F2247F04500F53CEF /* DynamicNodeDecodingTest.swift in Sources */,
OBJ_81 /* BreakfastTest.swift in Sources */,
OBJ_82 /* CDCatalog.swift in Sources */,
BF63EF0021CCDED2001D38C5 /* XMLStackParserTests.swift in Sources */,