Skip to content

Commit

Permalink
Merge pull request #12 from LiveUI/mapCodingStrategy
Browse files Browse the repository at this point in the history
Added strategies for decoding and encoding maps.
  • Loading branch information
rafiki270 authored Oct 23, 2018
2 parents 65d576b + 2ef05f8 commit 8c760e9
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 1 deletion.
63 changes: 62 additions & 1 deletion Sources/XMLCoding/Decoder/XMLDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@ open class XMLDecoder {
case collapseListUsingItemTag(String)
}

/// The strategy to use when decoding maps.
public enum MapDecodingStrategy {
/// Preserves the XML structure. Key and Value tags will be retained producing a
/// list of elements with these two attributes. This is the default strategy.
case preserveStructure

/// Collapse the XML structure to avoid key and value tags. For example-
/// <Result>
/// <Tag>
/// <Key>QueueType</Key>
/// <Value>Production</Value>
/// </Tag>
/// <Tag>
/// <Key>Owner</Key>
/// <Value>Developer123</Value>
/// </Tag>
/// </Result>
///
/// will be decoded into
/// struct Result {
/// let tags: [String: String]
/// }
case collapseMapUsingTags(keyTag: String, valueTag: String)
}

/// The strategy to use in decoding dates. Defaults to `.secondsSince1970`.
open var dateDecodingStrategy: DateDecodingStrategy = .secondsSince1970

Expand All @@ -127,6 +152,9 @@ open class XMLDecoder {
/// The strategy to use in decoding lists. Defaults to `.preserveStructure`.
open var listDecodingStrategy: ListDecodingStrategy = .preserveStructure

/// The strategy to use in decoding maps. Defaults to `.preserveStructure`.
open var mapDecodingStrategy: MapDecodingStrategy = .preserveStructure

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

Expand All @@ -136,6 +164,7 @@ open class XMLDecoder {
let dataDecodingStrategy: DataDecodingStrategy
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
let listDecodingStrategy: ListDecodingStrategy
let mapDecodingStrategy: MapDecodingStrategy
let userInfo: [CodingUserInfoKey : Any]
}

Expand All @@ -145,6 +174,7 @@ open class XMLDecoder {
dataDecodingStrategy: dataDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
listDecodingStrategy: listDecodingStrategy,
mapDecodingStrategy: mapDecodingStrategy,
userInfo: userInfo)
}

Expand Down Expand Up @@ -209,14 +239,45 @@ internal class _XMLDecoder : Decoder {

// MARK: - Decoder Methods

private func getMapFromListOfEntries(entryList: [Any], keyTag: String, valueTag: String) -> [String : Any] {
var newContainer: [String: Any] = [:]

// construct a dictionary from each entry and the key and value tags
entryList.forEach { entry in
if let keyedContainer = entry as? [String : Any],
let key = keyedContainer[keyTag] as? String,
let value = keyedContainer[valueTag] {
newContainer[key] = value
}
}

return newContainer
}

public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
guard !(self.storage.topContainer is NSNull) else {
throw DecodingError.valueNotFound(KeyedDecodingContainer<Key>.self,
DecodingError.Context(codingPath: self.codingPath,
debugDescription: "Cannot get keyed decoding container -- found null value instead."))
}

guard let topContainer = self.storage.topContainer as? [String : Any] else {
let topContainer: [String : Any]
// if this is a dictionary
if let currentContainer = self.storage.topContainer as? [String : Any] {

// if we are combining collapsing lists and maps
if case let .collapseListUsingItemTag(itemTag) = options.listDecodingStrategy,
case let .collapseMapUsingTags(keyTag: keyTag, valueTag: valueTag) = options.mapDecodingStrategy,
let itemList = currentContainer[itemTag] as? [Any] {
topContainer = getMapFromListOfEntries(entryList: itemList, keyTag: keyTag, valueTag: valueTag)
} else {
topContainer = currentContainer
}
// if this is a list and the mapDecodingStrategy is collapseMapUsingTags
} else if let currentContainer = self.storage.topContainer as? [Any],
case let .collapseMapUsingTags(keyTag: keyTag, valueTag: valueTag) = options.mapDecodingStrategy {
topContainer = getMapFromListOfEntries(entryList: currentContainer, keyTag: keyTag, valueTag: valueTag)
} else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String : Any].self, reality: self.storage.topContainer)
}

Expand Down
88 changes: 88 additions & 0 deletions Sources/XMLCoding/Encoder/XMLEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,31 @@ open class XMLEncoder {
case expandListWithItemTag(String)
}

