From 6224caed0625c127a3032299846fd0b2559ea81e Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Thu, 28 Jul 2022 12:27:33 +0200 Subject: [PATCH] V9 (#26) * Killing AsyncResult & Sync APIs * Update project.pbxproj * Add more tests * fileprivate -> private * Fixing code smells reported by sonar * Reformat * Comments * Async/await extensions, additional sync extensions * Reformat * Update API availablitiy * Update ConfigCatClientProtocol.swift * Update README.md [skip ci] * Fix prefereCache & local tests * PR comments * Mutex asserts * Update ConfigService.swift * Update README.md --- .gitignore | 1 + ConfigCat.podspec | 2 +- ConfigCat.xcconfig | 2 +- ConfigCat.xcodeproj/project.pbxproj | 86 ++-- DEPLOY.md | 2 +- README.md | 36 +- Sources/ConfigCat/AsyncResult.swift | 173 ------- Sources/ConfigCat/AutoPollingPolicy.swift | 101 ---- Sources/ConfigCat/Config.swift | 43 +- Sources/ConfigCat/ConfigCache.swift | 2 +- Sources/ConfigCat/ConfigCatClient.swift | 457 ++++++----------- .../ConfigCat/ConfigCatClientProtocol.swift | 125 ++--- Sources/ConfigCat/ConfigCatUser.swift | 24 +- Sources/ConfigCat/ConfigFetcher.swift | 233 ++++----- Sources/ConfigCat/ConfigJsonCache.swift | 32 -- Sources/ConfigCat/ConfigService.swift | 188 +++++++ Sources/ConfigCat/Extensions.swift | 185 +++++++ Sources/ConfigCat/KeyValue.swift | 3 +- Sources/ConfigCat/LazyLoadingPolicy.swift | 101 ---- .../ConfigCat/LocalDictionaryDataSource.swift | 2 +- Sources/ConfigCat/Log.swift | 40 +- Sources/ConfigCat/ManualPollingPolicy.swift | 24 - Sources/ConfigCat/MutableQueue.swift | 19 + Sources/ConfigCat/Mutex.swift | 43 ++ Sources/ConfigCat/OverrideBehaviour.swift | 2 +- Sources/ConfigCat/OverrideDataSource.swift | 6 +- Sources/ConfigCat/PollingMode.swift | 107 ++-- Sources/ConfigCat/PollingModes.swift | 13 +- Sources/ConfigCat/RefreshPolicy.swift | 83 ---- Sources/ConfigCat/Resources/Info.plist | 40 +- Sources/ConfigCat/RolloutEvaluator.swift | 203 ++++---- Sources/ConfigCat/Synced.swift | 55 ++- Sources/ConfigCat/Utils.swift | 49 ++ Tests/ConfigCatTests/AsyncAwaitTests.swift | 59 +++ Tests/ConfigCatTests/AutoPollingTests.swift | 170 ++++--- .../ConfigCatClientIntegrationTests.swift | 29 +- .../ConfigCatTests/ConfigCatClientTests.swift | 458 ++++++++++++------ Tests/ConfigCatTests/ConfigFetcherTests.swift | 89 ++-- .../ConfigCatTests/DataGovernanceTests.swift | 219 +++++---- .../LazyLoadingAsyncTests.swift | 62 --- .../ConfigCatTests/LazyLoadingSyncTests.swift | 89 ---- Tests/ConfigCatTests/LazyLoadingTests.swift | 140 ++++++ Tests/ConfigCatTests/LocalTests.swift | 56 ++- Tests/ConfigCatTests/ManualPollingTests.swift | 149 ++++-- Tests/ConfigCatTests/Mock.swift | 106 ++-- Tests/ConfigCatTests/Resources/Info.plist | 36 +- .../RolloutIntegrationTests.swift | 116 ++--- Tests/ConfigCatTests/SyncTests.swift | 51 ++ Tests/ConfigCatTests/VariationIdTests.swift | 188 ++++--- 49 files changed, 2383 insertions(+), 2116 deletions(-) delete mode 100755 Sources/ConfigCat/AsyncResult.swift delete mode 100755 Sources/ConfigCat/AutoPollingPolicy.swift delete mode 100644 Sources/ConfigCat/ConfigJsonCache.swift create mode 100644 Sources/ConfigCat/ConfigService.swift create mode 100644 Sources/ConfigCat/Extensions.swift delete mode 100755 Sources/ConfigCat/LazyLoadingPolicy.swift delete mode 100755 Sources/ConfigCat/ManualPollingPolicy.swift create mode 100644 Sources/ConfigCat/MutableQueue.swift create mode 100644 Sources/ConfigCat/Mutex.swift delete mode 100755 Sources/ConfigCat/RefreshPolicy.swift create mode 100644 Sources/ConfigCat/Utils.swift create mode 100644 Tests/ConfigCatTests/AsyncAwaitTests.swift delete mode 100755 Tests/ConfigCatTests/LazyLoadingAsyncTests.swift delete mode 100755 Tests/ConfigCatTests/LazyLoadingSyncTests.swift create mode 100755 Tests/ConfigCatTests/LazyLoadingTests.swift create mode 100644 Tests/ConfigCatTests/SyncTests.swift diff --git a/.gitignore b/.gitignore index 5618727..6e6b1f2 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,4 @@ fastlane/test_output .DS_Store samples/ios/Pods samples/osx/Pods +.idea diff --git a/ConfigCat.podspec b/ConfigCat.podspec index 2a71d01..4f9318a 100755 --- a/ConfigCat.podspec +++ b/ConfigCat.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = "ConfigCat" - spec.version = "8.0.1" + spec.version = "9.0.0" spec.summary = "ConfigCat Swift SDK" spec.swift_version = "4.2" diff --git a/ConfigCat.xcconfig b/ConfigCat.xcconfig index ca8eafd..69ed16b 100644 --- a/ConfigCat.xcconfig +++ b/ConfigCat.xcconfig @@ -47,4 +47,4 @@ SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator watchos watchsimulator app SWIFT_VERSION = 4.2 // ConfigCat SDK version -MARKETING_VERSION = 8.0.1 +MARKETING_VERSION = 9.0.0 diff --git a/ConfigCat.xcodeproj/project.pbxproj b/ConfigCat.xcodeproj/project.pbxproj index 540bb93..1a8e8f1 100755 --- a/ConfigCat.xcodeproj/project.pbxproj +++ b/ConfigCat.xcodeproj/project.pbxproj @@ -14,16 +14,11 @@ B4BD2AB4258CA6FF007371E2 /* ConfigFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880A9F207BF1B100087A6B /* ConfigFetcher.swift */; }; B4BD2AB5258CA6FF007371E2 /* Version+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A65478323A511E300EA53B8 /* Version+Comparable.swift */; }; B4BD2AB6258CA6FF007371E2 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E1180A257532D700DA245A /* Log.swift */; }; - B4BD2AB7258CA6FF007371E2 /* LazyLoadingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880A9C207BF1B100087A6B /* LazyLoadingPolicy.swift */; }; B4BD2AB8258CA6FF007371E2 /* KeyValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45414AE24AF2BF2004E66E0 /* KeyValue.swift */; }; B4BD2AB9258CA6FF007371E2 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AEDBE8223876064008803E7 /* Config.swift */; }; B4BD2ABA258CA6FF007371E2 /* RolloutEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15F9AFB216922F000F490CD /* RolloutEvaluator.swift */; }; - B4BD2ABB258CA6FF007371E2 /* AutoPollingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880AA0207BF1B100087A6B /* AutoPollingPolicy.swift */; }; - B4BD2ABC258CA6FF007371E2 /* AsyncResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880A9E207BF1B100087A6B /* AsyncResult.swift */; }; - B4BD2ABD258CA6FF007371E2 /* RefreshPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880A9D207BF1B100087A6B /* RefreshPolicy.swift */; }; B4BD2ABE258CA6FF007371E2 /* Synced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880AA2207BF1B100087A6B /* Synced.swift */; }; B4BD2ABF258CA6FF007371E2 /* Version+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A65478523A511E300EA53B8 /* Version+Codable.swift */; }; - B4BD2AC0258CA6FF007371E2 /* ManualPollingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880AC3207BFA4A00087A6B /* ManualPollingPolicy.swift */; }; B4BD2AC1258CA6FF007371E2 /* ConfigCatUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15F9AF62169176A00F490CD /* ConfigCatUser.swift */; }; B4BD2AC2258CA6FF007371E2 /* Version+Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A65478223A511E300EA53B8 /* Version+Range.swift */; }; B4BD2AC3258CA6FF007371E2 /* ConfigCatClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880AC8207BFFA400087A6B /* ConfigCatClientProtocol.swift */; }; @@ -31,23 +26,18 @@ B4BD2AC6258CA6FF007371E2 /* Version+Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A65478423A511E300EA53B8 /* Version+Foundation.swift */; }; B4BD2AC9258CA6FF007371E2 /* ConfigCat.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F880A2C207BE8F000087A6B /* ConfigCat.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4BD2AE4258CA7DF007371E2 /* Version+Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A65478223A511E300EA53B8 /* Version+Range.swift */; }; - B4BD2AE5258CA7DF007371E2 /* ManualPollingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880AC3207BFA4A00087A6B /* ManualPollingPolicy.swift */; }; B4BD2AE6258CA7DF007371E2 /* ConfigFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880A9F207BF1B100087A6B /* ConfigFetcher.swift */; }; B4BD2AE7258CA7DF007371E2 /* RolloutIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15F9B16216973B000F490CD /* RolloutIntegrationTests.swift */; }; B4BD2AE8258CA7DF007371E2 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AEDBE8223876064008803E7 /* Config.swift */; }; B4BD2AE9258CA7DF007371E2 /* Version+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A65478523A511E300EA53B8 /* Version+Codable.swift */; }; - B4BD2AEA258CA7DF007371E2 /* RefreshPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880A9D207BF1B100087A6B /* RefreshPolicy.swift */; }; B4BD2AEB258CA7DF007371E2 /* Version+Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A65478423A511E300EA53B8 /* Version+Foundation.swift */; }; B4BD2AEC258CA7DF007371E2 /* ConfigFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F4D40C4207EC17000BBAEC6 /* ConfigFetcherTests.swift */; }; B4BD2AED258CA7DF007371E2 /* DataGovernanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10F787D2528950D0021F468 /* DataGovernanceTests.swift */; }; B4BD2AEE258CA7DF007371E2 /* PollingMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F1F2C6023E103C600AFA7D2 /* PollingMode.swift */; }; B4BD2AEF258CA7DF007371E2 /* ConfigCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880AA1207BF1B100087A6B /* ConfigCache.swift */; }; - B4BD2AF0258CA7DF007371E2 /* AutoPollingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880AA0207BF1B100087A6B /* AutoPollingPolicy.swift */; }; B4BD2AF1258CA7DF007371E2 /* Version+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A65478323A511E300EA53B8 /* Version+Comparable.swift */; }; - B4BD2AF2258CA7DF007371E2 /* LazyLoadingSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F36F5382083D5C400949B8F /* LazyLoadingSyncTests.swift */; }; B4BD2AF3258CA7DF007371E2 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3B096423909055002A3A62 /* Version.swift */; }; B4BD2AF4258CA7DF007371E2 /* KeyValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45414AE24AF2BF2004E66E0 /* KeyValue.swift */; }; - B4BD2AF5258CA7DF007371E2 /* AsyncResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880A9E207BF1B100087A6B /* AsyncResult.swift */; }; B4BD2AF6258CA7DF007371E2 /* RolloutEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15F9AFB216922F000F490CD /* RolloutEvaluator.swift */; }; B4BD2AF7258CA7DF007371E2 /* PollingModes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F1F2C6523E10BF300AFA7D2 /* PollingModes.swift */; }; B4BD2AF8258CA7DF007371E2 /* ConfigCatClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8EDF9F2084194700906339 /* ConfigCatClientTests.swift */; }; @@ -55,10 +45,8 @@ B4BD2AFA258CA7DF007371E2 /* ConfigCatClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880AC8207BFFA400087A6B /* ConfigCatClientProtocol.swift */; }; B4BD2AFB258CA7DF007371E2 /* Synced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880AA2207BF1B100087A6B /* Synced.swift */; }; B4BD2AFC258CA7DF007371E2 /* AutoPollingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8EDF9B208415FC00906339 /* AutoPollingTests.swift */; }; - B4BD2AFD258CA7DF007371E2 /* LazyLoadingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880A9C207BF1B100087A6B /* LazyLoadingPolicy.swift */; }; B4BD2AFF258CA7DF007371E2 /* VariationIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41F8AB324AD38F70004CF03 /* VariationIdTests.swift */; }; B4BD2B00258CA7DF007371E2 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F029E89207EB3BD0019942F /* Mock.swift */; }; - B4BD2B01258CA7DF007371E2 /* LazyLoadingAsyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F36F53C2083DA3600949B8F /* LazyLoadingAsyncTests.swift */; }; B4BD2B02258CA7DF007371E2 /* ManualPollingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8EDF9720840FE900906339 /* ManualPollingTests.swift */; }; B4BD2B03258CA7DF007371E2 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E1180A257532D700DA245A /* Log.swift */; }; B4BD2B04258CA7DF007371E2 /* ConfigCatUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15F9AF62169176A00F490CD /* ConfigCatUser.swift */; }; @@ -76,8 +64,19 @@ C4FA1B3E278D919A00BFA8C3 /* OverrideDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FA1B3C278D919900BFA8C3 /* OverrideDataSource.swift */; }; C4FA1B40278D953300BFA8C3 /* OverrideBehaviour.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FA1B3F278D953300BFA8C3 /* OverrideBehaviour.swift */; }; C4FA1B41278D953300BFA8C3 /* OverrideBehaviour.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FA1B3F278D953300BFA8C3 /* OverrideBehaviour.swift */; }; - C4FA1B6127959C1900BFA8C3 /* ConfigJsonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FA1B6027959C1800BFA8C3 /* ConfigJsonCache.swift */; }; - C4FA1B6227959C1900BFA8C3 /* ConfigJsonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FA1B6027959C1800BFA8C3 /* ConfigJsonCache.swift */; }; + F11F76BC288AD6CA0097939F /* AsyncAwaitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BB288AD6CA0097939F /* AsyncAwaitTests.swift */; }; + F11F76BE288AE7540097939F /* SyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BD288AE7540097939F /* SyncTests.swift */; }; + F11F76C0288AE7650097939F /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BF288AE7640097939F /* Extensions.swift */; }; + F11F76C1288AE7970097939F /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BF288AE7640097939F /* Extensions.swift */; }; + F17DEE23288876AE009C3E48 /* LazyLoadingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE22288876AE009C3E48 /* LazyLoadingTests.swift */; }; + F17DEE28288876F7009C3E48 /* MutableQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE24288876F6009C3E48 /* MutableQueue.swift */; }; + F17DEE29288876F7009C3E48 /* MutableQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE24288876F6009C3E48 /* MutableQueue.swift */; }; + F17DEE2A288876F7009C3E48 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE25288876F6009C3E48 /* Mutex.swift */; }; + F17DEE2B288876F7009C3E48 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE25288876F6009C3E48 /* Mutex.swift */; }; + F17DEE2C288876F7009C3E48 /* ConfigService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE26288876F6009C3E48 /* ConfigService.swift */; }; + F17DEE2D288876F7009C3E48 /* ConfigService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE26288876F6009C3E48 /* ConfigService.swift */; }; + F17DEE2E288876F7009C3E48 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE27288876F6009C3E48 /* Utils.swift */; }; + F17DEE2F288876F7009C3E48 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE27288876F6009C3E48 /* Utils.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -104,19 +103,12 @@ 3F029E89207EB3BD0019942F /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = ""; }; 3F1F2C6023E103C600AFA7D2 /* PollingMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollingMode.swift; sourceTree = ""; }; 3F1F2C6523E10BF300AFA7D2 /* PollingModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollingModes.swift; sourceTree = ""; }; - 3F36F5382083D5C400949B8F /* LazyLoadingSyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyLoadingSyncTests.swift; sourceTree = ""; }; - 3F36F53C2083DA3600949B8F /* LazyLoadingAsyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyLoadingAsyncTests.swift; sourceTree = ""; }; 3F4D40C4207EC17000BBAEC6 /* ConfigFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigFetcherTests.swift; sourceTree = ""; }; 3F880A2C207BE8F000087A6B /* ConfigCat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConfigCat.h; sourceTree = ""; }; 3F880A39207BE8F000087A6B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Resources/Info.plist; sourceTree = ""; }; - 3F880A9C207BF1B100087A6B /* LazyLoadingPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LazyLoadingPolicy.swift; sourceTree = ""; }; - 3F880A9D207BF1B100087A6B /* RefreshPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshPolicy.swift; sourceTree = ""; }; - 3F880A9E207BF1B100087A6B /* AsyncResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncResult.swift; sourceTree = ""; }; 3F880A9F207BF1B100087A6B /* ConfigFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigFetcher.swift; sourceTree = ""; }; - 3F880AA0207BF1B100087A6B /* AutoPollingPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoPollingPolicy.swift; sourceTree = ""; }; 3F880AA1207BF1B100087A6B /* ConfigCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigCache.swift; sourceTree = ""; }; 3F880AA2207BF1B100087A6B /* Synced.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Synced.swift; sourceTree = ""; }; - 3F880AC3207BFA4A00087A6B /* ManualPollingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualPollingPolicy.swift; sourceTree = ""; }; 3F880AC8207BFFA400087A6B /* ConfigCatClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigCatClientProtocol.swift; sourceTree = ""; }; 3F880ACD207C072400087A6B /* ConfigCatClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigCatClient.swift; sourceTree = ""; }; 3F8EDF9720840FE900906339 /* ManualPollingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualPollingTests.swift; sourceTree = ""; }; @@ -134,12 +126,19 @@ C4D34D3A249B6F2900908D76 /* testmatrix_variationId.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_variationId.csv; path = Resources/testmatrix_variationId.csv; sourceTree = ""; }; C4FA1B3C278D919900BFA8C3 /* OverrideDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverrideDataSource.swift; sourceTree = ""; }; C4FA1B3F278D953300BFA8C3 /* OverrideBehaviour.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverrideBehaviour.swift; sourceTree = ""; }; - C4FA1B6027959C1800BFA8C3 /* ConfigJsonCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigJsonCache.swift; sourceTree = ""; }; F10F787D2528950D0021F468 /* DataGovernanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGovernanceTests.swift; sourceTree = ""; }; + F11F76BB288AD6CA0097939F /* AsyncAwaitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncAwaitTests.swift; sourceTree = ""; }; + F11F76BD288AE7540097939F /* SyncTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncTests.swift; sourceTree = ""; }; + F11F76BF288AE7640097939F /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; F15F9AF62169176A00F490CD /* ConfigCatUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigCatUser.swift; sourceTree = ""; }; F15F9AFB216922F000F490CD /* RolloutEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RolloutEvaluator.swift; sourceTree = ""; }; F15F9B122169738100F490CD /* testmatrix.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix.csv; path = Resources/testmatrix.csv; sourceTree = ""; }; F15F9B16216973B000F490CD /* RolloutIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RolloutIntegrationTests.swift; sourceTree = ""; }; + F17DEE22288876AE009C3E48 /* LazyLoadingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LazyLoadingTests.swift; sourceTree = ""; }; + F17DEE24288876F6009C3E48 /* MutableQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableQueue.swift; sourceTree = ""; }; + F17DEE25288876F6009C3E48 /* Mutex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; + F17DEE26288876F6009C3E48 /* ConfigService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigService.swift; sourceTree = ""; }; + F17DEE27288876F6009C3E48 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; F1E1180A257532D700DA245A /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -206,20 +205,19 @@ 3F880A42207BE91900087A6B /* Sources */ = { isa = PBXGroup; children = ( + F11F76BF288AE7640097939F /* Extensions.swift */, + F17DEE26288876F6009C3E48 /* ConfigService.swift */, + F17DEE24288876F6009C3E48 /* MutableQueue.swift */, + F17DEE25288876F6009C3E48 /* Mutex.swift */, + F17DEE27288876F6009C3E48 /* Utils.swift */, C40CF51327B557EE00D9F88A /* LocalDictionaryDataSource.swift */, - C4FA1B6027959C1800BFA8C3 /* ConfigJsonCache.swift */, C4FA1B3F278D953300BFA8C3 /* OverrideBehaviour.swift */, C4FA1B3C278D919900BFA8C3 /* OverrideDataSource.swift */, C45414AE24AF2BF2004E66E0 /* KeyValue.swift */, 1AEDBE8223876064008803E7 /* Config.swift */, - 3F880A9E207BF1B100087A6B /* AsyncResult.swift */, - 3F880AA0207BF1B100087A6B /* AutoPollingPolicy.swift */, 3F880AA1207BF1B100087A6B /* ConfigCache.swift */, 3F880A9F207BF1B100087A6B /* ConfigFetcher.swift */, - 3F880A9C207BF1B100087A6B /* LazyLoadingPolicy.swift */, - 3F880A9D207BF1B100087A6B /* RefreshPolicy.swift */, 3F880AA2207BF1B100087A6B /* Synced.swift */, - 3F880AC3207BFA4A00087A6B /* ManualPollingPolicy.swift */, 3F880AC8207BFFA400087A6B /* ConfigCatClientProtocol.swift */, 3F880ACD207C072400087A6B /* ConfigCatClient.swift */, F15F9AF62169176A00F490CD /* ConfigCatUser.swift */, @@ -236,6 +234,9 @@ 3F880A43207BE92000087A6B /* Tests */ = { isa = PBXGroup; children = ( + F11F76BD288AE7540097939F /* SyncTests.swift */, + F11F76BB288AD6CA0097939F /* AsyncAwaitTests.swift */, + F17DEE22288876AE009C3E48 /* LazyLoadingTests.swift */, C40CF51127B5533800D9F88A /* LocalTests.swift */, C480E82B2768144400916320 /* ConfigCatClientIntegrationTests.swift */, C41F8AB324AD38F70004CF03 /* VariationIdTests.swift */, @@ -248,8 +249,6 @@ 3F880A39207BE8F000087A6B /* Info.plist */, 3F029E89207EB3BD0019942F /* Mock.swift */, 3F4D40C4207EC17000BBAEC6 /* ConfigFetcherTests.swift */, - 3F36F5382083D5C400949B8F /* LazyLoadingSyncTests.swift */, - 3F36F53C2083DA3600949B8F /* LazyLoadingAsyncTests.swift */, 3F8EDF9720840FE900906339 /* ManualPollingTests.swift */, 3F8EDF9B208415FC00906339 /* AutoPollingTests.swift */, 3F8EDF9F2084194700906339 /* ConfigCatClientTests.swift */, @@ -376,30 +375,29 @@ files = ( B4BD2AB0258CA6FF007371E2 /* ConfigCatClient.swift in Sources */, B4BD2AB1258CA6FF007371E2 /* Version.swift in Sources */, - C4FA1B6127959C1900BFA8C3 /* ConfigJsonCache.swift in Sources */, B4BD2AB2258CA6FF007371E2 /* PollingModes.swift in Sources */, + F17DEE2C288876F7009C3E48 /* ConfigService.swift in Sources */, B4BD2AB3258CA6FF007371E2 /* PollingMode.swift in Sources */, B4BD2AB4258CA6FF007371E2 /* ConfigFetcher.swift in Sources */, B4BD2AB5258CA6FF007371E2 /* Version+Comparable.swift in Sources */, + F17DEE28288876F7009C3E48 /* MutableQueue.swift in Sources */, C4FA1B3D278D919A00BFA8C3 /* OverrideDataSource.swift in Sources */, B4BD2AB6258CA6FF007371E2 /* Log.swift in Sources */, - B4BD2AB7258CA6FF007371E2 /* LazyLoadingPolicy.swift in Sources */, B4BD2AB8258CA6FF007371E2 /* KeyValue.swift in Sources */, B4BD2AB9258CA6FF007371E2 /* Config.swift in Sources */, B4BD2ABA258CA6FF007371E2 /* RolloutEvaluator.swift in Sources */, - B4BD2ABB258CA6FF007371E2 /* AutoPollingPolicy.swift in Sources */, - B4BD2ABC258CA6FF007371E2 /* AsyncResult.swift in Sources */, - B4BD2ABD258CA6FF007371E2 /* RefreshPolicy.swift in Sources */, B4BD2ABE258CA6FF007371E2 /* Synced.swift in Sources */, C40CF51427B557EE00D9F88A /* LocalDictionaryDataSource.swift in Sources */, B4BD2ABF258CA6FF007371E2 /* Version+Codable.swift in Sources */, C4FA1B40278D953300BFA8C3 /* OverrideBehaviour.swift in Sources */, - B4BD2AC0258CA6FF007371E2 /* ManualPollingPolicy.swift in Sources */, B4BD2AC1258CA6FF007371E2 /* ConfigCatUser.swift in Sources */, + F17DEE2A288876F7009C3E48 /* Mutex.swift in Sources */, B4BD2AC2258CA6FF007371E2 /* Version+Range.swift in Sources */, + F11F76C1288AE7970097939F /* Extensions.swift in Sources */, B4BD2AC3258CA6FF007371E2 /* ConfigCatClientProtocol.swift in Sources */, B4BD2AC5258CA6FF007371E2 /* ConfigCache.swift in Sources */, B4BD2AC6258CA6FF007371E2 /* Version+Foundation.swift in Sources */, + F17DEE2E288876F7009C3E48 /* Utils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -408,27 +406,27 @@ buildActionMask = 2147483647; files = ( B4BD2AE4258CA7DF007371E2 /* Version+Range.swift in Sources */, - B4BD2AE5258CA7DF007371E2 /* ManualPollingPolicy.swift in Sources */, C40CF51527B557EE00D9F88A /* LocalDictionaryDataSource.swift in Sources */, B4BD2AE6258CA7DF007371E2 /* ConfigFetcher.swift in Sources */, B4BD2AE7258CA7DF007371E2 /* RolloutIntegrationTests.swift in Sources */, - C4FA1B6227959C1900BFA8C3 /* ConfigJsonCache.swift in Sources */, B4BD2AE8258CA7DF007371E2 /* Config.swift in Sources */, B4BD2AE9258CA7DF007371E2 /* Version+Codable.swift in Sources */, - B4BD2AEA258CA7DF007371E2 /* RefreshPolicy.swift in Sources */, B4BD2AEB258CA7DF007371E2 /* Version+Foundation.swift in Sources */, + F11F76C0288AE7650097939F /* Extensions.swift in Sources */, B4BD2AEC258CA7DF007371E2 /* ConfigFetcherTests.swift in Sources */, B4BD2AED258CA7DF007371E2 /* DataGovernanceTests.swift in Sources */, B4BD2AEE258CA7DF007371E2 /* PollingMode.swift in Sources */, + F17DEE2F288876F7009C3E48 /* Utils.swift in Sources */, + F11F76BC288AD6CA0097939F /* AsyncAwaitTests.swift in Sources */, C4FA1B41278D953300BFA8C3 /* OverrideBehaviour.swift in Sources */, + F17DEE23288876AE009C3E48 /* LazyLoadingTests.swift in Sources */, B4BD2AEF258CA7DF007371E2 /* ConfigCache.swift in Sources */, - B4BD2AF0258CA7DF007371E2 /* AutoPollingPolicy.swift in Sources */, B4BD2AF1258CA7DF007371E2 /* Version+Comparable.swift in Sources */, - B4BD2AF2258CA7DF007371E2 /* LazyLoadingSyncTests.swift in Sources */, B4BD2AF3258CA7DF007371E2 /* Version.swift in Sources */, B4BD2AF4258CA7DF007371E2 /* KeyValue.swift in Sources */, - B4BD2AF5258CA7DF007371E2 /* AsyncResult.swift in Sources */, B4BD2AF6258CA7DF007371E2 /* RolloutEvaluator.swift in Sources */, + F17DEE2D288876F7009C3E48 /* ConfigService.swift in Sources */, + F17DEE2B288876F7009C3E48 /* Mutex.swift in Sources */, B4BD2AF7258CA7DF007371E2 /* PollingModes.swift in Sources */, B4BD2AF8258CA7DF007371E2 /* ConfigCatClientTests.swift in Sources */, B4BD2AF9258CA7DF007371E2 /* ConfigCatClient.swift in Sources */, @@ -436,12 +434,12 @@ B4BD2AFB258CA7DF007371E2 /* Synced.swift in Sources */, C480E82C2768144400916320 /* ConfigCatClientIntegrationTests.swift in Sources */, B4BD2AFC258CA7DF007371E2 /* AutoPollingTests.swift in Sources */, - B4BD2AFD258CA7DF007371E2 /* LazyLoadingPolicy.swift in Sources */, B4BD2AFF258CA7DF007371E2 /* VariationIdTests.swift in Sources */, B4BD2B00258CA7DF007371E2 /* Mock.swift in Sources */, - B4BD2B01258CA7DF007371E2 /* LazyLoadingAsyncTests.swift in Sources */, + F11F76BE288AE7540097939F /* SyncTests.swift in Sources */, C40CF51227B5533800D9F88A /* LocalTests.swift in Sources */, B4BD2B02258CA7DF007371E2 /* ManualPollingTests.swift in Sources */, + F17DEE29288876F7009C3E48 /* MutableQueue.swift in Sources */, B4BD2B03258CA7DF007371E2 /* Log.swift in Sources */, C4FA1B3E278D919A00BFA8C3 /* OverrideDataSource.swift in Sources */, B4BD2B04258CA7DF007371E2 /* ConfigCatUser.swift in Sources */, diff --git a/DEPLOY.md b/DEPLOY.md index 77335de..e4b3008 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -5,7 +5,7 @@ pod lib lint ``` 2. Run tests -3. Increase the version in the ConfigCat.podspec file (`spec.version`), the `ConfigFetcher.swift` file (`private static let version: String`) and may need to update ConfigCat.xcconfig (MARKETING_VERSION) as well. +3. Increase the version in the ConfigCat.podspec file (`spec.version`), the `Utils.swift` file and may need to update ConfigCat.xcconfig (MARKETING_VERSION) as well. 4. Commit & push. ## Publish Use the **same version** for the git tag as in the podspec. diff --git a/README.md b/README.md index ec2cfdc..b515571 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ If you want to use ConfigCat in a [SwiftPM](https://swift.org/package-manager/) ``` swift dependencies: [ - .package(url: "https://github.com/configcat/swift-sdk", from: "8.0.1") + .package(url: "https://github.com/configcat/swift-sdk", from: "9.0.0") ] ``` @@ -68,23 +68,22 @@ let client = ConfigCatClient(sdkKey: "#YOUR-SDK-KEY#") ### 5. Get your setting value ```swift -let isMyAwesomeFeatureEnabled = client.getValue(for: "isMyAwesomeFeatureEnabled", defaultValue: false) -if(isMyAwesomeFeatureEnabled) { +client.getValue(for: "isMyAwesomeFeatureEnabled", defaultValue: false) { isMyAwesomeFeatureEnabled in + if isMyAwesomeFeatureEnabled { + doTheNewThing() + } else { + doTheOldThing() + } +} + +// or with async/await +let isMyAwesomeFeatureEnabled = await client.getValue(for: "isMyAwesomeFeatureEnabled", defaultValue: false) +if isMyAwesomeFeatureEnabled { doTheNewThing() } else { doTheOldThing() } ``` -Or use the async APIs: -```swift -client.getValueAsync(for: "isMyAwesomeFeatureEnabled", defaultValue: false, completion: { isMyAwesomeFeatureEnabled in - if(isMyAwesomeFeatureEnabled) { - doTheNewThing() - } else { - doTheOldThing() - } - }) -``` ## Getting user specific setting values with Targeting Using this feature, you will be able to get different setting values for different users in your application by passing a `User Object` to the `getValue()` function. @@ -93,11 +92,12 @@ Read more about [Targeting here](https://configcat.com/docs/advanced/targeting/) ```swift let user = ConfigCatUser(identifier: "#USER-IDENTIFIER#") -let isMyAwesomeFeatureEnabled = client.getValue(for: "isMyAwesomeFeatureEnabled", user: user, defaultValue: false) -if(isMyAwesomeFeatureEnabled) { - doTheNewThing() -} else { - doTheOldThing() +client.getValue(for: "isMyAwesomeFeatureEnabled", defaultValue: false, user: user) { isMyAwesomeFeatureEnabled in + if isMyAwesomeFeatureEnabled { + doTheNewThing() + } else { + doTheOldThing() + } } ``` diff --git a/Sources/ConfigCat/AsyncResult.swift b/Sources/ConfigCat/AsyncResult.swift deleted file mode 100755 index d053850..0000000 --- a/Sources/ConfigCat/AsyncResult.swift +++ /dev/null @@ -1,173 +0,0 @@ -import Foundation - -enum AsyncError: Error { - case timedOut - case resultNotPresent -} - -enum AsyncState { - case pending - case completed - - func isCompleted() -> Bool { - return self == .completed - } - - func isPending() -> Bool { - return self == .pending - } -} - -class Async { - fileprivate let queue = DispatchQueue(label: "Async queue") - fileprivate let semaphore = DispatchSemaphore(value: 0) - fileprivate var completions = [() -> Void]() - fileprivate var state = Synced(initValue: AsyncState.pending) - - var completed: Bool { - return self.state.get().isCompleted() - } - - func complete() { - self.queue.async { - if(self.state.get().isPending()) { - self.state.set(new: .completed) - for completion in self.completions { - completion() - } - self.completions.removeAll() - self.semaphore.signal() - } - } - } - - func wait(timeout: Int) throws { - _ = self.semaphore.wait(timeout: DispatchTime.now() + DispatchTimeInterval.seconds(timeout)) - if(self.state.get().isPending()) { - throw AsyncError.timedOut - } - } - - func wait() { - self.semaphore.wait() - } - - @discardableResult - func accept(completion: @escaping () -> Void) -> Async { - self.queue.async { - if self.state.get().isCompleted() { - completion() - } else { - self.completions.append(completion) - } - } - - return self - } - - @discardableResult - func apply(completion: @escaping () -> NewValue) -> AsyncResult { - let result = AsyncResult() - self.accept { - let newResult = completion() - result.complete(result: newResult) - } - - return result - } -} - -final class AsyncResult : Async { - fileprivate var result: Value? - - override init() { - super.init() - } - - init(result: Value) { - super.init() - self.result = result - self.state.set(new: .completed) - self.semaphore.signal() - } - - func complete(result: Value) { - self.result = result - super.complete() - } - - func get(timeout: Int) throws -> Value { - try super.wait(timeout: timeout) - guard let result = self.result else { - throw AsyncError.timedOut - } - - return result - } - - func get() throws -> Value { - super.wait() - guard let result = self.result else { - throw AsyncError.resultNotPresent - } - - return result - } - - @discardableResult - func apply(completion: @escaping (Value) -> Void) -> AsyncResult { - self.queue.async { - if self.state.get().isCompleted() { - guard let result = self.result else { - assert(false, "completion handlers executed on an incomplete AsyncResult") - return - } - - completion(result) - } else { - self.completions.append({ - guard let result = self.result else { - assert(false, "completion handlers executed on an incomplete AsyncResult") - return - } - - completion(result) - }) - } - } - - return self - } - - @discardableResult - func apply(completion: @escaping (Value) -> NewValue) -> AsyncResult { - let result = AsyncResult() - self.accept { value in - let newResult = completion(value) - result.complete(result: newResult) - } - - return result - } - - @discardableResult - func accept(completion: @escaping (Value) -> Void) -> Async { - return self.apply(completion: completion) - } - - func compose(completion: @escaping (Value) -> AsyncResult) -> AsyncResult { - let result = AsyncResult() - self.accept { value in - let newResult = completion(value) - newResult.accept { newVal in - result.complete(result: newVal) - } - } - - return result - } - - class func completed(result: Value) -> AsyncResult { - return AsyncResult(result: result) - } -} diff --git a/Sources/ConfigCat/AutoPollingPolicy.swift b/Sources/ConfigCat/AutoPollingPolicy.swift deleted file mode 100755 index 8549ef7..0000000 --- a/Sources/ConfigCat/AutoPollingPolicy.swift +++ /dev/null @@ -1,101 +0,0 @@ -import Foundation - -/// Describes a `RefreshPolicy` which polls the latest configuration over HTTP and updates the local cache repeatedly. -final class AutoPollingPolicy : RefreshPolicy { - fileprivate let autoPollIntervalInSeconds: Double - fileprivate let initialized = Synced(initValue: false) - fileprivate var initResult = Async() - fileprivate let timer = DispatchSource.makeTimerSource() - fileprivate let initTimer = DispatchSource.makeTimerSource() - fileprivate let onConfigChanged: ConfigCatClient.ConfigChangedHandler? - - /** - Initializes a new `AutoPollingPolicy`. - - - Parameter cache: the internal cache instance. - - Parameter fetcher: the internal config fetcher instance. - - Parameter sdkKey: the sdk key. - - Returns: A new `AutoPollingPolicy`. - */ - convenience required init(cache: ConfigCache?, fetcher: ConfigFetcher, logger: Logger, configJsonCache: ConfigJsonCache, sdkKey: String) { - self.init(cache: cache, fetcher: fetcher, logger: logger, configJsonCache: configJsonCache, sdkKey: sdkKey, config: AutoPollingMode()) - } - - /** - Initializes a new `AutoPollingPolicy`. - - - Parameter cache: the internal cache instance. - - Parameter fetcher: the internal config fetcher instance. - - Parameter sdkKey: the sdk key. - - Parameter config: the configuration. - - Returns: A new `AutoPollingPolicy`. - */ - init(cache: ConfigCache?, - fetcher: ConfigFetcher, - logger: Logger, - configJsonCache: ConfigJsonCache, - sdkKey: String, - config: AutoPollingMode) { - self.autoPollIntervalInSeconds = config.autoPollIntervalInSeconds - self.onConfigChanged = config.onConfigChanged - super.init(cache: cache, fetcher: fetcher, logger: logger, configJsonCache: configJsonCache, sdkKey: sdkKey) - - timer.schedule(deadline: DispatchTime.now(), repeating: autoPollIntervalInSeconds) - timer.setEventHandler(handler: { [weak self] in - guard let `self` = self else { - return - } - - if self.fetcher.isFetching() { - self.log.debug(message: "Config fetching is skipped because there is an ongoing fetch request") - return; - } - - self.fetcher.getConfiguration() - .apply(completion: { response in - let cached = self.readConfigCache() - if let config = response.config, response.isFetched() && config.jsonString != cached.jsonString { - self.writeConfigCache(value: config) - self.onConfigChanged?() - } - - if !self.initialized.getAndSet(new: true) { - self.initResult.complete() - } - }) - }) - timer.resume() - - // Waiting for the client initialization. - // After the maxInitWaitTimeInSeconds timeout the client will be initialized and while the config is not ready - // the default value will be returned. - initTimer.schedule(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(config.maxInitWaitTimeInSeconds)) - initTimer.setEventHandler(handler: { [weak self] in - guard let `self` = self else {return} - if !self.initialized.getAndSet(new: true) { - self.initResult.complete() - } - }) - initTimer.resume() - } - - /// Deinitalizes the AutoPollingPolicy instance. - deinit { - self.timer.cancel() - self.initTimer.cancel() - } - - override func getConfiguration() -> AsyncResult { - if self.initResult.completed { - return self.readCacheAsync() - } - - return self.initResult.apply(completion: { - return self.readConfigCache() - }) - } - - private func readCacheAsync() -> AsyncResult { - return AsyncResult.completed(result: self.readConfigCache()) - } -} diff --git a/Sources/ConfigCat/Config.swift b/Sources/ConfigCat/Config.swift index 161c271..b149029 100644 --- a/Sources/ConfigCat/Config.swift +++ b/Sources/ConfigCat/Config.swift @@ -1,3 +1,35 @@ +import Foundation + +struct ConfigEntry: Equatable { + static func ==(lhs: ConfigEntry, rhs: ConfigEntry) -> Bool { + lhs.jsonString == rhs.jsonString && lhs.eTag == rhs.eTag + } + + let jsonString: String + let config: Config + let eTag: String + let fetchTime: Date + + init(jsonString: String = "", config: Config = Config.empty, eTag: String = "", fetchTime: Date = Date.distantPast) { + self.jsonString = jsonString + self.config = config + self.eTag = eTag + self.fetchTime = fetchTime + } + + func withFetchTime(time: Date) -> ConfigEntry { + ConfigEntry(jsonString: jsonString, config: config, eTag: eTag, fetchTime: time) + } + + var isEmpty: Bool { + get { + self == .empty + } + } + + static let empty = ConfigEntry() +} + struct Config { static let value = "v" static let comparator = "t" @@ -12,15 +44,18 @@ struct Config { static let preferencesRedirect = "r" static let entries = "f" - let jsonString: String let preferences: [String: Any] let entries: [String: Any] - init(jsonString: String = "{}", preferences: [String: Any] = [:], entries: [String: Any] = [:]) { - self.jsonString = jsonString + init(preferences: [String: Any] = [:], entries: [String: Any] = [:]) { self.preferences = preferences self.entries = entries } - static let empty = Config(); + var isEmpty: Bool { + get { + entries.isEmpty && preferences.isEmpty + } + } + static let empty = Config() } diff --git a/Sources/ConfigCat/ConfigCache.swift b/Sources/ConfigCat/ConfigCache.swift index 31fb2f4..d30be08 100755 --- a/Sources/ConfigCat/ConfigCache.swift +++ b/Sources/ConfigCat/ConfigCache.swift @@ -11,7 +11,7 @@ import Foundation - Throws: Exception if unable to read the cache. */ func read(for key: String) throws -> String - + /** Child classes has to implement this method, the `ConfigCatClient` uses it to set the actual cached value. diff --git a/Sources/ConfigCat/ConfigCatClient.swift b/Sources/ConfigCat/ConfigCatClient.swift index 15aaaa7..298d0e3 100755 --- a/Sources/ConfigCat/ConfigCatClient.swift +++ b/Sources/ConfigCat/ConfigCatClient.swift @@ -6,7 +6,7 @@ extension ConfigCatClient { } /// Describes the location of your feature flag and setting data within the ConfigCat CDN. -@objc public enum DataGovernance : Int { +@objc public enum DataGovernance: Int { /// Select this if your feature flags are published to all global CDN nodes. case global /// Select this if your feature flags are published to CDN nodes only in the EU. @@ -14,13 +14,13 @@ extension ConfigCatClient { } /// A client for handling configurations provided by ConfigCat. -public final class ConfigCatClient : NSObject, ConfigCatClientProtocol { - fileprivate let log: Logger - fileprivate let evaluator: RolloutEvaluator - fileprivate let refreshPolicy: RefreshPolicy? - fileprivate let sdkKey: String - fileprivate let overrideDataSource: OverrideDataSource? - fileprivate static var sdkKeys: Set = [] +public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { + private let log: Logger + private let evaluator: RolloutEvaluator + private let configService: ConfigService? + private let sdkKey: String + private let overrideDataSource: OverrideDataSource? + private static var sdkKeys: Set = [] /** Initializes a new `ConfigCatClient`. @@ -37,55 +37,53 @@ public final class ConfigCatClient : NSObject, ConfigCatClientProtocol { - Returns: A new `ConfigCatClient`. */ @objc public convenience init(sdkKey: String, - dataGovernance: DataGovernance = DataGovernance.global, - configCache: ConfigCache? = nil, - refreshMode: PollingMode? = nil, - sessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default, - baseUrl: String = "", - flagOverrides: OverrideDataSource? = nil, - logLevel: LogLevel = .warning) { + dataGovernance: DataGovernance = DataGovernance.global, + configCache: ConfigCache? = nil, + refreshMode: PollingMode? = nil, + sessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default, + baseUrl: String = "", + flagOverrides: OverrideDataSource? = nil, + logLevel: LogLevel = .warning) { self.init(sdkKey: sdkKey, refreshMode: refreshMode, session: URLSession(configuration: sessionConfiguration), - configCache: configCache, baseUrl: baseUrl, dataGovernance: dataGovernance, flagOverrides: flagOverrides, logLevel: logLevel) + configCache: configCache, baseUrl: baseUrl, dataGovernance: dataGovernance, flagOverrides: flagOverrides, logLevel: logLevel) } - + init(sdkKey: String, - refreshMode: PollingMode?, - session: URLSession?, - configCache: ConfigCache? = nil, - baseUrl: String = "", - dataGovernance: DataGovernance = DataGovernance.global, - flagOverrides: OverrideDataSource? = nil, - logLevel: LogLevel = .warning) { + refreshMode: PollingMode?, + session: URLSession?, + configCache: ConfigCache? = nil, + baseUrl: String = "", + dataGovernance: DataGovernance = DataGovernance.global, + flagOverrides: OverrideDataSource? = nil, + logLevel: LogLevel = .warning) { if sdkKey.isEmpty { - assert(false, "projectSecret cannot be empty") + assert(false, "sdkKey cannot be empty") } - self.log = Logger(level: logLevel) + log = Logger(level: logLevel) if (!ConfigCatClient.sdkKeys.insert(sdkKey).inserted) { - self.log.warning(message: """ - A ConfigCat Client is already initialized with sdkKey %@. - We strongly recommend you to use the ConfigCat Client as a Singleton object in your application. - """, sdkKey) + log.warning(message: """ + A ConfigCat Client is already initialized with sdkKey %@. + We strongly recommend you to use the ConfigCat Client as a Singleton object in your application. + """, sdkKey) } self.sdkKey = sdkKey - self.overrideDataSource = flagOverrides - self.evaluator = RolloutEvaluator(logger: self.log) + overrideDataSource = flagOverrides + evaluator = RolloutEvaluator(logger: log) - if let overrideDataSource = self.overrideDataSource, overrideDataSource.behaviour == .localOnly { - // RefreshPolicy is not needed in localOnly mode - self.refreshPolicy = nil + if let overrideDataSource = overrideDataSource, overrideDataSource.behaviour == .localOnly { + // configService is not needed in localOnly mode + configService = nil } else { - let mode = refreshMode ?? PollingModes.autoPoll(autoPollIntervalInSeconds: 60) - let configJsonCache = ConfigJsonCache(logger: self.log) + let mode = refreshMode ?? PollingModes.autoPoll() let fetcher = ConfigFetcher(session: session ?? URLSession(configuration: URLSessionConfiguration.default), - logger: self.log, - configJsonCache: configJsonCache, - sdkKey: sdkKey, - mode: mode.getPollingIdentifier(), - dataGovernance: dataGovernance, - baseUrl: baseUrl) + logger: log, + sdkKey: sdkKey, + mode: mode.identifier, + dataGovernance: dataGovernance, + baseUrl: baseUrl) - self.refreshPolicy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, cache: configCache, logger: self.log, configJsonCache: configJsonCache, sdkKey: sdkKey)) + configService = ConfigService(log: log, fetcher: fetcher, cache: configCache, pollingMode: mode, sdkKey: sdkKey) } } @@ -93,121 +91,178 @@ public final class ConfigCatClient : NSObject, ConfigCatClientProtocol { ConfigCatClient.sdkKeys.remove(sdkKey) } - func getSettingsAsync() -> AsyncResult<[String: Any]> { - if let overrideDataSource = self.overrideDataSource, overrideDataSource.behaviour == .localOnly { - return AsyncResult<[String: Any]>.completed(result: overrideDataSource.getOverrides()) + // MARK: ConfigCatClientProtocol + + public func getValue(for key: String, defaultValue: Value, user: ConfigCatUser? = nil, completion: @escaping (Value) -> ()) { + if key.isEmpty { + assert(false, "key cannot be empty") + } + getSettings { settings in + completion(self.getValueFromSettings(settings: settings, key: key, defaultValue: defaultValue, user: user)) + } + } + + @objc public func getAllKeys(completion: @escaping ([String]) -> ()) { + getSettings { settings in + completion([String](settings.keys)) + } + } + + @objc public func getVariationId(for key: String, defaultVariationId: String?, user: ConfigCatUser? = nil, completion: @escaping (String?) -> ()) { + if key.isEmpty { + assert(false, "key cannot be empty") + } + getSettings { settings in + completion(self.getVariationIdFromSettings(settings: settings, key: key, defaultVariationId: defaultVariationId, user: user)) + } + } + + @objc public func getAllVariationIds(user: ConfigCatUser? = nil, completion: @escaping ([String]) -> ()) { + getSettings { settings in + completion(self.getAllVariationIdsFromSettings(settings: settings, user: user)) + } + } + + @objc public func getKeyAndValue(for variationId: String, completion: @escaping (KeyValue?) -> ()) { + getSettings { settings in + completion(self.getKeyAndValueFromSettings(settings: settings, variationId: variationId)) } + } - guard let refreshPolicy = self.refreshPolicy else { - return AsyncResult<[String: Any]>.completed(result:[:]) + @objc public func getAllValues(user: ConfigCatUser? = nil, completion: @escaping ([String: Any]) -> ()) { + getSettings { settings in + completion(self.getAllValuesFromSettings(settings: settings, user: user)) } + } - if let overrideDataSource = self.overrideDataSource { + @objc public func refresh(completion: @escaping () -> ()) { + if let configService = configService { + configService.refresh(completion: completion) + } else { + log.warning(message: "The ConfigCat SDK is in local-only mode. Calling .refresh() has no effect.") + completion() + } + } + + func getSettings(completion: @escaping ([String: Any]) -> Void) { + if let overrideDataSource = overrideDataSource, overrideDataSource.behaviour == .localOnly { + completion(overrideDataSource.getOverrides()) + return + } + guard let configService = configService else { + completion([:]) + return + } + if let overrideDataSource = overrideDataSource { if overrideDataSource.behaviour == .localOverRemote { - return refreshPolicy.getSettings() - .apply(completion: { settings in - return settings.merging(overrideDataSource.getOverrides()) { (_, new) in new } + configService.settings { settings in + completion(settings.merging(overrideDataSource.getOverrides()) { (_, new) in + new }) + } + return } if overrideDataSource.behaviour == .remoteOverLocal { - return refreshPolicy.getSettings() - .apply(completion: { settings in - return settings.merging(overrideDataSource.getOverrides()) { (current, _) in current } + configService.settings { settings in + completion(settings.merging(overrideDataSource.getOverrides()) { (current, _) in + current }) + } + return } } - - return refreshPolicy.getSettings() + configService.settings { settings in + completion(settings) + } } - public func getValueFromSettings(settings: [String: Any], key: String, defaultValue: Value, user: ConfigCatUser? = nil) -> Value { + func getValueFromSettings(settings: [String: Any], key: String, defaultValue: Value, user: ConfigCatUser? = nil) -> Value { if Value.self != String.self && - Value.self != String?.self && - Value.self != Int.self && - Value.self != Int?.self && - Value.self != Double.self && - Value.self != Double?.self && - Value.self != Bool.self && - Value.self != Bool?.self && - Value.self != Any.self && - Value.self != Any?.self { - self.log.error(message: "Only String, Integer, Double, Bool or Any types are supported.") + Value.self != String?.self && + Value.self != Int.self && + Value.self != Int?.self && + Value.self != Double.self && + Value.self != Double?.self && + Value.self != Bool.self && + Value.self != Bool?.self && + Value.self != Any.self && + Value.self != Any?.self { + log.error(message: "Only String, Integer, Double, Bool or Any types are supported.") return defaultValue } - if settings.isEmpty { - self.log.error(message: "Config is not present. Returning defaultValue: [%@].", "\(defaultValue)"); + log.error(message: "Config is not present. Returning defaultValue: [%@].", "\(defaultValue)"); return defaultValue } - let (value, _, evaluateLog): (Value?, String?, String?) = self.evaluator.evaluate(json: settings[key], key: key, user: user) + let (value, _, evaluateLog): (Value?, String?, String?) = evaluator.evaluate(json: settings[key], key: key, user: user) if let evaluateLog = evaluateLog { - self.log.info(message: "%@", evaluateLog) + log.info(message: "%@", evaluateLog) } if let value = value { return value } - self.log.error(message: """ - Evaluating the value for the key '%@' failed. - Returning defaultValue: [%@]. - Here are the available keys: %@ - """, key, "\(defaultValue)", [String](settings.keys)) + log.error(message: """ + Evaluating the value for the key '%@' failed. + Returning defaultValue: [%@]. + Here are the available keys: %@ + """, key, "\(defaultValue)", [String](settings.keys)) return defaultValue } - public func getVariationIdFromSettings(settings: [String: Any], key: String, defaultVariationId: String?, user: ConfigCatUser? = nil) -> String? { - let (_, variationId, evaluateLog): (Any?, String?, String?) = self.evaluator.evaluate(json: settings[key], key: key, user: user) + func getVariationIdFromSettings(settings: [String: Any], key: String, defaultVariationId: String?, user: ConfigCatUser? = nil) -> String? { + let (_, variationId, evaluateLog): (Any?, String?, String?) = evaluator.evaluate(json: settings[key], key: key, user: user) if let evaluateLog = evaluateLog { - self.log.info(message: "%@", evaluateLog) + log.info(message: "%@", evaluateLog) } if let variationId = variationId { return variationId } - self.log.error(message: """ - Evaluating the variation id for the key '%@' failed. - Returning defaultVariationId: %@ - Here are the available keys: %@ - """, key, defaultVariationId ?? "nil", [String](settings.keys)) + log.error(message: """ + Evaluating the variation id for the key '%@' failed. + Returning defaultVariationId: %@ + Here are the available keys: %@ + """, key, defaultVariationId ?? "nil", [String](settings.keys)) return defaultVariationId } - public func getAllVariationIdsFromSettings(settings: [String: Any], user: ConfigCatUser? = nil) -> [String] { + func getAllVariationIdsFromSettings(settings: [String: Any], user: ConfigCatUser? = nil) -> [String] { var variationIds = [String]() for key in settings.keys { - let (_, variationId, evaluateLog): (Any?, String?, String?) = self.evaluator.evaluate(json: settings[key], key: key, user: user) + let (_, variationId, evaluateLog): (Any?, String?, String?) = evaluator.evaluate(json: settings[key], key: key, user: user) if let evaluateLog = evaluateLog { - self.log.info(message: "%@", evaluateLog) + log.info(message: "%@", evaluateLog) } if let variationId = variationId { variationIds.append(variationId) } else { - self.log.error(message: "Evaluating the variation id for the key '%@' failed.", key) + log.error(message: "Evaluating the variation id for the key '%@' failed.", key) } } return variationIds } - public func getAllValuesFromSettings(settings: [String: Any], user: ConfigCatUser? = nil) -> [String: Any] { + func getAllValuesFromSettings(settings: [String: Any], user: ConfigCatUser? = nil) -> [String: Any] { var allValues = [String: Any]() for key in settings.keys { - let (value, _, evaluateLog): (Any?, String?, String?) = self.evaluator.evaluate(json: settings[key], key: key, user: user) + let (value, _, evaluateLog): (Any?, String?, String?) = evaluator.evaluate(json: settings[key], key: key, user: user) if let evaluateLog = evaluateLog { - self.log.info(message: "%@", evaluateLog) + log.info(message: "%@", evaluateLog) } if let value = value { allValues[key] = value } else { - self.log.error(message: "Evaluating the value for the key '%@' failed.", key) + log.error(message: "Evaluating the value for the key '%@' failed.", key) } } return allValues } - public func getKeyAndValueFromSettings(settings: [String: Any], variationId: String) -> KeyValue? { + func getKeyAndValueFromSettings(settings: [String: Any], variationId: String) -> KeyValue? { for (key, json) in settings { if let json = json as? [String: Any], let value = json[Config.value] { if variationId == json[Config.variationId] as? String { @@ -216,7 +271,7 @@ public final class ConfigCatClient : NSObject, ConfigCatClientProtocol { let rolloutRules = json[Config.rolloutRules] as? [[String: Any]] ?? [] for rule in rolloutRules { - if variationId == rule[Config.variationId] as? String, let value = rule[Config.value] { + if variationId == rule[Config.variationId] as? String, let value = rule[Config.value] { return KeyValue(key: key, value: value) } } @@ -230,213 +285,7 @@ public final class ConfigCatClient : NSObject, ConfigCatClientProtocol { } } - self.log.error(message: "Could not find the setting for the given variationId: '%@'", variationId); + log.error(message: "Could not find the setting for the given variationId: '%@'", variationId); return nil } - - // MARK: ConfigCatClientProtocol - - public func getValue(for key: String, defaultValue: Value, user: ConfigCatUser?) -> Value { - if key.isEmpty { - assert(false, "key cannot be empty") - } - - do { - let settings = try self.getSettingsAsync().get() - return self.getValueFromSettings(settings: settings, key: key, defaultValue: defaultValue, user: user) - } catch { - self.log.error(message: "An error occurred during reading the configuration. %@", error.localizedDescription) - return defaultValue - } - } - - public func getValue(for key: String, defaultValue: Value) -> Value { - return getValue(for: key, defaultValue: defaultValue, user: nil) - } - - public func getValueAsync(for key: String, defaultValue: Value, user: ConfigCatUser?, completion: @escaping (Value) -> ()) { - if key.isEmpty { - assert(false, "key cannot be empty") - } - - self.getSettingsAsync() - .apply { settings in - let result: Value = self.getValueFromSettings(settings: settings, key: key, defaultValue: defaultValue, user: user) - completion(result) - } - } - - public func getValueAsync(for key: String, defaultValue: Value, completion: @escaping (Value) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, user: nil, completion: completion) - } - - @objc public func getAllKeys() -> [String] { - do { - let settings = try self.getSettingsAsync().get() - return [String](settings.keys) - } catch { - self.log.error(message: "An error occurred during reading the configuration. %@", error.localizedDescription) - return [] - } - } - - @objc public func getAllKeysAsync(completion: @escaping ([String]) -> ()) { - self.getSettingsAsync() - .apply { settings in - completion([String](settings.keys)) - } - } - - @objc public func getVariationId(for key: String, defaultVariationId: String?, user: ConfigCatUser? = nil) -> String? { - if key.isEmpty { - assert(false, "key cannot be empty") - } - - do { - let settings = try self.getSettingsAsync().get() - return self.getVariationIdFromSettings(settings: settings, key: key, defaultVariationId: defaultVariationId, user: user) - } catch { - self.log.error(message: "An error occurred during reading the configuration. %@", error.localizedDescription) - return defaultVariationId - } - } - - @objc public func getVariationIdAsync(for key: String, defaultVariationId: String?, user: ConfigCatUser? = nil, completion: @escaping (String?) -> ()) { - if key.isEmpty { - assert(false, "key cannot be empty") - } - - self.getSettingsAsync() - .apply { settings in - completion(self.getVariationIdFromSettings(settings: settings, key: key, defaultVariationId: defaultVariationId, user: user)) - } - } - - @objc public func getAllVariationIds(user: ConfigCatUser? = nil) -> [String] { - do { - let settings = try self.getSettingsAsync().get() - return self.getAllVariationIdsFromSettings(settings: settings, user: user) - } catch { - self.log.error(message: "An error occurred during reading the configuration. %@", error.localizedDescription) - return [] - } - } - - @objc public func getAllVariationIdsAsync(user: ConfigCatUser? = nil, completion: @escaping ([String]) -> ()) { - self.getSettingsAsync() - .apply { settings in - let result = self.getAllVariationIdsFromSettings(settings: settings, user: user) - completion(result) - } - } - - @objc public func getKeyAndValue(for variationId: String) -> KeyValue? { - do { - let settings = try self.getSettingsAsync().get() - return self.getKeyAndValueFromSettings(settings: settings, variationId: variationId) - } catch { - self.log.error(message: "An error occurred during reading the configuration. %@", error.localizedDescription) - return nil - } - } - - @objc public func getKeyAndValueAsync(for variationId: String, completion: @escaping (KeyValue?) -> ()) { - self.getSettingsAsync() - .apply { settings in - completion(self.getKeyAndValueFromSettings(settings: settings, variationId: variationId)) - } - } - - @objc public func getAllValues(user: ConfigCatUser? = nil) -> [String: Any] { - do { - let settings = try self.getSettingsAsync().get() - return self.getAllValuesFromSettings(settings: settings, user: user) - } catch { - self.log.error(message: "An error occurred during reading the configuration. %@", error.localizedDescription) - return [:] - } - } - - @objc public func getAllValuesAsync(user: ConfigCatUser? = nil, completion: @escaping ([String: Any]) -> ()) { - self.getSettingsAsync() - .apply { settings in - completion(self.getAllValuesFromSettings(settings: settings, user: user)) - } - } - - @objc public func refresh() { - self.refreshPolicy?.refresh().wait() - } - - @objc public func refreshAsync(completion: @escaping () -> ()) { - self.refreshPolicy?.refresh().accept(completion: completion) - } -} - -/// Objectiv-C interface extension. -/// Generic parameters are not available in Objectiv-C (getValue, getValueAsync cannot be marked @objc) -extension ConfigCatClient { - @objc public func getStringValue(for key: String, defaultValue: String) -> String { - return getValue(for: key, defaultValue: defaultValue, user: nil) - } - @objc public func getIntValue(for key: String, defaultValue: Int) -> Int { - return getValue(for: key, defaultValue: defaultValue, user: nil) - } - @objc public func getDoubleValue(for key: String, defaultValue: Double) -> Double { - return getValue(for: key, defaultValue: defaultValue, user: nil) - } - @objc public func getBoolValue(for key: String, defaultValue: Bool) -> Bool { - return getValue(for: key, defaultValue: defaultValue, user: nil) - } - @objc public func getAnyValue(for key: String, defaultValue: Any) -> Any { - return getValue(for: key, defaultValue: defaultValue, user: nil) - } - - @objc public func getStringValue(for key: String, defaultValue: String, user: ConfigCatUser?) -> String { - return getValue(for: key, defaultValue: defaultValue, user: user) - } - @objc public func getIntValue(for key: String, defaultValue: Int, user: ConfigCatUser?) -> Int { - return getValue(for: key, defaultValue: defaultValue, user: user) - } - @objc public func getDoubleValue(for key: String, defaultValue: Double, user: ConfigCatUser?) -> Double { - return getValue(for: key, defaultValue: defaultValue, user: user) - } - @objc public func getBoolValue(for key: String, defaultValue: Bool, user: ConfigCatUser?) -> Bool { - return getValue(for: key, defaultValue: defaultValue, user: user) - } - @objc public func getAnyValue(for key: String, defaultValue: Any, user: ConfigCatUser?) -> Any { - return getValue(for: key, defaultValue: defaultValue, user: user) - } - - @objc public func getStringValueAsync(for key: String, defaultValue: String, completion: @escaping (String) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, completion: completion) - } - @objc public func getIntValueAsync(for key: String, defaultValue: Int, completion: @escaping (Int) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, completion: completion) - } - @objc public func getDoubleValueAsync(for key: String, defaultValue: Double, completion: @escaping (Double) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, completion: completion) - } - @objc public func getBoolValueAsync(for key: String, defaultValue: Bool, completion: @escaping (Bool) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, completion: completion) - } - @objc public func getAnyValueAsync(for key: String, defaultValue: Any, completion: @escaping (Any) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, completion: completion) - } - - @objc public func getStringValueAsync(for key: String, defaultValue: String, user: ConfigCatUser?, completion: @escaping (String) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, user: user, completion: completion) - } - @objc public func getIntValueAsync(for key: String, defaultValue: Int, user: ConfigCatUser?, completion: @escaping (Int) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, user: user, completion: completion) - } - @objc public func getDoubleValueAsync(for key: String, defaultValue: Double, user: ConfigCatUser?, completion: @escaping (Double) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, user: user, completion: completion) - } - @objc public func getBoolValueAsync(for key: String, defaultValue: Bool, user: ConfigCatUser?, completion: @escaping (Bool) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, user: user, completion: completion) - } - @objc public func getAnyValueAsync(for key: String, defaultValue: Any, user: ConfigCatUser?, completion: @escaping (Any) -> ()) { - return getValueAsync(for: key, defaultValue: defaultValue, user: user, completion: completion) - } -} +} \ No newline at end of file diff --git a/Sources/ConfigCat/ConfigCatClientProtocol.swift b/Sources/ConfigCat/ConfigCatClientProtocol.swift index 6fe9ead..daa1ea0 100755 --- a/Sources/ConfigCat/ConfigCatClientProtocol.swift +++ b/Sources/ConfigCat/ConfigCatClientProtocol.swift @@ -2,103 +2,80 @@ import Foundation /// Defines the public protocol of the `ConfigCatClient`. public protocol ConfigCatClientProtocol { - - /** - Gets a value synchronously as `Value` from the configuration identified by the given `key`. - - - Parameter for: the identifier of the configuration value. - - Parameter defaultValue: in case of any failure, this value will be returned. - */ - func getValue(for key: String, defaultValue: Value) -> Value - /** Gets a value asynchronously as `Value` from the configuration identified by the given `key`. - - Parameter for: the identifier of the configuration value. + - Parameter key: the identifier of the configuration value. - Parameter defaultValue: in case of any failure, this value will be returned. + - Parameter user: the user object to identify the caller. - Parameter completion: the function which will be called when the configuration is successfully fetched. */ - func getValueAsync(for key: String, defaultValue: Value, completion: @escaping (Value) -> ()) + func getValue(for key: String, defaultValue: Value, user: ConfigCatUser?, completion: @escaping (Value) -> ()) + + /// Gets all the setting keys asynchronously. + func getAllKeys(completion: @escaping ([String]) -> ()) + + /// Gets the Variation ID (analytics) of a feature flag or setting based on it's key asynchronously. + func getVariationId(for key: String, defaultVariationId: String?, user: ConfigCatUser?, completion: @escaping (String?) -> ()) + + /// Gets the Variation IDs (analytics) of all feature flags or settings asynchronously. + func getAllVariationIds(user: ConfigCatUser?, completion: @escaping ([String]) -> ()) + + /// Gets the key of a setting and it's value identified by the given Variation ID (analytics) + func getKeyAndValue(for variationId: String, completion: @escaping (KeyValue?) -> ()) + + /// Gets the values of all feature flags or settings asynchronously. + func getAllValues(user: ConfigCatUser?, completion: @escaping ([String: Any]) -> ()) /** - Gets a value synchronously as `Value` from the configuration identified by the given `key`. + Initiates a force refresh asynchronously on the cached configuration. - - Parameter for: the identifier of the configuration value. - - Parameter defaultValue: in case of any failure, this value will be returned. - - Parameter user: the user object to identify the caller. + - Parameter completion: the function which will be called when refresh completed successfully. */ - func getValue(for key: String, defaultValue: Value, user: ConfigCatUser?) -> Value - + func refresh(completion: @escaping () -> ()) + + /// Async/await interface + #if compiler(>=5.5) && canImport(_Concurrency) /** Gets a value asynchronously as `Value` from the configuration identified by the given `key`. - - - Parameter for: the identifier of the configuration value. + + - Parameter key: the identifier of the configuration value. - Parameter defaultValue: in case of any failure, this value will be returned. - Parameter user: the user object to identify the caller. - - Parameter completion: the function which will be called when the configuration is successfully fetched. */ - func getValueAsync(for key: String, defaultValue: Value, user: ConfigCatUser?, completion: @escaping (Value) -> ()) - - /// Gets all the setting keys. - func getAllKeys() -> [String] - - /// Gets all the setting keys asynchronously. - func getAllKeysAsync(completion: @escaping ([String]) -> ()) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func getValue(for key: String, defaultValue: Value, user: ConfigCatUser?) async -> Value - /// Gets the Variation ID (analytics) of a feature flag or setting based on it's key. - func getVariationId(for key: String, defaultVariationId: String?, user: ConfigCatUser?) -> String? + /// Gets all the setting keys asynchronously. + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func getAllKeys() async -> [String] /// Gets the Variation ID (analytics) of a feature flag or setting based on it's key asynchronously. - func getVariationIdAsync(for key: String, defaultVariationId: String?, user: ConfigCatUser?, completion: @escaping (String?) -> ()) - - /// Gets the Variation IDs (analytics) of all feature flags or settings. - func getAllVariationIds(user: ConfigCatUser?) -> [String] + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func getVariationId(for key: String, defaultVariationId: String?, user: ConfigCatUser?) async -> String? /// Gets the Variation IDs (analytics) of all feature flags or settings asynchronously. - func getAllVariationIdsAsync(user: ConfigCatUser?, completion: @escaping ([String]) -> ()) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func getAllVariationIds(user: ConfigCatUser?) async -> [String] /// Gets the key of a setting and it's value identified by the given Variation ID (analytics) - func getKeyAndValue(for variationId: String) -> KeyValue? - - /// Gets the key of a setting and it's value identified by the given Variation ID (analytics) - func getKeyAndValueAsync(for variationId: String, completion: @escaping (KeyValue?) -> ()) - - /// Gets the values of all feature flags or settings. - func getAllValues(user: ConfigCatUser?) -> [String: Any] + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func getKeyAndValue(for variationId: String) async -> KeyValue? /// Gets the values of all feature flags or settings asynchronously. - func getAllValuesAsync(user: ConfigCatUser?, completion: @escaping ([String: Any]) -> ()) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func getAllValues(user: ConfigCatUser?) async -> [String: Any] - /// Initiates a force refresh synchronously on the cached configuration. - func refresh() - - /** - Initiates a force refresh asynchronously on the cached configuration. - - - Parameter completion: the function which will be called when refresh completed successfully. - */ - func refreshAsync(completion: @escaping () -> ()) - - /// Objectiv-C interface - /// Generic parameters are not available in Objectiv-C (getValue, getValueAsync cannot be marked @objc) - func getStringValue(for key: String, defaultValue: String) -> String - func getIntValue(for key: String, defaultValue: Int) -> Int - func getDoubleValue(for key: String, defaultValue: Double) -> Double - func getBoolValue(for key: String, defaultValue: Bool) -> Bool - func getAnyValue(for key: String, defaultValue: Any) -> Any - func getStringValue(for key: String, defaultValue: String, user: ConfigCatUser?) -> String - func getIntValue(for key: String, defaultValue: Int, user: ConfigCatUser?) -> Int - func getDoubleValue(for key: String, defaultValue: Double, user: ConfigCatUser?) -> Double - func getBoolValue(for key: String, defaultValue: Bool, user: ConfigCatUser?) -> Bool - func getAnyValue(for key: String, defaultValue: Any, user: ConfigCatUser?) -> Any - func getStringValueAsync(for key: String, defaultValue: String, completion: @escaping (String) -> ()) - func getIntValueAsync(for key: String, defaultValue: Int, completion: @escaping (Int) -> ()) - func getDoubleValueAsync(for key: String, defaultValue: Double, completion: @escaping (Double) -> ()) - func getBoolValueAsync(for key: String, defaultValue: Bool, completion: @escaping (Bool) -> ()) - func getAnyValueAsync(for key: String, defaultValue: Any, completion: @escaping (Any) -> ()) - func getStringValueAsync(for key: String, defaultValue: String, user: ConfigCatUser?, completion: @escaping (String) -> ()) - func getIntValueAsync(for key: String, defaultValue: Int, user: ConfigCatUser?, completion: @escaping (Int) -> ()) - func getDoubleValueAsync(for key: String, defaultValue: Double, user: ConfigCatUser?, completion: @escaping (Double) -> ()) - func getBoolValueAsync(for key: String, defaultValue: Bool, user: ConfigCatUser?, completion: @escaping (Bool) -> ()) - func getAnyValueAsync(for key: String, defaultValue: Any, user: ConfigCatUser?, completion: @escaping (Any) -> ()) + /// Initiates a force refresh asynchronously on the cached configuration. + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func refresh() async + #endif + + /// Objective-C interface + /// Generic parameters are not available in Objective-C (getValue, getValueAsync cannot be marked @objc) + func getStringValue(for key: String, defaultValue: String, user: ConfigCatUser?, completion: @escaping (String) -> ()) + func getIntValue(for key: String, defaultValue: Int, user: ConfigCatUser?, completion: @escaping (Int) -> ()) + func getDoubleValue(for key: String, defaultValue: Double, user: ConfigCatUser?, completion: @escaping (Double) -> ()) + func getBoolValue(for key: String, defaultValue: Bool, user: ConfigCatUser?, completion: @escaping (Bool) -> ()) + func getAnyValue(for key: String, defaultValue: Any, user: ConfigCatUser?, completion: @escaping (Any) -> ()) } diff --git a/Sources/ConfigCat/ConfigCatUser.swift b/Sources/ConfigCat/ConfigCatUser.swift index e6b789f..de94cab 100644 --- a/Sources/ConfigCat/ConfigCatUser.swift +++ b/Sources/ConfigCat/ConfigCatUser.swift @@ -1,10 +1,10 @@ import Foundation /// An object containing attributes to properly identify a given user for rollout evaluation. -public final class ConfigCatUser : NSObject { - fileprivate var attributes: [String: String] - fileprivate(set) var identifier: String - +public final class ConfigCatUser: NSObject { + private var attributes: [String: String] + private(set) var identifier: String + /** Initializes a new `User`. @@ -22,34 +22,34 @@ public final class ConfigCatUser : NSObject { attributes = [:] self.identifier = identifier attributes["Identifier"] = identifier - + if let email = email { attributes["Email"] = email } - + if let country = country { attributes["Country"] = country } - + if let custom = custom { for (key, value) in custom { attributes[key] = value } } } - + func getAttribute(for key: String) -> String? { if key.isEmpty { assert(false, "key cannot be empty") } - - if let value = self.attributes[key] { + + if let value = attributes[key] { return value } - + return nil } - + public override var description: String { let jsonEncoder = JSONEncoder() jsonEncoder.outputFormatting = .prettyPrinted diff --git a/Sources/ConfigCat/ConfigFetcher.swift b/Sources/ConfigCat/ConfigFetcher.swift index f940458..0b49bc2 100755 --- a/Sources/ConfigCat/ConfigFetcher.swift +++ b/Sources/ConfigCat/ConfigFetcher.swift @@ -1,195 +1,152 @@ import Foundation -enum Status { - case fetched - case notModified - case failure -} - enum RedirectMode: Int { case noRedirect case shouldRedirect case forceRedirect } -/// Represents a fetch response. -struct FetchResponse { - fileprivate let status: Status - - /** - Gets the fetched configuration value, should be used when the response - has a `FETCHED` status code. - - - Returns: the fetched config. - */ - let config: Config? +enum FetchResponse: Equatable { + case fetched(ConfigEntry) + case notModified + case failure - /** - Gets whether a new configuration value was fetched or not. - - - Returns: true if a new configuration value was fetched, otherwise false. - */ - func isFetched() -> Bool { - return self.status == .fetched - } - - /** - Gets whether the fetch resulted a '304 Not Modified' or not. - - - Returns: true if the fetch resulted a '304 Not Modified' code, otherwise false. - */ - func isNotModified() -> Bool { - return self.status == .notModified - } - - /** - Gets whether the fetch failed or not. - - - Returns: true if the fetch is failed, otherwise false. - */ - func isFailed() -> Bool { - return self.status == .failure + public var entry: ConfigEntry? { + switch self { + case .fetched(let entry): + return entry + default: + return nil + } } } -class ConfigFetcher : NSObject { - private static let version: String = "8.0.1" - fileprivate let log: Logger - fileprivate let session: URLSession - fileprivate var url: String - fileprivate var etag: String - fileprivate let mode: String - fileprivate let sdkKey: String - fileprivate let urlIsCustom: Bool - fileprivate let configJsonCache: ConfigJsonCache - fileprivate var fetchingRequest: AsyncResult? - - static let configJsonName: String = "config_v5" - - static let globalBaseUrl: String = "https://cdn-global.configcat.com" - static let euOnlyBaseUrl: String = "https://cdn-eu.configcat.com" +func ==(lhs: FetchResponse, rhs: FetchResponse) -> Bool { + switch (lhs, rhs) { + case (.fetched(_), .fetched(_)), + (.notModified, .notModified), + (.failure, .failure): + return true + default: + return false + } +} - init(session: URLSession, logger: Logger, configJsonCache: ConfigJsonCache, sdkKey: String, mode: String, - dataGovernance: DataGovernance, baseUrl: String = "") { - self.log = logger - self.configJsonCache = configJsonCache +class ConfigFetcher: NSObject { + private let log: Logger + private let session: URLSession + private let baseUrl: Synced + private let mode: String + private let sdkKey: String + private let urlIsCustom: Bool + + init(session: URLSession, logger: Logger, sdkKey: String, mode: String, + dataGovernance: DataGovernance, baseUrl: String = "") { + log = logger self.session = session self.sdkKey = sdkKey - self.urlIsCustom = !baseUrl.isEmpty - self.url = baseUrl.isEmpty - ? dataGovernance == DataGovernance.euOnly - ? ConfigFetcher.euOnlyBaseUrl - : ConfigFetcher.globalBaseUrl - : baseUrl - self.etag = "" + urlIsCustom = !baseUrl.isEmpty + self.baseUrl = Synced(initValue: baseUrl.isEmpty + ? dataGovernance == DataGovernance.euOnly + ? Constants.euOnlyBaseUrl + : Constants.globalBaseUrl + : baseUrl) self.mode = mode } - func isFetching() -> Bool { - guard let fetchingRequest = fetchingRequest else {return false} - return !fetchingRequest.completed - } - - func getConfiguration() -> AsyncResult { - return self.executeFetch(executionCount: 2) - } - - private func executeFetch(executionCount: Int) -> AsyncResult { - return self.sendFetchRequest().compose { response in - if !response.isFetched() { - return AsyncResult.completed(result: response) + func fetch(eTag: String, completion: @escaping (FetchResponse) -> Void) { + let cachedUrl = baseUrl.get() + executeFetch(url: cachedUrl, eTag: eTag, executionCount: 2) { response in + if let newUrl = response.entry?.config.preferences[Config.preferencesUrl] as? String, !newUrl.isEmpty && newUrl != cachedUrl { + _ = self.baseUrl.testAndSet(expect: cachedUrl, new: newUrl) } - - guard let config = response.config else {return AsyncResult.completed(result: response)} - guard !config.preferences.isEmpty else {return AsyncResult.completed(result: response)} - guard let newUrl = config.preferences[Config.preferencesUrl] as? String else {return AsyncResult.completed(result: response)} + completion(response) + } + } - if newUrl.isEmpty || newUrl == self.url { - return AsyncResult.completed(result: response) + private func executeFetch(url: String, eTag: String, executionCount: Int, completion: @escaping (FetchResponse) -> Void) { + sendFetchRequest(url: url, eTag: eTag, completion: { response in + guard case .fetched(let entry) = response, !entry.config.preferences.isEmpty else { + completion(response) + return + } + guard let newUrl = entry.config.preferences[Config.preferencesUrl] as? String, !newUrl.isEmpty, newUrl != url else { + completion(response) + return + } + guard let redirect = entry.config.preferences[Config.preferencesRedirect] as? Int else { + completion(response) + return } - - guard let redirect = config.preferences[Config.preferencesRedirect] as? Int else {return AsyncResult.completed(result: response)} - if self.urlIsCustom && redirect != RedirectMode.forceRedirect.rawValue { - return AsyncResult.completed(result: response) + completion(response) + return } - - self.url = newUrl - if redirect == RedirectMode.noRedirect.rawValue { - return AsyncResult.completed(result: response) + completion(response) + return } - if redirect == RedirectMode.shouldRedirect.rawValue { self.log.warning(message: """ - Your dataGovernance parameter at ConfigCatClient - initialization is not in sync with your preferences on the ConfigCat - Dashboard: https://app.configcat.com/organization/data-governance. - Only Organization Admins can access this preference. - """) + Your dataGovernance parameter at ConfigCatClient + initialization is not in sync with your preferences on the ConfigCat + Dashboard: https://app.configcat.com/organization/data-governance. + Only Organization Admins can access this preference. + """) } - if executionCount > 0 { - return self.executeFetch(executionCount: executionCount - 1) + self.executeFetch(url: newUrl, eTag: eTag, executionCount: executionCount - 1, completion: completion) + return } - self.log.error(message: "Redirect loop during config.json fetch. Please contact support@configcat.com.") - return AsyncResult.completed(result: response) - } + completion(response) + }) } - - private func sendFetchRequest() -> AsyncResult { - if let fetchingRequest = fetchingRequest { - if !fetchingRequest.completed { - self.log.debug(message: "Config fetching is skipped because there is an ongoing fetch request") - return fetchingRequest - } - } - let request = self.getRequest() - let result = AsyncResult() - - self.session.dataTask(with: request) { data, resp, error in + private func sendFetchRequest(url: String, eTag: String, completion: @escaping (FetchResponse) -> Void) { + let request = getRequest(url: url, eTag: eTag) + session.dataTask(with: request) { (data, resp, error) in if let error = error { var extraInfo = "" if error._code == NSURLErrorTimedOut { extraInfo = String(format: " Timeout interval for request: %.2f seconds.", self.session.configuration.timeoutIntervalForRequest) } self.log.error(message: "An error occurred during the config fetch: %@%@", error.localizedDescription, extraInfo) - result.complete(result: FetchResponse(status: .failure, config: nil)) + completion(.failure) } else { let response = resp as! HTTPURLResponse if response.statusCode >= 200 && response.statusCode < 300, let data = data { self.log.debug(message: "Fetch was successful: new config fetched") - if let etag = response.allHeaderFields["Etag"] as? String { - self.etag = etag + let etag = response.allHeaderFields["Etag"] as? String ?? "" + let jsonString = String(data: data, encoding: .utf8) ?? "" + let configResult = jsonString.parseConfigFromJson() + switch configResult { + case .success(let config): + completion(.fetched(ConfigEntry(jsonString: jsonString, config: config, eTag: etag, fetchTime: Date()))) + case .failure(let error): + self.log.error(message: "An error occurred during JSON deserialization. %@", error.localizedDescription) + completion(.failure) } - result.complete(result: FetchResponse(status: .fetched, config: self.configJsonCache.getConfigFromJson(json: String(data: data, encoding: .utf8)!))) } else if response.statusCode == 304 { self.log.debug(message: "Fetch was successful: not modified") - result.complete(result: FetchResponse(status: .notModified, config: nil)) + completion(.notModified) } else { self.log.error(message: """ - Double-check your SDK Key at https://app.configcat.com/sdkkey. Non success status code: %@ - """, String(response.statusCode)) - result.complete(result: FetchResponse(status: .failure, config: nil)) + Double-check your SDK Key at https://app.configcat.com/sdkkey. Non success status code: %@ + """, String(response.statusCode)) + completion(.failure) } } }.resume() - - fetchingRequest = result - return result } - - private func getRequest() -> URLRequest { - var request = URLRequest(url: URL(string: self.url + "/configuration-files/" + sdkKey + "/" + ConfigFetcher.configJsonName + ".json")!) + + private func getRequest(url: String, eTag: String) -> URLRequest { + var request = URLRequest(url: URL(string: url + "/configuration-files/" + sdkKey + "/" + Constants.configJsonName + ".json")!) request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData - request.addValue("ConfigCat-Swift/" + self.mode + "-" + ConfigFetcher.version, forHTTPHeaderField: "X-ConfigCat-UserAgent") - - if !self.etag.isEmpty { - request.addValue(self.etag, forHTTPHeaderField: "If-None-Match") + request.addValue("ConfigCat-Swift/" + mode + "-" + Constants.version, forHTTPHeaderField: "X-ConfigCat-UserAgent") + if !eTag.isEmpty { + request.addValue(eTag, forHTTPHeaderField: "If-None-Match") } - return request } } diff --git a/Sources/ConfigCat/ConfigJsonCache.swift b/Sources/ConfigCat/ConfigJsonCache.swift deleted file mode 100644 index bbfe0b9..0000000 --- a/Sources/ConfigCat/ConfigJsonCache.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import os.log - -class ConfigJsonCache { - var config: Config = .empty - private let log: Logger - - init(logger: Logger) { - self.log = logger - } - - func getConfigFromJson(json: String) -> Config { - if json.isEmpty { - return .empty - } - - if self.config.jsonString == json { - return self.config - } - - do { - guard let data = json.data(using: .utf8) else {return .empty} - guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {return .empty} - return Config(jsonString: json, preferences: jsonObject[Config.preferences] as? [String: Any] ?? [:], entries: jsonObject[Config.entries] as? [String: Any] ?? [:]) - } catch { - self.log.error(message: "An error occurred during deserializaton. %@", error.localizedDescription) - return .empty - } - } -} - - diff --git a/Sources/ConfigCat/ConfigService.swift b/Sources/ConfigCat/ConfigService.swift new file mode 100644 index 0000000..84acaf5 --- /dev/null +++ b/Sources/ConfigCat/ConfigService.swift @@ -0,0 +1,188 @@ +import Foundation + +class ConfigService { + private let log: Logger + private let fetcher: ConfigFetcher + private let mutex: Mutex = Mutex(recursive: true) + private let cache: ConfigCache? + private let pollingMode: PollingMode + private let cacheKey: String + private var initialized: Bool + private var completions: MutableQueue<(Config) -> Void>? + private var cachedEntry: ConfigEntry = .empty + private var polltimer: DispatchSourceTimer? = nil + private var initTimer: DispatchSourceTimer? = nil + + init(log: Logger, fetcher: ConfigFetcher, cache: ConfigCache?, pollingMode: PollingMode, sdkKey: String) { + self.log = log + self.fetcher = fetcher + self.cache = cache + self.pollingMode = pollingMode + let keyToHash = "swift_" + sdkKey + "_" + Constants.configJsonName + cacheKey = String(keyToHash.sha1hex ?? keyToHash) + + if let autoPoll = pollingMode as? AutoPollingMode { + initialized = false + polltimer = DispatchSource.makeTimerSource() + polltimer?.schedule(deadline: .now(), repeating: .seconds(autoPoll.autoPollIntervalInSeconds)) + polltimer?.setEventHandler(handler: { [weak self] in + guard let this = self else { + return + } + this.fetchIfOlder(time: Date.distantFuture) { _ in + // we don't have to do anything with the result in the timer ticks. + } + }) + polltimer?.resume() + + // Waiting for the client initialization. + // After the maxInitWaitTimeInSeconds timeout the client will be initialized and while the config is not ready + // the default value will be returned. + initTimer = DispatchSource.makeTimerSource() + initTimer?.schedule(deadline: .now() + .seconds(autoPoll.maxInitWaitTimeInSeconds)) + initTimer?.setEventHandler(handler: { [weak self] in + guard let this = self else { + return + } + this.mutex.lock() + defer { this.mutex.unlock() } + + // Max wait time expired without result, notify subscribers with the cached config. + if !this.initialized { + this.initialized = true + this.callCompletions(config: this.cachedEntry.config) + this.completions = nil + } + }) + initTimer?.resume() + } else { + initialized = true + } + } + + deinit { + mutex.lock() + defer { mutex.unlock() } + + callCompletions(config: cachedEntry.config) + completions = nil + polltimer?.cancel() + initTimer?.cancel() + } + + func settings(completion: @escaping ([String: Any]) -> Void) { + switch pollingMode { + case let lazy as LazyLoadingMode: + fetchIfOlder(time: Date().subtract(seconds: lazy.cacheRefreshIntervalInSeconds)!) { config in + completion(config.entries) + } + default: + fetchIfOlder(time: Date.distantPast, preferCache: true) { config in + completion(config.entries) + } + } + } + + func refresh(completion: @escaping () -> Void) { + fetchIfOlder(time: Date.distantFuture) { _ in + completion() + } + } + + private func fetchIfOlder(time: Date, preferCache: Bool = false, completion: @escaping (Config) -> Void) { + mutex.lock() + defer { mutex.unlock() } + + // Sync up with the cache and use it when it's not expired. + if cachedEntry.isEmpty || cachedEntry.fetchTime > time { + let json = readConfigCache() + if !json.isEmpty && json != cachedEntry.jsonString { + let parseResult = json.parseConfigFromJson() + switch parseResult { + case .success(let config): + cachedEntry = ConfigEntry(jsonString: json, config: config, eTag: "") + case .failure(let error): + log.error(message: "An error occurred during JSON deserialization. %@", error.localizedDescription) + } + } + if cachedEntry.fetchTime > time { + completion(cachedEntry.config) + return + } + } + // Use cache anyway (get calls on auto & manual poll must not initiate fetch). + // The initialized check ensures that we subscribe for the ongoing fetch during the + // max init wait time window in case of auto poll. + if preferCache && initialized { + completion(cachedEntry.config) + return + } + // There's an ongoing fetch running, save the callback to call it later when the ongoing fetch finishes. + if completions != nil { + completions?.enqueue(item: completion) + return + } + // No fetch is running, initiate a new one. + completions = MutableQueue<(Config) -> Void>() + completions?.enqueue(item: completion) + fetcher.fetch(eTag: cachedEntry.eTag) { response in + self.processResponse(response: response) + } + } + + private func processResponse(response: FetchResponse) { + mutex.lock() + defer { mutex.unlock() } + + initialized = true + switch response { + case .fetched(let entry) where entry != cachedEntry: + cachedEntry = entry + writeConfigCache(json: entry.jsonString) + if let auto = pollingMode as? AutoPollingMode { + auto.onConfigChanged?() + } + callCompletions(config: entry.config) + case .notModified: + cachedEntry = cachedEntry.withFetchTime(time: Date()) + callCompletions(config: cachedEntry.config) + default: + callCompletions(config: cachedEntry.config) + } + completions = nil + } + + private func callCompletions(config: Config) { + if let completions = completions { + while !completions.isEmpty { + guard let current = completions.dequeue() else { + return + } + current(config) + } + } + } + + private func writeConfigCache(json: String) { + guard let cache = cache else { + return + } + do { + try cache.write(for: cacheKey, value: json) + } catch { + log.error(message: "An error occurred during the cache write: %@", error.localizedDescription) + } + } + + private func readConfigCache() -> String { + guard let cache = cache else { + return "" + } + do { + return try cache.read(for: cacheKey) + } catch { + log.error(message: "An error occurred during the cache read: %@", error.localizedDescription) + return "" + } + } +} diff --git a/Sources/ConfigCat/Extensions.swift b/Sources/ConfigCat/Extensions.swift new file mode 100644 index 0000000..8dcedf5 --- /dev/null +++ b/Sources/ConfigCat/Extensions.swift @@ -0,0 +1,185 @@ +import Foundation + +extension ConfigCatClient { + @objc public func getStringValue(for key: String, defaultValue: String, user: ConfigCatUser?, completion: @escaping (String) -> ()) { + return getValue(for: key, defaultValue: defaultValue, user: user, completion: completion) + } + + @objc public func getIntValue(for key: String, defaultValue: Int, user: ConfigCatUser?, completion: @escaping (Int) -> ()) { + return getValue(for: key, defaultValue: defaultValue, user: user, completion: completion) + } + + @objc public func getDoubleValue(for key: String, defaultValue: Double, user: ConfigCatUser?, completion: @escaping (Double) -> ()) { + return getValue(for: key, defaultValue: defaultValue, user: user, completion: completion) + } + + @objc public func getBoolValue(for key: String, defaultValue: Bool, user: ConfigCatUser?, completion: @escaping (Bool) -> ()) { + return getValue(for: key, defaultValue: defaultValue, user: user, completion: completion) + } + + @objc public func getAnyValue(for key: String, defaultValue: Any, user: ConfigCatUser?, completion: @escaping (Any) -> ()) { + return getValue(for: key, defaultValue: defaultValue, user: user, completion: completion) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func getValue(for key: String, defaultValue: Value, user: ConfigCatUser? = nil) async -> Value { + await withCheckedContinuation { continuation in + getValue(for: key, defaultValue: defaultValue) { value in + continuation.resume(returning: value) + } + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func getAllKeys() async -> [String] { + await withCheckedContinuation { continuation in + getAllKeys { keys in + continuation.resume(returning: keys) + } + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func getVariationId(for key: String, defaultVariationId: String?, user: ConfigCatUser? = nil) async -> String? { + await withCheckedContinuation { continuation in + getVariationId(for: key, defaultVariationId: defaultVariationId) { variationId in + continuation.resume(returning: variationId) + } + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func getAllVariationIds(user: ConfigCatUser? = nil) async -> [String] { + await withCheckedContinuation { continuation in + getAllVariationIds { variationIds in + continuation.resume(returning: variationIds) + } + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func getKeyAndValue(for variationId: String) async -> KeyValue? { + await withCheckedContinuation { continuation in + getKeyAndValue(for: variationId) { value in + continuation.resume(returning: value) + } + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func getAllValues(user: ConfigCatUser? = nil) async -> [String: Any] { + await withCheckedContinuation { continuation in + getAllValues(user: user) { values in + continuation.resume(returning: values) + } + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func refresh() async { + await withCheckedContinuation { continuation in + refresh { + continuation.resume() + } + } + } + #endif + + // Synchronous extensions + + public func getValueSync(for key: String, defaultValue: Value, user: ConfigCatUser? = nil) -> Value { + let semaphore = DispatchSemaphore(value: 0) + var result: Value? + getValue(for: key, defaultValue: defaultValue) { value in + result = value + semaphore.signal() + } + semaphore.wait() + return result ?? defaultValue + } + + @objc public func getAllKeysSync() -> [String] { + let semaphore = DispatchSemaphore(value: 0) + var result = [String]() + getAllKeys { keys in + result = keys + semaphore.signal() + } + semaphore.wait() + return result + } + + @objc public func getVariationIdSync(for key: String, defaultVariationId: String?, user: ConfigCatUser? = nil) -> String? { + let semaphore = DispatchSemaphore(value: 0) + var result: String? + getVariationId(for: key, defaultVariationId: defaultVariationId) { variationId in + result = variationId + semaphore.signal() + } + semaphore.wait() + return result ?? defaultVariationId + } + + @objc public func getAllVariationIdsSync(user: ConfigCatUser? = nil) -> [String] { + let semaphore = DispatchSemaphore(value: 0) + var result = [String]() + getAllVariationIds(user: user) { variationIds in + result = variationIds + semaphore.signal() + } + semaphore.wait() + return result + } + + @objc public func getKeyAndValueSync(for variationId: String) -> KeyValue? { + let semaphore = DispatchSemaphore(value: 0) + var result: KeyValue? + getKeyAndValue(for: variationId) { value in + result = value + semaphore.signal() + } + semaphore.wait() + return result + } + + @objc public func getAllValuesSync(user: ConfigCatUser? = nil) -> [String: Any] { + let semaphore = DispatchSemaphore(value: 0) + var result = [String: Any]() + getAllValues(user: user) { values in + result = values + semaphore.signal() + } + semaphore.wait() + return result + } + + @objc public func refreshSync() { + let semaphore = DispatchSemaphore(value: 0) + refresh { + semaphore.signal() + } + semaphore.wait() + } + + @objc public func getStringValueSync(for key: String, defaultValue: String, user: ConfigCatUser?) -> String { + getValueSync(for: key, defaultValue: defaultValue, user: user) + } + + @objc public func getIntValueSync(for key: String, defaultValue: Int, user: ConfigCatUser?) -> Int { + getValueSync(for: key, defaultValue: defaultValue, user: user) + } + + @objc public func getDoubleValueSync(for key: String, defaultValue: Double, user: ConfigCatUser?) -> Double { + getValueSync(for: key, defaultValue: defaultValue, user: user) + } + + @objc public func getBoolValueSync(for key: String, defaultValue: Bool, user: ConfigCatUser?) -> Bool { + getValueSync(for: key, defaultValue: defaultValue, user: user) + } + + @objc public func getAnyValueSync(for key: String, defaultValue: Any, user: ConfigCatUser?) -> Any { + getValueSync(for: key, defaultValue: defaultValue, user: user) + } +} + diff --git a/Sources/ConfigCat/KeyValue.swift b/Sources/ConfigCat/KeyValue.swift index fb563ad..bde4777 100644 --- a/Sources/ConfigCat/KeyValue.swift +++ b/Sources/ConfigCat/KeyValue.swift @@ -1,8 +1,9 @@ import Foundation -public class KeyValue : NSObject { +public class KeyValue: NSObject { public let key: String? public let value: Any? + init(key: String?, value: Any?) { self.key = key self.value = value diff --git a/Sources/ConfigCat/LazyLoadingPolicy.swift b/Sources/ConfigCat/LazyLoadingPolicy.swift deleted file mode 100755 index fe0928a..0000000 --- a/Sources/ConfigCat/LazyLoadingPolicy.swift +++ /dev/null @@ -1,101 +0,0 @@ -import Foundation - -/// Describes a `RefreshPolicy` which uses an expiring cache to maintain the internally stored configuration. -final class LazyLoadingPolicy : RefreshPolicy { - fileprivate let cacheRefreshIntervalInSeconds: Double - fileprivate let useAsyncRefresh: Bool - fileprivate var lastRefreshTime = Date.distantPast - fileprivate let initialized = Synced(initValue: false) - fileprivate let isFetching = Synced(initValue: false) - fileprivate var fetching = AsyncResult() - fileprivate let initAsync = Async() - - /** - Initializes a new `LazyLoadingPolicy`. - - - Parameter cache: the internal cache instance. - - Parameter fetcher: the internal config fetcher instance. - - Parameter sdkKey: the sdk key. - - Returns: A new `LazyLoadingPolicy`. - */ - convenience required init(cache: ConfigCache?, fetcher: ConfigFetcher, logger: Logger, configJsonCache: ConfigJsonCache, sdkKey: String) { - self.init(cache: cache, fetcher: fetcher, logger: logger, configJsonCache: configJsonCache, sdkKey: sdkKey, config: LazyLoadingMode()) - } - - /** - Initializes a new `LazyLoadingPolicy`. - - - Parameter cache: the internal cache instance. - - Parameter fetcher: the internal config fetcher instance. - - Parameter sdkKey: the sdk key. - - Parameter config: the configuration. - - Returns: A new `LazyLoadingPolicy`. - */ - init(cache: ConfigCache?, - fetcher: ConfigFetcher, - logger: Logger, - configJsonCache: ConfigJsonCache, - sdkKey: String, - config: LazyLoadingMode) { - self.cacheRefreshIntervalInSeconds = config.cacheRefreshIntervalInSeconds - self.useAsyncRefresh = config.useAsyncRefresh - super.init(cache: cache, fetcher: fetcher, logger: logger, configJsonCache: configJsonCache, sdkKey: sdkKey) - } - - override func getConfiguration() -> AsyncResult { - if self.lastRefreshTime.timeIntervalSinceNow < -self.cacheRefreshIntervalInSeconds { - let initialized = self.initAsync.completed - if initialized && !self.isFetching.testAndSet(expect: false, new: true) { - return self.useAsyncRefresh - ? self.readCacheAsync() - : self.fetching - } - - if initialized { - self.fetching = self.fetch() - if self.useAsyncRefresh { - return self.readCacheAsync() - } - return self.fetching - } else { - if self.isFetching.testAndSet(expect: false, new: true) { - self.fetching = self.fetch() - } - return self.initAsync.apply(completion: { - return super.readConfigCache() - }) - } - } - - return self.readCacheAsync() - } - - private func fetch() -> AsyncResult { - return self.fetcher.getConfiguration() - .apply(completion: { response in - let cached = super.readConfigCache() - if let config = response.config, response.isFetched() && config.jsonString != cached.jsonString { - super.writeConfigCache(value: config) - } - - if !response.isFailed() { - self.lastRefreshTime = Date() - } - - if(self.initialized.testAndSet(expect: false, new: true)) { - self.initAsync.complete() - } - - self.isFetching.set(new: false) - - if let config = response.config, response.isFetched() { - return config - } - return cached - }) - } - - private func readCacheAsync() -> AsyncResult { - return AsyncResult.completed(result: self.readConfigCache()) - } -} diff --git a/Sources/ConfigCat/LocalDictionaryDataSource.swift b/Sources/ConfigCat/LocalDictionaryDataSource.swift index 92871a6..7a1da9b 100755 --- a/Sources/ConfigCat/LocalDictionaryDataSource.swift +++ b/Sources/ConfigCat/LocalDictionaryDataSource.swift @@ -11,6 +11,6 @@ public class LocalDictionaryDataSource: OverrideDataSource { } public override func getOverrides() -> [String: Any] { - return settings + settings } } diff --git a/Sources/ConfigCat/Log.swift b/Sources/ConfigCat/Log.swift index 35cebee..9c04c4b 100644 --- a/Sources/ConfigCat/Log.swift +++ b/Sources/ConfigCat/Log.swift @@ -12,47 +12,47 @@ import Foundation class Logger { static let noLogger: Logger = Logger(level: .nolog) - fileprivate static let log: OSLog = OSLog(subsystem: "com.configcat", category: "main") - fileprivate let level: LogLevel - + private static let log: OSLog = OSLog(subsystem: "com.configcat", category: "main") + private let level: LogLevel + init(level: LogLevel) { self.level = level } - + func debug(message: StaticString, _ args: CVarArg...) { - self.log(message: message, currentLevel: .debug, args: args) + log(message: message, currentLevel: .debug, args: args) } - + func warning(message: StaticString, _ args: CVarArg...) { - self.log(message: message, currentLevel: .warning, args: args) + log(message: message, currentLevel: .warning, args: args) } - + func info(message: StaticString, _ args: CVarArg...) { - self.log(message: message, currentLevel: .info, args: args) + log(message: message, currentLevel: .info, args: args) } - + func error(message: StaticString, _ args: CVarArg...) { - self.log(message: message, currentLevel: .error, args: args) + log(message: message, currentLevel: .error, args: args) } - + func log(message: StaticString, currentLevel: LogLevel, args: Array) { - if currentLevel.rawValue >= self.level.rawValue { + if currentLevel.rawValue >= level.rawValue { switch args.count { case 0: - os_log(message, log: Logger.log, type: self.getLogType(level: currentLevel)) + os_log(message, log: Logger.log, type: getLogType(level: currentLevel)) case 1: - os_log(message, log: Logger.log, type: self.getLogType(level: currentLevel), args[0]) + os_log(message, log: Logger.log, type: getLogType(level: currentLevel), args[0]) case 2: - os_log(message, log: Logger.log, type: self.getLogType(level: currentLevel), args[0], args[1]) + os_log(message, log: Logger.log, type: getLogType(level: currentLevel), args[0], args[1]) case 3: - os_log(message, log: Logger.log, type: self.getLogType(level: currentLevel), args[0], args[1], args[2]) + os_log(message, log: Logger.log, type: getLogType(level: currentLevel), args[0], args[1], args[2]) default: - os_log(message, log: Logger.log, type: self.getLogType(level: currentLevel)) + os_log(message, log: Logger.log, type: getLogType(level: currentLevel)) } - + } } - + func getLogType(level: LogLevel) -> OSLogType { switch level { case .debug: diff --git a/Sources/ConfigCat/ManualPollingPolicy.swift b/Sources/ConfigCat/ManualPollingPolicy.swift deleted file mode 100755 index 9b695ec..0000000 --- a/Sources/ConfigCat/ManualPollingPolicy.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -/// Describes a `RefreshPolicy` which fetches the latest configuration over HTTP every time when a get is called on the `ConfigCatClient`. -final class ManualPollingPolicy : RefreshPolicy { - /** - Initializes a new `ManualPollingPolicy`. - - - Parameter cache: the internal cache instance. - - Parameter fetcher: the internal config fetcher instance. - - Parameter sdkKey: the sdk key. - - Returns: A new `ManualPollingPolicy`. - */ - required init(cache: ConfigCache?, - fetcher: ConfigFetcher, - logger: Logger, - configJsonCache: ConfigJsonCache, - sdkKey: String) { - super.init(cache: cache, fetcher: fetcher, logger: logger, configJsonCache: configJsonCache, sdkKey: sdkKey) - } - - override func getConfiguration() -> AsyncResult { - return AsyncResult.completed(result: self.readConfigCache()) - } -} diff --git a/Sources/ConfigCat/MutableQueue.swift b/Sources/ConfigCat/MutableQueue.swift new file mode 100644 index 0000000..9b64e72 --- /dev/null +++ b/Sources/ConfigCat/MutableQueue.swift @@ -0,0 +1,19 @@ +import Foundation + +class MutableQueue { + private var store = [T]() + + func enqueue(item: T) { + store.append(item) + } + + func dequeue() -> T? { + store.isEmpty ? nil : store.removeFirst() + } + + var isEmpty: Bool { + get { + store.isEmpty + } + } +} diff --git a/Sources/ConfigCat/Mutex.swift b/Sources/ConfigCat/Mutex.swift new file mode 100644 index 0000000..82d7da6 --- /dev/null +++ b/Sources/ConfigCat/Mutex.swift @@ -0,0 +1,43 @@ +#if os(Linux) +import Glibc +#else +import Darwin +#endif + +import Foundation + +class Mutex { + private let mutex: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) + + init(recursive: Bool = false) { + var attr = pthread_mutexattr_t() + pthread_mutexattr_init(&attr) + if recursive { + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) + } else { + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL) + } + let result = pthread_mutex_init(mutex, &attr) + assert(result == 0, "Failed to init mutex.") + } + + deinit { + let result = pthread_mutex_destroy(mutex) + assert(result == 0, "Failed to destroy mutex.") + mutex.deallocate() + } + + func lock() { + let result = pthread_mutex_lock(mutex) + assert(result == 0, "Failed to lock mutex.") + } + + func tryLock() -> Bool { + pthread_mutex_trylock(mutex) == 0 + } + + func unlock() { + let result = pthread_mutex_unlock(mutex) + assert(result == 0, "Failed to unlock mutex.") + } +} diff --git a/Sources/ConfigCat/OverrideBehaviour.swift b/Sources/ConfigCat/OverrideBehaviour.swift index 296e879..f008555 100644 --- a/Sources/ConfigCat/OverrideBehaviour.swift +++ b/Sources/ConfigCat/OverrideBehaviour.swift @@ -1,6 +1,6 @@ import Foundation -@objc public enum OverrideBehaviour : Int { +@objc public enum OverrideBehaviour: Int { /** When evaluating values, the SDK will not use feature flags & settings from the ConfigCat CDN, but it will use all feature flags & settings that are loaded from local-override sources. diff --git a/Sources/ConfigCat/OverrideDataSource.swift b/Sources/ConfigCat/OverrideDataSource.swift index 251845b..6b93019 100755 --- a/Sources/ConfigCat/OverrideDataSource.swift +++ b/Sources/ConfigCat/OverrideDataSource.swift @@ -1,11 +1,13 @@ import Foundation -public class OverrideDataSource : NSObject { +public class OverrideDataSource: NSObject { let behaviour: OverrideBehaviour init(behaviour: OverrideBehaviour) { self.behaviour = behaviour } - @objc public func getOverrides() -> [String: Any] { return [:] } + @objc public func getOverrides() -> [String: Any] { + [:] + } } diff --git a/Sources/ConfigCat/PollingMode.swift b/Sources/ConfigCat/PollingMode.swift index 060c27b..885c83e 100644 --- a/Sources/ConfigCat/PollingMode.swift +++ b/Sources/ConfigCat/PollingMode.swift @@ -1,97 +1,52 @@ import Foundation /// Describes a polling mode. -public class PollingMode : NSObject { - func getPollingIdentifier() -> String { - assert(false, "Method must be overidden!") - return "" - } - - func accept(visitor: PollingModeVisitor) -> RefreshPolicy { - assert(false, "Method must be overidden!") - return visitor.visit(pollingMode: ManualPollingMode()) - } -} - -protocol PollingModeVisitor { - func visit(pollingMode: AutoPollingMode) -> RefreshPolicy - func visit(pollingMode: ManualPollingMode) -> RefreshPolicy - func visit(pollingMode: LazyLoadingMode) -> RefreshPolicy +@objc public protocol PollingMode { + var identifier: String { get } } -class AutoPollingMode : PollingMode { - let autoPollIntervalInSeconds: Double +class AutoPollingMode: PollingMode { + let autoPollIntervalInSeconds: Int let maxInitWaitTimeInSeconds: Int let onConfigChanged: ConfigCatClient.ConfigChangedHandler? - init(autoPollIntervalInSeconds: Double = 60, maxInitWaitTimeInSeconds: Int = 5, onConfigChanged: ConfigCatClient.ConfigChangedHandler? = nil) { - self.autoPollIntervalInSeconds = autoPollIntervalInSeconds - self.maxInitWaitTimeInSeconds = maxInitWaitTimeInSeconds + init(autoPollIntervalInSeconds: Int, maxInitWaitTimeInSeconds: Int, onConfigChanged: ConfigCatClient.ConfigChangedHandler? = nil) { + self.autoPollIntervalInSeconds = autoPollIntervalInSeconds < 1 + ? 1 + : autoPollIntervalInSeconds + self.maxInitWaitTimeInSeconds = maxInitWaitTimeInSeconds < 1 + ? 1 + : maxInitWaitTimeInSeconds self.onConfigChanged = onConfigChanged } - - override func getPollingIdentifier() -> String { - return "a" - } - - override func accept(visitor: PollingModeVisitor) -> RefreshPolicy { - return visitor.visit(pollingMode: self) - } -} -class LazyLoadingMode : PollingMode { - let cacheRefreshIntervalInSeconds: Double - let useAsyncRefresh: Bool - - init(cacheRefreshIntervalInSeconds: Double = 60, useAsyncRefresh: Bool = false) { - self.cacheRefreshIntervalInSeconds = cacheRefreshIntervalInSeconds - self.useAsyncRefresh = useAsyncRefresh - } - - override func getPollingIdentifier() -> String { - return "l" - } - - override func accept(visitor: PollingModeVisitor) -> RefreshPolicy { - return visitor.visit(pollingMode: self) + var identifier: String { + get { + "a" + } } } +class LazyLoadingMode: PollingMode { + let cacheRefreshIntervalInSeconds: Int -class ManualPollingMode : PollingMode { - override func getPollingIdentifier() -> String { - return "m" + init(cacheRefreshIntervalInSeconds: Int) { + self.cacheRefreshIntervalInSeconds = cacheRefreshIntervalInSeconds < 1 + ? 1 + : cacheRefreshIntervalInSeconds } - - override func accept(visitor: PollingModeVisitor) -> RefreshPolicy { - return visitor.visit(pollingMode: self) + + var identifier: String { + get { + "l" + } } } -class RefreshPolicyFactory : PollingModeVisitor { - private let cache: ConfigCache? - private let fetcher: ConfigFetcher - private let sdkKey: String - private let logger: Logger - private let configJsonCache: ConfigJsonCache - - init(fetcher: ConfigFetcher, cache: ConfigCache? = nil, logger: Logger, configJsonCache: ConfigJsonCache, sdkKey: String) { - self.fetcher = fetcher - self.cache = cache - self.sdkKey = sdkKey - self.logger = logger - self.configJsonCache = configJsonCache - } - - func visit(pollingMode: AutoPollingMode) -> RefreshPolicy { - return AutoPollingPolicy(cache: self.cache, fetcher: self.fetcher, logger: self.logger, configJsonCache: self.configJsonCache, sdkKey: self.sdkKey, config: pollingMode) - } - - func visit(pollingMode: ManualPollingMode) -> RefreshPolicy { - return ManualPollingPolicy(cache: self.cache, fetcher: self.fetcher, logger: self.logger, configJsonCache: self.configJsonCache, sdkKey: self.sdkKey) - } - - func visit(pollingMode: LazyLoadingMode) -> RefreshPolicy { - return LazyLoadingPolicy(cache: self.cache, fetcher: self.fetcher, logger: self.logger, configJsonCache: self.configJsonCache, sdkKey: self.sdkKey, config: pollingMode) +class ManualPollingMode: PollingMode { + var identifier: String { + get { + "m" + } } } diff --git a/Sources/ConfigCat/PollingModes.swift b/Sources/ConfigCat/PollingModes.swift index d6f2fc4..130daf1 100644 --- a/Sources/ConfigCat/PollingModes.swift +++ b/Sources/ConfigCat/PollingModes.swift @@ -10,25 +10,26 @@ public final class PollingModes { - Parameter onConfigChanged: the configuration changed event handler. - Returns: A new `AutoPollingMode`. */ - public class func autoPoll(autoPollIntervalInSeconds: Double, maxInitWaitTimeInSeconds: Int = 5, onConfigChanged: ConfigCatClient.ConfigChangedHandler? = nil) -> PollingMode { - return AutoPollingMode(autoPollIntervalInSeconds: autoPollIntervalInSeconds, maxInitWaitTimeInSeconds: maxInitWaitTimeInSeconds, onConfigChanged: onConfigChanged) + public class func autoPoll(autoPollIntervalInSeconds: Int = 60, maxInitWaitTimeInSeconds: Int = 5, onConfigChanged: ConfigCatClient.ConfigChangedHandler? = nil) -> PollingMode { + AutoPollingMode(autoPollIntervalInSeconds: autoPollIntervalInSeconds, maxInitWaitTimeInSeconds: maxInitWaitTimeInSeconds, onConfigChanged: onConfigChanged) } + /** Creates a new `LazyLoadingMode`. - Parameter cacheRefreshIntervalInSeconds: sets how long the cache will store its value before fetching the latest from the network again. - - Parameter useAsyncRefresh: sets whether the cache should refresh itself asynchronously or synchronously. If it's set to `true` reading from the policy will not wait for the refresh to be finished, instead it returns immediately with the previous stored value. If it's set to `false` the policy will wait until the expired value is being refreshed with the latest configuration. - Returns: A new `LazyLoadingMode`. */ - public class func lazyLoad(cacheRefreshIntervalInSeconds: Double, useAsyncRefresh: Bool = false) -> PollingMode { - return LazyLoadingMode(cacheRefreshIntervalInSeconds: cacheRefreshIntervalInSeconds, useAsyncRefresh: useAsyncRefresh) + public class func lazyLoad(cacheRefreshIntervalInSeconds: Int = 60) -> PollingMode { + LazyLoadingMode(cacheRefreshIntervalInSeconds: cacheRefreshIntervalInSeconds) } + /** Creates a new `ManualPollingMode`. - Returns: A new `ManualPollingMode`. */ public class func manualPoll() -> PollingMode { - return ManualPollingMode() + ManualPollingMode() } } diff --git a/Sources/ConfigCat/RefreshPolicy.swift b/Sources/ConfigCat/RefreshPolicy.swift deleted file mode 100755 index d99039e..0000000 --- a/Sources/ConfigCat/RefreshPolicy.swift +++ /dev/null @@ -1,83 +0,0 @@ -import os.log -import Foundation - -/// The public interface of a refresh policy which's implementors should describe the configuration update rules. -class RefreshPolicy : NSObject { - let cache: ConfigCache? - let fetcher: ConfigFetcher - let log: Logger - - fileprivate let configJsonCache: ConfigJsonCache - fileprivate let cacheKey: String - - final func writeConfigCache(value: Config) { - self.configJsonCache.config = value - if let cache = self.cache { - do { - try cache.write(for: self.cacheKey, value: value.jsonString) - } catch { - self.log.error(message: "An error occurred during the cache write: %@", error.localizedDescription) - } - } - } - - final func readConfigCache() -> Config { - guard let cache = self.cache else { - return self.configJsonCache.config - } - - do { - return try self.configJsonCache.getConfigFromJson(json: cache.read(for: self.cacheKey)) - } catch { - self.log.error(message: "An error occurred during the cache read, using in memory value: %@", error.localizedDescription) - return self.configJsonCache.config - } - } - - /** - Initializes a new `RefreshPolicy`. - - - Parameter cache: the internal cache instance. - - Parameter fetcher: the internal config fetcher instance. - - Returns: A new `RefreshPolicy`. - */ - required init(cache: ConfigCache?, fetcher: ConfigFetcher, logger: Logger, configJsonCache: ConfigJsonCache, sdkKey: String) { - self.cache = cache - self.fetcher = fetcher - self.log = logger - self.configJsonCache = configJsonCache - let keyToHash = "swift_" + sdkKey + "_" + ConfigFetcher.configJsonName - self.cacheKey = String(keyToHash.sha1hex ?? keyToHash) - } - - /** - Child classes has to implement this method, the `ConfigCatClient` uses it - to read the current configuration value through the applied policy. - - - Returns: the AsyncResult object which computes the configuration. - */ - func getConfiguration() -> AsyncResult { - assert(false, "Method must be overridden!") - return AsyncResult(result: .empty) - } - - func getSettings() -> AsyncResult<[String: Any]> { - self.getConfiguration() - .apply(completion: { config in - return config.entries - }) - } - - /** - Initiates a force refresh on the cached configuration. - - - Returns: the Async object which executes the refresh. - */ - final func refresh() -> Async { - return self.fetcher.getConfiguration().accept { response in - if let config = response.config, response.isFetched() { - self.writeConfigCache(value: config) - } - } - } -} diff --git a/Sources/ConfigCat/Resources/Info.plist b/Sources/ConfigCat/Resources/Info.plist index fcd3639..edd9808 100755 --- a/Sources/ConfigCat/Resources/Info.plist +++ b/Sources/ConfigCat/Resources/Info.plist @@ -1,24 +1,24 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + diff --git a/Sources/ConfigCat/RolloutEvaluator.swift b/Sources/ConfigCat/RolloutEvaluator.swift index 27b1b4c..0e8e72e 100644 --- a/Sources/ConfigCat/RolloutEvaluator.swift +++ b/Sources/ConfigCat/RolloutEvaluator.swift @@ -7,7 +7,7 @@ import Version #endif class RolloutEvaluator { - fileprivate static let comparatorTexts = [ + private static let comparatorTexts = [ "IS ONE OF", "IS NOT ONE OF", "CONTAINS", @@ -27,14 +27,14 @@ class RolloutEvaluator { "IS ONE OF (Sensitive)", "IS NOT ONE OF (Sensitive)", ] - fileprivate let log: Logger; - - + private let log: Logger; + + init(logger: Logger) { - self.log = logger + log = logger } - - + + func evaluate(json: Any?, key: String, user: ConfigCatUser?) -> (value: Value?, variationId: String?, evaluateLog: String?) { guard let json = json as? [String: Any] else { return (nil, nil, nil) @@ -42,19 +42,19 @@ class RolloutEvaluator { let rolloutRules = json[Config.rolloutRules] as? [[String: Any]] ?? [] let rolloutPercentageItems = json[Config.rolloutPercentageItems] as? [[String: Any]] ?? [] - + guard let user = user else { if rolloutRules.count > 0 || rolloutPercentageItems.count > 0 { - self.log.warning(message: - """ - Evaluating getValue(%@). UserObject missing! - You should pass a UserObject to getValue(), - in order to make targeting work properly. - Read more: https://configcat.com/docs/advanced/user-object/ - """, - key) + log.warning(message: + """ + Evaluating getValue(%@). UserObject missing! + You should pass a UserObject to getValue(), + in order to make targeting work properly. + Read more: https://configcat.com/docs/advanced/user-object/ + """, + key) } - + return (json[Config.value] as? Value, json[Config.variationId] as? String, nil) } @@ -62,83 +62,95 @@ class RolloutEvaluator { for rule in rolloutRules { if let comparisonAttribute = rule[Config.comparisonAttribute] as? String, - let comparisonValue = rule[Config.comparisonValue] as? String, - let comparator = rule[Config.comparator] as? Int, - let userValue = user.getAttribute(for: comparisonAttribute) { - + let comparisonValue = rule[Config.comparisonValue] as? String, + let comparator = rule[Config.comparator] as? Int, + let userValue = user.getAttribute(for: comparisonAttribute) { + if comparisonValue.isEmpty || userValue.isEmpty { evaluateLog += "\n" + formatNoMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue) continue } - + switch comparator { - // IS ONE OF + // IS ONE OF case 0: - let splitted = comparisonValue.components(separatedBy: ",") - .map {val in val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)} - - if splitted.contains(userValue) { + let split = comparisonValue.components(separatedBy: ",") + .map { val in + val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } + + if split.contains(userValue) { let returnValue = rule[Config.value] as? Value evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) return (returnValue, rule[Config.variationId] as? String, evaluateLog) } - // IS NOT ONE OF + // IS NOT ONE OF case 1: - let splitted = comparisonValue.components(separatedBy: ",") - .map {val in val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)} - - if !splitted.contains(userValue) { + let split = comparisonValue.components(separatedBy: ",") + .map { val in + val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } + + if !split.contains(userValue) { let returnValue = rule[Config.value] as? Value evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) return (returnValue, rule[Config.variationId] as? String, evaluateLog) } - // CONTAINS + // CONTAINS case 2: if userValue.contains(comparisonValue) { let returnValue = rule[Config.value] as? Value evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) return (returnValue, rule[Config.variationId] as? String, evaluateLog) } - // DOES NOT CONTAIN + // DOES NOT CONTAIN case 3: if !userValue.contains(comparisonValue) { let returnValue = rule[Config.value] as? Value evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) return (returnValue, rule[Config.variationId] as? String, evaluateLog) } - // IS ONE OF (Semantic version), IS NOT ONE OF (Semantic version) + // IS ONE OF (Semantic version), IS NOT ONE OF (Semantic version) case 4...5: - let splitted = comparisonValue.components(separatedBy: ",") - .map {val in val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)} - .filter {val -> Bool in return !val.isEmpty} - + let split = comparisonValue.components(separatedBy: ",") + .map { val in + val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } + .filter { val -> Bool in + !val.isEmpty + } + // The rule will be ignored if we found an invalid semantic version - if let invalidValue = (splitted.first {val -> Bool in Version(val) == nil}) { + if let invalidValue = (split.first { val -> Bool in + Version(val) == nil + }) { let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, error: "Invalid semantic version: \(invalidValue)") - self.log.warning(message: "%@", message) + log.warning(message: "%@", message) evaluateLog += "\n" + message continue } if Version(userValue) == nil { let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, error: "Invalid semantic version: \(userValue)") - self.log.warning(message: "%@", message) + log.warning(message: "%@", message) evaluateLog += "\n" + message continue } - + if comparator == 4 { // IS ONE OF if Version(userValue) == nil { let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, error: "Invalid semantic version: \(userValue)") - self.log.warning(message: "%@", message) + log.warning(message: "%@", message) evaluateLog += "\n" + message continue } - + if let userValueVersion = Version(userValue) { - if (splitted.first {val -> Bool in userValueVersion == Version(val)} != nil) { + if (split.first { val -> Bool in + userValueVersion == Version(val) + } != nil) { let returnValue = rule[Config.value] as? Value evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) return (returnValue, rule[Config.variationId] as? String, evaluateLog) @@ -148,16 +160,18 @@ class RolloutEvaluator { if Version(userValue) == nil { let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, error: "Invalid semantic version: \(userValue)") - self.log.warning(message: "%@", message) + log.warning(message: "%@", message) evaluateLog += "\n" + message continue } - + if let userValueVersion = Version(userValue) { - if let invalidValue = (splitted.first {val -> Bool in userValueVersion == Version(val)}) { + if let invalidValue = (split.first { val -> Bool in + userValueVersion == Version(val) + }) { let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, error: "Invalid semantic version: \(invalidValue)") - self.log.warning(message: "%@", message) + log.warning(message: "%@", message) evaluateLog += "\n" + message continue } @@ -167,38 +181,38 @@ class RolloutEvaluator { return (returnValue, rule[Config.variationId] as? String, evaluateLog) } } - // LESS THAN, LESS THAN OR EQUALS TO, GREATER THAN, GREATER THAN OR EQUALS TO (Semantic version) + // LESS THAN, LESS THAN OR EQUALS TO, GREATER THAN, GREATER THAN OR EQUALS TO (Semantic version) case 6...9: let comparison = comparisonValue.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) if Version(userValue) == nil { let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, error: "Invalid semantic version: \(userValue)") - self.log.warning(message: "%@", message) + log.warning(message: "%@", message) evaluateLog += "\n" + message continue } - + if Version(comparison) == nil { let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, error: "Invalid semantic version: \(comparison)") - self.log.warning(message: "%@", message) + log.warning(message: "%@", message) evaluateLog += "\n" + message continue } if let userValueVersion = Version(userValue), - let comparisonValueVersion = Version(comparison) { + let comparisonValueVersion = Version(comparison) { let userValueVersionWithoutMetadata = Version(major: userValueVersion.major, - minor: userValueVersion.minor, - patch: userValueVersion.patch, - prereleaseIdentifiers: userValueVersion.prereleaseIdentifiers) + minor: userValueVersion.minor, + patch: userValueVersion.patch, + prereleaseIdentifiers: userValueVersion.prereleaseIdentifiers) let comparisonValueVersionWithoutMetadata = Version(major: comparisonValueVersion.major, - minor: comparisonValueVersion.minor, - patch: comparisonValueVersion.patch, - prereleaseIdentifiers: comparisonValueVersion.prereleaseIdentifiers) + minor: comparisonValueVersion.minor, + patch: comparisonValueVersion.patch, + prereleaseIdentifiers: comparisonValueVersion.prereleaseIdentifiers) if (comparator == 6 && userValueVersionWithoutMetadata < comparisonValueVersionWithoutMetadata) - || (comparator == 7 && userValueVersionWithoutMetadata <= comparisonValueVersionWithoutMetadata) - || (comparator == 8 && userValueVersionWithoutMetadata > comparisonValueVersionWithoutMetadata) - || (comparator == 9 && userValueVersionWithoutMetadata >= comparisonValueVersionWithoutMetadata) { + || (comparator == 7 && userValueVersionWithoutMetadata <= comparisonValueVersionWithoutMetadata) + || (comparator == 8 && userValueVersionWithoutMetadata > comparisonValueVersionWithoutMetadata) + || (comparator == 9 && userValueVersionWithoutMetadata >= comparisonValueVersionWithoutMetadata) { let returnValue = rule[Config.value] as? Value evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) return (returnValue, rule[Config.variationId] as? String, evaluateLog) @@ -206,23 +220,25 @@ class RolloutEvaluator { } case 10...15: if let userValueFloat = Float(userValue.replacingOccurrences(of: ",", with: ".")), - let comparisonValueFloat = Float(comparisonValue.replacingOccurrences(of: ",", with: ".")) { + let comparisonValueFloat = Float(comparisonValue.replacingOccurrences(of: ",", with: ".")) { if (comparator == 10 && userValueFloat == comparisonValueFloat) - || (comparator == 11 && userValueFloat != comparisonValueFloat) - || (comparator == 12 && userValueFloat < comparisonValueFloat) - || (comparator == 13 && userValueFloat <= comparisonValueFloat) - || (comparator == 14 && userValueFloat > comparisonValueFloat) - || (comparator == 15 && userValueFloat >= comparisonValueFloat) { + || (comparator == 11 && userValueFloat != comparisonValueFloat) + || (comparator == 12 && userValueFloat < comparisonValueFloat) + || (comparator == 13 && userValueFloat <= comparisonValueFloat) + || (comparator == 14 && userValueFloat > comparisonValueFloat) + || (comparator == 15 && userValueFloat >= comparisonValueFloat) { let returnValue = rule[Config.value] as? Value evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) return (returnValue, rule[Config.variationId] as? String, evaluateLog) } } - // IS ONE OF (Sensitive) + // IS ONE OF (Sensitive) case 16: let splitted = comparisonValue.components(separatedBy: ",") - .map {val in val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)} - + .map { val in + val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } + if let userValueHash = userValue.sha1hex { if splitted.contains(userValueHash) { let returnValue = rule[Config.value] as? Value @@ -230,11 +246,13 @@ class RolloutEvaluator { return (returnValue, rule[Config.variationId] as? String, evaluateLog) } } - // IS NOT ONE OF (Sensitive) + // IS NOT ONE OF (Sensitive) case 17: let splitted = comparisonValue.components(separatedBy: ",") - .map {val in val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)} - + .map { val in + val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } + if let userValueHash = userValue.sha1hex { if !splitted.contains(userValueHash) { let returnValue = rule[Config.value] as? Value @@ -250,13 +268,13 @@ class RolloutEvaluator { } } - if (rolloutPercentageItems.count > 0){ + if (rolloutPercentageItems.count > 0) { let hashCandidate = key + user.identifier if let hash = hashCandidate.sha1hex?.prefix(7) { let hashString = String(hash) if let num = Int(hashString, radix: 16) { let scaled = num % 100 - + var bucket = 0 for rule in rolloutPercentageItems { if let percentage = rule[Config.percentage] as? Int { @@ -274,25 +292,25 @@ class RolloutEvaluator { evaluateLog += "\n" + String(format: "Returning %@", json[Config.value] as? String ?? "") return (json[Config.value] as? Value, json[Config.variationId] as? String, evaluateLog) } - + private func formatMatchRule(comparisonAttribute: String, userValue: String, comparator: Int, comparisonValue: String, value: Value?) -> String { let format = String(format: "Evaluating rule: [%@:%@] [%@] [%@] => match, returning: ", - comparisonAttribute, userValue, RolloutEvaluator.comparatorTexts[comparator], comparisonValue) - + comparisonAttribute, userValue, RolloutEvaluator.comparatorTexts[comparator], comparisonValue) + guard let value = value else { return format + "nil" } return format + "\(value)" } - + private func formatNoMatchRule(comparisonAttribute: String, userValue: String, comparator: Int, comparisonValue: String) -> String { - return String(format: "Evaluating rule: [%@:%@] [%@] [%@] => no match", - comparisonAttribute, userValue, RolloutEvaluator.comparatorTexts[comparator], comparisonValue) + String(format: "Evaluating rule: [%@:%@] [%@] [%@] => no match", + comparisonAttribute, userValue, RolloutEvaluator.comparatorTexts[comparator], comparisonValue) } - + private func formatValidationErrorRule(comparisonAttribute: String, userValue: String, comparator: Int, comparisonValue: String, error: String) -> String { - return String(format: "Evaluating rule: [%@:%@] [%@] [%@] => SKIP rule. Validation error: %@", - comparisonAttribute, userValue, RolloutEvaluator.comparatorTexts[comparator], comparisonValue, error) + String(format: "Evaluating rule: [%@:%@] [%@] [%@] => SKIP rule. Validation error: %@", + comparisonAttribute, userValue, RolloutEvaluator.comparatorTexts[comparator], comparisonValue, error) } } @@ -309,12 +327,15 @@ internal extension Data { var digestSHA1: Data { var bytes: [UInt8] = Array(repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) withUnsafeBytes { - _ = CC_SHA1($0, CC_LONG(count), &bytes) + _ = CC_SHA1($0.baseAddress, CC_LONG(count), &bytes) } - return Data(bytes: bytes) + return Data(_: bytes) } - + var hexString: String { - return map { String(format: "%02x", UInt8($0)) }.joined() + map { + String(format: "%02x", UInt8($0)) + } + .joined() } } diff --git a/Sources/ConfigCat/Synced.swift b/Sources/ConfigCat/Synced.swift index 4b97fb4..1a047ad 100755 --- a/Sources/ConfigCat/Synced.swift +++ b/Sources/ConfigCat/Synced.swift @@ -1,38 +1,47 @@ import Foundation -class Synced { - fileprivate let lock = DispatchSemaphore(value: 1) - fileprivate var value: Value - +class Synced { + private let mutex = Mutex() + private var value: Value + init(initValue: Value) { - self.value = initValue + value = initValue } - + func get() -> Value { - lock.wait() - defer { lock.signal() } + mutex.lock() + defer { + mutex.unlock() + } return value } - + func set(new: Value) { - lock.wait() - defer { lock.signal() } - self.value = new + mutex.lock() + defer { + mutex.unlock() + } + value = new } - + + @discardableResult func testAndSet(expect: Value, new: Value) -> Bool { - lock.wait() - defer { lock.signal() } - let challange = self.value == expect - self.value = challange ? new : self.value - return challange + mutex.lock() + defer { + mutex.unlock() + } + let challenge = value == expect + value = challenge ? new : value + return challenge } - + func getAndSet(new: Value) -> Value { - lock.wait() - defer { lock.signal() } - let old = self.value - self.value = new + mutex.lock() + defer { + mutex.unlock() + } + let old = value + value = new return old } } diff --git a/Sources/ConfigCat/Utils.swift b/Sources/ConfigCat/Utils.swift new file mode 100644 index 0000000..59563cb --- /dev/null +++ b/Sources/ConfigCat/Utils.swift @@ -0,0 +1,49 @@ +import Foundation + +struct ParseError: Error, CustomStringConvertible { + private let message: String + + init(message: String) { + self.message = message + } + + var description: String { + get { + message + } + } +} + +extension String { + func parseConfigFromJson() -> Result { + do { + guard let data = data(using: .utf8) else { + return .failure(ParseError(message: "Decode to utf8 data failed.")) + } + guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + return .failure(ParseError(message: "Convert to [String: Any] map failed.")) + } + return .success(Config(preferences: jsonObject[Config.preferences] as? [String: Any] ?? [:], entries: jsonObject[Config.entries] as? [String: Any] ?? [:])) + } catch { + return .failure(error) + } + } +} + +extension Date { + func add(years: Int = 0, months: Int = 0, days: Int = 0, hours: Int = 0, minutes: Int = 0, seconds: Int = 0) -> Date? { + let comp = DateComponents(year: years, month: months, day: days, hour: hours, minute: minutes, second: seconds) + return Calendar.current.date(byAdding: comp, to: self) + } + + func subtract(years: Int = 0, months: Int = 0, days: Int = 0, hours: Int = 0, minutes: Int = 0, seconds: Int = 0) -> Date? { + add(years: -years, months: -months, days: -days, hours: -hours, minutes: -minutes, seconds: -seconds) + } +} + +class Constants { + static let version: String = "9.0.0" + static let configJsonName: String = "config_v5" + static let globalBaseUrl: String = "https://cdn-global.configcat.com" + static let euOnlyBaseUrl: String = "https://cdn-eu.configcat.com" +} \ No newline at end of file diff --git a/Tests/ConfigCatTests/AsyncAwaitTests.swift b/Tests/ConfigCatTests/AsyncAwaitTests.swift new file mode 100644 index 0000000..525747b --- /dev/null +++ b/Tests/ConfigCatTests/AsyncAwaitTests.swift @@ -0,0 +1,59 @@ +import XCTest +@testable import ConfigCat + +class AsyncAwaitTests: XCTestCase { + #if compiler(>=5.5) && canImport(_Concurrency) + let testJsonMultiple = #"{ "f": { "key1": { "v": true, "i": "fakeId1", "p": [], "r": [] }, "key2": { "v": false, "i": "fakeId2", "p": [], "r": [] } } }"# + + override func setUp() { + super.setUp() + MockHTTP.reset() + MockHTTP.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testGetValue() async { + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(), session: MockHTTP.session()) + let value = await client.getValue(for: "key1", defaultValue: false) + XCTAssertTrue(value) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testGetVariationId() async { + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(), session: MockHTTP.session()) + let id = await client.getVariationId(for: "key1", defaultVariationId: "") + XCTAssertEqual("fakeId1", id) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testGetKeyValue() async { + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(), session: MockHTTP.session()) + let id = await client.getKeyAndValue(for: "fakeId1") + XCTAssertEqual(true, id?.value as? Bool) + XCTAssertEqual("key1", id?.key) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testGetAllKeys() async { + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(), session: MockHTTP.session()) + let keys = await client.getAllKeys() + XCTAssertEqual(2, keys.count) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testGetAllValues() async { + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(), session: MockHTTP.session()) + let values = await client.getAllValues() + XCTAssertEqual(2, values.count) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testRefresh() async { + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.manualPoll(), session: MockHTTP.session()) + await client.refresh() + let value = await client.getValue(for: "key2", defaultValue: true) + XCTAssertFalse(value) + XCTAssertEqual(1, MockHTTP.requests.count) + } + #endif +} diff --git a/Tests/ConfigCatTests/AutoPollingTests.swift b/Tests/ConfigCatTests/AutoPollingTests.swift index 349ed22..61407a5 100755 --- a/Tests/ConfigCatTests/AutoPollingTests.swift +++ b/Tests/ConfigCatTests/AutoPollingTests.swift @@ -2,93 +2,123 @@ import XCTest @testable import ConfigCat class AutoPollingTests: XCTestCase { - private var mockSession = MockURLSession() private let testJsonFormat = #"{ "f": { "fakeKey": { "v": "%@", "p": [], "r": [] } } }"# override func setUp() { super.setUp() - self.mockSession = MockURLSession() + MockHTTP.reset() } func testGet() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 2) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, sdkKey: "") + + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - sleep(3) - - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + + let expectation2 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test2", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + wait(for: [expectation2], timeout: 2) } - + func testGetFailedRequest() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 500)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 500)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 2) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(),dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, sdkKey: "") + + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) + sleep(3) - - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + + let expectation2 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + wait(for: [expectation2], timeout: 2) } func testOnConfigChanged() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200)) - + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) + var called = false - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 2, onConfigChanged: { () in called = true }) - let fetcher = ConfigFetcher(session: mockSession,logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, sdkKey: "") + sleep(1) - + XCTAssertTrue(called) - + sleep(3) - - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test2", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) } func testRequestTimeout() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200, delay: 3)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200, delay: 3)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 1) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, sdkKey: "") sleep(2) - XCTAssertEqual(1, mockSession.requests.count) + XCTAssertEqual(1, MockHTTP.requests.count) sleep(2) - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) } func testInitWaitTimeTimeout() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200, delay: 5)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200, delay: 5)) let start = Date() - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 60, maxInitWaitTimeInSeconds: 1) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - XCTAssertTrue(try policy.getConfiguration().get().entries.isEmpty) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, sdkKey: "") + + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertTrue(settings.isEmpty) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) let endTime = Date() let elapsedTimeInSeconds = endTime.timeIntervalSince(start) @@ -99,39 +129,59 @@ class AutoPollingTests: XCTestCase { func testCache() throws { let mockCache = InMemoryConfigCache() - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 2) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, cache: mockCache, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: mockCache, pollingMode: mode, sdkKey: "") + + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) XCTAssertEqual(1, mockCache.store.count) - XCTAssertEqual(String(format: self.testJsonFormat, "test"), mockCache.store.values.first) + XCTAssertEqual(String(format: testJsonFormat, "test"), mockCache.store.values.first) sleep(3) - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + let expectation2 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test2", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + wait(for: [expectation2], timeout: 2) + XCTAssertEqual(1, mockCache.store.count) - XCTAssertEqual(String(format: self.testJsonFormat, "test2"), mockCache.store.values.first) + XCTAssertEqual(String(format: testJsonFormat, "test2"), mockCache.store.values.first) } func testCacheFails() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 2) - let fetcher = ConfigFetcher(session: mockSession,logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, cache: FailingCache(), logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, sdkKey: "") - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) sleep(3) - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + let expectation2 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test2", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + wait(for: [expectation2], timeout: 2) } } diff --git a/Tests/ConfigCatTests/ConfigCatClientIntegrationTests.swift b/Tests/ConfigCatTests/ConfigCatClientIntegrationTests.swift index ec99e1c..0991ef5 100755 --- a/Tests/ConfigCatTests/ConfigCatClientIntegrationTests.swift +++ b/Tests/ConfigCatTests/ConfigCatClientIntegrationTests.swift @@ -4,26 +4,23 @@ import XCTest class ConfigCatClientIntegrationTests: XCTestCase { func testGetAllKeys() { let client = ConfigCatClient(sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A") - let keys = client.getAllKeys() - XCTAssertEqual(16, keys.count) - XCTAssertTrue(keys.contains("stringDefaultCat")) + let expectation = expectation(description: "wait for all keys") + client.getAllKeys { keys in + XCTAssertEqual(16, keys.count) + XCTAssertTrue(keys.contains("stringDefaultCat")) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) } func testGetAllValues() { let client = ConfigCatClient(sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A") - let allValues = client.getAllValues() - XCTAssertEqual(16, allValues.count) - XCTAssertEqual("Cat", allValues["stringDefaultCat"] as! String) - } - - func testGetAllValuesAsync() throws { - let client = ConfigCatClient(sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A") - let allValuesResult = AsyncResult<[String: Any]>() - client.getAllValuesAsync() { (result) in - allValuesResult.complete(result: result) + let expectation = expectation(description: "wait for all values") + client.getAllValues { allValues in + XCTAssertEqual(16, allValues.count) + XCTAssertEqual("Cat", allValues["stringDefaultCat"] as! String) + expectation.fulfill() } - let allValues = try allValuesResult.get() - XCTAssertEqual(16, allValues.count) - XCTAssertEqual("Cat", allValues["stringDefaultCat"] as! String) + wait(for: [expectation], timeout: 2) } } diff --git a/Tests/ConfigCatTests/ConfigCatClientTests.swift b/Tests/ConfigCatTests/ConfigCatClientTests.swift index 604cef3..7a557d7 100755 --- a/Tests/ConfigCatTests/ConfigCatClientTests.swift +++ b/Tests/ConfigCatTests/ConfigCatClientTests.swift @@ -2,173 +2,361 @@ import XCTest @testable import ConfigCat class ConfigCatClientTests: XCTestCase { - var mockSession = MockURLSession() let testJsonFormat = #"{ "f": { "fakeKey": { "v": %@, "p": [], "r": [] } } }"# let testJsonMultiple = #"{ "f": { "key1": { "v": true, "i": "fakeId1", "p": [], "r": [] }, "key2": { "v": false, "i": "fakeId2", "p": [], "r": [] } } }"# - + override func setUp() { super.setUp() - self.mockSession = MockURLSession() + MockHTTP.reset() } func testGetIntValue() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "43"), statusCode: 200)) - let client = self.createClient() - client.refresh() - let config = client.getValue(for: "fakeKey", defaultValue: 10) - - XCTAssertEqual(43, config) - } - + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "43"), statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: 10) { value in + XCTAssertEqual(43, value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) + } + func testGetIntValueFailed() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "fake"), statusCode: 200)) - let client = self.createClient() - let config = client.getValue(for: "fakeKey", defaultValue: 10) - - XCTAssertEqual(10, config) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "fake"), statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: 10) { value in + XCTAssertEqual(10, value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) } - + func testGetIntValueFailedInvalidJson() { - mockSession.enqueueResponse(response: Response(body: "", statusCode: 200)) - let client = self.createClient() - let config = client.getValue(for: "fakeKey", defaultValue: 10) - - XCTAssertEqual(10, config) + MockHTTP.enqueueResponse(response: Response(body: "{", statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: 10) { value in + XCTAssertEqual(10, value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) } - + func testGetStringValue() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "\"fake\""), statusCode: 200)) - let client = self.createClient() - client.refresh() - let config = client.getValue(for: "fakeKey", defaultValue: "def") - - XCTAssertEqual("fake", config) - } - + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: "def") { value in + XCTAssertEqual("fake", value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) + } + func testGetStringValueFailed() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "33"), statusCode: 200)) - let client = self.createClient() - let config = client.getValue(for: "fakeKey", defaultValue: "def") - - XCTAssertEqual("def", config) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "33"), statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: "def") { value in + XCTAssertEqual("def", value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) } - + func testGetDoubleValue() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "43.56"), statusCode: 200)) - let client = self.createClient() - client.refresh() - let config = client.getValue(for: "fakeKey", defaultValue: 34.23) - - XCTAssertEqual(43.56, config) - } - + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "43.56"), statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: 3.14) { value in + XCTAssertEqual(43.56, value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) + } + func testGetDoubleValueFailed() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "fake"), statusCode: 200)) - let client = self.createClient() - let config = client.getValue(for: "fakeKey", defaultValue: 23.54) - - XCTAssertEqual(23.54, config) + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 404, error: TestError.test)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: 3.14) { value in + XCTAssertEqual(3.14, value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) } - + func testGetBoolValue() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "true"), statusCode: 200)) - let client = self.createClient() - client.refresh() - let config = client.getValue(for: "fakeKey", defaultValue: false) - - XCTAssertTrue(config) - } - + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "true"), statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: false) { value in + XCTAssertTrue(value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) + } + func testGetBoolValueFailed() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "fake"), statusCode: 200)) - let client = self.createClient() - let config = client.getValue(for: "fakeKey", defaultValue: true) - - XCTAssertTrue(config) + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 404, error: TestError.test)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: false) { value in + XCTAssertFalse(value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) } - + func testGetValueWithInvalidTypeFailed() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "fake"), statusCode: 200)) - let client = self.createClient() - let config = client.getValue(for: "fakeKey", defaultValue: Float(55)) - - XCTAssertEqual(Float(55), config) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "fake"), statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: Float(55)) { value in + XCTAssertEqual(Float(55), value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) } - + func testGetLatestOnFail() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "55"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: "", statusCode: 500)) - let client = self.createClient() - client.refresh() - var config = client.getValue(for: "fakeKey", defaultValue: 0) - XCTAssertEqual(55, config) - client.refresh() - config = client.getValue(for: "fakeKey", defaultValue: 0) - XCTAssertEqual(55, config) - } - - func testGetLatestOnFailAsync() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "55"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: "", statusCode: 500)) - let client = self.createClient() - let config = AsyncResult() - client.refresh() - client.getValueAsync(for: "fakeKey", defaultValue: 0) { (result) in - config.complete(result: result) - } - - XCTAssertEqual(55, try config.get()) - - client.refresh() - let config2 = AsyncResult() - client.getValueAsync(for: "fakeKey", defaultValue: 0) { (result) in - config2.complete(result: result) - } - - XCTAssertEqual(55, try config2.get()) - } - - func testForceRefresh() { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "\"test\""), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "\"test2\""), statusCode: 200)) - - let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 120), session: self.mockSession) - - XCTAssertEqual("test", client.getValue(for: "fakeKey", defaultValue: "def")) - - client.refresh() - - XCTAssertEqual("test2", client.getValue(for: "fakeKey", defaultValue: "def")) - } - + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "55"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 500)) + let client = createClient() + let expectation1 = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: 0) { value in + XCTAssertEqual(55, value) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 2) + + let expectation2 = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: 0) { value in + XCTAssertEqual(55, value) + expectation2.fulfill() + } + } + wait(for: [expectation2], timeout: 2) + } + + func testForceRefreshLazy() { + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"test\""), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"test2\""), statusCode: 200)) + + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 120), session: MockHTTP.session()) + + let expectation1 = self.expectation(description: "wait for response") + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("test", value) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) + + let expectation2 = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("test2", value) + expectation2.fulfill() + } + } + wait(for: [expectation2], timeout: 2) + } + + func testForceRefreshAuto() { + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"test\""), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"test2\""), statusCode: 200)) + + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), session: MockHTTP.session()) + + let expectation1 = self.expectation(description: "wait for response") + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("test", value) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) + + let expectation2 = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("test2", value) + expectation2.fulfill() + } + } + wait(for: [expectation2], timeout: 2) + } + func testFailingAutoPoll() { - mockSession.enqueueResponse(response: Response(body: "", statusCode: 500)) - - let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), session: self.mockSession) - - XCTAssertEqual("def", client.getValue(for: "fakeKey", defaultValue: "def")) + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 500)) + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), session: MockHTTP.session()) + let expectation1 = self.expectation(description: "wait for response") + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("", value) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) } - + + func testRequestTimeout() { + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"test\""), statusCode: 200, delay: 3)) + let config = URLSessionConfiguration.default + config.timeoutIntervalForRequest = 1 + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), session: MockHTTP.session(config: config)) + let expectation1 = self.expectation(description: "wait for response") + let start = Date() + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("", value) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) + + let endTime = Date() + let elapsedTimeInSeconds = endTime.timeIntervalSince(start) + XCTAssert(elapsedTimeInSeconds > 1) + XCTAssert(elapsedTimeInSeconds < 2) + } + + func testFromCacheOnly() throws { + let cache = InMemoryConfigCache() + let sdkKey = "test" + let keyToHash = "swift_" + sdkKey + "_" + Constants.configJsonName + let cacheKey = String(keyToHash.sha1hex ?? keyToHash) + try cache.write(for: cacheKey, value: String(format: testJsonFormat, "\"fake\"")) + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 500)) + + let client = ConfigCatClient(sdkKey: sdkKey, refreshMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), session: MockHTTP.session(), configCache: cache) + let expectation = self.expectation(description: "wait for response") + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("fake", value) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) + } + + func testFromCacheOnlyRefresh() throws { + let cache = InMemoryConfigCache() + let sdkKey = "test" + let keyToHash = "swift_" + sdkKey + "_" + Constants.configJsonName + let cacheKey = String(keyToHash.sha1hex ?? keyToHash) + try cache.write(for: cacheKey, value: String(format: testJsonFormat, "\"fake\"")) + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 500)) + + let client = ConfigCatClient(sdkKey: sdkKey, refreshMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), session: MockHTTP.session(), configCache: cache) + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("fake", value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) + } + + func testFailingAutoPollRefresh() { + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 500)) + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), session: MockHTTP.session()) + let expectation1 = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("", value) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 2) + } + func testFailingExpiringCache() { - mockSession.enqueueResponse(response: Response(body: "", statusCode: 500)) - - let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 120), session: self.mockSession) - - XCTAssertEqual("def", client.getValue(for: "fakeKey", defaultValue: "def")) + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 500)) + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 120), session: MockHTTP.session()) + let expectation1 = self.expectation(description: "wait for response") + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("", value) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) } func testGetAllValues() { - mockSession.enqueueResponse(response: Response(body: self.testJsonMultiple, statusCode: 200)) - let client = self.createClient() - client.refresh() - let allValues = client.getAllValues() + MockHTTP.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) + let client = createClient() + let expectation1 = self.expectation(description: "wait for response") + client.refresh { + client.getAllValues { allValues in + XCTAssertEqual(2, allValues.count) + XCTAssertEqual(true, allValues["key1"] as! Bool) + XCTAssertEqual(false, allValues["key2"] as! Bool) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 2) + } + + func testAutoPollUserAgentHeader() { + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.autoPoll(), session: MockHTTP.session()) + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("fake", value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) + XCTAssertEqual("ConfigCat-Swift/a-" + Constants.version, MockHTTP.requests.last?.value(forHTTPHeaderField: "X-ConfigCat-UserAgent")) + } - XCTAssertEqual(2, allValues.count) - XCTAssertEqual(true, allValues["key1"] as! Bool) - XCTAssertEqual(false, allValues["key2"] as! Bool) + func testLazyUserAgentHeader() { + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.lazyLoad(), session: MockHTTP.session()) + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("fake", value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) + XCTAssertEqual("ConfigCat-Swift/l-" + Constants.version, MockHTTP.requests.last?.value(forHTTPHeaderField: "X-ConfigCat-UserAgent")) + } + + func testManualPollUserAgentHeader() { + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) + let client = ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.manualPoll(), session: MockHTTP.session()) + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getValue(for: "fakeKey", defaultValue: "") { value in + XCTAssertEqual("fake", value) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) + XCTAssertEqual("ConfigCat-Swift/m-" + Constants.version, MockHTTP.requests.last?.value(forHTTPHeaderField: "X-ConfigCat-UserAgent")) } private func createClient() -> ConfigCatClient { - return ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.manualPoll(), session: self.mockSession) + ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.manualPoll(), session: MockHTTP.session()) } } diff --git a/Tests/ConfigCatTests/ConfigFetcherTests.swift b/Tests/ConfigCatTests/ConfigFetcherTests.swift index e941863..5b9ef86 100755 --- a/Tests/ConfigCatTests/ConfigFetcherTests.swift +++ b/Tests/ConfigCatTests/ConfigFetcherTests.swift @@ -2,68 +2,75 @@ import XCTest @testable import ConfigCat class ConfigFetcherTests: XCTestCase { - private var mockSession = MockURLSession() override func setUp() { super.setUp() - self.mockSession = MockURLSession() + MockHTTP.reset() } func testSimpleFetchSuccess() throws { let testBody = #"{ "f": { "fakeKey": { "v": "fakeValue", "p": [], "r": [] } } }"# - mockSession.enqueueResponse(response: Response(body: testBody, statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: testBody, statusCode: 200)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) - XCTAssertEqual("fakeValue", (try fetcher.getConfiguration().get().config?.entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + let expectation = self.expectation(description: "wait for response") + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertEqual("fakeValue", (response.entry?.config.entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) } func testSimpleFetchNotModified() throws { - mockSession.enqueueResponse(response: Response(body: "", statusCode: 304)) + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 304)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) - let response = try fetcher.getConfiguration().get() - XCTAssertTrue(response.isNotModified()) - XCTAssertNil(response.config) + let expectation = self.expectation(description: "wait for response") + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.notModified, response) + XCTAssertNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) } func testSimpleFetchFailed() throws { - mockSession.enqueueResponse(response: Response(body: "", statusCode: 404)) + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 404)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) - let response = try fetcher.getConfiguration().get() - XCTAssertTrue(response.isFailed()) - XCTAssertNil(response.config) + let expectation = self.expectation(description: "wait for response") + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.failure, response) + XCTAssertNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) } func testFetchNotModifiedEtag() throws { let etag = "test" - mockSession.enqueueResponse(response: Response(body: "", statusCode: 200, headers: ["Etag": etag])) - mockSession.enqueueResponse(response: Response(body: "", statusCode: 304)) - - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) - var response = try fetcher.getConfiguration().get() - XCTAssertTrue(response.isFetched()) - response = try fetcher.getConfiguration().get() - XCTAssertTrue(response.isNotModified()) - - XCTAssertEqual(etag, mockSession.requests.last?.value(forHTTPHeaderField: "If-None-Match")) - } - - func testOngoingFetch() throws { - mockSession.enqueueResponse(response: Response(body: "", statusCode: 200, delay: 1)) + let testBody = #"{ "f": { "fakeKey": { "v": "fakeValue", "p": [], "r": [] } } }"# + MockHTTP.enqueueResponse(response: Response(body: testBody, statusCode: 200, headers: ["Etag": etag])) + MockHTTP.enqueueResponse(response: Response(body: "", statusCode: 304)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let fetcher = ConfigFetcher(session: mockSession,logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) - let asyncResponse = fetcher.getConfiguration() - var isFetching = fetcher.isFetching() - XCTAssertTrue(isFetching) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) + let expectation = self.expectation(description: "wait for response") + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertNotNil(response.entry) + XCTAssertEqual(etag, response.entry?.eTag) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) - _ = try asyncResponse.get() - isFetching = fetcher.isFetching() - XCTAssertFalse(isFetching) + let notModifiedExpectation = self.expectation(description: "wait for response") + fetcher.fetch(eTag: etag) { response in + XCTAssertEqual(.notModified, response) + XCTAssertNil(response.entry) + notModifiedExpectation.fulfill() + } + wait(for: [notModifiedExpectation], timeout: 2) + XCTAssertEqual(etag, MockHTTP.requests.last?.value(forHTTPHeaderField: "If-None-Match")) } } diff --git a/Tests/ConfigCatTests/DataGovernanceTests.swift b/Tests/ConfigCatTests/DataGovernanceTests.swift index 0ceb204..c47b7d7 100644 --- a/Tests/ConfigCatTests/DataGovernanceTests.swift +++ b/Tests/ConfigCatTests/DataGovernanceTests.swift @@ -4,166 +4,217 @@ import XCTest class DataGovernanceTests: XCTestCase { private let jsonTemplate: String = #"{ "p": { "u": "%@", "r": %@ }, "f": {} }"# private let customCdnUrl: String = "https://custom-cdn.configcat.com" - private var mockSession = MockURLSession() override func setUp() { super.setUp() - self.mockSession = MockURLSession() + MockHTTP.reset() } func testShouldStayOnServer() throws { // Arrange - let body = String(format: self.jsonTemplate, "https://fakeUrl", "0") - self.mockSession.enqueueResponse(response: Response(body: body, statusCode: 200)) - let fetcher = self.createfetcher() + let body = String(format: jsonTemplate, "https://fakeUrl", "0") + MockHTTP.enqueueResponse(response: Response(body: body, statusCode: 200)) + let fetcher = createFetcher() // Act - _ = try fetcher.getConfiguration().get() + let expectation = expectation(description: "wait for response") + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertNotNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) // Assert - XCTAssertEqual(1, self.mockSession.requests.count) - XCTAssertTrue(self.mockSession.requests.last?.url?.absoluteString.starts(with: ConfigFetcher.globalBaseUrl) ?? false) + XCTAssertEqual(1, MockHTTP.requests.count) + XCTAssertTrue(MockHTTP.requests.last?.url?.absoluteString.starts(with: Constants.globalBaseUrl) ?? false) } func testShouldStayOnSameUrl() throws { // Arrange - let body = String(format: self.jsonTemplate, ConfigFetcher.globalBaseUrl, "1") - self.mockSession.enqueueResponse(response: Response(body: body, statusCode: 200)) - let fetcher = self.createfetcher() + let body = String(format: jsonTemplate, Constants.globalBaseUrl, "1") + MockHTTP.enqueueResponse(response: Response(body: body, statusCode: 200)) + let fetcher = createFetcher() // Act - _ = try fetcher.getConfiguration().get() + let expectation = expectation(description: "wait for response") + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertNotNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) // Assert - XCTAssertEqual(1, self.mockSession.requests.count) - XCTAssertTrue(self.mockSession.requests.last?.url?.absoluteString.starts(with: ConfigFetcher.globalBaseUrl) ?? false) + XCTAssertEqual(1, MockHTTP.requests.count) + XCTAssertTrue(MockHTTP.requests.last?.url?.absoluteString.starts(with: Constants.globalBaseUrl) ?? false) } func testShouldStayOnSameUrlEvenWithForce() throws { // Arrange - let body = String(format: self.jsonTemplate, ConfigFetcher.globalBaseUrl, "2") - self.mockSession.enqueueResponse(response: Response(body: body, statusCode: 200)) - let fetcher = self.createfetcher() + let body = String(format: jsonTemplate, Constants.globalBaseUrl, "2") + MockHTTP.enqueueResponse(response: Response(body: body, statusCode: 200)) + let fetcher = createFetcher() // Act - _ = try fetcher.getConfiguration().get() + let expectation = expectation(description: "wait for response") + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertNotNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) // Assert - XCTAssertEqual(1, self.mockSession.requests.count) - XCTAssertTrue(self.mockSession.requests.last?.url?.absoluteString.starts(with: ConfigFetcher.globalBaseUrl) ?? false) + XCTAssertEqual(1, MockHTTP.requests.count) + XCTAssertTrue(MockHTTP.requests.last?.url?.absoluteString.starts(with: Constants.globalBaseUrl) ?? false) } func testShouldRedirectToAnotherServer() throws { // Arrange - let firstBody = String(format: self.jsonTemplate, ConfigFetcher.euOnlyBaseUrl, "1") - let secondBody = String(format: self.jsonTemplate, ConfigFetcher.euOnlyBaseUrl, "0") - self.mockSession.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) - self.mockSession.enqueueResponse(response: Response(body: secondBody, statusCode: 200)) - let fetcher = self.createfetcher() + let firstBody = String(format: jsonTemplate, Constants.euOnlyBaseUrl, "1") + let secondBody = String(format: jsonTemplate, Constants.euOnlyBaseUrl, "0") + MockHTTP.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: secondBody, statusCode: 200)) + let fetcher = createFetcher() // Act - _ = try fetcher.getConfiguration().get() + let expectation = expectation(description: "wait for response") + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertNotNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) // Assert - XCTAssertEqual(2, self.mockSession.requests.count) - XCTAssertTrue(self.mockSession.requests[0].url?.absoluteString.starts(with: ConfigFetcher.globalBaseUrl) ?? false) - XCTAssertTrue(self.mockSession.requests[1].url?.absoluteString.starts(with: ConfigFetcher.euOnlyBaseUrl) ?? false) + XCTAssertEqual(2, MockHTTP.requests.count) + XCTAssertTrue(MockHTTP.requests[0].url?.absoluteString.starts(with: Constants.globalBaseUrl) ?? false) + XCTAssertTrue(MockHTTP.requests[1].url?.absoluteString.starts(with: Constants.euOnlyBaseUrl) ?? false) } func testShouldRedirectToAnotherServerWhenForced() throws { // Arrange - let firstBody = String(format: self.jsonTemplate, ConfigFetcher.euOnlyBaseUrl, "2") - let secondBody = String(format: self.jsonTemplate, ConfigFetcher.euOnlyBaseUrl, "0") - self.mockSession.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) - self.mockSession.enqueueResponse(response: Response(body: secondBody, statusCode: 200)) - let fetcher = self.createfetcher() + let firstBody = String(format: jsonTemplate, Constants.euOnlyBaseUrl, "2") + let secondBody = String(format: jsonTemplate, Constants.euOnlyBaseUrl, "0") + MockHTTP.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: secondBody, statusCode: 200)) + let fetcher = createFetcher() // Act - _ = try fetcher.getConfiguration().get() + let expectation = expectation(description: "wait for response") + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertNotNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) // Assert - XCTAssertEqual(2, self.mockSession.requests.count) - XCTAssertTrue(self.mockSession.requests[0].url?.absoluteString.starts(with: ConfigFetcher.globalBaseUrl) ?? false) - XCTAssertTrue(self.mockSession.requests[1].url?.absoluteString.starts(with: ConfigFetcher.euOnlyBaseUrl) ?? false) + XCTAssertEqual(2, MockHTTP.requests.count) + XCTAssertTrue(MockHTTP.requests[0].url?.absoluteString.starts(with: Constants.globalBaseUrl) ?? false) + XCTAssertTrue(MockHTTP.requests[1].url?.absoluteString.starts(with: Constants.euOnlyBaseUrl) ?? false) } func testShouldBreakRedirectLoop() throws { // Arrange - let firstBody = String(format: self.jsonTemplate, ConfigFetcher.euOnlyBaseUrl, "1") - let secondBody = String(format: self.jsonTemplate, ConfigFetcher.globalBaseUrl, "1") - self.mockSession.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) - self.mockSession.enqueueResponse(response: Response(body: secondBody, statusCode: 200)) - self.mockSession.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) - let fetcher = self.createfetcher() + let firstBody = String(format: jsonTemplate, Constants.euOnlyBaseUrl, "1") + let secondBody = String(format: jsonTemplate, Constants.globalBaseUrl, "1") + MockHTTP.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: secondBody, statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) + let fetcher = createFetcher() // Act - _ = try fetcher.getConfiguration().get() + let expectation = expectation(description: "wait for response") + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertNotNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) // Assert - XCTAssertEqual(3, self.mockSession.requests.count) - XCTAssertTrue(self.mockSession.requests[0].url?.absoluteString.starts(with: ConfigFetcher.globalBaseUrl) ?? false) - XCTAssertTrue(self.mockSession.requests[1].url?.absoluteString.starts(with: ConfigFetcher.euOnlyBaseUrl) ?? false) - XCTAssertTrue(self.mockSession.requests[2].url?.absoluteString.starts(with: ConfigFetcher.globalBaseUrl) ?? false) + XCTAssertEqual(3, MockHTTP.requests.count) + XCTAssertTrue(MockHTTP.requests[0].url?.absoluteString.starts(with: Constants.globalBaseUrl) ?? false) + XCTAssertTrue(MockHTTP.requests[1].url?.absoluteString.starts(with: Constants.euOnlyBaseUrl) ?? false) + XCTAssertTrue(MockHTTP.requests[2].url?.absoluteString.starts(with: Constants.globalBaseUrl) ?? false) } func testShouldBreakRedirectLoopWhenForced() throws { // Arrange - let firstBody = String(format: self.jsonTemplate, ConfigFetcher.euOnlyBaseUrl, "2") - let secondBody = String(format: self.jsonTemplate, ConfigFetcher.globalBaseUrl, "2") - self.mockSession.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) - self.mockSession.enqueueResponse(response: Response(body: secondBody, statusCode: 200)) - self.mockSession.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) - let fetcher = self.createfetcher() + let firstBody = String(format: jsonTemplate, Constants.euOnlyBaseUrl, "2") + let secondBody = String(format: jsonTemplate, Constants.globalBaseUrl, "2") + MockHTTP.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: secondBody, statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) + let fetcher = createFetcher() // Act - _ = try fetcher.getConfiguration().get() + let expectation = expectation(description: "wait for response") + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertNotNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) // Assert - XCTAssertEqual(3, self.mockSession.requests.count) - XCTAssertTrue(self.mockSession.requests[0].url?.absoluteString.starts(with: ConfigFetcher.globalBaseUrl) ?? false) - XCTAssertTrue(self.mockSession.requests[1].url?.absoluteString.starts(with: ConfigFetcher.euOnlyBaseUrl) ?? false) - XCTAssertTrue(self.mockSession.requests[2].url?.absoluteString.starts(with: ConfigFetcher.globalBaseUrl) ?? false) + XCTAssertEqual(3, MockHTTP.requests.count) + XCTAssertTrue(MockHTTP.requests[0].url?.absoluteString.starts(with: Constants.globalBaseUrl) ?? false) + XCTAssertTrue(MockHTTP.requests[1].url?.absoluteString.starts(with: Constants.euOnlyBaseUrl) ?? false) + XCTAssertTrue(MockHTTP.requests[2].url?.absoluteString.starts(with: Constants.globalBaseUrl) ?? false) } func testShouldRespectCustomUrlWhenNotForced() throws { // Arrange - let body = String(format: self.jsonTemplate, ConfigFetcher.globalBaseUrl, "1") - self.mockSession.enqueueResponse(response: Response(body: body, statusCode: 200)) - let fetcher = self.createfetcher(url: self.customCdnUrl) + let body = String(format: jsonTemplate, Constants.globalBaseUrl, "1") + MockHTTP.enqueueResponse(response: Response(body: body, statusCode: 200)) + let fetcher = createFetcher(url: customCdnUrl) // Act - _ = try fetcher.getConfiguration().get() + let expectation = expectation(description: "wait for response") + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertNotNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) // Assert - XCTAssertEqual(1, self.mockSession.requests.count) - XCTAssertTrue(self.mockSession.requests.last?.url?.absoluteString.starts(with: self.customCdnUrl) ?? false) + XCTAssertEqual(1, MockHTTP.requests.count) + XCTAssertTrue(MockHTTP.requests.last?.url?.absoluteString.starts(with: customCdnUrl) ?? false) } func testShouldNotRespectCustomUrlWhenForced() throws { // Arrange - let firstBody = String(format: self.jsonTemplate, ConfigFetcher.globalBaseUrl, "2") - let secondBody = String(format: self.jsonTemplate, ConfigFetcher.globalBaseUrl, "0") - self.mockSession.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) - self.mockSession.enqueueResponse(response: Response(body: secondBody, statusCode: 200)) - let fetcher = self.createfetcher(url: self.customCdnUrl) + let firstBody = String(format: jsonTemplate, Constants.globalBaseUrl, "2") + let secondBody = String(format: jsonTemplate, Constants.globalBaseUrl, "0") + MockHTTP.enqueueResponse(response: Response(body: firstBody, statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: secondBody, statusCode: 200)) + let fetcher = createFetcher(url: customCdnUrl) // Act - _ = try fetcher.getConfiguration().get() + let expectation = expectation(description: "wait for response") + fetcher.fetch(eTag: "") { response in + XCTAssertEqual(.fetched(.empty), response) + XCTAssertNotNil(response.entry) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) // Assert - XCTAssertEqual(2, self.mockSession.requests.count) - XCTAssertTrue(self.mockSession.requests[0].url?.absoluteString.starts(with: self.customCdnUrl) ?? false) - XCTAssertTrue(self.mockSession.requests[1].url?.absoluteString.starts(with: ConfigFetcher.globalBaseUrl) ?? false) + XCTAssertEqual(2, MockHTTP.requests.count) + XCTAssertTrue(MockHTTP.requests[0].url?.absoluteString.starts(with: customCdnUrl) ?? false) + XCTAssertTrue(MockHTTP.requests[1].url?.absoluteString.starts(with: Constants.globalBaseUrl) ?? false) } - private func createfetcher(url: String = "") -> ConfigFetcher { - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - return ConfigFetcher(session: self.mockSession, - logger: Logger.noLogger, - configJsonCache: configJsonCache, - sdkKey: "", - mode: "", - dataGovernance: DataGovernance.global, - baseUrl: url) + private func createFetcher(url: String = "") -> ConfigFetcher { + ConfigFetcher(session: MockHTTP.session(), + logger: Logger.noLogger, + sdkKey: "", + mode: "", + dataGovernance: DataGovernance.global, + baseUrl: url) } } diff --git a/Tests/ConfigCatTests/LazyLoadingAsyncTests.swift b/Tests/ConfigCatTests/LazyLoadingAsyncTests.swift deleted file mode 100755 index ff8406e..0000000 --- a/Tests/ConfigCatTests/LazyLoadingAsyncTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -import XCTest -@testable import ConfigCat - -class LazyLoadingAsyncTests: XCTestCase { - private var mockSession = MockURLSession() - private let testJsonFormat = #"{ "f": { "fakeKey": { "v": "%@", "p": [], "r": [] } } }"# - - override func setUp() { - super.setUp() - self.mockSession = MockURLSession() - } - - func testGet() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200, delay: 2)) - - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 5, useAsyncRefresh: true) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - - //wait for cache invalidation - sleep(6) - - //previous value returned until the new is not fetched - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - - //wait for refresh response - sleep(3) - - //new value is present - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - } - - func testGetFailedRefresh() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 500, delay: 2)) - - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 5, useAsyncRefresh: true) - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - - //wait for cache invalidation - sleep(6) - - //previous value returned until the new is not fetched - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - - //wait for refresh response - sleep(1) - - //new value is present - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - } -} diff --git a/Tests/ConfigCatTests/LazyLoadingSyncTests.swift b/Tests/ConfigCatTests/LazyLoadingSyncTests.swift deleted file mode 100755 index b247642..0000000 --- a/Tests/ConfigCatTests/LazyLoadingSyncTests.swift +++ /dev/null @@ -1,89 +0,0 @@ -import XCTest -@testable import ConfigCat - -class LazyLoadingSyncTests: XCTestCase { - private var mockSession = MockURLSession() - private let testJsonFormat = #"{ "f": { "fakeKey": { "v": "%@", "p": [], "r": [] } } }"# - - override func setUp() { - super.setUp() - self.mockSession = MockURLSession() - } - - func testGet() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200, delay: 2)) - - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) - let fetcher = ConfigFetcher(session: mockSession,logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - - //wait for cache invalidation - sleep(3) - - //next call will block until the new value is fetched - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - } - - func testGetFailedRefresh() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 500)) - - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) - let fetcher = ConfigFetcher(session: mockSession,logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - - //wait for cache invalidation - sleep(3) - - //next call will block until the new value is fetched - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - } - - func testCache() throws { - let mockCache = InMemoryConfigCache() - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200)) - - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) - let fetcher = ConfigFetcher(session: mockSession,logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, cache: mockCache, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - XCTAssertEqual(1, mockCache.store.count) - XCTAssertEqual(String(format: self.testJsonFormat, "test"), mockCache.store.values.first) - - //wait for cache invalidation - sleep(3) - - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - XCTAssertEqual(1, mockCache.store.count) - XCTAssertEqual(String(format: self.testJsonFormat, "test2"), mockCache.store.values.first) - } - - func testCacheFails() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200)) - - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) - let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) - let fetcher = ConfigFetcher(session: mockSession,logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, cache: FailingCache(), logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - - //wait for cache invalidation - sleep(3) - - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - } -} diff --git a/Tests/ConfigCatTests/LazyLoadingTests.swift b/Tests/ConfigCatTests/LazyLoadingTests.swift new file mode 100755 index 0000000..557a4ff --- /dev/null +++ b/Tests/ConfigCatTests/LazyLoadingTests.swift @@ -0,0 +1,140 @@ +import XCTest +@testable import ConfigCat + +class LazyLoadingTests: XCTestCase { + private let testJsonFormat = #"{ "f": { "fakeKey": { "v": "%@", "p": [], "r": [] } } }"# + + override func setUp() { + super.setUp() + MockHTTP.reset() + } + + func testGet() throws { + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200, delay: 2)) + + let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, sdkKey: "") + + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) + + let expectation2 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + wait(for: [expectation2], timeout: 2) + + XCTAssertEqual(1, MockHTTP.requests.count) + + //wait for cache invalidation + sleep(3) + + let expectation3 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test2", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation3.fulfill() + } + wait(for: [expectation3], timeout: 4) + } + + func testGetFailedRefresh() throws { + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 500)) + + let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, sdkKey: "") + + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) + + let expectation2 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + wait(for: [expectation2], timeout: 2) + + XCTAssertEqual(1, MockHTTP.requests.count) + + //wait for cache invalidation + sleep(3) + + let expectation3 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation3.fulfill() + } + wait(for: [expectation3], timeout: 2) + } + + func testCache() throws { + let mockCache = InMemoryConfigCache() + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) + + let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: mockCache, pollingMode: mode, sdkKey: "") + + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) + + XCTAssertEqual(1, mockCache.store.count) + XCTAssertEqual(String(format: testJsonFormat, "test"), mockCache.store.values.first) + + //wait for cache invalidation + sleep(3) + + let expectation2 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test2", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + wait(for: [expectation2], timeout: 2) + + XCTAssertEqual(1, mockCache.store.count) + XCTAssertEqual(String(format: testJsonFormat, "test2"), mockCache.store.values.first) + } + + func testCacheFails() throws { + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) + + let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, sdkKey: "") + + let expectation1 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) + + //wait for cache invalidation + sleep(3) + + let expectation2 = expectation(description: "wait for settings") + service.settings { settings in + XCTAssertEqual("test2", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + wait(for: [expectation2], timeout: 2) + } +} diff --git a/Tests/ConfigCatTests/LocalTests.swift b/Tests/ConfigCatTests/LocalTests.swift index 02bea5c..2100529 100755 --- a/Tests/ConfigCatTests/LocalTests.swift +++ b/Tests/ConfigCatTests/LocalTests.swift @@ -5,7 +5,7 @@ class LocalTests: XCTestCase { private let testJsonFormat = #"{ "f": { "fakeKey": { "v": %@, "p": [], "r": [] } } }"# func testDictionary() throws { - let dictionary:[String: Any] = [ + let dictionary: [String: Any] = [ "enabledFeature": true, "disabledFeature": false, "intSetting": 5, @@ -13,41 +13,51 @@ class LocalTests: XCTestCase { "stringSetting": "test" ] let client = ConfigCatClient(sdkKey: "testKey", flagOverrides: LocalDictionaryDataSource(source: dictionary, behaviour: .localOnly)) - - XCTAssertTrue(client.getValue(for: "enabledFeature", defaultValue: false)); - XCTAssertFalse(client.getValue(for: "disabledFeature", defaultValue: true)); - XCTAssertEqual(5, client.getValue(for: "intSetting", defaultValue: 0)); - XCTAssertEqual(3.14, client.getValue(for: "doubleSetting", defaultValue: 0.0)); - XCTAssertEqual("test", client.getValue(for: "stringSetting", defaultValue: "")); + let expectation = self.expectation(description: "wait for response") + client.getAllValues { values in + XCTAssertTrue(values["enabledFeature"] as? Bool ?? false) + XCTAssertFalse(values["disabledFeature"] as? Bool ?? true) + XCTAssertEqual(5, values["intSetting"] as? Int ?? 0) + XCTAssertEqual(3.14, values["doubleSetting"] as? Double ?? 3.14) + XCTAssertEqual("test", values["stringSetting"] as? String ?? "") + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) } func testLocalOverRemote() throws { - let mockSession = MockURLSession() - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "false"), statusCode: 200)) + MockHTTP.reset() + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "false"), statusCode: 200)) - let dictionary:[String: Any] = [ + let dictionary: [String: Any] = [ "fakeKey": true, "nonexisting": true ] - let client = ConfigCatClient(sdkKey: "testKey", refreshMode: PollingModes.manualPoll(), session: mockSession, flagOverrides: LocalDictionaryDataSource(source: dictionary, behaviour: .localOverRemote)) - client.refresh() - - XCTAssertTrue(client.getValue(for: "fakeKey", defaultValue: false)); - XCTAssertTrue(client.getValue(for: "nonexisting", defaultValue: false)); + let client = ConfigCatClient(sdkKey: "testKey", refreshMode: PollingModes.autoPoll(), session: MockHTTP.session(), flagOverrides: LocalDictionaryDataSource(source: dictionary, behaviour: .localOverRemote)) + let expectation = self.expectation(description: "wait for response") + client.getAllValues { values in + XCTAssertTrue(values["fakeKey"] as? Bool ?? false) + XCTAssertTrue(values["nonexisting"] as? Bool ?? false) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) } func testRemoteOverLocal() throws { - let mockSession = MockURLSession() - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "false"), statusCode: 200)) + MockHTTP.reset() + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "false"), statusCode: 200)) - let dictionary:[String: Any] = [ + let dictionary: [String: Any] = [ "fakeKey": true, "nonexisting": true ] - let client = ConfigCatClient(sdkKey: "testKey", refreshMode: PollingModes.manualPoll(), session: mockSession, flagOverrides: LocalDictionaryDataSource(source: dictionary, behaviour: .remoteOverLocal)) - client.refresh() - - XCTAssertFalse(client.getValue(for: "fakeKey", defaultValue: true)); - XCTAssertTrue(client.getValue(for: "nonexisting", defaultValue: false)); + let client = ConfigCatClient(sdkKey: "testKey", refreshMode: PollingModes.autoPoll(), session: MockHTTP.session(), flagOverrides: LocalDictionaryDataSource(source: dictionary, behaviour: .remoteOverLocal)) + let expectation = self.expectation(description: "wait for response") + client.getAllValues { values in + XCTAssertFalse(values["fakeKey"] as? Bool ?? true) + XCTAssertTrue(values["nonexisting"] as? Bool ?? false) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) } } diff --git a/Tests/ConfigCatTests/ManualPollingTests.swift b/Tests/ConfigCatTests/ManualPollingTests.swift index 042d8a5..1467e8a 100755 --- a/Tests/ConfigCatTests/ManualPollingTests.swift +++ b/Tests/ConfigCatTests/ManualPollingTests.swift @@ -2,73 +2,142 @@ import XCTest @testable import ConfigCat class ManualPollingTests: XCTestCase { - private var mockSession = MockURLSession() private let testJsonFormat = #"{ "f": { "fakeKey": { "v": "%@", "p": [], "r": [] } } }"# override func setUp() { super.setUp() - self.mockSession = MockURLSession() + MockHTTP.reset() } func testGet() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200, delay: 2)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200, delay: 2)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.manualPoll() - let fetcher = ConfigFetcher(session: mockSession,logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, sdkKey: "") - policy.refresh().wait() - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - policy.refresh().wait() - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + let expectation1 = self.expectation(description: "wait for response") + service.refresh { + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 2) + + let expectation2 = self.expectation(description: "wait for response") + service.refresh { + service.settings { settings in + XCTAssertEqual("test2", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + } + wait(for: [expectation2], timeout: 4) } func testGetFailedRefresh() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 500)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 500)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.manualPoll() - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - policy.refresh().wait() - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - policy.refresh().wait() - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, sdkKey: "") + + let expectation1 = self.expectation(description: "wait for response") + service.refresh { + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 2) + + let expectation2 = self.expectation(description: "wait for response") + service.refresh { + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + } + wait(for: [expectation2], timeout: 2) } func testCache() throws { let mockCache = InMemoryConfigCache() - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.manualPoll() - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, cache: mockCache, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - policy.refresh().wait() - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: mockCache, pollingMode: mode, sdkKey: "") + + let expectation1 = self.expectation(description: "wait for response") + service.refresh { + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 2) + XCTAssertEqual(1, mockCache.store.count) - XCTAssertEqual(String(format: self.testJsonFormat, "test"), mockCache.store.values.first) - policy.refresh().wait() - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + XCTAssertEqual(String(format: testJsonFormat, "test"), mockCache.store.values.first) + + let expectation2 = self.expectation(description: "wait for response") + service.refresh { + service.settings { settings in + XCTAssertEqual("test2", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + } + wait(for: [expectation2], timeout: 2) + XCTAssertEqual(1, mockCache.store.count) - XCTAssertEqual(String(format: self.testJsonFormat, "test2"), mockCache.store.values.first) + XCTAssertEqual(String(format: testJsonFormat, "test2"), mockCache.store.values.first) } func testCacheFails() throws { - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test"), statusCode: 200)) - mockSession.enqueueResponse(response: Response(body: String(format: self.testJsonFormat, "test2"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) - let configJsonCache = ConfigJsonCache(logger: Logger.noLogger) let mode = PollingModes.manualPoll() - let fetcher = ConfigFetcher(session: mockSession, logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "", mode: mode.getPollingIdentifier(), dataGovernance: DataGovernance.global) - let policy = mode.accept(visitor: RefreshPolicyFactory(fetcher: fetcher, cache: FailingCache(), logger: Logger.noLogger, configJsonCache: configJsonCache, sdkKey: "")) - policy.refresh().wait() - XCTAssertEqual("test", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) - policy.refresh().wait() - XCTAssertEqual("test2", (try policy.getConfiguration().get().entries["fakeKey"] as? [String: Any])?[Config.value] as? String) + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, sdkKey: "") + + let expectation1 = self.expectation(description: "wait for response") + service.refresh { + service.settings { settings in + XCTAssertEqual("test", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 2) + + let expectation2 = self.expectation(description: "wait for response") + service.refresh { + service.settings { settings in + XCTAssertEqual("test2", (settings["fakeKey"] as? [String: Any])?[Config.value] as? String) + expectation2.fulfill() + } + } + wait(for: [expectation2], timeout: 2) + } + + func testEmptyCacheDoesNotInitiateHTTP() throws { + MockHTTP.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) + + let mode = PollingModes.manualPoll() + let fetcher = ConfigFetcher(session: MockHTTP.session(), logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, sdkKey: "") + + let expectation1 = self.expectation(description: "wait for response") + service.settings { settings in + XCTAssertTrue(settings.isEmpty) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 2) + + XCTAssertEqual(0, MockHTTP.requests.count) } } diff --git a/Tests/ConfigCatTests/Mock.swift b/Tests/ConfigCatTests/Mock.swift index 710e54b..c0627bc 100755 --- a/Tests/ConfigCatTests/Mock.swift +++ b/Tests/ConfigCatTests/Mock.swift @@ -1,42 +1,70 @@ import Foundation @testable import ConfigCat -class MockURLSessionDataTask: URLSessionDataTask { - private let closure: () -> () - init(closure: @escaping () -> ()) { - self.closure = closure +class MockHTTP { + private static var responses = [Response]() + static var requests = [URLRequest]() + + static func enqueueResponse(response: Response) { + responses.append(response) + } + + static func next() -> Response { + responses.count == 1 ? responses[0] : responses.removeFirst() + } + + static func reset() { + responses.removeAll() + requests.removeAll() } - - override func resume() { - closure() + + static func session(config: URLSessionConfiguration = URLSessionConfiguration.default) -> URLSession { + config.protocolClasses = [MockURLProtocol.self] + return URLSession.init(configuration: config) } } -class MockURLSession: URLSession { - typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void - fileprivate var responses = [Response]() - fileprivate let queue = DispatchQueue(label: "testQueue") - - var requests = [URLRequest]() - - func enqueueResponse(response: Response) { - self.responses.append(response) +class MockURLProtocol: URLProtocol { + private var requestJob: DispatchWorkItem? + + override class func canInit(with request: URLRequest) -> Bool { + true } - - override func dataTask( - with request: URLRequest, - completionHandler: @escaping CompletionHandler - ) -> URLSessionDataTask { - self.requests.append(request) - let response = self.responses.count == 1 ? self.responses[0] : self.responses.removeFirst() - let semaphore = DispatchSemaphore(value: 0) - return MockURLSessionDataTask { - self.queue.async { - if response.delay > 0 { - let _ = semaphore.wait(timeout: DispatchTime.now() + DispatchTimeInterval.seconds(response.delay)) - } - completionHandler(response.data, response.httpResponse, response.error) + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + override func startLoading() { + MockHTTP.requests.append(request) + let response = MockHTTP.next() + if response.delay <= 0 { + finish(response: response) + return + } + + requestJob = DispatchWorkItem(block: { [weak self] in + guard let self = self else { + return } + self.finish(response: response) + }) + + DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated) + .asyncAfter(deadline: .now() + .seconds(response.delay), execute: requestJob!) + } + + override func stopLoading() { + requestJob?.cancel() + } + + private func finish(response: Response) { + if let error = response.error { + client?.urlProtocol(self, didFailWithError: error) + } else { + client?.urlProtocol(self, didReceive: response.httpResponse, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: response.data ?? Data()) + client?.urlProtocolDidFinishLoading(self) } } } @@ -46,37 +74,37 @@ struct Response { let httpResponse: HTTPURLResponse let error: Error? let delay: Int - + init(body: String, statusCode: Int, error: Error? = nil, headers: [String: String]? = nil, delay: Int = 0) { - self.data = body.data(using: .utf8) - self.httpResponse = HTTPURLResponse(url: URL(string: "url")!, statusCode: statusCode, httpVersion: nil, headerFields: headers)! + data = body.data(using: .utf8) + httpResponse = HTTPURLResponse(url: URL(string: "url")!, statusCode: statusCode, httpVersion: nil, headerFields: headers)! self.error = error self.delay = delay } } -enum TestError : Error { +enum TestError: Error { case test } -public class FailingCache : ConfigCache { +public class FailingCache: ConfigCache { public func read(for key: String) throws -> String { throw TestError.test } - + public func write(for key: String, value: String) throws { throw TestError.test } } -public class InMemoryConfigCache : NSObject, ConfigCache { +public class InMemoryConfigCache: NSObject, ConfigCache { public var store = [String: String]() public func read(for key: String) throws -> String { - return self.store[key] ?? "" + store[key] ?? "" } public func write(for key: String, value: String) throws { - self.store[key] = value + store[key] = value } } diff --git a/Tests/ConfigCatTests/Resources/Info.plist b/Tests/ConfigCatTests/Resources/Info.plist index 6c40a6c..9055281 100755 --- a/Tests/ConfigCatTests/Resources/Info.plist +++ b/Tests/ConfigCatTests/Resources/Info.plist @@ -1,22 +1,22 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + diff --git a/Tests/ConfigCatTests/RolloutIntegrationTests.swift b/Tests/ConfigCatTests/RolloutIntegrationTests.swift index 5a5b6b1..6b552b0 100644 --- a/Tests/ConfigCatTests/RolloutIntegrationTests.swift +++ b/Tests/ConfigCatTests/RolloutIntegrationTests.swift @@ -9,12 +9,12 @@ class RolloutIntegrationTests: XCTestCase { lazy var testBundle: Bundle = { #if SWIFT_PACKAGE - return Bundle.module + return Bundle.module #else - return Bundle(for: type(of: self)) + return Bundle(for: type(of: self)) #endif }() - + func testRolloutMatrixText() throws { if let url = testBundle.url(forResource: "testmatrix", withExtension: "csv") { try testRolloutMatrix(url: url, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", type: .value) @@ -30,7 +30,7 @@ class RolloutIntegrationTests: XCTestCase { XCTFail() } } - + func testRolloutMatrixSemantic2() throws { if let url = testBundle.url(forResource: "testmatrix_semantic_2", withExtension: "csv") { try testRolloutMatrix(url: url, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w", type: .value) @@ -65,121 +65,123 @@ class RolloutIntegrationTests: XCTestCase { func testRolloutMatrix(url: URL, sdkKey: String, type: TestType) throws { let client: ConfigCatClient = ConfigCatClient(sdkKey: sdkKey) - + guard let matrixData = try? Data(contentsOf: url), let content = String(bytes: matrixData, encoding: .utf8) else { XCTFail() return } - + let rows = content.components(separatedBy: "\n") - .map{ row in row.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)} - + .map { row in + row.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } + let header = rows[0].components(separatedBy: ";") - + let customKey = header[3] - + let settingKeys = header - .map{ key in key.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)} - .skip(count: 4) - + .map { key in + key.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } + .skip(count: 4) + var errors: [String] = [] - for i in 1.. [Element] { return [Element](self[count.. [Element] { + [Element](self[count..() - client.refresh() - client.getVariationIdAsync(for: "key2", defaultVariationId: nil) { (result) in - variationId.complete(result: result) + MockHTTP.enqueueResponse(response: Response(body: testJson, statusCode: 200)) + let client = createClient() + + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getVariationId(for: "key1", defaultVariationId: nil) { variationId in + XCTAssertEqual("fakeId1", variationId) + expectation.fulfill() + } } - - XCTAssertEqual("fakeId2", try variationId.get()) + wait(for: [expectation], timeout: 2) } func testGetVariationIdNotFound() { - mockSession.enqueueResponse(response: Response(body: self.testJson, statusCode: 200)) - let client = self.createClient() - client.refresh() - let variationId = client.getVariationId(for: "nonexisting", defaultVariationId: "defaultId") - - XCTAssertEqual("defaultId", variationId) + MockHTTP.enqueueResponse(response: Response(body: testJson, statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getVariationId(for: "nonexisting", defaultVariationId: "def") { variationId in + XCTAssertEqual("def", variationId) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) } func testGetAllVariationIds() { - mockSession.enqueueResponse(response: Response(body: self.testJson, statusCode: 200)) - let client = self.createClient() - client.refresh() - let variationIds = client.getAllVariationIds() - - XCTAssertEqual(2, variationIds.count) - XCTAssertTrue(variationIds.contains("fakeId1")) - XCTAssertTrue(variationIds.contains("fakeId2")) + MockHTTP.enqueueResponse(response: Response(body: testJson, statusCode: 200)) + let client = createClient() + + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getAllVariationIds { variationIds in + XCTAssertEqual(2, variationIds.count) + XCTAssertTrue(variationIds.contains("fakeId1")) + XCTAssertTrue(variationIds.contains("fakeId2")) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) } func testGetAllVariationIdsEmpty() { - mockSession.enqueueResponse(response: Response(body: "{}", statusCode: 200)) - let client = self.createClient() - client.refresh() - let variationIds = client.getAllVariationIds() - - XCTAssertEqual(0, variationIds.count) - } - - func testGetAllVariationIdsAsync() throws { - mockSession.enqueueResponse(response: Response(body: self.testJson, statusCode: 200)) - let client = self.createClient() - let variationIdsResult = AsyncResult<[String]>() - client.refresh() - client.getAllVariationIdsAsync() { (result) in - variationIdsResult.complete(result: result) + MockHTTP.enqueueResponse(response: Response(body: "{}", statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getAllVariationIds { variationIds in + XCTAssertEqual(0, variationIds.count) + expectation.fulfill() + } } - - let variationIds = try variationIdsResult.get() - XCTAssertEqual(2, variationIds.count) - XCTAssertTrue(variationIds.contains("fakeId1")) - XCTAssertTrue(variationIds.contains("fakeId2")) + wait(for: [expectation], timeout: 2) } func testGetKeyAndValue() { - mockSession.enqueueResponse(response: Response(body: self.testJson, statusCode: 200)) - let client = self.createClient() - client.refresh() - if let result = client.getKeyAndValue(for: "fakeId2") { - XCTAssertEqual("key2", result.key); - XCTAssertFalse(result.value as! Bool); - } else { - XCTFail() - } - - if let result = client.getKeyAndValue(for: "percentageId2") { - XCTAssertEqual("key1", result.key); - XCTAssertFalse(result.value as! Bool); - } else { - XCTFail() - } - - if let result = client.getKeyAndValue(for: "rolloutId2") { - XCTAssertEqual("key1", result.key); - XCTAssertFalse(result.value as! Bool); - } else { - XCTFail() - } - } - - func testGetKeyAndValueAsync() throws { - mockSession.enqueueResponse(response: Response(body: self.testJson, statusCode: 200)) - let client = self.createClient() - let keyValueResult = AsyncResult() - client.refresh() - client.getKeyAndValueAsync(for: "fakeId1") { (result) in - if let result = result { - keyValueResult.complete(result: result) - } else { - XCTFail() + MockHTTP.enqueueResponse(response: Response(body: testJson, statusCode: 200)) + let client = createClient() + + let expectation1 = self.expectation(description: "wait for response") + let expectation2 = self.expectation(description: "wait for response") + let expectation3 = self.expectation(description: "wait for response") + client.refresh { + client.getKeyAndValue(for: "fakeId2") { kv in + if let result = kv { + XCTAssertEqual("key2", result.key) + XCTAssertFalse(result.value as! Bool) + } else { + XCTFail() + } + expectation1.fulfill() + } + client.getKeyAndValue(for: "percentageId2") { kv in + if let result = kv { + XCTAssertEqual("key1", result.key) + XCTAssertFalse(result.value as! Bool) + } else { + XCTFail() + } + expectation2.fulfill() + } + client.getKeyAndValue(for: "rolloutId2") { kv in + if let result = kv { + XCTAssertEqual("key1", result.key) + XCTAssertFalse(result.value as! Bool) + } else { + XCTFail() + } + expectation3.fulfill() } } - - let keyValue = try keyValueResult.get() - XCTAssertEqual("key1", keyValue.key); - XCTAssertTrue(keyValue.value as! Bool); + wait(for: [expectation1, expectation2, expectation3], timeout: 2) } func testGetKeyAndValueNotFound() { - mockSession.enqueueResponse(response: Response(body: "{}", statusCode: 200)) - let client = self.createClient() - client.refresh() - let result = client.getKeyAndValue(for: "nonexisting") - XCTAssertNil(result) + MockHTTP.enqueueResponse(response: Response(body: "{}", statusCode: 200)) + let client = createClient() + let expectation = self.expectation(description: "wait for response") + client.refresh { + client.getKeyAndValue(for: "nonexisting") { result in + XCTAssertNil(result) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 2) } private func createClient() -> ConfigCatClient { - return ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.manualPoll(), session: self.mockSession) + ConfigCatClient(sdkKey: "test", refreshMode: PollingModes.manualPoll(), session: MockHTTP.session()) } }