Skip to content

Commit

Permalink
Add NodeDecodingStrategy, mirroring NodeEncodingStrategy (#45)
Browse files Browse the repository at this point in the history
The way things are right now we cannot encode an XML string like this …

```xml
<element name="foo">
    <name>bar</name>
</element>
```

… due to XMLCoder picking one `name` over the other with no way of specifying "check for a `name` in the attributes" rather than "check for a `name` in the elements", e.g.

Also all other strategies exist as variants for encoding and decoding, never just one of them.

* Add `NodeDecodingStrategy`, mirroring `NodeEncodingStrategy `
* Fix coding style in DecodingContainerTests
* Fix unused constant after merge
* Fix most of the tests
* Fix all the tests!
* Rename case `both` to `elementOrAttribute`
* Remove print statements
* Fix formatting in tests
  • Loading branch information
regexident authored and MaxDesiatov committed Mar 18, 2019
1 parent 85a4e01 commit b8deb55
Show file tree
Hide file tree
Showing 19 changed files with 269 additions and 62 deletions.
2 changes: 0 additions & 2 deletions Sources/XMLCoder/Auxiliaries/Box/BoolBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Vincent Esche on 12/17/18.
//

import Foundation

struct BoolBox: Equatable {
typealias Unboxed = Bool

Expand Down
2 changes: 0 additions & 2 deletions Sources/XMLCoder/Auxiliaries/Box/Box.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Vincent Esche on 12/17/18.
//

import Foundation

protocol Box {
var isNull: Bool { get }
func xmlString() -> String?
Expand Down
2 changes: 0 additions & 2 deletions Sources/XMLCoder/Auxiliaries/Box/FloatBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Vincent Esche on 12/17/18.
//

import Foundation

struct FloatBox: Equatable {
typealias Unboxed = Float64

Expand Down
2 changes: 0 additions & 2 deletions Sources/XMLCoder/Auxiliaries/Box/IntBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Vincent Esche on 12/17/18.
//

import Foundation

struct IntBox: Equatable {
typealias Unboxed = Int64

Expand Down
2 changes: 0 additions & 2 deletions Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Vincent Esche on 11/19/18.
//

import Foundation

struct KeyedStorage<Key: Hashable & Comparable, Value> {
struct Iterator: IteratorProtocol {
fileprivate var orderIterator: Order.Iterator
Expand Down
6 changes: 1 addition & 5 deletions Sources/XMLCoder/Auxiliaries/Box/NullBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
// Created by Vincent Esche on 12/17/18.
//

import Foundation

struct NullBox {
init() {}
}
struct NullBox {}

extension NullBox: Box {
var isNull: Bool {
Expand Down
2 changes: 0 additions & 2 deletions Sources/XMLCoder/Auxiliaries/Box/SharedBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Vincent Esche on 12/22/18.
//

import Foundation

class SharedBox<Unboxed: Box> {
fileprivate var unboxed: Unboxed

Expand Down
2 changes: 0 additions & 2 deletions Sources/XMLCoder/Auxiliaries/Box/StringBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Vincent Esche on 12/17/18.
//

import Foundation

struct StringBox: Equatable {
typealias Unboxed = String

Expand Down
2 changes: 0 additions & 2 deletions Sources/XMLCoder/Auxiliaries/Box/UIntBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Vincent Esche on 12/17/18.
//

import Foundation

struct UIntBox: Equatable {
typealias Unboxed = UInt64

Expand Down
2 changes: 0 additions & 2 deletions Sources/XMLCoder/Auxiliaries/Box/UnkeyedBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Vincent Esche on 11/20/18.
//

import Foundation

// Minimalist implementation of an order-preserving unkeyed box:
struct UnkeyedBox {
typealias Element = Box
Expand Down
69 changes: 59 additions & 10 deletions Sources/XMLCoder/Decoder/XMLDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ open class XMLDecoder {
static func keyFormatted(
_ formatterForKey: @escaping (CodingKey) throws -> DateFormatter?
) -> XMLDecoder.DateDecodingStrategy {
return .custom({ (decoder) -> Date in
return .custom { (decoder) -> Date in
guard let codingKey = decoder.codingPath.last else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
Expand Down Expand Up @@ -72,7 +72,7 @@ open class XMLDecoder {
debugDescription: "Cannot decode date string \(text)"
)
}
})
}
}
}

Expand All @@ -91,7 +91,7 @@ open class XMLDecoder {
static func keyFormatted(
_ formatterForKey: @escaping (CodingKey) throws -> Data?
) -> XMLDecoder.DataDecodingStrategy {
return .custom({ (decoder) -> Data in
return .custom { (decoder) -> Data in
guard let codingKey = decoder.codingPath.last else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
Expand All @@ -115,7 +115,7 @@ open class XMLDecoder {
}

return data
})
}
}
}

Expand Down Expand Up @@ -232,6 +232,37 @@ open class XMLDecoder {
/// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys

/// A node's decoding tyoe
public enum NodeDecoding {
case attribute
case element
case elementOrAttribute
}

/// The strategy to use in encoding encoding attributes. Defaults to `.deferredToEncoder`.
open var nodeDecodingStrategy: NodeDecodingStrategy = .deferredToDecoder

/// Set of strategies to use for encoding of nodes.
public enum NodeDecodingStrategy {
/// Defer to `Encoder` for choosing an encoding. This is the default strategy.
case deferredToDecoder

/// Return a closure computing the desired node encoding for the value by its coding key.
case custom((Decodable.Type, Decoder) -> ((CodingKey) -> NodeDecoding))

func nodeDecodings(
forType codableType: Decodable.Type,
with decoder: Decoder
) -> ((CodingKey) -> NodeDecoding) {
switch self {
case .deferredToDecoder:
return { _ in .elementOrAttribute }
case let .custom(closure):
return closure(codableType, decoder)
}
}
}

/// Contextual user-provided information for use during decoding.
open var userInfo: [CodingUserInfoKey: Any] = [:]

Expand All @@ -256,16 +287,20 @@ open class XMLDecoder {
let dataDecodingStrategy: DataDecodingStrategy
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
let keyDecodingStrategy: KeyDecodingStrategy
let nodeDecodingStrategy: NodeDecodingStrategy
let userInfo: [CodingUserInfoKey: Any]
}

/// The options set on the top-level decoder.
var options: Options {
return Options(dateDecodingStrategy: dateDecodingStrategy,
dataDecodingStrategy: dataDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
keyDecodingStrategy: keyDecodingStrategy,
userInfo: userInfo)
return Options(
dateDecodingStrategy: dateDecodingStrategy,
dataDecodingStrategy: dataDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
keyDecodingStrategy: keyDecodingStrategy,
nodeDecodingStrategy: nodeDecodingStrategy,
userInfo: userInfo
)
}

// MARK: - Constructing a XML Decoder
Expand All @@ -292,7 +327,21 @@ open class XMLDecoder {
shouldProcessNamespaces: shouldProcessNamespaces
)

let decoder = XMLDecoderImplementation(referencing: topLevel, options: options)
let decoder = XMLDecoderImplementation(
referencing: topLevel,
options: options,
nodeDecodings: []
)
decoder.nodeDecodings = [
options.nodeDecodingStrategy.nodeDecodings(
forType: T.self,
with: decoder
),
]

defer {
_ = decoder.nodeDecodings.removeLast()
}

guard let box: T = try decoder.unbox(topLevel) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(
Expand Down
10 changes: 9 additions & 1 deletion Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class XMLDecoderImplementation: Decoder {
/// The path to the current point in encoding.
public internal(set) var codingPath: [CodingKey]

public var nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding]

/// Contextual user-provided information for use during encoding.
public var userInfo: [CodingUserInfoKey: Any] {
return options.userInfo
Expand All @@ -31,9 +33,15 @@ class XMLDecoderImplementation: Decoder {
// MARK: - Initialization

/// Initializes `self` with the given top-level container and options.
init(referencing container: Box, at codingPath: [CodingKey] = [], options: XMLDecoder.Options) {
init(
referencing container: Box,
options: XMLDecoder.Options,
nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding],
codingPath: [CodingKey] = []
) {
storage.push(container: container)
self.codingPath = codingPath
self.nodeDecodings = nodeDecodings
self.options = options
}

Expand Down
59 changes: 49 additions & 10 deletions Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
_ 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 elementOrNil = container.withShared { keyedBox -> KeyedBox.Element? in
if ["value", ""].contains(key.stringValue) {
return keyedBox.elements[key.stringValue] ?? keyedBox.value
Expand All @@ -178,17 +182,47 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
keyedBox.attributes[key.stringValue]
}

guard let entry = elementOrNil ?? attributeOrNil else {
throw DecodingError.keyNotFound(key, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "No value associated with key \(_errorDescription(of: key))."
))
}

decoder.codingPath.append(key)
defer { decoder.codingPath.removeLast() }
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
switch strategy(key) {
case .attribute:
guard let attributeBox = attributeOrNil else {
throw DecodingError.keyNotFound(key, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "No attribute found for key \(_errorDescription(of: key))."
))
}
box = attributeBox
case .element:
guard let elementBox = elementOrNil else {
throw DecodingError.keyNotFound(key, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "No element found for key \(_errorDescription(of: key))."
))
}
box = elementBox
case .elementOrAttribute:
guard
let anyBox = elementOrNil ?? attributeOrNil
else {
throw DecodingError.keyNotFound(key, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "No attribute found for key \(_errorDescription(of: key))."
))
}
box = anyBox
}

let value: T? = try decoder.unbox(entry)
let value: T? = try decoder.unbox(box)

if value == nil {
if let type = type as? AnyArray.Type,
Expand Down Expand Up @@ -295,7 +329,12 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
}

let box: Box = elementOrNil ?? attributeOrNil ?? NullBox()
return XMLDecoderImplementation(referencing: box, at: decoder.codingPath, options: decoder.options)
return XMLDecoderImplementation(
referencing: box,
options: decoder.options,
nodeDecodings: decoder.nodeDecodings,
codingPath: decoder.codingPath
)
}

public func superDecoder() throws -> Decoder {
Expand Down
23 changes: 20 additions & 3 deletions Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer {
_ type: T.Type,
decode: (XMLDecoderImplementation, Box) throws -> T?
) throws -> T {
guard let strategy = self.decoder.nodeDecodings.last else {
preconditionFailure("Attempt to access node decoding strategy from empty stack.")
}
decoder.codingPath.append(XMLKey(index: currentIndex))
let nodeDecodings = decoder.options.nodeDecodingStrategy.nodeDecodings(
forType: T.self,
with: decoder
)
decoder.nodeDecodings.append(nodeDecodings)
defer {
_ = decoder.nodeDecodings.removeLast()
_ = decoder.codingPath.removeLast()
}
guard !isAtEnd else {
throw DecodingError.valueNotFound(type, DecodingError.Context(
codingPath: decoder.codingPath + [XMLKey(index: self.currentIndex)],
Expand Down Expand Up @@ -211,8 +224,12 @@ struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer {
unkeyedBox[self.currentIndex]
}
currentIndex += 1
return XMLDecoderImplementation(referencing: value,
at: decoder.codingPath,
options: decoder.options)

return XMLDecoderImplementation(
referencing: value,
options: decoder.options,
nodeDecodings: decoder.nodeDecodings,
codingPath: decoder.codingPath
)
}
}
4 changes: 2 additions & 2 deletions Sources/XMLCoder/Encoder/XMLEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ open class XMLEncoder {
searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
}
words.append(wordStart..<searchRange.upperBound)
let result = words.map({ range in
let result = words.map { range in
stringKey[range].lowercased()
}).joined(separator: "_")
}.joined(separator: "_")
return result
}

Expand Down
1 change: 1 addition & 0 deletions Sources/XMLCoder/Encoder/XMLUnkeyedEncodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ struct XMLUnkeyedEncodingContainer: UnkeyedEncodingContainer {
) rethrows {
encoder.codingPath.append(XMLKey(index: count))
defer { self.encoder.codingPath.removeLast() }

try container.withShared { container in
container.append(try encode(encoder, value))
}
Expand Down
Loading

0 comments on commit b8deb55

Please sign in to comment.