Skip to content

Commit 8c760e9

Browse files
authored
Merge pull request #12 from LiveUI/mapCodingStrategy
Added strategies for decoding and encoding maps.
2 parents 65d576b + 2ef05f8 commit 8c760e9

File tree

3 files changed

+292
-1
lines changed

3 files changed

+292
-1
lines changed

Sources/XMLCoding/Decoder/XMLDecoder.swift

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,31 @@ open class XMLDecoder {
115115
case collapseListUsingItemTag(String)
116116
}
117117

118+
/// The strategy to use when decoding maps.
119+
public enum MapDecodingStrategy {
120+
/// Preserves the XML structure. Key and Value tags will be retained producing a
121+
/// list of elements with these two attributes. This is the default strategy.
122+
case preserveStructure
123+
124+
/// Collapse the XML structure to avoid key and value tags. For example-
125+
/// <Result>
126+
/// <Tag>
127+
/// <Key>QueueType</Key>
128+
/// <Value>Production</Value>
129+
/// </Tag>
130+
/// <Tag>
131+
/// <Key>Owner</Key>
132+
/// <Value>Developer123</Value>
133+
/// </Tag>
134+
/// </Result>
135+
///
136+
/// will be decoded into
137+
/// struct Result {
138+
/// let tags: [String: String]
139+
/// }
140+
case collapseMapUsingTags(keyTag: String, valueTag: String)
141+
}
142+
118143
/// The strategy to use in decoding dates. Defaults to `.secondsSince1970`.
119144
open var dateDecodingStrategy: DateDecodingStrategy = .secondsSince1970
120145

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

155+
/// The strategy to use in decoding maps. Defaults to `.preserveStructure`.
156+
open var mapDecodingStrategy: MapDecodingStrategy = .preserveStructure
157+
130158
/// Contextual user-provided information for use during decoding.
131159
open var userInfo: [CodingUserInfoKey : Any] = [:]
132160

@@ -136,6 +164,7 @@ open class XMLDecoder {
136164
let dataDecodingStrategy: DataDecodingStrategy
137165
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
138166
let listDecodingStrategy: ListDecodingStrategy
167+
let mapDecodingStrategy: MapDecodingStrategy
139168
let userInfo: [CodingUserInfoKey : Any]
140169
}
141170

@@ -145,6 +174,7 @@ open class XMLDecoder {
145174
dataDecodingStrategy: dataDecodingStrategy,
146175
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
147176
listDecodingStrategy: listDecodingStrategy,
177+
mapDecodingStrategy: mapDecodingStrategy,
148178
userInfo: userInfo)
149179
}
150180

@@ -209,14 +239,45 @@ internal class _XMLDecoder : Decoder {
209239

210240
// MARK: - Decoder Methods
211241

242+
private func getMapFromListOfEntries(entryList: [Any], keyTag: String, valueTag: String) -> [String : Any] {
243+
var newContainer: [String: Any] = [:]
244+
245+
// construct a dictionary from each entry and the key and value tags
246+
entryList.forEach { entry in
247+
if let keyedContainer = entry as? [String : Any],
248+
let key = keyedContainer[keyTag] as? String,
249+
let value = keyedContainer[valueTag] {
250+
newContainer[key] = value
251+
}
252+
}
253+
254+
return newContainer
255+
}
256+
212257
public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
213258
guard !(self.storage.topContainer is NSNull) else {
214259
throw DecodingError.valueNotFound(KeyedDecodingContainer<Key>.self,
215260
DecodingError.Context(codingPath: self.codingPath,
216261
debugDescription: "Cannot get keyed decoding container -- found null value instead."))
217262
}
218263

219-
guard let topContainer = self.storage.topContainer as? [String : Any] else {
264+
let topContainer: [String : Any]
265+
// if this is a dictionary
266+
if let currentContainer = self.storage.topContainer as? [String : Any] {
267+
268+
// if we are combining collapsing lists and maps
269+
if case let .collapseListUsingItemTag(itemTag) = options.listDecodingStrategy,
270+
case let .collapseMapUsingTags(keyTag: keyTag, valueTag: valueTag) = options.mapDecodingStrategy,
271+
let itemList = currentContainer[itemTag] as? [Any] {
272+
topContainer = getMapFromListOfEntries(entryList: itemList, keyTag: keyTag, valueTag: valueTag)
273+
} else {
274+
topContainer = currentContainer
275+
}
276+
// if this is a list and the mapDecodingStrategy is collapseMapUsingTags
277+
} else if let currentContainer = self.storage.topContainer as? [Any],
278+
case let .collapseMapUsingTags(keyTag: keyTag, valueTag: valueTag) = options.mapDecodingStrategy {
279+
topContainer = getMapFromListOfEntries(entryList: currentContainer, keyTag: keyTag, valueTag: valueTag)
280+
} else {
220281
throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String : Any].self, reality: self.storage.topContainer)
221282
}
222283

Sources/XMLCoding/Encoder/XMLEncoder.swift

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,31 @@ open class XMLEncoder {
187187
case expandListWithItemTag(String)
188188
}
189189