/// The strategy to use when encoding maps.
public enum MapEncodingStrategy {
/// Preserves the XML structure. Key and Value tags will be retained producing a
/// list of elements with these two attributes. This is the default strategy.
case preserveStructure

/// Expands the XML structure to avoid key and value tags. For example-
/// <Result>
/// <Tag>
/// <Key>QueueType</Key>
/// <Value>Production</Value>
/// </Tag>
/// <Tag>
/// <Key>Owner</Key>
/// <Value>Developer123</Value>
/// </Tag>
/// </Result>
///
/// will be encoded from
/// struct Result {
/// let tags: [String: String]
/// }
case expandMapUsingTags(keyTag: String, valueTag: String)
}

/// The output format to produce. Defaults to `[]`.
open var outputFormatting: OutputFormatting = []

Expand All @@ -211,6 +236,9 @@ open class XMLEncoder {
/// The strategy to use in encoding lists. Defaults to `.preserveStructure`.
open var listEncodingStrategy: ListEncodingStrategy = .preserveStructure

/// The strategy to use in encoding map. Defaults to `.preserveStructure`.
open var mapEncodingStrategy: MapEncodingStrategy = .preserveStructure

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

Expand All @@ -223,6 +251,7 @@ open class XMLEncoder {
let attributeEncodingStrategy: AttributeEncodingStrategy
let stringEncodingStrategy: StringEncodingStrategy
let listEncodingStrategy: ListEncodingStrategy
let mapEncodingStrategy: MapEncodingStrategy
let userInfo: [CodingUserInfoKey : Any]
}

Expand All @@ -235,6 +264,7 @@ open class XMLEncoder {
attributeEncodingStrategy: attributeEncodingStrategy,
stringEncodingStrategy: stringEncodingStrategy,
listEncodingStrategy: listEncodingStrategy,
mapEncodingStrategy: mapEncodingStrategy,
userInfo: userInfo)
}

Expand Down Expand Up @@ -936,6 +966,9 @@ extension _XMLEncoder {
return .uint64(UInt64(int))
} else if let int = value as? UInt32 {
return .uint64(UInt64(int))
} else if let boxableEncodable = value as? BoxableEncodable {
// this type knows how to box itself
return try boxableEncodable.box(forEncoder: self)
}

let depth = self.storage.count
Expand All @@ -950,3 +983,58 @@ extension _XMLEncoder {
}
}

/// Protocol for a type that knows how to box itself
protocol BoxableEncodable: Encodable {
func box(forEncoder encoder: _XMLEncoder) throws -> MutableContainer?
}

extension Dictionary: BoxableEncodable where Key == String, Value: Encodable {
/// function to box (and potentially expand
internal func box(forEncoder encoder: _XMLEncoder) throws -> MutableContainer? {
let depth = encoder.storage.count
// if we are expanding maps
if case let .expandMapUsingTags(keyTag: keyTag, valueTag: valueTag) = encoder.options.mapEncodingStrategy {
let outerMutableContainer: (MutableContainerDictionary, String)?
// if we are expanding lists, wrap everything in a dictionary with that item
if case let .expandListWithItemTag(itemTag) = encoder.options.listEncodingStrategy {
let newMutableContainerDictionary = MutableContainerDictionary()
encoder.storage.push(container: .dictionary(newMutableContainerDictionary))
outerMutableContainer = (newMutableContainerDictionary, itemTag)
} else {
outerMutableContainer = nil
}

// create the expanded array
let mutableContainerArray = MutableContainerArray()
encoder.storage.push(container: .array(mutableContainerArray))

try self.forEach { (key, value) in
// create the dictionary for the key and value entries
let mutableContainerDictionary = MutableContainerDictionary()
encoder.storage.push(container: .dictionary(mutableContainerDictionary))
mutableContainerDictionary[keyTag] = .string(key)
mutableContainerDictionary[valueTag] = try encoder.box(value)

// add to the array
mutableContainerArray.append(encoder.storage.popContainer())
}

// add to the outer container if it is needed
if let outerMutableContainer = outerMutableContainer {
outerMutableContainer.0[outerMutableContainer.1] = encoder.storage.popContainer()
}
} else {
// otherwise just encode this array as normal
try self.encode(to: encoder)
}

// The top container should be a new container.
guard encoder.storage.count > depth else {
return nil
}

// return what has been created
return encoder.storage.popContainer()
}
}

Loading

0 comments on commit 8c760e9

Please sign in to comment.