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

Mixed choice/non-choice encoding #154

Merged
merged 15 commits into from Nov 27, 2019

Conversation

bwetherfield
Copy link
Collaborator

Overview

Fixes bug encountered when encoding structs that hold a mixture of choice-element and non-choice-element (or multiple choice-element) properties.

Example

Given a structure that stores both a choice and non-choice property,

private struct IntOrStringAndDouble: Equatable {
    let intOrString: IntOrString
    let decimal: Double
}

the natural encoding approach (now available) is

extension IntOrStringAndDouble: Encodable {
    enum CodingKeys: String, CodingKey {
        case decimal
    }

    func encode(to encoder: Encoder) {
        try intOrString.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(decimal, forKey: .decimal)
    }
}
The following encode implementation also works

extension IntOrStringAndDouble: Encodable {
    enum CodingKeys: String, CodingKey {
        case decimal
    }

    func encode(to encoder: Encoder) {
        var singleValueContainer = encoder.singleValueContainer()
        try singleValueContainer.encode(intOrString)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(decimal, forKey: .decimal)
    }
}

IntOrString as defined in #119

enum IntOrString: Equatable {
    case int(Int)
    case string(String)
}

extension IntOrString: Encodable {
    enum CodingKeys: String, CodingKey {
        case int
        case string
    }

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

extension IntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `IntOrString` is a choice element

Implementation Details

In cases where choice and non-choice elements (or multiple choice elements) co-exist in a keyed container, we merge them into a single XMLKeyedEncodingContainer (wrapping a SharedBox<KeyedBox>). Arrays of choice elements (using XMLUnkeyedEncodingContainer under the hood) are encoded the same way as before, as we do not hit the merging cases. For the array case, we still need the XMLChoiceEncodingContainer structure.

Source Compatibility

This is an additive change.

@bwetherfield bwetherfield changed the title Mixed choice encoding Mixed choice/non-choice encoding Nov 25, 2019
@MaxDesiatov MaxDesiatov added the bug Something isn't working label Nov 25, 2019
Comment on lines 118 to 119
// We haven't yet pushed a container at this level; do so here.
topContainer = storage.pushChoiceContainer()
let container = XMLChoiceEncodingContainer<Key>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likewise here

Comment on lines 77 to 78
// We haven't yet pushed a container at this level; do so here.
topContainer = storage.pushKeyedContainer()
let container = XMLKeyedEncodingContainer<Key>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment above seems to be outdated now

Attempt to push new (single element) keyed encoding container when already \
previously encoded at this path.
Attempt to push new keyed encoding container when already previously encoded \
at this path.
"""
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this switch seems to be a duplicate of switch in public func keyedContainer modified above, could it be generalized somehow?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point!

Copy link
Collaborator

@MaxDesiatov MaxDesiatov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing, many thanks!

@MaxDesiatov MaxDesiatov merged commit deb2ab2 into CoreOffice:master Nov 27, 2019
MaxDesiatov pushed a commit that referenced this pull request Dec 1, 2019
Mirrors #154 

Fixes bugs 
1) Decoding multiple choice elements in the same Keyed Container.
2) Decoding choice elements after decoding regular keyed elements in the same container.

Case 1 refers to decoding implementations of the form: 
```swift
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        otherValue = try container.decode(String.self, forKey: .otherValue)
        intOrString = try IntOrString(from: decoder)
    }
```
where `IntOrString` is a choice coding element

`IntOrString` defined as follows:

```swift 
enum IntOrString {
    case int(Int)
    case string(String)
}

extension IntOrString: Decodable {
    enum CodingKeys: String, CodingKey {
        case int
        case string
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if container.contains(.int) {
            self = .int(try container.decode(Int.self, forKey: .int))
        } else {
            self = .string(try container.decode(String.self, forKey: .string))
        }
    }
}

extension IntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `IntOrString` is a choice element
```

Case 2 refers to decoding implementations of the following form:

```swift
    init(from decoder: Decoder) throws {
        self.first = try IntOrString(from: decoder)
        self.second = try AlternateIntOrString(from: decoder)
    }
```

Where both `first: IntOrString` and `second: AlternateIntOrString` are choice elements.

`AlternateIntOrString` defined as follows:

```swift
private enum AlternateIntOrString {
    case alternateInt(Int)
    case alternateString(String)
}

extension AlternateIntOrString: Decodable {
    enum CodingKeys: String, CodingKey {
        case alternateInt = "alternate-int"
        case alternateString = "alternate-string"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if container.contains(.alternateInt) {
            self = .alternateInt(try container.decode(Int.self, forKey: .alternateInt))
        } else {
            self = .alternateString(try container.decode(String.self, forKey: .alternateString))
        }
    }
}

extension AlternateIntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `AlternateIntOrString` is a choice element
```

* Add tests
* Fix failing tests
arjungupta0107 pushed a commit to salido/XMLCoder that referenced this pull request Jun 26, 2020
## Overview

Fixes bug encountered when encoding structs that hold a mixture of choice-element and non-choice-element (or multiple choice-element) properties.

## Example

Given a structure that stores both a choice and non-choice property,
```swift
private struct IntOrStringAndDouble: Equatable {
    let intOrString: IntOrString
    let decimal: Double
}
```

the natural encoding approach (now available) is 
```swift
extension IntOrStringAndDouble: Encodable {
    enum CodingKeys: String, CodingKey {
        case decimal
    }

    func encode(to encoder: Encoder) {
        try intOrString.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(decimal, forKey: .decimal)
    }
}
```

The following `encode` implementation also works:

```swift
extension IntOrStringAndDouble: Encodable {
    enum CodingKeys: String, CodingKey {
        case decimal
    }

    func encode(to encoder: Encoder) {
        var singleValueContainer = encoder.singleValueContainer()
        try singleValueContainer.encode(intOrString)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(decimal, forKey: .decimal)
    }
}
```

`IntOrString` as defined in CoreOffice#119:

```swift 
enum IntOrString: Equatable {
    case int(Int)
    case string(String)
}

extension IntOrString: Encodable {
    enum CodingKeys: String, CodingKey {
        case int
        case string
    }

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

extension IntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `IntOrString` is a choice element
```

## Implementation Details

In cases where choice and non-choice elements (or multiple choice elements) co-exist in a keyed container, we merge them into a single `XMLKeyedEncodingContainer` (wrapping a `SharedBox<KeyedBox>`). Arrays of choice elements (using `XMLUnkeyedEncodingContainer` under the hood) are encoded the same way as before, as we do not hit the merging cases. For the array case, we still need the `XMLChoiceEncodingContainer` structure.

## Source Compatibility

This is an additive change.

* Add breaking case
* Add choice and keyed merging encode functionality
* Refactor
* Fix commented code
* Fix misnamed file
* Fix xcode project
* Fix precondition catch
* Use switch syntax
* Add multiple choice element case
* Add explicit types in KeyedBox initialization
* Add explicitly empty parameter to KeyedBox initializer
* Use more concise type inference
* Unify switch syntax
* Cut down code duplication
* Fix formatting
arjungupta0107 pushed a commit to salido/XMLCoder that referenced this pull request Jun 26, 2020
Mirrors CoreOffice#154 

Fixes bugs 
1) Decoding multiple choice elements in the same Keyed Container.
2) Decoding choice elements after decoding regular keyed elements in the same container.

Case 1 refers to decoding implementations of the form: 
```swift
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        otherValue = try container.decode(String.self, forKey: .otherValue)
        intOrString = try IntOrString(from: decoder)
    }
```
where `IntOrString` is a choice coding element

`IntOrString` defined as follows:

```swift 
enum IntOrString {
    case int(Int)
    case string(String)
}

extension IntOrString: Decodable {
    enum CodingKeys: String, CodingKey {
        case int
        case string
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if container.contains(.int) {
            self = .int(try container.decode(Int.self, forKey: .int))
        } else {
            self = .string(try container.decode(String.self, forKey: .string))
        }
    }
}

extension IntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `IntOrString` is a choice element
```

Case 2 refers to decoding implementations of the following form:

```swift
    init(from decoder: Decoder) throws {
        self.first = try IntOrString(from: decoder)
        self.second = try AlternateIntOrString(from: decoder)
    }
```

Where both `first: IntOrString` and `second: AlternateIntOrString` are choice elements.

`AlternateIntOrString` defined as follows:

```swift
private enum AlternateIntOrString {
    case alternateInt(Int)
    case alternateString(String)
}

extension AlternateIntOrString: Decodable {
    enum CodingKeys: String, CodingKey {
        case alternateInt = "alternate-int"
        case alternateString = "alternate-string"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if container.contains(.alternateInt) {
            self = .alternateInt(try container.decode(Int.self, forKey: .alternateInt))
        } else {
            self = .alternateString(try container.decode(String.self, forKey: .alternateString))
        }
    }
}

extension AlternateIntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `AlternateIntOrString` is a choice element
```

* Add tests
* Fix failing tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants