├── .env ├── Config.xcconfig ├── .github ├── CODEOWNERS └── workflows │ └── codeql.yml ├── Cartfile ├── Scripts ├── .env ├── Config.xcconfig ├── set-version.sh ├── BuildPhases │ └── swiftlint.sh ├── travis-build-test.sh ├── release.sh ├── reference-docs.sh └── setup-env.sh ├── Cartfile.private ├── docs ├── img │ ├── gh.png │ ├── carat.png │ ├── dash.png │ └── spinner.gif ├── docsets │ ├── ContentfulPersistence.tgz │ └── ContentfulPersistence.docset │ │ └── Contents │ │ ├── Resources │ │ ├── docSet.dsidx │ │ └── Documents │ │ │ ├── img │ │ │ ├── dash.png │ │ │ ├── gh.png │ │ │ ├── carat.png │ │ │ └── spinner.gif │ │ │ ├── js │ │ │ ├── jazzy.js │ │ │ └── jazzy.search.js │ │ │ └── css │ │ │ └── highlight.css │ │ └── Info.plist ├── badge.svg ├── js │ ├── jazzy.js │ └── jazzy.search.js ├── css │ └── highlight.css └── undocumented.json ├── Cartfile.resolved ├── Screenshots ├── Asset.png ├── Product.png ├── SyncInfo.png └── CoreDataOptionality.png ├── renovate.json ├── .slather.yml ├── Gemfile ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Tests └── ContentfulPersistenceTests │ ├── BundledPreseededDatabaseTests │ ├── Test.sqlite │ └── CoreDataStorePreseedTests.swift │ ├── PreseedJSONFiles │ ├── cache_2ReMHJhXoAcy4AyamgsgwQ.jpg │ ├── cache_2xA3oKlZTuQ0Wgs2Wm2Mkk.jpeg │ ├── cache_3S1ngcWajSia6I4sssQwyK.jpg │ ├── cache_5Q6yYElPe8w8AEsKeki4M4.png │ ├── cache_6JCShApjO0O4CUkUKAKAaS.png │ ├── cache_bXvdSYHB3Guy2uUmuEco8.gif │ └── locales.json │ ├── Mocks │ ├── MockSpacePersistable.swift │ ├── MockURLProtocol.swift │ └── MockPersistenceStore.swift │ ├── TestModels │ ├── Asset.swift │ ├── Post.swift │ ├── Author.swift │ ├── Category.swift │ ├── SyncInfo.swift │ ├── SyncInfo+CoreDataProperties.swift │ ├── Category+CoreDataProperties.swift │ ├── Asset+CoreDataProperties.swift │ ├── Author+CoreDataProperties.swift │ └── Post+CoreDataProperties.swift │ ├── ComplexTestModels │ ├── ComplexAsset.swift │ ├── Link.swift │ ├── RichTextDocumentRecord.swift │ ├── ComplexSyncInfo.swift │ ├── SingleRecord.swift │ ├── RecordWithNonOptionalRelation.swift │ ├── ComplexSyncInfo+CoreDataProperties.swift │ ├── Link+CoreDataProperties.swift │ ├── RecordWithNonOptionalRelation+CoreDataProperties.swift │ ├── RichTextDocumentRecord+CoreDataProperties.swift │ ├── ComplexAsset+CoreDataProperties.swift │ └── SingleRecord+CoreDataProperties.swift │ ├── ContentStubs │ ├── space.json │ ├── single-author.json │ └── single-post.json │ ├── MultifilePreseedJSONFiles │ └── locales.json │ ├── MultilocalePreseedJSONFiles │ └── locales.json │ ├── Relationships │ ├── RelationshipChildIdTests.swift │ ├── RelationshipTests.swift │ ├── RelationshipCacheTests.swift │ └── RelationshipManagerTests.swift │ ├── Info.plist │ ├── ComplexTestStubs │ ├── deleted-asset-next.json │ ├── deleted-entry-next.json │ ├── nullified-link-next.json │ ├── simple-update-next-sync.json │ ├── clear-field-next-sync.json │ ├── deleted-entry-initial.json │ ├── multi-page-non-optional-link-resolution2.json │ ├── location.json │ ├── video-asset.json │ ├── deleted-asset-initial.json │ ├── symbols-array.json │ ├── multi-page-non-optional-link-resolution1.json │ ├── simple-update-initial-sync-page2.json │ ├── clear-field-initial-sync.json │ ├── shared-linked-asset.json │ ├── linked-assets-array.json │ ├── multi-page-link-resolution2.json │ └── now-resolvable-relationships.json │ ├── RichTextDocumentTransformableTest.xcdatamodeld │ └── RichTextDocumentTransformable.xcdatamodel │ │ └── contents │ ├── TestHelpers.swift │ ├── UnresolvedRelationshipCacheTests.swift │ ├── BatchSyncTests.swift │ └── RichTextDocumentTransformableTests.swift ├── .gitignore ├── ContentfulPersistence.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── ContentfulPersistence_watchOS.xcscheme │ ├── ContentfulPersistence_iOS.xcscheme │ ├── ContentfulPersistence_tvOS.xcscheme │ └── ContentfulPersistence_macOS.xcscheme ├── fastlane ├── Appfile ├── README.md └── Fastfile ├── .gitmodules ├── ContentfulPersistence.xcworkspace ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── Package.resolved ├── catalog-info.yaml ├── Package.swift ├── Sources └── ContentfulPersistence │ ├── Seeding │ └── BundledDatabase │ │ ├── PersistenceStore+Preseeding.swift │ │ ├── FileManaging.swift │ │ ├── PreseedStrategy.swift │ │ ├── CoreDataStore+Preseeding.swift │ │ ├── PreseedConfiguration.swift │ │ └── FilePreseedManager.swift │ ├── Relationships │ ├── RelationshipChildId.swift │ ├── RelationshipsManager.swift │ ├── RelationshipCache.swift │ ├── Relationship.swift │ └── RelationshipData.swift │ ├── DateFormatterCache.swift │ ├── PersistenceStore.swift │ ├── DataCache.swift │ └── Persistable.swift ├── Supporting Files ├── ContentfulPersistence.h ├── Info_tvOS.plist ├── Info_watchOS.plist ├── Info_iOS.plist └── Info_macOS.plist ├── .swiftlint.yml ├── Makefile ├── PrivacyInfo.xcprivacy ├── LICENSE ├── ContentfulPersistenceSwift.podspec ├── old-travis-integration.yml ├── .circleci └── config.yml └── Gemfile.lock /.env: -------------------------------------------------------------------------------- 1 | CONTENTFUL_PERSISTENCE_VERSION=0.18.1 2 | 3 | -------------------------------------------------------------------------------- /Config.xcconfig: -------------------------------------------------------------------------------- 1 | CONTENTFUL_PERSISTENCE_VERSION=0.18.1 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @contentful/team-developer-experience 2 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "contentful/contentful.swift" ~> 5.5.1 2 | 3 | -------------------------------------------------------------------------------- /Scripts/.env: -------------------------------------------------------------------------------- 1 | CONTENTFUL_PERSISTENCE_VERSION=0.18.1 2 | 3 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "mariuskatcontentful/OHHTTPStubs" ~> 9.0.0 2 | -------------------------------------------------------------------------------- /Scripts/Config.xcconfig: -------------------------------------------------------------------------------- 1 | CONTENTFUL_PERSISTENCE_VERSION=0.18.1 2 | 3 | -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/docs/img/spinner.gif -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "contentful/contentful.swift" "5.5.12" 2 | github "mariuskatcontentful/OHHTTPStubs" "9.1.0" 3 | -------------------------------------------------------------------------------- /Screenshots/Asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Screenshots/Asset.png -------------------------------------------------------------------------------- /Screenshots/Product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Screenshots/Product.png -------------------------------------------------------------------------------- /Screenshots/SyncInfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Screenshots/SyncInfo.png -------------------------------------------------------------------------------- /Screenshots/CoreDataOptionality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Screenshots/CoreDataOptionality.png -------------------------------------------------------------------------------- /docs/docsets/ContentfulPersistence.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/docs/docsets/ContentfulPersistence.tgz -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.slather.yml: -------------------------------------------------------------------------------- 1 | coverage_service: coveralls 2 | workspace: ContentfulPersistence.xcworkspace 3 | xcodeproj: ContentfulPersistence.xcodeproj 4 | scheme: "ContentfulPersistence_iOS" 5 | 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods' 4 | gem 'xcpretty' 5 | gem 'slather' 6 | gem 'jazzy' 7 | gem 'dotenv' 8 | 9 | gem 'rb-readline' 10 | gem "fastlane" -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/docsets/ContentfulPersistence.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/docs/docsets/ContentfulPersistence.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/BundledPreseededDatabaseTests/Test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Tests/ContentfulPersistenceTests/BundledPreseededDatabaseTests/Test.sqlite -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gutter.json 3 | *.gcda 4 | *.gcno 5 | *.xccheckout 6 | xcuserdata 7 | .idea 8 | Pods 9 | build 10 | .build 11 | Packages 12 | 13 | # Carthage 14 | Carthage/Build/ 15 | 16 | *.xcscmblueprint 17 | 18 | .envrc -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_2ReMHJhXoAcy4AyamgsgwQ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_2ReMHJhXoAcy4AyamgsgwQ.jpg -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_2xA3oKlZTuQ0Wgs2Wm2Mkk.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_2xA3oKlZTuQ0Wgs2Wm2Mkk.jpeg -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_3S1ngcWajSia6I4sssQwyK.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_3S1ngcWajSia6I4sssQwyK.jpg -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_5Q6yYElPe8w8AEsKeki4M4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_5Q6yYElPe8w8AEsKeki4M4.png -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_6JCShApjO0O4CUkUKAKAaS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_6JCShApjO0O4CUkUKAKAaS.png -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_bXvdSYHB3Guy2uUmuEco8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/Tests/ContentfulPersistenceTests/PreseedJSONFiles/cache_bXvdSYHB3Guy2uUmuEco8.gif -------------------------------------------------------------------------------- /docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /Scripts/set-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | echo "CONTENTFUL_PERSISTENCE_VERSION=$1" > Config.xcconfig 5 | echo "CONTENTFUL_PERSISTENCE_VERSION=$1" > .env 6 | echo "export CONTENTFUL_PERSISTENCE_VERSION=$1" > .envrc 7 | direnv allow 8 | 9 | -------------------------------------------------------------------------------- /docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-persistence.swift/master/docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/img/spinner.gif -------------------------------------------------------------------------------- /ContentfulPersistence.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | # app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app 2 | # apple_id("[[APPLE_ID]]") # Your Apple email address 3 | 4 | 5 | # For more information about the Appfile, see: 6 | # https://docs.fastlane.tools/advanced/#appfile 7 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/Mocks/MockSpacePersistable.swift: -------------------------------------------------------------------------------- 1 | @testable import ContentfulPersistence 2 | import Foundation 3 | 4 | class MockSyncSpacePersistable: SyncSpacePersistable { 5 | var syncToken: String? = nil 6 | var dbVersion: NSNumber? = nil 7 | } 8 | -------------------------------------------------------------------------------- /Scripts/BuildPhases/swiftlint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if $TRAVIS == true; then 4 | exit 0 5 | fi 6 | 7 | if which swiftlint >/dev/null; then 8 | swiftlint 9 | else 10 | echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" 11 | fi 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/OHHTTPStubs"] 2 | path = Carthage/Checkouts/OHHTTPStubs 3 | url = https://github.com/mariuskatcontentful/OHHTTPStubs.git 4 | [submodule "Carthage/Checkouts/contentful.swift"] 5 | path = Carthage/Checkouts/contentful.swift 6 | url = https://github.com/contentful/contentful.swift.git 7 | -------------------------------------------------------------------------------- /ContentfulPersistence.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/TestModels/Asset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Asset.swift 3 | // 4 | // 5 | // Created by Boris Bügling on 31/03/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Asset) 13 | class Asset: NSManagedObject { 14 | 15 | // Insert code here to add functionality to your managed object subclass 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/TestModels/Post.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post.swift 3 | // 4 | // 5 | // Created by Boris Bügling on 31/03/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Post) 13 | class Post: NSManagedObject { 14 | 15 | // Insert code here to add functionality to your managed object subclass 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/TestModels/Author.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Author.swift 3 | // 4 | // 5 | // Created by Boris Bügling on 31/03/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Author) 13 | class Author: NSManagedObject { 14 | 15 | // Insert code here to add functionality to your managed object subclass 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ContentfulPersistence.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/ComplexAsset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComplexAsset.swift 3 | // 4 | // 5 | // Created by JP Wright on 31/03/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(ComplexAsset) 13 | class ComplexAsset: NSManagedObject { 14 | 15 | // Insert code here to add functionality to your managed object subclass 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/TestModels/Category.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Category.swift 3 | // 4 | // 5 | // Created by Boris Bügling on 31/03/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import ContentfulPersistence 12 | 13 | @objc(Category) 14 | class Category: NSManagedObject { 15 | 16 | // Insert code here to add functionality to your managed object subclass 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/TestModels/SyncInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncInfo.swift 3 | // 4 | // 5 | // Created by Boris Bügling on 31/03/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import ContentfulPersistence 12 | 13 | @objc(SyncInfo) 14 | class SyncInfo: NSManagedObject { 15 | 16 | // Insert code here to add functionality to your managed object subclass 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Contentful", 6 | "repositoryURL": "https://github.com/contentful/contentful.swift", 7 | "state": { 8 | "branch": null, 9 | "revision": "265a67e67fb3a722f73bd298f0879f3878528ca8", 10 | "version": "5.5.1" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/Link.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Link.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by JP Wright on 12.07.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Link) 13 | class Link: NSManagedObject { 14 | // Insert code here to add functionality to your managed object subclass 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/RichTextDocumentRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextDocumentRecord.swift 3 | // 4 | // 5 | // Created by Manuel Maly on 23/07/19. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(RichTextDocumentRecord) 13 | class RichTextDocumentRecord: NSManagedObject { 14 | 15 | // Insert code here to add functionality to your managed object subclass 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ContentStubs/space.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Space", 4 | "id": "dqpnpm0n4e75" 5 | }, 6 | "name": "blog space", 7 | "locales": [ 8 | { 9 | "code": "en-US", 10 | "default": true, 11 | "name": "U.S. English", 12 | "fallbackCode": null 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/ComplexSyncInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComplexSyncInfo.swift 3 | // 4 | // 5 | // Created by JP Wright on 31/03/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import ContentfulPersistence 12 | 13 | @objc(ComplexSyncInfo) 14 | class ComplexSyncInfo: NSManagedObject { 15 | 16 | // Insert code here to add functionality to your managed object subclass 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/SingleRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleRecord.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by JP Wright on 12.07.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(SingleRecord) 13 | class SingleRecord: NSManagedObject { 14 | // Insert code here to add functionality to your managed object subclass 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/PreseedJSONFiles/locales.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "total": 1, 6 | "skip": 0, 7 | "limit": 1000, 8 | "items": [ 9 | { 10 | "code": "en-US", 11 | "name": "U.S. English", 12 | "default": true, 13 | "fallbackCode": null, 14 | "sys": { 15 | "id": "0iQEhldAjjRLh5Hrak03dH", 16 | "type": "Locale", 17 | "version": 0 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/MultifilePreseedJSONFiles/locales.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "total": 1, 6 | "skip": 0, 7 | "limit": 1000, 8 | "items": [ 9 | { 10 | "code": "en-US", 11 | "name": "U.S. English", 12 | "default": true, 13 | "fallbackCode": null, 14 | "sys": { 15 | "id": "0iQEhldAjjRLh5Hrak03dH", 16 | "type": "Locale", 17 | "version": 0 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/RecordWithNonOptionalRelation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordWithNonOptionalRelation.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Manuel Maly on 28.06.19. 6 | // Copyright © 2019 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(RecordWithNonOptionalRelation) 13 | class RecordWithNonOptionalRelation: NSManagedObject { 14 | // Insert code here to add functionality to your managed object subclass 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/TestModels/SyncInfo+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncInfo+CoreDataProperties.swift 3 | // 4 | // 5 | // Created by Boris Bügling on 31/03/16. 6 | // 7 | // 8 | // Choose "Create NSManagedObject Subclass…" from the Core Data editor menu 9 | // to delete and recreate this implementation file for your updated model. 10 | // 11 | 12 | import Foundation 13 | import CoreData 14 | import ContentfulPersistence 15 | 16 | extension SyncInfo: SyncSpacePersistable { 17 | 18 | @NSManaged var syncToken: String? 19 | 20 | @NSManaged var dbVersion: NSNumber? 21 | } 22 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: contentful-persistence.swift 5 | description: An integration to simplify persisting data from Contentful to a local CoreData database; built on top of the official Contentful Swift Library. 6 | annotations: 7 | github.com/project-slug: contentful/contentful-persistence.swift 8 | contentful.com/service-tier: "4" 9 | contentful.com/ci-alert-slack: prd-ecosystem-dx-bots 10 | tags: 11 | - tier-4 12 | spec: 13 | type: library 14 | lifecycle: production 15 | owner: group:team-developer-experience 16 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/ComplexSyncInfo+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComplexSyncInfo+CoreDataProperties.swift 3 | // 4 | // 5 | // Created by JP Wright on 31/03/16. 6 | // 7 | // 8 | // Choose "Create NSManagedObject Subclass…" from the Core Data editor menu 9 | // to delete and recreate this implementation file for your updated model. 10 | // 11 | 12 | import Foundation 13 | import CoreData 14 | import ContentfulPersistence 15 | 16 | extension ComplexSyncInfo: SyncSpacePersistable { 17 | 18 | @NSManaged var syncToken: String? 19 | 20 | @NSManaged var dbVersion: NSNumber? 21 | } 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ContentfulPersistence", 6 | products: [ 7 | .library( 8 | name: "ContentfulPersistence", 9 | targets: ["ContentfulPersistence"]) 10 | ], 11 | dependencies: [ 12 | .package(url: "https://github.com/contentful/contentful.swift", .upToNextMajor(from: "5.5.13")) 13 | ], 14 | targets: [ 15 | .target( 16 | name: "ContentfulPersistence", 17 | dependencies: [ 18 | "Contentful" 19 | ]) 20 | ] 21 | 22 | ) 23 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Seeding/BundledDatabase/PersistenceStore+Preseeding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreseedableStore.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Marius Kurgonas on 30/05/2025. 6 | // Copyright © 2025 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension PersistenceStore { 12 | /// Default: no-op. Override to remove side-car files, reset contexts, and remove the store. 13 | func onStorePreseedingWillBegin(at storeFileURL: URL) throws { } 14 | /// Default: no-op. Override to re-add or re-open the store. 15 | func onStorePreseedingCompleted(at seededFileURL: URL) throws { } 16 | } 17 | -------------------------------------------------------------------------------- /Scripts/travis-build-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x -o pipefail 4 | 5 | WORKSPACE=ContentfulPersistence.xcworkspace 6 | 7 | echo "Building" 8 | 9 | rm -rf ${HOME}/Library/Developer/Xcode/DerivedData/* 10 | 11 | if [[ "$SWIFT_BUILD" == "true" ]]; then 12 | swift build 13 | exit 0 14 | fi 15 | 16 | # -jobs -- specify the number of concurrent jobs 17 | # `sysctl -n hw.ncpu` -- fetch number of 'logical' cores in macOS machine 18 | xcodebuild -jobs `sysctl -n hw.ncpu` test -workspace ${WORKSPACE} -scheme ${SCHEME} \ 19 | -sdk ${SDK} -destination "platform=${PLATFORM}" ONLY_ACTIVE_ARCH=YES CODE_SIGNING_IDENTITY="" CODE_SIGNING_REQUIRED=NO | bundle exec xcpretty -c 20 | 21 | -------------------------------------------------------------------------------- /Supporting Files/ContentfulPersistence.h: -------------------------------------------------------------------------------- 1 | // 2 | // ContentfulPersistence.h 3 | // ContentfulPersistence 4 | // 5 | // Created by JP Wright on 06/02/2017. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ContentfulPersistence 12 | FOUNDATION_EXPORT double ContentfulPersistenceVersionNumber; 13 | 14 | //! Project version string for ContentfulPersistence. 15 | FOUNDATION_EXPORT const unsigned char ContentfulPersistenceVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | -------------------------------------------------------------------------------- /Scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | source .env 4 | 5 | echo "Making release for version $CONTENTFUL_PERSISTENCE_VERSION of the persistence library" 6 | 7 | git tag $CONTENTFUL_PERSISTENCE_VERSION 8 | git push --tags 9 | bundle exec pod trunk push ContentfulPersistenceSwift.podspec --allow-warnings 10 | make carthage 11 | git stash --all 12 | git checkout gh-pages 13 | git rebase master 14 | ./Scripts/reference-docs.sh 15 | git add . 16 | git commit --amend --no-edit 17 | git push -f 18 | git checkout master 19 | git stash pop 20 | 21 | echo "ContentfulPersistence v$CONTENTFUL_PERSISTENCE_VERSION is officially released! Attach the binary found at ContentfulPersistence.framework.zip to the release on Github" 22 | 23 | -------------------------------------------------------------------------------- /Scripts/reference-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | source .env 4 | 5 | echo "Generating Jazzy Reference Documentation for version $CONTENTFUL_PERSISTENCE_VERSION of the persistence library" 6 | 7 | bundle exec jazzy \ 8 | --clean \ 9 | --author Contentful \ 10 | --author_url https://www.contentful.com \ 11 | --github_url https://github.com/contentful/contentful-persistence.swift \ 12 | --github-file-prefix https://github.com/contentful/contentful-persistence.swift/tree/$CONTENTFUL_PERSISTENCE_VERSION \ 13 | --xcodebuild-arguments -workspace,ContentfulPersistence.xcworkspace,-scheme,ContentfulPersistence_iOS \ 14 | --module-version $CONTENTFUL_PERSISTENCE_VERSION \ 15 | --module ContentfulPersistence \ 16 | --theme apple 17 | 18 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/MultilocalePreseedJSONFiles/locales.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "total": 2, 6 | "skip": 0, 7 | "limit": 1000, 8 | "items": [ 9 | { 10 | "code": "en-US", 11 | "name": "U.S. English", 12 | "default": true, 13 | "fallbackCode": null, 14 | "sys": { 15 | "id": "3eBuGzfoQ0TIn8TZ8MTAEe", 16 | "type": "Locale", 17 | "version": 0 18 | } 19 | }, 20 | { 21 | "code": "es-MX", 22 | "name": "Spanish (Mexico)", 23 | "default": false, 24 | "fallbackCode": "en-US", 25 | "sys": { 26 | "id": "6YSd9VpjhlZHpyTeO0kRb8", 27 | "type": "Locale", 28 | "version": 0 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /docs/docsets/ContentfulPersistence.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.contentfulpersistence 7 | CFBundleName 8 | ContentfulPersistence 9 | DocSetPlatformFamily 10 | contentfulpersistence 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Seeding/BundledDatabase/FileManaging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManaging.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Marius Kurgonas on 30/05/2025. 6 | // Copyright © 2025 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Abstracts file I/O so you can inject a fake in tests. 12 | public protocol FileManaging { 13 | func fileExists(atPath path: String) -> Bool 14 | func removeItem(at url: URL) throws 15 | func createDirectory(at url: URL, 16 | withIntermediateDirectories createIntermediates: Bool, 17 | attributes: [FileAttributeKey: Any]?) throws 18 | func copyItem(at src: URL, to dst: URL) throws 19 | } 20 | 21 | extension FileManager: FileManaging {} 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "CodeQL Scan for GitHub Actions Workflows" 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | paths: [".github/workflows/**"] 8 | pull_request: 9 | branches: [master] 10 | paths: [".github/workflows/**"] 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze GitHub Actions workflows 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v3 26 | with: 27 | languages: actions 28 | 29 | - name: Run CodeQL Analysis 30 | uses: github/codeql-action/analyze@v3 31 | with: 32 | category: actions 33 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/Relationships/RelationshipChildIdTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentfulPersistence 3 | // 4 | 5 | @testable import ContentfulPersistence 6 | import XCTest 7 | 8 | class RelationshipChildIdTests: XCTestCase { 9 | 10 | func test_idAndLocale_areSet() { 11 | let id = "abc-def" 12 | let localeCode = "en-US" 13 | 14 | let value = "\(id)_\(localeCode)" 15 | 16 | let childId = RelationshipChildId(rawValue: value) 17 | XCTAssertEqual(childId.id, id) 18 | XCTAssertEqual(childId.localeCode, localeCode) 19 | } 20 | 21 | func test_id_isSet() { 22 | let id = "abc-def" 23 | 24 | let childId = RelationshipChildId(rawValue: id) 25 | XCTAssertEqual(childId.id, id) 26 | XCTAssertNil(childId.localeCode) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/Link+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Link+CoreDataProperties.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by JP Wright on 13.07.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import Contentful 12 | import ContentfulPersistence 13 | 14 | extension Link: EntryPersistable { 15 | 16 | static let contentTypeId = "link" 17 | 18 | @NSManaged var id: String 19 | @NSManaged var localeCode: String? 20 | @NSManaged var awesomeLinkTitle: String? 21 | @NSManaged var createdAt: Date? 22 | @NSManaged var updatedAt: Date? 23 | 24 | static func fieldMapping() -> [FieldName: String] { 25 | return [ 26 | "awesomeLinkTitle": "awesomeLinkTitle" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Scripts/setup-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # forward failure to the rest of the program 4 | 5 | which -s brew 6 | if [[ $? != 0 ]] ; then 7 | echo "ERROR: Homebrew must be installed on your machine in order to configure your environment" 8 | echo "for developing contentful.swift. Please visit https://brew.sh/ for installation instructions." 9 | exit 1 10 | else 11 | brew update 12 | fi 13 | 14 | if ! brew ls --versions carthage > /dev/null; then 15 | echo "Installing carthage via homebrew" 16 | brew install carthage 17 | fi 18 | 19 | if ! brew ls --versions swiftlint > /dev/null; then 20 | echo "Installing swiftlint via homebrew" 21 | brew install swiftlint 22 | fi 23 | 24 | # Update carthage and swiftlint 25 | brew outdated carthage || brew upgrade carthage 26 | brew outdated swiftlint || brew upgrade swiftlint 27 | 28 | bundle install 29 | 30 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Seeding/BundledDatabase/PreseedStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreseedStrategy.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Marius Kurgonas on 30/05/2025. 6 | // Copyright © 2025 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A hook for “seed my store from a bundled file” logic. 12 | public protocol PreseedStrategy { 13 | /** 14 | Perform the bundle-seed. 15 | 16 | - Parameters: 17 | - store: The `PersistenceStore` to seed. 18 | - config: Bundled resource info + target directory + version. 19 | - spaceType: Your `SyncSpacePersistable` type (for reading/writing `dbVersion`). 20 | */ 21 | func apply(to store: PersistenceStore, 22 | with config: PreseedConfiguration, 23 | spaceType: SyncSpacePersistable.Type) throws 24 | } 25 | -------------------------------------------------------------------------------- /Supporting Files/Info_tvOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Supporting Files/Info_watchOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/RecordWithNonOptionalRelation+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordWithNonOptionalRelation+CoreDataProperties.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Manuel Maly on 28.06.19. 6 | // Copyright © 2019 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import Contentful 12 | import ContentfulPersistence 13 | 14 | extension RecordWithNonOptionalRelation: EntryPersistable { 15 | 16 | static let contentTypeId = "recordWithNonOptionalRelation" 17 | 18 | @NSManaged var id: String 19 | @NSManaged var localeCode: String? 20 | @NSManaged var createdAt: Date? 21 | @NSManaged var updatedAt: Date? 22 | @NSManaged var nonOptionalLink: Link 23 | 24 | static func fieldMapping() -> [FieldName: String] { 25 | return [ 26 | "nonOptionalLink": "nonOptionalLink" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Supporting Files/Info_iOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Supporting Files/Info_macOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2017 Contentful GmbH. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/RichTextDocumentRecord+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextDocumentRecord+CoreDataProperties.swift 3 | // 4 | // 5 | // Created by Manuel Maly on 23/07/19. 6 | // 7 | // 8 | // Choose "Create NSManagedObject Subclass…" from the Core Data editor menu 9 | // to delete and recreate this implementation file for your updated model. 10 | // 11 | 12 | import Foundation 13 | import CoreData 14 | import Contentful 15 | import ContentfulPersistence 16 | 17 | extension RichTextDocumentRecord: EntryPersistable { 18 | 19 | static let contentTypeId = "richTextDocumentRecord" 20 | 21 | @NSManaged var id: String 22 | @NSManaged var createdAt: Date? 23 | @NSManaged var updatedAt: Date? 24 | @NSManaged var localeCode: String? 25 | @NSManaged var richTextDocument: RichTextDocument? 26 | 27 | static func fieldMapping() -> [FieldName: String] { 28 | return [ 29 | "richTextDocument": "richTextDocument" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | 2 | excluded: 3 | - Carthage 4 | - Packages 5 | - Tests 6 | - .build 7 | - build 8 | disabled_rules: 9 | - syntactic_sugar 10 | - type_body_length 11 | - type_name 12 | - cyclomatic_complexity 13 | - function_parameter_count 14 | - legacy_constant 15 | - force_cast 16 | - force_try 17 | # Rule that says says type that conforms to protocol must be a class. 18 | # If we are in pure swift, this is not necessary. 19 | - class_delegate_protocol 20 | - weak_delegate 21 | - vertical_parameter_alignment 22 | # ignoring since some file that exists on travis, but not locally is throwing this as a compile error 23 | # Also, swiftlint says it will be removed in future versions 24 | - identifier_name 25 | 26 | # Parameterized 27 | line_length: 28 | warning: 150 29 | ignores_comments: true 30 | ignores_urls: true 31 | file_length: 32 | warning: 800 33 | vertical_whitespace: 34 | max_empty_lines: 2 35 | function_body_length: 110 36 | #identifier_name: 37 | # min_length: 2 38 | # max_length: 30 39 | # 40 | 41 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/TestModels/Category+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Category+CoreDataProperties.swift 3 | // 4 | // 5 | // Created by Boris Bügling on 31/03/16. 6 | // 7 | // 8 | // Choose "Create NSManagedObject Subclass…" from the Core Data editor menu 9 | // to delete and recreate this implementation file for your updated model. 10 | // 11 | 12 | import Foundation 13 | import CoreData 14 | import Contentful 15 | import ContentfulPersistence 16 | 17 | extension Category: EntryPersistable { 18 | 19 | static let contentTypeId = "5KMiN6YPvi42icqAUQMCQe" 20 | 21 | @NSManaged var id: String 22 | @NSManaged var localeCode: String? 23 | @NSManaged var createdAt: Date? 24 | @NSManaged var updatedAt: Date? 25 | @NSManaged var title: String? 26 | @NSManaged var categoryInverse: NSSet? 27 | @NSManaged var icon: Asset? 28 | 29 | static func fieldMapping() -> [FieldName: String] { 30 | return [ 31 | "title": "title", 32 | "icon": "icon" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT=ContentfulPersistence.xcodeproj 2 | WORKSPACE=ContentfulPersistence.xcworkspace 3 | 4 | .PHONY: test setup lint coverage carthage clean open release 5 | 6 | open: 7 | open $(WORKSPACE) 8 | 9 | clean: 10 | rm -rf $(HOME)/Library/Developer/Xcode/DerivedData/* 11 | 12 | clean_simulators: kill_simulator 13 | xcrun simctl erase all 14 | 15 | kill_simulator: 16 | killall "Simulator" || true 17 | 18 | test: clean 19 | set -x -o pipefail && xcodebuild test -workspace $(WORKSPACE) \ 20 | -scheme ContentfulPersistence_macOS -destination 'platform=macOS' | bundle exec xcpretty -c 21 | 22 | setup_env: 23 | ./Scripts/setup-env.sh 24 | 25 | setup: 26 | bundle install 27 | git submodule sync 28 | git submodule update --init --recursive 29 | 30 | lint: 31 | swiftlint 32 | bundle exec pod lib lint ContentfulPersistenceSwift.podspec --verbose 33 | 34 | coverage: 35 | bundle exec slather coverage -s $(PROJECT) 36 | 37 | carthage: 38 | carthage build ContentfulPersistence --no-skip-current --platform all --use-xcframeworks 39 | 40 | release: 41 | ./Scripts/release.sh 42 | 43 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/ComplexAsset+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComplexAsset+CoreDataProperties.swift 3 | // 4 | // 5 | // Created by JP Wright on 31/03/16. 6 | // 7 | // 8 | // Choose "Create NSManagedObject Subclass…" from the Core Data editor menu 9 | // to delete and recreate this implementation file for your updated model. 10 | // 11 | 12 | import Foundation 13 | import CoreData 14 | import Contentful 15 | import ContentfulPersistence 16 | 17 | extension ComplexAsset: AssetPersistable { 18 | 19 | // FlatResource 20 | @NSManaged var id: String 21 | @NSManaged var localeCode: String? 22 | @NSManaged var createdAt: Date? 23 | @NSManaged var updatedAt: Date? 24 | 25 | // AssetPersistable 26 | @NSManaged var title: String? 27 | @NSManaged var assetDescription: String? 28 | @NSManaged var urlString: String? 29 | @NSManaged var fileType: String? 30 | @NSManaged var fileName: String? 31 | 32 | @NSManaged var size: NSNumber? 33 | @NSManaged var width: NSNumber? 34 | @NSManaged var height: NSNumber? 35 | } 36 | -------------------------------------------------------------------------------- /PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryUserDefaults 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | CA92.1 21 | 22 | 23 | 24 | NSPrivacyAccessedAPIType 25 | NSPrivacyAccessedAPICategorySystemBootTime 26 | NSPrivacyAccessedAPITypeReasons 27 | 28 | 35F9.1 29 | 30 | 31 | 32 | NSPrivacyCollectedDataTypes 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Contentful GmbH - https://www.contentful.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/deleted-asset-next.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "YokO2rWbOoo68QmiEUkqe", 16 | "type": "DeletedAsset", 17 | "createdAt": "2017-06-20T14:03:28.985Z", 18 | "updatedAt": "2017-07-13T09:49:17.125Z", 19 | "deletedAt": "2014-03-25T11:05:16.051Z", 20 | "revision": 3, 21 | "contentType": { 22 | "sys": { 23 | "type": "Link", 24 | "linkType": "ContentType", 25 | "id": "singleRecord" 26 | } 27 | } 28 | } 29 | } 30 | ], 31 | "nextSyncUrl": "https://cdn.contentful.com/spaces/smf0sqiu0c5s/sync?sync_token=deletion-next" 32 | } 33 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/deleted-entry-next.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "aNt2d7YR4AIwEAMcG4OwI", 16 | "type": "DeletedEntry", 17 | "createdAt": "2017-06-20T14:03:28.985Z", 18 | "updatedAt": "2017-07-13T09:49:17.125Z", 19 | "deletedAt": "2014-03-25T11:05:16.051Z", 20 | "revision": 3, 21 | "contentType": { 22 | "sys": { 23 | "type": "Link", 24 | "linkType": "ContentType", 25 | "id": "singleRecord" 26 | } 27 | } 28 | } 29 | } 30 | ], 31 | "nextSyncUrl": "https://cdn.contentful.com/spaces/smf0sqiu0c5s/sync?sync_token=deletion-next" 32 | } 33 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/nullified-link-next.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "5GiLOZvY7SiMeUIgIIAssS", 16 | "type": "Entry", 17 | "createdAt": "2017-06-20T14:03:29.319Z", 18 | "updatedAt": "2017-07-17T08:54:04.647Z", 19 | "revision": 4, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "singleRecord" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "textBody": { 30 | "en-US": "Record with nullified link" 31 | } 32 | } 33 | } 34 | ], 35 | "nextSyncUrl": "http://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=w5ZGw6JFwqZmVcKsE8Kow4grw45QdybCp3rDsSHDqVwfRcK9bHfDrBfDuMKrb8ONwqbDuXTDiEwDJRZWwqYyw5TDvMKRBgfDgXtYJMKOY0smw4HCg8OmH8Oaw6DDl0PCrcKJw4PCl8KEUUPCoMOHwrhuw4VeR2c" 36 | } 37 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/simple-update-next-sync.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "aNt2d7YR4AIwEAMcG4OwI", 16 | "type": "Entry", 17 | "createdAt": "2017-06-20T14:03:28.985Z", 18 | "updatedAt": "2017-07-13T13:08:34.383Z", 19 | "revision": 4, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "singleRecord" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "textBody": { 30 | "en-US": "Hello FooBar" 31 | } 32 | } 33 | } 34 | ], 35 | "nextSyncUrl": "https://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=w5ZGw6JFwqZmVcKsE8Kow4grw45QdybCpMOBFS3CksKJasKKw4HCriw3wofDhsKAXcKXQMOlZMKgw5AKHMKhw7zCmMOSc2ADwozCpAHDpsOmw4XDt0VPwrXCm3how4UII8OswrzDlMKDwqRewpQTw4oNwqBYw4RPXsKxLA" 36 | } 37 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/RichTextDocumentTransformableTest.xcdatamodeld/RichTextDocumentTransformable.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 83% 23 | 24 | 25 | 83% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/Relationships/RelationshipTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentfulPersistence 3 | // 4 | 5 | @testable import ContentfulPersistence 6 | import XCTest 7 | 8 | class RelationshipTests: XCTestCase { 9 | 10 | func testInitWithOneChild() { 11 | let child1 = RelationshipChildId(id: "child1", localeCode: nil) 12 | let relationship = Relationship( 13 | parentType: "parentType", 14 | parentId: "parentId", 15 | fieldName: "fieldName", 16 | childId: child1 17 | ) 18 | 19 | XCTAssertEqual(relationship.children, .one(child1)) 20 | } 21 | 22 | func testInitWithManyChildren() { 23 | let child1 = RelationshipChildId(id: "child1", localeCode: nil) 24 | let child2 = RelationshipChildId(id: "child2", localeCode: nil) 25 | let child3 = RelationshipChildId(id: "child3", localeCode: nil) 26 | let relationship = Relationship( 27 | parentType: "parentType", 28 | parentId: "parentId", 29 | fieldName: "fieldName", 30 | childIds: [child1, child2, child3] 31 | ) 32 | 33 | XCTAssertEqual(relationship.children, .many([child1, child2, child3])) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/TestModels/Asset+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Asset+CoreDataProperties.swift 3 | // 4 | // 5 | // Created by Boris Bügling on 31/03/16. 6 | // 7 | // 8 | // Choose "Create NSManagedObject Subclass…" from the Core Data editor menu 9 | // to delete and recreate this implementation file for your updated model. 10 | // 11 | 12 | import Foundation 13 | import CoreData 14 | import ContentfulPersistence 15 | 16 | extension Asset: AssetPersistable { 17 | 18 | @NSManaged var id: String 19 | @NSManaged var localeCode: String? 20 | @NSManaged var createdAt: Date? 21 | @NSManaged var updatedAt: Date? 22 | 23 | // AssetPersistable 24 | @NSManaged var title: String? 25 | @NSManaged var assetDescription: String? 26 | @NSManaged var urlString: String? 27 | @NSManaged var fileType: String? 28 | @NSManaged var fileName: String? 29 | 30 | @NSManaged var size: NSNumber? 31 | @NSManaged var width: NSNumber? 32 | @NSManaged var height: NSNumber? 33 | 34 | @NSManaged var featuredImage_2wKn6yEnZewu2SCCkus4as_Inverse: NSSet? 35 | @NSManaged var icon_5KMiN6YPvi42icqAUQMCQe_Inverse: NSSet? 36 | @NSManaged var profilePhoto_1kUEViTN4EmGiEaaeC6ouY_Inverse: NSSet? 37 | } 38 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/clear-field-next-sync.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "5GiLOZvY7SiMeUIgIIAssS", 16 | "type": "Entry", 17 | "createdAt": "2017-06-20T14:03:29.319Z", 18 | "updatedAt": "2017-06-20T14:03:29.319Z", 19 | "revision": 1, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "singleRecord" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | } 30 | }, 31 | ], 32 | "nextSyncUrl": "https://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=w5ZGw6JFwqZmVcKsE8Kow4grw45QdybCpMOBFS3CksKJasKKw4HCriw3wofDhsKAXcKXQMOlZMKgw5AKHMKhw7zCmMOSc2ADwozCpAHDpsOmw4XDt0VPwrXCm3how4UII8OswrzDlMKDwqRewpQTw4oNwqBYw4RPXsKxLA" 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Relationships/RelationshipChildId.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentfulPersistence 3 | // 4 | 5 | struct RelationshipChildId: RawRepresentable, Codable, Equatable { 6 | 7 | typealias RawValue = String 8 | 9 | let rawValue: RawValue 10 | 11 | /// Id without locale code. 12 | let id: String 13 | 14 | /// Locale code associated with the id. 15 | let localeCode: String? 16 | 17 | init(rawValue: String) { 18 | self.rawValue = rawValue 19 | (self.id, self.localeCode) = rawValue.splitToIdAndLocaleCode() 20 | } 21 | 22 | init(id: String, localeCode: String?) { 23 | self.rawValue = [id, localeCode] 24 | .compactMap { $0 } 25 | .joined(separator: "_") 26 | 27 | self.id = id 28 | self.localeCode = localeCode 29 | } 30 | } 31 | 32 | private extension String { 33 | 34 | func splitToIdAndLocaleCode() -> (String, String?) { 35 | 36 | if let index = self.firstIndex(of: "_") { 37 | let localeCodeStartIndex = self.index(index, offsetBy: 1) 38 | return (String(self[self.startIndex.. [FieldName: String] { 32 | return [ 33 | "name": "name", 34 | "biography": "biography", 35 | "website": "website", 36 | "createdEntries": "createdEntries", 37 | "profilePhoto": "profilePhoto" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## iOS 17 | 18 | ### ios test_ios 19 | 20 | ```sh 21 | [bundle exec] fastlane ios test_ios 22 | ``` 23 | 24 | Description of what the lane does 25 | 26 | ### ios test_macos 27 | 28 | ```sh 29 | [bundle exec] fastlane ios test_macos 30 | ``` 31 | 32 | Description of what the lane does 33 | 34 | ### ios test_tvos 35 | 36 | ```sh 37 | [bundle exec] fastlane ios test_tvos 38 | ``` 39 | 40 | Description of what the lane does 41 | 42 | ### ios build 43 | 44 | ```sh 45 | [bundle exec] fastlane ios build 46 | ``` 47 | 48 | Description of what the lane does 49 | 50 | ---- 51 | 52 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 53 | 54 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 55 | 56 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 57 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/deleted-entry-initial.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "aNt2d7YR4AIwEAMcG4OwI", 16 | "type": "Entry", 17 | "createdAt": "2017-06-20T14:03:28.985Z", 18 | "updatedAt": "2017-07-13T09:49:17.125Z", 19 | "revision": 3, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "singleRecord" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "textBody": { 30 | "en-US": "Hello", 31 | "es-MX": "Hola" 32 | } 33 | } 34 | } 35 | ], 36 | "nextSyncUrl": "https://cdn.contentful.com/spaces/smf0sqiu0c5s/sync?sync_token=deletion-intial" 37 | } 38 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:ios) 17 | 18 | platform :ios do 19 | desc "Description of what the lane does" 20 | lane :test_ios do 21 | # test ios 22 | scan( 23 | scheme: "ContentfulPersistence_iOS", 24 | clean: true, 25 | ) 26 | end 27 | 28 | desc "Description of what the lane does" 29 | lane :test_macos do 30 | # test macos 31 | scan( 32 | scheme: "ContentfulPersistence_macOS", 33 | clean: true, 34 | ) 35 | end 36 | 37 | desc "Description of what the lane does" 38 | lane :test_tvos do 39 | # test tvos 40 | scan( 41 | scheme: "ContentfulPersistence_tvOS", 42 | clean: true, 43 | ) 44 | end 45 | 46 | desc "Description of what the lane does" 47 | lane :build do 48 | # verify project builds 49 | sh("cd .. && swift build") 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/DateFormatterCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatterCache.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Donny Wals on 05/08/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | class DateFormatterCache { 11 | static let shared = DateFormatterCache() 12 | 13 | private let queue = DispatchQueue(label: "com.contentful-persistence.formattercache") 14 | private var cache = [String: DateFormatter]() 15 | 16 | private init() {} 17 | 18 | func get(_ format: String, timeZone: TimeZone? = nil) -> DateFormatter { 19 | return queue.sync { 20 | if let formatter = cache[format] { 21 | return formatter 22 | } 23 | 24 | let formatter = DateFormatter() 25 | formatter.calendar = Calendar(identifier: .iso8601) 26 | // The locale and timezone properties must be exactly as follows to have a true, time-zone agnostic (i.e. offset of 00:00 from UTC) ISO stamp. 27 | formatter.locale = Foundation.Locale(identifier: "en_US_POSIX") 28 | formatter.timeZone = timeZone ?? TimeZone(secondsFromGMT: 0) 29 | formatter.dateFormat = format 30 | 31 | cache[format] = formatter 32 | 33 | return formatter 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /ContentfulPersistenceSwift.podspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'dotenv/load' 4 | 5 | Pod::Spec.new do |spec| 6 | spec.name = "ContentfulPersistenceSwift" 7 | spec.version = ENV['CONTENTFUL_PERSISTENCE_VERSION'] 8 | spec.summary = "Simplified persistence for the Contentful Swift SDK." 9 | spec.homepage = "https://github.com/contentful/contentful-persistence.swift/" 10 | spec.social_media_url = 'https://twitter.com/contentful' 11 | 12 | spec.license = { 13 | :type => 'MIT', 14 | :file => 'LICENSE' 15 | } 16 | 17 | spec.authors = { "JP Wright" => "jp@contentful.com", "Boris Bügling" => "boris@buegling.com" } 18 | spec.source = { :git => "https://github.com/contentful/contentful-persistence.swift.git", 19 | :tag => spec.version.to_s } 20 | spec.requires_arc = true 21 | spec.swift_version = '5.0' 22 | 23 | spec.source_files = 'Sources/**/*.swift' 24 | spec.module_name = 'ContentfulPersistence' 25 | spec.frameworks = 'CoreData' 26 | 27 | spec.ios.deployment_target = '12.0' 28 | spec.osx.deployment_target = '10.13' 29 | spec.watchos.deployment_target = '4.0' 30 | spec.tvos.deployment_target = '12.0' 31 | 32 | spec.dependency 'Contentful', '~> 5.5.9' 33 | end 34 | 35 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/multi-page-non-optional-link-resolution2.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys" : { 3 | "type" : "Array" 4 | }, 5 | "items" : [ 6 | { 7 | "sys" : { 8 | "space" : { 9 | "sys" : { 10 | "type" : "Link", 11 | "linkType" : "Space", 12 | "id" : "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id" : "3YYdCPjS0I6TNAFiCOEplO", 16 | "type" : "Entry", 17 | "createdAt" : "2017-06-20T14:03:29.046Z", 18 | "updatedAt" : "2017-07-14T09:10:48.128Z", 19 | "revision" : 3, 20 | "contentType" : { 21 | "sys" : { 22 | "type" : "Link", 23 | "linkType" : "ContentType", 24 | "id" : "link" 25 | } 26 | } 27 | }, 28 | "fields" : { 29 | "awesomeLinkTitle" : { 30 | "en-US" : "Non-optional Link" 31 | } 32 | } 33 | } 34 | ], 35 | "nextSyncUrl" : "http://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=multi-page-non-optional-link-resolution1.json" 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/TestModels/Post+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post+CoreDataProperties.swift 3 | // 4 | // 5 | // Created by Boris Bügling on 31/03/16. 6 | // 7 | // 8 | // Choose "Create NSManagedObject Subclass…" from the Core Data editor menu 9 | // to delete and recreate this implementation file for your updated model. 10 | // 11 | 12 | import Foundation 13 | import CoreData 14 | import ContentfulPersistence 15 | import Contentful 16 | 17 | extension Post: EntryPersistable { 18 | 19 | static let contentTypeId = "2wKn6yEnZewu2SCCkus4as" 20 | 21 | @NSManaged var id: String 22 | @NSManaged var localeCode: String? 23 | @NSManaged var createdAt: Date? 24 | @NSManaged var updatedAt: Date? 25 | @NSManaged var body: String? 26 | @NSManaged var comments: NSNumber? 27 | @NSManaged var date: Date? 28 | @NSManaged var slug: String? 29 | @NSManaged var tags: Data? 30 | @NSManaged var title: String? 31 | @NSManaged var authors: NSOrderedSet? 32 | @NSManaged var category: NSOrderedSet? 33 | @NSManaged var theFeaturedImage: Asset? 34 | 35 | static func fieldMapping() -> [FieldName: String] { 36 | return [ 37 | "title": "title", 38 | "featuredImage": "theFeaturedImage", 39 | "author": "authors", 40 | "date": "date" 41 | ] 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/location.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "4VTL2TY7rikiS6c2MI2is4", 16 | "type": "Entry", 17 | "createdAt": "2017-12-18T09:23:29.280Z", 18 | "updatedAt": "2017-12-18T09:23:29.280Z", 19 | "revision": 1, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "singleRecord" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "textBody": { 30 | "en-US": "Record with Location" 31 | }, 32 | "locationField": { 33 | "en-US": { 34 | "lon": -119.69819010000003, 35 | "lat": 34.4208305 36 | } 37 | } 38 | } 39 | } 40 | ], 41 | "nextSyncUrl": "http://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=wonDrcKnRgcSOF4-wrDCgcKefWzCgsOxwrfCq8KOfMOdXUPCvEnChwEEO8KFwqHDjyPCuxtwVcO7KEvCmAp7P8Oqw4ppKsOic8ONw4rDgMOzw7LDusKaaMO5w5zCo8K6SsOkeMOjwoTCji8lwrHDtcKjf8KDw73DmsOyAjZ9woPCiMOeVMOydsOtCMOCbMOGI8OXw4_Drk_DmDwtDhM_w4XCu8KpJEw3w7Q7bz_CvTzDsFTCvWfDuwzCnBHClGvCiWjDiQ" 42 | } 43 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/Mocks/MockURLProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class MockURLProtocol: URLProtocol { 4 | /// Dictionary maps URLs to error, data, and response 5 | static var mockURLs = [URL?: (error: Error?, data: Data?, response: HTTPURLResponse?)]() 6 | 7 | override class func canInit(with _: URLRequest) -> Bool { 8 | true 9 | } 10 | 11 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 12 | request 13 | } 14 | 15 | override func startLoading() { 16 | defer { 17 | self.client?.urlProtocolDidFinishLoading(self) 18 | } 19 | 20 | if let url = request.url { 21 | guard MockURLProtocol.mockURLs.keys.contains(url) else { 22 | fatalError("URL not mocked") 23 | } 24 | 25 | if let (error, data, response) = MockURLProtocol.mockURLs[url] { 26 | if let data { 27 | client?.urlProtocol(self, didLoad: data) 28 | } 29 | 30 | if let response { 31 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) 32 | } 33 | 34 | if let error { 35 | client?.urlProtocol(self, didFailWithError: error) 36 | } 37 | } 38 | } 39 | } 40 | 41 | override func stopLoading() {} 42 | } 43 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/video-asset.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "r3rkxrglg2d1" 13 | } 14 | }, 15 | "id": "YokO2rWbOoo68QmiEUkqe", 16 | "type": "Asset", 17 | "createdAt": "2018-04-02T08:13:10.692Z", 18 | "updatedAt": "2018-04-02T08:13:10.692Z", 19 | "revision": 1 20 | }, 21 | "fields": { 22 | "title": { 23 | "en-US": "Video asset" 24 | }, 25 | "description": { 26 | "en-US": "this is a video" 27 | }, 28 | "file": { 29 | "en-US": { 30 | "url": "//videos.ctfassets.net/r3rkxrglg2d1/YokO2rWbOoo68QmiEUkqe/5cd5ab8fc90e7b9b4d99d56ea29de768/JP_Swift_Demo.mp4", 31 | "details": { 32 | "size": 12429451 33 | }, 34 | "fileName": "JP_Swift_Demo.mp4", 35 | "contentType": "video/mp4" 36 | } 37 | } 38 | } 39 | } 40 | ], 41 | "nextSyncUrl": "http://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=wonDrcKnRgcSOF4-wrDCgcKefWzCgsOxwrfCq8KOfMOdXUPCvEnChwEEO8KFwqHDjyPCuxtwVcO7KEvCmAp7P8Oqw4ppKsOic8ONw4rDgMOzw7LDusKaaMO5w5zCo8K6SsOkeMOjwoTCji8lwrHDtcKjf8KDw73DmsOyAjZ9woPCiMOeVMOydsOtCMOCbMOGI8OXw4_Drk_DmDwtDhM_w4XCu8KpJEw3w7Q7bz_CvTzDsFTCvWfDuwzCnBHClGvCiWjDiQ" 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestModels/SingleRecord+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleRecord+CoreDataProperties.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by JP Wright on 12.07.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import Contentful 12 | import ContentfulPersistence 13 | 14 | extension SingleRecord: EntryPersistable { 15 | 16 | static let contentTypeId = "singleRecord" 17 | 18 | @NSManaged var id: String 19 | @NSManaged var localeCode: String? 20 | @NSManaged var textBody: String? 21 | @NSManaged var postedDate: Date? 22 | @NSManaged var createdAt: Date? 23 | @NSManaged var updatedAt: Date? 24 | @NSManaged var linkField: Link? 25 | @NSManaged var locationField: Contentful.Location? 26 | @NSManaged var assetLinkField: ComplexAsset? 27 | @NSManaged var assetsArrayLinkField: NSOrderedSet? 28 | @NSManaged var symbolsArray: Data? 29 | @NSManaged var symbolsArrayTransformable: [String]? 30 | 31 | static func fieldMapping() -> [FieldName: String] { 32 | return [ 33 | "textBody": "textBody", 34 | "linkField": "linkField", 35 | "locationField": "locationField", 36 | "postedDate": "postedDate", 37 | "assetLinkField": "assetLinkField", 38 | "assetsArrayLinkField": "assetsArrayLinkField", 39 | "symbolsArray": "symbolsArray", 40 | "symbolsArrayTransformable": "symbolsArrayTransformable" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/deleted-asset-initial.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "YokO2rWbOoo68QmiEUkqe", 16 | "type": "Asset", 17 | "createdAt": "2018-04-02T08:13:10.692Z", 18 | "updatedAt": "2018-04-02T08:13:10.692Z", 19 | "revision": 1 20 | }, 21 | "fields": { 22 | "title": { 23 | "en-US": "Video asset" 24 | }, 25 | "description": { 26 | "en-US": "this is a video" 27 | }, 28 | "file": { 29 | "en-US": { 30 | "url": "//videos.ctfassets.net/smf0sqiu0c5s/YokO2rWbOoo68QmiEUkqe/5cd5ab8fc90e7b9b4d99d56ea29de768/JP_Swift_Demo.mp4", 31 | "details": { 32 | "size": 12429451 33 | }, 34 | "fileName": "JP_Swift_Demo.mp4", 35 | "contentType": "video/mp4" 36 | } 37 | } 38 | } 39 | } 40 | ], 41 | "nextSyncUrl": "https://cdn.contentful.com/spaces/smf0sqiu0c5s/sync?sync_token=deletion-intial" 42 | } 43 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ContentStubs/single-author.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "space": { 4 | "sys": { 5 | "type": "Link", 6 | "linkType": "Space", 7 | "id": "dqpnpm0n4e75" 8 | } 9 | }, 10 | "id": "5JQ715oDQW68k8EiEuKOk8", 11 | "type": "Entry", 12 | "createdAt": "2015-02-05T12:11:56.980Z", 13 | "updatedAt": "2015-02-05T12:11:56.980Z", 14 | "revision": 1, 15 | "contentType": { 16 | "sys": { 17 | "type": "Link", 18 | "linkType": "ContentType", 19 | "id": "1kUEViTN4EmGiEaaeC6ouY" 20 | } 21 | }, 22 | "locale": "en-US" 23 | }, 24 | "fields": { 25 | "name": "Mike Springer", 26 | "website": "https://plus.google.com/+openculture/posts", 27 | "profilePhoto": { 28 | "sys": { 29 | "type": "Link", 30 | "linkType": "Asset", 31 | "id": "2xA3oKlZTuQ0Wgs2Wm2Mkk" 32 | } 33 | }, 34 | "biography": "Mike Springer is a journalist living in Cambridge, Massachusetts, he writes daily for Open Culture.", 35 | "createdEntries": [ 36 | { 37 | "sys": { 38 | "type": "Link", 39 | "linkType": "Entry", 40 | "id": "3lHulSxvby04sO0q0k64aA" 41 | } 42 | } 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/Mocks/MockPersistenceStore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @testable import ContentfulPersistence 4 | 5 | class MockPersistenceStore: PersistenceStore { 6 | 7 | var returnValue: Any? 8 | 9 | func create(type: any Any.Type) throws -> T { 10 | if let returnValue = returnValue as? T { 11 | return returnValue 12 | } else { 13 | fatalError( 14 | "MockPersistenceStore.create was called without a return value being set." 15 | ) 16 | } 17 | } 18 | 19 | func delete(type _: any Any.Type, predicate _: NSPredicate) throws {} 20 | 21 | func fetchAll(type _: any Any.Type, predicate _: NSPredicate) throws 22 | -> [T] 23 | { 24 | [] 25 | } 26 | 27 | func fetchOne(type: any Any.Type, predicate _: NSPredicate) throws -> T { 28 | if let returnValue = returnValue as? T { 29 | return returnValue 30 | } else { 31 | fatalError( 32 | "MockPersistenceStore.fetchOne was called without a return value being set." 33 | ) 34 | } 35 | } 36 | 37 | func properties(for _: any Any.Type) throws -> [String] { 38 | [] 39 | } 40 | 41 | func relationships(for _: any Any.Type) throws -> [String] { 42 | [] 43 | } 44 | 45 | func save() throws {} 46 | 47 | func wipe() throws {} 48 | 49 | func performBlock(block: @escaping () -> Void) { 50 | block() 51 | } 52 | 53 | func performAndWait(block: @escaping () -> Void) { 54 | block() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/symbols-array.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "2mhGzgf3oQOquo0SyGWCQE", 16 | "type": "Entry", 17 | "createdAt": "2018-01-08T19:42:13.766Z", 18 | "updatedAt": "2018-01-08T19:42:13.766Z", 19 | "revision": 1, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "singleRecord" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "textBody": { 30 | "en-US": "Record with symbols array" 31 | }, 32 | "symbolsArray": { 33 | "en-US": [ 34 | "one", 35 | "two", 36 | "three", 37 | "four", 38 | "five" 39 | ] 40 | }, 41 | "symbolsArrayTransformable": { 42 | "en-US": [ 43 | "one", 44 | "two", 45 | "three", 46 | "four", 47 | "five" 48 | ] 49 | } 50 | } 51 | } 52 | ], 53 | "nextSyncUrl": "http://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=wonDrcKnRgcSOF4-wrDCgcKefWzCgsOxwrfCq8KOfMOdXUPCvEnChwEEO8KFwqHDj8KBwrXDq8KnKiQTBA3DkDw2w7o0JcOyQmlaV8O5Q8ORPjbDpiUGwptdSMOcwrXDnsK4aQ4HwpnDkB5SYMKQRm4UXRsSbW1AU8K6LsOJw5_CvS1iV2bCvcKED0XClcKuw7t4UsKIwoHCvMOAcMOmwrlYF8OmUcKUwq1aw5NQwrBpw7RdwqrClsK9wrc" 54 | } 55 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/multi-page-non-optional-link-resolution1.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys" : { 3 | "type" : "Array" 4 | }, 5 | "items" : [ 6 | { 7 | "sys" : { 8 | "space" : { 9 | "sys" : { 10 | "type" : "Link", 11 | "linkType" : "Space", 12 | "id" : "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id" : "15OmnIzspI44uKCcNzcPUS", 16 | "type" : "Entry", 17 | "createdAt" : "2017-06-20T14:03:29.046Z", 18 | "updatedAt" : "2017-07-14T09:10:48.128Z", 19 | "revision" : 3, 20 | "contentType" : { 21 | "sys" : { 22 | "type" : "Link", 23 | "linkType" : "ContentType", 24 | "id" : "recordWithNonOptionalRelation" 25 | } 26 | } 27 | }, 28 | "fields" : { 29 | "textBody" : { 30 | "en-US" : "Record With Link" 31 | }, 32 | "nonOptionalLink" : { 33 | "en-US" : { 34 | "sys" : { 35 | "type" : "Link", 36 | "linkType" : "Entry", 37 | "id" : "3YYdCPjS0I6TNAFiCOEplO" 38 | } 39 | } 40 | } 41 | } 42 | } 43 | ], 44 | "nextPageUrl" : "http://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=multi-page-non-optional-link-resolution-token" 45 | } 46 | -------------------------------------------------------------------------------- /old-travis-integration.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11.4 3 | rvm: 4 | - 2.4.3 5 | cache: bundler 6 | 7 | git: 8 | submodules: false # By default travis does a recursive submodule update which we don't want. 9 | 10 | # Whitelist `master` branch as the only branch to build pushes for. 11 | branches: 12 | only: 13 | - master 14 | matrix: 15 | include: 16 | - env: SDK=iphonesimulator PLATFORM="iOS Simulator,name=iPhone X,OS=12.2" SCHEME=ContentfulPersistence_iOS 17 | - env: SDK=appletvsimulator PLATFORM="tvOS Simulator,name=Apple TV,OS=12.2" SCHEME=ContentfulPersistence_tvOS 18 | - env: SDK=macosx PLATFORM="macOS" SCHEME=ContentfulPersistence_macOS 19 | - env: SWIFT_BUILD=true 20 | before_install: 21 | git submodule update --init # 22 | script: 23 | - ./Scripts/travis-build-test.sh 24 | after_success: 25 | - bundle exec slather coverage -s --coveralls 26 | - pod lib lint ContentfulPersistenceSwift.podspec 27 | - if [[ $TRAVIS_PULL_REQUEST == false && -n $TRAVIS_TAG && $SWIFT_BUILD == true ]]; then ./Scripts/reference-docs.sh; fi 28 | deploy: 29 | provider: pages 30 | skip_cleanup: true 31 | github_token: $GITHUB_ACCESS_TOKEN 32 | local_dir: docs 33 | on: 34 | condition: $TRAVIS_PULL_REQUEST == false && -n $TRAVIS_TAG && $SWIFT_BUILD == true 35 | env: 36 | global: 37 | secure: Xohxw56tpFlfTXtxR+Krg6vYoNr70YPXXGxuhCkwys4bQDwHooPNcdLcNL8Q2jpOS4ZsNIKhn3zEy6mbdCYGSu8ifd44v2haWS5Bl+zbomnnB5qkZveMwpEt+sG/MuKIO2dVhD3AULNM5CkqRXfCAUTmxFbafe1wIoB8Y6pjt4RSWmVoBidNgv7WeT9x4XPXUUjjJJOekdRUpOjgSleE3QCJetWTZfcG+6u6B1pvEoOMMMsMCkb7jk9RqevqlUQ0Kv0VI5ATvJaSdjgOIIAROL+deKq63zU1n/6IfXhVVSMMeuREnfb924pZCaDX0+tG/ZxveFdYjOwN9OZniveXJEdrJ3ROsCZq58G9rTTJuh4+g/J2SFLV83bi7ZIg6aDJxlBIP6CjyxeLaYT9z2kVZgpU2kihDu+lVBSKIEa8eHo/SO2T92VCYxgo3iHwvCiMiyT59h7uSVK69zCXkoejjpcrw/Ud2OC6WgZEWHjHJYote5e8PN2task2Hgi+NCkhkf27IId5N5QUDbVSbSx7mDPdV/QIErcnfTuJFNLIGAMAP9dbGBhOpfLEexx03ar2/6+f41Rrjg1COfBVipV8C0s9rW48t8vE7rAR5NkrQC7VOeZvsKWdXkH6hwuqvXw03ENIE0ak+Tn83Bo1PQaIykqN7oEGBsvPeudAeyNXsw4= 38 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Seeding/BundledDatabase/CoreDataStore+Preseeding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataStore+Preseeding.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Marius Kurgonas on 30/05/2025. 6 | // Copyright © 2025 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | extension CoreDataStore { 12 | /// Before swapping: delete WAL/SHM side-cars and remove only the store matching `storeFileURL`. 13 | public func onStorePreseedingWillBegin(at storeFileURL: URL) throws { 14 | let fm = FileManager.default 15 | 16 | // 1) Delete WAL & SHM side-cars if they exist 17 | let wal = URL(fileURLWithPath: storeFileURL.path + "-wal") 18 | let shm = URL(fileURLWithPath: storeFileURL.path + "-shm") 19 | [wal, shm].forEach { url in 20 | if fm.fileExists(atPath: url.path) { 21 | try? fm.removeItem(at: url) 22 | } 23 | } 24 | 25 | // 2) Find and remove only the persistent store whose URL equals `storeFileURL` 26 | guard let coord = context.persistentStoreCoordinator else { return } 27 | if let psToRemove = coord.persistentStores.first(where: { $0.url == storeFileURL }) { 28 | try coord.remove(psToRemove) 29 | } 30 | } 31 | 32 | /// After swapping: add the store back *and then* reset the context. 33 | public func onStorePreseedingCompleted(at seededFileURL: URL) throws { 34 | guard let coord = context.persistentStoreCoordinator else { return } 35 | let options = coord.persistentStores.first?.options 36 | 37 | // 1) Re-add the SQLite store at the new URL 38 | try coord.addPersistentStore( 39 | ofType: NSSQLiteStoreType, 40 | configurationName: nil, 41 | at: seededFileURL, 42 | options: options 43 | ) 44 | 45 | // 2) Now flush any in-memory objects so we start fresh 46 | context.performAndWait { 47 | context.reset() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | test-ios: 5 | macos: 6 | xcode: 15.4 7 | steps: 8 | - checkout 9 | - run: 10 | name: Install Carthage 11 | command: brew install carthage 12 | - run: 13 | name: Update Carthage Dependencies 14 | command: carthage update --use-xcframeworks 15 | - run: bundle install 16 | - run: 17 | name: Run Tests with Fastlane 18 | command: bundle exec fastlane test_ios 19 | 20 | test-macos: 21 | macos: 22 | xcode: 15.4 23 | steps: 24 | - checkout 25 | - run: 26 | name: Install Carthage 27 | command: brew install carthage 28 | - run: 29 | name: Update Carthage Dependencies 30 | command: carthage update --use-xcframeworks 31 | - run: bundle install 32 | - run: 33 | name: Run Tests with Fastlane 34 | command: bundle exec fastlane test_macos 35 | 36 | test-tvos: 37 | macos: 38 | xcode: 15.4 39 | steps: 40 | - checkout 41 | - run: 42 | name: Install Carthage 43 | command: brew install carthage 44 | - run: 45 | name: Update Carthage Dependencies 46 | command: carthage update --use-xcframeworks 47 | - run: bundle install 48 | - run: 49 | name: Run Tests with Fastlane 50 | command: bundle exec fastlane test_tvos 51 | 52 | build: 53 | macos: 54 | xcode: 15.4 55 | steps: 56 | - checkout 57 | - run: 58 | name: Install Carthage 59 | command: brew install carthage 60 | - run: 61 | name: Update Carthage Dependencies 62 | command: carthage update --use-xcframeworks 63 | - run: bundle install 64 | - run: 65 | name: Run Tests with Fastlane 66 | command: bundle exec fastlane build 67 | workflows: 68 | test-workflow: 69 | jobs: 70 | - test-ios 71 | - test-macos 72 | - test-tvos 73 | - build 74 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Seeding/BundledDatabase/PreseedConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreseedConfiguration.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Marius Kurgonas on 30/05/2025. 6 | // Copyright © 2025 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Describes a bundled seed and the exact directory on disk where 12 | /// `.` will be placed. 13 | public struct PreseedConfiguration { 14 | /// e.g. "SeedDB" (no “.sqlite”) 15 | public let resourceName: String 16 | 17 | /// e.g. "sqlite" 18 | public let resourceExtension: String 19 | 20 | /// Optional bundle subfolder 21 | public let subdirectory: String? 22 | 23 | /// Bundle containing the resource (default: .main) 24 | public let bundle: Bundle 25 | 26 | /// Directory on disk where the main `.sqlite` lives. **Required.** 27 | public let sqliteContainerPath: URL 28 | 29 | /// The version that this bundled seed represents. 30 | public let dbVersion: Int 31 | 32 | /// - Parameters: 33 | /// - resourceName: Base name of the bundle file. 34 | /// - resourceExtension: Extension (e.g. "sqlite"). 35 | /// - subdirectory: Bundle subfolder (nil = top). 36 | /// - bundle: The bundle containing it. 37 | /// - overrideStoreDirectory: On-disk folder to wipe & seed. 38 | /// - dbVersion: The migration version for this seed. 39 | public init(resourceName: String, 40 | resourceExtension: String, 41 | subdirectory: String? = nil, 42 | bundle: Bundle = .main, 43 | sqliteContainerPath: URL, 44 | dbVersion: Int) { 45 | self.resourceName = resourceName 46 | self.resourceExtension = resourceExtension 47 | self.subdirectory = subdirectory 48 | self.bundle = bundle 49 | self.sqliteContainerPath = sqliteContainerPath 50 | self.dbVersion = dbVersion 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Relationships/RelationshipsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentfulPersistenceSwift 3 | // 4 | 5 | import Foundation 6 | 7 | /// Manages relationships of the entries using internal cache. It is used to recreate relationship when 8 | /// unpublished entry is published again. 9 | final class RelationshipsManager { 10 | 11 | private let cache: RelationshipCache 12 | 13 | var relationships: RelationshipData { 14 | cache.relationships 15 | } 16 | 17 | init(cacheFileName: String) { 18 | self.cache = RelationshipCache(cacheFileName: cacheFileName) 19 | } 20 | 21 | /// Creates one-to-one relationship if does not exist yet. 22 | func cacheToOneRelationship( 23 | parent: EntryPersistable, 24 | childId: RelationshipChildId, 25 | fieldName: String 26 | ) { 27 | 28 | let parentType = type(of: parent).contentTypeId 29 | 30 | let relationship = Relationship( 31 | parentType: parentType, 32 | parentId: parent.id, 33 | fieldName: fieldName, 34 | childId: childId 35 | ) 36 | 37 | cache.add(relationship: relationship) 38 | } 39 | 40 | func cacheToManyRelationship( 41 | parent: EntryPersistable, 42 | childIds: [RelationshipChildId], 43 | fieldName: String 44 | ) { 45 | let parentType = type(of: parent).contentTypeId 46 | 47 | let relationship = Relationship( 48 | parentType: parentType, 49 | parentId: parent.id, 50 | fieldName: fieldName, 51 | childIds: childIds 52 | ) 53 | 54 | cache.add(relationship: relationship) 55 | } 56 | 57 | func delete(parentId: String) { 58 | cache.delete(parentId: parentId) 59 | } 60 | 61 | func delete(parentId: String, fieldName: String, localeCode: String?) { 62 | cache.delete(parentId: parentId, fieldName: fieldName, localeCode: localeCode) 63 | } 64 | 65 | func save() { 66 | cache.save() 67 | } 68 | 69 | func wipe() { 70 | cache.wipe() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | // Jazzy - https://github.com/realm/jazzy 2 | // Copyright Realm Inc. 3 | // SPDX-License-Identifier: MIT 4 | 5 | window.jazzy = {'docset': false} 6 | if (typeof window.dash != 'undefined') { 7 | document.documentElement.className += ' dash' 8 | window.jazzy.docset = true 9 | } 10 | if (navigator.userAgent.match(/xcode/i)) { 11 | document.documentElement.className += ' xcode' 12 | window.jazzy.docset = true 13 | } 14 | 15 | function toggleItem($link, $content) { 16 | var animationDuration = 300; 17 | $link.toggleClass('token-open'); 18 | $content.slideToggle(animationDuration); 19 | } 20 | 21 | function itemLinkToContent($link) { 22 | return $link.parent().parent().next(); 23 | } 24 | 25 | // On doc load + hash-change, open any targetted item 26 | function openCurrentItemIfClosed() { 27 | if (window.jazzy.docset) { 28 | return; 29 | } 30 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 31 | $content = itemLinkToContent($link); 32 | if ($content.is(':hidden')) { 33 | toggleItem($link, $content); 34 | } 35 | } 36 | 37 | $(openCurrentItemIfClosed); 38 | $(window).on('hashchange', openCurrentItemIfClosed); 39 | 40 | // On item link ('token') click, toggle its discussion 41 | $('.token').on('click', function(event) { 42 | if (window.jazzy.docset) { 43 | return; 44 | } 45 | var $link = $(this); 46 | toggleItem($link, itemLinkToContent($link)); 47 | 48 | // Keeps the document from jumping to the hash. 49 | var href = $link.attr('href'); 50 | if (history.pushState) { 51 | history.pushState({}, '', href); 52 | } else { 53 | location.hash = href; 54 | } 55 | event.preventDefault(); 56 | }); 57 | 58 | // Clicks on links to the current, closed, item need to open the item 59 | $("a:not('.token')").on('click', function() { 60 | if (location == this.href) { 61 | openCurrentItemIfClosed(); 62 | } 63 | }); 64 | 65 | // KaTeX rendering 66 | if ("katex" in window) { 67 | $($('.math').each( (_, element) => { 68 | katex.render(element.textContent, element, { 69 | displayMode: $(element).hasClass('m-block'), 70 | throwOnError: false, 71 | trust: true 72 | }); 73 | })) 74 | } 75 | -------------------------------------------------------------------------------- /docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | // Jazzy - https://github.com/realm/jazzy 2 | // Copyright Realm Inc. 3 | // SPDX-License-Identifier: MIT 4 | 5 | window.jazzy = {'docset': false} 6 | if (typeof window.dash != 'undefined') { 7 | document.documentElement.className += ' dash' 8 | window.jazzy.docset = true 9 | } 10 | if (navigator.userAgent.match(/xcode/i)) { 11 | document.documentElement.className += ' xcode' 12 | window.jazzy.docset = true 13 | } 14 | 15 | function toggleItem($link, $content) { 16 | var animationDuration = 300; 17 | $link.toggleClass('token-open'); 18 | $content.slideToggle(animationDuration); 19 | } 20 | 21 | function itemLinkToContent($link) { 22 | return $link.parent().parent().next(); 23 | } 24 | 25 | // On doc load + hash-change, open any targetted item 26 | function openCurrentItemIfClosed() { 27 | if (window.jazzy.docset) { 28 | return; 29 | } 30 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 31 | $content = itemLinkToContent($link); 32 | if ($content.is(':hidden')) { 33 | toggleItem($link, $content); 34 | } 35 | } 36 | 37 | $(openCurrentItemIfClosed); 38 | $(window).on('hashchange', openCurrentItemIfClosed); 39 | 40 | // On item link ('token') click, toggle its discussion 41 | $('.token').on('click', function(event) { 42 | if (window.jazzy.docset) { 43 | return; 44 | } 45 | var $link = $(this); 46 | toggleItem($link, itemLinkToContent($link)); 47 | 48 | // Keeps the document from jumping to the hash. 49 | var href = $link.attr('href'); 50 | if (history.pushState) { 51 | history.pushState({}, '', href); 52 | } else { 53 | location.hash = href; 54 | } 55 | event.preventDefault(); 56 | }); 57 | 58 | // Clicks on links to the current, closed, item need to open the item 59 | $("a:not('.token')").on('click', function() { 60 | if (location == this.href) { 61 | openCurrentItemIfClosed(); 62 | } 63 | }); 64 | 65 | // KaTeX rendering 66 | if ("katex" in window) { 67 | $($('.math').each( (_, element) => { 68 | katex.render(element.textContent, element, { 69 | displayMode: $(element).hasClass('m-block'), 70 | throwOnError: false, 71 | trust: true 72 | }); 73 | })) 74 | } 75 | -------------------------------------------------------------------------------- /docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | // Jazzy - https://github.com/realm/jazzy 2 | // Copyright Realm Inc. 3 | // SPDX-License-Identifier: MIT 4 | 5 | $(function(){ 6 | var $typeahead = $('[data-typeahead]'); 7 | var $form = $typeahead.parents('form'); 8 | var searchURL = $form.attr('action'); 9 | 10 | function displayTemplate(result) { 11 | return result.name; 12 | } 13 | 14 | function suggestionTemplate(result) { 15 | var t = '
'; 16 | t += '' + result.name + ''; 17 | if (result.parent_name) { 18 | t += '' + result.parent_name + ''; 19 | } 20 | t += '
'; 21 | return t; 22 | } 23 | 24 | $typeahead.one('focus', function() { 25 | $form.addClass('loading'); 26 | 27 | $.getJSON(searchURL).then(function(searchData) { 28 | const searchIndex = lunr(function() { 29 | this.ref('url'); 30 | this.field('name'); 31 | this.field('abstract'); 32 | for (const [url, doc] of Object.entries(searchData)) { 33 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 34 | } 35 | }); 36 | 37 | $typeahead.typeahead( 38 | { 39 | highlight: true, 40 | minLength: 3, 41 | autoselect: true 42 | }, 43 | { 44 | limit: 10, 45 | display: displayTemplate, 46 | templates: { suggestion: suggestionTemplate }, 47 | source: function(query, sync) { 48 | const lcSearch = query.toLowerCase(); 49 | const results = searchIndex.query(function(q) { 50 | q.term(lcSearch, { boost: 100 }); 51 | q.term(lcSearch, { 52 | boost: 10, 53 | wildcard: lunr.Query.wildcard.TRAILING 54 | }); 55 | }).map(function(result) { 56 | var doc = searchData[result.ref]; 57 | doc.url = result.ref; 58 | return doc; 59 | }); 60 | sync(results); 61 | } 62 | } 63 | ); 64 | $form.removeClass('loading'); 65 | $typeahead.trigger('focus'); 66 | }); 67 | }); 68 | 69 | var baseURL = searchURL.slice(0, -"search.json".length); 70 | 71 | $typeahead.on('typeahead:select', function(e, result) { 72 | window.location = baseURL + result.url; 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | // Jazzy - https://github.com/realm/jazzy 2 | // Copyright Realm Inc. 3 | // SPDX-License-Identifier: MIT 4 | 5 | $(function(){ 6 | var $typeahead = $('[data-typeahead]'); 7 | var $form = $typeahead.parents('form'); 8 | var searchURL = $form.attr('action'); 9 | 10 | function displayTemplate(result) { 11 | return result.name; 12 | } 13 | 14 | function suggestionTemplate(result) { 15 | var t = '
'; 16 | t += '' + result.name + ''; 17 | if (result.parent_name) { 18 | t += '' + result.parent_name + ''; 19 | } 20 | t += '
'; 21 | return t; 22 | } 23 | 24 | $typeahead.one('focus', function() { 25 | $form.addClass('loading'); 26 | 27 | $.getJSON(searchURL).then(function(searchData) { 28 | const searchIndex = lunr(function() { 29 | this.ref('url'); 30 | this.field('name'); 31 | this.field('abstract'); 32 | for (const [url, doc] of Object.entries(searchData)) { 33 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 34 | } 35 | }); 36 | 37 | $typeahead.typeahead( 38 | { 39 | highlight: true, 40 | minLength: 3, 41 | autoselect: true 42 | }, 43 | { 44 | limit: 10, 45 | display: displayTemplate, 46 | templates: { suggestion: suggestionTemplate }, 47 | source: function(query, sync) { 48 | const lcSearch = query.toLowerCase(); 49 | const results = searchIndex.query(function(q) { 50 | q.term(lcSearch, { boost: 100 }); 51 | q.term(lcSearch, { 52 | boost: 10, 53 | wildcard: lunr.Query.wildcard.TRAILING 54 | }); 55 | }).map(function(result) { 56 | var doc = searchData[result.ref]; 57 | doc.url = result.ref; 58 | return doc; 59 | }); 60 | sync(results); 61 | } 62 | } 63 | ); 64 | $form.removeClass('loading'); 65 | $typeahead.trigger('focus'); 66 | }); 67 | }); 68 | 69 | var baseURL = searchURL.slice(0, -"search.json".length); 70 | 71 | $typeahead.on('typeahead:select', function(e, result) { 72 | window.location = baseURL + result.url; 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/simple-update-initial-sync-page2.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "7vervB1KtUYWC22OCcmsKc", 16 | "type": "Entry", 17 | "createdAt": "2017-06-20T12:51:06.955Z", 18 | "updatedAt": "2017-06-20T12:51:06.955Z", 19 | "revision": 1, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "singleRecord" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "textBody": { 30 | "en-US": "3" 31 | } 32 | } 33 | }, 34 | { 35 | "sys": { 36 | "space": { 37 | "sys": { 38 | "type": "Link", 39 | "linkType": "Space", 40 | "id": "smf0sqiu0c5s" 41 | } 42 | }, 43 | "id": "NNPT58qeyYKauym8S0MUk", 44 | "type": "Entry", 45 | "createdAt": "2017-06-20T12:51:00.210Z", 46 | "updatedAt": "2017-06-20T12:51:00.210Z", 47 | "revision": 1, 48 | "contentType": { 49 | "sys": { 50 | "type": "Link", 51 | "linkType": "ContentType", 52 | "id": "singleRecord" 53 | } 54 | } 55 | }, 56 | "fields": { 57 | "textBody": { 58 | "en-US": "2" 59 | } 60 | } 61 | }, 62 | { 63 | "sys": { 64 | "space": { 65 | "sys": { 66 | "type": "Link", 67 | "linkType": "Space", 68 | "id": "smf0sqiu0c5s" 69 | } 70 | }, 71 | "id": "3PbLvOJldSc6MqKEaIE6Ce", 72 | "type": "Entry", 73 | "createdAt": "2017-06-20T12:50:15.724Z", 74 | "updatedAt": "2017-06-20T12:50:15.724Z", 75 | "revision": 1, 76 | "contentType": { 77 | "sys": { 78 | "type": "Link", 79 | "linkType": "ContentType", 80 | "id": "singleRecord" 81 | } 82 | } 83 | }, 84 | "fields": { 85 | "textBody": { 86 | "en-US": "1" 87 | } 88 | } 89 | } 90 | ], 91 | "nextSyncUrl": "https://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=w5ZGw6JFwqZmVcKsE8Kow4grw45QdyYxPMK6EcOWw7swwqJhAcOTwqwMwoLCkUFqwpkGMHnDicOeCls-wpHCtSHDiMOJwp9Zw6URwqHCqcK4fFNpDV12wpzDpcKAw71Uw53Dk8KdwrYDw7DCqQU1w4bCg8KkwojDhsOb" 92 | } 93 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Seeding/BundledDatabase/FilePreseedManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilePreseedManager.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Marius Kurgonas on 30/05/2025. 6 | // Copyright © 2025 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Default strategy that: 12 | /// 1) calls `onStorePreseedingWillBegin(at:)` 13 | /// 2) wipes the entire seed folder 14 | /// 3) copies `.` 15 | /// 4) calls `onStorePreseedingCompleted(at:)` 16 | /// 5) writes back `dbVersion` 17 | public class FilePreseedManager: PreseedStrategy { 18 | private let fileManager: FileManaging 19 | 20 | /// - Parameter fileManager: Test‐injectable; default = `FileManager.default` 21 | public init(fileManager: FileManaging = FileManager.default) { 22 | self.fileManager = fileManager 23 | } 24 | 25 | public func apply(to store: PersistenceStore, 26 | with config: PreseedConfiguration, 27 | spaceType: SyncSpacePersistable.Type) throws 28 | { 29 | let filename = "\(config.resourceName).\(config.resourceExtension)" 30 | let dbURL = config.sqliteContainerPath.appendingPathComponent(filename) 31 | 32 | guard let seedURL = config.bundle.url( 33 | forResource: config.resourceName, 34 | withExtension: config.resourceExtension, 35 | subdirectory: config.subdirectory) 36 | else { 37 | throw NSError( 38 | domain: "ContentfulPersistence", 39 | code: 1, 40 | userInfo: [NSLocalizedDescriptionKey: 41 | "Seed file not found: \(config.resourceName).\(config.resourceExtension)"]) 42 | } 43 | 44 | // Read existing version (0 if none) 45 | let lastVersion: Int = { 46 | do { 47 | let spaces: [SyncSpacePersistable] = try store.fetchAll(type: spaceType, predicate: NSPredicate(value: true)) 48 | return spaces.count > 0 ? spaces[0].dbVersion?.intValue ?? 0 : 0 49 | } catch { 50 | return 0 51 | } 52 | }() 53 | 54 | // Only seed if fresh or version bumped 55 | let missing = !fileManager.fileExists(atPath: dbURL.path) 56 | guard missing || config.dbVersion > lastVersion else { return } 57 | 58 | // Prepare the store 59 | try store.onStorePreseedingWillBegin(at: dbURL) 60 | 61 | // Remove existing 62 | try? fileManager.removeItem(at: config.sqliteContainerPath) 63 | 64 | try fileManager.createDirectory( 65 | at: config.sqliteContainerPath, 66 | withIntermediateDirectories: true, 67 | attributes: nil 68 | ) 69 | 70 | try fileManager.copyItem(at: seedURL, to: dbURL) 71 | 72 | // Re-open the store 73 | try store.onStorePreseedingCompleted(at: dbURL) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Relationships/RelationshipCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentfulPersistence 3 | // 4 | 5 | import Foundation 6 | 7 | /** 8 | Stores all relationships in the database. It acts like a backup for relationships in case an entry has been 9 | unpublished and is published in the future. 10 | 11 | With this class the library can bring back a relationship on the Core Data Model level. Otherwise, in the 12 | following scenario, the model would not reflect a correct state of the model. 13 | 14 | Scenario: 15 | 1) Fetch all data. 16 | 2) Unpublish entry that is referenced by other entry. 17 | 3) See the unpublished entry reference is represented by `nil` in Core Data. Relationship is `nil`. 18 | 4) Publish entry again. 19 | 5) See the relationship is broken. The reference is still `nil`. instead of the published entry. 20 | */ 21 | final class RelationshipCache { 22 | 23 | private let cacheFileName: String 24 | 25 | init(cacheFileName: String) { 26 | self.cacheFileName = cacheFileName 27 | } 28 | 29 | private(set) lazy var relationships: RelationshipData = loadFromCache() 30 | 31 | func add(relationship: Relationship) { 32 | relationships.append(relationship) 33 | } 34 | 35 | func delete(parentId: String) { 36 | relationships.delete(parentId: parentId) 37 | } 38 | 39 | func delete(parentId: String, fieldName: String, localeCode: String?) { 40 | relationships.delete(parentId: parentId, fieldName: fieldName, localeCode: localeCode) 41 | } 42 | 43 | func save() { 44 | do { 45 | guard let localUrl = cacheUrl() else { return } 46 | let data = try JSONEncoder().encode(relationships) 47 | try data.write(to: localUrl) 48 | } catch let error { 49 | print("Couldn't persist relationships: \(error)") 50 | } 51 | } 52 | 53 | func wipe() { 54 | do { 55 | guard let localUrl = cacheUrl() else { return } 56 | try FileManager.default.removeItem(at: localUrl) 57 | } catch let error { 58 | print("Couldn't delete relationships: \(error)") 59 | } 60 | } 61 | 62 | private func cacheUrl() -> URL? { 63 | guard let url = try? FileManager.default.url( 64 | for: .documentDirectory, 65 | in: .userDomainMask, 66 | appropriateFor: nil, 67 | create: true 68 | ) else { 69 | return nil 70 | } 71 | 72 | return url.appendingPathComponent(cacheFileName) 73 | } 74 | 75 | private func loadFromCache() -> RelationshipData { 76 | do { 77 | guard let localURL = cacheUrl() else { return .init() } 78 | let data = try Data(contentsOf: localURL, options: []) 79 | return try JSONDecoder().decode(RelationshipData.self, from: data) 80 | } catch { 81 | return .init() 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ContentfulPersistence.xcodeproj/xcshareddata/xcschemes/ContentfulPersistence_watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/clear-field-initial-sync.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "aNt2d7YR4AIwEAMcG4OwI", 16 | "type": "Entry", 17 | "createdAt": "2017-06-20T14:03:28.985Z", 18 | "updatedAt": "2017-07-13T09:49:17.125Z", 19 | "revision": 3, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "singleRecord" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "textBody": { 30 | "en-US": "Hello" 31 | } 32 | } 33 | }, 34 | { 35 | "sys": { 36 | "space": { 37 | "sys": { 38 | "type": "Link", 39 | "linkType": "Space", 40 | "id": "smf0sqiu0c5s" 41 | } 42 | }, 43 | "id": "14XouHzspI44uKCcMicWUY", 44 | "type": "Entry", 45 | "createdAt": "2017-06-20T14:03:29.046Z", 46 | "updatedAt": "2017-06-22T09:09:16.166Z", 47 | "revision": 2, 48 | "contentType": { 49 | "sys": { 50 | "type": "Link", 51 | "linkType": "ContentType", 52 | "id": "singleRecord" 53 | } 54 | } 55 | }, 56 | "fields": { 57 | "textBody": { 58 | "en-US": "12" 59 | } 60 | } 61 | }, 62 | { 63 | "sys": { 64 | "space": { 65 | "sys": { 66 | "type": "Link", 67 | "linkType": "Space", 68 | "id": "smf0sqiu0c5s" 69 | } 70 | }, 71 | "id": "5GiLOZvY7SiMeUIgIIAssS", 72 | "type": "Entry", 73 | "createdAt": "2017-06-20T14:03:29.319Z", 74 | "updatedAt": "2017-06-20T14:03:29.319Z", 75 | "revision": 1, 76 | "contentType": { 77 | "sys": { 78 | "type": "Link", 79 | "linkType": "ContentType", 80 | "id": "singleRecord" 81 | } 82 | } 83 | }, 84 | "fields": { 85 | "textBody": { 86 | "en-US": "INITIAL TEXT BODY" 87 | } 88 | } 89 | }, 90 | { 91 | "sys": { 92 | "space": { 93 | "sys": { 94 | "type": "Link", 95 | "linkType": "Space", 96 | "id": "smf0sqiu0c5s" 97 | } 98 | }, 99 | "id": "2eXtYKYxYAue2IQgaucoYW", 100 | "type": "Entry", 101 | "createdAt": "2017-06-20T12:51:13.910Z", 102 | "updatedAt": "2017-06-20T12:51:13.910Z", 103 | "revision": 1, 104 | "contentType": { 105 | "sys": { 106 | "type": "Link", 107 | "linkType": "ContentType", 108 | "id": "singleRecord" 109 | } 110 | } 111 | }, 112 | "fields": { 113 | "textBody": { 114 | "en-US": "4" 115 | } 116 | } 117 | } 118 | ], 119 | "nextSyncUrl": "https://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=clear-field-sync-token" 120 | } 121 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ContentStubs/single-post.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "space": { 4 | "sys": { 5 | "type": "Link", 6 | "linkType": "Space", 7 | "id": "dqpnpm0n4e75" 8 | } 9 | }, 10 | "id": "1asN98Ph3mUiCYIYiiqwko", 11 | "type": "Entry", 12 | "createdAt": "2015-02-05T12:11:56.916Z", 13 | "updatedAt": "2015-02-05T12:11:56.916Z", 14 | "revision": 1, 15 | "contentType": { 16 | "sys": { 17 | "type": "Link", 18 | "linkType": "ContentType", 19 | "id": "2wKn6yEnZewu2SCCkus4as" 20 | } 21 | }, 22 | "locale": "en-US" 23 | }, 24 | "fields": { 25 | "title": "Down the Rabbit Hole", 26 | "slug": "down-the-rabbit-hole", 27 | "author": [ 28 | { 29 | "sys": { 30 | "type": "Link", 31 | "linkType": "Entry", 32 | "id": "6EczfGnuHCIYGGwEwIqiq2" 33 | } 34 | } 35 | ], 36 | "body": "Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, 'and what is the use of a book,' thought Alice 'without pictures or conversation?' So she was considering in her own mind (as well as she could, for the hot day made her feel very sleepy and stupid), whether the pleasure of making a daisy- chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her. There was nothing so very remarkable in that; nor did Alice think it so very much out of the way to hear the Rabbit say to itself, `Oh dear! Oh dear! I shall be late!' (when she thought it over afterwards, it occurred to her that she ought to have wondered at this, but at the time it all seemed quite natural); but when the Rabbit actually took a watch out of its waistcoat- pocket, and looked at it, and then hurried on, Alice started to her feet, for it flashed across her mind that she had never before seen a rabbit with either a waistcoat-pocket, or a watch to take out of it, and burning with curiosity, she ran across the field after it, and fortunately was...", 37 | "category": [ 38 | { 39 | "sys": { 40 | "type": "Link", 41 | "linkType": "Entry", 42 | "id": "6XL7nwqRZ6yEw0cUe4y0y6" 43 | } 44 | }, 45 | { 46 | "sys": { 47 | "type": "Link", 48 | "linkType": "Entry", 49 | "id": "FJlJfypzaewiwyukGi2kI" 50 | } 51 | } 52 | ], 53 | "tags": [ 54 | "Literature", 55 | "fantasy", 56 | "children", 57 | "novel", 58 | "fiction", 59 | "animals", 60 | "rabbit", 61 | "girl" 62 | ], 63 | "featuredImage": { 64 | "sys": { 65 | "type": "Link", 66 | "linkType": "Asset", 67 | "id": "bXvdSYHB3Guy2uUmuEco8" 68 | } 69 | }, 70 | "date": "1865-11-26", 71 | "comments": false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/PersistenceStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentStore.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by JP Wright on 16.06.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Protocol for persistence stores used by `SynchronizationManager` 12 | public protocol PersistenceStore { 13 | /** 14 | Create a new object of the given type. 15 | 16 | - parameter type: The type of which a new object should be created 17 | 18 | - throws: If a invalid type was specified 19 | 20 | - returns: A newly created object of the given type 21 | */ 22 | func create(type: Any.Type) throws -> T 23 | 24 | /** 25 | Delete objects of the given type which also match the predicate. 26 | 27 | - parameter type: The type of which objects should be deleted 28 | - parameter predicate: The predicate used for matching objects to delete 29 | 30 | - throws: If an invalid type was specified 31 | */ 32 | func delete(type: Any.Type, predicate: NSPredicate) throws 33 | 34 | /** 35 | Fetches all objects of a specific type which also match the predicate. 36 | 37 | - parameter type: The type of which objects should be fetched 38 | - parameter predicate: The predicate used for matching object to fetch 39 | 40 | - throws: If an invalid type was specified 41 | 42 | - returns: An array of matching objects 43 | */ 44 | func fetchAll(type: Any.Type, predicate: NSPredicate) throws -> [T] 45 | 46 | /** 47 | Fetches one object of a specific type which matches the predicate. 48 | 49 | - parameter type: Type of which object should be fetched. 50 | - parameter predicate: The predicate used for matching object to fetch. 51 | 52 | - throws: If an invalid type was specified 53 | 54 | - returns: Matching object 55 | */ 56 | func fetchOne(type: Any.Type, predicate: NSPredicate) throws -> T 57 | 58 | /** 59 | Returns an array of names of properties the given type stores persistently. 60 | 61 | This should omit any properties returned by `relationshipsFor(type:)`. 62 | 63 | - parameter type: The type of which properties should be returned for 64 | 65 | - throws: If an invalid type was specified 66 | 67 | - returns: An array of property names 68 | */ 69 | func properties(for type: Any.Type) throws -> [String] 70 | 71 | /** 72 | Returns an array of names of properties for any relationship the given type stores persistently. 73 | 74 | - parameter type: The type of which properties should be returned for 75 | 76 | - throws: If an invalid type was specified 77 | 78 | - returns: An array of property names 79 | */ 80 | func relationships(for type: Any.Type) throws -> [String] 81 | 82 | /** 83 | Performs the actual save to the persistence store. 84 | 85 | - throws: If any error occured during the save operation 86 | */ 87 | func save() throws 88 | 89 | /// Deletes all the data in the database 90 | func wipe() throws 91 | 92 | func performBlock(block: @escaping () -> Void) 93 | 94 | func performAndWait(block: @escaping () -> Void) 95 | 96 | /// Called **before** the main `.sqlite` is swapped. Gives you the full file URL. 97 | func onStorePreseedingWillBegin(at storeFileURL: URL) throws 98 | 99 | /// Called **after** the main file is in place. 100 | func onStorePreseedingCompleted(at seededFileURL: URL) throws 101 | } 102 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Relationships/Relationship.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentfulPersistenceSwift 3 | // 4 | 5 | import Contentful 6 | 7 | /// Represents a relationship between two entries. 8 | struct Relationship: Codable, Equatable, Identifiable { 9 | 10 | typealias ID = String 11 | typealias ParentId = String 12 | typealias FieldName = String 13 | typealias LocaleCode = String? 14 | 15 | let id: ID 16 | let parentType: ContentTypeId 17 | let parentId: ParentId 18 | let fieldName: FieldName 19 | let children: RelationshipChildren 20 | 21 | var localeCode: LocaleCode { 22 | Self.localeCode(for: children) 23 | } 24 | 25 | init(parentType: ContentTypeId, parentId: ParentId, fieldName: FieldName, childId: RelationshipChildId) { 26 | self.init(parentType: parentType, parentId: parentId, fieldName: fieldName, children: .one(childId)) 27 | } 28 | 29 | init(parentType: ContentTypeId, parentId: ParentId, fieldName: FieldName, childIds: [RelationshipChildId]) { 30 | self.init(parentType: parentType, parentId: parentId, fieldName: fieldName, children: .many(childIds)) 31 | } 32 | 33 | private init(parentType: ContentTypeId, parentId: ParentId, fieldName: FieldName, children: RelationshipChildren) { 34 | self.parentType = parentType 35 | self.parentId = parentId 36 | self.fieldName = fieldName 37 | self.children = children 38 | self.id = [parentType, parentId, fieldName, Self.localeCode(for: children) ?? "-"].joined(separator: ",") 39 | } 40 | 41 | private static func localeCode(for children: RelationshipChildren) -> LocaleCode { 42 | switch children { 43 | case .one(let childId): 44 | return childId.localeCode 45 | case .many(let childIds): 46 | return childIds.first?.localeCode 47 | } 48 | } 49 | 50 | } 51 | 52 | enum RelationshipChildren: Codable, Equatable { 53 | 54 | private enum CodingKeys: CodingKey { 55 | case kind 56 | case value 57 | } 58 | 59 | private enum Kind: String, Codable { 60 | case one 61 | case many 62 | } 63 | 64 | case one(RelationshipChildId) 65 | case many([RelationshipChildId]) 66 | 67 | var elements: [RelationshipChildId] { 68 | switch self { 69 | case .one(let relationshipChildId): 70 | return [relationshipChildId] 71 | case .many(let relationshipChildIds): 72 | return relationshipChildIds 73 | } 74 | } 75 | 76 | // MARK: Codable 77 | 78 | init(from decoder: Decoder) throws { 79 | let container = try decoder.container(keyedBy: CodingKeys.self) 80 | let kind = try container.decode(Kind.self, forKey: .kind) 81 | 82 | switch kind { 83 | case .one: 84 | self = .one(try container.decode(RelationshipChildId.self, forKey: .value)) 85 | case .many: 86 | self = .many(try container.decode([RelationshipChildId].self, forKey: .value)) 87 | } 88 | } 89 | 90 | func encode(to encoder: Encoder) throws { 91 | var container = encoder.container(keyedBy: CodingKeys.self) 92 | 93 | switch self { 94 | case .one(let childId): 95 | try container.encode(Kind.one, forKey: .kind) 96 | try container.encode(childId, forKey: .value) 97 | case .many(let childIds): 98 | try container.encode(Kind.many, forKey: .kind) 99 | try container.encode(childIds, forKey: .value) 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/shared-linked-asset.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "12f37qR1CGOOcqoWOgqC2o", 16 | "type": "Entry", 17 | "createdAt": "2017-12-20T19:04:05.031Z", 18 | "updatedAt": "2017-12-20T19:04:05.031Z", 19 | "revision": 1, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "singleRecord" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "textBody": { 30 | "en-US": "Second record with shared asset" 31 | }, 32 | "assetLinkField": { 33 | "en-US": { 34 | "sys": { 35 | "type": "Link", 36 | "linkType": "Asset", 37 | "id": "6Wsz8owhtCGSICg44IUYAm" 38 | } 39 | } 40 | } 41 | } 42 | }, 43 | { 44 | "sys": { 45 | "space": { 46 | "sys": { 47 | "type": "Link", 48 | "linkType": "Space", 49 | "id": "smf0sqiu0c5s" 50 | } 51 | }, 52 | "id": "4DiVtM6u08uMA2QSgg0OoY", 53 | "type": "Entry", 54 | "createdAt": "2017-12-20T19:03:45.612Z", 55 | "updatedAt": "2017-12-20T19:03:45.612Z", 56 | "revision": 1, 57 | "contentType": { 58 | "sys": { 59 | "type": "Link", 60 | "linkType": "ContentType", 61 | "id": "singleRecord" 62 | } 63 | } 64 | }, 65 | "fields": { 66 | "textBody": { 67 | "en-US": "First record with shared asset" 68 | }, 69 | "assetLinkField": { 70 | "en-US": { 71 | "sys": { 72 | "type": "Link", 73 | "linkType": "Asset", 74 | "id": "6Wsz8owhtCGSICg44IUYAm" 75 | } 76 | } 77 | } 78 | } 79 | }, 80 | { 81 | "sys": { 82 | "space": { 83 | "sys": { 84 | "type": "Link", 85 | "linkType": "Space", 86 | "id": "smf0sqiu0c5s" 87 | } 88 | }, 89 | "id": "6Wsz8owhtCGSICg44IUYAm", 90 | "type": "Asset", 91 | "createdAt": "2017-11-27T09:23:17.444Z", 92 | "updatedAt": "2017-11-27T09:23:17.444Z", 93 | "revision": 1 94 | }, 95 | "fields": { 96 | "title": { 97 | "en-US": "First asset in array" 98 | }, 99 | "file": { 100 | "en-US": { 101 | "url": "//images.contentful.com/smf0sqiu0c5s/6Wsz8owhtCGSICg44IUYAm/4061427d7cba2050a87579033efb3fb9/dog.jpg", 102 | "details": { 103 | "size": 79169, 104 | "image": { 105 | "width": 830, 106 | "height": 830 107 | } 108 | }, 109 | "fileName": "dog.jpg", 110 | "contentType": "image/jpeg" 111 | } 112 | } 113 | } 114 | } 115 | ], 116 | "nextSyncUrl": "http://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=wonDrcKnRgcSOF4-wrDCgcKefWzCgsOxwrfCq8KOfMOdXUPCvEnChwEEO8KFwqHDj8KpJMKkJn0Xw5LCgcO3w5oOLkJxwpPDr8KZV1rCh3nDi8Obwq0-wp1EwpRGwrApMWvDkjNgI3TCgWvDmH4zVFHDpBgkwpoCflTDg8KqbmHCpMKuU8KTHcONX8Ogw67CusO_wo3DucO9w6jDj8Kiw6TDpMKUw44bwr_CqsOqMsOkwqYWKBrCvcK0BlzDmsOtw4nDgMK2" 117 | } 118 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/TestHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHelpers.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by JP Wright on 12.07.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import XCTest 12 | 13 | class TestHelpers { 14 | 15 | static func jsonData(_ fileName: String) -> Data { 16 | let bundle = Bundle(for: TestHelpers.self) 17 | let urlPath = bundle.path(forResource: fileName, ofType: "json")! 18 | return try! Data(contentsOf: URL(fileURLWithPath: urlPath)) 19 | } 20 | 21 | static func managedObjectContext(forMOMInTestBundleNamed momName: String) -> NSManagedObjectContext { 22 | let modelURL = Bundle(for: TestHelpers.self).url(forResource: momName, withExtension: "momd") 23 | let mom = NSManagedObjectModel(contentsOf: modelURL!) 24 | XCTAssertNotNil(mom) 25 | 26 | let psc = NSPersistentStoreCoordinator(managedObjectModel: mom!) 27 | 28 | do { 29 | // Store in memory so there is no caching between test methods. 30 | let store = try psc.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil) 31 | XCTAssertNotNil(store) 32 | } catch { 33 | XCTAssert(false, "Recreating the persistent store SQL files should not throw an error") 34 | } 35 | 36 | let managedObjectContext = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType) 37 | managedObjectContext.persistentStoreCoordinator = psc 38 | return managedObjectContext 39 | } 40 | 41 | /// Spins up a file-backed Core Data stack for testing. 42 | /// 43 | /// - Parameter momName: The `.momd` name in your test bundle. 44 | /// - Returns: A tuple `(context, sqliteURL)` where `context` is 45 | /// an `NSManagedObjectContext` backed by the sqlite at `sqliteURL`. 46 | static func sqliteBackedContext(forMOMInTestBundleNamed momName: String) 47 | throws 48 | -> (context: NSManagedObjectContext, sqliteURL: URL, storeContainerPath: URL) 49 | { 50 | let bundle = Bundle(for: TestHelpers.self) 51 | guard 52 | let modelURL = bundle.url(forResource: momName, withExtension: "momd"), 53 | let mom = NSManagedObjectModel(contentsOf: modelURL) 54 | else { 55 | XCTFail("Couldn’t load model \(momName).momd from test bundle") 56 | fatalError() 57 | } 58 | 59 | let psc = NSPersistentStoreCoordinator(managedObjectModel: mom) 60 | let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) 61 | context.persistentStoreCoordinator = psc 62 | 63 | // Create a unique temp directory for the sqlite file 64 | let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) 65 | .appendingPathComponent(UUID().uuidString) 66 | try FileManager.default.createDirectory( 67 | at: tempDir, 68 | withIntermediateDirectories: true, 69 | attributes: nil 70 | ) 71 | 72 | let sqliteURL = tempDir.appendingPathComponent("\(momName).sqlite") 73 | do { 74 | let store = try psc.addPersistentStore( 75 | ofType: NSSQLiteStoreType, 76 | configurationName: nil, 77 | at: sqliteURL, 78 | options: nil 79 | ) 80 | XCTAssertNotNil(store, "Failed to add SQLite store at \(sqliteURL)") 81 | } catch { 82 | XCTFail("Error adding SQLite store: \(error)") 83 | } 84 | 85 | return (context, sqliteURL, tempDir) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /*! Jazzy - https://github.com/realm/jazzy 2 | * Copyright Realm Inc. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | /* Credit to https://gist.github.com/wataru420/2048287 */ 6 | .highlight .c { 7 | color: #999988; 8 | font-style: italic; } 9 | 10 | .highlight .err { 11 | color: #a61717; 12 | background-color: #e3d2d2; } 13 | 14 | .highlight .k { 15 | color: #000000; 16 | font-weight: bold; } 17 | 18 | .highlight .o { 19 | color: #000000; 20 | font-weight: bold; } 21 | 22 | .highlight .cm { 23 | color: #999988; 24 | font-style: italic; } 25 | 26 | .highlight .cp { 27 | color: #999999; 28 | font-weight: bold; } 29 | 30 | .highlight .c1 { 31 | color: #999988; 32 | font-style: italic; } 33 | 34 | .highlight .cs { 35 | color: #999999; 36 | font-weight: bold; 37 | font-style: italic; } 38 | 39 | .highlight .gd { 40 | color: #000000; 41 | background-color: #ffdddd; } 42 | 43 | .highlight .gd .x { 44 | color: #000000; 45 | background-color: #ffaaaa; } 46 | 47 | .highlight .ge { 48 | color: #000000; 49 | font-style: italic; } 50 | 51 | .highlight .gr { 52 | color: #aa0000; } 53 | 54 | .highlight .gh { 55 | color: #999999; } 56 | 57 | .highlight .gi { 58 | color: #000000; 59 | background-color: #ddffdd; } 60 | 61 | .highlight .gi .x { 62 | color: #000000; 63 | background-color: #aaffaa; } 64 | 65 | .highlight .go { 66 | color: #888888; } 67 | 68 | .highlight .gp { 69 | color: #555555; } 70 | 71 | .highlight .gs { 72 | font-weight: bold; } 73 | 74 | .highlight .gu { 75 | color: #aaaaaa; } 76 | 77 | .highlight .gt { 78 | color: #aa0000; } 79 | 80 | .highlight .kc { 81 | color: #000000; 82 | font-weight: bold; } 83 | 84 | .highlight .kd { 85 | color: #000000; 86 | font-weight: bold; } 87 | 88 | .highlight .kp { 89 | color: #000000; 90 | font-weight: bold; } 91 | 92 | .highlight .kr { 93 | color: #000000; 94 | font-weight: bold; } 95 | 96 | .highlight .kt { 97 | color: #445588; } 98 | 99 | .highlight .m { 100 | color: #009999; } 101 | 102 | .highlight .s { 103 | color: #d14; } 104 | 105 | .highlight .na { 106 | color: #008080; } 107 | 108 | .highlight .nb { 109 | color: #0086B3; } 110 | 111 | .highlight .nc { 112 | color: #445588; 113 | font-weight: bold; } 114 | 115 | .highlight .no { 116 | color: #008080; } 117 | 118 | .highlight .ni { 119 | color: #800080; } 120 | 121 | .highlight .ne { 122 | color: #990000; 123 | font-weight: bold; } 124 | 125 | .highlight .nf { 126 | color: #990000; } 127 | 128 | .highlight .nn { 129 | color: #555555; } 130 | 131 | .highlight .nt { 132 | color: #000080; } 133 | 134 | .highlight .nv { 135 | color: #008080; } 136 | 137 | .highlight .ow { 138 | color: #000000; 139 | font-weight: bold; } 140 | 141 | .highlight .w { 142 | color: #bbbbbb; } 143 | 144 | .highlight .mf { 145 | color: #009999; } 146 | 147 | .highlight .mh { 148 | color: #009999; } 149 | 150 | .highlight .mi { 151 | color: #009999; } 152 | 153 | .highlight .mo { 154 | color: #009999; } 155 | 156 | .highlight .sb { 157 | color: #d14; } 158 | 159 | .highlight .sc { 160 | color: #d14; } 161 | 162 | .highlight .sd { 163 | color: #d14; } 164 | 165 | .highlight .s2 { 166 | color: #d14; } 167 | 168 | .highlight .se { 169 | color: #d14; } 170 | 171 | .highlight .sh { 172 | color: #d14; } 173 | 174 | .highlight .si { 175 | color: #d14; } 176 | 177 | .highlight .sx { 178 | color: #d14; } 179 | 180 | .highlight .sr { 181 | color: #009926; } 182 | 183 | .highlight .s1 { 184 | color: #d14; } 185 | 186 | .highlight .ss { 187 | color: #990073; } 188 | 189 | .highlight .bp { 190 | color: #999999; } 191 | 192 | .highlight .vc { 193 | color: #008080; } 194 | 195 | .highlight .vg { 196 | color: #008080; } 197 | 198 | .highlight .vi { 199 | color: #008080; } 200 | 201 | .highlight .il { 202 | color: #009999; } 203 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/linked-assets-array.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "2JFSeiPTZYm4goMSUeYSCU", 16 | "type": "Entry", 17 | "createdAt": "2017-11-27T09:33:47.250Z", 18 | "updatedAt": "2017-11-27T09:33:47.250Z", 19 | "revision": 1, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "singleRecord" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "textBody": { 30 | "en-US": "Record with array of assets" 31 | }, 32 | "assetsArrayLinkField": { 33 | "en-US": [ 34 | { 35 | "sys": { 36 | "type": "Link", 37 | "linkType": "Asset", 38 | "id": "6Wsz8owhtCGSICg44IUYAm" 39 | } 40 | }, 41 | { 42 | "sys": { 43 | "type": "Link", 44 | "linkType": "Asset", 45 | "id": "6G30n5w3sWcS6yEWo8q6CI" 46 | } 47 | } 48 | ] 49 | } 50 | } 51 | }, 52 | { 53 | "sys": { 54 | "space": { 55 | "sys": { 56 | "type": "Link", 57 | "linkType": "Space", 58 | "id": "smf0sqiu0c5s" 59 | } 60 | }, 61 | "id": "6G30n5w3sWcS6yEWo8q6CI", 62 | "type": "Asset", 63 | "createdAt": "2017-11-27T09:24:19.117Z", 64 | "updatedAt": "2017-11-27T09:24:19.117Z", 65 | "revision": 1 66 | }, 67 | "fields": { 68 | "title": { 69 | "en-US": "Second asset in array" 70 | }, 71 | "file": { 72 | "en-US": { 73 | "url": "//images.contentful.com/smf0sqiu0c5s/6G30n5w3sWcS6yEWo8q6CI/6d875fdfb4bbdb7fce1a96d7d8ccb559/cat.jpg", 74 | "details": { 75 | "size": 1168727, 76 | "image": { 77 | "width": 2067, 78 | "height": 1163 79 | } 80 | }, 81 | "fileName": "cat.jpg", 82 | "contentType": "image/jpeg" 83 | } 84 | } 85 | } 86 | }, 87 | { 88 | "sys": { 89 | "space": { 90 | "sys": { 91 | "type": "Link", 92 | "linkType": "Space", 93 | "id": "smf0sqiu0c5s" 94 | } 95 | }, 96 | "id": "6Wsz8owhtCGSICg44IUYAm", 97 | "type": "Asset", 98 | "createdAt": "2017-11-27T09:23:17.444Z", 99 | "updatedAt": "2017-11-27T09:23:17.444Z", 100 | "revision": 1 101 | }, 102 | "fields": { 103 | "title": { 104 | "en-US": "First asset in array" 105 | }, 106 | "file": { 107 | "en-US": { 108 | "url": "//images.contentful.com/smf0sqiu0c5s/6Wsz8owhtCGSICg44IUYAm/4061427d7cba2050a87579033efb3fb9/dog.jpg", 109 | "details": { 110 | "size": 79169, 111 | "image": { 112 | "width": 830, 113 | "height": 830 114 | } 115 | }, 116 | "fileName": "dog.jpg", 117 | "contentType": "image/jpeg" 118 | } 119 | } 120 | } 121 | } 122 | ], 123 | "nextSyncUrl": "http://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=wonDrcKnRgcSOF4-wrDCgcKefWzCgsOxwrfCq8KOfMOdXUPCvEnChwEEO8KFwqHDj8KpJMKkJn0Xw5LCgcO3w5oOLkJxwpPDr8KZV1rCh3nDi8Obwq0-wp1EwpRGwrApMWvDkjNgI3TCgWvDmH4zVFHDpBgkwpoCflTDg8KqbmHCpMKuU8KTHcONX8Ogw67CusO_wo3DucO9w6jDj8Kiw6TDpMKUw44bwr_CqsOqMsOkwqYWKBrCvcK0BlzDmsOtw4nDgMK2" 124 | } 125 | -------------------------------------------------------------------------------- /docs/docsets/ContentfulPersistence.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /*! Jazzy - https://github.com/realm/jazzy 2 | * Copyright Realm Inc. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | /* Credit to https://gist.github.com/wataru420/2048287 */ 6 | .highlight .c { 7 | color: #999988; 8 | font-style: italic; } 9 | 10 | .highlight .err { 11 | color: #a61717; 12 | background-color: #e3d2d2; } 13 | 14 | .highlight .k { 15 | color: #000000; 16 | font-weight: bold; } 17 | 18 | .highlight .o { 19 | color: #000000; 20 | font-weight: bold; } 21 | 22 | .highlight .cm { 23 | color: #999988; 24 | font-style: italic; } 25 | 26 | .highlight .cp { 27 | color: #999999; 28 | font-weight: bold; } 29 | 30 | .highlight .c1 { 31 | color: #999988; 32 | font-style: italic; } 33 | 34 | .highlight .cs { 35 | color: #999999; 36 | font-weight: bold; 37 | font-style: italic; } 38 | 39 | .highlight .gd { 40 | color: #000000; 41 | background-color: #ffdddd; } 42 | 43 | .highlight .gd .x { 44 | color: #000000; 45 | background-color: #ffaaaa; } 46 | 47 | .highlight .ge { 48 | color: #000000; 49 | font-style: italic; } 50 | 51 | .highlight .gr { 52 | color: #aa0000; } 53 | 54 | .highlight .gh { 55 | color: #999999; } 56 | 57 | .highlight .gi { 58 | color: #000000; 59 | background-color: #ddffdd; } 60 | 61 | .highlight .gi .x { 62 | color: #000000; 63 | background-color: #aaffaa; } 64 | 65 | .highlight .go { 66 | color: #888888; } 67 | 68 | .highlight .gp { 69 | color: #555555; } 70 | 71 | .highlight .gs { 72 | font-weight: bold; } 73 | 74 | .highlight .gu { 75 | color: #aaaaaa; } 76 | 77 | .highlight .gt { 78 | color: #aa0000; } 79 | 80 | .highlight .kc { 81 | color: #000000; 82 | font-weight: bold; } 83 | 84 | .highlight .kd { 85 | color: #000000; 86 | font-weight: bold; } 87 | 88 | .highlight .kp { 89 | color: #000000; 90 | font-weight: bold; } 91 | 92 | .highlight .kr { 93 | color: #000000; 94 | font-weight: bold; } 95 | 96 | .highlight .kt { 97 | color: #445588; } 98 | 99 | .highlight .m { 100 | color: #009999; } 101 | 102 | .highlight .s { 103 | color: #d14; } 104 | 105 | .highlight .na { 106 | color: #008080; } 107 | 108 | .highlight .nb { 109 | color: #0086B3; } 110 | 111 | .highlight .nc { 112 | color: #445588; 113 | font-weight: bold; } 114 | 115 | .highlight .no { 116 | color: #008080; } 117 | 118 | .highlight .ni { 119 | color: #800080; } 120 | 121 | .highlight .ne { 122 | color: #990000; 123 | font-weight: bold; } 124 | 125 | .highlight .nf { 126 | color: #990000; } 127 | 128 | .highlight .nn { 129 | color: #555555; } 130 | 131 | .highlight .nt { 132 | color: #000080; } 133 | 134 | .highlight .nv { 135 | color: #008080; } 136 | 137 | .highlight .ow { 138 | color: #000000; 139 | font-weight: bold; } 140 | 141 | .highlight .w { 142 | color: #bbbbbb; } 143 | 144 | .highlight .mf { 145 | color: #009999; } 146 | 147 | .highlight .mh { 148 | color: #009999; } 149 | 150 | .highlight .mi { 151 | color: #009999; } 152 | 153 | .highlight .mo { 154 | color: #009999; } 155 | 156 | .highlight .sb { 157 | color: #d14; } 158 | 159 | .highlight .sc { 160 | color: #d14; } 161 | 162 | .highlight .sd { 163 | color: #d14; } 164 | 165 | .highlight .s2 { 166 | color: #d14; } 167 | 168 | .highlight .se { 169 | color: #d14; } 170 | 171 | .highlight .sh { 172 | color: #d14; } 173 | 174 | .highlight .si { 175 | color: #d14; } 176 | 177 | .highlight .sx { 178 | color: #d14; } 179 | 180 | .highlight .sr { 181 | color: #009926; } 182 | 183 | .highlight .s1 { 184 | color: #d14; } 185 | 186 | .highlight .ss { 187 | color: #990073; } 188 | 189 | .highlight .bp { 190 | color: #999999; } 191 | 192 | .highlight .vc { 193 | color: #008080; } 194 | 195 | .highlight .vg { 196 | color: #008080; } 197 | 198 | .highlight .vi { 199 | color: #008080; } 200 | 201 | .highlight .il { 202 | color: #009999; } 203 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.1.3.2) 9 | base64 10 | bigdecimal 11 | concurrent-ruby (~> 1.0, >= 1.0.2) 12 | connection_pool (>= 2.2.5) 13 | drb 14 | i18n (>= 1.6, < 2) 15 | minitest (>= 5.1) 16 | mutex_m 17 | tzinfo (~> 2.0) 18 | addressable (2.8.6) 19 | public_suffix (>= 2.0.2, < 6.0) 20 | algoliasearch (1.27.5) 21 | httpclient (~> 2.8, >= 2.8.3) 22 | json (>= 1.5.1) 23 | atomos (0.1.3) 24 | base64 (0.2.0) 25 | bigdecimal (3.1.7) 26 | claide (1.1.0) 27 | clamp (1.3.2) 28 | cocoapods (1.15.2) 29 | addressable (~> 2.8) 30 | claide (>= 1.0.2, < 2.0) 31 | cocoapods-core (= 1.15.2) 32 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 33 | cocoapods-downloader (>= 2.1, < 3.0) 34 | cocoapods-plugins (>= 1.0.0, < 2.0) 35 | cocoapods-search (>= 1.0.0, < 2.0) 36 | cocoapods-trunk (>= 1.6.0, < 2.0) 37 | cocoapods-try (>= 1.1.0, < 2.0) 38 | colored2 (~> 3.1) 39 | escape (~> 0.0.4) 40 | fourflusher (>= 2.3.0, < 3.0) 41 | gh_inspector (~> 1.0) 42 | molinillo (~> 0.8.0) 43 | nap (~> 1.0) 44 | ruby-macho (>= 2.3.0, < 3.0) 45 | xcodeproj (>= 1.23.0, < 2.0) 46 | cocoapods-core (1.15.2) 47 | activesupport (>= 5.0, < 8) 48 | addressable (~> 2.8) 49 | algoliasearch (~> 1.0) 50 | concurrent-ruby (~> 1.1) 51 | fuzzy_match (~> 2.0.4) 52 | nap (~> 1.0) 53 | netrc (~> 0.11) 54 | public_suffix (~> 4.0) 55 | typhoeus (~> 1.0) 56 | cocoapods-deintegrate (1.0.5) 57 | cocoapods-downloader (2.1) 58 | cocoapods-plugins (1.0.0) 59 | nap 60 | cocoapods-search (1.0.1) 61 | cocoapods-trunk (1.6.0) 62 | nap (>= 0.8, < 2.0) 63 | netrc (~> 0.11) 64 | cocoapods-try (1.2.0) 65 | colored2 (3.1.2) 66 | concurrent-ruby (1.2.3) 67 | connection_pool (2.4.1) 68 | dotenv (3.1.0) 69 | drb (2.2.1) 70 | escape (0.0.4) 71 | ethon (0.16.0) 72 | ffi (>= 1.15.0) 73 | ffi (1.16.3) 74 | fourflusher (2.3.1) 75 | fuzzy_match (2.0.4) 76 | gh_inspector (1.1.3) 77 | httpclient (2.8.3) 78 | i18n (1.14.4) 79 | concurrent-ruby (~> 1.0) 80 | jazzy (0.14.4) 81 | cocoapods (~> 1.5) 82 | mustache (~> 1.1) 83 | open4 (~> 1.3) 84 | redcarpet (~> 3.4) 85 | rexml (~> 3.2) 86 | rouge (>= 2.0.6, < 5.0) 87 | sassc (~> 2.1) 88 | sqlite3 (~> 1.3) 89 | xcinvoke (~> 0.3.0) 90 | json (2.7.2) 91 | liferaft (0.0.6) 92 | minitest (5.22.3) 93 | molinillo (0.8.0) 94 | mustache (1.1.1) 95 | mutex_m (0.2.0) 96 | nanaimo (0.3.0) 97 | nap (1.1.0) 98 | netrc (0.11.0) 99 | nkf (0.2.0) 100 | nokogiri (1.16.4-arm64-darwin) 101 | racc (~> 1.4) 102 | open4 (1.3.4) 103 | public_suffix (4.0.7) 104 | racc (1.7.3) 105 | redcarpet (3.6.0) 106 | rexml (3.2.6) 107 | rouge (2.0.7) 108 | ruby-macho (2.5.1) 109 | sassc (2.4.0) 110 | ffi (~> 1.9) 111 | slather (2.8.0) 112 | CFPropertyList (>= 2.2, < 4) 113 | activesupport 114 | clamp (~> 1.3) 115 | nokogiri (>= 1.14.3) 116 | xcodeproj (~> 1.21) 117 | sqlite3 (1.7.3-arm64-darwin) 118 | typhoeus (1.4.1) 119 | ethon (>= 0.9.0) 120 | tzinfo (2.0.6) 121 | concurrent-ruby (~> 1.0) 122 | xcinvoke (0.3.0) 123 | liferaft (~> 0.0.6) 124 | xcodeproj (1.24.0) 125 | CFPropertyList (>= 2.3.3, < 4.0) 126 | atomos (~> 0.1.3) 127 | claide (>= 1.0.2, < 2.0) 128 | colored2 (~> 3.1) 129 | nanaimo (~> 0.3.0) 130 | rexml (~> 3.2.4) 131 | xcpretty (0.3.0) 132 | rouge (~> 2.0.7) 133 | 134 | PLATFORMS 135 | arm64-darwin-21 136 | 137 | DEPENDENCIES 138 | cocoapods 139 | dotenv 140 | jazzy 141 | slather 142 | xcpretty 143 | 144 | BUNDLED WITH 145 | 2.4.3 146 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/BundledPreseededDatabaseTests/CoreDataStorePreseedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataStorePreseedTests.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Marius Kurgonas on 30/05/2025. 6 | // Copyright © 2025 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ContentfulPersistence 11 | import CoreData 12 | 13 | class CoreDataStorePreseedTests: XCTestCase { 14 | var ctx: NSManagedObjectContext! 15 | var store: CoreDataStore! 16 | var sqliteURL: URL! 17 | var syncManager: SynchronizationManager! 18 | 19 | override func setUpWithError() throws { 20 | // Obtain a file-backed Core Data stack and its SQLite URL: 21 | let data = try TestHelpers.sqliteBackedContext( 22 | forMOMInTestBundleNamed: "Test" 23 | ) 24 | ctx = data.context 25 | sqliteURL = data.sqliteURL 26 | 27 | store = CoreDataStore(context: ctx) 28 | 29 | let entryTypes: [EntryPersistable.Type] = [Author.self, Category.self, Post.self] 30 | 31 | let persistenceModel = PersistenceModel(spaceType: SyncInfo.self, assetType: Asset.self, entryTypes: entryTypes) 32 | 33 | syncManager = SynchronizationManager(localizationScheme: .default, persistenceStore: store, persistenceModel: persistenceModel) 34 | } 35 | 36 | func testWillBegin_removesSideCarsAndStore() throws { 37 | let directoryName = "PreseedJSONFiles" 38 | let testBundle = Bundle(for: Swift.type(of: self)) 39 | do { 40 | try syncManager.seedDBFromJSONFiles(in: directoryName, in: testBundle) 41 | } catch let error { 42 | XCTFail(error.localizedDescription) 43 | } 44 | 45 | // At this point Core Data should have created: 46 | // .sqlite-wal and .sqlite-shm 47 | let wal = URL(fileURLWithPath: sqliteURL.path + "-wal") 48 | let shm = URL(fileURLWithPath: sqliteURL.path + "-shm") 49 | XCTAssertTrue(FileManager.default.fileExists(atPath: wal.path), 50 | "WAL file should exist after saving.") 51 | XCTAssertTrue(FileManager.default.fileExists(atPath: shm.path), 52 | "SHM file should exist after saving.") 53 | XCTAssertEqual(ctx.persistentStoreCoordinator?.persistentStores.count, 1, 54 | "There should be exactly one persistent store before wiping.") 55 | 56 | // 2) Call the hook under test: 57 | try store.onStorePreseedingWillBegin(at: sqliteURL) 58 | 59 | // 3) Verify WAL and SHM have been deleted: 60 | XCTAssertFalse(FileManager.default.fileExists(atPath: wal.path), 61 | "WAL must be removed by onStorePreseedingWillBegin.") 62 | XCTAssertFalse(FileManager.default.fileExists(atPath: shm.path), 63 | "SHM must be removed by onStorePreseedingWillBegin.") 64 | 65 | // 4) Verify the persistent store was removed from the coordinator: 66 | XCTAssertEqual(ctx.persistentStoreCoordinator?.persistentStores.count, 0, 67 | "Persistent store should be removed by onStorePreseedingWillBegin.") 68 | } 69 | 70 | func testCompleted_readdsStoreAndResetsContext() throws { 71 | // First remove the store so the coordinator is empty: 72 | try store.onStorePreseedingWillBegin(at: sqliteURL) 73 | XCTAssertEqual(ctx.persistentStoreCoordinator?.persistentStores.count, 0, 74 | "Store should be gone after onStorePreseedingWillBegin.") 75 | 76 | // Now re-open the store and reset the context: 77 | try store.onStorePreseedingCompleted(at: sqliteURL) 78 | 79 | // Verify the store was re-added: 80 | XCTAssertEqual(ctx.persistentStoreCoordinator?.persistentStores.count, 1, 81 | "Persistent store should be re-added by onStorePreseedingCompleted.") 82 | 83 | // Verify the context was reset (no remaining registered objects): 84 | XCTAssertTrue(ctx.registeredObjects.isEmpty, 85 | "Context must be reset after onStorePreseedingCompleted.") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/UnresolvedRelationshipCacheTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnresolvedRelationshipCacheTests.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by JP Wright on 02.07.18. 6 | // Copyright © 2018 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | @testable import ContentfulPersistence 10 | import Contentful 11 | import XCTest 12 | import Foundation 13 | import CoreData 14 | import OHHTTPStubs 15 | import CoreLocation 16 | 17 | class UnresolvedRelationshipCacheTests: XCTestCase { 18 | 19 | var syncManager: SynchronizationManager! 20 | 21 | var client: Client! 22 | 23 | lazy var store: CoreDataStore = { 24 | return CoreDataStore(context: self.managedObjectContext) 25 | }() 26 | 27 | lazy var managedObjectContext: NSManagedObjectContext = { 28 | return TestHelpers.managedObjectContext(forMOMInTestBundleNamed: "ComplexTest") 29 | }() 30 | 31 | // Before each test. 32 | override func setUp() { 33 | HTTPStubs.removeAllStubs() 34 | 35 | let persistenceModel = PersistenceModel(spaceType: ComplexSyncInfo.self, assetType: ComplexAsset.self, entryTypes: [SingleRecord.self, Link.self]) 36 | 37 | 38 | client = Client(spaceId: "smf0sqiu0c5s", 39 | accessToken: "14d305ad526d4487e21a99b5b9313a8877ce6fbf540f02b12189eea61550ef34") 40 | self.syncManager = SynchronizationManager(client: client, 41 | localizationScheme: .default, 42 | persistenceStore: self.store, 43 | persistenceModel: persistenceModel) 44 | } 45 | 46 | // After each test. 47 | override func tearDown() { 48 | HTTPStubs.removeAllStubs() 49 | } 50 | 51 | func testRelationshipsAreCachedMidSync() { 52 | var syncSpace: SyncSpace! 53 | 54 | let expectation = self.expectation(description: "Initial sync succeeded") 55 | 56 | stub(condition: isPath("/spaces/smf0sqiu0c5s/environments/master/sync")) { request -> HTTPStubsResponse in 57 | let stubPath = OHPathForFile("unresolvable-links.json", UnresolvedRelationshipCacheTests.self) 58 | return fixture(filePath: stubPath!, headers: ["Content-Type": "application/json"]) 59 | 60 | }.name = "Initial sync stub" 61 | 62 | client.sync() { result in 63 | switch result { 64 | case .success(let space): 65 | XCTAssertFalse(self.syncManager.cachedUnresolvedRelationships!.isEmpty) 66 | XCTAssertEqual((self.syncManager.cachedUnresolvedRelationships?["14XouHzspI44uKCcMicWUY_en-US"])?["linkField"] as? String, "2XYdAPiR0I6SMAGiCOEukU_en-US") 67 | syncSpace = space 68 | case .failure(let error): 69 | XCTFail("\(error)") 70 | 71 | } 72 | expectation.fulfill() 73 | } 74 | waitForExpectations(timeout: 10.0, handler: nil) 75 | HTTPStubs.removeAllStubs() 76 | 77 | // ============================NEXT SYNC================================================== 78 | let nextExpectation = self.expectation(description: "Next sync clears the cached JSON after relationships are resolved") 79 | 80 | stub(condition: isPath("/spaces/smf0sqiu0c5s/environments/master/sync")) { request -> HTTPStubsResponse in 81 | let stubPath = OHPathForFile("now-resolvable-relationships.json", UnresolvedRelationshipCacheTests.self) 82 | return fixture(filePath: stubPath!, headers: ["Content-Type": "application/json"]) 83 | }.name = "Next sync: relationships resolved." 84 | 85 | client.sync(for: syncSpace) { result in 86 | switch result { 87 | case .success: 88 | XCTAssertNil(self.syncManager.cachedUnresolvedRelationships) 89 | case .failure(let error): 90 | XCTFail("\(error)") 91 | 92 | } 93 | nextExpectation.fulfill() 94 | } 95 | 96 | waitForExpectations(timeout: 10.0, handler: nil) 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /ContentfulPersistence.xcodeproj/xcshareddata/xcschemes/ContentfulPersistence_iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /ContentfulPersistence.xcodeproj/xcshareddata/xcschemes/ContentfulPersistence_tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /ContentfulPersistence.xcodeproj/xcshareddata/xcschemes/ContentfulPersistence_macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Relationships/RelationshipData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentfulPersistence 3 | // 4 | 5 | import Foundation 6 | import Contentful 7 | 8 | struct RelationshipData: Codable { 9 | 10 | private typealias FieldId = String 11 | private typealias ChildLookupKey = String 12 | 13 | private struct RelationshipKeyPath: Codable, Hashable { 14 | 15 | var parentId: Relationship.ParentId 16 | var fieldId: FieldId 17 | 18 | init(parentId: Relationship.ParentId, fieldName: Relationship.FieldName, localeCode: Relationship.LocaleCode) { 19 | let fieldId = "\(fieldName),\(localeCode ?? "-")" 20 | self.init(parentId: parentId, fieldId: fieldId) 21 | } 22 | 23 | init(parentId: Relationship.ParentId, fieldId: FieldId) { 24 | self.parentId = parentId 25 | self.fieldId = fieldId 26 | } 27 | 28 | } 29 | 30 | var count: Int { 31 | relationships.reduce(0) { $0 + $1.value.count } 32 | } 33 | 34 | var isEmpty: Bool { 35 | relationships.isEmpty 36 | } 37 | 38 | private var relationships: [Relationship.ParentId: [FieldId: Relationship]] = [:] 39 | private var relationshipKeyPathsByChild: [RelationshipChildId.RawValue: Set] = [:] 40 | 41 | mutating func append(_ relationship: Relationship) { 42 | let keyPath = Self.keyPath(for: relationship) 43 | setRelationship(relationship, for: keyPath) 44 | } 45 | 46 | mutating func delete(parentId: Relationship.ParentId) { 47 | let keyPaths = relationships[parentId]?.keys 48 | .map { RelationshipKeyPath(parentId: parentId, fieldId: $0) } ?? [] 49 | 50 | for keyPath in keyPaths { 51 | setRelationship(nil, for: keyPath) 52 | } 53 | } 54 | 55 | mutating func delete(parentId: Relationship.ParentId, fieldName: Relationship.FieldName, localeCode: Relationship.LocaleCode) { 56 | let keyPath = RelationshipKeyPath(parentId: parentId, fieldName: fieldName, localeCode: localeCode) 57 | setRelationship(nil, for: keyPath) 58 | } 59 | 60 | func relationships(for childId: RelationshipChildId) -> [Relationship] { 61 | relationshipKeyPathsByChild[childId.rawValue]? 62 | .compactMap(relationship) ?? [] 63 | } 64 | 65 | private func relationship(keyPath: RelationshipKeyPath) -> Relationship? { 66 | relationships[keyPath.parentId]?[keyPath.fieldId] 67 | } 68 | 69 | private mutating func setRelationship(_ relationship: Relationship?, for keyPath: RelationshipKeyPath) { 70 | var relationshipsByFieldIdentifier = relationships[keyPath.parentId] ?? [:] 71 | 72 | let newChildIds = Set(relationship?.children.elements.map { $0.id } ?? []) 73 | 74 | if let existingRelationship = relationshipsByFieldIdentifier[keyPath.fieldId] { 75 | let existingChildIds = Set(existingRelationship.children.elements.map { $0.id }) 76 | let removedChildIds = existingChildIds.subtracting(newChildIds) 77 | 78 | for childId in removedChildIds { 79 | if var keyPaths = relationshipKeyPathsByChild[childId] { 80 | keyPaths.remove(keyPath) 81 | relationshipKeyPathsByChild[childId] = keyPaths 82 | } 83 | } 84 | } 85 | 86 | for childId in newChildIds { 87 | var keyPaths = relationshipKeyPathsByChild[childId] ?? Set() 88 | keyPaths.insert(keyPath) 89 | relationshipKeyPathsByChild[childId] = keyPaths 90 | } 91 | 92 | relationshipsByFieldIdentifier[keyPath.fieldId] = relationship 93 | 94 | if relationshipsByFieldIdentifier.isEmpty { 95 | relationships[keyPath.parentId] = nil 96 | } else { 97 | relationships[keyPath.parentId] = relationshipsByFieldIdentifier 98 | } 99 | } 100 | 101 | private static func keyPath(for relationship: Relationship) -> RelationshipKeyPath { 102 | RelationshipKeyPath(parentId: relationship.parentId, fieldName: relationship.fieldName, localeCode: relationship.localeCode) 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/multi-page-link-resolution2.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "2XYdAPiR0I6SMAGiCOEukU", 16 | "type": "Entry", 17 | "createdAt": "2017-07-14T09:10:20.703Z", 18 | "updatedAt": "2017-07-14T09:10:20.703Z", 19 | "revision": 1, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "link" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "awesomeLinkTitle": { 30 | "en-US": "AWESOMELINK!!!" 31 | } 32 | } 33 | }, 34 | { 35 | "sys": { 36 | "space": { 37 | "sys": { 38 | "type": "Link", 39 | "linkType": "Space", 40 | "id": "smf0sqiu0c5s" 41 | } 42 | }, 43 | "id": "2eXtYKYxYAue2IQgaucoYW", 44 | "type": "Entry", 45 | "createdAt": "2017-06-20T12:51:13.910Z", 46 | "updatedAt": "2017-06-20T12:51:13.910Z", 47 | "revision": 1, 48 | "contentType": { 49 | "sys": { 50 | "type": "Link", 51 | "linkType": "ContentType", 52 | "id": "singleRecord" 53 | } 54 | } 55 | }, 56 | "fields": { 57 | "textBody": { 58 | "en-US": "4" 59 | } 60 | } 61 | }, 62 | { 63 | "sys": { 64 | "space": { 65 | "sys": { 66 | "type": "Link", 67 | "linkType": "Space", 68 | "id": "smf0sqiu0c5s" 69 | } 70 | }, 71 | "id": "7vervB1KtUYWC22OCcmsKc", 72 | "type": "Entry", 73 | "createdAt": "2017-06-20T12:51:06.955Z", 74 | "updatedAt": "2017-06-20T12:51:06.955Z", 75 | "revision": 1, 76 | "contentType": { 77 | "sys": { 78 | "type": "Link", 79 | "linkType": "ContentType", 80 | "id": "singleRecord" 81 | } 82 | } 83 | }, 84 | "fields": { 85 | "textBody": { 86 | "en-US": "3" 87 | } 88 | } 89 | }, 90 | { 91 | "sys": { 92 | "space": { 93 | "sys": { 94 | "type": "Link", 95 | "linkType": "Space", 96 | "id": "smf0sqiu0c5s" 97 | } 98 | }, 99 | "id": "NNPT58qeyYKauym8S0MUk", 100 | "type": "Entry", 101 | "createdAt": "2017-06-20T12:51:00.210Z", 102 | "updatedAt": "2017-06-20T12:51:00.210Z", 103 | "revision": 1, 104 | "contentType": { 105 | "sys": { 106 | "type": "Link", 107 | "linkType": "ContentType", 108 | "id": "singleRecord" 109 | } 110 | } 111 | }, 112 | "fields": { 113 | "textBody": { 114 | "en-US": "2" 115 | } 116 | } 117 | }, 118 | { 119 | "sys": { 120 | "space": { 121 | "sys": { 122 | "type": "Link", 123 | "linkType": "Space", 124 | "id": "smf0sqiu0c5s" 125 | } 126 | }, 127 | "id": "3PbLvOJldSc6MqKEaIE6Ce", 128 | "type": "Entry", 129 | "createdAt": "2017-06-20T12:50:15.724Z", 130 | "updatedAt": "2017-06-20T12:50:15.724Z", 131 | "revision": 1, 132 | "contentType": { 133 | "sys": { 134 | "type": "Link", 135 | "linkType": "ContentType", 136 | "id": "singleRecord" 137 | } 138 | } 139 | }, 140 | "fields": { 141 | "textBody": { 142 | "en-US": "1" 143 | } 144 | } 145 | } 146 | ], 147 | "nextSyncUrl": "http://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=w5ZGw6JFwqZmVcKsE8Kow4grw45QdyYJwpfCm3bCmxJXGsOQIUbCk8OeIMKTw6nCvsOCQsOAw4VTEsO5DCLDkldRwrfDmCgFw78vHcOaCMKcW0fDmMO3ORw_ZMKdbCfDrQ4NPMOOwpPDjWxqesK5e8KKwok" 148 | } 149 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/ComplexTestStubs/now-resolvable-relationships.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "items": [ 6 | { 7 | "sys": { 8 | "space": { 9 | "sys": { 10 | "type": "Link", 11 | "linkType": "Space", 12 | "id": "smf0sqiu0c5s" 13 | } 14 | }, 15 | "id": "2XYdAPiR0I6SMAGiCOEukU", 16 | "type": "Entry", 17 | "createdAt": "2017-07-14T09:10:20.703Z", 18 | "updatedAt": "2017-07-14T09:10:20.703Z", 19 | "revision": 1, 20 | "contentType": { 21 | "sys": { 22 | "type": "Link", 23 | "linkType": "ContentType", 24 | "id": "link" 25 | } 26 | } 27 | }, 28 | "fields": { 29 | "awesomeLinkTitle": { 30 | "en-US": "AWESOMELINK!!!" 31 | } 32 | } 33 | }, 34 | { 35 | "sys": { 36 | "space": { 37 | "sys": { 38 | "type": "Link", 39 | "linkType": "Space", 40 | "id": "smf0sqiu0c5s" 41 | } 42 | }, 43 | "id": "2eXtYKYxYAue2IQgaucoYW", 44 | "type": "Entry", 45 | "createdAt": "2017-06-20T12:51:13.910Z", 46 | "updatedAt": "2017-06-20T12:51:13.910Z", 47 | "revision": 1, 48 | "contentType": { 49 | "sys": { 50 | "type": "Link", 51 | "linkType": "ContentType", 52 | "id": "singleRecord" 53 | } 54 | } 55 | }, 56 | "fields": { 57 | "textBody": { 58 | "en-US": "4" 59 | } 60 | } 61 | }, 62 | { 63 | "sys": { 64 | "space": { 65 | "sys": { 66 | "type": "Link", 67 | "linkType": "Space", 68 | "id": "smf0sqiu0c5s" 69 | } 70 | }, 71 | "id": "7vervB1KtUYWC22OCcmsKc", 72 | "type": "Entry", 73 | "createdAt": "2017-06-20T12:51:06.955Z", 74 | "updatedAt": "2017-06-20T12:51:06.955Z", 75 | "revision": 1, 76 | "contentType": { 77 | "sys": { 78 | "type": "Link", 79 | "linkType": "ContentType", 80 | "id": "singleRecord" 81 | } 82 | } 83 | }, 84 | "fields": { 85 | "textBody": { 86 | "en-US": "3" 87 | } 88 | } 89 | }, 90 | { 91 | "sys": { 92 | "space": { 93 | "sys": { 94 | "type": "Link", 95 | "linkType": "Space", 96 | "id": "smf0sqiu0c5s" 97 | } 98 | }, 99 | "id": "NNPT58qeyYKauym8S0MUk", 100 | "type": "Entry", 101 | "createdAt": "2017-06-20T12:51:00.210Z", 102 | "updatedAt": "2017-06-20T12:51:00.210Z", 103 | "revision": 1, 104 | "contentType": { 105 | "sys": { 106 | "type": "Link", 107 | "linkType": "ContentType", 108 | "id": "singleRecord" 109 | } 110 | } 111 | }, 112 | "fields": { 113 | "textBody": { 114 | "en-US": "2" 115 | } 116 | } 117 | }, 118 | { 119 | "sys": { 120 | "space": { 121 | "sys": { 122 | "type": "Link", 123 | "linkType": "Space", 124 | "id": "smf0sqiu0c5s" 125 | } 126 | }, 127 | "id": "3PbLvOJldSc6MqKEaIE6Ce", 128 | "type": "Entry", 129 | "createdAt": "2017-06-20T12:50:15.724Z", 130 | "updatedAt": "2017-06-20T12:50:15.724Z", 131 | "revision": 1, 132 | "contentType": { 133 | "sys": { 134 | "type": "Link", 135 | "linkType": "ContentType", 136 | "id": "singleRecord" 137 | } 138 | } 139 | }, 140 | "fields": { 141 | "textBody": { 142 | "en-US": "1" 143 | } 144 | } 145 | } 146 | ], 147 | "nextSyncUrl": "http://cdn.contentful.com/spaces/smf0sqiu0c5s/environments/master/sync?sync_token=w5ZGw6JFwqZmVcKsE8Kow4grw45QdyYJwpfCm3bCmxJXGsOQIUbCk8OeIMKTw6nCvsOCQsOAw4VTEsO5DCLDkldRwrfDmCgFw78vHcOaCMKcW0fDmMO3ORw_ZMKdbCfDrQ4NPMOOwpPDjWxqesK5e8KKwok" 148 | } 149 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/DataCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataCache.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Boris Bügling on 15/04/16. 6 | // Copyright © 2016 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Contentful 11 | 12 | typealias CacheKey = String 13 | 14 | protocol DataCacheProtocol { 15 | init(persistenceStore: PersistenceStore, assetType: AssetPersistable.Type, entryTypes: [EntryPersistable.Type]) 16 | 17 | func entry(for cacheKey: CacheKey) -> EntryPersistable? 18 | func item(for cacheKey: CacheKey) -> NSObject? 19 | } 20 | 21 | /// Does not actually cache anything, but directly uses the persistence store instead 22 | class NoDataCache: DataCacheProtocol { 23 | fileprivate let assetType: AssetPersistable.Type 24 | fileprivate let entryTypes: [EntryPersistable.Type] 25 | fileprivate let store: PersistenceStore 26 | 27 | required init(persistenceStore: PersistenceStore, assetType: AssetPersistable.Type, entryTypes: [EntryPersistable.Type]) { 28 | self.assetType = assetType 29 | self.entryTypes = entryTypes 30 | self.store = persistenceStore 31 | } 32 | 33 | fileprivate func itemsOf(_ types: [ContentSysPersistable.Type], cacheKey: CacheKey) -> EntryPersistable? { 34 | let predicate = ContentfulPersistence.predicate(for: cacheKey) 35 | 36 | let items: [EntryPersistable] = types.compactMap { 37 | if let result = try? store.fetchAll(type: $0, predicate: predicate) as [EntryPersistable] { 38 | return result.first 39 | } 40 | return nil 41 | } 42 | 43 | return items.first 44 | } 45 | 46 | func entry(for cacheKey: CacheKey) -> EntryPersistable? { 47 | return itemsOf(entryTypes, cacheKey: cacheKey) 48 | } 49 | 50 | func item(for cacheKey: CacheKey) -> NSObject? { 51 | return itemsOf([assetType] + entryTypes, cacheKey: cacheKey) 52 | } 53 | } 54 | 55 | 56 | /// Implemented using `NSCache` 57 | class DataCache: DataCacheProtocol { 58 | 59 | public static func cacheKey(for resource: ContentSysPersistable) -> CacheKey { 60 | let localeCode = resource.localeCode ?? "" 61 | let cacheKey = resource.id + "_" + localeCode 62 | return cacheKey 63 | } 64 | 65 | public static func cacheKey(for resource: LocalizableResource) -> CacheKey { 66 | let cacheKey = resource.id + "_" + resource.currentlySelectedLocale.code 67 | return cacheKey 68 | } 69 | 70 | public static func cacheKey(for identifier: String, localeCode: String?) -> CacheKey { 71 | let cacheKey = identifier + "_" + (localeCode ?? "") 72 | return cacheKey 73 | } 74 | 75 | fileprivate let assetCache = NSCache() 76 | fileprivate let entryCache = NSCache() 77 | 78 | required init(persistenceStore: PersistenceStore, assetType: AssetPersistable.Type, entryTypes: [EntryPersistable.Type]) { 79 | let truePredicate = NSPredicate(value: true) 80 | 81 | let assets: [AssetPersistable]? = try? persistenceStore.fetchAll(type: assetType, predicate: truePredicate) 82 | assets?.forEach { type(of: self).cacheResource(in: assetCache, resource: $0) } 83 | 84 | for entryType in entryTypes { 85 | let entries: [EntryPersistable]? = try? persistenceStore.fetchAll(type: entryType, predicate: truePredicate) 86 | entries?.forEach { type(of: self).cacheResource(in: entryCache, resource: $0) } 87 | } 88 | } 89 | 90 | func asset(for cacheKey: CacheKey) -> AssetPersistable? { 91 | return assetCache.object(forKey: cacheKey as AnyObject) as? AssetPersistable 92 | } 93 | 94 | func entry(for cacheKey: CacheKey) -> EntryPersistable? { 95 | return entryCache.object(forKey: cacheKey as AnyObject) as? EntryPersistable 96 | } 97 | 98 | func item(for cacheKey: CacheKey) -> NSObject? { 99 | var target: NSObject? = self.asset(for: cacheKey) 100 | 101 | if target == nil { 102 | target = self.entry(for: cacheKey) 103 | } 104 | 105 | return target 106 | } 107 | 108 | fileprivate static func cacheResource(in cache: NSCache, resource: ContentSysPersistable) { 109 | let cacheKey = DataCache.cacheKey(for: resource) 110 | cache.setObject(resource as AnyObject, forKey: cacheKey as AnyObject) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/BatchSyncTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BatchSyncTests.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Marius Kurgonas on 29/02/2024. 6 | // Copyright © 2024 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | @testable import ContentfulPersistence 10 | import Contentful 11 | import XCTest 12 | import Foundation 13 | import CoreData 14 | import OHHTTPStubs 15 | import CoreLocation 16 | 17 | class BatchSyncTests: XCTestCase { 18 | 19 | var syncManager: SynchronizationManager! 20 | 21 | var client: Client! 22 | 23 | lazy var store: CoreDataStore = { 24 | return CoreDataStore(context: self.managedObjectContext) 25 | }() 26 | 27 | lazy var managedObjectContext: NSManagedObjectContext = { 28 | return TestHelpers.managedObjectContext(forMOMInTestBundleNamed: "ComplexTest") 29 | }() 30 | 31 | // Before each test. 32 | override func setUp() { 33 | HTTPStubs.removeAllStubs() 34 | 35 | let persistenceModel = PersistenceModel(spaceType: ComplexSyncInfo.self, assetType: ComplexAsset.self, entryTypes: [SingleRecord.self, Link.self, RecordWithNonOptionalRelation.self]) 36 | 37 | 38 | client = Client(spaceId: "smf0sqiu0c5s", 39 | accessToken: "14d305ad526d4487e21a99b5b9313a8877ce6fbf540f02b12189eea61550ef34") 40 | self.syncManager = SynchronizationManager(client: client, 41 | localizationScheme: .default, 42 | persistenceStore: self.store, 43 | persistenceModel: persistenceModel) 44 | } 45 | 46 | // After each test. 47 | override func tearDown() { 48 | HTTPStubs.removeAllStubs() 49 | } 50 | 51 | func testBatchUpdating() { 52 | 53 | let expectation = self.expectation(description: "testBatchUpdating") 54 | 55 | stub(condition: isPath("/spaces/smf0sqiu0c5s/environments/master/sync")) { request -> HTTPStubsResponse in 56 | let urlString = request.url!.absoluteString 57 | let queryItems = URLComponents(string: urlString)!.queryItems! 58 | for queryItem in queryItems { 59 | if queryItem.name == "initial" { 60 | let stubPath = OHPathForFile("batch-page.json", ComplexSyncTests.self) 61 | return fixture(filePath: stubPath!, headers: ["Content-Type": "application/json"]) 62 | } else if queryItem.name == "sync_token" && queryItem.value == "wonDrcKnRgcSOF4-wrDCgcKefWzCgsOxwrfCq8KOfMOdXUPCvEnChwEEO8KFwqHDj8KIKHJYwr0Mw4wKw7UAVcOWYsOtw4nCvTUvDn0pw4gBRMKrGyVxIsOcQMKrwpfDhcOZwq3CkzDDvcKsJ8Oew58fJAQLwr3Cv2MGw4h6LcK_w4whQ8KMwr3ClMOhw5zDjMOswqAyw7XDpXDDsiXCvsKfw7JcwqjDucKmKMODwpDDlyvCrCnCjsOww43ClMKUwq8Xw75pXA" { 63 | let stubPath = OHPathForFile("batch-page2.json", ComplexSyncTests.self) 64 | return fixture(filePath: stubPath!, headers: ["Content-Type": "application/json"]) 65 | } 66 | } 67 | let stubPath = OHPathForFile("simple-update-initial-sync.json", ComplexSyncTests.self) 68 | return fixture(filePath: stubPath!, headers: ["Content-Type": "application/json"]) 69 | }.name = "Initial sync stub" 70 | 71 | client.sync() { result in 72 | switch result { 73 | case .success(let space): 74 | // There are only SingleRecords, Links and ComplexAssets in stub files 75 | // All should be saved to CoreData 76 | self.managedObjectContext.perform { 77 | do { 78 | let records: [SingleRecord] = try self.store.fetchAll(type: SingleRecord.self, predicate: NSPredicate(value: true)) 79 | let links: [Link] = try self.store.fetchAll(type: Link.self, predicate: NSPredicate(value: true)) 80 | XCTAssertEqual(records.count + links.count, space.entries.count) 81 | let assets: [ComplexAsset] = try self.store.fetchAll(type: ComplexAsset.self, predicate: NSPredicate(value: true)) 82 | XCTAssertEqual(assets.count, space.assets.count) 83 | } catch { 84 | XCTAssert(false, "Fetching posts should not throw an error") 85 | } 86 | expectation.fulfill() 87 | } 88 | case .failure(let error): 89 | XCTFail("\(error)") 90 | expectation.fulfill() 91 | } 92 | } 93 | 94 | waitForExpectations(timeout: 10.0, handler: nil) 95 | HTTPStubs.removeAllStubs() 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /Sources/ContentfulPersistence/Persistable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Persistable.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by JP Wright on 15.06.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Contentful 11 | 12 | /** 13 | Structure used to define the schema of your CoreData model and it's correlation to your Content Model 14 | in Contentful. Pass in your `NSManagedObject` subclasses conforming to `SyncSpacePersistable`, 15 | `AssetPersistable` and `EntryPersistable` to properly map your content to CoreData entities. 16 | */ 17 | public struct PersistenceModel { 18 | public let spaceType: SyncSpacePersistable.Type 19 | public let assetType: AssetPersistable.Type 20 | public let entryTypes: [EntryPersistable.Type] 21 | 22 | public init(spaceType: SyncSpacePersistable.Type, 23 | assetType: AssetPersistable.Type, 24 | entryTypes: [EntryPersistable.Type]) { 25 | 26 | self.spaceType = spaceType 27 | self.assetType = assetType 28 | self.entryTypes = entryTypes 29 | } 30 | } 31 | 32 | /** 33 | Base protocol for all `AssetPersistable` and `EntryPersistable`. 34 | */ 35 | // Protocols are marked with @objc attribute for two reasons: 36 | // 1) CoreData requires that model classes inherit from `NSManagedObject` 37 | // 2) @objc enables optional protocol methods that don't require implementation. 38 | public protocol ContentSysPersistable: NSObject { 39 | /// The unique identifier of the Resource. 40 | var id: String { get set } 41 | 42 | /// The code which represents which locale the Resource of interest contains data for. 43 | var localeCode: String? { get set } 44 | 45 | /// The date representing the last time the Contentful Resource was updated. 46 | var updatedAt: Date? { get set } 47 | 48 | /// The date that the Contentful Resource was first created. 49 | var createdAt: Date? { get set } 50 | } 51 | 52 | /** 53 | Your `NSManagedObject` subclass should conform to this `SyncSpacePersistable` to enable continuing 54 | a sync from a sync token on subsequent launches of your application rather than re-fetching all data 55 | during an initialSync. See [Contentful's Content Delivery API docs](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization/pagination-and-subsequent-syncs) 56 | for more information. 57 | */ 58 | public protocol SyncSpacePersistable: AnyObject { 59 | /// The current synchronization token 60 | var syncToken: String? { get set } 61 | 62 | /// Database version to do migrations 63 | var dbVersion: NSNumber? { get set } 64 | } 65 | 66 | /** 67 | Conform to `AssetPersistable` protocol to enable mapping of your Contentful media Assets to 68 | your `NSManagedObject` subclass. 69 | */ 70 | public protocol AssetPersistable: ContentSysPersistable, AssetProtocol { 71 | 72 | /// The title of the Asset. 73 | var title: String? { get set } 74 | 75 | /// The description of the asset. Named `assetDescription` to avoid clashing with `description` 76 | /// property that all NSObject's have. 77 | var assetDescription: String? { get set } 78 | 79 | /// URL of the Asset. 80 | var urlString: String? { get set } 81 | 82 | /// The name of the underlying binary media file. 83 | var fileName: String? { get set } 84 | 85 | /// The type of the underlying binary media file: e.g. `image/png` 86 | var fileType: String? { get set } 87 | 88 | /// The byte size of the underlying binary media file. 89 | var size: NSNumber? { get set } 90 | 91 | /// If the binary media file is an image, this property describes the images width in pixels. 92 | var width: NSNumber? { get set } 93 | 94 | /// If the binary media file is an image, this property describes the images height in pixels. 95 | var height: NSNumber? { get set } 96 | } 97 | 98 | /** 99 | Conform to `EntryPersistable` protocol to enable mapping of your Contentful content type to 100 | your `NSManagedObject` subclass. 101 | */ 102 | public protocol EntryPersistable: ContentSysPersistable { 103 | /// The identifier of the Contentful content type that will map to this type of `EntryPersistable` 104 | static var contentTypeId: ContentTypeId { get } 105 | 106 | /// This method must be implemented to provide a custom mapping from Contentful fields to Swift properties. 107 | /// Note that after Swift 4 is release, this method will be deprecated in favor of leveraging the 108 | /// auto-synthesized `CodingKeys` enum that is generated for all types conforming to `Codable`. 109 | static func fieldMapping() -> [FieldName: String] 110 | } 111 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/Relationships/RelationshipCacheTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentfulPersistence 3 | // 4 | 5 | @testable import ContentfulPersistence 6 | import XCTest 7 | 8 | class RelationshipCacheTests: XCTestCase { 9 | 10 | func test_relationships_areCachedOnDisk() { 11 | let fileName = makeFileName() 12 | let cache = RelationshipCache(cacheFileName: fileName) 13 | 14 | XCTAssertEqual(cache.relationships.count, 0) 15 | 16 | cache.add(relationship: makeToOne1()) 17 | cache.add(relationship: makeToOne2()) 18 | cache.add(relationship: makeToMany1()) 19 | 20 | cache.save() 21 | 22 | // Verify 23 | let verifyCache = RelationshipCache(cacheFileName: fileName) 24 | 25 | verifyCache.add(relationship: makeToOne1(localeCode: "en-US")) 26 | 27 | XCTAssertEqual(verifyCache.relationships.count, 4) 28 | } 29 | 30 | func test_relationship_isDeleted() { 31 | let fileName = makeFileName() 32 | let cache = RelationshipCache(cacheFileName: fileName) 33 | 34 | XCTAssertEqual(cache.relationships.count, 0) 35 | 36 | cache.add(relationship: makeToOne1()) 37 | 38 | let toOne2 = makeToOne2() 39 | cache.add(relationship: toOne2) 40 | 41 | let toMany1 = makeToMany1() 42 | cache.add(relationship: toMany1) 43 | 44 | XCTAssertEqual(cache.relationships.count, 3) 45 | 46 | cache.delete(parentId: toOne2.parentId) 47 | XCTAssertEqual(cache.relationships.count, 2) 48 | 49 | cache.delete(parentId: toMany1.parentId) 50 | XCTAssertEqual(cache.relationships.count, 1) 51 | 52 | cache.add(relationship: toOne2) 53 | cache.add(relationship: toMany1) 54 | 55 | cache.delete( 56 | parentId: toOne2.parentId, 57 | fieldName: toOne2.fieldName, 58 | localeCode: toOne2.localeCode 59 | ) 60 | 61 | XCTAssertEqual(cache.relationships.count, 2) 62 | 63 | cache.delete( 64 | parentId: toMany1.parentId, 65 | fieldName: toMany1.fieldName, 66 | localeCode: toMany1.localeCode 67 | ) 68 | 69 | XCTAssertEqual(cache.relationships.count, 1) 70 | } 71 | 72 | func test_deleteRelationship_byLocale() { 73 | let fileName = makeFileName() 74 | let cache = RelationshipCache(cacheFileName: fileName) 75 | 76 | XCTAssertEqual(cache.relationships.count, 0) 77 | 78 | let toOne1a = makeToOne1(localeCode: "en-US") 79 | let toOne1b = makeToOne1(localeCode: "pl-PL") 80 | cache.add(relationship: toOne1a) 81 | cache.add(relationship: toOne1b) 82 | 83 | cache.delete( 84 | parentId: toOne1a.parentId, 85 | fieldName: toOne1a.fieldName, 86 | localeCode: nil 87 | ) 88 | 89 | XCTAssertEqual(cache.relationships.count, 2) 90 | 91 | cache.delete( 92 | parentId: toOne1a.parentId, 93 | fieldName: toOne1a.fieldName, 94 | localeCode: "en-GB" 95 | ) 96 | 97 | XCTAssertEqual(cache.relationships.count, 2) 98 | 99 | cache.delete( 100 | parentId: toOne1a.parentId, 101 | fieldName: toOne1a.fieldName, 102 | localeCode: toOne1a.localeCode 103 | ) 104 | 105 | XCTAssertEqual(cache.relationships.count, 1) 106 | 107 | cache.delete( 108 | parentId: toOne1b.parentId, 109 | fieldName: toOne1b.fieldName, 110 | localeCode: toOne1b.localeCode 111 | ) 112 | 113 | XCTAssertEqual(cache.relationships.count, 0) 114 | } 115 | 116 | private func makeToOne1(localeCode: String? = nil) -> Relationship { 117 | var childId = "dog-1" 118 | if let localeCode = localeCode { 119 | childId += "_\(localeCode)" 120 | } 121 | 122 | return .init( 123 | parentType: "person", 124 | parentId: "person-1", 125 | fieldName: "dog", 126 | childId: .init(rawValue: childId) 127 | ) 128 | } 129 | 130 | private func makeToOne2() -> Relationship { 131 | .init( 132 | parentType: "person", 133 | parentId: "person-2", 134 | fieldName: "cat", 135 | childId: .init(rawValue: "cat-1") 136 | ) 137 | } 138 | 139 | private func makeToMany1() -> Relationship { 140 | .init( 141 | parentType: "person", 142 | parentId: "person-3", 143 | fieldName: "things", 144 | childIds: [ 145 | .init(rawValue: "cat-1"), 146 | .init(rawValue: "dog-2") 147 | ] 148 | ) 149 | } 150 | 151 | private func makeFileName() -> String { 152 | UUID().uuidString + "-\(Date().timeIntervalSince1970)" 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | { 4 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/CoreDataStore.swift", 5 | "line": 31, 6 | "symbol": "CoreDataStore.fetchRequest(for:predicate:)", 7 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 8 | "warning": "undocumented" 9 | }, 10 | { 11 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/CoreDataStore.swift", 12 | "line": 188, 13 | "symbol": "CoreDataStore.performBlock(block:)", 14 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 15 | "warning": "undocumented" 16 | }, 17 | { 18 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/CoreDataStore.swift", 19 | "line": 201, 20 | "symbol": "CoreDataStore.performAndWait(block:)", 21 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 22 | "warning": "undocumented" 23 | }, 24 | { 25 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/Persistable.swift", 26 | "line": 18, 27 | "symbol": "PersistenceModel.spaceType", 28 | "symbol_kind": "source.lang.swift.decl.var.instance", 29 | "warning": "undocumented" 30 | }, 31 | { 32 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/Persistable.swift", 33 | "line": 19, 34 | "symbol": "PersistenceModel.assetType", 35 | "symbol_kind": "source.lang.swift.decl.var.instance", 36 | "warning": "undocumented" 37 | }, 38 | { 39 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/Persistable.swift", 40 | "line": 20, 41 | "symbol": "PersistenceModel.entryTypes", 42 | "symbol_kind": "source.lang.swift.decl.var.instance", 43 | "warning": "undocumented" 44 | }, 45 | { 46 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/Persistable.swift", 47 | "line": 22, 48 | "symbol": "PersistenceModel.init(spaceType:assetType:entryTypes:)", 49 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 50 | "warning": "undocumented" 51 | }, 52 | { 53 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/Persistable.swift", 54 | "line": 38, 55 | "symbol": "ContentSysPersistable", 56 | "symbol_kind": "source.lang.swift.decl.protocol", 57 | "warning": "undocumented" 58 | }, 59 | { 60 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/PersistenceStore.swift", 61 | "line": 92, 62 | "symbol": "PersistenceStore.performBlock(block:)", 63 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 64 | "warning": "undocumented" 65 | }, 66 | { 67 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/PersistenceStore.swift", 68 | "line": 94, 69 | "symbol": "PersistenceStore.performAndWait(block:)", 70 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 71 | "warning": "undocumented" 72 | }, 73 | { 74 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/SynchronizationManager.swift", 75 | "line": 54, 76 | "symbol": "SynchronizationManager.DBVersions", 77 | "symbol_kind": "source.lang.swift.decl.enum", 78 | "warning": "undocumented" 79 | }, 80 | { 81 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/SynchronizationManager.swift", 82 | "line": 55, 83 | "symbol": "SynchronizationManager.DBVersions.default", 84 | "symbol_kind": "source.lang.swift.decl.enumelement", 85 | "warning": "undocumented" 86 | }, 87 | { 88 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/SynchronizationManager.swift", 89 | "line": 155, 90 | "symbol": "SynchronizationManager.syncToken", 91 | "symbol_kind": "source.lang.swift.decl.var.instance", 92 | "warning": "undocumented" 93 | }, 94 | { 95 | "file": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence/Sources/ContentfulPersistence/SynchronizationManager.swift", 96 | "line": 163, 97 | "symbol": "SynchronizationManager.performAndWait(block:)", 98 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 99 | "warning": "undocumented" 100 | } 101 | ], 102 | "source_directory": "/Volumes/Extreme_SSD/Dev/Contentful/ContentfulTests/contentful_persistence" 103 | } -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/Relationships/RelationshipManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentfulPersistence 3 | // 4 | 5 | import Contentful 6 | @testable import ContentfulPersistence 7 | import XCTest 8 | 9 | class RelationshipManagerTests: XCTestCase { 10 | 11 | func test_manager_worksCorrectly() { 12 | let manager = RelationshipsManager(cacheFileName: makeFileName()) 13 | 14 | let entry1 = EntryA(id: "person-1") 15 | 16 | for _ in 0..<5 { 17 | manager.cacheToOneRelationship( 18 | parent: entry1, 19 | childId: RelationshipChildId(rawValue: "dog-1"), 20 | fieldName: "dog" 21 | ) 22 | } 23 | 24 | XCTAssertEqual(manager.relationships.count, 1) 25 | 26 | let entry2 = EntryA(id: "person-2") 27 | 28 | for _ in 0..<5 { 29 | manager.cacheToOneRelationship( 30 | parent: entry2, 31 | childId: RelationshipChildId(rawValue: "dog-1"), 32 | fieldName: "dog" 33 | ) 34 | } 35 | 36 | XCTAssertEqual(manager.relationships.count, 2) 37 | 38 | for _ in 0..<5 { 39 | manager.cacheToManyRelationship( 40 | parent: entry1, 41 | childIds: [ 42 | RelationshipChildId(rawValue: "cat-1"), 43 | RelationshipChildId(rawValue: "cat-2"), 44 | RelationshipChildId(rawValue: "cat-3") 45 | ], 46 | fieldName: "cats" 47 | ) 48 | } 49 | 50 | XCTAssertEqual(manager.relationships.count, 3) 51 | 52 | manager.delete(parentId: entry1.id, fieldName: "cats", localeCode: nil) 53 | XCTAssertEqual(manager.relationships.count, 2) 54 | 55 | manager.delete(parentId: entry2.id) 56 | XCTAssertEqual(manager.relationships.count, 1) 57 | 58 | manager.delete(parentId: entry1.id) 59 | XCTAssertEqual(manager.relationships.count, 0) 60 | } 61 | 62 | func testStaleToOneRelationshipsAreRemoved() { 63 | let manager = RelationshipsManager(cacheFileName: makeFileName()) 64 | 65 | let entry1 = EntryA(id: "person-1") 66 | let dog1 = RelationshipChildId(rawValue: "dog-1") 67 | let dog2 = RelationshipChildId(rawValue: "dog-2") 68 | 69 | manager.cacheToOneRelationship( 70 | parent: entry1, 71 | childId: dog1, 72 | fieldName: "dog" 73 | ) 74 | 75 | XCTAssertNotNil(manager.relationships.relationships(for: dog1)) 76 | 77 | manager.cacheToOneRelationship( 78 | parent: entry1, 79 | childId: dog2, 80 | fieldName: "dog" 81 | ) 82 | 83 | XCTAssertTrue(manager.relationships.relationships(for: dog1).isEmpty) 84 | XCTAssertFalse(manager.relationships.relationships(for: dog2).isEmpty) 85 | } 86 | 87 | func testStaleToManyRelationshipsAreRemoved() { 88 | let manager = RelationshipsManager(cacheFileName: makeFileName()) 89 | 90 | let entry1 = EntryA(id: "person-1") 91 | let cat1 = RelationshipChildId(rawValue: "cat-1") 92 | let cat2 = RelationshipChildId(rawValue: "cat-2") 93 | let cat3 = RelationshipChildId(rawValue: "cat-3") 94 | let cat4 = RelationshipChildId(rawValue: "cat-4") 95 | 96 | manager.cacheToManyRelationship( 97 | parent: entry1, 98 | childIds: [ 99 | cat1, 100 | cat2, 101 | cat3 102 | ], 103 | fieldName: "cats" 104 | ) 105 | 106 | XCTAssertFalse(manager.relationships.relationships(for: cat1).isEmpty) 107 | XCTAssertFalse(manager.relationships.relationships(for: cat2).isEmpty) 108 | XCTAssertFalse(manager.relationships.relationships(for: cat3).isEmpty) 109 | 110 | manager.cacheToManyRelationship( 111 | parent: entry1, 112 | childIds: [ 113 | cat1, 114 | cat2, 115 | cat4 116 | ], 117 | fieldName: "cats" 118 | ) 119 | 120 | XCTAssertFalse(manager.relationships.relationships(for: cat1).isEmpty) 121 | XCTAssertFalse(manager.relationships.relationships(for: cat2).isEmpty) 122 | XCTAssertTrue(manager.relationships.relationships(for: cat3).isEmpty) 123 | XCTAssertFalse(manager.relationships.relationships(for: cat4).isEmpty) 124 | } 125 | 126 | private func makeFileName() -> String { 127 | UUID().uuidString + "-\(Date().timeIntervalSince1970)" 128 | } 129 | } 130 | 131 | private class EntryA: NSObject, EntryPersistable { 132 | 133 | static var contentTypeId: ContentTypeId = "entry-a" 134 | 135 | static func fieldMapping() -> [FieldName : String] { 136 | [:] 137 | } 138 | 139 | var id: String 140 | var localeCode: String? 141 | var updatedAt: Date? = nil 142 | var createdAt: Date? = nil 143 | 144 | init(id: String = "", localeCode: String? = nil) { 145 | self.id = id 146 | self.localeCode = localeCode 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Tests/ContentfulPersistenceTests/RichTextDocumentTransformableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextDocumentTransformableTests.swift 3 | // ContentfulPersistence 4 | // 5 | // Created by Manuel Maly on 23.07.19. 6 | // Copyright © 2019 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | 10 | @testable import ContentfulPersistence 11 | import Contentful 12 | import XCTest 13 | import Foundation 14 | import CoreData 15 | 16 | class RichTextDocumentTransformableTests: XCTestCase { 17 | 18 | lazy var store: CoreDataStore = { 19 | return CoreDataStore(context: self.managedObjectContext) 20 | }() 21 | 22 | lazy var managedObjectContext: NSManagedObjectContext = { 23 | return TestHelpers.managedObjectContext(forMOMInTestBundleNamed: "RichTextDocumentTransformableTest") 24 | }() 25 | 26 | override func tearDown() { 27 | try? store.delete(type: RichTextDocumentRecord.self, predicate: NSPredicate(value: true)) 28 | } 29 | 30 | func testRichTextDocumentPersistence() { 31 | 32 | guard let richTextDocumentRecord: RichTextDocumentRecord = try? store.create(type: RichTextDocumentRecord.self) else { 33 | XCTFail() 34 | return 35 | } 36 | 37 | let document = richTextDocument() 38 | richTextDocumentRecord.richTextDocument = document 39 | 40 | try? store.save() 41 | managedObjectContext.reset() 42 | 43 | // For some reason, resetting the managed object context does indeed 44 | // lead to `richTextDocumentRecord` being faulted, but its `richTextDocument` 45 | // seems to live on (maybe in _CDSnapshot_RichTextDocumentRecord_?) and 46 | // comes up again when fetching the first record from the store. A better way 47 | // of destroying the linked `RichTextDocument` needs to be found to make this 48 | // test case actually useful. 49 | 50 | let documents: [RichTextDocumentRecord]? = try? store.fetchAll(type: RichTextDocumentRecord.self, predicate: NSPredicate(value: true)) 51 | guard let fetchedDocument = documents?.first else { 52 | XCTFail() 53 | return 54 | } 55 | 56 | XCTAssertEqual(fetchedDocument.richTextDocument?.content.count ?? 0, document.content.count) 57 | } 58 | 59 | private func richTextDocument() -> RichTextDocument { 60 | let paragraphText1: Text = { 61 | return Text(value: "paragraphText1", marks: []) 62 | }() 63 | 64 | let paragraphText2: Text = { 65 | return Text(value: "paragraphText2", marks: []) 66 | }() 67 | 68 | let paragraph = Paragraph(nodeType: .paragraph, content: [paragraphText1, paragraphText2]) 69 | 70 | let headingText: Text = { 71 | let bold = Text.Mark(type: Text.MarkType.bold) 72 | let italic = Text.Mark(type: Text.MarkType.italic) 73 | return Text(value: "headingText", marks: [bold, italic]) 74 | }() 75 | 76 | let headingH1 = Heading(level: 1, content: [headingText])! 77 | let headingH2 = Heading(level: 2, content: [headingText])! // test copy of headingText 78 | 79 | 80 | let blockQuoteText: Text = { 81 | let code = Text.Mark(type: Text.MarkType.code) 82 | return Text(value: "blockQuoteText", marks: [code]) 83 | }() 84 | let blockQuote = BlockQuote(nodeType: NodeType.blockquote, content: [blockQuoteText]) 85 | 86 | let horizontalRule = HorizontalRule(nodeType: NodeType.horizontalRule, content: []) 87 | 88 | let listItem1 = ListItem(nodeType: .listItem, content: [paragraphText1]) 89 | let listItem2 = ListItem(nodeType: .listItem, content: [paragraphText2]) 90 | let listItem3 = ListItem(nodeType: .listItem, content: [paragraph]) 91 | let orderedList = OrderedList(nodeType: .orderedList, content: [listItem1, listItem2, listItem3]) 92 | 93 | // Use listItem2 twice: 94 | let unorderedList = OrderedList(nodeType: .orderedList, content: [listItem1, listItem2, listItem2]) 95 | 96 | let link = Contentful.Link 97 | .unresolved(Contentful.Link.Sys(id: "unlinked-entry", linkType: "Entry", type: "Entry")) 98 | let embeddedAssetBlock = ResourceLinkBlock( 99 | resolvedData: ResourceLinkData(resolvedTarget: link, title: "linkTitle"), 100 | nodeType: NodeType.embeddedAssetBlock, 101 | content: [] 102 | ) 103 | 104 | let hyperlink = Hyperlink( 105 | data: Hyperlink.Data(uri: "https://contentful.com", title: "Contentful"), 106 | content: [] 107 | ) 108 | 109 | return RichTextDocument( 110 | content: ( 111 | [ 112 | paragraphText1, 113 | paragraph, 114 | headingH1, 115 | headingH2, 116 | blockQuote, 117 | horizontalRule, 118 | orderedList, 119 | unorderedList, 120 | embeddedAssetBlock, 121 | hyperlink 122 | ] as [Node?] // compiler needs this cast 123 | ).compactMap { $0 } 124 | ) 125 | } 126 | 127 | } 128 | --------------------------------------------------------------------------------