190+
/// The strategy to use when encoding maps.
191+
public enum MapEncodingStrategy {
192+
/// Preserves the XML structure. Key and Value tags will be retained producing a
193+
/// list of elements with these two attributes. This is the default strategy.
194+
case preserveStructure
195+
196+
/// Expands the XML structure to avoid key and value tags. For example-
197+
/// <Result>
198+
/// <Tag>
199+
/// <Key>QueueType</Key>
200+
/// <Value>Production</Value>
201+
/// </Tag>
202+
/// <Tag>
203+
/// <Key>Owner</Key>
204+
/// <Value>Developer123</Value>
205+
/// </Tag>
206+
/// </Result>
207+
///
208+
/// will be encoded from
209+
/// struct Result {
210+
/// let tags: [String: String]
211+
/// }
212+
case expandMapUsingTags(keyTag: String, valueTag: String)
213+
}
214+
190215
/// The output format to produce. Defaults to `[]`.
191216
open var outputFormatting: OutputFormatting = []
192217

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

239+
/// The strategy to use in encoding map. Defaults to `.preserveStructure`.
240+
open var mapEncodingStrategy: MapEncodingStrategy = .preserveStructure
241+
214242
/// Contextual user-provided information for use during encoding.
215243
open var userInfo: [CodingUserInfoKey : Any] = [:]
216244

@@ -223,6 +251,7 @@ open class XMLEncoder {
223251
let attributeEncodingStrategy: AttributeEncodingStrategy
224252
let stringEncodingStrategy: StringEncodingStrategy
225253
let listEncodingStrategy: ListEncodingStrategy
254+
let mapEncodingStrategy: MapEncodingStrategy
226255
let userInfo: [CodingUserInfoKey : Any]
227256
}
228257

@@ -235,6 +264,7 @@ open class XMLEncoder {
235264
attributeEncodingStrategy: attributeEncodingStrategy,
236265
stringEncodingStrategy: stringEncodingStrategy,
237266
listEncodingStrategy: listEncodingStrategy,
267+
mapEncodingStrategy: mapEncodingStrategy,
238268
userInfo: userInfo)
239269
}
240270

@@ -936,6 +966,9 @@ extension _XMLEncoder {
936966
return .uint64(UInt64(int))
937967
} else if let int = value as? UInt32 {
938968
return .uint64(UInt64(int))
969+
} else if let boxableEncodable = value as? BoxableEncodable {
970+
// this type knows how to box itself
971+
return try boxableEncodable.box(forEncoder: self)
939972
}
940973

941974
let depth = self.storage.count
@@ -950,3 +983,58 @@ extension _XMLEncoder {
950983
}
951984
}
952985

986+
/// Protocol for a type that knows how to box itself
987+
protocol BoxableEncodable: Encodable {
988+
func box(forEncoder encoder: _XMLEncoder) throws -> MutableContainer?
989+
}
990+
991+
extension Dictionary: BoxableEncodable where Key == String, Value: Encodable {
992+
/// function to box (and potentially expand
993+
internal func box(forEncoder encoder: _XMLEncoder) throws -> MutableContainer? {
994+
let depth = encoder.storage.count
995+
// if we are expanding maps
996+
if case let .expandMapUsingTags(keyTag: keyTag, valueTag: valueTag) = encoder.options.mapEncodingStrategy {
997+
let outerMutableContainer: (MutableContainerDictionary, String)?
998+
// if we are expanding lists, wrap everything in a dictionary with that item
999+
if case let .expandListWithItemTag(itemTag) = encoder.options.listEncodingStrategy {
1000+
let newMutableContainerDictionary = MutableContainerDictionary()
1001+
encoder.storage.push(container: .dictionary(newMutableContainerDictionary))
1002+
outerMutableContainer = (newMutableContainerDictionary, itemTag)
1003+
} else {
1004+
outerMutableContainer = nil
1005+
}
1006+
1007+
// create the expanded array
1008+
let mutableContainerArray = MutableContainerArray()
1009+
encoder.storage.push(container: .array(mutableContainerArray))
1010+
1011+
try self.forEach { (key, value) in
1012+
// create the dictionary for the key and value entries
1013+
let mutableContainerDictionary = MutableContainerDictionary()
1014+
encoder.storage.push(container: .dictionary(mutableContainerDictionary))
1015+
mutableContainerDictionary[keyTag] = .string(key)
1016+
mutableContainerDictionary[valueTag] = try encoder.box(value)
1017+
1018+
// add to the array
1019+
mutableContainerArray.append(encoder.storage.popContainer())
1020+
}
1021+
1022+
// add to the outer container if it is needed
1023+
if let outerMutableContainer = outerMutableContainer {
1024+
outerMutableContainer.0[outerMutableContainer.1] = encoder.storage.popContainer()
1025+
}
1026+
} else {
1027+
// otherwise just encode this array as normal
1028+
try self.encode(to: encoder)
1029+
}
1030+
1031+
// The top container should be a new container.
1032+
guard encoder.storage.count > depth else {
1033+
return nil
1034+
}
1035+
1036+
// return what has been created
1037+
return encoder.storage.popContainer()
1038+
}
1039+
}
1040+

0 commit comments

Comments
 (0)