Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement choice element decoding
Create interface for branching off XMLChoiceKeys

Add XMLSingleElementDecodingContainer copy pasta

Add XMLDecoderImplementation.singleElementContainer(keyedBy:) copy pasta

Push XMLChoiceKeys through singleElementContainer

Add XMLUnkeyedDecodingContainer.nestedSingleElementContainer copy pasta

Add XMLKeyedDecodingContainer.nestedSingleElementContainer copy pasta

Add XMLSingleElementDecodingContainer.nestedSingleElementContainer copy pasta

Remove print statement from test

Make IntOrStringWrapper.CodingKeys: XMLChoiceKey

Make Entry.CodingKeys: XMLChoiceKey

Only allow KeyedBoxes pass through to SingleElementDecodingContainer

Actually use XMLSingleElementDecodingContainer

Make tests pass

Rename XMLSingleElementDecodingContainer -> XMLChoiceDecodingContainer

Use ChoiceBox

Get rid of some prints

Unimplement singleElementContainer

Unimplement singleElementContainer

Tidy xcscheme

Unimplement nestedSingleElementContainer

Remove dump

Replace fatalError with thrown error

Omit type label

Omit type label

Fix formatting
  • Loading branch information
jsbean committed Jul 27, 2019
commit c128f2890091f5ddce9da8546468e0c2b19610a5
32 changes: 32 additions & 0 deletions Sources/XMLCoder/Auxiliaries/Box/ChoiceBox.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// 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 else { return nil }
let firstElement = keyedBox.elements[firstKey]
self.init(key: firstKey, element: firstElement)
}
}
8 changes: 8 additions & 0 deletions Sources/XMLCoder/Auxiliaries/Box/SingleElementBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ extension SingleElementBox: Box {
return nil
}
}

extension SingleElementBox {
init?(_ keyedBox: KeyedBox) {
guard let firstKey = keyedBox.elements.keys.first else { return nil }
let firstElement = keyedBox.elements[firstKey]
self.init(attributes: keyedBox.attributes, key: firstKey, element: firstElement)
}
}
254 changes: 254 additions & 0 deletions Sources/XMLCoder/Decoder/XMLChoiceDecodingContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
//
// XMLChoiceDecodingContainer.swift
// XMLCoder
//
// Created by James Bean on 7/18/19.
//

import Foundation

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

func mapKeys(
_ container: SharedBox<ChoiceBox>, closure: (String) -> String
) -> SharedBox<ChoiceBox> {
return SharedBox(
ChoiceBox(
key: closure(container.withShared { $0.key }),
element: container.withShared { $0.element }
)
)
}
// FIXME: Keep DRY from XMLKeyedDecodingContainer.init
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
}
}
codingPath = decoder.codingPath
}

// MARK: - KeyedDecodingContainerProtocol Methods

public var allKeys: [Key] {
return container.withShared { Key(stringValue: $0.key) }.map { [$0] } ?? []
}

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 }) else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: type,
reality: container
)
}
return try decodeConcrete(type, forKey: key)
}

public func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type, forKey key: Key
) throws -> KeyedDecodingContainer<NestedKey> {
decoder.codingPath.append(key)
defer { decoder.codingPath.removeLast() }

let value = container.withShared { $0.element }
let container: XMLKeyedDecodingContainer<NestedKey>

if let keyedContainer = value as? SharedBox<KeyedBox> {
container = XMLKeyedDecodingContainer<NestedKey>(
referencing: decoder,
wrapping: keyedContainer
)
} else if let keyedContainer = value as? KeyedBox {
container = XMLKeyedDecodingContainer<NestedKey>(
referencing: decoder,
wrapping: SharedBox(keyedContainer)
)
} else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: value
)
}
return KeyedDecodingContainer(container)
}

public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
decoder.codingPath.append(key)
defer { decoder.codingPath.removeLast() }
guard let unkeyedElement = container.withShared({ $0.element }) as? UnkeyedBox else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: UnkeyedBox.self,
reality: container
)
}
return XMLUnkeyedDecodingContainer(
referencing: decoder,
wrapping: SharedBox(unkeyedElement)
)
}

public func superDecoder() throws -> Decoder {
return try _superDecoder(forKey: XMLKey.super)
}

public func superDecoder(forKey key: Key) throws -> Decoder {
return try _superDecoder(forKey: key)
}
}

/// Private functions
extension XMLChoiceDecodingContainer {
private func _errorDescription(of key: CodingKey) -> String {
switch decoder.options.keyDecodingStrategy {
case .convertFromSnakeCase:
// In this case we can attempt to recover the original value by
// reversing the transform
let original = key.stringValue
let converted = XMLEncoder.KeyEncodingStrategy
._convertToSnakeCase(original)
if converted == original {
return "\(key) (\"\(original)\")"
} else {
return "\(key) (\"\(original)\"), converted to \(converted)"
}
default:
// Otherwise, just report the converted string
return "\(key) (\"\(key.stringValue)\")"
}
}

private func decodeSignedInteger<T>(_ type: T.Type,
forKey key: Key) throws -> T
where T: BinaryInteger & SignedInteger & Decodable {
return try decodeConcrete(type, forKey: key)
}

private func decodeUnsignedInteger<T>(_ type: T.Type,
forKey key: Key) throws -> T
where T: BinaryInteger & UnsignedInteger & Decodable {
return try decodeConcrete(type, forKey: key)
}

private func decodeFloatingPoint<T>(_ type: T.Type,
forKey key: Key) throws -> T
where T: BinaryFloatingPoint & Decodable {
return try decodeConcrete(type, forKey: key)
}

private func decodeConcrete<T: Decodable>(
_ type: T.Type,
forKey key: Key
) throws -> T {
guard let strategy = self.decoder.nodeDecodings.last else {
preconditionFailure(
"""
Attempt to access node decoding strategy from empty stack.
"""
)
}
let elements = container
.withShared { singleElementBox -> [KeyedBox.Element] in
if let unkeyed = singleElementBox.element as? UnkeyedBox {
return unkeyed
} else if let keyed = singleElementBox.element as? KeyedBox {
return keyed.elements[key.stringValue]
} else {
return []
}
}
decoder.codingPath.append(key)
let nodeDecodings = decoder.options.nodeDecodingStrategy.nodeDecodings(
forType: T.self,
with: decoder
)
decoder.nodeDecodings.append(nodeDecodings)
defer {
_ = decoder.nodeDecodings.removeLast()
decoder.codingPath.removeLast()
}
let box: Box = elements
let value: T?
if !(type is AnySequence.Type), let unkeyedBox = box as? UnkeyedBox,
let first = unkeyedBox.first {
value = try decoder.unbox(first)
} else {
value = try decoder.unbox(box)
}

if value == nil, let type = type as? AnyOptional.Type,
let result = type.init() as? T {
return result
}

guard let unwrapped = value else {
throw DecodingError.valueNotFound(type, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription:
"Expected \(type) value but found null instead."
))
}
return unwrapped
}

private func _superDecoder(forKey key: CodingKey) throws -> Decoder {
decoder.codingPath.append(key)
defer { decoder.codingPath.removeLast() }
let box: Box = container.withShared { $0.element }
return XMLDecoderImplementation(
referencing: box,
options: decoder.options,
nodeDecodings: decoder.nodeDecodings,
codingPath: decoder.codingPath
)
}
}
40 changes: 34 additions & 6 deletions Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift
Original file line number Diff line number Diff line change
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 XMLChoiceKey.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,26 @@ 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()
guard
let keyed = topContainer as? SharedBox<KeyedBox>,
let choiceBox = ChoiceBox(keyed.withShared { $0 })
else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: topContainer
)
}
let container = XMLChoiceDecodingContainer<Key>(
referencing: self,
wrapping: SharedBox(choiceBox)
)
return KeyedDecodingContainer(container)
}

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

Expand All @@ -140,10 +166,12 @@ class XMLDecoderImplementation: Decoder {
case let keyed as SharedBox<KeyedBox>:
return XMLUnkeyedDecodingContainer(
referencing: self,
wrapping: SharedBox(keyed.withShared { $0.elements.map { key, box in
SingleElementBox(attributes: SingleElementBox.Attributes(), key: key, element: box)
wrapping: SharedBox(
keyed.withShared { $0.elements.map { key, box in
SingleElementBox(attributes: .init(), key: key, element: box)
}
}
})
)
)
default:
throw DecodingError.typeMismatch(
Expand Down
2 changes: 1 addition & 1 deletion Tests/XMLCoderTests/CompositeChoiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ private enum IntOrStringWrapper: Equatable {
}

extension IntOrStringWrapper: Codable {
enum CodingKeys: String, CodingKey {
enum CodingKeys: String, XMLChoiceKey {
case int
case string
}
Expand Down
8 changes: 4 additions & 4 deletions Tests/XMLCoderTests/NestedChoiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,19 @@ extension Paragraph: Codable {
}
}

extension Entry: XMLChoiceCodable {
private enum CodingKeys: String, CodingKey {
extension Entry: Codable {
private enum CodingKeys: String, XMLChoiceKey {
case run, properties, br
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self = .run(try container.decode(Run.self, forKey: .run))
} catch DecodingError.keyNotFound {
} catch {
do {
self = .properties(try container.decode(Properties.self, forKey: .properties))
} catch DecodingError.keyNotFound {
} catch {
self = .br(try container.decode(Break.self, forKey: .br))
}
}
Expand Down
Loading