forked from CoreOffice/XMLCoder
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Encodable and Decodable support for choice elements (CoreOffice#119)
## 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.
- Loading branch information
1 parent
3ae4113
commit ab9fef0
Showing
23 changed files
with
1,988 additions
and
722 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// | ||
// ChoiceBox.swift | ||
// XMLCoder | ||
// | ||
// Created by James Bean on 7/18/19. | ||
// | ||
|
||
/// A `Box` which represents an element which is known to contain an XML choice element. | ||
struct ChoiceBox { | ||
var key: String = "" | ||
var element: Box = NullBox() | ||
} | ||
|
||
extension ChoiceBox: Box { | ||
var isNull: Bool { | ||
return false | ||
} | ||
|
||
func xmlString() -> String? { | ||
return nil | ||
} | ||
} | ||
|
||
extension ChoiceBox: SimpleBox {} | ||
|
||
extension ChoiceBox { | ||
init?(_ keyedBox: KeyedBox) { | ||
guard | ||
let firstKey = keyedBox.elements.keys.first, | ||
let firstElement = keyedBox.elements[firstKey].first | ||
else { | ||
return nil | ||
} | ||
self.init(key: firstKey, element: firstElement) | ||
} | ||
|
||
init(_ singleKeyedBox: SingleKeyedBox) { | ||
self.init(key: singleKeyedBox.key, element: singleKeyedBox.element) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// | ||
// SingleKeyedBox.swift | ||
// XMLCoder | ||
// | ||
// Created by James Bean on 7/15/19. | ||
// | ||
|
||
/// A `Box` which contains a single `key` and `element` pair. This is useful for disambiguating elements which could either represent | ||
/// an element nested in a keyed or unkeyed container, or an choice between multiple known-typed values (implemented in Swift using | ||
/// enums with associated values). | ||
struct SingleKeyedBox: SimpleBox { | ||
var key: String | ||
var element: Box | ||
} | ||
|
||
extension SingleKeyedBox: Box { | ||
var isNull: Bool { | ||
return false | ||
} | ||
|
||
func xmlString() -> String? { | ||
return nil | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// | ||
// XMLChoiceCodingKey.swift | ||
// XMLCoder | ||
// | ||
// Created by Benjamin Wetherfield on 7/17/19. | ||
// | ||
|
||
/// An empty marker protocol that can be used in place of `CodingKey`. It must be used when | ||
/// attempting to encode and decode union-type–like enums with associated values to and from `XML` | ||
/// choice elements. | ||
/// | ||
/// - Important: In order for your `XML`-destined `Codable` type to be encoded and/or decoded | ||
/// properly, you must conform your custom `CodingKey` type additionally to `XMLChoiceCodingKey`. | ||
/// | ||
/// For example, say you have defined a type which can hold _either_ an `Int` _or_ a `String`: | ||
/// | ||
/// enum IntOrString { | ||
/// case int(Int) | ||
/// case string(String) | ||
/// } | ||
/// | ||
/// Implementing the requirements for the `Codable` protocol like this: | ||
/// | ||
/// extension IntOrString: Codable { | ||
/// enum CodingKeys: String, XMLChoiceCodingKey { | ||
/// 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) | ||
/// } | ||
/// } | ||
/// | ||
/// 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)) | ||
/// } | ||
/// } | ||
/// } | ||
/// | ||
/// Retroactively conform the `CodingKeys` enum to `XMLChoiceCodingKey` when targeting `XML` as your | ||
/// encoded format. | ||
/// | ||
/// extension IntOrString.CodingKeys: XMLChoiceCodingKey {} | ||
/// | ||
/// - Note: The `XMLChoiceCodingKey` marker protocol allows the `XMLEncoder` / `XMLDecoder` to | ||
/// resolve ambiguities particular to the `XML` format between nested unkeyed container elements and | ||
/// choice elements. | ||
public protocol XMLChoiceCodingKey: CodingKey {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// | ||
// XMLChoiceDecodingContainer.swift | ||
// XMLCoder | ||
// | ||
// Created by James Bean on 7/18/19. | ||
// | ||
|
||
/// Container specialized for decoding XML choice elements. | ||
struct XMLChoiceDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol { | ||
typealias Key = K | ||
|
||
// MARK: Properties | ||
|
||
/// A reference to the decoder we're reading from. | ||
private let decoder: XMLDecoderImplementation | ||
|
||
/// A reference to the container we're reading from. | ||
private let container: SharedBox<ChoiceBox> | ||
|
||
/// The path of coding keys taken to get to this point in decoding. | ||
public private(set) var codingPath: [CodingKey] | ||
|
||
// MARK: - Initialization | ||
|
||
/// Initializes `self` by referencing the given decoder and container. | ||
init(referencing decoder: XMLDecoderImplementation, wrapping container: SharedBox<ChoiceBox>) { | ||
self.decoder = decoder | ||
container.withShared { $0.key = decoder.keyTransform($0.key) } | ||
self.container = container | ||
codingPath = decoder.codingPath | ||
} | ||
|
||
// MARK: - KeyedDecodingContainerProtocol Methods | ||
|
||
public var allKeys: [Key] { | ||
return container.withShared { [Key(stringValue: $0.key)!] } | ||
} | ||
|
||
public func contains(_ key: Key) -> Bool { | ||
return container.withShared { $0.key == key.stringValue } | ||
} | ||
|
||
public func decodeNil(forKey key: Key) throws -> Bool { | ||
return container.withShared { $0.element.isNull } | ||
} | ||
|
||
public func decode<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T { | ||
guard container.withShared({ $0.key == key.stringValue }), key is XMLChoiceCodingKey else { | ||
throw DecodingError.typeMismatch( | ||
at: codingPath, | ||
expectation: type, | ||
reality: container | ||
) | ||
} | ||
return try decoder.unbox(container.withShared { $0.element }) | ||
} | ||
|
||
public func nestedContainer<NestedKey>( | ||
keyedBy _: NestedKey.Type, forKey key: Key | ||
) throws -> KeyedDecodingContainer<NestedKey> { | ||
throw DecodingError.typeMismatch( | ||
at: codingPath, | ||
expectation: NestedKey.self, | ||
reality: container | ||
) | ||
} | ||
|
||
public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { | ||
throw DecodingError.typeMismatch( | ||
at: codingPath, | ||
expectation: Key.self, | ||
reality: container | ||
) | ||
} | ||
|
||
public func superDecoder() throws -> Decoder { | ||
throw DecodingError.typeMismatch( | ||
at: codingPath, | ||
expectation: Key.self, | ||
reality: container | ||
) | ||
} | ||
|
||
public func superDecoder(forKey key: Key) throws -> Decoder { | ||
throw DecodingError.typeMismatch( | ||
at: codingPath, | ||
expectation: Key.self, | ||
reality: container | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.