From 6a9c984809f835dd21b5daa502c71bab3eeb2b0a Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Wed, 1 Jan 2020 18:55:44 +0100 Subject: [PATCH 01/23] first implementation --- Package.swift | 6 ++ .../CloudKitFeatureToggles.swift | 3 - .../CloudKitSubscriptionProtocol.swift | 78 ++++++++++++++++++ .../FeatureToggleRepository.swift | 54 +++++++++++++ .../FeatureToggleSubscriptor.swift | 52 ++++++++++++ .../CloudKitFeatureTogglesTests.swift | 15 ---- .../FeatureToggleRepositoryTests.swift | 81 +++++++++++++++++++ .../XCTestManifests.swift | 2 +- 8 files changed, 272 insertions(+), 19 deletions(-) delete mode 100644 Sources/CloudKitFeatureToggles/CloudKitFeatureToggles.swift create mode 100644 Sources/CloudKitFeatureToggles/CloudKitSubscriptionProtocol.swift create mode 100644 Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift create mode 100644 Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift delete mode 100644 Tests/CloudKitFeatureTogglesTests/CloudKitFeatureTogglesTests.swift create mode 100644 Tests/CloudKitFeatureTogglesTests/FeatureToggleRepositoryTests.swift diff --git a/Package.swift b/Package.swift index 4cc44c5..da614fe 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,12 @@ import PackageDescription let package = Package( name: "CloudKitFeatureToggles", + platforms: [ + .iOS(SupportedPlatform.IOSVersion.v10), + .macOS(SupportedPlatform.MacOSVersion.v10_12), + .tvOS(SupportedPlatform.TVOSVersion.v9), + .watchOS(SupportedPlatform.WatchOSVersion.v3) + ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( diff --git a/Sources/CloudKitFeatureToggles/CloudKitFeatureToggles.swift b/Sources/CloudKitFeatureToggles/CloudKitFeatureToggles.swift deleted file mode 100644 index 07b03d2..0000000 --- a/Sources/CloudKitFeatureToggles/CloudKitFeatureToggles.swift +++ /dev/null @@ -1,3 +0,0 @@ -struct CloudKitFeatureToggles { - var text = "Hello, World!" -} diff --git a/Sources/CloudKitFeatureToggles/CloudKitSubscriptionProtocol.swift b/Sources/CloudKitFeatureToggles/CloudKitSubscriptionProtocol.swift new file mode 100644 index 0000000..dcfda29 --- /dev/null +++ b/Sources/CloudKitFeatureToggles/CloudKitSubscriptionProtocol.swift @@ -0,0 +1,78 @@ +// +// CloudKitSubscriptionProtocol.swift +// nSuns +// +// Created by Jonas Reichert on 01.09.18. +// Copyright © 2018 Jonas Reichert. All rights reserved. +// + +import Foundation +import CloudKit + +protocol CloudKitSubscriptionProtocol { + var subscriptionID: String { get } + + func fetchAll() + func saveSubscription() + func handleNotification() +} + +extension CloudKitSubscriptionProtocol { + func saveSubscription(subscriptionID: String, recordType: String, database: CKDatabase = CKContainer.default().publicCloudDatabase) { + // Let's keep a local flag handy to avoid saving the subscription more than once. + // Even if you try saving the subscription multiple times, the server doesn't save it more than once + // Nevertheless, let's save some network operation and conserve resources + let subscriptionSaved = UserDefaults.standard.bool(forKey: subscriptionID) + guard !subscriptionSaved else { + return + } + + // Subscribing is nothing but saving a query which the server would use to generate notifications. + // The below predicate (query) will raise a notification for all changes. + let predicate = NSPredicate(value: true) + let subscription = CKQuerySubscription(recordType: recordType, predicate: predicate, subscriptionID: subscriptionID, options: CKQuerySubscription.Options.firesOnRecordUpdate) + + let notificationInfo = CKSubscription.NotificationInfo() + // Set shouldSendContentAvailable to true for receiving silent pushes + // Silent notifications are not shown to the user and don’t require the user's permission. + notificationInfo.shouldSendContentAvailable = true + subscription.notificationInfo = notificationInfo + + // Use CKModifySubscriptionsOperation to save the subscription to CloudKit + let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) + operation.modifySubscriptionsCompletionBlock = { (_, _, error) in + guard error == nil else { + return + } + UserDefaults.standard.set(true, forKey: subscriptionID) + } + operation.qualityOfService = .utility + // Add the operation to the corresponding private or public database + database.add(operation) + } + + func handleNotification(database: CKDatabase = CKContainer.default().publicCloudDatabase, recordType: String, recordFetchedBlock: @escaping (CKRecord) -> Void) { + let queryOperation = CKQueryOperation(query: query(recordType: recordType)) + + queryOperation.recordFetchedBlock = recordFetchedBlock + queryOperation.qualityOfService = .utility + + database.add(queryOperation) + } + + func fetchAll(recordType: String, handler: @escaping ([CKRecord]) -> Void, database: CKDatabase = CKContainer.default().publicCloudDatabase) { + database.perform(query(recordType: recordType), inZoneWith: nil) { (ckRecords, error) in + guard error == nil, let ckRecords = ckRecords else { + // don't update last fetched date, simply do nothing and try again next time + return + } + + handler(ckRecords) + } + } + + private func query(recordType: String) -> CKQuery { + let predicate = NSPredicate(value: true) + return CKQuery(recordType: recordType, predicate: predicate) + } +} diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift b/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift new file mode 100644 index 0000000..84e6f04 --- /dev/null +++ b/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift @@ -0,0 +1,54 @@ +// +// FeatureToggleManager.swift +// CloudKitFeatureToggles +// +// Created by Jonas Reichert on 01.01.20. +// + +import Foundation + +public protocol FeatureToggleRepresentable { + var identifier: String { get } + var isActive: Bool { get } +} + +public protocol FeatureToggleIdentifiable { + var identifier: String { get } + var fallbackValue: Bool { get } +} + +public protocol FeatureToggleRetrievable { + /// retrieves a stored `FeatureToggleRepresentable` from the underlying store. + func retrieve(identifiable: FeatureToggleIdentifiable) -> FeatureToggleRepresentable +} + +protocol FeatureToggleRepository: FeatureToggleRetrievable { + /// saves a supplied `FeatureToggleRepresentable` to the underlying store + func save(featureToggle: FeatureToggleRepresentable) +} + +struct FeatureToggle: FeatureToggleRepresentable { + let identifier: String + let isActive: Bool +} + +public class FeatureToggleUserDefaultsRepository { + + private let defaults: UserDefaults + + init(defaults: UserDefaults = .standard) { + self.defaults = defaults + } +} + +extension FeatureToggleUserDefaultsRepository: FeatureToggleRepository { + public func retrieve(identifiable: FeatureToggleIdentifiable) -> FeatureToggleRepresentable { + let isActive = defaults.value(forKey: identifiable.identifier) as? Bool + + return FeatureToggle(identifier: identifiable.identifier, isActive: isActive ?? identifiable.fallbackValue) + } + + func save(featureToggle: FeatureToggleRepresentable) { + defaults.set(featureToggle.isActive, forKey: featureToggle.identifier) + } +} diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift new file mode 100644 index 0000000..c6a0f24 --- /dev/null +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -0,0 +1,52 @@ +// +// FeatureSwitchHelper.swift +// nSuns +// +// Created by Jonas Reichert on 22.07.18. +// Copyright © 2018 Jonas Reichert. All rights reserved. +// + +import Foundation +import CloudKit + +class FeatureSwitchSubscriptor: CloudKitSubscriptionProtocol { + + private let featureToggleRecordID: String + private let featureToggleNameFieldID: String + private let featureToggleIsActiveFieldID: String + + private let toggleRepository: FeatureToggleRepository + + let subscriptionID = "cloudkit-recordType-FeatureToggle" + + init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive") { + self.toggleRepository = toggleRepository + self.featureToggleRecordID = featureToggleRecordID + self.featureToggleNameFieldID = featureToggleNameFieldID + self.featureToggleIsActiveFieldID = featureToggleIsActiveFieldID + } + + func fetchAll() { + fetchAll(recordType: featureToggleRecordID, handler: { (ckRecords) in + self.updateRepository(with: ckRecords) + }) + } + + func saveSubscription() { + saveSubscription(subscriptionID: subscriptionID, recordType: featureToggleRecordID) + } + + func handleNotification() { + handleNotification(recordType: featureToggleRecordID) { (record) in + self.updateRepository(with: [record]) + } + } + + private func updateRepository(with ckRecords: [CKRecord]) { + ckRecords.forEach { (record) in + if let active = record[featureToggleIsActiveFieldID] as? Int64, let featureName = record[featureToggleNameFieldID] as? String { + toggleRepository.save(featureToggle: FeatureToggle(identifier: featureName, isActive: NSNumber(value: active).boolValue)) + } + } + } +} diff --git a/Tests/CloudKitFeatureTogglesTests/CloudKitFeatureTogglesTests.swift b/Tests/CloudKitFeatureTogglesTests/CloudKitFeatureTogglesTests.swift deleted file mode 100644 index c968c5f..0000000 --- a/Tests/CloudKitFeatureTogglesTests/CloudKitFeatureTogglesTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import XCTest -@testable import CloudKitFeatureToggles - -final class CloudKitFeatureTogglesTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(CloudKitFeatureToggles().text, "Hello, World!") - } - - static var allTests = [ - ("testExample", testExample), - ] -} diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleRepositoryTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleRepositoryTests.swift new file mode 100644 index 0000000..ae61e77 --- /dev/null +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleRepositoryTests.swift @@ -0,0 +1,81 @@ +// +// FeatureToggleRepositoryTests.swift +// CloudKitFeatureTogglesTests +// +// Created by Jonas Reichert on 01.01.20. +// + +import XCTest +@testable import CloudKitFeatureToggles + +class FeatureToggleRepositoryTests: XCTestCase { + + enum TestToggle: String, FeatureToggleIdentifiable { + var identifier: String { + return self.rawValue + } + + var fallbackValue: Bool { + switch self { + case .feature1: + return false + case .feature2: + return true + } + } + + case feature1 + case feature2 + } + + let suiteName = "repositoryTest" + var defaults: UserDefaults! + + var subject: FeatureToggleRepository! + + override func setUp() { + super.setUp() + + guard let defaults = UserDefaults(suiteName: suiteName) else { + XCTFail() + return + } + + self.defaults = defaults + self.subject = FeatureToggleUserDefaultsRepository(defaults: defaults) + } + + override func tearDown() { + self.defaults.removePersistentDomain(forName: suiteName) + + super.tearDown() + } + + func testRetrieveBeforeSave() { + XCTAssertEqual(subject.retrieve(identifiable: TestToggle.feature1).isActive, TestToggle.feature1.fallbackValue) + XCTAssertEqual(subject.retrieve(identifiable: TestToggle.feature2).isActive, TestToggle.feature2.fallbackValue) + + XCTAssertFalse(subject.retrieve(identifiable: TestToggle.feature1).isActive) + subject.save(featureToggle: FeatureToggle(identifier: TestToggle.feature1.rawValue, isActive: true)) + XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature1).isActive) + } + + func testSaveAndRetrieve() { + XCTAssertFalse(subject.retrieve(identifiable: TestToggle.feature1).isActive) + XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature2).isActive) + + subject.save(featureToggle: FeatureToggle(identifier: TestToggle.feature1.rawValue, isActive: true)) + XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature1).isActive) + XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature2).isActive) + + subject.save(featureToggle: FeatureToggle(identifier: TestToggle.feature2.rawValue, isActive: false)) + XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature1).isActive) + XCTAssertFalse(subject.retrieve(identifiable: TestToggle.feature2).isActive) + } + + static var allTests = [ + ("testSaveAndRetrieve", testSaveAndRetrieve), + ("testRetrieveBeforeSave", testRetrieveBeforeSave), + ] + +} diff --git a/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift b/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift index 96aae70..39fde04 100644 --- a/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift +++ b/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift @@ -3,7 +3,7 @@ import XCTest #if !canImport(ObjectiveC) public func allTests() -> [XCTestCaseEntry] { return [ - testCase(CloudKitFeatureTogglesTests.allTests), + testCase(FeatureToggleRepositoryTests.allTests), ] } #endif From 868658263867fa21d685afd60ed3c16d00d10941 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Wed, 1 Jan 2020 18:55:44 +0100 Subject: [PATCH 02/23] first implementation --- .../CloudKitSubscriptionProtocol.swift | 18 +++-- .../FeatureToggleRepository.swift | 5 +- .../FeatureToggleSubscriptor.swift | 13 +++- .../FeatureToggleSubscriptorTests.swift | 76 +++++++++++++++++++ .../XCTestManifests.swift | 1 + 5 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift diff --git a/Sources/CloudKitFeatureToggles/CloudKitSubscriptionProtocol.swift b/Sources/CloudKitFeatureToggles/CloudKitSubscriptionProtocol.swift index dcfda29..cf3e62f 100644 --- a/Sources/CloudKitFeatureToggles/CloudKitSubscriptionProtocol.swift +++ b/Sources/CloudKitFeatureToggles/CloudKitSubscriptionProtocol.swift @@ -9,8 +9,16 @@ import Foundation import CloudKit +protocol CloudKitDatabaseConformable { + func add(_ operation: CKDatabaseOperation) + func perform(_ query: CKQuery, inZoneWith zoneID: CKRecordZone.ID?, completionHandler: @escaping ([CKRecord]?, Error?) -> Void) +} + +extension CKDatabase: CloudKitDatabaseConformable {} + protocol CloudKitSubscriptionProtocol { var subscriptionID: String { get } + var database: CloudKitDatabaseConformable { get } func fetchAll() func saveSubscription() @@ -18,11 +26,11 @@ protocol CloudKitSubscriptionProtocol { } extension CloudKitSubscriptionProtocol { - func saveSubscription(subscriptionID: String, recordType: String, database: CKDatabase = CKContainer.default().publicCloudDatabase) { + func saveSubscription(subscriptionID: String, recordType: String, defaults: UserDefaults) { // Let's keep a local flag handy to avoid saving the subscription more than once. // Even if you try saving the subscription multiple times, the server doesn't save it more than once // Nevertheless, let's save some network operation and conserve resources - let subscriptionSaved = UserDefaults.standard.bool(forKey: subscriptionID) + let subscriptionSaved = defaults.bool(forKey: subscriptionID) guard !subscriptionSaved else { return } @@ -44,14 +52,14 @@ extension CloudKitSubscriptionProtocol { guard error == nil else { return } - UserDefaults.standard.set(true, forKey: subscriptionID) + defaults.set(true, forKey: subscriptionID) } operation.qualityOfService = .utility // Add the operation to the corresponding private or public database database.add(operation) } - func handleNotification(database: CKDatabase = CKContainer.default().publicCloudDatabase, recordType: String, recordFetchedBlock: @escaping (CKRecord) -> Void) { + func handleNotification(recordType: String, recordFetchedBlock: @escaping (CKRecord) -> Void) { let queryOperation = CKQueryOperation(query: query(recordType: recordType)) queryOperation.recordFetchedBlock = recordFetchedBlock @@ -60,7 +68,7 @@ extension CloudKitSubscriptionProtocol { database.add(queryOperation) } - func fetchAll(recordType: String, handler: @escaping ([CKRecord]) -> Void, database: CKDatabase = CKContainer.default().publicCloudDatabase) { + func fetchAll(recordType: String, handler: @escaping ([CKRecord]) -> Void) { database.perform(query(recordType: recordType), inZoneWith: nil) { (ckRecords, error) in guard error == nil, let ckRecords = ckRecords else { // don't update last fetched date, simply do nothing and try again next time diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift b/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift index 84e6f04..915565a 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift @@ -34,10 +34,11 @@ struct FeatureToggle: FeatureToggleRepresentable { public class FeatureToggleUserDefaultsRepository { + private static let defaultsSuiteName = "featureToggleUserDefaultsRepositorySuite" private let defaults: UserDefaults - init(defaults: UserDefaults = .standard) { - self.defaults = defaults + public init(defaults: UserDefaults? = nil) { + self.defaults = defaults ?? UserDefaults(suiteName: FeatureToggleUserDefaultsRepository.defaultsSuiteName) ?? .standard } } diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift index c6a0f24..a51d47c 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -9,21 +9,26 @@ import Foundation import CloudKit -class FeatureSwitchSubscriptor: CloudKitSubscriptionProtocol { +class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { + private static let defaultsSuiteName = "featureToggleDefaultsSuite" private let featureToggleRecordID: String private let featureToggleNameFieldID: String private let featureToggleIsActiveFieldID: String private let toggleRepository: FeatureToggleRepository + private let defaults: UserDefaults let subscriptionID = "cloudkit-recordType-FeatureToggle" + let database: CloudKitDatabaseConformable - init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive") { - self.toggleRepository = toggleRepository + init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { + self.toggleRepository = FeatureToggleUserDefaultsRepository(defaults: defaults) self.featureToggleRecordID = featureToggleRecordID self.featureToggleNameFieldID = featureToggleNameFieldID self.featureToggleIsActiveFieldID = featureToggleIsActiveFieldID + self.defaults = defaults + self.database = cloudKitDatabaseConformable } func fetchAll() { @@ -33,7 +38,7 @@ class FeatureSwitchSubscriptor: CloudKitSubscriptionProtocol { } func saveSubscription() { - saveSubscription(subscriptionID: subscriptionID, recordType: featureToggleRecordID) + saveSubscription(subscriptionID: subscriptionID, recordType: featureToggleRecordID, defaults: defaults) } func handleNotification() { diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift new file mode 100644 index 0000000..96e98cc --- /dev/null +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift @@ -0,0 +1,76 @@ +// +// FeatureToggleSubscriptorTests.swift +// CloudKitFeatureTogglesTests +// +// Created by Jonas Reichert on 02.01.20. +// + +import XCTest +import CloudKit +@testable import CloudKitFeatureToggles + +class FeatureToggleSubscriptorTests: XCTestCase { + + var subject: FeatureToggleSubscriptor! + var cloudKitDatabase: CloudKitDatabaseConformable! + let defaults = UserDefaults(suiteName: "testSuite") ?? .standard + + override func setUp() { + super.setUp() + + cloudKitDatabase = MockCloudKitDatabaseConformable() + subject = FeatureToggleSubscriptor(toggleRepository: MockToggleRepository(), defaults: defaults, cloudKitDatabaseConformable: cloudKitDatabase) + } + + override func tearDown() { + defaults.removePersistentDomain(forName: "testSuite") + + super.tearDown() + } + + func testFetchAll() { + + } + + func testSaveSubscription() { + + } + + func testHandleNotification() { + + } + + static var allTests = [ + ("testFetchAll", testFetchAll), + ("testSaveSubscription", testSaveSubscription), + ("testHandleNotification", testHandleNotification), + ] + +} + +class MockToggleRepository: FeatureToggleRepository { + var toggles: [String: FeatureToggleRepresentable] = [:] + + func save(featureToggle: FeatureToggleRepresentable) { + toggles[featureToggle.identifier] = featureToggle + } + + func retrieve(identifiable: FeatureToggleIdentifiable) -> FeatureToggleRepresentable { + return toggles[identifiable.identifier] ?? MockToggleRepresentable(identifier: identifiable.identifier, isActive: identifiable.fallbackValue) + } +} + +struct MockToggleRepresentable: FeatureToggleRepresentable { + var identifier: String + var isActive: Bool +} + +class MockCloudKitDatabaseConformable: CloudKitDatabaseConformable { + func add(_ operation: CKDatabaseOperation) { + + } + + func perform(_ query: CKQuery, inZoneWith zoneID: CKRecordZone.ID?, completionHandler: @escaping ([CKRecord]?, Error?) -> Void) { + completionHandler(nil, nil) + } +} diff --git a/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift b/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift index 39fde04..583a88c 100644 --- a/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift +++ b/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift @@ -4,6 +4,7 @@ import XCTest public func allTests() -> [XCTestCaseEntry] { return [ testCase(FeatureToggleRepositoryTests.allTests), + testCase(FeatureToggleSubscriptorTests.allTests), ] } #endif From b2ac04be2a251ee0c105c6a97c51bedcf198d8a3 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Sat, 4 Jan 2020 07:45:33 +0100 Subject: [PATCH 03/23] unit tests --- .../FeatureToggleSubscriptor.swift | 2 +- .../FeatureToggleSubscriptorTests.swift | 112 ++++++++++++++++-- 2 files changed, 106 insertions(+), 8 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift index a51d47c..fb7a847 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -23,7 +23,7 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { let database: CloudKitDatabaseConformable init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { - self.toggleRepository = FeatureToggleUserDefaultsRepository(defaults: defaults) + self.toggleRepository = toggleRepository self.featureToggleRecordID = featureToggleRecordID self.featureToggleNameFieldID = featureToggleNameFieldID self.featureToggleIsActiveFieldID = featureToggleIsActiveFieldID diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift index 96e98cc..70dca63 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift @@ -12,14 +12,16 @@ import CloudKit class FeatureToggleSubscriptorTests: XCTestCase { var subject: FeatureToggleSubscriptor! - var cloudKitDatabase: CloudKitDatabaseConformable! + var cloudKitDatabase: MockCloudKitDatabaseConformable! + var repository: MockToggleRepository! let defaults = UserDefaults(suiteName: "testSuite") ?? .standard override func setUp() { super.setUp() cloudKitDatabase = MockCloudKitDatabaseConformable() - subject = FeatureToggleSubscriptor(toggleRepository: MockToggleRepository(), defaults: defaults, cloudKitDatabaseConformable: cloudKitDatabase) + repository = MockToggleRepository() + subject = FeatureToggleSubscriptor(toggleRepository: repository, featureToggleRecordID: "TestFeatureStatus", featureToggleNameFieldID: "toggleName", featureToggleIsActiveFieldID: "isActive", defaults: defaults, cloudKitDatabaseConformable: cloudKitDatabase) } override func tearDown() { @@ -29,15 +31,93 @@ class FeatureToggleSubscriptorTests: XCTestCase { } func testFetchAll() { + XCTAssertNil(cloudKitDatabase.recordType) + XCTAssertEqual(repository.toggles.count, 0) + cloudKitDatabase.recordFetched["isActive"] = 1 + cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + + subject.fetchAll() + + XCTAssertEqual(repository.toggles.count, 1) + guard let toggle = repository.toggles.first else { + XCTFail() + return + } + XCTAssertEqual(toggle.identifier, "Toggle1") + XCTAssertTrue(toggle.isActive) + + cloudKitDatabase.recordFetched["isActive"] = 0 + cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + + subject.fetchAll() + + XCTAssertEqual(repository.toggles.count, 1) + + guard let toggle2 = repository.toggles.first else { + XCTFail() + return + } + XCTAssertEqual(toggle2.identifier, "Toggle1") + XCTAssertFalse(toggle2.isActive) } func testSaveSubscription() { + XCTAssertNil(cloudKitDatabase.subscriptionsToSave) + XCTAssertFalse(defaults.bool(forKey: subject.subscriptionID)) + + subject.saveSubscription() + + guard let firstSubscription = cloudKitDatabase.subscriptionsToSave?.first else { + XCTFail() + return + } + + XCTAssertEqual(firstSubscription.subscriptionID, subject.subscriptionID) + XCTAssertTrue(defaults.bool(forKey: subject.subscriptionID)) + XCTAssertEqual(cloudKitDatabase.addCalledCount, 1) + subject.saveSubscription() + XCTAssertEqual(firstSubscription.subscriptionID, subject.subscriptionID) + XCTAssertTrue(defaults.bool(forKey: subject.subscriptionID)) + XCTAssertEqual(cloudKitDatabase.addCalledCount, 1) } func testHandleNotification() { + XCTAssertNil(cloudKitDatabase.recordType) + XCTAssertEqual(cloudKitDatabase.addCalledCount, 0) + XCTAssertEqual(repository.toggles.count, 0) + cloudKitDatabase.recordFetched["isActive"] = 1 + cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + + subject.handleNotification() + + XCTAssertEqual(cloudKitDatabase.addCalledCount, 1) + XCTAssertEqual(cloudKitDatabase.recordType, "TestFeatureStatus") + XCTAssertEqual(repository.toggles.count, 1) + guard let toggle = repository.toggles.first else { + XCTFail() + return + } + XCTAssertEqual(toggle.identifier, "Toggle1") + XCTAssertTrue(toggle.isActive) + + cloudKitDatabase.recordFetched["isActive"] = 0 + cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + + subject.handleNotification() + + XCTAssertEqual(cloudKitDatabase.addCalledCount, 2) + XCTAssertEqual(cloudKitDatabase.recordType, "TestFeatureStatus") + XCTAssertEqual(repository.toggles.count, 1) + + guard let toggle2 = repository.toggles.first else { + XCTFail() + return + } + XCTAssertEqual(toggle2.identifier, "Toggle1") + XCTAssertFalse(toggle2.isActive) } static var allTests = [ @@ -49,14 +129,19 @@ class FeatureToggleSubscriptorTests: XCTestCase { } class MockToggleRepository: FeatureToggleRepository { - var toggles: [String: FeatureToggleRepresentable] = [:] + var toggles: [FeatureToggleRepresentable] = [] func save(featureToggle: FeatureToggleRepresentable) { - toggles[featureToggle.identifier] = featureToggle + toggles.removeAll { (representable) -> Bool in + representable.identifier == featureToggle.identifier + } + toggles.append(featureToggle) } func retrieve(identifiable: FeatureToggleIdentifiable) -> FeatureToggleRepresentable { - return toggles[identifiable.identifier] ?? MockToggleRepresentable(identifier: identifiable.identifier, isActive: identifiable.fallbackValue) + toggles.first { (representable) -> Bool in + representable.identifier == identifiable.identifier + } ?? MockToggleRepresentable(identifier: identifiable.identifier, isActive: identifiable.fallbackValue) } } @@ -66,11 +151,24 @@ struct MockToggleRepresentable: FeatureToggleRepresentable { } class MockCloudKitDatabaseConformable: CloudKitDatabaseConformable { + var addCalledCount = 0 + var subscriptionsToSave: [CKSubscription]? + var recordType: CKRecord.RecordType? + + var recordFetched = CKRecord(recordType: "TestFeatureStatus") + func add(_ operation: CKDatabaseOperation) { - + if let op = operation as? CKModifySubscriptionsOperation { + subscriptionsToSave = op.subscriptionsToSave + op.modifySubscriptionsCompletionBlock?(nil, nil, nil) + } else if let op = operation as? CKQueryOperation { + recordType = op.query?.recordType + op.recordFetchedBlock?(recordFetched) + } + addCalledCount += 1 } func perform(_ query: CKQuery, inZoneWith zoneID: CKRecordZone.ID?, completionHandler: @escaping ([CKRecord]?, Error?) -> Void) { - completionHandler(nil, nil) + completionHandler([recordFetched], nil) } } From 9b42ebe7e0e71d978e1d8773b54752e42ea362c3 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Sat, 4 Jan 2020 09:56:12 +0100 Subject: [PATCH 04/23] error test cases --- .../FeatureToggleSubscriptorTests.swift | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift index 70dca63..826f93b 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift @@ -11,6 +11,10 @@ import CloudKit class FeatureToggleSubscriptorTests: XCTestCase { + enum TestError: Error { + case generic + } + var subject: FeatureToggleSubscriptor! var cloudKitDatabase: MockCloudKitDatabaseConformable! var repository: MockToggleRepository! @@ -62,6 +66,21 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertFalse(toggle2.isActive) } + func testFetchAllError() { + cloudKitDatabase.error = TestError.generic + + XCTAssertNil(cloudKitDatabase.recordType) + XCTAssertEqual(repository.toggles.count, 0) + + cloudKitDatabase.recordFetched["isActive"] = 1 + cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + + subject.fetchAll() + + XCTAssertNil(cloudKitDatabase.recordType) + XCTAssertEqual(repository.toggles.count, 0) + } + func testSaveSubscription() { XCTAssertNil(cloudKitDatabase.subscriptionsToSave) XCTAssertFalse(defaults.bool(forKey: subject.subscriptionID)) @@ -83,6 +102,17 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertEqual(cloudKitDatabase.addCalledCount, 1) } + func testSaveSubscriptionError() { + cloudKitDatabase.error = TestError.generic + + XCTAssertNil(cloudKitDatabase.subscriptionsToSave) + XCTAssertFalse(defaults.bool(forKey: subject.subscriptionID)) + + subject.saveSubscription() + + XCTAssertFalse(defaults.bool(forKey: subject.subscriptionID)) + } + func testHandleNotification() { XCTAssertNil(cloudKitDatabase.recordType) XCTAssertEqual(cloudKitDatabase.addCalledCount, 0) @@ -156,11 +186,12 @@ class MockCloudKitDatabaseConformable: CloudKitDatabaseConformable { var recordType: CKRecord.RecordType? var recordFetched = CKRecord(recordType: "TestFeatureStatus") + var error: Error? func add(_ operation: CKDatabaseOperation) { if let op = operation as? CKModifySubscriptionsOperation { subscriptionsToSave = op.subscriptionsToSave - op.modifySubscriptionsCompletionBlock?(nil, nil, nil) + op.modifySubscriptionsCompletionBlock?(nil, nil, error) } else if let op = operation as? CKQueryOperation { recordType = op.query?.recordType op.recordFetchedBlock?(recordFetched) @@ -169,6 +200,10 @@ class MockCloudKitDatabaseConformable: CloudKitDatabaseConformable { } func perform(_ query: CKQuery, inZoneWith zoneID: CKRecordZone.ID?, completionHandler: @escaping ([CKRecord]?, Error?) -> Void) { - completionHandler([recordFetched], nil) + if let error = error { + completionHandler(nil, error) + } else { + completionHandler([recordFetched], error) + } } } From d478331e0539cd6aacf40bafdf59ce2f94b5b078 Mon Sep 17 00:00:00 2001 From: Jonas Reichert <2844335+JonnyBeeGod@users.noreply.github.com> Date: Sat, 4 Jan 2020 10:14:02 +0100 Subject: [PATCH 05/23] Create swift.yml --- .github/workflows/swift.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/swift.yml diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..fccadcc --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,17 @@ +name: Swift + +on: [push] + +jobs: + build: + + runs-on: macOS-latest + + steps: + - uses: actions/checkout@v1 + - name: Build + run: swift build -v + - name: Prepare xcodeproj + run: swift package generate-xcodeproj + - name: Run tests + run: xcodebuild test -scheme CloudKitFeatureToggles-Package -destination platform="macOS" -enableCodeCoverage YES -derivedDataPath .build/derivedData From 1effa741bfdce6d6b01ff5b5bdf3a3f2bf2a46a1 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Sat, 4 Jan 2020 10:18:07 +0100 Subject: [PATCH 06/23] added codecov --- .github/workflows/swift.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index fccadcc..caa5ce6 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -15,3 +15,5 @@ jobs: run: swift package generate-xcodeproj - name: Run tests run: xcodebuild test -scheme CloudKitFeatureToggles-Package -destination platform="macOS" -enableCodeCoverage YES -derivedDataPath .build/derivedData + - name: Codecov + run: bash <(curl -s https://codecov.io/bash) -D .build/derivedData/ -t ${{ secrets.CODECOV_TOKEN }} -J '^CloudKitFeatureToggles$' \ No newline at end of file From 9f9b5544d7a8c65f21a92ef52df48423c6fe7512 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Sat, 4 Jan 2020 10:47:56 +0100 Subject: [PATCH 07/23] added all unit tests to manifest --- .../FeatureToggleSubscriptorTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift index 826f93b..4f48b37 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift @@ -152,7 +152,9 @@ class FeatureToggleSubscriptorTests: XCTestCase { static var allTests = [ ("testFetchAll", testFetchAll), + ("testFetchAllError", testFetchAllError), ("testSaveSubscription", testSaveSubscription), + ("testSaveSubscriptionError", testSaveSubscriptionError), ("testHandleNotification", testHandleNotification), ] From 921c67d95fc109324c199a33781031d68a5f84fa Mon Sep 17 00:00:00 2001 From: Jonas Reichert <2844335+JonnyBeeGod@users.noreply.github.com> Date: Sat, 4 Jan 2020 10:52:49 +0100 Subject: [PATCH 08/23] Update README.md --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c08f77e..6a86981 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ -# CloudKitFeatureToggles +# CloudKit FeatureToggles -A description of this package. +![](https://github.com/JonnyBeeGod/CloudKitFeatureToggles/workflows/Swift/badge.svg) +[![codecov](https://codecov.io/gh/JonnyBeeGod/CloudKitFeatureToggles/branch/master/graph/badge.svg?token=y21zGNAsLL)](https://codecov.io/gh/JonnyBeeGod/CloudKitFeatureToggles) + + + Swift Package Manager + +Mac + Linux + + Twitter: @jonezdotcom + From a464f056f6aaa7936e328e393e7c3da84c4be9d6 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Sun, 5 Jan 2020 07:43:15 +0100 Subject: [PATCH 09/23] started implementing applicationService --- .../FeatureToggleApplicationService.swift | 52 +++++++++++++++++++ .../FeatureToggleRepository.swift | 7 +-- 2 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift new file mode 100644 index 0000000..38da0d3 --- /dev/null +++ b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift @@ -0,0 +1,52 @@ +// +// FeatureToggleApplicationService.swift +// CloudKitFeatureToggles +// +// Created by Jonas Reichert on 04.01.20. +// + +import Foundation +import CloudKit +#if canImport(UIKit) +import UIKit +#endif + +public protocol FeatureToggleApplicationServiceProtocol { + var featureToggleRepository: FeatureToggleRepository { get } +} + +public class FeatureToggleApplicationService: FeatureToggleApplicationServiceProtocol { + + private let featureToggleSubscriptor: CloudKitSubscriptionProtocol + private (set) public var featureToggleRepository: FeatureToggleRepository + + public convenience init(featureToggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository()) { + self.init(featureToggleSubscriptor: FeatureToggleSubscriptor(toggleRepository: featureToggleRepository), featureToggleRepository: featureToggleRepository) + self.featureToggleRepository = featureToggleRepository + } + + init(featureToggleSubscriptor: CloudKitSubscriptionProtocol = FeatureToggleSubscriptor(), featureToggleRepository: FeatureToggleRepository) { + self.featureToggleSubscriptor = featureToggleSubscriptor + self.featureToggleRepository = featureToggleRepository + } +} + +#if canImport(UIKit) +extension FeatureToggleApplicationService: UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + application.registerForRemoteNotifications() + featureToggleSubscriptor.saveSubscriptions() + featureToggleSubscriptor.fetchAllProviders() + + return true + } + + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) else { + return + } + + featureToggleSubscriptor.handleNotification(subscriptionID: notification.subscriptionID, completionHandler: completionHandler) + } +} +#endif diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift b/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift index 915565a..33a0355 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift @@ -17,12 +17,9 @@ public protocol FeatureToggleIdentifiable { var fallbackValue: Bool { get } } -public protocol FeatureToggleRetrievable { +public protocol FeatureToggleRepository { /// retrieves a stored `FeatureToggleRepresentable` from the underlying store. func retrieve(identifiable: FeatureToggleIdentifiable) -> FeatureToggleRepresentable -} - -protocol FeatureToggleRepository: FeatureToggleRetrievable { /// saves a supplied `FeatureToggleRepresentable` to the underlying store func save(featureToggle: FeatureToggleRepresentable) } @@ -49,7 +46,7 @@ extension FeatureToggleUserDefaultsRepository: FeatureToggleRepository { return FeatureToggle(identifier: identifiable.identifier, isActive: isActive ?? identifiable.fallbackValue) } - func save(featureToggle: FeatureToggleRepresentable) { + public func save(featureToggle: FeatureToggleRepresentable) { defaults.set(featureToggle.isActive, forKey: featureToggle.identifier) } } From 70057a36539876b3fb26f0951cb457d5a7f12862 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Sun, 5 Jan 2020 08:00:07 +0100 Subject: [PATCH 10/23] platform adjustments --- .../FeatureToggleApplicationService.swift | 23 ++++++++++++++--- ...FeatureToggleApplicationServiceTests.swift | 25 +++++++++++++++++++ .../XCTestManifests.swift | 1 + 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift index 38da0d3..d242474 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift @@ -13,6 +13,11 @@ import UIKit public protocol FeatureToggleApplicationServiceProtocol { var featureToggleRepository: FeatureToggleRepository { get } + + #if canImport(UIKit) + func register(application: UIApplication) + func handleNotification(subscriptionID: String, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) + #endif } public class FeatureToggleApplicationService: FeatureToggleApplicationServiceProtocol { @@ -29,14 +34,24 @@ public class FeatureToggleApplicationService: FeatureToggleApplicationServicePro self.featureToggleSubscriptor = featureToggleSubscriptor self.featureToggleRepository = featureToggleRepository } + + #if canImport(UIKit) + public func register(application: UIApplication) { + application.registerForRemoteNotifications() + featureToggleSubscriptor.saveSubscriptions() + featureToggleSubscriptor.fetchAllProviders() + } + + public func handleNotification(subscriptionID: String, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + featureToggleSubscriptor.handleNotification(subscriptionID: notification.subscriptionID, completionHandler: completionHandler) + } + #endif } #if canImport(UIKit) extension FeatureToggleApplicationService: UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - application.registerForRemoteNotifications() - featureToggleSubscriptor.saveSubscriptions() - featureToggleSubscriptor.fetchAllProviders() + register(application: application) return true } @@ -46,7 +61,7 @@ extension FeatureToggleApplicationService: UIApplicationDelegate { return } - featureToggleSubscriptor.handleNotification(subscriptionID: notification.subscriptionID, completionHandler: completionHandler) + handleNotification(subscriptionID: notification.subscriptionID, completionHandler: completionHandler) } } #endif diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift new file mode 100644 index 0000000..c00ff4e --- /dev/null +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift @@ -0,0 +1,25 @@ +// +// FeatureToggleApplicationServiceTests.swift +// CloudKitFeatureTogglesTests +// +// Created by Jonas Reichert on 05.01.20. +// + +import XCTest + +class FeatureToggleApplicationServiceTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + + + static var allTests = [ +// ("testFetchAll", testFetchAll), + ] +} diff --git a/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift b/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift index 583a88c..beb7796 100644 --- a/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift +++ b/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift @@ -5,6 +5,7 @@ public func allTests() -> [XCTestCaseEntry] { return [ testCase(FeatureToggleRepositoryTests.allTests), testCase(FeatureToggleSubscriptorTests.allTests), + testCase(FeatureToggleApplicationServiceTests.allTests), ] } #endif From c692a5c12ff169f8f232ecfb87ebdfa623a6a295 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Sun, 5 Jan 2020 08:13:08 +0100 Subject: [PATCH 11/23] unit tests --- .../FeatureToggleApplicationService.swift | 3 +- ...FeatureToggleApplicationServiceTests.swift | 46 ++++++++++++++++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift index d242474..817ee47 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift @@ -27,10 +27,9 @@ public class FeatureToggleApplicationService: FeatureToggleApplicationServicePro public convenience init(featureToggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository()) { self.init(featureToggleSubscriptor: FeatureToggleSubscriptor(toggleRepository: featureToggleRepository), featureToggleRepository: featureToggleRepository) - self.featureToggleRepository = featureToggleRepository } - init(featureToggleSubscriptor: CloudKitSubscriptionProtocol = FeatureToggleSubscriptor(), featureToggleRepository: FeatureToggleRepository) { + init(featureToggleSubscriptor: CloudKitSubscriptionProtocol, featureToggleRepository: FeatureToggleRepository) { self.featureToggleSubscriptor = featureToggleSubscriptor self.featureToggleRepository = featureToggleRepository } diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift index c00ff4e..ca96bc0 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift @@ -6,20 +6,54 @@ // import XCTest +@testable import CloudKitFeatureToggles class FeatureToggleApplicationServiceTests: XCTestCase { + + let defaults = UserDefaults(suiteName: "testSuite") ?? .standard + var repository: MockToggleRepository! + var subscriptor: CloudKitSubscriptionProtocol! + var subject: FeatureToggleApplicationServiceProtocol! override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. + repository = MockToggleRepository() + subscriptor = FeatureToggleSubscriptor(toggleRepository: repository, defaults: defaults, cloudKitDatabaseConformable: MockCloudKitDatabaseConformable()) + subject = FeatureToggleApplicationService(featureToggleSubscriptor: subscriptor, featureToggleRepository: repository) } + func testRegister() { + #if canImport(UIKit) + subject.register(application: UIApplication.shared()) + + #endif + } + func testHandle() { + + } static var allTests = [ -// ("testFetchAll", testFetchAll), + ("testRegister", testRegister), + ("testHandle", testHandle), ] } + +class MockFeatureToggleSubscriptor: CloudKitSubscriptionProtocol { + var subscriptionID: String = "Mock" + var database: CloudKitDatabaseConformable = MockCloudKitDatabaseConformable() + + var saveSubscriptionCalled = false + var handleCalled = false + + func handleNotification() { + handleCalled = true + } + + func saveSubscription() { + saveSubscriptionCalled = true + } + + func fetchAll() { + + } +} From c858d7d4e93297e49c6411851267ff5eb271bca2 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Sun, 5 Jan 2020 08:39:08 +0100 Subject: [PATCH 12/23] unit tests --- .../FeatureToggleApplicationService.swift | 28 +++++++++++-------- ...FeatureToggleApplicationServiceTests.swift | 28 +++++++++++++++---- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift index 817ee47..83f5e04 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift @@ -16,13 +16,13 @@ public protocol FeatureToggleApplicationServiceProtocol { #if canImport(UIKit) func register(application: UIApplication) - func handleNotification(subscriptionID: String, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) + func handleNotification(subscriptionID: String?, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) #endif } -public class FeatureToggleApplicationService: FeatureToggleApplicationServiceProtocol { +public class FeatureToggleApplicationService: NSObject, FeatureToggleApplicationServiceProtocol { - private let featureToggleSubscriptor: CloudKitSubscriptionProtocol + private var featureToggleSubscriptor: CloudKitSubscriptionProtocol private (set) public var featureToggleRepository: FeatureToggleRepository public convenience init(featureToggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository()) { @@ -37,30 +37,36 @@ public class FeatureToggleApplicationService: FeatureToggleApplicationServicePro #if canImport(UIKit) public func register(application: UIApplication) { application.registerForRemoteNotifications() - featureToggleSubscriptor.saveSubscriptions() - featureToggleSubscriptor.fetchAllProviders() + featureToggleSubscriptor.saveSubscription() + featureToggleSubscriptor.fetchAll() } - public func handleNotification(subscriptionID: String, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - featureToggleSubscriptor.handleNotification(subscriptionID: notification.subscriptionID, completionHandler: completionHandler) + public func handleNotification(subscriptionID: String?, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + if let subscriptionID = subscriptionID, featureToggleSubscriptor.subscriptionID == subscriptionID { + featureToggleSubscriptor.handleNotification() + completionHandler(.newData) + } + else { + completionHandler(.noData) + } } #endif } #if canImport(UIKit) extension FeatureToggleApplicationService: UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { register(application: application) return true } - func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) else { + public func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo), let subscriptionID = notification.subscriptionID else { return } - handleNotification(subscriptionID: notification.subscriptionID, completionHandler: completionHandler) + handleNotification(subscriptionID: subscriptionID, completionHandler: completionHandler) } } #endif diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift index ca96bc0..ac0a0e5 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift @@ -10,26 +10,43 @@ import XCTest class FeatureToggleApplicationServiceTests: XCTestCase { - let defaults = UserDefaults(suiteName: "testSuite") ?? .standard var repository: MockToggleRepository! - var subscriptor: CloudKitSubscriptionProtocol! + var subscriptor: MockFeatureToggleSubscriptor! var subject: FeatureToggleApplicationServiceProtocol! override func setUp() { repository = MockToggleRepository() - subscriptor = FeatureToggleSubscriptor(toggleRepository: repository, defaults: defaults, cloudKitDatabaseConformable: MockCloudKitDatabaseConformable()) + subscriptor = MockFeatureToggleSubscriptor() subject = FeatureToggleApplicationService(featureToggleSubscriptor: subscriptor, featureToggleRepository: repository) } func testRegister() { #if canImport(UIKit) - subject.register(application: UIApplication.shared()) + XCTAssertFalse(subscriptor.saveSubscriptionCalled) + XCTAssertFalse(subscriptor.handleCalled) + XCTAssertFalse(subscriptor.fetchAllCalled) + subject.register(application: UIApplication.shared) + + XCTAssertTrue(subscriptor.saveSubscriptionCalled) + XCTAssertFalse(subscriptor.handleCalled) + XCTAssertTrue(subscriptor.fetchAllCalled) #endif } func testHandle() { + #if canImport(UIKit) + XCTAssertFalse(subscriptor.saveSubscriptionCalled) + XCTAssertFalse(subscriptor.handleCalled) + XCTAssertFalse(subscriptor.fetchAllCalled) + + subject.handleNotification(subscriptionID: "Mock", completionHandler: { result in + }) + XCTAssertFalse(subscriptor.saveSubscriptionCalled) + XCTAssertTrue(subscriptor.handleCalled) + XCTAssertFalse(subscriptor.fetchAllCalled) + #endif } static var allTests = [ @@ -44,6 +61,7 @@ class MockFeatureToggleSubscriptor: CloudKitSubscriptionProtocol { var saveSubscriptionCalled = false var handleCalled = false + var fetchAllCalled = false func handleNotification() { handleCalled = true @@ -54,6 +72,6 @@ class MockFeatureToggleSubscriptor: CloudKitSubscriptionProtocol { } func fetchAll() { - + fetchAllCalled = true } } From de30e6be9d0ccd8cd0b693c6d20142bffcfe710b Mon Sep 17 00:00:00 2001 From: Jonas Reichert <2844335+JonnyBeeGod@users.noreply.github.com> Date: Sun, 5 Jan 2020 09:23:02 +0100 Subject: [PATCH 13/23] Update README.md --- README.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a86981..4837d84 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,37 @@ Swift Package Manager -Mac + Linux +iOS Twitter: @jonezdotcom + +## What does it do? + +## How to install? +CloudKitFeatureToggles is compatible with Swift Package Manager. To install, simply add this repository URL to your swift packages as package dependency in Xcode. +Alternatively, add this line to your `Package.swift` file: + +``` +dependencies: [ + .package(url: "https://github.com/JonnyBeeGod/CloudKitFeatureToggles", from: "0.1.0") +] +``` + +And don't forget to add the dependency to your target(s). + +## How to use? +1. In your AppDelegate, initialize a `FeatureToggleApplicationService` and hook its two `UIApplicationDelegate` methods into the AppDelegate lifecycle like so: + +``` +func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return featureToggleApplicationService.application(application, didFinishLaunchingWithOptions: launchOptions) +} +func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + featureToggleApplicationService.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) +} + +``` +2. Anywhere in your code you can create an instance of `FeatureToggleUserDefaultsRepository` and call `retrieve` to fetch the latest status for your feature toggle. + From 7ab9a77e1c1360e8b991fae04a9eca7d27cd1b56 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Mon, 6 Jan 2020 07:02:23 +0100 Subject: [PATCH 14/23] implemented sending of notification --- .../FeatureToggleApplicationService.swift | 6 +++--- .../FeatureToggleSubscriptor.swift | 10 +++++++++- .../Notification+Extensions.swift | 12 ++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 Sources/CloudKitFeatureToggles/Notification+Extensions.swift diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift index 83f5e04..acc66c7 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift @@ -16,7 +16,7 @@ public protocol FeatureToggleApplicationServiceProtocol { #if canImport(UIKit) func register(application: UIApplication) - func handleNotification(subscriptionID: String?, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) + func handleRemoteNotification(subscriptionID: String?, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) #endif } @@ -41,7 +41,7 @@ public class FeatureToggleApplicationService: NSObject, FeatureToggleApplication featureToggleSubscriptor.fetchAll() } - public func handleNotification(subscriptionID: String?, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + public func handleRemoteNotification(subscriptionID: String?, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if let subscriptionID = subscriptionID, featureToggleSubscriptor.subscriptionID == subscriptionID { featureToggleSubscriptor.handleNotification() completionHandler(.newData) @@ -66,7 +66,7 @@ extension FeatureToggleApplicationService: UIApplicationDelegate { return } - handleNotification(subscriptionID: subscriptionID, completionHandler: completionHandler) + handleRemoteNotification(subscriptionID: subscriptionID, completionHandler: completionHandler) } } #endif diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift index fb7a847..12fc776 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -18,22 +18,25 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { private let toggleRepository: FeatureToggleRepository private let defaults: UserDefaults + private let notificationCenter: NotificationCenter let subscriptionID = "cloudkit-recordType-FeatureToggle" let database: CloudKitDatabaseConformable - init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { + init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, notificationCenter: NotificationCenter = .default, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { self.toggleRepository = toggleRepository self.featureToggleRecordID = featureToggleRecordID self.featureToggleNameFieldID = featureToggleNameFieldID self.featureToggleIsActiveFieldID = featureToggleIsActiveFieldID self.defaults = defaults + self.notificationCenter = notificationCenter self.database = cloudKitDatabaseConformable } func fetchAll() { fetchAll(recordType: featureToggleRecordID, handler: { (ckRecords) in self.updateRepository(with: ckRecords) + self.sendNotification(records: ckRecords) }) } @@ -44,6 +47,7 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { func handleNotification() { handleNotification(recordType: featureToggleRecordID) { (record) in self.updateRepository(with: [record]) + self.sendNotification(records: [record]) } } @@ -54,4 +58,8 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { } } } + + private func sendNotification(records: [CKRecord]) { + notificationCenter.post(name: Notification.Name.onRecordsUpdated, object: nil, userInfo: ["records" : records]) + } } diff --git a/Sources/CloudKitFeatureToggles/Notification+Extensions.swift b/Sources/CloudKitFeatureToggles/Notification+Extensions.swift new file mode 100644 index 0000000..d6ef3e8 --- /dev/null +++ b/Sources/CloudKitFeatureToggles/Notification+Extensions.swift @@ -0,0 +1,12 @@ +// +// Notification+Extensions.swift +// CloudKitFeatureToggles +// +// Created by Jonas Reichert on 06.01.20. +// + +import Foundation + +extension Notification.Name { + public static let onRecordsUpdated = Notification.Name("ckFeatureTogglesRecordsUpdatedNotification") +} From 10318ebf545a0ba2e2b6eff172abad0e21cabe0a Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Mon, 6 Jan 2020 07:07:57 +0100 Subject: [PATCH 15/23] updated unit tests --- ...FeatureToggleApplicationServiceTests.swift | 2 +- .../FeatureToggleSubscriptorTests.swift | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift index ac0a0e5..9d07c53 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleApplicationServiceTests.swift @@ -40,7 +40,7 @@ class FeatureToggleApplicationServiceTests: XCTestCase { XCTAssertFalse(subscriptor.handleCalled) XCTAssertFalse(subscriptor.fetchAllCalled) - subject.handleNotification(subscriptionID: "Mock", completionHandler: { result in + subject.handleRemoteNotification(subscriptionID: "Mock", completionHandler: { result in }) XCTAssertFalse(subscriptor.saveSubscriptionCalled) diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift index 4f48b37..165d2c6 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift @@ -81,6 +81,15 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertEqual(repository.toggles.count, 0) } + func testFetchAllNotification() { + let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil, handler: nil) + cloudKitDatabase.recordFetched["isActive"] = 1 + cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + + subject.fetchAll() + wait(for: [expectation], timeout: 0.1) + } + func testSaveSubscription() { XCTAssertNil(cloudKitDatabase.subscriptionsToSave) XCTAssertFalse(defaults.bool(forKey: subject.subscriptionID)) @@ -150,6 +159,16 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertFalse(toggle2.isActive) } + func testHandleNotificationSendNotification() { + let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil, handler: nil) + cloudKitDatabase.recordFetched["isActive"] = 1 + cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + + subject.handleNotification() + wait(for: [expectation], timeout: 0.1) + + } + static var allTests = [ ("testFetchAll", testFetchAll), ("testFetchAllError", testFetchAllError), From e5f7621cc34220024c737531b4b52c93cc1d43de Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Mon, 6 Jan 2020 07:31:39 +0100 Subject: [PATCH 16/23] implemented mapper for feature toggle, wrote unit tests and some refactoring --- .../FeatureToggleMapper.swift | 34 +++++++ .../FeatureToggleRepository.swift | 15 ---- .../FeatureToggleSubscriptor.swift | 8 +- .../FeatureToggleMapperTests.swift | 90 +++++++++++++++++++ .../FeatureToggleSubscriptorTests.swift | 2 + .../XCTestManifests.swift | 1 + 6 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift create mode 100644 Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift new file mode 100644 index 0000000..09c7e03 --- /dev/null +++ b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift @@ -0,0 +1,34 @@ +// +// FeatureToggleMapper.swift +// CloudKitFeatureToggles +// +// Created by Jonas Reichert on 06.01.20. +// + +import Foundation +import CloudKit + +public protocol FeatureToggleRepresentable { + var identifier: String { get } + var isActive: Bool { get } +} + +public protocol FeatureToggleIdentifiable { + var identifier: String { get } + var fallbackValue: Bool { get } +} + +public struct FeatureToggle: FeatureToggleRepresentable, Equatable { + public let identifier: String + public let isActive: Bool +} + +protocol FeatureToggleMappable { + func map(record: CKRecord) -> FeatureToggle? +} + +class FeatureToggleMapper: FeatureToggleMappable { + func map(record: CKRecord) -> FeatureToggle? { + return nil + } +} diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift b/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift index 33a0355..ffb4163 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift @@ -7,16 +7,6 @@ import Foundation -public protocol FeatureToggleRepresentable { - var identifier: String { get } - var isActive: Bool { get } -} - -public protocol FeatureToggleIdentifiable { - var identifier: String { get } - var fallbackValue: Bool { get } -} - public protocol FeatureToggleRepository { /// retrieves a stored `FeatureToggleRepresentable` from the underlying store. func retrieve(identifiable: FeatureToggleIdentifiable) -> FeatureToggleRepresentable @@ -24,11 +14,6 @@ public protocol FeatureToggleRepository { func save(featureToggle: FeatureToggleRepresentable) } -struct FeatureToggle: FeatureToggleRepresentable { - let identifier: String - let isActive: Bool -} - public class FeatureToggleUserDefaultsRepository { private static let defaultsSuiteName = "featureToggleUserDefaultsRepositorySuite" diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift index 12fc776..2586482 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -17,14 +17,16 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { private let featureToggleIsActiveFieldID: String private let toggleRepository: FeatureToggleRepository + private let toggleMapper: FeatureToggleMappable private let defaults: UserDefaults private let notificationCenter: NotificationCenter let subscriptionID = "cloudkit-recordType-FeatureToggle" let database: CloudKitDatabaseConformable - init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, notificationCenter: NotificationCenter = .default, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { + init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), toggleMapper: FeatureToggleMappable = FeatureToggleMapper(), featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, notificationCenter: NotificationCenter = .default, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { self.toggleRepository = toggleRepository + self.toggleMapper = toggleMapper self.featureToggleRecordID = featureToggleRecordID self.featureToggleNameFieldID = featureToggleNameFieldID self.featureToggleIsActiveFieldID = featureToggleIsActiveFieldID @@ -60,6 +62,8 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { } private func sendNotification(records: [CKRecord]) { - notificationCenter.post(name: Notification.Name.onRecordsUpdated, object: nil, userInfo: ["records" : records]) + notificationCenter.post(name: Notification.Name.onRecordsUpdated, object: nil, userInfo: ["records" : records.compactMap({ (record) -> FeatureToggle? in + return toggleMapper.map(record: record) + })]) } } diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift new file mode 100644 index 0000000..297dd72 --- /dev/null +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift @@ -0,0 +1,90 @@ +// +// FeatureToggleMapperTests.swift +// CloudKitFeatureTogglesTests +// +// Created by Jonas Reichert on 06.01.20. +// + +import XCTest +import CloudKit +@testable import CloudKitFeatureToggles + +class FeatureToggleMapperTests: XCTestCase { + + var subject: FeatureToggleMappable! + + override func setUp() { + subject = FeatureToggleMapper() + } + + func testMapInvalidInput() { + let everythingWrong = CKRecord(recordType: "RecordType", recordID: CKRecord.ID(recordName: "identifier")) + everythingWrong["bla"] = true + everythingWrong["muh"] = 1283765 + + XCTAssertNil(subject.map(record: everythingWrong)) + + let wrongFields = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier2")) + wrongFields["bla"] = true + wrongFields["muh"] = 1283765 + + XCTAssertNil(subject.map(record: wrongFields)) + + let wrongIsActiveField = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier3")) + wrongIsActiveField["bla"] = true + wrongIsActiveField["featureName"] = 1283765 + + XCTAssertNil(subject.map(record: wrongIsActiveField)) + + let wrongFeatureNameField = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier4")) + wrongFeatureNameField["isActive"] = true + wrongFeatureNameField["muh"] = 1283765 + + XCTAssertNil(subject.map(record: wrongFeatureNameField)) + + let wrongIsActiveType = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier5")) + wrongIsActiveType["isActive"] = "true" + wrongIsActiveType["featureName"] = "1283765" + + XCTAssertNil(subject.map(record: wrongIsActiveType)) + + let wrongFeatureNameType = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier6")) + wrongFeatureNameType["isActive"] = true + wrongFeatureNameType["featureName"] = 1283765 + + XCTAssertNil(subject.map(record: wrongFeatureNameType)) + } + + func testMap() { + let expectedIdentifier = "1283765" + let expectedIsActive = true + + let record = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier")) + record["isActive"] = expectedIsActive + record["featureName"] = expectedIdentifier + + let result = subject.map(record: record) + XCTAssertNotNil(result) + XCTAssertEqual(result, FeatureToggle(identifier: expectedIdentifier, isActive: expectedIsActive)) + } + + func testMap2() { + let expectedIdentifier = "akjshgdjaskd(/(/&%$§" + let expectedIsActive = false + + let record = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier")) + record["isActive"] = expectedIsActive + record["featureName"] = expectedIdentifier + + let result = subject.map(record: record) + XCTAssertNotNil(result) + XCTAssertEqual(result, FeatureToggle(identifier: expectedIdentifier, isActive: expectedIsActive)) + } + + static var allTests = [ + ("testMapInvalidInput", testMapInvalidInput), + ("testMap", testMap), + ("testMap2", testMap2), + ] + +} diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift index 165d2c6..6f67678 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift @@ -172,9 +172,11 @@ class FeatureToggleSubscriptorTests: XCTestCase { static var allTests = [ ("testFetchAll", testFetchAll), ("testFetchAllError", testFetchAllError), + ("testFetchAllNotification", testFetchAllNotification), ("testSaveSubscription", testSaveSubscription), ("testSaveSubscriptionError", testSaveSubscriptionError), ("testHandleNotification", testHandleNotification), + ("testHandleNotificationSendNotification", testHandleNotificationSendNotification), ] } diff --git a/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift b/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift index beb7796..93b9594 100644 --- a/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift +++ b/Tests/CloudKitFeatureTogglesTests/XCTestManifests.swift @@ -6,6 +6,7 @@ public func allTests() -> [XCTestCaseEntry] { testCase(FeatureToggleRepositoryTests.allTests), testCase(FeatureToggleSubscriptorTests.allTests), testCase(FeatureToggleApplicationServiceTests.allTests), + testCase(FeatureToggleMapperTests.allTests), ] } #endif From 0ae989cdce54c48f8463da80d6c158890ae544fc Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Mon, 6 Jan 2020 07:42:03 +0100 Subject: [PATCH 17/23] implemented mapper --- .../FeatureToggleMapper.swift | 14 +++++++++++++- .../FeatureToggleSubscriptor.swift | 16 ++++++++-------- .../FeatureToggleMapperTests.swift | 2 +- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift index 09c7e03..404537e 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift @@ -28,7 +28,19 @@ protocol FeatureToggleMappable { } class FeatureToggleMapper: FeatureToggleMappable { + private let featureToggleNameFieldID: String + private let featureToggleIsActiveFieldID: String + + init(featureToggleNameFieldID: String, featureToggleIsActiveFieldID: String) { + self.featureToggleNameFieldID = featureToggleNameFieldID + self.featureToggleIsActiveFieldID = featureToggleIsActiveFieldID + } + func map(record: CKRecord) -> FeatureToggle? { - return nil + guard let isActive = record[featureToggleIsActiveFieldID] as? Int64, let featureName = record[featureToggleNameFieldID] as? String else { + return nil + } + + return FeatureToggle(identifier: featureName, isActive: NSNumber(value: isActive).boolValue) } } diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift index 2586482..9842c2d 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -13,8 +13,6 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { private static let defaultsSuiteName = "featureToggleDefaultsSuite" private let featureToggleRecordID: String - private let featureToggleNameFieldID: String - private let featureToggleIsActiveFieldID: String private let toggleRepository: FeatureToggleRepository private let toggleMapper: FeatureToggleMappable @@ -24,12 +22,10 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { let subscriptionID = "cloudkit-recordType-FeatureToggle" let database: CloudKitDatabaseConformable - init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), toggleMapper: FeatureToggleMappable = FeatureToggleMapper(), featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, notificationCenter: NotificationCenter = .default, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { + init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), toggleMapper: FeatureToggleMappable? = nil, featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, notificationCenter: NotificationCenter = .default, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { self.toggleRepository = toggleRepository - self.toggleMapper = toggleMapper + self.toggleMapper = toggleMapper ?? FeatureToggleMapper(featureToggleNameFieldID: featureToggleNameFieldID, featureToggleIsActiveFieldID: featureToggleIsActiveFieldID) self.featureToggleRecordID = featureToggleRecordID - self.featureToggleNameFieldID = featureToggleNameFieldID - self.featureToggleIsActiveFieldID = featureToggleIsActiveFieldID self.defaults = defaults self.notificationCenter = notificationCenter self.database = cloudKitDatabaseConformable @@ -55,8 +51,12 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { private func updateRepository(with ckRecords: [CKRecord]) { ckRecords.forEach { (record) in - if let active = record[featureToggleIsActiveFieldID] as? Int64, let featureName = record[featureToggleNameFieldID] as? String { - toggleRepository.save(featureToggle: FeatureToggle(identifier: featureName, isActive: NSNumber(value: active).boolValue)) + let toggles = ckRecords.compactMap { (record) -> FeatureToggle? in + return toggleMapper.map(record: record) + } + + toggles.forEach { (toggle) in + toggleRepository.save(featureToggle: toggle) } } } diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift index 297dd72..a174253 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift @@ -14,7 +14,7 @@ class FeatureToggleMapperTests: XCTestCase { var subject: FeatureToggleMappable! override func setUp() { - subject = FeatureToggleMapper() + subject = FeatureToggleMapper(featureToggleNameFieldID: "featureName", featureToggleIsActiveFieldID: "isActive") } func testMapInvalidInput() { From 08f28a4d4c973092ea2a357fbf7be604c4756832 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Mon, 6 Jan 2020 07:46:44 +0100 Subject: [PATCH 18/23] refactoring --- .../FeatureToggleSubscriptor.swift | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift index 9842c2d..a17ddc6 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -33,8 +33,12 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { func fetchAll() { fetchAll(recordType: featureToggleRecordID, handler: { (ckRecords) in - self.updateRepository(with: ckRecords) - self.sendNotification(records: ckRecords) + let toggles = ckRecords.compactMap { (record) -> FeatureToggle? in + return self.toggleMapper.map(record: record) + } + + self.updateRepository(with: toggles) + self.sendNotification(with: toggles) }) } @@ -44,26 +48,22 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { func handleNotification() { handleNotification(recordType: featureToggleRecordID) { (record) in - self.updateRepository(with: [record]) - self.sendNotification(records: [record]) + guard let toggle = self.toggleMapper.map(record: record) else { + return + } + + self.updateRepository(with: [toggle]) + self.sendNotification(with: [toggle]) } } - private func updateRepository(with ckRecords: [CKRecord]) { - ckRecords.forEach { (record) in - let toggles = ckRecords.compactMap { (record) -> FeatureToggle? in - return toggleMapper.map(record: record) - } - - toggles.forEach { (toggle) in - toggleRepository.save(featureToggle: toggle) - } + private func updateRepository(with toggles: [FeatureToggle]) { + toggles.forEach { (toggle) in + toggleRepository.save(featureToggle: toggle) } } - private func sendNotification(records: [CKRecord]) { - notificationCenter.post(name: Notification.Name.onRecordsUpdated, object: nil, userInfo: ["records" : records.compactMap({ (record) -> FeatureToggle? in - return toggleMapper.map(record: record) - })]) + private func sendNotification(with toggles: [FeatureToggle]) { + notificationCenter.post(name: Notification.Name.onRecordsUpdated, object: nil, userInfo: ["records" : toggles]) } } From 7c5e04ec946942226b8b6fce0efc55b3f0b00201 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Mon, 6 Jan 2020 08:19:31 +0100 Subject: [PATCH 19/23] unit test adjustments --- .../FeatureToggleSubscriptor.swift | 8 ++- .../FeatureToggleSubscriptorTests.swift | 49 ++++++++++++++++++- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift index a17ddc6..632900a 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -48,12 +48,10 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { func handleNotification() { handleNotification(recordType: featureToggleRecordID) { (record) in - guard let toggle = self.toggleMapper.map(record: record) else { - return - } + let toggle = self.toggleMapper.map(record: record) - self.updateRepository(with: [toggle]) - self.sendNotification(with: [toggle]) + self.updateRepository(with: [toggle].compactMap { $0 }) + self.sendNotification(with: [toggle].compactMap { $0 }) } } diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift index 6f67678..77e5af7 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift @@ -82,7 +82,13 @@ class FeatureToggleSubscriptorTests: XCTestCase { } func testFetchAllNotification() { - let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil, handler: nil) + let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil) { (notification) -> Bool in + guard let userInfo = notification.userInfo, let toggles = userInfo["records"] as? [FeatureToggle] else { + return false + } + + return toggles.count == 1 + } cloudKitDatabase.recordFetched["isActive"] = 1 cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" @@ -90,6 +96,22 @@ class FeatureToggleSubscriptorTests: XCTestCase { wait(for: [expectation], timeout: 0.1) } + func testFetchAllNotMappableRecord() { + let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil) { (notification) -> Bool in + guard let userInfo = notification.userInfo, let toggles = userInfo["records"] as? [FeatureToggle] else { + return false + } + + return toggles.count == 0 + } + cloudKitDatabase.recordFetched["isActive123"] = 1 + cloudKitDatabase.recordFetched["toggleName1234"] = "Toggle1" + + subject.fetchAll() + wait(for: [expectation], timeout: 0.1) + XCTAssertEqual(repository.toggles.count, 0) + } + func testSaveSubscription() { XCTAssertNil(cloudKitDatabase.subscriptionsToSave) XCTAssertFalse(defaults.bool(forKey: subject.subscriptionID)) @@ -160,7 +182,13 @@ class FeatureToggleSubscriptorTests: XCTestCase { } func testHandleNotificationSendNotification() { - let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil, handler: nil) + let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil) { (notification) -> Bool in + guard let userInfo = notification.userInfo, let toggles = userInfo["records"] as? [FeatureToggle] else { + return false + } + + return toggles.count == 1 + } cloudKitDatabase.recordFetched["isActive"] = 1 cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" @@ -169,6 +197,23 @@ class FeatureToggleSubscriptorTests: XCTestCase { } + func testHandleNotificationNotMappableRecord() { + let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil) { (notification) -> Bool in + guard let userInfo = notification.userInfo, let toggles = userInfo["records"] as? [FeatureToggle] else { + return false + } + + return toggles.count == 0 + } + + cloudKitDatabase.recordFetched["isActive123"] = 1 + cloudKitDatabase.recordFetched["toggleName1234"] = "Toggle1" + + subject.handleNotification() + wait(for: [expectation], timeout: 0.1) + XCTAssertEqual(repository.toggles.count, 0) + } + static var allTests = [ ("testFetchAll", testFetchAll), ("testFetchAllError", testFetchAllError), From c576ab0847ead301df9dc7ead203c81c39553283 Mon Sep 17 00:00:00 2001 From: Jonas Reichert Date: Mon, 6 Jan 2020 08:21:37 +0100 Subject: [PATCH 20/23] refactoring --- .../CloudKitFeatureToggles/FeatureToggleSubscriptor.swift | 2 +- .../CloudKitFeatureToggles/Notification+Extensions.swift | 4 ++++ .../FeatureToggleSubscriptorTests.swift | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift index 632900a..c82fbc2 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -62,6 +62,6 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { } private func sendNotification(with toggles: [FeatureToggle]) { - notificationCenter.post(name: Notification.Name.onRecordsUpdated, object: nil, userInfo: ["records" : toggles]) + notificationCenter.post(name: Notification.Name.onRecordsUpdated, object: nil, userInfo: [Notification.featureTogglesUserInfoKey : toggles]) } } diff --git a/Sources/CloudKitFeatureToggles/Notification+Extensions.swift b/Sources/CloudKitFeatureToggles/Notification+Extensions.swift index d6ef3e8..853200c 100644 --- a/Sources/CloudKitFeatureToggles/Notification+Extensions.swift +++ b/Sources/CloudKitFeatureToggles/Notification+Extensions.swift @@ -10,3 +10,7 @@ import Foundation extension Notification.Name { public static let onRecordsUpdated = Notification.Name("ckFeatureTogglesRecordsUpdatedNotification") } + +extension Notification { + public static let featureTogglesUserInfoKey = "featureToggles" +} diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift index 77e5af7..e00f77b 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift @@ -83,7 +83,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { func testFetchAllNotification() { let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil) { (notification) -> Bool in - guard let userInfo = notification.userInfo, let toggles = userInfo["records"] as? [FeatureToggle] else { + guard let userInfo = notification.userInfo, let toggles = userInfo["featureToggles"] as? [FeatureToggle] else { return false } @@ -98,7 +98,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { func testFetchAllNotMappableRecord() { let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil) { (notification) -> Bool in - guard let userInfo = notification.userInfo, let toggles = userInfo["records"] as? [FeatureToggle] else { + guard let userInfo = notification.userInfo, let toggles = userInfo["featureToggles"] as? [FeatureToggle] else { return false } @@ -183,7 +183,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { func testHandleNotificationSendNotification() { let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil) { (notification) -> Bool in - guard let userInfo = notification.userInfo, let toggles = userInfo["records"] as? [FeatureToggle] else { + guard let userInfo = notification.userInfo, let toggles = userInfo["featureToggles"] as? [FeatureToggle] else { return false } @@ -199,7 +199,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { func testHandleNotificationNotMappableRecord() { let expectation = self.expectation(forNotification: NSNotification.Name.onRecordsUpdated, object: nil) { (notification) -> Bool in - guard let userInfo = notification.userInfo, let toggles = userInfo["records"] as? [FeatureToggle] else { + guard let userInfo = notification.userInfo, let toggles = userInfo["featureToggles"] as? [FeatureToggle] else { return false } From c20056d1a36fde0567ac607872aea2264d1b62cb Mon Sep 17 00:00:00 2001 From: Jonas Reichert <2844335+JonnyBeeGod@users.noreply.github.com> Date: Mon, 6 Jan 2020 10:35:50 +0100 Subject: [PATCH 21/23] Update README.md --- README.md | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4837d84..d465c1d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,19 @@ dependencies: [ And don't forget to add the dependency to your target(s). ## How to use? + +### CloudKit Preparations +1. If your application does not support CloudKit yet start with adding the `CloudKit` and `remote background notification` entitlements to your application +2. Add a new custom record type 'FeatureStatus' with two fields: + +| Field | Type | +| --- | --- | +| `featureName` | `String` | +| `isActive` | `Int64` | + +For each feature toggle you want to support in your application later add a new record in your CloudKit *public database*. + +### In your project 1. In your AppDelegate, initialize a `FeatureToggleApplicationService` and hook its two `UIApplicationDelegate` methods into the AppDelegate lifecycle like so: ``` @@ -38,5 +51,27 @@ func application(_ application: UIApplication, didReceiveRemoteNotification user } ``` -2. Anywhere in your code you can create an instance of `FeatureToggleUserDefaultsRepository` and call `retrieve` to fetch the latest status for your feature toggle. +2. Anywhere in your code you can create an instance of `FeatureToggleUserDefaultsRepository` and call `retrieve` to fetch the current status of a feature toggle. + +... Note that `retrieve` returns the locally saved status of your toggle, this command does not trigger a fetch from CloudKit. Feature Toggles are fetched from CloudKit once at app start from within the `FeatureToggleApplicationService` `UIApplicationDelegate` hook. Additionally you can subscribe to updates whenever there was a change to the feature toggles in CloudKit as shown in the next section. + +### Notifications + +You can subscribe to updates from your feature toggles in CloudKit by subscribing to the `onRecordsUpdated` Notification like so: + +``` +NotificationCenter.default.addObserver(self, selector: #selector(updateToggleStatusFromNotification), name: NSNotification.Name.onRecordsUpdated, object: nil) +``` + +``` +@objc +private func updateToggleStatusFromNotification(notification NSNotification) { + guard let updatedToggles = notification.userInfo[Notification.featureToggleUserInfoKey] as? [FeatureToggle] else { + return + } + + // do something with the updated toggle like e.g. disabling UI elements +} +``` +Note that the updated Feature Toggles are attached to the notifications userInfo dictionary. When this notification has been sent the updated values are also already stored in the repository. From d3542e5b43c58c846c1d3c9bf290bc33a12dc89c Mon Sep 17 00:00:00 2001 From: Jonas Reichert <2844335+JonnyBeeGod@users.noreply.github.com> Date: Mon, 6 Jan 2020 10:45:06 +0100 Subject: [PATCH 22/23] Update README.md --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d465c1d..d2828d9 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,29 @@ func application(_ application: UIApplication, didReceiveRemoteNotification user ``` 2. Anywhere in your code you can create an instance of `FeatureToggleUserDefaultsRepository` and call `retrieve` to fetch the current status of a feature toggle. -... Note that `retrieve` returns the locally saved status of your toggle, this command does not trigger a fetch from CloudKit. Feature Toggles are fetched from CloudKit once at app start from within the `FeatureToggleApplicationService` `UIApplicationDelegate` hook. Additionally you can subscribe to updates whenever there was a change to the feature toggles in CloudKit as shown in the next section. +> :warning: Note that `retrieve` returns the locally saved status of your toggle, this command does not trigger a fetch from CloudKit. Feature Toggles are fetched from CloudKit once at app start from within the `FeatureToggleApplicationService` `UIApplicationDelegate` hook. Additionally you can subscribe to updates whenever there was a change to the feature toggles in CloudKit as shown in the next section. +3. You have to call `retrieve` with your implementation of a `FeatureToggleIdentifiable`. What I think works well is creating an enum which implements `FeatureToggleIdentifiable`: + +``` +enum FeatureToggle: String, FeatureToggleIdentifiable { + case feature1 + case feature2 + + var identifier: String { + return self.rawValue + } + + var fallbackValue: Bool { + switch self { + case .feature1: + return false + case .feature2: + return true + } + } + } +``` ### Notifications You can subscribe to updates from your feature toggles in CloudKit by subscribing to the `onRecordsUpdated` Notification like so: From ab5ead2db8f34f97d1db2743e2ae87ed15f7fb4f Mon Sep 17 00:00:00 2001 From: Jonas Reichert <2844335+JonnyBeeGod@users.noreply.github.com> Date: Mon, 6 Jan 2020 10:54:33 +0100 Subject: [PATCH 23/23] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d2828d9..7d2eb6b 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ ## What does it do? +Feature Toggles offer a way to enable or disable certain features that are present in your codebase, switch environments or configurations or toggle between multiple implementations of a protocol - even in your live system at runtime. *CloudKit FeatureToggles* are implemented using `CloudKit` and are therefor associated with no run costs for the developer. Existing Feature Toggles can be changed in the [CloudKit Dashboard](https://icloud.developer.apple.com/dashboard/) and are delivered immediately via silent push notifications to your users. ## How to install? CloudKitFeatureToggles is compatible with Swift Package Manager. To install, simply add this repository URL to your swift packages as package dependency in Xcode.