Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Improvements in Entry.resolveLinks #417

Merged
merged 8 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions Sources/Contentful/ArrayResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ private protocol HomogeneousArray: Array {
public struct ArrayResponseError: Decodable {
/// The system fields of the error.
public struct Sys: Decodable {
/// The identifer of the error.
/// The identifier of the error.
public let id: String
/// The type identifier for the error.
public let type: String
Expand Down Expand Up @@ -111,7 +111,7 @@ extension HomogeneousArrayResponse: Decodable {

if areItemsOfCustomType {
// Since self's items are of a custom (i.e. user-defined) type,
// we must accomodate the scenario that included Entries are
// we must accommodate the scenario that included Entries are
// heterogeneous, since items can link to whatever custom Entries
// the user defined.
// As a consequence, we have to use the LinkResolver in order to
Expand Down Expand Up @@ -182,7 +182,7 @@ extension HomogeneousArrayResponse: Decodable {
entriesMap[entry.sys.id] = entry
}

// Rememember `Entry`s are classes (passed by reference) so we can change them in place.
// Remember `Entry`s are classes (passed by reference) so we can change them in place.
for entry in allIncludedEntries {
entry.resolveLinks(against: entriesMap, and: assetsMap)
}
Expand Down
65 changes: 36 additions & 29 deletions Sources/Contentful/Client+Sync.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extension Client {
in order to allow chaining of operations.

- Parameters:
- syncSpace: Instance to perform subsqeuent sync on. Empty instance by default.
- syncSpace: Instance to perform subsequent sync on. Empty instance by default.
- syncableTypes: The types that can be synchronized.
- completion: The completion handler to call when the operation is complete.
*/
Expand All @@ -30,46 +30,53 @@ extension Client {
// Preview mode only supports `initialSync` not `nextSync`. The only reason `nextSync` should
// be called while in preview mode, is internally by the SDK to finish a multiple page sync.
// We are doing a multi page sync only when syncSpace.hasMorePages is true.
if !syncSpace.syncToken.isEmpty, host == Host.preview, syncSpace.hasMorePages == false {
guard !(host == Host.preview && !syncSpace.syncToken.isEmpty && !syncSpace.hasMorePages) else {
completion(.failure(SDKError.previewAPIDoesNotSupportSync))
return nil
}

// Send only sync space parameters when accessing another page.
let parameters: [String: String]
if syncSpace.hasMorePages {
parameters = syncSpace.parameters
} else {
parameters = syncableTypes.parameters + syncSpace.parameters
}
let parameters = syncSpace.hasMorePages
? syncSpace.parameters
: (syncableTypes.parameters + syncSpace.parameters)

return fetchDecodable(
url: url(
endpoint: .sync,
parameters: parameters
)
) { (result: Result<SyncSpace, Error>) in
return fetchDecodable(url: url(endpoint: .sync, parameters: parameters)) { (result: Result<SyncSpace, Error>) in
switch result {
case let .success(newSyncSpace):
syncSpace.updateWithDiffs(from: newSyncSpace)

if newSyncSpace.hasMorePages {
self.sync(for: syncSpace, syncableTypes: syncableTypes, then: completion)
} else {
_ = self.fetchContentTypes { contentTypeResult in
switch contentTypeResult {
case let .success(contentTypes):
for entry in syncSpace.entries {
let type = contentTypes.first { $0.sys.id == entry.sys.contentTypeId }
entry.type = type
}
self.persistenceIntegration?.update(with: syncSpace)
completion(.success(syncSpace))
case let .failure(error):
completion(.failure(error))
}
// Continue syncing if there are more pages
guard newSyncSpace.hasMorePages else {
self.handleContentTypeFetch(for: syncSpace, completion: completion)
return
}

// Recursive call to continue syncing
self.sync(for: syncSpace, syncableTypes: syncableTypes, then: completion)

case let .failure(error):
completion(.failure(error))
}
}
}

private func handleContentTypeFetch(
for syncSpace: SyncSpace,
completion: @escaping ResultsHandler<SyncSpace>
) {
_ = self.fetchContentTypes { result in
switch result {
case let .success(contentTypes):
for entry in syncSpace.entries {
// Assign content type to each entry in the sync space
entry.type = contentTypes.first { contentType in
contentType.sys.id == entry.sys.contentTypeId
}
}

self.persistenceIntegration?.update(with: syncSpace)
completion(.success(syncSpace))

case let .failure(error):
completion(.failure(error))
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/Contentful/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -488,10 +488,11 @@ open class Client {
} else {
let error = SDKError.unparseableJSON(
data: data,
errorMessage: "Unknown error occured during decoding."
errorMessage: "Unknown error occurred during decoding."
)
ContentfulLogger.log(.error, message: error.message)
return .failure(error)
}
}
}

2 changes: 1 addition & 1 deletion Sources/Contentful/Decodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public extension Decoder {
guard let contentTypes = userInfo[.contentTypesContextKey] as? [ContentTypeId: EntryDecodable.Type] else {
fatalError(
"""
Make sure to pass your content types into the Client intializer
Make sure to pass your content types into the Client initializer
so the SDK can properly deserializer your own types if you are using the `fetchMappedEntries` methods
""")
}
Expand Down
40 changes: 32 additions & 8 deletions Sources/Contentful/Entry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,37 @@ public class Entry: LocalizableResource {
// those that are of type Link.
for (localeCode, fieldValueForLocaleCode) in localizableFieldMap {
switch fieldValueForLocaleCode {
case let oneToOneLink as Link where oneToOneLink.isResolved == false:
let resolvedLink = oneToOneLink.resolve(against: includedEntries, and: includedAssets)
resolvedLocalizableFieldMap[localeCode] = resolvedLink
case let oneToManyLinks as [Link]:

// If the links do not need resolution, we can skip the resolution
guard oneToManyLinks.needsResolution else {
resolvedLocalizableFieldMap[localeCode] = oneToManyLinks
continue
}

// Check if the links are already cached
if let resolvedLinks = SyncSpace.cachedLinks[id + fieldName] {
resolvedLocalizableFieldMap[localeCode] = resolvedLinks
continue
}

// Resolve all links
let resolvedLinks = oneToManyLinks.map { link -> Link in
if link.isResolved {
return link
} else {
return link.resolve(against: includedEntries, and: includedAssets)
}
link.isResolved
? link
: link.resolve(against: includedEntries, and: includedAssets)
}

resolvedLocalizableFieldMap[localeCode] = resolvedLinks
SyncSpace.cachedLinks[id + fieldName] = resolvedLinks

case let oneToOneLink as Link where oneToOneLink.isResolved == false:
let resolvedLink = oneToOneLink.resolve(against: includedEntries, and: includedAssets)
resolvedLocalizableFieldMap[localeCode] = resolvedLink

case let recursiveNode as RecursiveNode:
recursiveNode.resolveLinks(against: includedEntries, and: includedAssets)

default:
continue
}
Expand All @@ -109,3 +126,10 @@ extension Entry: ResourceQueryable {
/// The QueryType for an EntryQuery is Query.
public typealias QueryType = Query
}

extension [Link] {
/// Needs resolution if any of the links in the array need resolution
var needsResolution: Bool {
return contains { $0.isResolved == false }
}
}
12 changes: 12 additions & 0 deletions Sources/Contentful/Link.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,15 @@ public enum Link: Codable {
case sys
}
}

// MARK: - Hashable, Equatable

extension Link: Hashable, Equatable {
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}

public static func == (lhs: Link, rhs: Link) -> Bool {
return lhs.id == rhs.id
}
}
3 changes: 3 additions & 0 deletions Sources/Contentful/SyncSpace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@ public final class SyncSpace: Decodable {
case items
}

static var cachedLinks = [String: [Link]]()

func updateWithDiffs(from syncSpace: SyncSpace) {
Self.cachedLinks = [:]
// Resolve all entries in-memory.
for entry in entries {
entry.resolveLinks(against: entriesMap, and: assetsMap)
Expand Down