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

Make value intrinsic smarter #89

Merged
merged 8 commits into from
Apr 8, 2019
91 changes: 64 additions & 27 deletions Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
public func container<Key>(
keyedBy _: Key.Type
) throws -> KeyedDecodingContainer<Key> {
let topContainer = try self.topContainer()

guard !topContainer.isNull else {
throw DecodingError.valueNotFound(KeyedDecodingContainer<Key>.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<Key>.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription:
"""
Cannot get keyed decoding container -- found null box instead.
"""
)
)
case let string as StringBox:
return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
referencing: self,
wrapping: SharedBox(KeyedBox(
elements: KeyedStorage([("value", string)]),
attributes: KeyedStorage()
))
))
}

guard let keyed = topContainer as? SharedBox<KeyedBox> else {
case let keyed as SharedBox<KeyedBox>:
return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
referencing: self,
wrapping: keyed
))
default:
throw DecodingError._typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: topContainer
)
}

let container = XMLKeyedDecodingContainer<Key>(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<UnkeyedBox>) ?? SharedBox(UnkeyedBox([topContainer]))
let unkeyed = (topContainer as? SharedBox<UnkeyedBox>) ??
SharedBox(UnkeyedBox([topContainer]))

return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
}
Expand All @@ -115,18 +142,28 @@ extension XMLDecoderImplementation {
/// Returns the given box unboxed from a container.

private func typedBox<T, B: Box>(_ 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<KeyedBox>:
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 {
Expand Down
124 changes: 124 additions & 0 deletions Tests/XMLCoderTests/AttributedEnumIntrinsicTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//
// AttributedEnumIntrinsicTest.swift
// XMLCoder
//
// Created by Max Desiatov on 29/03/2019.
//

import Foundation
import XCTest
@testable import XMLCoder

let attributedEnumXML = """
<?xml version="1.0" encoding="UTF-8"?>
<foo><number type="string">ABC</number><number type="int">123</number></foo>
""".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
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
// 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),
]
}
Loading