Skip to content

Commit 71701ea

Browse files
author
JP Wright
committed
Refactor decoding of heterogenous EntryDecodables collections
1 parent 7abb512 commit 71701ea

File tree

3 files changed

+54
-76
lines changed

3 files changed

+54
-76
lines changed

Sources/Contentful/ArrayResponse.swift

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -154,41 +154,25 @@ internal struct MappedIncludes: Decodable {
154154
}
155155

156156
init(from decoder: Decoder) throws {
157-
let container = try decoder.container(keyedBy: CodingKeys.self)
158-
159-
assets = try container.decodeIfPresent([Asset].self, forKey: CodingKeys.assets)
157+
let container = try decoder.container(keyedBy: CodingKeys.self)
158+
assets = try container.decodeIfPresent([Asset].self, forKey: .assets)
159+
entries = try container.decodeHeterogeneousEntries(forKey: .entries,
160+
contentTypes: decoder.contentTypes,
161+
throwIfNotPresent: false)
160162
// Cache to enable link resolution.
161163
if let assets = assets {
162164
decoder.linkResolver.cache(assets: assets)
163165
}
164-
165-
// A copy as an array of dictionaries just to extract "sys.type" field.
166-
guard let jsonItems = try container.decodeIfPresent(Swift.Array<Any>.self, forKey: .entries) as? [[String: Any]] else {
167-
self.entries = nil
168-
return
169-
}
170-
var entriesJSONContainer = try container.nestedUnkeyedContainer(forKey: .entries)
171-
var entries: [EntryDecodable] = []
172-
let contentTypes = decoder.userInfo[DecoderContext.contentTypesContextKey] as! [ContentTypeId: EntryDecodable.Type]
173-
174-
while entriesJSONContainer.isAtEnd == false {
175-
let contentTypeInfo = try jsonItems.contentTypeInfo(at: entriesJSONContainer.currentIndex)
176-
177-
// For includes, if the type of this entry isn't defined by the user, we skip serialization.
178-
if let type = contentTypes[contentTypeInfo.id] {
179-
let entryModellable = try type.popEntryDecodable(from: &entriesJSONContainer)
180-
entries.append(entryModellable)
181-
}
182-
}
183-
self.entries = entries
184-
185166
// Cache to enable link resolution.
186-
if let entries = self.entries {
167+
if let entries = entries {
187168
decoder.linkResolver.cache(entryDecodables: entries)
188169
}
189170
}
190171
}
191172

