-
Notifications
You must be signed in to change notification settings - Fork 110
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
Array of enums with associated values #25
Comments
Hi @jsbean, first of all thanks a lot for the report and sorry for the delay caused by our current focus on test coverage in This indeed looks like a bug caused by |
Unifies the representation of `T` where `T: Box` and makes it explicit where shared reference semantics is expected. With this code performance is consistently 5-30% worse, could this be caused by copies of value types? Can this be improved when Swift 5.0 is released or whatever the version is that allows us to improve performance of value types? After reviewing related issues we'd prefer XMLCoder to be slightly slower, but with #12, #17 and #25 fixed than the opposite 😞 * Added `SharedBox` type for on-demand reference semantics * Remove `print` that kills performance tests * Handle all cases with SharedBox containers * Add RJITest back to the project file, fix the test * Remove unused internal initializer
@MaxDesiatov, no worries! Apologies for disappearing for a bit. Went unplugged. I'm glad the effort was put into enhancing the test coverage. I'll keep an eye on this. Let me know if there is anything I can do to contribute. |
Hello! I am also affected by this issue, any news? |
I am currently only looking at this from the decoding side of things, but here are some thoughts. Here is a pipeline that seems important for this feature:
Inside Here's an enum: enum IntOrString {
case int(Int)
case string(String)
} Here's its extension IntOrString: Decodable {
enum CodingKeys: String, CodingKey {
case int
case string
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self = .int(try container.decode(Int.self, forKey: .int))
} catch {
self = .string(try container.decode(String.self, forKey: .string))
}
}
} Here's an example of it in the wild: let xml = """
<container>
<int>"42"</int>
<string>"forty-two"</string>
<int>"43"</int>
</container>
""" If we try to decode it like this: let values = try XMLDecoder().decode([IntOrString].self, from: xml.data(using: .utf8)!) Within ["int": ["42", "43"], "string": "forty-two"] In this case, it does not behoove us to immediately treat this as a @MaxDesiatov, am I looking in the right place for this? |
I've been working on this issue for the last couple of days in attempt to resolve #91 and you're right, most of the problems are in how A temporary hack I've applied during my experimentation was to transform a {"int": ["42", "43"], "string": "forty-two"} into [{"int": "42"}, {"int": "43"}, {"string": "forty-two"}] which still loses the original order of keys. To clean this up I'm thinking of removing |
`UnkeyedBox` is a wrapper for `[Box]`, the assumption is that adding direct `Box` conformance to `[Box]` would simplify things a bit. Also, `KeyedStorage` now always stores `[Box]` per key without any special handling for single items per key to simplify this even more. In addition, order of values is preserved for all keys as required for #91 and #25. This should also unblock #101 providing unified handling for elements without any content and attributes. By pure serendipity this also fixes tests introduced in #38. * Replace UnkeyedBox with Array, refine KeyedStorage * Fix more of the broken tests * One unfixed test left in benchmarks * Single out failing benchmark in a separate test * Fix all tests 🎉 * Fix compiler warning * Fix Xcode 10.1 compilation error * Remove unused AnyArray protocol * Remove unused elementType function * Simplify code to improve test coverage
## Introduction This PR introduces the `XMLChoiceCodingKey` protocol, which enables the encoding and decoding of union-type–like enums with associated values to and from `XML` choice elements. Resolves #25. Resolves #91. ## Motivation XML schemas support [choice](https://www.w3schools.com/xml/el_choice.asp) elements, which constrain their contents to a single instance of a member of a known set of types. Choice elements exhibit the properties of [union types](https://en.wikipedia.org/wiki/Union_type) and can be represented in Swift as enums with associated values, wherein each case of the enum carries with it a single associated value that is one of the types representable. An example of how such a type is implemented in Swift: ```Swift enum IntOrString { case int(Int) case string(String) } ``` There is currently no automatic synthesis of the `Codable` protocol requirements for enums with assocated types in today's Swift. As such, it is required to provide custom implementations of the `init(from: Decoder)` initializer and the `encode(to: Encoder)` method to conform to the `Encodable` and `Decodable` protocols, respectively. When encoding to and decoding from `JSON`, a single-element keyed container is created that uses the enum case name as the single key. An example of adding Codable conformance to such a type when working with JSON ```Swift extension IntOrString: Codable { enum CodingKeys: String, CodingKey { case int, string } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) do { self = .int(try container.decode(Int.self, forKey: .int)) } catch { self = .string(try container.decode(String.self, forKey: .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) } } } ``` This may not be the most handsome approach, but it does the job without imprinting any format-specfic logic onto otherwise format-agnostic types. This pattern works out of the box with the `JSONEncoder` and `JSONDecoder` provided by the `Foundation` framework. However, due to syntactic characteristics of the `XML` format, this pattern will **_not_** work automatically for encoding and decoding `XML`-formatted data, regardless of the tool used. ## Proposed solution The proposed solution is to define a new `XMLChoiceCodingKey` protocol: ```Swift /// An empty marker protocol that can be used in place of `CodingKey`. /// It must be used when conforming a union-type–like enum with associated values to `Codable` /// when the encoded format is `XML`. public protocol XMLChoiceCodingKey: CodingKey {} ``` The `XMLChoiceCodingKey` protocol inherits from `CodingKey` and adds no new requirements. This conformance can be made retroactively, without additional implementation. An example usage: ```Swift extension IntOrString.CodingKeys: XMLChoiceCodingKey {} ``` ## Detailed design This proposal adds a single `public` `protocol` `XMLChoiceCodingKey`, as well as several `internal` types. Under the hood, the `XMLChoiceEncodingContainer` and `XMLChoiceDecodingContainer` are used to provide `encode` and `decode` methods tuned for `XML` choice elements. Because of the characteristics of the `XML` format, there are some ambiguities (from an encoding and decoding perpsective) between unkeyed container elements that contain choice elements and those that contain nested unkeyed container elements. In order to untangle these ambiguities, the new container types utilize a couple of new `Box` types to redirect elements along the encoding and decoding process. ## Source compatibility This is purely an additive change.
`UnkeyedBox` is a wrapper for `[Box]`, the assumption is that adding direct `Box` conformance to `[Box]` would simplify things a bit. Also, `KeyedStorage` now always stores `[Box]` per key without any special handling for single items per key to simplify this even more. In addition, order of values is preserved for all keys as required for CoreOffice#91 and CoreOffice#25. This should also unblock CoreOffice#101 providing unified handling for elements without any content and attributes. By pure serendipity this also fixes tests introduced in CoreOffice#38. * Replace UnkeyedBox with Array, refine KeyedStorage * Fix more of the broken tests * One unfixed test left in benchmarks * Single out failing benchmark in a separate test * Fix all tests 🎉 * Fix compiler warning * Fix Xcode 10.1 compilation error * Remove unused AnyArray protocol * Remove unused elementType function * Simplify code to improve test coverage
## Introduction This PR introduces the `XMLChoiceCodingKey` protocol, which enables the encoding and decoding of union-type–like enums with associated values to and from `XML` choice elements. Resolves CoreOffice#25. Resolves CoreOffice#91. ## Motivation XML schemas support [choice](https://www.w3schools.com/xml/el_choice.asp) elements, which constrain their contents to a single instance of a member of a known set of types. Choice elements exhibit the properties of [union types](https://en.wikipedia.org/wiki/Union_type) and can be represented in Swift as enums with associated values, wherein each case of the enum carries with it a single associated value that is one of the types representable. An example of how such a type is implemented in Swift: ```Swift enum IntOrString { case int(Int) case string(String) } ``` There is currently no automatic synthesis of the `Codable` protocol requirements for enums with assocated types in today's Swift. As such, it is required to provide custom implementations of the `init(from: Decoder)` initializer and the `encode(to: Encoder)` method to conform to the `Encodable` and `Decodable` protocols, respectively. When encoding to and decoding from `JSON`, a single-element keyed container is created that uses the enum case name as the single key. An example of adding Codable conformance to such a type when working with JSON ```Swift extension IntOrString: Codable { enum CodingKeys: String, CodingKey { case int, string } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) do { self = .int(try container.decode(Int.self, forKey: .int)) } catch { self = .string(try container.decode(String.self, forKey: .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) } } } ``` This may not be the most handsome approach, but it does the job without imprinting any format-specfic logic onto otherwise format-agnostic types. This pattern works out of the box with the `JSONEncoder` and `JSONDecoder` provided by the `Foundation` framework. However, due to syntactic characteristics of the `XML` format, this pattern will **_not_** work automatically for encoding and decoding `XML`-formatted data, regardless of the tool used. ## Proposed solution The proposed solution is to define a new `XMLChoiceCodingKey` protocol: ```Swift /// An empty marker protocol that can be used in place of `CodingKey`. /// It must be used when conforming a union-type–like enum with associated values to `Codable` /// when the encoded format is `XML`. public protocol XMLChoiceCodingKey: CodingKey {} ``` The `XMLChoiceCodingKey` protocol inherits from `CodingKey` and adds no new requirements. This conformance can be made retroactively, without additional implementation. An example usage: ```Swift extension IntOrString.CodingKeys: XMLChoiceCodingKey {} ``` ## Detailed design This proposal adds a single `public` `protocol` `XMLChoiceCodingKey`, as well as several `internal` types. Under the hood, the `XMLChoiceEncodingContainer` and `XMLChoiceDecodingContainer` are used to provide `encode` and `decode` methods tuned for `XML` choice elements. Because of the characteristics of the `XML` format, there are some ambiguities (from an encoding and decoding perpsective) between unkeyed container elements that contain choice elements and those that contain nested unkeyed container elements. In order to untangle these ambiguities, the new container types utilize a couple of new `Box` types to redirect elements along the encoding and decoding process. ## Source compatibility This is purely an additive change.
Thanks for taking up the maintenance on this project!
I am working with an XML schema which requires support for heterogeneous arrays of values within a set of specified types.
Consider the following enum which defines each of the types allowable:
Currently, the
Decodable
implementation is as follows (based on objc.io/codable-enums):A minimal example of the source XML could look something like this (though there could be arbitrary amounts of either type, in any order):
Upon decoding it with the
XMLDecoder
:We get the following:
The desired result should be:
I have tried many combinations of keyed, unkeyed, singleValue, and nested containers, yet haven't found a solution that treats the data as an
Array
rather than aDictionary
.Let me know if I am holding this wrong (at either the
Decodable
or theXMLDecoder
stages), or if this could be a current limitation of theXMLCoder
implementation. I'd be happy to contribute, but if anyone notices something glaring here, this would be very helpful.The text was updated successfully, but these errors were encountered: