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

Encodable and Decodable support for choice elements #119

Merged
merged 35 commits into from Jul 30, 2019
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
749e0fc
Add testing preliminaries for choice elements
bwetherfield Jul 15, 2019
ffbb395
Add ChoiceKey protocol conforming to CodingKey
jsbean Jul 26, 2019
77f9699
Implement choice element encoding
bwetherfield Jul 26, 2019
c128f28
Implement choice element decoding
jsbean Jul 27, 2019
312c4f5
Refactor clean up choice coding implementation
jsbean Jul 27, 2019
91693e9
Rename XMLChoiceKey -> XMLChoiceCodingKey
jsbean Jul 27, 2019
b5684f3
Rename SingleElementBox to SingleKeyedBox
jsbean Jul 26, 2019
5fd8d8f
Rename nestedSingleElementContainer -> nestedChoiceContainer
jsbean Jul 27, 2019
5bda791
Cull redundancies
jsbean Jul 27, 2019
045e07c
Add enum with associated value encoding tests
bwetherfield Jul 27, 2019
a7fb985
Fix usage to one key in the XMLChoiceDecodingContainer
jsbean Jul 26, 2019
b1b6c27
Factor out mapKeys to XMLDecoderImplementation.transformKeyedContainer
jsbean Jul 26, 2019
048d0c3
Be more assertive in NestingTests (#44)
jsbean Jul 27, 2019
5748eee
Merge branch 'master' into choice-implementation
jsbean Jul 27, 2019
c6ee065
Use KeyedBox like we used to (#46)
jsbean Jul 27, 2019
e6467d5
Rename scheme XMLCoder-Package -> XMLCoder
jsbean Jul 28, 2019
ce09102
Share scheme
jsbean Jul 28, 2019
f09c79d
Use Swift 4.2
jsbean Jul 28, 2019
0414fd8
Use Swift 4.2 everywhere
jsbean Jul 28, 2019
e1f0c45
Bring back old performance testing baseline
jsbean Jul 29, 2019
0b9c5cc
Whitespace
jsbean Jul 29, 2019
a8125e2
Bring back scheme management plist
jsbean Jul 29, 2019
bf52ca8
Bring back in empty AdditionalOptions
jsbean Jul 29, 2019
fd594fd
Whitespace
jsbean Jul 29, 2019
a930d00
Remove print statement
jsbean Jul 29, 2019
5a7a64a
Merge early exits in ChoiceBox.init?(_: KeyedBox)
jsbean Jul 30, 2019
c000573
Tighten up SharedBox init callsite
jsbean Jul 30, 2019
d4bd9f4
Rename _converted -> converted
jsbean Jul 30, 2019
4a99e95
Beef up XMLChoiceCodingKey doc comment
jsbean Jul 30, 2019
7920b72
Rename local variable mySelf -> oldSelf
jsbean Jul 30, 2019
683cb34
Wrangle long preconditionFailure messages
jsbean Jul 30, 2019
7db9627
Reword Implement -> Implementing in doc comment
jsbean Jul 30, 2019
c213808
Throw errors instead of fatallyErroring
jsbean Jul 30, 2019
32195c5
Add brief description to README
jsbean Jul 30, 2019
8149ead
Keep README in tag-ological order
jsbean Jul 30, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 40 additions & 0 deletions Sources/XMLCoder/Auxiliaries/Box/ChoiceBox.swift
@@ -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)
}
}
24 changes: 24 additions & 0 deletions Sources/XMLCoder/Auxiliaries/Box/SingleKeyedBox.swift
@@ -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
}
}
58 changes: 58 additions & 0 deletions Sources/XMLCoder/Auxiliaries/XMLChoiceCodingKey.swift
@@ -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)
/// }
///
/// Implement the requirements for the `Codable` protocol like this:
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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 {}
22 changes: 16 additions & 6 deletions Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
Expand Up @@ -63,9 +63,7 @@ struct XMLCoderElement: Equatable {
if elements.isEmpty, let value = value {
elements.append(StringBox(value), at: "value")
}
let keyedBox = KeyedBox(elements: elements, attributes: attributes)

return keyedBox
return KeyedBox(elements: elements, attributes: attributes)
}

