diff --git a/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift b/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift index 408c2d9f..9d4d5156 100644 --- a/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift +++ b/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift @@ -106,6 +106,17 @@ extension KeyedBox: Box { } } +extension KeyedBox { + var value: SimpleBox? { + guard + elements.count == 1, + let value = elements["value"] as? SimpleBox + ?? elements[""] as? SimpleBox, + !value.isNull else { return nil } + return value + } +} + extension KeyedBox: CustomStringConvertible { var description: String { return "{attributes: \(attributes), elements: \(elements)}" diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index fc1f5cd6..db0045c1 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -47,7 +47,7 @@ struct XMLCoderElement: Equatable { func flatten() -> KeyedBox { let attributes = self.attributes.mapValues { StringBox($0) } - let keyedElements: [String: Box] = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in + var keyedElements = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in var result = result let key = element.key @@ -93,6 +93,11 @@ struct XMLCoderElement: Equatable { return result } + // Handle attributed unkeyed value zap + // Value should be zap. Detect only when no other elements exist + if keyedElements.isEmpty, let value = value { + keyedElements["value"] = StringBox(value) + } let keyedBox = KeyedBox(elements: keyedElements, attributes: attributes) return keyedBox diff --git a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift index b1f4d55d..5f5cf63d 100644 --- a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift @@ -126,15 +126,18 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { public func decode( _ type: T.Type, forKey key: Key ) throws -> T { - let attributeNotFound = container.withShared { keyedBox in - keyedBox.attributes[key.stringValue] == nil + let attributeFound = container.withShared { keyedBox in + keyedBox.attributes[key.stringValue] != nil } - let elementNotFound = container.withShared { keyedBox in - keyedBox.elements[key.stringValue] == nil + + let elementFound = container.withShared { keyedBox in + keyedBox.elements[key.stringValue] != nil || keyedBox.value != nil } - if let type = type as? AnyEmptySequence.Type, attributeNotFound, - elementNotFound, let result = type.init() as? T { + if let type = type as? AnyEmptySequence.Type, + !attributeFound, + !elementFound, + let result = type.init() as? T { return result } @@ -163,8 +166,12 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { _ type: T.Type, forKey key: Key ) throws -> T { - let elementOrNil = container.withShared { keyedBox in - keyedBox.elements[key.stringValue] + let elementOrNil = container.withShared { keyedBox -> KeyedBox.Element? in + if ["value", ""].contains(key.stringValue) { + return keyedBox.elements[key.stringValue] ?? keyedBox.value + } else { + return keyedBox.elements[key.stringValue] + } } let attributeOrNil = container.withShared { keyedBox in diff --git a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift new file mode 100644 index 00000000..b64ce0fd --- /dev/null +++ b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift @@ -0,0 +1,206 @@ +// +// AttributedIntrinsicTest.swift +// XMLCoderTests +// +// Created by Joseph Mattiello on 1/23/19. +// + +import Foundation +import XCTest +@testable import XMLCoder + +let fooXML = """ + +456 +""".data(using: .utf8)! + +private struct Foo: Codable, DynamicNodeEncoding { + let id: String + let value: String + + enum CodingKeys: String, CodingKey { + case id + case value + } + + static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.id: + return .attribute + default: + return .element + } + } +} + +private struct FooEmptyKeyed: Codable, DynamicNodeEncoding { + let id: String + let unkeyedValue: Int + + enum CodingKeys: String, CodingKey { + case id + case unkeyedValue = "" + } + + static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.id: + return .attribute + default: + return .element + } + } +} + +final class AttributedIntrinsicTest: XCTestCase { + func testEncode() throws { + let encoder = XMLEncoder() + encoder.outputFormatting = [] + + let foo1 = FooEmptyKeyed(id: "123", unkeyedValue: 456) + + let header = XMLHeader(version: 1.0, encoding: "UTF-8") + let encoded = try encoder.encode(foo1, withRootKey: "foo", header: header) + let xmlString = String(data: encoded, encoding: .utf8) + XCTAssertNotNil(xmlString) + + // Test string equivalency + let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines) + let originalXML = String(data: fooXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(encodedXML, originalXML) + } + + func testDecode() throws { + let decoder = XMLDecoder() + decoder.errorContextLength = 10 + + let foo1 = try decoder.decode(Foo.self, from: fooXML) + XCTAssertEqual(foo1.id, "123") + XCTAssertEqual(foo1.value, "456") + + let foo2 = try decoder.decode(FooEmptyKeyed.self, from: fooXML) + XCTAssertEqual(foo2.id, "123") + XCTAssertEqual(foo2.unkeyedValue, 456) + } + + static var allTests = [ + ("testEncode", testEncode), + ("testDecode", testDecode), + ] +} + +// MARK: - Enums + +let attributedEnumXML = """ + +ABC123 +""".data(using: .utf8)! + +private struct Foo2: Codable { + let number: [FooNumber] +} + +private struct FooNumber: Codable, DynamicNodeEncoding { + public let type: FooEnum + + public init(type: FooEnum) { + self.type = type + } + + enum CodingKeys: String, CodingKey { + case type + case typeValue = "" + } + + public static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case FooNumber.CodingKeys.type: return .attribute + default: return .element + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + type = try container.decode(FooEnum.self, forKey: .type) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch type { + case let .string(value): + try container.encode("string", forKey: .type) + try container.encode(value, forKey: .typeValue) + case let .int(value): + try container.encode("int", forKey: .type) + try container.encode(value, forKey: .typeValue) + } + } +} + +private enum FooEnum: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case string + case int + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + if let value = try values.decodeIfPresent(String.self, forKey: .string) { + self = .string(value) + return + } else if let value = try values.decodeIfPresent(Int.self, forKey: .int) { + self = .int(value) + return + } else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, + debugDescription: "No coded value for string or int")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .string(value): + try container.encode(value, forKey: .string) + case let .int(value): + try container.encode(value, forKey: .int) + } + } + + case string(String) + case int(Int) +} + +final class AttributedEnumIntrinsicTest: XCTestCase { + func testEncode() throws { + let encoder = XMLEncoder() + encoder.outputFormatting = [] + + let foo1 = Foo2(number: [FooNumber(type: FooEnum.string("ABC")), FooNumber(type: FooEnum.int(123))]) + + let header = XMLHeader(version: 1.0, encoding: "UTF-8") + let encoded = try encoder.encode(foo1, withRootKey: "foo", header: header) + let xmlString = String(data: encoded, encoding: .utf8) + XCTAssertNotNil(xmlString) + // Test string equivalency + let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines) + let originalXML = String(data: attributedEnumXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(encodedXML, originalXML) + } + + // TODO: Fix decoding +// func testDecode() throws { +// let decoder = XMLDecoder() +// decoder.errorContextLength = 10 +// +// let foo = try decoder.decode(Foo2.self, from: attributedEnumXML) +// XCTAssertEqual(foo.number[0].type, FooEnum.string("ABC")) +// XCTAssertEqual(foo.number[1].type, FooEnum.int(123)) +// } + + static var allTests = [ + ("testEncode", testEncode), +// ("testDecode", testDecode), + ] +} diff --git a/Tests/XMLCoderTests/BooksTest.swift b/Tests/XMLCoderTests/BooksTest.swift index de1b5785..f596a086 100644 --- a/Tests/XMLCoderTests/BooksTest.swift +++ b/Tests/XMLCoderTests/BooksTest.swift @@ -218,7 +218,7 @@ final class BooksTest: XCTestCase { XCTAssertEqual(book1, book2) - // Test string equivlancy + // Test string equivalency let encodedXML = String(data: data, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) let originalXML = String(data: bookXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) XCTAssertEqual(encodedXML, originalXML) diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index d9fb784a..33627d76 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ A61FE03C21E4EAB10015D993 /* KeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */; }; B34B3C08220381AC00BCBA30 /* String+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34B3C07220381AB00BCBA30 /* String+ExtensionsTests.swift */; }; B35157CE21F986DD009CA0CC /* DynamicNodeEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */; }; + B3B6902E220A71DF0084D407 /* AttributedIntrinsicTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */; }; B3BE1D612202C1F600259831 /* DynamicNodeEncodingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */; }; B3BE1D632202CB1400259831 /* XMLEncoderImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */; }; B3BE1D652202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D642202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift */; }; @@ -135,6 +136,7 @@ A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedIntTests.swift; sourceTree = ""; }; B34B3C07220381AB00BCBA30 /* String+ExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+ExtensionsTests.swift"; sourceTree = ""; }; B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncoding.swift; sourceTree = ""; }; + B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedIntrinsicTest.swift; sourceTree = ""; }; B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncodingTest.swift; sourceTree = ""; }; B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLEncoderImplementation.swift; sourceTree = ""; }; B3BE1D642202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XMLEncoderImplementation+SingleValueEncodingContainer.swift"; sourceTree = ""; }; @@ -378,6 +380,7 @@ OBJ_38 /* RelationshipsTest.swift */, BF63EF1D21CEC99A001D38C5 /* BenchmarkTests.swift */, D1FC040421C7EF8200065B43 /* RJISample.swift */, + B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */, B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */, A61DCCD621DF8DB300C0A19D /* ClassTests.swift */, D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */, @@ -618,6 +621,7 @@ A61FE03921E4D60B0015D993 /* UnkeyedIntTests.swift in Sources */, BF63EF6B21D10284001D38C5 /* XMLElementTests.swift in Sources */, BF9457ED21CBB6BC005ACFDE /* BoolTests.swift in Sources */, + B3B6902E220A71DF0084D407 /* AttributedIntrinsicTest.swift in Sources */, D1FC040521C7EF8200065B43 /* RJISample.swift in Sources */, BF63EF0A21CD7C1A001D38C5 /* URLTests.swift in Sources */, BF9457CE21CBB516005ACFDE /* StringBoxTests.swift in Sources */,