173+
// Empty type so that we can continue to the end of a UnkeyedContainer
174+
internal struct EmptyDecodable: Decodable {}
175+
192176
extension MappedArrayResponse: Decodable {
193177

194178
public init(from decoder: Decoder) throws {
@@ -282,25 +266,9 @@ extension MixedMappedArrayResponse: Decodable {
282266

283267
// All items and includes.
284268
includes = try container.decodeIfPresent(MappedIncludes.self, forKey: .includes)
285-
286-
// A copy as an array of dictionaries just to extract "sys.type" field.
287-
guard let jsonItems = try container.decode(Swift.Array<Any>.self, forKey: .items) as? [[String: Any]] else {
288-
throw SDKError.unparseableJSON(data: nil, errorMessage: "SDK was unable to serialize returned resources")
289-
}
290-
var entriesJSONContainer = try container.nestedUnkeyedContainer(forKey: .items)
291-
var entries: [EntryDecodable] = []
292-
let contentTypes = decoder.userInfo[DecoderContext.contentTypesContextKey] as! [ContentTypeId: EntryDecodable.Type]
293-
294-
while entriesJSONContainer.isAtEnd == false {
295-
let contentTypeInfo = try jsonItems.contentTypeInfo(at: entriesJSONContainer.currentIndex)
296-
297-
// After implementing handling of the errors array, we can append an SDKError when the type isn't found.
298-
if let entryDecodableType = contentTypes[contentTypeInfo.id] {
299-
let entryDecodable = try entryDecodableType.popEntryDecodable(from: &entriesJSONContainer)
300-
entries.append(entryDecodable)
301-
}
302-
}
303-
self.items = entries
269+
items = try container.decodeHeterogeneousEntries(forKey: .items,
270+
contentTypes: decoder.contentTypes,
271+
throwIfNotPresent: true) ?? []
304272

305273
// Cache to enable link resolution.
306274
decoder.linkResolver.cache(entryDecodables: self.items)
@@ -321,3 +289,37 @@ internal extension Swift.Array where Element == Dictionary<String, Any> {
321289
return contentTypeInfo
322290
}
323291
}
292+
293+
extension KeyedDecodingContainer {
294+
295+
internal func decodeHeterogeneousEntries(forKey key: K,
296+
contentTypes: [ContentTypeId: EntryDecodable.Type],
297+
throwIfNotPresent: Bool) throws -> [EntryDecodable]? {
298+
299+
300+
guard let itemsAsDictionaries = try self.decodeIfPresent(Swift.Array<Any>.self, forKey: key) as? [[String: Any]] else {
301+
if throwIfNotPresent {
302+
throw SDKError.unparseableJSON(data: nil, errorMessage: "SDK was unable to serialize returned resources")
303+
} else {
304+
return nil
305+
}
306+
}
307+
var entriesJSONContainer = try self.nestedUnkeyedContainer(forKey: key)
308+
309+
var entries: [EntryDecodable] = []
310+
while entriesJSONContainer.isAtEnd == false {
311+
let contentTypeInfo = try itemsAsDictionaries.contentTypeInfo(at: entriesJSONContainer.currentIndex)
312+
313+
// For includes, if the type of this entry isn't defined by the user, we skip serialization.
314+
if let type = contentTypes[contentTypeInfo.id] {
315+
let entryModellable = try type.popEntryDecodable(from: &entriesJSONContainer)
316+
entries.append(entryModellable)
317+
} else {
318+
// Another annoying workaround: there is no mechanism for incrementing the `currentIndex` of an
319+
// UnkeyedCodingContainer other than actually decoding an item
320+
_ = try? entriesJSONContainer.decode(EmptyDecodable.self)
321+
}
322+
}
323+
return entries
324+
}
325+
}

Sources/Contentful/Decodable.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public extension Decoder {
3232
return userInfo[DecoderContext.linkResolverContextKey] as! LinkResolver
3333
}
3434

35+
internal var contentTypes: [ContentTypeId: EntryDecodable.Type] {
36+
return userInfo[DecoderContext.contentTypesContextKey] as! [ContentTypeId: EntryDecodable.Type]
37+
}
38+
3539
/// Helper method to extract the sys property of a Contentful resource.
3640
public func sys() throws -> Sys {
3741
let container = try self.container(keyedBy: LocalizableResource.CodingKeys.self)
@@ -178,7 +182,7 @@ internal class LinkResolver {
178182
let onlyKeysString = linkKey[firstKeyIndex ..< linkKey.endIndex]
179183
// Split creates a [Substring] array, but we need [String] to index the cache
180184
let keys = onlyKeysString.split(separator: ",").map { String($0) }
181-
let items: [Any] = keys.map { dataCache.item(for: $0) as Any }
185+
let items: [Any] = keys.flatMap { dataCache.item(for: $0) }
182186
for callback in callbacksList {
183187
callback(items as Any)
184188
}

Tests/ContentfulTests/QueryTests.swift

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -93,42 +93,13 @@ final class Dog: EntryDecodable, ResourceQueryable {
9393
}
9494
}
9595

96-
class Human: EntryDecodable, ResourceQueryable {
97-
98-
static let contentTypeId = "human"
99-
100-
let sys: Sys
101-
let name: String?
102-
let description: String?
103-
let likes: [String]?
104-
105-
var image: Asset?
106-
107-
public required init(from decoder: Decoder) throws {
108-
sys = try decoder.sys()
109-
let fields = try decoder.contentfulFieldsContainer(keyedBy: Human.Fields.self)
110-
name = try fields.decode(String.self, forKey: .name)
111-
description = try fields.decode(String.self, forKey: .description)
112-
likes = try fields.decode(Array<String>.self, forKey: .likes)
113-
114-
try fields.resolveLink(forKey: .image, decoder: decoder) { [weak self] linkedImage in
115-
self?.image = linkedImage as? Asset
116-
}
117-
}
118-
119-
enum Fields: String, CodingKey {
120-
case name, description, likes, image
121-
}
122-
}
123-
12496
class QueryTests: XCTestCase {
12597

12698
static let client: Client = {
12799
let contentTypeClasses: [EntryDecodable.Type] = [
128100
Cat.self,
129101
Dog.self,
130-
City.self,
131-
Human.self
102+
City.self
132103
]
133104
return TestClientFactory.testClient(withCassetteNamed: "QueryTests", contentTypeClasses: contentTypeClasses)
134105
}()
@@ -193,7 +164,8 @@ class QueryTests: XCTestCase {
193164
switch result {
194165
case .success(let response):
195166
let entries = response.items
196-
expect(entries.count).to(equal(10))
167+
// We didn't decode the "human" content type so only 9 decoded entries should be returned instead of 10
168+
expect(entries.count).to(equal(9))
197169

198170
if let cat = entries.first as? Cat, let bestFriend = cat.bestFriend {
199171
expect(bestFriend.name).to(equal("Nyan Cat"))

0 commit comments

Comments
 (0)