func toXMLString(with header: XMLHeader? = nil,
Expand Down Expand Up @@ -245,9 +243,17 @@ struct XMLCoderElement: Equatable {

extension XMLCoderElement {
init(key: String, box: UnkeyedBox) {
self.init(key: key, elements: box.map {
XMLCoderElement(key: key, box: $0)
})
if let containsChoice = box as? [ChoiceBox] {
self.init(key: key, elements: containsChoice.map {
XMLCoderElement(key: $0.key, box: $0.element)
})
} else {
self.init(key: key, elements: box.map { XMLCoderElement(key: key, box: $0) })
}
}

init(key: String, box: ChoiceBox) {
self.init(key: key, elements: [XMLCoderElement(key: box.key, box: box.element)])
}

init(key: String, box: KeyedBox) {
Expand Down Expand Up @@ -302,10 +308,14 @@ extension XMLCoderElement {
self.init(key: key, box: sharedUnkeyedBox.unboxed)
case let sharedKeyedBox as SharedBox<KeyedBox>:
self.init(key: key, box: sharedKeyedBox.unboxed)
case let sharedChoiceBox as SharedBox<ChoiceBox>:
self.init(key: key, box: sharedChoiceBox.unboxed)
case let unkeyedBox as UnkeyedBox:
self.init(key: key, box: unkeyedBox)
case let keyedBox as KeyedBox:
self.init(key: key, box: keyedBox)
case let choiceBox as ChoiceBox:
self.init(key: key, box: choiceBox)
case let simpleBox as SimpleBox:
self.init(key: key, box: simpleBox)
case let box:
Expand Down
75 changes: 75 additions & 0 deletions Sources/XMLCoder/Decoder/XMLChoiceDecodingContainer.swift
@@ -0,0 +1,75 @@
//
// 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> {
fatalError("Choice elements cannot produce a nested container.")
}

public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
fatalError("Choice elements cannot produce a unkeyed nested container.")
}

public func superDecoder() throws -> Decoder {
fatalError("XMLChoiceDecodingContainer cannot produce a super decoder.")
}

public func superDecoder(forKey key: Key) throws -> Decoder {
fatalError("XMLChoiceDecodingContainer cannot produce a super decoder.")
}
}
66 changes: 60 additions & 6 deletions Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift
Expand Up @@ -70,9 +70,15 @@ class XMLDecoderImplementation: Decoder {
return topContainer
}

public func container<Key>(
keyedBy keyType: Key.Type
) throws -> KeyedDecodingContainer<Key> {
public func container<Key>(keyedBy keyType: Key.Type) throws -> KeyedDecodingContainer<Key> {
if Key.self is XMLChoiceCodingKey.Type {
return try choiceContainer(keyedBy: keyType)
} else {
return try keyedContainer(keyedBy: keyType)
}
}

public func keyedContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
let topContainer = try self.topContainer()

switch topContainer {
Expand Down Expand Up @@ -118,6 +124,34 @@ class XMLDecoderImplementation: Decoder {
}
}

/// - Returns: A `KeyedDecodingContainer` for an XML choice element.
public func choiceContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
let topContainer = try self.topContainer()
let choiceBox: ChoiceBox?
switch topContainer {
case let choice as ChoiceBox:
choiceBox = choice
case let singleKeyed as SingleKeyedBox:
choiceBox = ChoiceBox(singleKeyed)
case let keyed as SharedBox<KeyedBox>:
choiceBox = ChoiceBox(keyed.withShared { $0 })
default:
choiceBox = nil
}
guard let box = choiceBox else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: topContainer
)
}
let container = XMLChoiceDecodingContainer<Key>(
referencing: self,
wrapping: SharedBox(box)
)
return KeyedDecodingContainer(container)
}

public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
let topContainer = try self.topContainer()

Expand All @@ -138,9 +172,10 @@ class XMLDecoderImplementation: Decoder {
case let unkeyed as SharedBox<UnkeyedBox>:
return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
case let keyed as SharedBox<KeyedBox>:
guard let firstKey = keyed.withShared({ $0.elements.keys.first }) else { fallthrough }

return XMLUnkeyedDecodingContainer(referencing: self, wrapping: SharedBox(keyed.withShared { $0.elements[firstKey] }))
return XMLUnkeyedDecodingContainer(
referencing: self,
wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleKeyedBox.init) })
)
default:
throw DecodingError.typeMismatch(
at: codingPath,
Expand Down Expand Up @@ -420,3 +455,22 @@ extension XMLDecoderImplementation {
return result
}
}

extension XMLDecoderImplementation {
var keyTransform: (String) -> String {
switch options.keyDecodingStrategy {
case .convertFromSnakeCase:
return XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase
case .convertFromCapitalized:
return XMLDecoder.KeyDecodingStrategy._convertFromCapitalized
case .convertFromKebabCase:
return XMLDecoder.KeyDecodingStrategy._convertFromKebabCase
case .useDefaultKeys:
return { key in key }
case let .custom(converter):
return { key in
converter(self.codingPath + [XMLKey(stringValue: key, intValue: nil)]).stringValue
}
}
}
}
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
44 changes: 4 additions & 40 deletions Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift
Expand Up @@ -34,47 +34,11 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
wrapping container: KeyedContainer
) {
self.decoder = decoder

func mapKeys(
_ container: KeyedContainer,
closure: (String) -> String
) -> KeyedContainer {
let attributes = container.withShared { keyedBox in
keyedBox.attributes.map { (closure($0), $1) }
}
let elements = container.withShared { keyedBox in
keyedBox.elements.map { (closure($0), $1) }
}
let keyedBox = KeyedBox(elements: elements, attributes: attributes)
return SharedBox(keyedBox)
}

switch decoder.options.keyDecodingStrategy {
case .useDefaultKeys:
self.container = container
case .convertFromSnakeCase:
// Convert the snake case keys in the container to camel case.
// If we hit a duplicate key after conversion, then we'll use the
// first one we saw. Effectively an undefined behavior with dictionaries.
self.container = mapKeys(container) { key in
XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase(key)
}
case .convertFromKebabCase:
self.container = mapKeys(container) { key in
XMLDecoder.KeyDecodingStrategy._convertFromKebabCase(key)
}
case .convertFromCapitalized:
self.container = mapKeys(container) { key in
XMLDecoder.KeyDecodingStrategy._convertFromCapitalized(key)
}
case let .custom(converter):
self.container = mapKeys(container) { key in
let codingPath = decoder.codingPath + [
XMLKey(stringValue: key, intValue: nil),
]
return converter(codingPath).stringValue
}
container.withShared {
$0.elements = .init($0.elements.map { (decoder.keyTransform($0), $1) })
$0.attributes = .init($0.attributes.map { (decoder.keyTransform($0), $1) })
}
self.container = container
codingPath = decoder.codingPath
}

Expand Down