diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift index 4b504580..4dd5c76b 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -61,45 +61,72 @@ class XMLDecoderImplementation: Decoder { guard let topContainer = storage.popContainer() else { throw DecodingError.valueNotFound(Box.self, DecodingError.Context( codingPath: codingPath, - debugDescription: "Cannot get decoding container -- empty container stack." + debugDescription: + """ + Cannot get decoding container -- empty container stack. + """ )) } return topContainer } - public func container(keyedBy _: Key.Type) throws -> KeyedDecodingContainer { + public func container( + keyedBy _: Key.Type + ) throws -> KeyedDecodingContainer { let topContainer = try self.topContainer() - guard !topContainer.isNull else { - throw DecodingError.valueNotFound(KeyedDecodingContainer.self, DecodingError.Context( - codingPath: codingPath, - debugDescription: "Cannot get keyed decoding container -- found null box instead." + switch topContainer { + case _ where topContainer.isNull: + throw DecodingError.valueNotFound( + KeyedDecodingContainer.self, + DecodingError.Context( + codingPath: codingPath, + debugDescription: + """ + Cannot get keyed decoding container -- found null box instead. + """ + ) + ) + case let string as StringBox: + return KeyedDecodingContainer(XMLKeyedDecodingContainer( + referencing: self, + wrapping: SharedBox(KeyedBox( + elements: KeyedStorage([("value", string)]), + attributes: KeyedStorage() + )) )) - } - - guard let keyed = topContainer as? SharedBox else { + case let keyed as SharedBox: + return KeyedDecodingContainer(XMLKeyedDecodingContainer( + referencing: self, + wrapping: keyed + )) + default: throw DecodingError._typeMismatch( at: codingPath, expectation: [String: Any].self, reality: topContainer ) } - - let container = XMLKeyedDecodingContainer(referencing: self, wrapping: keyed) - return KeyedDecodingContainer(container) } public func unkeyedContainer() throws -> UnkeyedDecodingContainer { let topContainer = try self.topContainer() guard !topContainer.isNull else { - throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, DecodingError.Context( - codingPath: codingPath, - debugDescription: "Cannot get unkeyed decoding container -- found null box instead." - )) + throw DecodingError.valueNotFound( + UnkeyedDecodingContainer.self, + DecodingError.Context( + codingPath: codingPath, + debugDescription: + """ + Cannot get unkeyed decoding container -- found null box instead. + """ + ) + ) } - let unkeyed = (topContainer as? SharedBox) ?? SharedBox(UnkeyedBox([topContainer])) + let unkeyed = (topContainer as? SharedBox) ?? + SharedBox(UnkeyedBox([topContainer])) return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed) } @@ -115,18 +142,28 @@ extension XMLDecoderImplementation { /// Returns the given box unboxed from a container. private func typedBox(_ box: Box, for valueType: T.Type) throws -> B { - guard let typedBox = box as? B else { - if box is NullBox { - throw DecodingError.valueNotFound(valueType, DecodingError.Context( - codingPath: codingPath, - debugDescription: "Expected \(valueType) but found null instead." - )) - } else { - throw DecodingError._typeMismatch(at: codingPath, expectation: valueType, reality: box) + switch box { + case let typedBox as B: + return typedBox + case let keyedBox as SharedBox: + guard + let value = keyedBox.withShared({ $0.elements["value"] as? B }) + else { + fallthrough } + return value + case is NullBox: + throw DecodingError.valueNotFound(valueType, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Expected \(valueType) but found null instead." + )) + default: + throw DecodingError._typeMismatch( + at: codingPath, + expectation: valueType, + reality: box + ) } - - return typedBox } func unbox(_ box: Box) throws -> Bool { diff --git a/Tests/XMLCoderTests/AttributedEnumIntrinsicTest.swift b/Tests/XMLCoderTests/AttributedEnumIntrinsicTest.swift new file mode 100644 index 00000000..fa26dc24 --- /dev/null +++ b/Tests/XMLCoderTests/AttributedEnumIntrinsicTest.swift @@ -0,0 +1,124 @@ +// +// AttributedEnumIntrinsicTest.swift +// XMLCoder +// +// Created by Max Desiatov on 29/03/2019. +// + +import Foundation +import XCTest +@testable import XMLCoder + +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(for 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/AttributedIntrinsicTest.swift b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift index d5c42deb..44020ad6 100644 --- a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift +++ b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift @@ -14,7 +14,33 @@ let fooXML = """ 456 """.data(using: .utf8)! -private struct Foo: Codable, DynamicNodeEncoding { +let fooArrayXML = """ + + +456 +123 + +""".data(using: .utf8)! + +let fooMixedXML = """ + + +456 +123 +789 + +""".data(using: .utf8)! + +let fooValueXML = """ + + +456 +123 +789 + +""".data(using: .utf8)! + +private struct Foo: Codable, DynamicNodeEncoding, Equatable { let id: String let value: String @@ -33,7 +59,30 @@ private struct Foo: Codable, DynamicNodeEncoding { } } -private struct FooEmptyKeyed: Codable, DynamicNodeEncoding { +private struct FooValue: Codable, Equatable { + let value: Int +} + +private struct FooOptional: Codable, DynamicNodeEncoding, Equatable { + let id: String? + let value: Int + + enum CodingKeys: String, CodingKey { + case id + case value + } + + static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.id: + return .attribute + default: + return .element + } + } +} + +private struct FooEmptyKeyed: Codable, DynamicNodeEncoding, Equatable { let id: String let unkeyedValue: Int @@ -52,6 +101,10 @@ private struct FooEmptyKeyed: Codable, DynamicNodeEncoding { } } +private struct Container: Codable, Equatable where T: Codable & Equatable { + let foo: [T] +} + private let previewXML = """ @@ -136,125 +189,67 @@ final class AttributedIntrinsicTest: XCTestCase { ), preview) } - static var allTests = [ - ("testEncode", testEncode), - ("testDecode", testDecode), - ("testDecodePreview", testDecodePreview), - ] -} - -// MARK: - Enums - -let attributedEnumXML = """ - -ABC123 -""".data(using: .utf8)! - -private struct Foo2: Codable { - let number: [FooNumber] -} + func testFooArray() throws { + let decoder = XMLDecoder() -private struct FooNumber: Codable, DynamicNodeEncoding { - public let type: FooEnum + let foo1 = try decoder.decode(Container.self, from: fooArrayXML) + XCTAssertEqual(foo1, Container(foo: [ + Foo(id: "123", value: "456"), + Foo(id: "789", value: "123"), + ])) - public init(type: FooEnum) { - self.type = type + let foo2 = try decoder.decode( + Container.self, + from: fooArrayXML + ) + XCTAssertEqual(foo2, Container(foo: [ + FooEmptyKeyed(id: "123", unkeyedValue: 456), + FooEmptyKeyed(id: "789", unkeyedValue: 123), + ])) } - enum CodingKeys: String, CodingKey { - case type - case typeValue = "" - } + func testIntArray() throws { + let decoder = XMLDecoder() - public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { - switch key { - case FooNumber.CodingKeys.type: return .attribute - default: return .element - } + let foo = try decoder.decode(Container.self, from: fooArrayXML) + XCTAssertEqual(foo, Container(foo: [456, 123])) } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) + func testMixedArray() throws { + let decoder = XMLDecoder() - type = try container.decode(FooEnum.self, forKey: .type) + let foo = try decoder.decode(Container.self, from: fooMixedXML) + XCTAssertEqual(foo, Container(foo: [456, 123, 789])) } - 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 - } + func testFooValueArray() throws { + let decoder = XMLDecoder() - 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")) - } + let foo = try decoder.decode(Container.self, from: fooValueXML) + XCTAssertEqual(foo, Container(foo: [ + FooValue(value: 456), + FooValue(value: 123), + FooValue(value: 789), + ])) } - 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))]) + func testFooOptionalArray() throws { + let decoder = XMLDecoder() - 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) + let foo = try decoder.decode( + Container.self, + from: fooValueXML + ) + XCTAssertEqual(foo, Container(foo: [ + FooOptional(id: nil, value: 456), + FooOptional(id: nil, value: 123), + FooOptional(id: nil, value: 789), + ])) } - // 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), + ("testDecode", testDecode), + ("testDecodePreview", testDecodePreview), ] } diff --git a/Tests/XMLCoderTests/DecodingContainerTests.swift b/Tests/XMLCoderTests/DecodingContainerTests.swift index 009d5992..37412d23 100644 --- a/Tests/XMLCoderTests/DecodingContainerTests.swift +++ b/Tests/XMLCoderTests/DecodingContainerTests.swift @@ -99,9 +99,8 @@ class DecodingContainerTests: XCTestCase { let encoder = XMLEncoder() encoder.outputFormatting = .prettyPrinted let encoded = try encoder.encode(foo, withRootKey: "foo") - let string = String(data: encoded, encoding: .utf8) let decoder = XMLDecoder() - let decoded = try decoder.decode(Foo.self, from: encoded) + _ = try decoder.decode(Foo.self, from: encoded) } } diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 9dacd3bc..6df52149 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ 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 */; }; + D1AC9466224E3E63004AB49B /* AttributedEnumIntrinsicTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1AC9464224E3E1F004AB49B /* AttributedEnumIntrinsicTest.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 */; }; @@ -207,6 +208,7 @@ 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 = ""; }; + D1AC9464224E3E1F004AB49B /* AttributedEnumIntrinsicTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedEnumIntrinsicTest.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 = ""; }; @@ -382,6 +384,7 @@ BF63EEFE21CCDEC1001D38C5 /* Auxiliary */, BF9457BD21CBB516005ACFDE /* Box */, BF9457E121CBB6BC005ACFDE /* Minimal */, + D1AC9464224E3E1F004AB49B /* AttributedEnumIntrinsicTest.swift */, B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */, BF63EF1D21CEC99A001D38C5 /* BenchmarkTests.swift */, OBJ_28 /* BooksTest.swift */, @@ -674,6 +677,7 @@ OBJ_87 /* PlantCatalog.swift in Sources */, BF9457C921CBB516005ACFDE /* KeyedBoxTests.swift in Sources */, OBJ_88 /* PlantTest.swift in Sources */, + D1AC9466224E3E63004AB49B /* AttributedEnumIntrinsicTest.swift in Sources */, BF63EF0821CD7AF8001D38C5 /* URLBoxTests.swift in Sources */, BF9457DD21CBB62C005ACFDE /* DateBoxTests.swift in Sources */, A61DCCD821DF9CA200C0A19D /* ClassTests.swift in Sources */,