Skip to content

Commit

Permalink
Fix Decoding of Arrays of Empty Elements (CoreOffice#152)
Browse files Browse the repository at this point in the history
## Overview

Fixes CoreOffice#123 and extends CoreOffice#145 to accommodate decoding of arrays of empty strings or mixed arrays of non-empty and empty strings.

## Example

We may now decode 
```xml
<container>
    <empty/>
    <empty/>
    <empty/>
</container>
```
into the following type
```swift
struct EmptyArray: Equatable, Codable {
    enum CodingKeys: String, CodingKey { case empties = "empty" }
    let empties: [Empty]
}
```
where 
```swift
struct Empty: Equatable, Codable { }
```
We can also decode a value of the following type
```swift
struct EmptyWrapper {
    let empty: Empty
}
```
from 
```xml
<wrapper>
    <empty/>
</wrapper>
```
Further, following from CoreOffice#145 we can decode arrays of empty strings
```xml
<container>
    <string-type/>
    <string-type>My neighbors are empty<string-type>
    <string-type/>
</container>
```
as
```swift
struct EmptyArray: Equatable, Codable {
    enum CodingKeys: String, CodingKey { case stringType = "string-type" }
    let stringType: [String]
}
```
and variants.
## Source Compatibility

In cases where we decode an array of type `[String?]`, an empty element is now decoded as `""` rather than `nil`, the justification being that `String` can itself take empty values. We use the convention that `nil` signifies the absence of an element (if 0 or 1 of the element are allowed) as in the updated [BreakfastTest](https://github.com/bwetherfield/XMLCoder/blob/0d20614e47df98d1a10174e992c585edf629c9b9/Tests/XMLCoderTests/BreakfastTest.swift) and in the following error-throwing [test case](https://github.com/MaxDesiatov/XMLCoder/blob/2855777ff868e8a4c1d944c7da0ddb49a8b3893e/Tests/XMLCoderTests/Minimal/NullTests.swift#L56-L68).

* Add nested choice unkeyed container decoding test
* Fix nested unkeyed container implementation for nested keyed box
* Clean up unwrapping syntax
* Treat corner case of empty string value intrinsic
* Remove xcodeproj junk
* Add some logging to assess where we're at
* Add cases for empty string as null element decoding
* Swiftformat
* Transform precondition to where clause in switch statement
* Remove print statements
* Add failing test for a nested array of empty-string value intrinsic elements
* Do a little cleanup of single keyed box passing around
* Refactor XMLKeyedDecodingContainer.decodeConcrete elements massaging
* Remove xcscheme junk
* Add fix for empty arrays, wrapped empties etc
* Clean up
* Revert singleKeyed dive change
* Accommodate singleKeyed reading options
* Alter Border Test
* Add test case that returns [nil] (exists a non-optional property)
* Eliminate possibly empty Int from Breakfast test
* Fix formatting
* Fix forcecast
* Fix formatting
* Update LinuxMain
* Fix tests such that null elements read as empty strings
* Fix linux main
* Add nested array of empty strings decoding in the explicit style
* Add mixed case empty and non-empty string cases
* Reinstate missing test
* Add test for decoding a null element into an optional type
  • Loading branch information
bwetherfield authored and Arjun Gupta committed Jun 26, 2020
1 parent f5220a1 commit 8d28b86
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 28 deletions.
12 changes: 9 additions & 3 deletions Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift
Expand Up @@ -319,10 +319,16 @@ extension XMLDecoderImplementation {
}

func unbox(_ box: Box) throws -> String {
let stringBox: StringBox = try typedBox(box, for: String.self)
let string = stringBox.unboxed
do {
let stringBox: StringBox = try typedBox(box, for: String.self)
return stringBox.unboxed
} catch {
if box is NullBox {
return ""
}
}

return string
return ""
}

func unbox(_ box: Box) throws -> Date {
Expand Down
23 changes: 14 additions & 9 deletions Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift
Expand Up @@ -79,8 +79,8 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {

let box = elements.first ?? attributes.first

if let singleKeyed = box as? SingleKeyedBox {
return singleKeyed.element.isNull
if box is SingleKeyedBox {
return false
}

return box?.isNull ?? true
Expand Down Expand Up @@ -160,14 +160,19 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
decoder.codingPath.append(key)
defer { decoder.codingPath.removeLast() }

let elements = container.withShared { keyedBox in
keyedBox.elements[key.stringValue]
}
let elements = container.unboxed.elements[key.stringValue]

return XMLUnkeyedDecodingContainer(
referencing: decoder,
wrapping: SharedBox(elements)
)
if let containsKeyed = elements as? [KeyedBox], let keyed = containsKeyed.first {
return XMLUnkeyedDecodingContainer(
referencing: decoder,
wrapping: SharedBox(keyed.elements.map(SingleKeyedBox.init))
)
} else {
return XMLUnkeyedDecodingContainer(
referencing: decoder,
wrapping: SharedBox(elements)
)
}
}

public func superDecoder() throws -> Decoder {
Expand Down
12 changes: 8 additions & 4 deletions Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift
Expand Up @@ -105,11 +105,15 @@ struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer {
var value: T?
if let singleKeyed = box as? SingleKeyedBox {
do {
// Drill down to the element in the case of an nested unkeyed element
value = try decode(decoder, singleKeyed.element)
value = try decode(decoder, singleKeyed)
} catch {
// Specialize for choice elements
value = try decode(decoder, ChoiceBox(key: singleKeyed.key, element: singleKeyed.element))
do {
// Drill down to the element in the case of an nested unkeyed element
value = try decode(decoder, singleKeyed.element)
} catch {
// Specialize for choice elements
value = try decode(decoder, ChoiceBox(key: singleKeyed.key, element: singleKeyed.element))
}
}
} else {
value = try decode(decoder, box)
Expand Down
30 changes: 30 additions & 0 deletions Tests/XMLCoderTests/BorderTest.swift
Expand Up @@ -24,6 +24,16 @@ struct Borders: Codable, Equatable {
}
}

struct LeftBorders: Codable, Equatable {
let items: [LeftBorder?]
let count: Int

enum CodingKeys: String, CodingKey {
case items = "border"
case count
}
}

struct Border: Codable, Equatable {
struct Value: Codable, Equatable {
let style: String?
Expand All @@ -48,10 +58,30 @@ struct Border: Codable, Equatable {
}
}

struct LeftBorder: Codable, Equatable {
struct Value: Codable, Equatable {
let style: String?
}

var left: Value
var right: Value?
var top: Value?
var bottom: Value?
var diagonal: Value?
var horizontal: Value?
var vertical: Value?
}

final class BorderTest: XCTestCase {
func testSingleEmpty() throws {
let result = try XMLDecoder().decode(Borders.self, from: xml)
XCTAssertEqual(result.count, 1)
XCTAssertEqual(result.items[0], Border())
}

func testLeftBorder() throws {
let result = try XMLDecoder().decode(LeftBorders.self, from: xml)
XCTAssertEqual(result.count, 1)
XCTAssertEqual(result.items[0], nil)
}
}
1 change: 0 additions & 1 deletion Tests/XMLCoderTests/BreakfastTest.swift
Expand Up @@ -16,7 +16,6 @@ private let xml = """
<name>Belgian Waffles</name>
<price>$5.95</price>
<description>Two of our famous Belgian Waffles with plenty of real maple syrup</description>
<calories></calories>
</food>
<food>
<name>Strawberry Belgian Waffles</name>
Expand Down
68 changes: 68 additions & 0 deletions Tests/XMLCoderTests/EmptyArrayTest.swift
@@ -0,0 +1,68 @@
//
// EmptyArrayTest.swift
// XMLCoderTests
//
// Created by Benjamin Wetherfield on 10/1/19.
//

import XCTest
@testable import XMLCoder

struct Empty: Equatable, Codable {}

struct EmptyArray: Equatable, Codable {
enum CodingKeys: String, CodingKey { case empties = "empty" }
let empties: [Empty]
}

struct EmptyWrapper: Equatable, Codable {
let empty: Empty
}

struct OptionalEmptyWrapper: Equatable, Codable {
let empty: Empty?
}

private let xml = """
<container>
<empty/>
<empty/>
<empty/>
</container>
"""

private let xmlArray = """
<container>
<empty/>
<empty/>
<empty/>
</container>
"""

private let xmlContainsEmpty = """
<container>
<empty/>
</container>
"""

class EmptyArrayTest: XCTestCase {
func testEmptyArrayDecode() throws {
let decoded = try XMLDecoder().decode([Empty].self, from: xml.data(using: .utf8)!)
XCTAssertEqual(decoded, [Empty(), Empty(), Empty()])
}

func testWrappedEmptyArrayDecode() throws {
let decoded = try XMLDecoder().decode(EmptyArray.self, from: xmlArray.data(using: .utf8)!)
XCTAssertEqual(decoded, EmptyArray(empties: [Empty(), Empty(), Empty()]))
}

func testWrappedEmptyDecode() throws {
let decoded = try XMLDecoder().decode(EmptyWrapper.self, from: xmlContainsEmpty.data(using: .utf8)!)
XCTAssertEqual(decoded, EmptyWrapper(empty: Empty()))
}

func testWrappedOptionalEmptyDecode() throws {
let decoded = try XMLDecoder().decode(OptionalEmptyWrapper.self, from: xmlContainsEmpty.data(using: .utf8)!)
XCTAssertEqual(decoded, OptionalEmptyWrapper(empty: Empty()))
}
}
137 changes: 137 additions & 0 deletions Tests/XMLCoderTests/EmptyElementEmptyStringTests.swift
Expand Up @@ -9,6 +9,42 @@ import XCTest
import XMLCoder

class EmptyElementEmptyStringTests: XCTestCase {
struct ExplicitNestingContainer: Equatable, Decodable {
let things: ContainedArray

struct ContainedArray: Equatable, Decodable {
let thing: [Thing]

init(_ things: [Thing]) {
thing = things
}
}
}

struct NestingContainer: Equatable, Decodable {
let things: [Thing]

enum CodingKeys: String, CodingKey {
case things
}

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

var things = [Thing]()
if var thingContainer = try? container.nestedUnkeyedContainer(forKey: .things) {
while !thingContainer.isAtEnd {
things.append(try thingContainer.decode(Thing.self))
}
}
self.things = things
}

init(things: [Thing]) {
self.things = things
}
}

struct Parent: Equatable, Codable {
let thing: Thing
}
Expand Down Expand Up @@ -58,6 +94,23 @@ class EmptyElementEmptyStringTests: XCTestCase {
XCTAssertEqual(expected, result)
}

func testArrayOfSomeEmptyElementStringDecoding() throws {
let xml = """
<container>
<thing></thing>
<thing attribute="x">Non-Empty!</thing>
<thing>Non-Empty!</thing>
</container>
"""
let expected = [
Thing(attribute: nil, value: ""),
Thing(attribute: "x", value: "Non-Empty!"),
Thing(attribute: nil, value: "Non-Empty!"),
]
let result = try XMLDecoder().decode([Thing].self, from: xml.data(using: .utf8)!)
XCTAssertEqual(expected, result)
}

func testNestedEmptyElementEmptyStringDecoding() throws {
let xml = """
<parent>
Expand All @@ -68,4 +121,88 @@ class EmptyElementEmptyStringTests: XCTestCase {
let result = try XMLDecoder().decode(Parent.self, from: xml.data(using: .utf8)!)
XCTAssertEqual(expected, result)
}

func testExplicitlyNestedArrayOfEmptyElementEmptyStringDecoding() throws {
let xml = """
<container>
<things>
<thing></thing>
<thing attribute="x"></thing>
<thing></thing>
</things>
</container>
"""
let expected = ExplicitNestingContainer(
things: .init([
Thing(attribute: nil, value: ""),
Thing(attribute: "x", value: ""),
Thing(attribute: nil, value: ""),
])
)
let result = try XMLDecoder().decode(ExplicitNestingContainer.self, from: xml.data(using: .utf8)!)
XCTAssertEqual(expected, result)
}

func testExplicitlyNestedArrayOfSomeEmptyElementEmptyStringDecoding() throws {
let xml = """
<container>
<things>
<thing></thing>
<thing attribute="x">Non-Empty!</thing>
<thing>Non-Empty!</thing>
</things>
</container>
"""
let expected = ExplicitNestingContainer(
things: .init([
Thing(attribute: nil, value: ""),
Thing(attribute: "x", value: "Non-Empty!"),
Thing(attribute: nil, value: "Non-Empty!"),
])
)
let result = try XMLDecoder().decode(ExplicitNestingContainer.self, from: xml.data(using: .utf8)!)
XCTAssertEqual(expected, result)
}

func testNestedArrayOfEmptyElementEmptyStringDecoding() throws {
let xml = """
<container>
<things>
<thing></thing>
<thing attribute="x"></thing>
<thing></thing>
</things>
</container>
"""
let expected = NestingContainer(
things: [
Thing(attribute: nil, value: ""),
Thing(attribute: "x", value: ""),
Thing(attribute: nil, value: ""),
]
)
let result = try XMLDecoder().decode(NestingContainer.self, from: xml.data(using: .utf8)!)
XCTAssertEqual(expected, result)
}

func testNestedArrayOfSomeEmptyElementEmptyStringDecoding() throws {
let xml = """
<container>
<things>
<thing></thing>
<thing attribute="x">Non-Empty!</thing>
<thing>Non-Empty!</thing>
</things>
</container>
"""
let expected = NestingContainer(
things: [
Thing(attribute: nil, value: ""),
Thing(attribute: "x", value: "Non-Empty!"),
Thing(attribute: nil, value: "Non-Empty!"),
]
)
let result = try XMLDecoder().decode(NestingContainer.self, from: xml.data(using: .utf8)!)
XCTAssertEqual(expected, result)
}
}
14 changes: 14 additions & 0 deletions Tests/XMLCoderTests/Minimal/NullTests.swift
Expand Up @@ -52,4 +52,18 @@ class NullTests: XCTestCase {
let encoded = try encoder.encode(decoded, withRootKey: "container")
XCTAssertEqual(String(data: encoded, encoding: .utf8)!, xmlString)
}

func testNullElement() {
let decoder = XMLDecoder()

let xmlString =
"""
<container>
<value/>
</container>
"""
let xmlData = xmlString.data(using: .utf8)!

XCTAssertThrowsError(try decoder.decode(Container.self, from: xmlData))
}
}

0 comments on commit 8d28b86

Please sign in to comment.