XMLCoder icon indicating copy to clipboard operation
XMLCoder copied to clipboard

Nested enums coding doesn't work

Open plushcube opened this issue 4 years ago • 2 comments

Seems like I found a bug with enums decoding.

Consider the following set of structures describing a book:

struct Book: Codable {
    let title: String
    let chapters: [Chapter]
}

enum Chapter {
    struct Content {
        let title: String
        let content: String
    }

    case intro(Content)
    case body(Content)
    case outro(Content)
}

and the example xml:

let example = """
<?xml version="1.0" encoding="UTF-8"?>
<book title="Example">
    <chapters>
        <intro title="Intro">Content of first chapter</intro>
        <chapter title="Chapter 1">Content of chapter 1</chapter>
        <chapter title="Chapter 2">Content of chapter 2</chapter>
        <outro title="Epilogue">Content of last chapter</outro>
    </chapters>
</book>
"""

let book = try XMLDecoder().decode(Book.self, from: example.data(using: .utf8)!)

I'm getting an object with only one chapter Intro:

Book(title: "Example", 
    chapters: [
        PDF.Chapter.intro(PDF.Chapter.Content(title: "Intro", content: "Content of first chapter"))
    ]
)

But if I modify my example like this:

let example2 = """
<?xml version="1.0" encoding="UTF-8"?>
<chapters>
    <intro title="Intro">Content of first chapter</intro>
    <chapter title="Chapter 1">Content of chapter 1</chapter>
    <chapter title="Chapter 2">Content of chapter 2</chapter>
    <outro title="Epilogue">Content of last chapter</outro>
</chapters>
"""

let book = try XMLDecoder().decode([Chapter].self, from: example2.data(using: .utf8)!)

the result will be a proper array of chapters:

[
PDF.Chapter.intro(PDF.Chapter.Content(title: "Intro", content: "Content of first chapter")), 
PDF.Chapter.body(PDF.Chapter.Content(title: "Chapter 1", content: "Content of chapter 1")), 
PDF.Chapter.body(PDF.Chapter.Content(title: "Chapter 2", content: "Content of chapter 2")), 
PDF.Chapter.outro(PDF.Chapter.Content(title: "Epilogue", content: "Content of last chapter"))
]

Codable extensions implemented like this:

extension Chapter: Codable {
    enum CodingKeys: String, XMLChoiceCodingKey {
        case intro, body = "chapter", outro
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try? container.decode(Content.self, forKey: .body) {
            self = .body(value)
        } else if let value = try? container.decode(Content.self, forKey: .intro) {
            self = .intro(value)
        } else if let value = try? container.decode(Content.self, forKey: .outro) {
            self = .outro(value)
        } else {
            throw BookError.unknownChapterType
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .intro(let value):
            try container.encode(value, forKey: .intro)
        case .body(let value):
            try container.encode(value, forKey: .body)
        case .outro(let value):
            try container.encode(value, forKey: .outro)
        }
    }
}

extension Chapter.Content: Codable, DynamicNodeEncoding {
    enum CodingKeys: String, CodingKey {
        case title
        case value = ""
    }

    static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
        switch key {
        case CodingKeys.value:
            return .element
        default:
            return .attribute
        }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        title = try container.decode(String.self, forKey: .title)
        content = try container.decode(String.self, forKey: .value)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(title, forKey: .title)
        try container.encode(content, forKey: .value)
    }
}

Would appreciate any help or suggestion in resolving this issue.

plushcube avatar Aug 05 '19 18:08 plushcube

If it helps, I have reproduced this.

It appears that the error occurs when the ChoiceBox.init(_: KeyedBox) is called within XMLDecoderImplementation.choiceContainer(keyedBy:).

jsbean avatar Aug 05 '19 20:08 jsbean

Hi @plushcat, I'm sorry for the delay. The solution would be to add an intermediate Chapters with a custom decoder that calls singleValueContainer(). This is done #126, but we also had a similar test in NestedChoiceTests. Does that resolve the issue for you?

MaxDesiatov avatar Oct 04 '19 22:10 MaxDesiatov