From 0d026117d1a701c6de7381192a6df08717ea8181 Mon Sep 17 00:00:00 2001 From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com> Date: Mon, 18 Nov 2024 12:40:36 +0100 Subject: [PATCH 1/7] typos --- Sources/Contentful/ArrayResponse.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Contentful/ArrayResponse.swift b/Sources/Contentful/ArrayResponse.swift index c2c15f80..75d2e05d 100644 --- a/Sources/Contentful/ArrayResponse.swift +++ b/Sources/Contentful/ArrayResponse.swift @@ -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 @@ -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 @@ -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) } From 95b14c9da1d08296c4c30f0bc08c3c2a538fbcb2 Mon Sep 17 00:00:00 2001 From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com> Date: Mon, 18 Nov 2024 12:41:02 +0100 Subject: [PATCH 2/7] typos --- Sources/Contentful/Client.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Contentful/Client.swift b/Sources/Contentful/Client.swift index 97e360cb..9a5627ca 100644 --- a/Sources/Contentful/Client.swift +++ b/Sources/Contentful/Client.swift @@ -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) } } } + From d9b850f007035ada67c6292f41cd50943c6c0f5e Mon Sep 17 00:00:00 2001 From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com> Date: Mon, 18 Nov 2024 12:41:14 +0100 Subject: [PATCH 3/7] typo --- Sources/Contentful/Decodable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Contentful/Decodable.swift b/Sources/Contentful/Decodable.swift index e805272f..f0477297 100644 --- a/Sources/Contentful/Decodable.swift +++ b/Sources/Contentful/Decodable.swift @@ -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 """) } From 9b0c903ebf178ab5d9c1a5302178a9fd7afff530 Mon Sep 17 00:00:00 2001 From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com> Date: Mon, 18 Nov 2024 12:42:24 +0100 Subject: [PATCH 4/7] improved logic --- Sources/Contentful/Entry.swift | 41 ++++++++++++++++++++++++------ Sources/Contentful/Link.swift | 13 +++++++++- Sources/Contentful/SyncSpace.swift | 3 +++ 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/Sources/Contentful/Entry.swift b/Sources/Contentful/Entry.swift index 23384e4b..5fd6c024 100644 --- a/Sources/Contentful/Entry.swift +++ b/Sources/Contentful/Entry.swift @@ -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.needsResolution + ? link + : link.resolve(against: includedEntries, and: includedAssets) } + resolvedLocalizableFieldMap[localeCode] = resolvedLinks + SyncSpace.cachedLinks[id + fieldName] = resolvedLinks + + case let oneToOneLink as Link where oneToOneLink.needsResolution == 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 } @@ -109,3 +126,11 @@ 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 + /// or if the array is empty. + var needsResolution: Bool { + return isEmpty || contains { $0.needsResolution } + } +} diff --git a/Sources/Contentful/Link.swift b/Sources/Contentful/Link.swift index ba5df315..c76e68b8 100644 --- a/Sources/Contentful/Link.swift +++ b/Sources/Contentful/Link.swift @@ -107,7 +107,7 @@ public enum Link: Codable { return sys } - var isResolved: Bool { + var needsResolution: Bool { switch self { case .asset, .entry, .entryDecodable: return true case .unresolved: return false @@ -176,3 +176,14 @@ public enum Link: Codable { case sys } } + +// MARK: - Hashable, Equatable +extension Link: Hashable, Equatable { + public var hashValue: Int { + return id.hashValue + } + + public static func == (lhs: Link, rhs: Link) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/Sources/Contentful/SyncSpace.swift b/Sources/Contentful/SyncSpace.swift index b3590c81..f257dbed 100755 --- a/Sources/Contentful/SyncSpace.swift +++ b/Sources/Contentful/SyncSpace.swift @@ -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) From cb668e93460353754b234535343c76ea625d84e3 Mon Sep 17 00:00:00 2001 From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com> Date: Mon, 18 Nov 2024 12:43:59 +0100 Subject: [PATCH 5/7] extracted function to improve readability --- Sources/Contentful/Client+Sync.swift | 65 +++++++++++++++------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/Sources/Contentful/Client+Sync.swift b/Sources/Contentful/Client+Sync.swift index e13629b9..03b788ab 100755 --- a/Sources/Contentful/Client+Sync.swift +++ b/Sources/Contentful/Client+Sync.swift @@ -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. */ @@ -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)) } From efac50e40a2ae265e138befab010980cbf023514 Mon Sep 17 00:00:00 2001 From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com> Date: Mon, 18 Nov 2024 12:52:28 +0100 Subject: [PATCH 6/7] corrected logic --- Sources/Contentful/Link.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Contentful/Link.swift b/Sources/Contentful/Link.swift index c76e68b8..61b9a0b3 100644 --- a/Sources/Contentful/Link.swift +++ b/Sources/Contentful/Link.swift @@ -109,8 +109,8 @@ public enum Link: Codable { var needsResolution: Bool { switch self { - case .asset, .entry, .entryDecodable: return true - case .unresolved: return false + case .asset, .entry, .entryDecodable: return false + case .unresolved: return true } } From da35f18f224271c1cc16c9f7c8fa9131c626bcba Mon Sep 17 00:00:00 2001 From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com> Date: Mon, 18 Nov 2024 21:12:39 +0100 Subject: [PATCH 7/7] reverted breaking changes --- Sources/Contentful/Entry.swift | 7 +++---- Sources/Contentful/Link.swift | 13 +++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/Contentful/Entry.swift b/Sources/Contentful/Entry.swift index 5fd6c024..318bf02b 100644 --- a/Sources/Contentful/Entry.swift +++ b/Sources/Contentful/Entry.swift @@ -92,7 +92,7 @@ public class Entry: LocalizableResource { // Resolve all links let resolvedLinks = oneToManyLinks.map { link -> Link in - link.needsResolution + link.isResolved ? link : link.resolve(against: includedEntries, and: includedAssets) } @@ -100,7 +100,7 @@ public class Entry: LocalizableResource { resolvedLocalizableFieldMap[localeCode] = resolvedLinks SyncSpace.cachedLinks[id + fieldName] = resolvedLinks - case let oneToOneLink as Link where oneToOneLink.needsResolution == false: + case let oneToOneLink as Link where oneToOneLink.isResolved == false: let resolvedLink = oneToOneLink.resolve(against: includedEntries, and: includedAssets) resolvedLocalizableFieldMap[localeCode] = resolvedLink @@ -129,8 +129,7 @@ extension Entry: ResourceQueryable { extension [Link] { /// Needs resolution if any of the links in the array need resolution - /// or if the array is empty. var needsResolution: Bool { - return isEmpty || contains { $0.needsResolution } + return contains { $0.isResolved == false } } } diff --git a/Sources/Contentful/Link.swift b/Sources/Contentful/Link.swift index 61b9a0b3..8c618be1 100644 --- a/Sources/Contentful/Link.swift +++ b/Sources/Contentful/Link.swift @@ -107,10 +107,10 @@ public enum Link: Codable { return sys } - var needsResolution: Bool { + var isResolved: Bool { switch self { - case .asset, .entry, .entryDecodable: return false - case .unresolved: return true + case .asset, .entry, .entryDecodable: return true + case .unresolved: return false } } @@ -178,11 +178,12 @@ public enum Link: Codable { } // MARK: - Hashable, Equatable + extension Link: Hashable, Equatable { - public var hashValue: Int { - return id.hashValue + public func hash(into hasher: inout Hasher) { + hasher.combine(id) } - + public static func == (lhs: Link, rhs: Link) -> Bool { return lhs.id == rhs.id }