From 6628d309ede0b86fe5cebbece70b8e62fbfa7bf2 Mon Sep 17 00:00:00 2001 From: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Date: Fri, 24 Apr 2020 14:08:51 -0700 Subject: [PATCH 01/50] Add xcode compatibility information to readme (#93) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05dd0ae3..94512d90 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ LaunchDarkly overview [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) -Supported iOS versions +Supported iOS and Xcode versions ------------------------- -This version of the LaunchDarkly SDK has been tested with iOS 12 and across mobile, desktop, watch, and tv devices. +This version of the LaunchDarkly SDK has been tested with iOS 12 and across mobile, desktop, watch, and tv devices. The SDK is built with Xcode 11.4. Getting started ----------- From 7deb14a32cb2bd64d6a284baed793349410d8b01 Mon Sep 17 00:00:00 2001 From: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Date: Wed, 3 Feb 2021 15:11:15 -0800 Subject: [PATCH 02/50] Removed the guides link --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 8a4270ba..7f47a9e7 100644 --- a/README.md +++ b/README.md @@ -106,4 +106,3 @@ About LaunchDarkly * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates - * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies From 503cb8ebc95308f0613804e8cad02efac5c9d13c Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 11 Jun 2021 11:44:12 -0700 Subject: [PATCH 03/50] [ch110317] Replace `class` with equivalent `AnyObject` as the prior is deprecated. (#154) --- LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 08a849c1..4d690e4c 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -12,14 +12,14 @@ typealias ServiceResponse = (data: Data?, urlResponse: URLResponse?, error: Erro typealias ServiceCompletionHandler = (ServiceResponse) -> Void //sourcery: autoMockable -protocol DarklyStreamingProvider: class { +protocol DarklyStreamingProvider: AnyObject { func start() func stop() } extension EventSource: DarklyStreamingProvider {} -protocol DarklyServiceProvider: class { +protocol DarklyServiceProvider: AnyObject { var config: LDConfig { get } var user: LDUser { get set } var diagnosticCache: DiagnosticCaching? { get } From 2d9acce46fdc0f1a34d2c8092a5cff7392f0dde2 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 17 Jun 2021 12:49:16 -0700 Subject: [PATCH 04/50] Update dependencies. (#155) --- LaunchDarkly.xcodeproj/project.pbxproj | 6 +++--- .../xcshareddata/swiftpm/Package.resolved | 16 ++++++++-------- Package.resolved | 16 ++++++++-------- Package.swift | 6 +++--- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 8d943539..748057b6 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -1982,7 +1982,7 @@ repositoryURL = "https://github.com/AliSoftware/OHHTTPStubs.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 9.0.0; + minimumVersion = 9.1.0; }; }; B4903D9924BD61D000F087C4 /* XCRemoteSwiftPackageReference "Nimble" */ = { @@ -1990,7 +1990,7 @@ repositoryURL = "https://github.com/Quick/Nimble.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 9.0.0; + minimumVersion = 9.2.0; }; }; B4903D9C24BD61EF00F087C4 /* XCRemoteSwiftPackageReference "Quick" */ = { @@ -1998,7 +1998,7 @@ repositoryURL = "https://github.com/Quick/Quick.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 3.0.0; + minimumVersion = 3.1.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2c1ddc0b..d087905f 100644 --- a/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", "state": { "branch": null, - "revision": "f809deb30dc5c9d9b78c872e553261a61177721a", - "version": "2.0.0" + "revision": "682841464136f8c66e04afe5dbd01ab51a3a56f2", + "version": "2.1.0" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/Quick/Nimble.git", "state": { "branch": null, - "revision": "e491a6731307bb23783bf664d003be9b2fa59ab5", - "version": "9.0.0" + "revision": "af1730dde4e6c0d45bf01b99f8a41713ce536790", + "version": "9.2.0" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git", "state": { "branch": null, - "revision": "e92b5a5746ef16add2a1424f1fc19529d9a75cde", - "version": "9.0.0" + "revision": "12f19662426d0434d6c330c6974d53e2eb10ecd9", + "version": "9.1.0" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/Quick/Quick.git", "state": { "branch": null, - "revision": "71c90eda2ab14b2110b22222d4b373163126561c", - "version": "3.0.1" + "revision": "8cce6acd38f965f5baa3167b939f86500314022b", + "version": "3.1.2" } }, { diff --git a/Package.resolved b/Package.resolved index 2c1ddc0b..d087905f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", "state": { "branch": null, - "revision": "f809deb30dc5c9d9b78c872e553261a61177721a", - "version": "2.0.0" + "revision": "682841464136f8c66e04afe5dbd01ab51a3a56f2", + "version": "2.1.0" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/Quick/Nimble.git", "state": { "branch": null, - "revision": "e491a6731307bb23783bf664d003be9b2fa59ab5", - "version": "9.0.0" + "revision": "af1730dde4e6c0d45bf01b99f8a41713ce536790", + "version": "9.2.0" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git", "state": { "branch": null, - "revision": "e92b5a5746ef16add2a1424f1fc19529d9a75cde", - "version": "9.0.0" + "revision": "12f19662426d0434d6c330c6974d53e2eb10ecd9", + "version": "9.1.0" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/Quick/Quick.git", "state": { "branch": null, - "revision": "71c90eda2ab14b2110b22222d4b373163126561c", - "version": "3.0.1" + "revision": "8cce6acd38f965f5baa3167b939f86500314022b", + "version": "3.1.2" } }, { diff --git a/Package.swift b/Package.swift index 4c91d43e..af4bdcdb 100644 --- a/Package.swift +++ b/Package.swift @@ -16,9 +16,9 @@ let package = Package( targets: ["LaunchDarkly"]), ], dependencies: [ - .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", .upToNextMinor(from: "9.0.0")), - .package(url: "https://github.com/Quick/Quick.git", .upToNextMinor(from: "3.0.0")), - .package(url: "https://github.com/Quick/Nimble.git", .upToNextMinor(from: "9.0.0")), + .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", .upToNextMinor(from: "9.1.0")), + .package(url: "https://github.com/Quick/Quick.git", .upToNextMinor(from: "3.1.0")), + .package(url: "https://github.com/Quick/Nimble.git", .upToNextMinor(from: "9.2.0")), .package(url: "https://github.com/LaunchDarkly/swift-eventsource.git", .upToNextMinor(from: "1.2.1")) ], targets: [ From bb22146999a62de85d637c91f45e8a291adea74a Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 17 Jun 2021 16:19:49 -0700 Subject: [PATCH 05/50] Restore compatibility with Swift 5.2 (#156) Parameterize CI against multiple Xcode versions. Pin to exact dependency versions and remove resolved package files. --- .circleci/config.yml | 91 ++++++++++++++----- .gitignore | 3 + Cartfile | 2 +- LaunchDarkly.xcodeproj/project.pbxproj | 16 ++-- .../xcshareddata/swiftpm/Package.resolved | 61 ------------- .../Networking/DarklyServiceSpec.swift | 4 +- .../Cache/UserEnvironmentFlagCacheSpec.swift | 6 +- Package.resolved | 61 ------------- Package.swift | 8 +- README.md | 11 ++- 10 files changed, 99 insertions(+), 164 deletions(-) delete mode 100644 LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 Package.resolved diff --git a/.circleci/config.yml b/.circleci/config.yml index a3608a4d..f036abc7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,10 +1,25 @@ version: 2.1 jobs: build: + parameters: + xcode-version: + type: string + ios-sim: + type: string + ssh-fix: + type: boolean + default: true + build-doc: + type: boolean + default: false + run-lint: + type: boolean + default: false + shell: /bin/bash --login -eo pipefail macos: - xcode: '12.0.0' + xcode: <> steps: - checkout @@ -12,13 +27,16 @@ jobs: # This hack shouldn't be necessary, as we don't actually use SSH # to get any dependencies, but for some reason starting in the # '12.0.0' Xcode image it's become necessary. - - run: - name: SSH fingerprint fix - command: | - sudo defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM YES - rm ~/.ssh/id_rsa || true - for ip in $(dig @8.8.8.8 bitbucket.org +short); do ssh-keyscan bitbucket.org,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts || true - for ip in $(dig @8.8.8.8 github.com +short); do ssh-keyscan github.com,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts || true + - when: + condition: <> + steps: + - run: + name: SSH fingerprint fix + command: | + sudo defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM YES + rm ~/.ssh/id_rsa || true + for ip in $(dig @8.8.8.8 bitbucket.org +short); do ssh-keyscan bitbucket.org,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts || true + for ip in $(dig @8.8.8.8 github.com +short); do ssh-keyscan github.com,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts || true - run: name: Setup for builds @@ -38,7 +56,7 @@ jobs: - run: name: Build & Test on iOS Simulator - command: xcodebuild test -scheme 'LaunchDarkly_iOS' -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 11 Pro Max' CODE_SIGN_IDENTITY= | tee 'artifacts/raw-logs-iphonesimulator.txt' | xcpretty -r junit -o 'test-results/platform-iphonesimulator/junit.xml' + command: xcodebuild test -scheme 'LaunchDarkly_iOS' -sdk iphonesimulator -destination '<>' CODE_SIGN_IDENTITY= | tee 'artifacts/raw-logs-iphonesimulator.txt' | xcpretty -r junit -o 'test-results/platform-iphonesimulator/junit.xml' when: always - run: @@ -66,23 +84,50 @@ jobs: command: swift test -v 2>&1 | tee 'artifacts/raw-logs-swiftpm.txt' | xcpretty -r junit -o 'test-results/swiftpm/junit.xml' when: always - - run: - name: Build Documentation - command: | - sudo gem install jazzy - jazzy -o artifacts/docs - - - run: - name: CocoaPods spec lint - command: | - if [ "$CIRCLE_BRANCH" = 'master' ]; then - pod spec lint - else - pod lib lint - fi + - when: + condition: <> + steps: + - run: + name: Build Documentation + command: | + sudo gem install jazzy + jazzy -o artifacts/docs + + - when: + condition: <> + steps: + - run: + name: CocoaPods spec lint + command: | + if [ "$CIRCLE_BRANCH" = 'master' ]; then + pod spec lint + else + pod lib lint + fi - store_test_results: path: test-results - store_artifacts: path: artifacts + +workflows: + version: 2 + + build: + jobs: + - build: + name: Xcode 12.5 - Swift 5.4 + xcode-version: '12.5.0' + ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=14.5' + build-doc: true + run-lint: true + - build: + name: Xcode 12.0 - Swift 5.3 + xcode-version: '12.0.1' + ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=14.0' + - build: + name: Xcode 11.4 - Swift 5.2 + xcode-version: '11.4.1' + ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=12.2' + ssh-fix: false diff --git a/.gitignore b/.gitignore index 70a5a672..ff647b61 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ xcuserdata /docs /Carthage/Checkouts /.swiftpm +/Package.resolved +/LaunchDarkly.xcworkspace/xcshareddata/swiftpm/Package.resolved +/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved \ No newline at end of file diff --git a/Cartfile b/Cartfile index b4551527..f234102f 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "launchdarkly/swift-eventsource" ~> 1.2.1 \ No newline at end of file +github "launchdarkly/swift-eventsource" == 1.2.1 \ No newline at end of file diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 748057b6..d6b0f7d4 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -1973,32 +1973,32 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/LaunchDarkly/swift-eventsource.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 1.2.1; + kind = exactVersion; + version = 1.2.1; }; }; B4903D9624BD61B200F087C4 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/AliSoftware/OHHTTPStubs.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 9.1.0; + kind = exactVersion; + version = 9.1.0; }; }; B4903D9924BD61D000F087C4 /* XCRemoteSwiftPackageReference "Nimble" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Quick/Nimble.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 9.2.0; + kind = exactVersion; + version = 9.2.0; }; }; B4903D9C24BD61EF00F087C4 /* XCRemoteSwiftPackageReference "Quick" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Quick/Quick.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 3.1.0; + kind = exactVersion; + version = 3.1.2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index d087905f..00000000 --- a/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,61 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "CwlCatchException", - "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", - "state": { - "branch": null, - "revision": "682841464136f8c66e04afe5dbd01ab51a3a56f2", - "version": "2.1.0" - } - }, - { - "package": "CwlPreconditionTesting", - "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", - "state": { - "branch": null, - "revision": "02b7a39a99c4da27abe03cab2053a9034379639f", - "version": "2.0.0" - } - }, - { - "package": "Nimble", - "repositoryURL": "https://github.com/Quick/Nimble.git", - "state": { - "branch": null, - "revision": "af1730dde4e6c0d45bf01b99f8a41713ce536790", - "version": "9.2.0" - } - }, - { - "package": "OHHTTPStubs", - "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git", - "state": { - "branch": null, - "revision": "12f19662426d0434d6c330c6974d53e2eb10ecd9", - "version": "9.1.0" - } - }, - { - "package": "Quick", - "repositoryURL": "https://github.com/Quick/Quick.git", - "state": { - "branch": null, - "revision": "8cce6acd38f965f5baa3167b939f86500314022b", - "version": "3.1.2" - } - }, - { - "package": "LDSwiftEventSource", - "repositoryURL": "https://github.com/LaunchDarkly/swift-eventsource.git", - "state": { - "branch": null, - "revision": "7c40adad054c9737afadffe42a2ce0bbcfa02f48", - "version": "1.2.1" - } - } - ] - }, - "version": 1 -} diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index 12502128..e5589dc4 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -209,7 +209,7 @@ final class DarklyServiceSpec: QuickSpec { } else { fail("request path is missing") } - expect(urlRequest?.cachePolicy) == .reloadRevalidatingCacheData + expect([.reloadIgnoringLocalCacheData, .reloadRevalidatingCacheData]).to(contain(urlRequest?.cachePolicy)) expect(urlRequest?.timeoutInterval) == testContext.config.connectionTimeout expect(urlRequest?.httpMethod) == URLRequest.HTTPMethods.get expect(urlRequest?.httpBody).to(beNil()) @@ -382,7 +382,7 @@ final class DarklyServiceSpec: QuickSpec { } else { fail("request path is missing") } - expect(urlRequest?.cachePolicy) == .reloadRevalidatingCacheData + expect([.reloadIgnoringLocalCacheData, .reloadRevalidatingCacheData]).to(contain(urlRequest?.cachePolicy)) expect(urlRequest?.timeoutInterval) == testContext.config.connectionTimeout expect(urlRequest?.httpMethod) == URLRequest.HTTPMethods.report expect(urlRequest?.httpBodyStream).toNot(beNil()) //Although the service sets the httpBody, OHHTTPStubs seems to convert that into an InputStream, which should be ok diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift index fe04e9ec..19654032 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift @@ -71,10 +71,10 @@ final class UserEnvironmentFlagCacheSpec: QuickSpec { mobileKey: String, lastUpdated: Date) { waitUntil { done in - self.subject.storeFeatureFlags(featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated, storeMode: storeMode, completion: done) - if storeMode == .sync { done() } + self.subject.storeFeatureFlags(featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated, storeMode: self.storeMode, completion: done) + if self.storeMode == .sync { done() } } - expect(keyedValueCacheMock.setReceivedArguments?.forKey) == UserEnvironmentFlagCache.CacheKeys.cachedUserEnvironmentFlags + expect(self.keyedValueCacheMock.setReceivedArguments?.forKey) == UserEnvironmentFlagCache.CacheKeys.cachedUserEnvironmentFlags } } diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index d087905f..00000000 --- a/Package.resolved +++ /dev/null @@ -1,61 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "CwlCatchException", - "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", - "state": { - "branch": null, - "revision": "682841464136f8c66e04afe5dbd01ab51a3a56f2", - "version": "2.1.0" - } - }, - { - "package": "CwlPreconditionTesting", - "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", - "state": { - "branch": null, - "revision": "02b7a39a99c4da27abe03cab2053a9034379639f", - "version": "2.0.0" - } - }, - { - "package": "Nimble", - "repositoryURL": "https://github.com/Quick/Nimble.git", - "state": { - "branch": null, - "revision": "af1730dde4e6c0d45bf01b99f8a41713ce536790", - "version": "9.2.0" - } - }, - { - "package": "OHHTTPStubs", - "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git", - "state": { - "branch": null, - "revision": "12f19662426d0434d6c330c6974d53e2eb10ecd9", - "version": "9.1.0" - } - }, - { - "package": "Quick", - "repositoryURL": "https://github.com/Quick/Quick.git", - "state": { - "branch": null, - "revision": "8cce6acd38f965f5baa3167b939f86500314022b", - "version": "3.1.2" - } - }, - { - "package": "LDSwiftEventSource", - "repositoryURL": "https://github.com/LaunchDarkly/swift-eventsource.git", - "state": { - "branch": null, - "revision": "7c40adad054c9737afadffe42a2ce0bbcfa02f48", - "version": "1.2.1" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index aeb71b4d..223e25f2 100644 --- a/Package.swift +++ b/Package.swift @@ -16,10 +16,10 @@ let package = Package( targets: ["LaunchDarkly"]), ], dependencies: [ - .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", .upToNextMinor(from: "9.1.0")), - .package(url: "https://github.com/Quick/Quick.git", .upToNextMinor(from: "3.1.0")), - .package(url: "https://github.com/Quick/Nimble.git", .upToNextMinor(from: "9.2.0")), - .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .upToNextMinor(from: "1.2.1")), + .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", .exact("9.1.0")), + .package(url: "https://github.com/Quick/Quick.git", .exact("3.1.2")), + .package(url: "https://github.com/Quick/Nimble.git", .exact("9.2.0")), + .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("1.2.1")) ], targets: [ .target( diff --git a/README.md b/README.md index 6b45ff5f..bb15afe9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,16 @@ LaunchDarkly overview Supported iOS and Xcode versions ------------------------- -This version of the LaunchDarkly SDK has been tested with iOS 13.5 and across mobile, desktop, watch, and tv devices. The SDK is built with Xcode 12.0. The minimum platform versions are: +This version of the LaunchDarkly SDK has been tested across iOS, macOS, watchOS, and tvOS devices. + +The LaunchDarkly iOS SDK requires the following minimum build tool versions: + +| Tool | Version | +| ----- | ------- | +| Xcode | 11.4+ | +| Swift | 5.2+ | + +And supports the following device platforms: | Platform | Version | | -------- | ------- | From b9d04c606bbcfb58c952e0f5420b5501d2511a04 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Tue, 22 Jun 2021 08:21:53 -0700 Subject: [PATCH 06/50] Update Sourcery and SwiftLint. (#157) Add spaces in comments for new comment spacing lint rule. --- .../GeneratedCode/mocks.generated.swift | 3 +- .../LaunchDarkly/Extensions/AnyComparer.swift | 4 +- .../Extensions/DateFormatter.swift | 2 +- LaunchDarkly/LaunchDarkly/LDClient.swift | 8 +- LaunchDarkly/LaunchDarkly/LDCommon.swift | 14 +- .../Cache/CacheableEnvironmentFlags.swift | 2 +- .../Cache/CacheableUserEnvironmentFlags.swift | 12 +- .../Models/ConnectionInformation.swift | 2 +- LaunchDarkly/LaunchDarkly/Models/Event.swift | 4 +- .../Models/FeatureFlag/FeatureFlag.swift | 4 +- .../FlagChange/LDChangedFlag.swift | 6 +- .../FlagValue/LDFlagBaseTypeConvertible.swift | 4 +- .../FeatureFlag/FlagValue/LDFlagValue.swift | 144 +++++++++--------- .../FlagValue/LDFlagValueConvertible.swift | 30 ++-- .../LaunchDarkly/Models/LDConfig.swift | 24 ++- .../LaunchDarkly/Models/User/LDUser.swift | 58 +++---- .../Networking/DarklyService.swift | 10 +- .../Networking/HTTPURLResponse.swift | 2 +- .../ObjectiveC/ObjcLDChangedFlag.swift | 52 +++---- .../ObjectiveC/ObjcLDConfig.swift | 12 +- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 48 +++--- .../ServiceObjects/Cache/CacheConverter.swift | 6 +- .../Cache/DeprecatedCache.swift | 8 +- .../Cache/DeprecatedCacheModelV2.swift | 2 +- .../Cache/DeprecatedCacheModelV3.swift | 2 +- .../Cache/DeprecatedCacheModelV4.swift | 2 +- .../Cache/DeprecatedCacheModelV5.swift | 2 +- .../Cache/DiagnosticCache.swift | 4 +- .../Cache/KeyedValueCache.swift | 4 +- .../Cache/UserEnvironmentFlagCache.swift | 6 +- .../ServiceObjects/CwlSysctl.swift | 4 +- .../ServiceObjects/DiagnosticReporter.swift | 2 +- .../ServiceObjects/EnvironmentReporter.swift | 32 ++-- .../ServiceObjects/ErrorNotifier.swift | 2 +- .../ServiceObjects/EventReporter.swift | 8 +- .../ServiceObjects/FlagChangeNotifier.swift | 8 +- .../ServiceObjects/FlagStore.swift | 2 +- .../ServiceObjects/FlagSynchronizer.swift | 20 +-- .../ServiceObjects/NetworkReporter.swift | 2 +- .../ServiceObjects/Throttler.swift | 2 +- .../Extensions/AnyComparerSpec.swift | 2 +- .../LaunchDarklyTests/LDClientSpec.swift | 28 ++-- .../Mocks/DarklyServiceMock.swift | 16 +- .../LaunchDarklyTests/Models/EventSpec.swift | 16 +- .../Models/FeatureFlag/FeatureFlagSpec.swift | 4 +- .../Models/LDConfigSpec.swift | 2 +- .../Models/User/LDUserSpec.swift | 32 ++-- .../Networking/DarklyServiceSpec.swift | 28 ++-- .../Networking/URLCacheSpec.swift | 4 +- .../EnvironmentReporterSpec.swift | 4 +- .../ServiceObjects/EventReporterSpec.swift | 16 +- .../FlagChangeNotifierSpec.swift | 16 +- .../ServiceObjects/FlagSynchronizerSpec.swift | 14 +- .../ServiceObjects/LDTimerSpec.swift | 22 +-- Mintfile | 4 +- 55 files changed, 384 insertions(+), 387 deletions(-) diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index b2d3c819..d049923a 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -1,7 +1,6 @@ -// Generated using Sourcery 0.16.1 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 1.2.1 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT - import Foundation import LDSwiftEventSource @testable import LaunchDarkly diff --git a/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift b/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift index 74c30b33..ac863ba1 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift @@ -10,8 +10,8 @@ import Foundation struct AnyComparer { private init() { } - //If editing this method to add classes here, update AnySpec with tests that verify the comparison for that class - //swiftlint:disable:next cyclomatic_complexity + // If editing this method to add classes here, update AnySpec with tests that verify the comparison for that class + // swiftlint:disable:next cyclomatic_complexity static func isEqual(_ value: Any, to other: Any) -> Bool { switch (value, other) { case let (value, other) as (Bool, Bool): diff --git a/LaunchDarkly/LaunchDarkly/Extensions/DateFormatter.swift b/LaunchDarkly/LaunchDarkly/Extensions/DateFormatter.swift index 693410d8..89160cc4 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/DateFormatter.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/DateFormatter.swift @@ -12,7 +12,7 @@ extension DateFormatter { let httpUrlHeaderFormatter = DateFormatter() httpUrlHeaderFormatter.locale = Locale(identifier: "en_US_POSIX") httpUrlHeaderFormatter.timeZone = TimeZone(abbreviation: "GMT") - httpUrlHeaderFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" //Mon, 07 May 2018 19:46:29 GMT + httpUrlHeaderFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" // Mon, 07 May 2018 19:46:29 GMT return httpUrlHeaderFormatter } diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index f8d00157..91457ebc 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -126,13 +126,13 @@ public class LDClient { internalSetOnlineQueue.sync { guard goOnline, self.canGoOnline else { - //go offline, which is not throttled + // go offline, which is not throttled self.go(online: false, reasonOnlineUnavailable: self.reasonOnlineUnavailable(goOnline: goOnline), completion: completion) return } self.throttler.runThrottled { - //since going online was throttled, check the last called setOnline value and whether we can go online + // since going online was throttled, check the last called setOnline value and whether we can go online self.go(online: goOnline && self.canGoOnline, reasonOnlineUnavailable: self.reasonOnlineUnavailable(goOnline: goOnline), completion: completion) } } @@ -375,7 +375,7 @@ public class LDClient { */ /// - Tag: variationWithdefaultValue public func variation(forKey flagKey: LDFlagKey, defaultValue: T) -> T { - //the defaultValue cast to 'as T?' directs the call to the Optional-returning variation method + // the defaultValue cast to 'as T?' directs the call to the Optional-returning variation method variation(forKey: flagKey, defaultValue: defaultValue as T?) ?? defaultValue } @@ -790,7 +790,7 @@ public class LDClient { Log.debug(typeName(and: #function) + "result: \(result)") switch result { case .success: - break //EventReporter handles removing events from the event store, so there's nothing to do here. It's here in case we want to do something in the future. + break // EventReporter handles removing events from the event store, so there's nothing to do here. It's here in case we want to do something in the future. case .error(let synchronizingError): process(synchronizingError, logPrefix: typeName(and: #function, appending: ": ")) } diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index f681d7ee..094284f1 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -7,20 +7,20 @@ import Foundation -///The feature flag key is a String. This typealias helps define where the SDK expects the string to be a feature flag key. +/// The feature flag key is a String. This typealias helps define where the SDK expects the string to be a feature flag key. public typealias LDFlagKey = String -///An object can own an observer for as long as the object exists. Swift structs and enums cannot be observer owners. +/// An object can own an observer for as long as the object exists. Swift structs and enums cannot be observer owners. public typealias LDObserverOwner = AnyObject -///A closure used to notify an observer owner of a change to a single feature flag's value. +/// A closure used to notify an observer owner of a change to a single feature flag's value. public typealias LDFlagChangeHandler = (LDChangedFlag) -> Void -///A closure used to notify an observer owner of a change to the feature flags in a collection of `LDChangedFlag`. +/// A closure used to notify an observer owner of a change to the feature flags in a collection of `LDChangedFlag`. public typealias LDFlagCollectionChangeHandler = ([LDFlagKey: LDChangedFlag]) -> Void -///A closure used to notify an observer owner that a feature flag request resulted in no changes to any feature flag. +/// A closure used to notify an observer owner that a feature flag request resulted in no changes to any feature flag. public typealias LDFlagsUnchangedHandler = () -> Void -///A closure used to notify an observer owner that the current connection mode has changed. +/// A closure used to notify an observer owner that the current connection mode has changed. public typealias LDConnectionModeChangedHandler = (ConnectionInformation.ConnectionMode) -> Void -///A closure used to notify an observer owner that an error occurred during feature flag processing. +/// A closure used to notify an observer owner that an error occurred during feature flag processing. public typealias LDErrorHandler = (Error) -> Void extension LDFlagKey { diff --git a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift index 330eebd0..243305fd 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift @@ -7,7 +7,7 @@ import Foundation -//Data structure used to cache feature flags for a specific user from a specific environment +// Data structure used to cache feature flags for a specific user from a specific environment struct CacheableEnvironmentFlags { enum CodingKeys: String, CodingKey, CaseIterable { case userKey, mobileKey, featureFlags diff --git a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift index 13f8f1da..56cb65f9 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift @@ -7,8 +7,8 @@ import Foundation -//Data structure used to cache feature flags for a specific user for multiple environments -//Cache model in use from 4.0.0 +// Data structure used to cache feature flags for a specific user for multiple environments +// Cache model in use from 4.0.0 /* [: [ “userKey”: , //CacheableUserEnvironmentFlags dictionary @@ -88,13 +88,13 @@ extension Date { /// Date string using the format 2018-08-13T19:06:38.123Z var stringValue: String { DateFormatter.ldDateFormatter.string(from: self) } - //When a date is converted to JSON, the resulting string is not as precise as the original date (only to the nearest .001s) - //By converting the date to json, then back into a date, the result can be compared with any date re-inflated from json - ///Date truncated to the nearest millisecond, which is the precision for string formatted dates + // When a date is converted to JSON, the resulting string is not as precise as the original date (only to the nearest .001s) + // By converting the date to json, then back into a date, the result can be compared with any date re-inflated from json + /// Date truncated to the nearest millisecond, which is the precision for string formatted dates var stringEquivalentDate: Date { stringValue.dateValue } } extension String { - ///Date converted from a string using the format 2018-08-13T19:06:38.123Z + /// Date converted from a string using the format 2018-08-13T19:06:38.123Z var dateValue: Date { DateFormatter.ldDateFormatter.date(from: self) ?? Date() } } diff --git a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift index 6629faa3..415285b7 100644 --- a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift +++ b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift @@ -26,7 +26,7 @@ public struct ConnectionInformation: Codable, CustomStringConvertible { } } - case unauthorized, httpError(Int), unknownError(String), none //We need .none for a non-failable initializer to conform to Codable + case unauthorized, httpError(Int), unknownError(String), none // We need .none for a non-failable initializer to conform to Codable var unknownValue: String? { guard case let .unknownError(value) = self diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index aa44c267..3168e45c 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -104,7 +104,7 @@ struct Event { static func customEvent(key: String, user: LDUser, data: Any? = nil, metricValue: Double? = nil) throws -> Event { Log.debug(typeName(and: #function) + "key: " + key + ", data: \(String(describing: data)), metricValue: \(String(describing: metricValue))") if let data = data { - guard JSONSerialization.isValidJSONObject([CodingKeys.data.rawValue: data]) //the top level object must be either an array or an object for isValidJSONObject to work correctly + guard JSONSerialization.isValidJSONObject([CodingKeys.data.rawValue: data]) // the top level object must be either an array or an object for isValidJSONObject to work correctly else { throw LDInvalidArgumentError("data is not a JSON convertible value") } @@ -145,7 +145,7 @@ struct Event { eventDictionary[CodingKeys.defaultValue.rawValue] = defaultValue ?? NSNull() } eventDictionary[CodingKeys.variation.rawValue] = featureFlag?.variation - //If the flagVersion exists, it is reported as the "version". If not, the version is reported using the "version" key. + // If the flagVersion exists, it is reported as the "version". If not, the version is reported using the "version" key. eventDictionary[CodingKeys.version.rawValue] = featureFlag?.flagVersion ?? featureFlag?.version eventDictionary[CodingKeys.data.rawValue] = data if let flagRequestTracker = flagRequestTracker { diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift index 92e85b6b..cf1f07b8 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift @@ -16,9 +16,9 @@ struct FeatureFlag { let flagKey: LDFlagKey let value: Any? let variation: Int? - ///The "environment" version. It changes whenever any feature flag in the environment changes. Used for version comparisons for streaming patch and delete. + /// The "environment" version. It changes whenever any feature flag in the environment changes. Used for version comparisons for streaming patch and delete. let version: Int? - ///The feature flag version. It changes whenever this feature flag changes. Used for event reporting only. Server json lists this as "flagVersion". Event json lists this as "version". + /// The feature flag version. It changes whenever this feature flag changes. Used for event reporting only. Server json lists this as "flagVersion". Event json lists this as "version". let flagVersion: Int? let trackEvents: Bool? let debugEventsUntilDate: Date? diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift index 40825c2d..d07afa87 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift @@ -11,11 +11,11 @@ import Foundation Collects the elements of a feature flag that changed as a result of a `clientstream` update or feature flag request. The SDK will pass a LDChangedFlag or a collection of LDChangedFlags into feature flag observer closures. The client app will have to convert the old/newValue into the expected type. See `LDClient.observe(key:owner:handler:)`, `LDClient.observe(keys:owner:handler:)`, and `LDClient.observeAll(owner:handler:)` for more details. */ public struct LDChangedFlag { - ///The key of the changed feature flag + /// The key of the changed feature flag public let key: LDFlagKey - ///The feature flag's value before the change. The client app will have to convert the oldValue into the expected type. + /// The feature flag's value before the change. The client app will have to convert the oldValue into the expected type. public let oldValue: Any? - ///The feature flag's value after the change. The client app will have to convert the newValue into the expected type. + /// The feature flag's value after the change. The client app will have to convert the newValue into the expected type. public let newValue: Any? init(key: LDFlagKey, oldValue: Any?, newValue: Any?) { diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagBaseTypeConvertible.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagBaseTypeConvertible.swift index 8a7ce3f5..ab3a6a85 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagBaseTypeConvertible.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagBaseTypeConvertible.swift @@ -7,9 +7,9 @@ import Foundation -///Protocol to convert LDFlagValue into it's Base Type. +/// Protocol to convert LDFlagValue into it's Base Type. protocol LDFlagBaseTypeConvertible { - ///Failable initializer. Client app developers should not use LDFlagBaseTypeConvertible. The SDK uses this protocol to limit feature flag types to those defined in `LDFlagValue`. + /// Failable initializer. Client app developers should not use LDFlagBaseTypeConvertible. The SDK uses this protocol to limit feature flag types to those defined in `LDFlagValue`. init?(_ flag: LDFlagValue?) } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift index b13fb0b0..138858c2 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift @@ -7,21 +7,21 @@ import Foundation -///Defines the types and values of a feature flag. The SDK limits feature flags to these types by use of the `LDFlagValueConvertible` protocol, which uses this type. Client app developers should not construct an LDFlagValue. +/// Defines the types and values of a feature flag. The SDK limits feature flags to these types by use of the `LDFlagValueConvertible` protocol, which uses this type. Client app developers should not construct an LDFlagValue. enum LDFlagValue: Equatable { - ///Bool flag value + /// Bool flag value case bool(Bool) - ///Int flag value + /// Int flag value case int(Int) - ///Double flag value + /// Double flag value case double(Double) - ///String flag value + /// String flag value case string(String) - ///Array flag value + /// Array flag value case array([LDFlagValue]) - ///Dictionary flag value + /// Dictionary flag value case dictionary([LDFlagKey: LDFlagValue]) - ///Null flag value + /// Null flag value case null } @@ -29,71 +29,71 @@ enum LDFlagValue: Equatable { // MARK: - Bool -//extension LDFlagValue: ExpressibleByBooleanLiteral { -// init(_ value: Bool) { -// self = .bool(value) -// } +// extension LDFlagValue: ExpressibleByBooleanLiteral { +// init(_ value: Bool) { +// self = .bool(value) +// } // -// public init(booleanLiteral value: Bool) { -// self.init(value) -// } -//} +// public init(booleanLiteral value: Bool) { +// self.init(value) +// } +// } // MARK: - Int -//extension LDFlagValue: ExpressibleByIntegerLiteral { -// public init(_ value: Int) { -// self = .int(value) -// } +// extension LDFlagValue: ExpressibleByIntegerLiteral { +// public init(_ value: Int) { +// self = .int(value) +// } // -// public init(integerLiteral value: Int) { -// self.init(value) -// } -//} +// public init(integerLiteral value: Int) { +// self.init(value) +// } +// } // MARK: - Double -//extension LDFlagValue: ExpressibleByFloatLiteral { -// public init(_ value: FloatLiteralType) { -// self = .double(value) -// } +// extension LDFlagValue: ExpressibleByFloatLiteral { +// public init(_ value: FloatLiteralType) { +// self = .double(value) +// } // -// public init(floatLiteral value: FloatLiteralType) { -// self.init(value) -// } -//} +// public init(floatLiteral value: FloatLiteralType) { +// self.init(value) +// } +// } // MARK: - String -//extension LDFlagValue: ExpressibleByStringLiteral { -// public init(_ value: StringLiteralType) { -// self = .string(value) -// } +// extension LDFlagValue: ExpressibleByStringLiteral { +// public init(_ value: StringLiteralType) { +// self = .string(value) +// } // -// public init(unicodeScalarLiteral value: StringLiteralType) { -// self.init(value) -// } +// public init(unicodeScalarLiteral value: StringLiteralType) { +// self.init(value) +// } // -// public init(extendedGraphemeClusterLiteral value: StringLiteralType) { -// self.init(value) -// } +// public init(extendedGraphemeClusterLiteral value: StringLiteralType) { +// self.init(value) +// } // -// public init(stringLiteral value: StringLiteralType) { -// self.init(value) -// } -//} +// public init(stringLiteral value: StringLiteralType) { +// self.init(value) +// } +// } // MARK: - Array -//extension LDFlagValue: ExpressibleByArrayLiteral { -// public init(_ collection: Collection) where Collection.Iterator.Element == LDFlagValue { -// self = .array(Array(collection)) -// } -// -// public init(arrayLiteral elements: LDFlagValue...) { -// self.init(elements) -// } -//} +// extension LDFlagValue: ExpressibleByArrayLiteral { +// public init(_ collection: Collection) where Collection.Iterator.Element == LDFlagValue { +// self = .array(Array(collection)) +// } +// +// public init(arrayLiteral elements: LDFlagValue...) { +// self.init(elements) +// } +// } extension LDFlagValue { var flagValueArray: [LDFlagValue]? { @@ -105,26 +105,26 @@ extension LDFlagValue { // MARK: - Dictionary -//extension LDFlagValue: ExpressibleByDictionaryLiteral { -// public typealias Key = LDFlagKey -// public typealias Value = LDFlagValue +// extension LDFlagValue: ExpressibleByDictionaryLiteral { +// public typealias Key = LDFlagKey +// public typealias Value = LDFlagValue // -// public init(_ keyValuePairs: Dictionary) where Dictionary.Iterator.Element == (Key, Value) { -// var dictionary = [Key: Value]() -// for (key, value) in keyValuePairs { -// dictionary[key] = value -// } -// self.init(dictionary) -// } +// public init(_ keyValuePairs: Dictionary) where Dictionary.Iterator.Element == (Key, Value) { +// var dictionary = [Key: Value]() +// for (key, value) in keyValuePairs { +// dictionary[key] = value +// } +// self.init(dictionary) +// } // -// public init(dictionaryLiteral elements: (Key, Value)...) { -// self.init(elements) -// } +// public init(dictionaryLiteral elements: (Key, Value)...) { +// self.init(elements) +// } // -// public init(_ dictionary: Dictionary) { -// self = .dictionary(dictionary) -// } -//} +// public init(_ dictionary: Dictionary) { +// self = .dictionary(dictionary) +// } +// } extension LDFlagValue { var flagValueDictionary: [LDFlagKey: LDFlagValue]? { diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift index 5322ac5a..fb778d45 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift @@ -7,7 +7,7 @@ import Foundation -///Protocol used by the SDK to limit feature flag types to those representable on LaunchDarkly servers. Client app developers should not need to use this protocol. The protocol is public because `LDClient.variation(forKey:defaultValue:)` and `LDClient.variationDetail(forKey:defaultValue:)` return a type that conforms to this protocol. See `LDFlagValue` for types that LaunchDarkly feature flags can take. +/// Protocol used by the SDK to limit feature flag types to those representable on LaunchDarkly servers. Client app developers should not need to use this protocol. The protocol is public because `LDClient.variation(forKey:defaultValue:)` and `LDClient.variationDetail(forKey:defaultValue:)` return a type that conforms to this protocol. See `LDFlagValue` for types that LaunchDarkly feature flags can take. public protocol LDFlagValueConvertible { // This commented out code here and in each extension will be used to support automatic typing. Version `4.0.0` does not support that capability. When that capability is added, uncomment this code. // func toLDFlagValue() -> LDFlagValue @@ -109,17 +109,17 @@ extension NSNull: LDFlagValueConvertible { // } } -//extension LDFlagValueConvertible { -// func isEqual(to other: LDFlagValueConvertible) -> Bool { -// switch (self.toLDFlagValue(), other.toLDFlagValue()) { -// case (.bool(let value), .bool(let otherValue)): return value == otherValue -// case (.int(let value), .int(let otherValue)): return value == otherValue -// case (.double(let value), .double(let otherValue)): return value == otherValue -// case (.string(let value), .string(let otherValue)): return value == otherValue -// case (.array(let value), .array(let otherValue)): return value == otherValue -// case (.dictionary(let value), .dictionary(let otherValue)): return value == otherValue -// case (.null, .null): return true -// default: return false -// } -// } -//} +// extension LDFlagValueConvertible { +// func isEqual(to other: LDFlagValueConvertible) -> Bool { +// switch (self.toLDFlagValue(), other.toLDFlagValue()) { +// case (.bool(let value), .bool(let otherValue)): return value == otherValue +// case (.int(let value), .int(let otherValue)): return value == otherValue +// case (.double(let value), .double(let otherValue)): return value == otherValue +// case (.string(let value), .string(let otherValue)): return value == otherValue +// case (.array(let value), .array(let otherValue)): return value == otherValue +// case (.dictionary(let value), .dictionary(let otherValue)): return value == otherValue +// case (.null, .null): return true +// default: return false +// } +// } +// } diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index d86bf94c..ad7715f6 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -17,7 +17,6 @@ public enum LDStreamingMode { typealias MobileKey = String - /** A callback for dynamically setting http headers when connection & reconnecting to a stream or on every poll request. This function should return a copy of the headers recieved with @@ -119,14 +118,14 @@ public struct LDConfig { /// The minimum values allowed to be set into LDConfig. public struct Minima { - //swiftlint:disable:next nesting + // swiftlint:disable:next nesting struct Production { static let flagPollingInterval: TimeInterval = 300.0 static let backgroundFlagPollingInterval: TimeInterval = 900.0 static let diagnosticRecordingInterval: TimeInterval = 300.0 } - //swiftlint:disable:next nesting + // swiftlint:disable:next nesting struct Debug { static let flagPollingInterval: TimeInterval = 30.0 static let backgroundFlagPollingInterval: TimeInterval = 60.0 @@ -178,19 +177,18 @@ public struct LDConfig { private var enableBgUpdates: Bool = Defaults.enableBackgroundUpdates /// Enables feature flag updates when your app is in the background. Allowed on macOS only. (Default: false) public var enableBackgroundUpdates: Bool { - set { - enableBgUpdates = newValue && allowBackgroundUpdates - } get { enableBgUpdates } + set { + enableBgUpdates = newValue && allowBackgroundUpdates + } } private var allowBackgroundUpdates: Bool /// Controls LDClient start behavior. When true, calling start causes LDClient to go online. When false, calling start causes LDClient to remain offline. If offline at start, set the client online to receive flag updates. (Default: true) public var startOnline: Bool = Defaults.startOnline - //Private Attributes /** Treat all user attributes as private for event reporting for all users. @@ -313,7 +311,7 @@ public struct LDConfig { /// Internal variable for secondaryMobileKeys computed property private var _secondaryMobileKeys: [String: String] - //Internal constructor to enable automated testing + // Internal constructor to enable automated testing init(mobileKey: String, environmentReporter: EnvironmentReporting) { self.mobileKey = mobileKey self.environmentReporter = environmentReporter @@ -326,19 +324,19 @@ public struct LDConfig { } } - ///LDConfig constructor. Configurable values are all set to their default values. The client app can modify these values as desired. Note that client app developers may prefer to get the LDConfig from `LDClient.config` in order to retain previously set values. + /// LDConfig constructor. Configurable values are all set to their default values. The client app can modify these values as desired. Note that client app developers may prefer to get the LDConfig from `LDClient.config` in order to retain previously set values. public init(mobileKey: String) { self.init(mobileKey: mobileKey, environmentReporter: EnvironmentReporter()) } - //Determine the effective flag polling interval based on runMode, configured foreground & background polling interval, and minimum foreground & background polling interval. + // Determine the effective flag polling interval based on runMode, configured foreground & background polling interval, and minimum foreground & background polling interval. func flagPollingInterval(runMode: LDClientRunMode) -> TimeInterval { let pollingInterval = runMode == .foreground ? max(flagPollingInterval, minima.flagPollingInterval) : max(backgroundFlagPollingInterval, minima.backgroundFlagPollingInterval) Log.debug(typeName(and: #function, appending: ": ") + "\(pollingInterval)") return pollingInterval } - //Determines if the status code is a code that should cause the SDK to retry a failed HTTP Request that used the REPORT method. Retried requests will use the GET method. + // Determines if the status code is a code that should cause the SDK to retry a failed HTTP Request that used the REPORT method. Retried requests will use the GET method. static func isReportRetryStatusCode(_ statusCode: Int) -> Bool { let isRetryStatusCode = LDConfig.flagRetryStatusCodes.contains(statusCode) Log.debug(LDConfig.typeName(and: #function, appending: ": ") + "\(isRetryStatusCode)") @@ -347,13 +345,13 @@ public struct LDConfig { } extension LDConfig: Equatable { - ///Compares the settable properties in 2 LDConfig structs + /// Compares the settable properties in 2 LDConfig structs public static func == (lhs: LDConfig, rhs: LDConfig) -> Bool { return lhs.mobileKey == rhs.mobileKey && lhs.baseUrl == rhs.baseUrl && lhs.eventsUrl == rhs.eventsUrl && lhs.streamUrl == rhs.streamUrl - && lhs.eventCapacity == rhs.eventCapacity //added + && lhs.eventCapacity == rhs.eventCapacity && lhs.connectionTimeout == rhs.connectionTimeout && lhs.eventFlushInterval == rhs.eventFlushInterval && lhs.flagPollingInterval == rhs.flagPollingInterval diff --git a/LaunchDarkly/LaunchDarkly/Models/User/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/User/LDUser.swift index bb0b4ce8..0cdf5cdf 100644 --- a/LaunchDarkly/LaunchDarkly/Models/User/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/User/LDUser.swift @@ -6,7 +6,7 @@ // import Foundation -typealias UserKey = String //use for identifying semantics for strings, particularly in dictionaries +typealias UserKey = String // use for identifying semantics for strings, particularly in dictionaries /** LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. For example, the client app may launch with the SDK defined anonymous user. As the user works with the client app, information may be collected as needed and sent to LaunchDarkly. The client app controls the information collected, which LaunchDarkly does not use except as the client directs to refine feature flags. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. The SDK caches last known feature flags for use on app startup to provide continuity with the last app run. Provided the LDClient is online and can establish a connection with LaunchDarkly servers, cached information will only be used a very short time. Once the latest feature flags arrive at the SDK, the SDK no longer uses cached feature flags. The SDK retains feature flags on the last 5 client defined users. The SDK will retain feature flags until they are overwritten by a different user's feature flags, or until the user removes the app from the device. @@ -14,9 +14,9 @@ typealias UserKey = String //use for identifying semantics for strings, particu */ public struct LDUser { - ///String keys associated with LDUser properties. + /// String keys associated with LDUser properties. public enum CodingKeys: String, CodingKey { - ///Key names match the corresponding LDUser property + /// Key names match the corresponding LDUser property case key, name, firstName, lastName, country, ipAddress = "ip", email, avatar, custom, isAnonymous = "anonymous", device, operatingSystem = "os", config, privateAttributes = "privateAttrs", secondary } @@ -38,31 +38,31 @@ public struct LDUser { static let storedIdKey: String = "ldDeviceIdentifier" - ///Client app defined string that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. The key cannot be made private. + /// Client app defined string that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. The key cannot be made private. public var key: String - ///The secondary key for the user. See the [documentation](https://docs.launchdarkly.com/home/managing-flags/targeting-users#percentage-rollout-logic) for more information on it's use for percentage rollout bucketing. + /// The secondary key for the user. See the [documentation](https://docs.launchdarkly.com/home/managing-flags/targeting-users#percentage-rollout-logic) for more information on it's use for percentage rollout bucketing. public var secondary: String? - ///Client app defined name for the user. (Default: nil) + /// Client app defined name for the user. (Default: nil) public var name: String? - ///Client app defined first name for the user. (Default: nil) + /// Client app defined first name for the user. (Default: nil) public var firstName: String? - ///Client app defined last name for the user. (Default: nil) + /// Client app defined last name for the user. (Default: nil) public var lastName: String? - ///Client app defined country for the user. (Default: nil) + /// Client app defined country for the user. (Default: nil) public var country: String? - ///Client app defined ipAddress for the user. (Default: nil) + /// Client app defined ipAddress for the user. (Default: nil) public var ipAddress: String? - ///Client app defined email address for the user. (Default: nil) + /// Client app defined email address for the user. (Default: nil) public var email: String? - ///Client app defined avatar for the user. (Default: nil) + /// Client app defined avatar for the user. (Default: nil) public var avatar: String? - ///Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) + /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) public var custom: [String: Any]? - ///Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: true) + /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: true) public var isAnonymous: Bool - ///Client app defined device for the user. The SDK will determine the device automatically, however the client app can override the value. The SDK will insert the device into the `custom` dictionary. The device cannot be made private. (Default: the system identified device) + /// Client app defined device for the user. The SDK will determine the device automatically, however the client app can override the value. The SDK will insert the device into the `custom` dictionary. The device cannot be made private. (Default: the system identified device) public var device: String? - ///Client app defined operatingSystem for the user. The SDK will determine the operatingSystem automatically, however the client app can override the value. The SDK will insert the operatingSystem into the `custom` dictionary. The operatingSystem cannot be made private. (Default: the system identified operating system) + /// Client app defined operatingSystem for the user. The SDK will determine the operatingSystem automatically, however the client app can override the value. The SDK will insert the operatingSystem into the `custom` dictionary. The operatingSystem cannot be made private. (Default: the system identified operating system) public var operatingSystem: String? /** @@ -73,7 +73,7 @@ public struct LDUser { */ public var privateAttributes: [String]? - ///An NSObject wrapper for the Swift LDUser struct. Intended for use in mixed apps when Swift code needs to pass a user into an Objective-C method. + /// An NSObject wrapper for the Swift LDUser struct. Intended for use in mixed apps when Swift code needs to pass a user into an Objective-C method. public var objcLdUser: ObjcLDUser { ObjcLDUser(self) } internal var flagStore: FlagMaintaining? @@ -161,7 +161,7 @@ public struct LDUser { self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), isAnonymous: true, device: environmentReporter.deviceModel, operatingSystem: environmentReporter.systemVersion) } - //swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity private func value(for attribute: String) -> Any? { switch attribute { case CodingKeys.key.rawValue: return key @@ -182,12 +182,12 @@ public struct LDUser { default: return nil } } - ///Returns the custom dictionary without the SDK set device and operatingSystem attributes + /// Returns the custom dictionary without the SDK set device and operatingSystem attributes var customWithoutSdkSetAttributes: [String: Any] { custom?.filter { key, _ in !LDUser.sdkSetAttributes.contains(key) } ?? [:] } - ///Dictionary with LDUser attribute keys and values, with options to include feature flags and private attributes. LDConfig object used to help resolving what attributes should be private. + /// Dictionary with LDUser attribute keys and values, with options to include feature flags and private attributes. LDConfig object used to help resolving what attributes should be private. /// - parameter includePrivateAttributes: Controls whether the resulting dictionary includes private attributes /// - parameter config: Provides supporting information for defining private attributes func dictionaryValue(includePrivateAttributes includePrivate: Bool, config: LDConfig) -> [String: Any] { @@ -228,11 +228,11 @@ public struct LDUser { return dictionary } - ///Default key is the LDUser.key the SDK provides when any intializer is called without defining the key. The key should be constant with respect to the client app installation on a specific device. (The key may change if the client app is uninstalled and then reinstalled on the same device.) - ///- parameter environmentReporter: The environmentReporter provides selected information that varies between OS regarding how it's determined + /// Default key is the LDUser.key the SDK provides when any intializer is called without defining the key. The key should be constant with respect to the client app installation on a specific device. (The key may change if the client app is uninstalled and then reinstalled on the same device.) + /// - parameter environmentReporter: The environmentReporter provides selected information that varies between OS regarding how it's determined static func defaultKey(environmentReporter: EnvironmentReporting) -> String { - //For iOS & tvOS, this should be UIDevice.current.identifierForVendor.UUIDString - //For macOS & watchOS, this should be a UUID that the sdk creates and stores so that the value returned here should be always the same + // For iOS & tvOS, this should be UIDevice.current.identifierForVendor.UUIDString + // For macOS & watchOS, this should be a UUID that the sdk creates and stores so that the value returned here should be always the same if let vendorUUID = environmentReporter.vendorUUID { return vendorUUID } @@ -246,13 +246,13 @@ public struct LDUser { } extension LDUser: Equatable { - ///Compares users by comparing their user keys only, to allow the client app to collect user information over time + /// Compares users by comparing their user keys only, to allow the client app to collect user information over time public static func == (lhs: LDUser, rhs: LDUser) -> Bool { lhs.key == rhs.key } } -///Class providing ObjC interoperability with the LDUser struct +/// Class providing ObjC interoperability with the LDUser struct @objc final class LDUserWrapper: NSObject { let wrapped: LDUser @@ -306,7 +306,7 @@ extension LDUserWrapper: NSCoding { self.init(user: user) } - ///Method to configure NSKeyed(Un)Archivers to convert version 2.3.0 and older user caches to 2.3.1 and later user cache formats. Note that the v3 SDK no longer caches LDUsers, rather only feature flags and the LDUser.key are cached. + /// Method to configure NSKeyed(Un)Archivers to convert version 2.3.0 and older user caches to 2.3.1 and later user cache formats. Note that the v3 SDK no longer caches LDUsers, rather only feature flags and the LDUser.key are cached. class func configureKeyedArchiversToHandleVersion2_3_0AndOlderUserCacheFormat() { NSKeyedUnarchiver.setClass(LDUserWrapper.self, forClassName: "LDUserModel") NSKeyedArchiver.setClassName("LDUserModel", for: LDUserWrapper.self) @@ -317,12 +317,12 @@ extension LDUser: TypeIdentifying { } #if DEBUG extension LDUser { - ///Testing method to get the user attribute value from a LDUser struct + /// Testing method to get the user attribute value from a LDUser struct func value(forAttribute attribute: String) -> Any? { value(for: attribute) } - //Compares all user properties. Excludes the composed FlagStore, which contains the users feature flags + // Compares all user properties. Excludes the composed FlagStore, which contains the users feature flags func isEqual(to otherUser: LDUser) -> Bool { key == otherUser.key && secondary == otherUser.secondary diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 4d690e4c..08797263 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -11,7 +11,7 @@ import LDSwiftEventSource typealias ServiceResponse = (data: Data?, urlResponse: URLResponse?, error: Error?) typealias ServiceCompletionHandler = (ServiceResponse) -> Void -//sourcery: autoMockable +// sourcery: autoMockable protocol DarklyStreamingProvider: AnyObject { func start() func stop() @@ -126,9 +126,9 @@ final class DarklyService: DarklyServiceProvider { return request } - //The flagRequestCachePolicy varies to allow the SDK to force a reload from the source on a user change. Both the SDK and iOS keep the etag from the last request. On a user change if we use .useProtocolCachePolicy, even though the SDK doesn't supply the etag, iOS does (despite clearing the URLCache!!!). In order to force iOS to ignore the etag, change the policy to .reloadIgnoringLocalCache when there is no etag. - //Note that after setting .reloadRevalidatingCacheData on the request, the property appears not to accept it, and instead sets .reloadIgnoringLocalCacheData. Despite this, there does appear to be a difference in cache policy, because the SDK behaves as expected: on a new user it requests flags without the cache, and on a request with an etag it requests flags allowing the cache. Although testing shows that we could always set .reloadIgnoringLocalCacheData here, because that is NOT symantecally the desired behavior, the method distinguishes between the use cases. - //watchOS logs an error when .useProtocolCachePolicy is set for flag requests with an etag. By setting .reloadRevalidatingCacheData, the SDK behaves correctly, but watchOS does not log an error. + // The flagRequestCachePolicy varies to allow the SDK to force a reload from the source on a user change. Both the SDK and iOS keep the etag from the last request. On a user change if we use .useProtocolCachePolicy, even though the SDK doesn't supply the etag, iOS does (despite clearing the URLCache!!!). In order to force iOS to ignore the etag, change the policy to .reloadIgnoringLocalCache when there is no etag. + // Note that after setting .reloadRevalidatingCacheData on the request, the property appears not to accept it, and instead sets .reloadIgnoringLocalCacheData. Despite this, there does appear to be a difference in cache policy, because the SDK behaves as expected: on a new user it requests flags without the cache, and on a request with an etag it requests flags allowing the cache. Although testing shows that we could always set .reloadIgnoringLocalCacheData here, because that is NOT symantecally the desired behavior, the method distinguishes between the use cases. + // watchOS logs an error when .useProtocolCachePolicy is set for flag requests with an etag. By setting .reloadRevalidatingCacheData, the SDK behaves correctly, but watchOS does not log an error. private var flagRequestCachePolicy: URLRequest.CachePolicy { return httpHeaders.hasFlagRequestEtag ? .reloadRevalidatingCacheData : .reloadIgnoringLocalCacheData } @@ -169,7 +169,7 @@ final class DarklyService: DarklyServiceProvider { HTTPHeaders.setFlagRequestEtag(serviceResponse.urlResponse?.httpHeaderEtag, for: config.mobileKey) } - //Although this does not need any info stored in the DarklyService instance, LDClient shouldn't have to distinguish between an actual and a mock. Making this an instance method does that. + // Although this does not need any info stored in the DarklyService instance, LDClient shouldn't have to distinguish between an actual and a mock. Making this an instance method does that. func clearFlagResponseCache() { URLCache.shared.removeAllCachedResponses() HTTPHeaders.removeFlagRequestEtags() diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift index f92efe87..c11bd342 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift @@ -15,7 +15,7 @@ extension HTTPURLResponse { } struct StatusCodes { - //swiftlint:disable:next identifier_name + // swiftlint:disable:next identifier_name static let ok = 200 static let accepted = 202 static let notModified = 304 diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift index b0f57ee4..dcc95c5b 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift @@ -19,7 +19,7 @@ public class ObjcLDChangedFlag: NSObject { changedFlag.oldValue ?? changedFlag.newValue } - ///The changed feature flag's key + /// The changed feature flag's key @objc public var key: String { changedFlag.key } @@ -29,16 +29,16 @@ public class ObjcLDChangedFlag: NSObject { } } -///Wraps the changed feature flag's BOOL values. +/// Wraps the changed feature flag's BOOL values. /// -///If the flag is not actually a BOOL the SDK sets the old and new value to false, and `typeMismatch` will be `YES`. +/// If the flag is not actually a BOOL the SDK sets the old and new value to false, and `typeMismatch` will be `YES`. @objc(LDBoolChangedFlag) public final class ObjcLDBoolChangedFlag: ObjcLDChangedFlag { - ///The changed flag's value before it changed + /// The changed flag's value before it changed @objc public var oldValue: Bool { (changedFlag.oldValue as? Bool) ?? false } - ///The changed flag's value after it changed + /// The changed flag's value after it changed @objc public var newValue: Bool { (changedFlag.newValue as? Bool) ?? false } @@ -52,16 +52,16 @@ public final class ObjcLDBoolChangedFlag: ObjcLDChangedFlag { } } -///Wraps the changed feature flag's NSInteger values. +/// Wraps the changed feature flag's NSInteger values. /// -///If the flag is not actually an NSInteger the SDK sets the old and new value to 0, and `typeMismatch` will be `YES`. +/// If the flag is not actually an NSInteger the SDK sets the old and new value to 0, and `typeMismatch` will be `YES`. @objc(LDIntegerChangedFlag) public final class ObjcLDIntegerChangedFlag: ObjcLDChangedFlag { - ///The changed flag's value before it changed + /// The changed flag's value before it changed @objc public var oldValue: Int { (changedFlag.oldValue as? Int) ?? 0 } - ///The changed flag's value after it changed + /// The changed flag's value after it changed @objc public var newValue: Int { (changedFlag.newValue as? Int) ?? 0 } @@ -75,16 +75,16 @@ public final class ObjcLDIntegerChangedFlag: ObjcLDChangedFlag { } } -///Wraps the changed feature flag's double values. +/// Wraps the changed feature flag's double values. /// -///If the flag is not actually a double the SDK sets the old and new value to 0.0, and `typeMismatch` will be `YES`. +/// If the flag is not actually a double the SDK sets the old and new value to 0.0, and `typeMismatch` will be `YES`. @objc(LDDoubleChangedFlag) public final class ObjcLDDoubleChangedFlag: ObjcLDChangedFlag { - ///The changed flag's value before it changed + /// The changed flag's value before it changed @objc public var oldValue: Double { (changedFlag.oldValue as? Double) ?? 0.0 } - ///The changed flag's value after it changed + /// The changed flag's value after it changed @objc public var newValue: Double { (changedFlag.newValue as? Double) ?? 0.0 } @@ -98,16 +98,16 @@ public final class ObjcLDDoubleChangedFlag: ObjcLDChangedFlag { } } -///Wraps the changed feature flag's NSString values. +/// Wraps the changed feature flag's NSString values. /// -///If the flag is not actually an NSString the SDK sets the old and new value to nil, and `typeMismatch` will be `YES`. +/// If the flag is not actually an NSString the SDK sets the old and new value to nil, and `typeMismatch` will be `YES`. @objc(LDStringChangedFlag) public final class ObjcLDStringChangedFlag: ObjcLDChangedFlag { - ///The changed flag's value before it changed + /// The changed flag's value before it changed @objc public var oldValue: String? { (changedFlag.oldValue as? String) } - ///The changed flag's value after it changed + /// The changed flag's value after it changed @objc public var newValue: String? { (changedFlag.newValue as? String) } @@ -121,16 +121,16 @@ public final class ObjcLDStringChangedFlag: ObjcLDChangedFlag { } } -///Wraps the changed feature flag's NSArray values. +/// Wraps the changed feature flag's NSArray values. /// -///If the flag is not actually a NSArray the SDK sets the old and new value to nil, and `typeMismatch` will be `YES`. +/// If the flag is not actually a NSArray the SDK sets the old and new value to nil, and `typeMismatch` will be `YES`. @objc(LDArrayChangedFlag) public final class ObjcLDArrayChangedFlag: ObjcLDChangedFlag { - ///The changed flag's value before it changed + /// The changed flag's value before it changed @objc public var oldValue: [Any]? { changedFlag.oldValue as? [Any] } - ///The changed flag's value after it changed + /// The changed flag's value after it changed @objc public var newValue: [Any]? { changedFlag.newValue as? [Any] } @@ -144,16 +144,16 @@ public final class ObjcLDArrayChangedFlag: ObjcLDChangedFlag { } } -///Wraps the changed feature flag's NSDictionary values. +/// Wraps the changed feature flag's NSDictionary values. /// -///If the flag is not actually an NSDictionary the SDK sets the old and new value to nil, and `typeMismatch` will be `YES`. +/// If the flag is not actually an NSDictionary the SDK sets the old and new value to nil, and `typeMismatch` will be `YES`. @objc(LDDictionaryChangedFlag) public final class ObjcLDDictionaryChangedFlag: ObjcLDChangedFlag { - ///The changed flag's value before it changed + /// The changed flag's value before it changed @objc public var oldValue: [String: Any]? { changedFlag.oldValue as? [String: Any] } - ///The changed flag's value after it changed + /// The changed flag's value after it changed @objc public var newValue: [String: Any]? { changedFlag.newValue as? [String: Any] } @@ -168,7 +168,7 @@ public final class ObjcLDDictionaryChangedFlag: ObjcLDChangedFlag { } public extension LDChangedFlag { - ///An NSObject wrapper for the Swift LDChangeFlag enum. Intended for use in mixed apps when Swift code needs to pass a LDChangeFlag into an Objective-C method. + /// An NSObject wrapper for the Swift LDChangeFlag enum. Intended for use in mixed apps when Swift code needs to pass a LDChangeFlag into an Objective-C method. var objcChangedFlag: ObjcLDChangedFlag { let extantValue = oldValue ?? newValue switch extantValue { diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index c9a76671..6276bb93 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -133,19 +133,19 @@ public final class ObjcLDConfig: NSObject { set { config.inlineUserInEvents = newValue } } - ///Enables logging for debugging. (Default: NO) + /// Enables logging for debugging. (Default: NO) @objc public var debugMode: Bool { get { config.isDebugMode } set { config.isDebugMode = newValue } } - ///Enables requesting evaluation reasons for all flags. (Default: NO) + /// Enables requesting evaluation reasons for all flags. (Default: NO) @objc public var evaluationReasons: Bool { get { config.evaluationReasons } set { config.evaluationReasons = newValue } } - ///An Integer that tells UserEnvironmentFlagCache the maximum number of users to locally cache. Can be set to -1 for unlimited cached users. (Default: 5) + /// An Integer that tells UserEnvironmentFlagCache the maximum number of users to locally cache. Can be set to -1 for unlimited cached users. (Default: 5) @objc public var maxCachedUsers: Int { get { config.maxCachedUsers } set { config.maxCachedUsers = newValue } @@ -197,17 +197,17 @@ public final class ObjcLDConfig: NSObject { try config.setSecondaryMobileKeys(keys) } - ///LDConfig constructor. Configurable values are all set to their default values. The client app can modify these values as desired. Note that client app developers may prefer to get the LDConfig from `LDClient.config` (`ObjcLDClient.config`) in order to retain previously set values. + /// LDConfig constructor. Configurable values are all set to their default values. The client app can modify these values as desired. Note that client app developers may prefer to get the LDConfig from `LDClient.config` (`ObjcLDClient.config`) in order to retain previously set values. @objc public init(mobileKey: String) { config = LDConfig(mobileKey: mobileKey) } - //Initializer to wrap the Swift LDConfig into ObjcLDConfig for use in Objective-C apps. + // Initializer to wrap the Swift LDConfig into ObjcLDConfig for use in Objective-C apps. init(_ config: LDConfig) { self.config = config } - ///Compares the settable properties in 2 LDConfig structs + /// Compares the settable properties in 2 LDConfig structs @objc public func isEqual(object: Any?) -> Bool { guard let other = object as? ObjcLDConfig else { return false } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index c4448a36..16554cc6 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -28,103 +28,103 @@ public final class ObjcLDUser: NSObject { @objc public class var privatizableAttributes: [String] { LDUser.privatizableAttributes } - ///LDUser secondary attribute used to make `secondary` private + /// LDUser secondary attribute used to make `secondary` private @objc public class var attributeSecondary: String { LDUser.CodingKeys.secondary.rawValue } - ///LDUser name attribute used to make `name` private + /// LDUser name attribute used to make `name` private @objc public class var attributeName: String { LDUser.CodingKeys.name.rawValue } - ///LDUser firstName attribute used to make `firstName` private + /// LDUser firstName attribute used to make `firstName` private @objc public class var attributeFirstName: String { LDUser.CodingKeys.firstName.rawValue } - ///LDUser lastName attribute used to make `lastName` private + /// LDUser lastName attribute used to make `lastName` private @objc public class var attributeLastName: String { LDUser.CodingKeys.lastName.rawValue } - ///LDUser country attribute used to make `country` private + /// LDUser country attribute used to make `country` private @objc public class var attributeCountry: String { LDUser.CodingKeys.country.rawValue } - ///LDUser ipAddress attribute used to make `ipAddress` private + /// LDUser ipAddress attribute used to make `ipAddress` private @objc public class var attributeIPAddress: String { LDUser.CodingKeys.ipAddress.rawValue } - ///LDUser email attribute used to make `email` private + /// LDUser email attribute used to make `email` private @objc public class var attributeEmail: String { LDUser.CodingKeys.email.rawValue } - ///LDUser avatar attribute used to make `avatar` private + /// LDUser avatar attribute used to make `avatar` private @objc public class var attributeAvatar: String { LDUser.CodingKeys.avatar.rawValue } - ///LDUser custom attribute used to make `custom` private + /// LDUser custom attribute used to make `custom` private @objc public class var attributeCustom: String { LDUser.CodingKeys.custom.rawValue } - ///Client app defined string that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. The key cannot be made private. + /// Client app defined string that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. The key cannot be made private. @objc public var key: String { return user.key } - ///The secondary key for the user. See the [documentation](https://docs.launchdarkly.com/home/managing-flags/targeting-users#percentage-rollout-logic) for more information on it's use for percentage rollout bucketing. + /// The secondary key for the user. See the [documentation](https://docs.launchdarkly.com/home/managing-flags/targeting-users#percentage-rollout-logic) for more information on it's use for percentage rollout bucketing. @objc public var secondary: String? { get { user.secondary } set { user.secondary = newValue } } - ///Client app defined name for the user. (Default: nil) + /// Client app defined name for the user. (Default: nil) @objc public var name: String? { get { user.name } set { user.name = newValue } } - ///Client app defined first name for the user. (Default: nil) + /// Client app defined first name for the user. (Default: nil) @objc public var firstName: String? { get { user.firstName } set { user.firstName = newValue } } - ///Client app defined last name for the user. (Default: nil) + /// Client app defined last name for the user. (Default: nil) @objc public var lastName: String? { get { user.lastName } set { user.lastName = newValue } } - ///Client app defined country for the user. (Default: nil) + /// Client app defined country for the user. (Default: nil) @objc public var country: String? { get { user.country } set { user.country = newValue } } - ///Client app defined ipAddress for the user. (Default: nil) + /// Client app defined ipAddress for the user. (Default: nil) @objc public var ipAddress: String? { get { user.ipAddress } set { user.ipAddress = newValue } } - ///Client app defined email address for the user. (Default: nil) + /// Client app defined email address for the user. (Default: nil) @objc public var email: String? { get { user.email } set { user.email = newValue } } - ///Client app defined avatar for the user. (Default: nil) + /// Client app defined avatar for the user. (Default: nil) @objc public var avatar: String? { get { user.avatar } set { user.avatar = newValue } } - ///Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) + /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) @objc public var custom: [String: Any]? { get { user.custom } set { user.custom = newValue } } - ///Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: YES) + /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: YES) @objc public var isAnonymous: Bool { get { user.isAnonymous } set { user.isAnonymous = newValue } } - ///Client app defined device for the user. The SDK will determine the device automatically, however the client app can override the value. The SDK will insert the device into the `custom` dictionary. The device cannot be made private. (Default: the system identified device) + /// Client app defined device for the user. The SDK will determine the device automatically, however the client app can override the value. The SDK will insert the device into the `custom` dictionary. The device cannot be made private. (Default: the system identified device) @objc public var device: String? { get { user.device } set { user.device = newValue } } - ///Client app defined operatingSystem for the user. The SDK will determine the operatingSystem automatically, however the client app can override the value. The SDK will insert the operatingSystem into the `custom` dictionary. The operatingSystem cannot be made private. (Default: the system identified operating system) + /// Client app defined operatingSystem for the user. The SDK will determine the operatingSystem automatically, however the client app can override the value. The SDK will insert the operatingSystem into the `custom` dictionary. The operatingSystem cannot be made private. (Default: the system identified operating system) @objc public var operatingSystem: String? { get { user.operatingSystem } set { user.operatingSystem = newValue } @@ -160,7 +160,7 @@ public final class ObjcLDUser: NSObject { user = LDUser(key: key) } - //Initializer to wrap the Swift LDUser into ObjcLDUser for use in Objective-C apps. + // Initializer to wrap the Swift LDUser into ObjcLDUser for use in Objective-C apps. init(_ user: LDUser) { self.user = user } @@ -174,7 +174,7 @@ public final class ObjcLDUser: NSObject { self.user = LDUser(userDictionary: userDictionary) } - ///Compares users by comparing their user keys only, to allow the client app to collect user information over time + /// Compares users by comparing their user keys only, to allow the client app to collect user information over time @objc public func isEqual(object: Any) -> Bool { guard let otherUser = object as? ObjcLDUser else { return false } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index 13bba878..7f562c64 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -7,12 +7,12 @@ import Foundation -//sourcery: autoMockable +// sourcery: autoMockable protocol CacheConverting { func convertCacheData(for user: LDUser, and config: LDConfig) } -//CacheConverter is not thread-safe; run it from a single thread and don't allow other threads to call convertCacheData or data corruption could occur +// CacheConverter is not thread-safe; run it from a single thread and don't allow other threads to call convertCacheData or data corruption could occur final class CacheConverter: CacheConverting { struct Constants { @@ -48,7 +48,7 @@ final class CacheConverter: CacheConverting { let cachedFlags = cachedData.featureFlags else { continue } currentCache.storeFeatureFlags(cachedFlags, userKey: user.key, mobileKey: mobileKey, lastUpdated: cachedData.lastUpdated ?? Date(), storeMode: .sync) - return //If we hit on a cached user, bailout since we converted the flags for that userKey-mobileKey combination; This prefers newer caches over older + return // If we hit on a cached user, bailout since we converted the flags for that userKey-mobileKey combination; This prefers newer caches over older } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift index aac2bc78..911a3134 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift @@ -13,7 +13,7 @@ protocol DeprecatedCache { func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan: Date) -> [UserKey] - func removeData(olderThan expirationDate: Date) //provided for testing, to allow the mock to override the protocol extension + func removeData(olderThan expirationDate: Date) // provided for testing, to allow the mock to override the protocol extension } extension DeprecatedCache { @@ -25,7 +25,7 @@ extension DeprecatedCache { else { return } // no expired user cached data, leave the cache alone guard expiredUserKeys.count != cachedUserData.count else { - keyedValueCache.removeObject(forKey: cachedDataKey) //all user cached data is expired, remove the cache key & values + keyedValueCache.removeObject(forKey: cachedDataKey) // all user cached data is expired, remove the cache key & values return } let unexpiredUserData: [UserKey: [String: Any]] = cachedUserData.filter { userKey, _ in @@ -36,12 +36,12 @@ extension DeprecatedCache { } enum DeprecatedCacheModel: String, CaseIterable { - case version5, version4, version3, version2 //version1 is not supported + case version5, version4, version3, version2 // version1 is not supported } // updatedAt in cached data was used as the LDUser.lastUpdated, which is deprecated in the Swift SDK private extension LDUser.CodingKeys { - static let lastUpdated = "updatedAt" //Can't use the CodingKey protocol here, this keeps the usage similar + static let lastUpdated = "updatedAt" // Can't use the CodingKey protocol here, this keeps the usage similar } extension Dictionary where Key == String, Value == Any { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift index 6cad17bb..16ac8c7c 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift @@ -7,7 +7,7 @@ import Foundation -//Cache model in use from 2.3.3 up to 2.11.0 +// Cache model in use from 2.3.3 up to 2.11.0 /* Cache model v2 schema [: [ “key: , //LDUserModel dictionary diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift index 63e1df23..6afb78f6 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift @@ -7,7 +7,7 @@ import Foundation -//Cache model in use from 2.11.0 up to 2.13.0 +// Cache model in use from 2.11.0 up to 2.13.0 /* Cache model v3 schema [: [ “key: , //LDUserModel dictionary diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift index 96b4c828..a0f18d98 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift @@ -7,7 +7,7 @@ import Foundation -//Cache model in use from 2.13.0 up to 2.14.0 +// Cache model in use from 2.13.0 up to 2.14.0 /* Cache model v4 schema [: [ “key: , //LDUserModel dictionary diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift index 36d6013e..3c0103fe 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift @@ -7,7 +7,7 @@ import Foundation -//Cache model in use from 2.14.0 up to 4.0.0 +// Cache model in use from 2.14.0 up to 4.0.0 /* Cache model v5 schema [: [ “userKey”: , //LDUserEnvironment dictionary diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift index e58709d6..81c08401 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift @@ -7,7 +7,7 @@ import Foundation -//sourcery: autoMockable +// sourcery: autoMockable protocol DiagnosticCaching { var lastStats: DiagnosticStats? { get } @@ -47,7 +47,7 @@ final class DiagnosticCache: DiagnosticCaching { func getCurrentStatsAndReset() -> DiagnosticStats { let now = Date().millisSince1970 - //swiftlint:disable:next implicitly_unwrapped_optional + // swiftlint:disable:next implicitly_unwrapped_optional var stored: StoreData! cacheQueue.sync { stored = loadOrSetup() diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift index 7662e812..64bc88e9 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift @@ -7,10 +7,10 @@ import Foundation -//sourcery: autoMockable +// sourcery: autoMockable protocol KeyedValueCaching { func set(_ value: Any?, forKey: String) - //sourcery: DefaultReturnValue = nil + // sourcery: DefaultReturnValue = nil func dictionary(forKey: String) -> [String: Any]? func removeObject(forKey: String) } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift index be9c368f..db7a4bee 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift @@ -11,9 +11,9 @@ enum FlagCachingStoreMode: CaseIterable { case async, sync } -//sourcery: autoMockable +// sourcery: autoMockable protocol FeatureFlagCaching { - //sourcery: defaultMockValue = 5 + // sourcery: defaultMockValue = 5 var maxCachedUsers: Int { get set } func retrieveFeatureFlags(forUserWithKey userKey: String, andMobileKey mobileKey: String) -> [LDFlagKey: FeatureFlag]? @@ -89,7 +89,7 @@ final class UserEnvironmentFlagCache: FeatureFlagCaching { else { return cacheableUserEnvironmentsCollection } - //sort collection into key-value pairs in descending order...youngest to oldest + // sort collection into key-value pairs in descending order...youngest to oldest var userEnvironmentsCollection = cacheableUserEnvironmentsCollection.sorted { pair1, pair2 -> Bool in pair2.value.lastUpdated.isEarlierThan(pair1.value.lastUpdated) } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/CwlSysctl.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/CwlSysctl.swift index 46c32d1e..457a6660 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/CwlSysctl.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/CwlSysctl.swift @@ -44,7 +44,7 @@ struct Sysctl { } // Run the actual request with an appropriately sized array buffer - let data = Array(repeating: 0, count: requiredSize) + let data = [Int8](repeating: 0, count: requiredSize) let result = data.withUnsafeBufferPointer { dataBuffer -> Int32 in return Darwin.sysctl(UnsafeMutablePointer(mutating: keysPointer.baseAddress), UInt32(keys.count), UnsafeMutableRawPointer(mutating: dataBuffer.baseAddress), &requiredSize, nil, 0) } @@ -74,7 +74,7 @@ struct Sysctl { /// e.g. "MacPro4,1" static var model: String { - //swiftlint:disable:next force_try + // swiftlint:disable:next force_try return try! Sysctl.stringForKeys([CTL_HW, HW_MODEL]) } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift index 643fc792..8d1f2a79 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift @@ -7,7 +7,7 @@ import Foundation -//sourcery: autoMockable +// sourcery: autoMockable protocol DiagnosticReporting { func setMode(_ runMode: LDClientRunMode, online: Bool) } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift index d220211b..dcf4b4b9 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -39,29 +39,29 @@ enum OperatingSystem: String { } } -//sourcery: autoMockable +// sourcery: autoMockable protocol EnvironmentReporting { - //sourcery: defaultMockValue = true + // sourcery: defaultMockValue = true var isDebugBuild: Bool { get } - //sourcery: defaultMockValue = Constants.deviceType + // sourcery: defaultMockValue = Constants.deviceType var deviceType: String { get } - //sourcery: defaultMockValue = Constants.deviceModel + // sourcery: defaultMockValue = Constants.deviceModel var deviceModel: String { get } - //sourcery: defaultMockValue = Constants.systemVersion + // sourcery: defaultMockValue = Constants.systemVersion var systemVersion: String { get } - //sourcery: defaultMockValue = Constants.systemName + // sourcery: defaultMockValue = Constants.systemName var systemName: String { get } - //sourcery: defaultMockValue = .iOS + // sourcery: defaultMockValue = .iOS var operatingSystem: OperatingSystem { get } - //sourcery: defaultMockValue = EnvironmentReporter().backgroundNotification + // sourcery: defaultMockValue = EnvironmentReporter().backgroundNotification var backgroundNotification: Notification.Name? { get } - //sourcery: defaultMockValue = EnvironmentReporter().foregroundNotification + // sourcery: defaultMockValue = EnvironmentReporter().foregroundNotification var foregroundNotification: Notification.Name? { get } - //sourcery: defaultMockValue = Constants.vendorUUID + // sourcery: defaultMockValue = Constants.vendorUUID var vendorUUID: String? { get } - //sourcery: defaultMockValue = Constants.sdkVersion + // sourcery: defaultMockValue = Constants.sdkVersion var sdkVersion: String { get } - //sourcery: defaultMockValue = true + // sourcery: defaultMockValue = true var shouldThrottleOnlineCalls: Bool { get } } @@ -80,11 +80,11 @@ struct EnvironmentReporter: EnvironmentReporting { #if os(OSX) return Sysctl.model #else - //Obtaining the device model from https://stackoverflow.com/questions/26028918/how-to-determine-the-current-iphone-device-model answer by Jens Schwarzer + // Obtaining the device model from https://stackoverflow.com/questions/26028918/how-to-determine-the-current-iphone-device-model answer by Jens Schwarzer if let simulatorModelIdentifier = ProcessInfo().environment[Constants.simulatorModelIdentifier] { return simulatorModelIdentifier } - //the physical device code here is not automatically testable. Manual testing on physical devices is required. + // the physical device code here is not automatically testable. Manual testing on physical devices is required. var systemInfo = utsname() _ = uname(&systemInfo) guard let deviceModel = String(bytes: Data(bytes: &systemInfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii) @@ -151,9 +151,9 @@ extension OperatingSystemVersion { extension Sysctl { static var modelWithoutVersion: String { - //swiftlint:disable:next force_try + // swiftlint:disable:next force_try let modelRegex = try! NSRegularExpression(pattern: "([A-Za-z]+)\\d{1,2},\\d") - let model = Sysctl.model //e.g. "MacPro4,1" + let model = Sysctl.model // e.g. "MacPro4,1" return modelRegex.firstCaptureGroup(in: model, options: [], range: model.range) ?? "mac" } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift index 15180cce..0bfe1578 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift @@ -7,7 +7,7 @@ import Foundation -//sourcery: autoMockable +// sourcery: autoMockable protocol ErrorNotifying { func addErrorObserver(_ observer: ErrorObserver) func removeObservers(for owner: LDObserverOwner) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index aca47b65..d67e9835 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -13,9 +13,9 @@ enum EventSyncResult { } typealias EventSyncCompleteClosure = ((EventSyncResult) -> Void) -//sourcery: autoMockable +// sourcery: autoMockable protocol EventReporting { - //sourcery: defaultMockValue = false + // sourcery: defaultMockValue = false var isOnline: Bool { get set } var lastEventResponseDate: Date? { get } @@ -63,7 +63,7 @@ class EventReporter: EventReporting { } func record(_ event: Event) { - //The eventReporter is created when the LDClient singleton is created, and kept for the app's lifetime. So while the use of self in the async block does setup a retain cycle, it's not going to cause a memory leak + // The eventReporter is created when the LDClient singleton is created, and kept for the app's lifetime. So while the use of self in the async block does setup a retain cycle, it's not going to cause a memory leak eventQueue.sync { recordNoSync(event) } } @@ -197,7 +197,7 @@ class EventReporter: EventReporting { } private func reportSyncComplete(_ result: EventSyncResult) { - //The eventReporter is created when the LDClient singleton is created, and kept for the app's lifetime. So while the use of self in the async block does setup a retain cycle, it's not going to cause a memory leak + // The eventReporter is created when the LDClient singleton is created, and kept for the app's lifetime. So while the use of self in the async block does setup a retain cycle, it's not going to cause a memory leak guard let onSyncComplete = onSyncComplete else { return } DispatchQueue.main.async { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift index 7584538f..d1681f82 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift @@ -7,7 +7,7 @@ import Foundation -//sourcery: autoMockable +// sourcery: autoMockable protocol FlagChangeNotifying { func addFlagChangeObserver(_ observer: FlagChangeObserver) func addFlagsUnchangedObserver(_ observer: FlagsUnchangedObserver) @@ -40,7 +40,7 @@ final class FlagChangeNotifier: FlagChangeNotifying { connectionModeChangedQueue.sync { connectionModeChangedObservers.append(observer) } } - ///Removes all change handling closures from owner + /// Removes all change handling closures from owner func removeObserver(owner: LDObserverOwner) { Log.debug(typeName(and: #function) + "owner: \(owner)") flagChangeQueue.sync { flagChangeObservers.removeAll { $0.owner === owner } } @@ -117,7 +117,7 @@ final class FlagChangeNotifier: FlagChangeNotifying { } private func findChangedFlagKeys(oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey] { - oldFlags.symmetricDifference(newFlags) //symmetricDifference tests for equality, which includes version. Exclude version here. + oldFlags.symmetricDifference(newFlags) // symmetricDifference tests for equality, which includes version. Exclude version here. .filter { flagKey in guard let oldFeatureFlag = oldFlags[flagKey], let newFeatureFlag = newFlags[flagKey] @@ -131,7 +131,7 @@ final class FlagChangeNotifier: FlagChangeNotifying { } extension FlagChangeNotifier: TypeIdentifying { } -//Test support +// Test support #if DEBUG extension FlagChangeNotifier { var flagObservers: [FlagChangeObserver] { flagChangeObservers } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift index 21b39067..a31b68cc 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -42,7 +42,7 @@ final class FlagStore: FlagMaintaining { self.init(featureFlags: featureFlagDictionary?.flagCollection) } - ///Replaces all feature flags with new flags. Pass nil to reset to an empty flag store + /// Replaces all feature flags with new flags. Pass nil to reset to an empty flag store func replaceStore(newFlags: [LDFlagKey: Any], completion: CompletionClosure?) { Log.debug(typeName(and: #function) + "newFlags: \(String(describing: newFlags))") flagQueue.async(flags: .barrier) { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift index dc793f1f..cb517408 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift @@ -9,13 +9,13 @@ import Foundation import Dispatch import LDSwiftEventSource -//sourcery: autoMockable +// sourcery: autoMockable protocol LDFlagSynchronizing { - //sourcery: defaultMockValue = false + // sourcery: defaultMockValue = false var isOnline: Bool { get set } - //sourcery: defaultMockValue = .streaming + // sourcery: defaultMockValue = .streaming var streamingMode: LDStreamingMode { get } - //sourcery: defaultMockValue = 60_000 + // sourcery: defaultMockValue = 60_000 var pollingInterval: TimeInterval { get } } @@ -141,9 +141,9 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { Log.debug(typeName(and: #function)) eventSourceStarted = Date() - //The LDConfig.connectionTimeout should NOT be set here. Heartbeat is sent every 3m. ES default timeout is 5m. This is an async operation. - //LDEventSource reacts to connection errors by closing the connection and establishing a new one after an exponentially increasing wait. That makes it self healing. - //While we could keep the LDEventSource state, there's not much we can do to help it connect. If it can't connect, it's likely we won't be able to poll the server either...so it seems best to just do nothing and let it heal itself. + // The LDConfig.connectionTimeout should NOT be set here. Heartbeat is sent every 3m. ES default timeout is 5m. This is an async operation. + // LDEventSource reacts to connection errors by closing the connection and establishing a new one after an exponentially increasing wait. That makes it self healing. + // While we could keep the LDEventSource state, there's not much we can do to help it connect. If it can't connect, it's likely we won't be able to poll the server either...so it seems best to just do nothing and let it heal itself. eventSource = service.createEventSource(useReport: useReport, handler: self, errorHandler: eventSourceErrorHandler) eventSource?.start() } @@ -264,7 +264,7 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { } } - //sourcery: noMock + // sourcery: noMock deinit { onSyncComplete = nil stopEventSource() @@ -294,7 +294,7 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { } func shouldAbortStreamUpdate() -> Bool { - //Because this method is called asynchronously by the LDEventSource, need to check these conditions prior to processing the event. + // Because this method is called asynchronously by the LDEventSource, need to check these conditions prior to processing the event. if !isOnline { Log.debug(typeName(and: #function) + "aborted. " + "Flag Synchronizer is offline.") reportSyncComplete(.error(.isOffline)) @@ -306,7 +306,7 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { return true } if !streamingActive { - //Since eventSource.close() is async, this prevents responding to events after .close() is called, but before it's actually closed + // Since eventSource.close() is async, this prevents responding to events after .close() is called, but before it's actually closed Log.debug(typeName(and: #function) + "aborted. " + "Clientstream is not active.") reportSyncComplete(.error(.isOffline)) return true diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/NetworkReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/NetworkReporter.swift index 0b4613bf..8f044956 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/NetworkReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/NetworkReporter.swift @@ -12,7 +12,7 @@ import SystemConfiguration class NetworkReporter { #if canImport(SystemConfiguration) - //Sourced from: https://stackoverflow.com/a/39782859 + // Sourced from: https://stackoverflow.com/a/39782859 static func isConnectedToNetwork() -> Bool { var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift index 91bcec6c..68b8188b 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift @@ -9,7 +9,7 @@ import Foundation typealias RunClosure = () -> Void -//sourcery: autoMockable +// sourcery: autoMockable protocol Throttling { func runThrottled(_ runClosure: @escaping RunClosure) func cancelThrottledRun() diff --git a/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift b/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift index a1439430..a0d7c2f6 100644 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift @@ -189,7 +189,7 @@ final class AnyComparerSpec: QuickSpec { featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false) } context("with differing value") { - it("returns true") { //Yeah, this is weird. Since the variation is missing the comparison succeeds + it("returns true") { // Yeah, this is weird. Since the variation is missing the comparison succeeds featureFlags.forEach { flagKey, featureFlag in otherFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey, includeVariation: false, diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 6b91c639..05cc0bf6 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -110,7 +110,7 @@ final class LDClientSpec: QuickSpec { config.startOnline = startOnline config.streamingMode = streamingMode config.enableBackgroundUpdates = enableBackgroundUpdates - config.eventFlushInterval = 300.0 //5 min...don't want this to trigger + config.eventFlushInterval = 300.0 // 5 min...don't want this to trigger config.autoAliasingOptOut = autoAliasingOptOut user = LDUser.stub() @@ -350,17 +350,17 @@ final class LDClientSpec: QuickSpec { expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user } it("uncaches the new users flags") { - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 //called on init and subsequent identify + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 // called on init and subsequent identify expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey } it("records an identify event") { - expect(testContext.eventReporterMock.recordCallCount) == 2 //both start and internalIdentify + expect(testContext.eventReporterMock.recordCallCount) == 2 // both start and internalIdentify expect(testContext.recordedEvent?.kind) == .identify expect(testContext.recordedEvent?.key) == testContext.user.key } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 //Both start and internalIdentify + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 // Both start and internalIdentify expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } @@ -973,7 +973,7 @@ final class LDClientSpec: QuickSpec { } context("non-Optional default value") { it("returns the flag value") { - //The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method + // The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) as Bool) == DarklyServiceMock.FlagValues.bool expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int) as Int) == DarklyServiceMock.FlagValues.int expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double) as Double) == DarklyServiceMock.FlagValues.double @@ -994,7 +994,7 @@ final class LDClientSpec: QuickSpec { } context("Optional default value") { it("returns the flag value") { - //The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the Optional variation method + // The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the Optional variation method expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?)) == DarklyServiceMock.FlagValues.bool expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int as Int?)) == DarklyServiceMock.FlagValues.int expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double as Double?)) == DarklyServiceMock.FlagValues.double @@ -1004,7 +1004,7 @@ final class LDClientSpec: QuickSpec { == DarklyServiceMock.FlagValues.dictionary).to(beTrue()) } it("records a flag evaluation event") { - //The cast in the variation call directs the compiler to the Optional variation method + // The cast in the variation call directs the compiler to the Optional variation method _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool @@ -1016,7 +1016,7 @@ final class LDClientSpec: QuickSpec { } context("No default value") { it("returns the flag value") { - //The casts in the expect() calls allow the compiler to determine the return type. + // The casts in the expect() calls allow the compiler to determine the return type. expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: nil as Bool?)) == DarklyServiceMock.FlagValues.bool expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: nil as Int?)) == DarklyServiceMock.FlagValues.int expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: nil as Double?)) == DarklyServiceMock.FlagValues.double @@ -1026,7 +1026,7 @@ final class LDClientSpec: QuickSpec { == DarklyServiceMock.FlagValues.dictionary).to(beTrue()) } it("records a flag evaluation event") { - //The cast in the variation call allows the compiler to determine the return type + // The cast in the variation call allows the compiler to determine the return type _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: nil as Bool?) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool @@ -1040,7 +1040,7 @@ final class LDClientSpec: QuickSpec { context("flag store does not contain the requested value") { context("non-Optional default value") { it("returns the default value") { - //The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method + // The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) as Bool) == DefaultFlagValues.bool expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int) as Int) == DefaultFlagValues.int expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double) as Double) == DefaultFlagValues.double @@ -1060,7 +1060,7 @@ final class LDClientSpec: QuickSpec { } context("Optional default value") { it("returns the default value") { - //The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method + // The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?)) == DefaultFlagValues.bool expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int as Int?)) == DefaultFlagValues.int expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double as Double?)) == DefaultFlagValues.double @@ -1069,7 +1069,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary as [String: Any]?) == DefaultFlagValues.dictionary).to(beTrue()) } it("records a flag evaluation event") { - //The cast in the variation call directs the compiler to the Optional variation method + // The cast in the variation call directs the compiler to the Optional variation method _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool @@ -1081,7 +1081,7 @@ final class LDClientSpec: QuickSpec { } context("no default value") { it("returns nil") { - //The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method + // The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: nil as Bool?)).to(beNil()) expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: nil as Int?)).to(beNil()) expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: nil as Double?)).to(beNil()) @@ -1090,7 +1090,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: nil as [String: Any]?)).to(beNil()) } it("records a flag evaluation event") { - //The cast in the variation call directs the compiler to the Optional variation method + // The cast in the variation call directs the compiler to the Optional variation method _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: nil as Bool?) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 0b3fae84..9143b991 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -24,7 +24,7 @@ final class DarklyServiceMock: DarklyServiceProvider { static let null = "null-flag" static let unknown = "unknown-flag" - static var knownFlags: [LDFlagKey] { //known means the SDK has the feature flag value + static var knownFlags: [LDFlagKey] { // known means the SDK has the feature flag value [bool, int, double, string, array, dictionary, null] } static var flagsWithAnAlternateValue: [LDFlagKey] { @@ -70,7 +70,7 @@ final class DarklyServiceMock: DarklyServiceProvider { case let value as String: return value + "-alternate" as! T case var value as [Any]: value.append(4) - return value as! T //Not sure why, but this crashes if you combine append the value into the return + return value as! T // Not sure why, but this crashes if you combine append the value into the return case var value as [String: Any]: value["new-flag"] = "new-value" return value as! T @@ -282,7 +282,7 @@ extension DarklyServiceMock { flagRequestStubTest && isMethodREPORT() } - ///Use when testing requires the mock service to actually make a flag request + /// Use when testing requires the mock service to actually make a flag request func stubFlagRequest(statusCode: Int, featureFlags: [LDFlagKey: FeatureFlag]? = nil, useReport: Bool, @@ -304,7 +304,7 @@ extension DarklyServiceMock { name: flagStubName(statusCode: statusCode, useReport: useReport), onActivation: activate) } - ///Use when testing requires the mock service to simulate a service response to the flag request callback + /// Use when testing requires the mock service to simulate a service response to the flag request callback func stubFlagResponse(statusCode: Int, badData: Bool = false, responseOnly: Bool = false, errorOnly: Bool = false, responseDate: Date? = nil) { let response = HTTPURLResponse(url: config.baseUrl, statusCode: statusCode, httpVersion: Constants.httpVersion, headerFields: HTTPURLResponse.dateHeader(from: responseDate)) if statusCode == HTTPURLResponse.StatusCodes.ok { @@ -342,7 +342,7 @@ extension DarklyServiceMock { isScheme(Constants.schemeHttps) && isHost(streamHost!) && isMethodREPORT() } - ///Use when testing requires the mock service to actually make an event source connection request + /// Use when testing requires the mock service to actually make an event source connection request func stubStreamRequest(useReport: Bool, success: Bool, onActivation activate: ((URLRequest, HTTPStubsDescriptor, HTTPStubsResponse) -> Void)? = nil) { var stubResponse: HTTPStubsResponseBlock = { _ in HTTPStubsResponse(error: Constants.error) @@ -364,7 +364,7 @@ extension DarklyServiceMock { isScheme(Constants.schemeHttps) && isHost(eventHost!) && isMethodPOST() } - ///Use when testing requires the mock service to actually make an event request + /// Use when testing requires the mock service to actually make an event request func stubEventRequest(success: Bool, onActivation activate: ((URLRequest, HTTPStubsDescriptor, HTTPStubsResponse) -> Void)? = nil) { let stubResponse: HTTPStubsResponseBlock = success ? { _ in HTTPStubsResponse(data: Data(), statusCode: Int32(HTTPURLResponse.StatusCodes.accepted), headers: nil) @@ -374,7 +374,7 @@ extension DarklyServiceMock { stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: Constants.stubNameEvent, onActivation: activate) } - ///Use when testing requires the mock service to provide a service response to the event request callback + /// Use when testing requires the mock service to provide a service response to the event request callback func stubEventResponse(success: Bool, responseOnly: Bool = false, errorOnly: Bool = false, responseDate: Date? = nil) { if success { let response = HTTPURLResponse(url: config.eventsUrl, @@ -399,7 +399,7 @@ extension DarklyServiceMock { // MARK: Publish Diagnostic - ///Use when testing requires the mock service to actually make an diagnostic request + /// Use when testing requires the mock service to actually make an diagnostic request func stubDiagnosticRequest(success: Bool, onActivation activate: ((URLRequest, HTTPStubsDescriptor, HTTPStubsResponse) -> Void)? = nil) { let stubResponse: HTTPStubsResponseBlock = success ? { _ in HTTPStubsResponse(data: Data(), statusCode: Int32(HTTPURLResponse.StatusCodes.accepted), headers: nil) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 92ea27a2..1862d8b8 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -320,7 +320,7 @@ final class EventSpec: QuickSpec { beforeEach { let featureFlagWithReason = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool, includeEvaluationReason: true, includeTrackReason: true) event = Event.featureEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlagWithReason, user: user, includeReason: true) - config.inlineUserInEvents = false //Default value, here for clarity + config.inlineUserInEvents = false // Default value, here for clarity eventDictionary = event.dictionaryValue(config: config) } it("creates a dictionary with matching non-user elements") { @@ -330,7 +330,7 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.flagVersion //Since feature flags include the flag version, it should be used. + expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. expect(eventDictionary.eventData).to(beNil()) expect(AnyComparer.isEqual(eventDictionary.reason, to: DarklyServiceMock.Constants.reason)).to(beTrue()) expect(eventDictionary.eventPreviousKey).to(beNil()) @@ -355,7 +355,7 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.flagVersion //Since feature flags include the flag version, it should be used. + expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. expect(eventDictionary.eventData).to(beNil()) expect(eventDictionary.reason).to(beNil()) } @@ -415,7 +415,7 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventValue, to: NSNull())).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: NSNull())).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.flagVersion //Since feature flags include the flag version, it should be used. + expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. expect(eventDictionary.eventData).to(beNil()) } it("creates a dictionary with the user key only") { @@ -563,7 +563,7 @@ final class EventSpec: QuickSpec { } catch { fail("customEvent threw an exception") } - config.inlineUserInEvents = false //Default value, here for clarity + config.inlineUserInEvents = false // Default value, here for clarity eventDictionary = event.dictionaryValue(config: config) } it("creates a dictionary with matching non-user elements") { @@ -649,7 +649,7 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.flagVersion //Since feature flags include the flag version, it should be used. + expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. expect(eventDictionary.eventData).to(beNil()) } it("creates a dictionary with the full user") { @@ -714,7 +714,7 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) expect(eventDictionary.eventUserKey).to(beNil()) expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.flagVersion //Since feature flags include the flag version, it should be used. + expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. expect(eventDictionary.eventData).to(beNil()) } } @@ -764,7 +764,7 @@ final class EventSpec: QuickSpec { } } - //Dictionary extension methods that extract an event key, or creationDateMillis, and compare them with another dictionary + // Dictionary extension methods that extract an event key, or creationDateMillis, and compare them with another dictionary private func eventDictionarySpec() { let config = LDConfig.stub let user = LDUser.stub() diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift index 7f90549d..7415338b 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift @@ -367,7 +367,7 @@ final class FeatureFlagSpec: QuickSpec { } } context("when values differ") { - it("returns true") { //This is a weird effect of comparing the variation, and not the value itself. The server should not return different values for the same variation. + it("returns true") { // This is a weird effect of comparing the variation, and not the value itself. The server should not return different values for the same variation. originalFlags.forEach { _, originalFlag in if originalFlag.value == nil { return @@ -519,7 +519,7 @@ final class FeatureFlagSpec: QuickSpec { } context("debugEventsUntilDate is system date") { beforeEach { - //Without creating a SystemDateServiceMock and corresponding service protocol, this is really difficult to test, but the level of accuracy is not crucial. Since the debugEventsUntilDate comes in millisSince1970, setting the debugEventsUntilDate to 1 millisecond beyond the date seems like it will get "close enough" to the current date + // Without creating a SystemDateServiceMock and corresponding service protocol, this is really difficult to test, but the level of accuracy is not crucial. Since the debugEventsUntilDate comes in millisSince1970, setting the debugEventsUntilDate to 1 millisecond beyond the date seems like it will get "close enough" to the current date flag = FeatureFlag(copying: flag, trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(0.001)) shouldCreateDebugEvents = flag.shouldCreateDebugEvents(lastEventReportResponseTime: nil) } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index c6d2bd16..4f3c3baa 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -195,7 +195,7 @@ final class LDConfigSpec: XCTestCase { func testEquals() { let environmentReporter = EnvironmentReportingMock() - //must use a background enabled OS to test inequality of background enabled + // must use a background enabled OS to test inequality of background enabled environmentReporter.operatingSystem = OperatingSystem.backgroundEnabledOperatingSystems.first! let defaultConfig = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: environmentReporter) // same config diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 8a4121b0..a557f156 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -271,12 +271,12 @@ final class LDUserSpec: QuickSpec { context("with individual private attributes") { let assertions = { it("creates a matching dictionary") { - //creates a dictionary with matching key value pairs + // creates a dictionary with matching key value pairs expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) expect({ user.customDictionaryPublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - //creates a dictionary without redacted attributes + // creates a dictionary without redacted attributes expect(userDictionary.redactedAttributes).to(beNil()) self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) @@ -458,21 +458,21 @@ final class LDUserSpec: QuickSpec { config.privateUserAttributes = privateAttributesForTest userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - //creates a dictionary with matching key value pairs + // creates a dictionary with matching key value pairs expect({ user.requiredAttributeKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - //creates a dictionary without private keys + // creates a dictionary without private keys expect({ user.optionalAttributePrivateKeysDontExist(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) - //creates a dictionary with redacted attributes + // creates a dictionary with redacted attributes expect({ user.optionalAttributePrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) expect({ user.optionalAttributePublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) - //creates a custom dictionary with matching key value pairs, without private keys, and with redacted attributes + // creates a custom dictionary with matching key value pairs, without private keys, and with redacted attributes if attribute == LDUser.CodingKeys.custom.rawValue { expect({ user.customDictionaryContainsOnlySdkSetAttributes(userDictionary: userDictionary) }).to(match()) expect(user.customWithoutSdkSetAttributes.allSatisfy { k, _ in userDictionary.redactedAttributes!.contains(k) }).to(beTrue()) @@ -486,7 +486,7 @@ final class LDUserSpec: QuickSpec { privateAttributes: privateAttributesForTest) }).to(match()) } - //creates a dictionary without flag config + // creates a dictionary without flag config expect(userDictionary.flagConfig).to(beNil()) } } @@ -502,21 +502,21 @@ final class LDUserSpec: QuickSpec { user.privateAttributes = privateAttributesForTest userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - //creates a dictionary with matching key value pairs + // creates a dictionary with matching key value pairs expect({ user.requiredAttributeKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - //creates a dictionary without private keys + // creates a dictionary without private keys expect({ user.optionalAttributePrivateKeysDontExist(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) - //creates a dictionary with redacted attributes + // creates a dictionary with redacted attributes expect({ user.optionalAttributePrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) expect({ user.optionalAttributePublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) - //creates a custom dictionary with matching key value pairs, without private keys, and with redacted attributes + // creates a custom dictionary with matching key value pairs, without private keys, and with redacted attributes if attribute == LDUser.CodingKeys.custom.rawValue { expect({ user.customDictionaryContainsOnlySdkSetAttributes(userDictionary: userDictionary) }).to(match()) expect(user.customWithoutSdkSetAttributes.allSatisfy { k, _ in userDictionary.redactedAttributes!.contains(k) }).to(beTrue()) @@ -532,7 +532,7 @@ final class LDUserSpec: QuickSpec { privateAttributes: privateAttributesForTest) }).to(match()) } - //creates a dictionary without flag config + // creates a dictionary without flag config expect(userDictionary.flagConfig).to(beNil()) } } @@ -548,18 +548,18 @@ final class LDUserSpec: QuickSpec { user.privateAttributes = privateAttributesForTest userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - //creates a dictionary with matching key value pairs + // creates a dictionary with matching key value pairs expect({ user.requiredAttributeKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) expect({ user.optionalAttributeMissingValueKeysDontExist(userDictionary: userDictionary) }).to(match()) expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - //creates a dictionary without private keys + // creates a dictionary without private keys expect({ user.customDictionaryContainsOnlySdkSetAttributes(userDictionary: userDictionary) }).to(match()) - //creates a dictionary without redacted attributes + // creates a dictionary without redacted attributes expect(userDictionary.redactedAttributes).to(beNil()) - //creates a dictionary without flag config + // creates a dictionary without flag config expect(userDictionary.flagConfig).to(beNil()) } } diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index e5589dc4..ab029e2d 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -133,7 +133,7 @@ final class DarklyServiceSpec: QuickSpec { expect(reportRequestCount) == 0 } it("creates a GET request") { - //GET request url has the form https:///msdk/evalx/users/ + // GET request url has the form https:///msdk/evalx/users/ expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) @@ -196,7 +196,7 @@ final class DarklyServiceSpec: QuickSpec { expect(reportRequestCount) == 0 } it("creates a GET request") { - //GET request url has the form https:///msdk/evalx/users/ + // GET request url has the form https:///msdk/evalx/users/ expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) @@ -319,7 +319,7 @@ final class DarklyServiceSpec: QuickSpec { expect(reportRequestCount) == 1 } it("creates a REPORT request") { - //REPORT request url has the form https:///msdk/evalx/user; httpBody contains the user dictionary + // REPORT request url has the form https:///msdk/evalx/user; httpBody contains the user dictionary expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasSuffix(DarklyService.FlagRequestPath.report)).to(beTrue()) @@ -329,7 +329,7 @@ final class DarklyServiceSpec: QuickSpec { expect(urlRequest?.cachePolicy) == .reloadIgnoringLocalCacheData expect(urlRequest?.timeoutInterval) == testContext.config.connectionTimeout expect(urlRequest?.httpMethod) == URLRequest.HTTPMethods.report - expect(urlRequest?.httpBodyStream).toNot(beNil()) //Although the service sets the httpBody, OHHTTPStubs seems to convert that into an InputStream, which should be ok + expect(urlRequest?.httpBodyStream).toNot(beNil()) // Although the service sets the httpBody, OHHTTPStubs seems to convert that into an InputStream, which should be ok guard let headers = urlRequest?.allHTTPHeaderFields else { fail("request is missing HTTP headers") @@ -375,7 +375,7 @@ final class DarklyServiceSpec: QuickSpec { expect(reportRequestCount) == 1 } it("creates a REPORT request") { - //REPORT request url has the form https:///msdk/evalx/user; httpBody contains the user dictionary + // REPORT request url has the form https:///msdk/evalx/user; httpBody contains the user dictionary expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasSuffix(DarklyService.FlagRequestPath.report)).to(beTrue()) @@ -385,7 +385,7 @@ final class DarklyServiceSpec: QuickSpec { expect([.reloadIgnoringLocalCacheData, .reloadRevalidatingCacheData]).to(contain(urlRequest?.cachePolicy)) expect(urlRequest?.timeoutInterval) == testContext.config.connectionTimeout expect(urlRequest?.httpMethod) == URLRequest.HTTPMethods.report - expect(urlRequest?.httpBodyStream).toNot(beNil()) //Although the service sets the httpBody, OHHTTPStubs seems to convert that into an InputStream, which should be ok + expect(urlRequest?.httpBodyStream).toNot(beNil()) // Although the service sets the httpBody, OHHTTPStubs seems to convert that into an InputStream, which should be ok guard let headers = urlRequest?.allHTTPHeaderFields else { fail("request is missing HTTP headers") @@ -507,7 +507,7 @@ final class DarklyServiceSpec: QuickSpec { } context("on not modified") { context("response has etag") { - //This should never happen, without an original etag the server should not send a 304 NOT MODIFIED. If it does ignore it. + // This should never happen, without an original etag the server should not send a 304 NOT MODIFIED. If it does ignore it. beforeEach { testContext = TestContext() flagRequestEtag = UUID().uuidString @@ -528,7 +528,7 @@ final class DarklyServiceSpec: QuickSpec { } } context("response has no etag") { - //This should never happen, without an original etag the server should not send a 304 NOT MODIFIED. If it does ignore it. + // This should never happen, without an original etag the server should not send a 304 NOT MODIFIED. If it does ignore it. beforeEach { testContext = TestContext() testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.notModified, @@ -549,7 +549,7 @@ final class DarklyServiceSpec: QuickSpec { } context("on failure") { context("response has etag") { - //This should never happen. The server should not send an etag with a failure status code If it does ignore it. + // This should never happen. The server should not send an etag with a failure status code If it does ignore it. beforeEach { testContext = TestContext() flagRequestEtag = UUID().uuidString @@ -674,7 +674,7 @@ final class DarklyServiceSpec: QuickSpec { } } context("that differs from the original etag") { - //This should not happen. If the response was not modified then the etags should match. In that case ignore the new etag + // This should not happen. If the response was not modified then the etags should match. In that case ignore the new etag beforeEach { testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) @@ -697,7 +697,7 @@ final class DarklyServiceSpec: QuickSpec { } } context("response has no etag") { - //This should not happen. If the response was not modified then the etags should match. In that case ignore the new etag + // This should not happen. If the response was not modified then the etags should match. In that case ignore the new etag beforeEach { testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) @@ -720,7 +720,7 @@ final class DarklyServiceSpec: QuickSpec { } context("on failure") { context("response has etag") { - //This should not happen. If the response was an error then there should be no new etag. Because of the error, clear the etag + // This should not happen. If the response was an error then there should be no new etag. Because of the error, clear the etag beforeEach { testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) @@ -1006,7 +1006,7 @@ final class DarklyServiceSpec: QuickSpec { expect(diagnosticRequest?.httpMethod) == URLRequest.HTTPMethods.post // Unfortunately, we can't actually test the body here, see: // https://github.com/AliSoftware/OHHTTPStubs#known-limitations - //expect(diagnosticRequest?.httpBody) == try? JSONEncoder().encode(self.stubDiagnostic()) + // expect(diagnosticRequest?.httpBody) == try? JSONEncoder().encode(self.stubDiagnostic()) // Actual header values are tested in HTTPHeadersSpec for (key, value) in testContext.httpHeaders.diagnosticRequestHeaders { @@ -1037,7 +1037,7 @@ final class DarklyServiceSpec: QuickSpec { expect(diagnosticRequest?.httpMethod) == URLRequest.HTTPMethods.post // Unfortunately, we can't actually test the body here, see: // https://github.com/AliSoftware/OHHTTPStubs#known-limitations - //expect(diagnosticRequest?.httpBody) == try? JSONEncoder().encode(self.stubDiagnostic()) + // expect(diagnosticRequest?.httpBody) == try? JSONEncoder().encode(self.stubDiagnostic()) // Actual header values are tested in HTTPHeadersSpec for (key, value) in testContext.httpHeaders.diagnosticRequestHeaders { diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/URLCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/URLCacheSpec.swift index 2ebb452a..d4b1aa27 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/URLCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/URLCacheSpec.swift @@ -11,7 +11,7 @@ import Nimble import OHHTTPStubs @testable import LaunchDarkly -//Normally we would not build an AT for system provided services, like URLCache. The SDK uses the URLCache in a non-standard way, sending HTTP requests with a custom verb REPORT. So building this test validates that the URLCache behaves as expected for GET and REPORT requests. Retaining these tests helps provide that assurance through future revisions. +// Normally we would not build an AT for system provided services, like URLCache. The SDK uses the URLCache in a non-standard way, sending HTTP requests with a custom verb REPORT. So building this test validates that the URLCache behaves as expected for GET and REPORT requests. Retaining these tests helps provide that assurance through future revisions. final class URLCacheSpec: QuickSpec { struct Constants { @@ -25,7 +25,7 @@ final class URLCacheSpec: QuickSpec { var serviceFactoryMock: ClientServiceMockFactory var flagStore: FlagMaintaining - //per user + // per user var userServiceObjects = [String: (user: LDUser, service: DarklyService, serviceMock: DarklyServiceMock)]() var userKeys: Dictionary.Keys { userServiceObjects.keys diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift index 855a9ab4..d7183e79 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift @@ -20,8 +20,8 @@ final class EnvironmentReporterSpec: QuickSpec { var environmentReporter: EnvironmentReporter! describe("shouldRunThrottled") { context("Debug Build") { - //This test is disabled. Configure the build for the integration harness before enabling & running this test. - //If you enable this test, you might want to disable the test that follows "not for the integration harness", which should fail when the SDK is configured for the integration harness. + // This test is disabled. Configure the build for the integration harness before enabling & running this test. + // If you enable this test, you might want to disable the test that follows "not for the integration harness", which should fail when the SDK is configured for the integration harness. // context("for the integration harness") { // beforeEach { // environmentReporter = EnvironmentReporter() diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index 11d415de..4c192675 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -247,7 +247,7 @@ final class EventReporterSpec: QuickSpec { context("success") { context("with events and tracked requests") { beforeEach { - //The EventReporter will try to report events if it's started online with events. By starting online without events, then adding them, we "beat the timer" by reporting them right away + // The EventReporter will try to report events if it's started online with events. By starting online without events, then adding them, we "beat the timer" by reporting them right away waitUntil { syncComplete in testContext = TestContext(eventStubResponseDate: eventStubResponseDate, onSyncComplete: { result in testContext.syncResult = result @@ -264,7 +264,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.isReportingActive) == true expect(testContext.serviceMock.publishEventDictionariesCallCount) == 1 expect(testContext.serviceMock.publishedEventDictionaries?.count) == Event.Kind.nonSummaryKinds.count + 1 - expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys //summary events have no key, this verifies non-summary events + expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys // summary events have no key, this verifies non-summary events expect(testContext.serviceMock.publishedEventDictionaryKinds?.contains(.summary)) == true expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 1 expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == Event.Kind.nonSummaryKinds.count + 1 @@ -281,7 +281,7 @@ final class EventReporterSpec: QuickSpec { } context("with events only") { beforeEach { - //The EventReporter will try to report events if it's started online with events. By starting online without events, then adding them, we "beat the timer" by reporting them right away + // The EventReporter will try to report events if it's started online with events. By starting online without events, then adding them, we "beat the timer" by reporting them right away waitUntil { syncComplete in testContext = TestContext(eventStubResponseDate: eventStubResponseDate, onSyncComplete: { result in testContext.syncResult = result @@ -297,7 +297,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.isReportingActive) == true expect(testContext.serviceMock.publishEventDictionariesCallCount) == 1 expect(testContext.serviceMock.publishedEventDictionaries?.count) == Event.Kind.nonSummaryKinds.count - expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys //summary events have no key, this verifies non-summary events + expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys // summary events have no key, this verifies non-summary events expect(testContext.serviceMock.publishedEventDictionaryKinds?.contains(.summary)) == false expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 1 expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == Event.Kind.nonSummaryKinds.count @@ -314,7 +314,7 @@ final class EventReporterSpec: QuickSpec { } context("with tracked requests only") { beforeEach { - //The EventReporter will try to report events if it's started online with events. By starting online without events, then adding them, we "beat the timer" by reporting them right away + // The EventReporter will try to report events if it's started online with events. By starting online without events, then adding them, we "beat the timer" by reporting them right away waitUntil { syncComplete in testContext = TestContext(eventStubResponseDate: eventStubResponseDate, onSyncComplete: { result in testContext.syncResult = result @@ -389,7 +389,7 @@ final class EventReporterSpec: QuickSpec { it("drops events after the failure") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 2 //1 retry attempt + expect(testContext.serviceMock.publishEventDictionariesCallCount) == 2 // 1 retry attempt expect(testContext.eventReporter.eventStoreKeys) == [] expect(testContext.eventReporter.eventStoreKinds.contains(.summary)) == false expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys @@ -422,7 +422,7 @@ final class EventReporterSpec: QuickSpec { it("drops events after the failure") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 2 //1 retry attempt + expect(testContext.serviceMock.publishEventDictionariesCallCount) == 2 // 1 retry attempt expect(testContext.eventReporter.eventStoreKeys) == [] expect(testContext.eventReporter.eventStoreKinds.contains(.summary)) == false expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys @@ -458,7 +458,7 @@ final class EventReporterSpec: QuickSpec { it("drops events events after the failure") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 2 //1 retry attempt + expect(testContext.serviceMock.publishEventDictionariesCallCount) == 2 // 1 retry attempt expect(testContext.eventReporter.eventStoreKeys) == [] expect(testContext.eventReporter.eventStoreKinds.contains(.summary)) == false expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift index 565308ab..b03169df 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift @@ -30,7 +30,7 @@ final class FlagChangeNotifierSpec: QuickSpec { let alternateFlagKeys = ["flag-key-1", "flag-key-2", "flag-key-3"] - //Use this initializer when stubbing observers for observer add & remove tests + // Use this initializer when stubbing observers for observer add & remove tests init(observers observerCount: Int = 0, observerType: ObserverType = .any, repeatFirstObserver: Bool = false) { subject = FlagChangeNotifier() guard observerCount > 0 @@ -53,7 +53,7 @@ final class FlagChangeNotifierSpec: QuickSpec { flagsUnchangedOwnerKey = flagChangeObservers.first!.flagKeys.observerKey var flagsUnchangedObservers = [FlagsUnchangedObserver]() - //use the flag change observer owners to own the flagsUnchangedObservers + // use the flag change observer owners to own the flagsUnchangedObservers flagChangeObservers.forEach { flagChangeObserver in flagsUnchangedObservers.append(FlagsUnchangedObserver(owner: flagChangeObserver.owner!, flagsUnchangedHandler: flagsUnchangedHandler)) } @@ -61,7 +61,7 @@ final class FlagChangeNotifierSpec: QuickSpec { originalFlagChangeObservers = subject.flagObservers } - //Use this initializer when stubbing observers that should execute a LDFlagChangeHandler during the test + // Use this initializer when stubbing observers that should execute a LDFlagChangeHandler during the test init(keys: [LDFlagKey], flagChangeHandler: @escaping LDFlagChangeHandler, flagsUnchangedHandler: @escaping LDFlagsUnchangedHandler) { subject = FlagChangeNotifier() guard !keys.isEmpty @@ -74,7 +74,7 @@ final class FlagChangeNotifierSpec: QuickSpec { } flagsUnchangedOwnerKey = flagChangeObservers.first!.flagKeys.observerKey var flagsUnchangedObservers = [FlagsUnchangedObserver]() - //use the flag change observer owners to own the flagsUnchangedObservers + // use the flag change observer owners to own the flagsUnchangedObservers flagChangeObservers.forEach { flagChangeObserver in flagsUnchangedObservers.append(FlagsUnchangedObserver(owner: flagChangeObserver.owner!, flagsUnchangedHandler: flagsUnchangedHandler)) } @@ -82,8 +82,8 @@ final class FlagChangeNotifierSpec: QuickSpec { originalFlagChangeObservers = subject.flagObservers } - //Use this initializer when stubbing observers that should execute a LDFlagCollectionChangeHandler during the test - //This initializer sets 2 observers, one for the specified flags, and a second for a disjoint set of flags. That way tests verify the notifier is choosing the correct observers + // Use this initializer when stubbing observers that should execute a LDFlagCollectionChangeHandler during the test + // This initializer sets 2 observers, one for the specified flags, and a second for a disjoint set of flags. That way tests verify the notifier is choosing the correct observers init(keys: [LDFlagKey], flagCollectionChangeHandler: @escaping LDFlagCollectionChangeHandler, flagsUnchangedHandler: @escaping LDFlagsUnchangedHandler) { subject = FlagChangeNotifier() guard !keys.isEmpty @@ -112,7 +112,7 @@ final class FlagChangeNotifierSpec: QuickSpec { stubOwner(key: keys.observerKey) } - //Flag change handler stubs + // Flag change handler stubs func flagChangeHandler(changedFlag: LDChangedFlag) { } func flagCollectionChangeHandler(changedFlags: [LDFlagKey: LDChangedFlag]) { } @@ -201,7 +201,7 @@ final class FlagChangeNotifierSpec: QuickSpec { context("when several observers exist") { beforeEach { testContext = TestContext(observers: Constants.observerCount) - targetObserver = testContext.subject.flagObservers[Constants.observerCount - 2] //Take the middle one + targetObserver = testContext.subject.flagObservers[Constants.observerCount - 2] // Take the middle one testContext.subject.removeObserver(owner: targetObserver.owner!) } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift index 55c5dd6f..507d5060 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift @@ -62,7 +62,7 @@ final class FlagSynchronizerSpec: QuickSpec { streamClosed: Bool? = nil) -> ToMatchResult { var messages = [String]() - //synchronizer state + // synchronizer state if flagSynchronizer.isOnline != isOnline { messages.append("isOnline equals \(flagSynchronizer.isOnline)") } @@ -76,7 +76,7 @@ final class FlagSynchronizerSpec: QuickSpec { messages.append("pollingActive equals \(flagSynchronizer.pollingActive)") } - //flag requests + // flag requests if serviceMock.getFeatureFlagsCallCount != flagRequests { messages.append("flag requests equals \(serviceMock.getFeatureFlagsCallCount)") } @@ -214,7 +214,7 @@ final class FlagSynchronizerSpec: QuickSpec { testContext.flagSynchronizer.isOnline = true } it("starts streaming") { - //streaming expects a ping on successful connection that triggers a flag request. No ping means no flag requests + // streaming expects a ping on successful connection that triggers a flag request. No ping means no flag requests expect({ testContext.synchronizerState(synchronizerOnline: true, streamingMode: .streaming, flagRequests: 0, @@ -235,7 +235,7 @@ final class FlagSynchronizerSpec: QuickSpec { testContext.flagSynchronizer.isOnline = false } it("starts polling") { - //polling starts by requesting flags + // polling starts by requesting flags expect({ testContext.synchronizerState(synchronizerOnline: true, streamingMode: .polling, flagRequests: 1, streamCreated: false) }).to(match()) } } @@ -997,7 +997,7 @@ final class FlagSynchronizerSpec: QuickSpec { testContext.flagSynchronizer.isOnline = true waitUntil(timeout: .seconds(2)) { done in - //In polling mode, the flagSynchronizer makes a flag request when set online right away. To verify the timer this test waits the polling interval (1s) for a second flag request + // In polling mode, the flagSynchronizer makes a flag request when set online right away. To verify the timer this test waits the polling interval (1s) for a second flag request testContext.flagSynchronizer.onSyncComplete = { result in if case .success(let flags, let streamEvent) = result { (newFlags, streamingEvent) = (flags.flagCollection, streamEvent) @@ -1014,7 +1014,7 @@ final class FlagSynchronizerSpec: QuickSpec { expect(newFlags == DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, includeVariations: true, includeVersions: true)).to(beTrue()) expect(streamingEvent).to(beNil()) } - //This particular test causes a retain cycle between the FlagSynchronizer and something else. By removing onSyncComplete, the closure is no longer called after the test is complete. + // This particular test causes a retain cycle between the FlagSynchronizer and something else. By removing onSyncComplete, the closure is no longer called after the test is complete. afterEach { testContext.flagSynchronizer.onSyncComplete = nil } @@ -1143,7 +1143,7 @@ final class FlagSynchronizerSpec: QuickSpec { } describe("makeFlagRequest") { var testContext: TestContext! - //This test completes the test suite on makeFlagRequest by validating the method bails out if it's called and the synchronizer is offline. While that shouldn't happen, there are 2 code paths that don't directly verify the SDK is online before calling the method, so it seems a wise precaution to validate that the method does bailout. Other tests exercise the rest of the method. + // This test completes the test suite on makeFlagRequest by validating the method bails out if it's called and the synchronizer is offline. While that shouldn't happen, there are 2 code paths that don't directly verify the SDK is online before calling the method, so it seems a wise precaution to validate that the method does bailout. Other tests exercise the rest of the method. context("offline") { var synchronizingError: SynchronizingError? beforeEach { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift index 1f008db0..2e11a23d 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift @@ -54,9 +54,9 @@ final class LDTimerSpec: QuickSpec { expect(testContext.ldTimer).toNot(beNil()) expect(testContext.ldTimer.timer).toNot(beNil()) expect(testContext.ldTimer.testFireQueue.label) == Constants.fireQueueLabel - expect(testContext.ldTimer.isRepeating) == testContext.repeats //true + expect(testContext.ldTimer.isRepeating) == testContext.repeats // true expect(testContext.ldTimer.isCancelled) == false - expect(testContext.ldTimer.fireDate?.isWithin(1.0, of: testContext.fireDate)).to(beTrue()) //1 second is arbitrary...just want it to be "close" + expect(testContext.ldTimer.fireDate?.isWithin(1.0, of: testContext.fireDate)).to(beTrue()) // 1 second is arbitrary...just want it to be "close" } } context("one-time timer") { @@ -67,9 +67,9 @@ final class LDTimerSpec: QuickSpec { expect(testContext.ldTimer).toNot(beNil()) expect(testContext.ldTimer.timer).toNot(beNil()) expect(testContext.ldTimer.testFireQueue.label) == Constants.fireQueueLabel - expect(testContext.ldTimer.isRepeating) == testContext.repeats //false + expect(testContext.ldTimer.isRepeating) == testContext.repeats // false expect(testContext.ldTimer.isCancelled) == false - expect(testContext.ldTimer.fireDate?.isWithin(1.0, of: testContext.fireDate)).to(beTrue()) //1 second is arbitrary...just want it to be "close" + expect(testContext.ldTimer.fireDate?.isWithin(1.0, of: testContext.fireDate)).to(beTrue()) // 1 second is arbitrary...just want it to be "close" } } } @@ -83,7 +83,7 @@ final class LDTimerSpec: QuickSpec { context("one-time timer") { beforeEach { waitUntil { done in - //timeInterval is arbitrary here. "Fast" so the test doesn't take a long time. + // timeInterval is arbitrary here. "Fast" so the test doesn't take a long time. testContext = TestContext(timeInterval: Constants.oneMilli, repeats: false, execute: { fireQueueLabel = DispatchQueue.currentQueueLabel done() @@ -99,13 +99,13 @@ final class LDTimerSpec: QuickSpec { context("repeating timer") { beforeEach { waitUntil { done in - //timeInterval is arbitrary here. "Fast" so the test doesn't take a long time. + // timeInterval is arbitrary here. "Fast" so the test doesn't take a long time. testContext = TestContext(timeInterval: Constants.oneMilli, repeats: true, execute: { if fireQueueLabel == nil { fireQueueLabel = DispatchQueue.currentQueueLabel } if fireCount < Constants.targetFireCount { - fireCount += 1 //If the timer fires again before the test is done, that's ok. This just measures an arbitrary point in time. + fireCount += 1 // If the timer fires again before the test is done, that's ok. This just measures an arbitrary point in time. if fireCount == Constants.targetFireCount { done() } @@ -121,7 +121,7 @@ final class LDTimerSpec: QuickSpec { expect(testContext.ldTimer.timer?.isValid) == true expect(fireQueueLabel).toNot(beNil()) expect(fireQueueLabel) == Constants.fireQueueLabel - expect(fireCount) == Constants.targetFireCount //targetFireCount is 5, and totally arbitrary. Want to measure that the repeating timer does in fact repeat. + expect(fireCount) == Constants.targetFireCount // targetFireCount is 5, and totally arbitrary. Want to measure that the repeating timer does in fact repeat. } } } @@ -137,7 +137,7 @@ final class LDTimerSpec: QuickSpec { testContext.ldTimer.cancel() } it("cancels the timer") { - expect(testContext.ldTimer.timer?.isValid ?? false) == false //the timer either doesn't exist or is invalid...could be either depending on timing + expect(testContext.ldTimer.timer?.isValid ?? false) == false // the timer either doesn't exist or is invalid...could be either depending on timing expect(testContext.ldTimer.isCancelled) == true } } @@ -148,7 +148,7 @@ final class LDTimerSpec: QuickSpec { testContext.ldTimer.cancel() } it("cancels the timer") { - expect(testContext.ldTimer.timer?.isValid ?? false) == false //the timer either doesn't exist or is invalid...could be either depending on timing + expect(testContext.ldTimer.timer?.isValid ?? false) == false // the timer either doesn't exist or is invalid...could be either depending on timing expect(testContext.ldTimer.isCancelled) == true } } @@ -158,6 +158,6 @@ final class LDTimerSpec: QuickSpec { extension DispatchQueue { class var currentQueueLabel: String? { - String(validatingUTF8: __dispatch_queue_get_label(nil)) //from https://gitlab.com/theswiftdev/swift/snippets/1741827/raw + String(validatingUTF8: __dispatch_queue_get_label(nil)) // from https://gitlab.com/theswiftdev/swift/snippets/1741827/raw } } diff --git a/Mintfile b/Mintfile index 0b78b0ea..8414be05 100644 --- a/Mintfile +++ b/Mintfile @@ -1,2 +1,2 @@ -realm/SwiftLint@0.39.2 -krzysztofzablocki/Sourcery@0.16.1 +realm/SwiftLint@0.43.1 +krzysztofzablocki/Sourcery@1.2.1 From 8ea40ad823c38dbe2f67edf62f8d5e92d5eefc64 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Mon, 9 Aug 2021 21:19:04 +0000 Subject: [PATCH 07/50] [ch115624] Fixes for polling mode (#158) --- LaunchDarkly.xcodeproj/project.pbxproj | 22 +- .../GeneratedCode/mocks.generated.swift | 13 +- LaunchDarkly/LaunchDarkly/LDClient.swift | 13 +- .../ConnectionModeChangeObserver.swift | 2 +- .../Models/{User => }/LDUser.swift | 0 .../Networking/DarklyService.swift | 220 ++-- .../LaunchDarkly/Networking/HTTPHeaders.swift | 23 +- .../ServiceObjects/ClientServiceFactory.swift | 23 +- .../ServiceObjects/FlagChangeNotifier.swift | 95 +- .../ServiceObjects/FlagSynchronizer.swift | 7 +- .../LaunchDarklyTests/LDClientSpec.swift | 8 +- .../Mocks/ClientServiceMockFactory.swift | 8 +- .../Mocks/DarklyServiceMock.swift | 27 +- .../Networking/DarklyServiceSpec.swift | 623 ++++-------- .../Networking/HTTPHeadersSpec.swift | 56 - .../Networking/HTTPURLResponse.swift | 11 - .../Networking/URLCacheSpec.swift | 183 ---- .../Networking/URLRequestSpec.swift | 33 +- .../FlagChangeNotifierSpec.swift | 954 +++++++----------- 19 files changed, 699 insertions(+), 1622 deletions(-) rename LaunchDarkly/LaunchDarkly/Models/{User => }/LDUser.swift (100%) delete mode 100644 LaunchDarkly/LaunchDarklyTests/Networking/URLCacheSpec.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 54977b2f..ed81d2c9 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; - 830DB3AA223409D800D65D25 /* URLCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3A9223409D800D65D25 /* URLCacheSpec.swift */; }; 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */; }; 830DB3AE2239B54900D65D25 /* URLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AD2239B54900D65D25 /* URLResponse.swift */; }; 830DB3AF2239B54900D65D25 /* URLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AD2239B54900D65D25 /* URLResponse.swift */; }; @@ -373,7 +372,6 @@ /* Begin PBXFileReference section */ 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLRequest.swift; sourceTree = ""; }; - 830DB3A9223409D800D65D25 /* URLCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLCacheSpec.swift; sourceTree = ""; }; 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersSpec.swift; sourceTree = ""; }; 830DB3AD2239B54900D65D25 /* URLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponse.swift; sourceTree = ""; }; 831188382113A16900D77CB5 /* LaunchDarkly_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LaunchDarkly_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -583,7 +581,6 @@ 83396BC81F7C3711000E256E /* DarklyServiceSpec.swift */, 832307A51F7D8D720029815A /* URLRequestSpec.swift */, 8392FFA22033565700320914 /* HTTPURLResponse.swift */, - 830DB3A9223409D800D65D25 /* URLCacheSpec.swift */, 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */, ); path = Networking; @@ -708,14 +705,14 @@ 8354EFE61F263E4200C05156 /* Models */ = { isa = PBXGroup; children = ( - 8354EFDD1F26380700C05156 /* LDConfig.swift */, - 83EBCB9E20D9A120003A7142 /* User */, - 83EBCB9D20D9A0A1003A7142 /* FeatureFlag */, - 8354EFDE1F26380700C05156 /* Event.swift */, - 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */, 8354AC5F224150C300CDE602 /* Cache */, C408884823033B7500420721 /* ConnectionInformation.swift */, B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */, + 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */, + 8354EFDE1F26380700C05156 /* Event.swift */, + 83EBCB9D20D9A0A1003A7142 /* FeatureFlag */, + 8354EFDD1F26380700C05156 /* LDConfig.swift */, + 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */, ); path = Models; sourceTree = ""; @@ -792,14 +789,6 @@ path = FeatureFlag; sourceTree = ""; }; - 83EBCB9E20D9A120003A7142 /* User */ = { - isa = PBXGroup; - children = ( - 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */, - ); - path = User; - sourceTree = ""; - }; 83EBCB9F20D9A143003A7142 /* FlagChange */ = { isa = PBXGroup; children = ( @@ -1474,7 +1463,6 @@ 8335299E1FC37727001166F8 /* FlagMaintainingMock.swift in Sources */, 83383A5120460DD30024D975 /* SynchronizingErrorSpec.swift in Sources */, 8354AC6E22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift in Sources */, - 830DB3AA223409D800D65D25 /* URLCacheSpec.swift in Sources */, 83B9A080204F56F4000C3F17 /* FlagChangeObserverSpec.swift in Sources */, 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, 83D15235225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift in Sources */, diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index d049923a..7a9069f3 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -371,12 +371,19 @@ final class FlagChangeNotifyingMock: FlagChangeNotifying { notifyConnectionModeChangedObserversCallback?() } + var notifyUnchangedCallCount = 0 + var notifyUnchangedCallback: (() -> Void)? + func notifyUnchanged() { + notifyUnchangedCallCount += 1 + notifyUnchangedCallback?() + } + var notifyObserversCallCount = 0 var notifyObserversCallback: (() -> Void)? - var notifyObserversReceivedArguments: (flagStore: FlagMaintaining, oldFlags: [LDFlagKey: FeatureFlag])? - func notifyObservers(flagStore: FlagMaintaining, oldFlags: [LDFlagKey: FeatureFlag]) { + var notifyObserversReceivedArguments: (oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag])? + func notifyObservers(oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) { notifyObserversCallCount += 1 - notifyObserversReceivedArguments = (flagStore: flagStore, oldFlags: oldFlags) + notifyObserversReceivedArguments = (oldFlags: oldFlags, newFlags: newFlags) notifyObserversCallback?() } } diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 91457ebc..e26ce92d 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -269,6 +269,7 @@ public class LDClient { } let config: LDConfig + let service: DarklyServiceProvider private(set) var user: LDUser /** @@ -310,6 +311,7 @@ public class LDClient { flagStore.replaceStore(newFlags: user.flagStore?.featureFlags ?? [:], completion: nil) } self.service.user = self.user + self.service.clearFlagResponseCache() flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), pollingInterval: config.flagPollingInterval(runMode: runMode), useReport: config.useReport, @@ -325,15 +327,11 @@ public class LDClient { if !config.autoAliasingOptOut && previousUser.isAnonymous && !newUser.isAnonymous { self.alias(context: newUser, previousContext: previousUser) } - - self.service.clearFlagResponseCache() } } private let internalIdentifyQueue: DispatchQueue = DispatchQueue(label: "InternalIdentifyQueue") - let service: DarklyServiceProvider - // MARK: Retrieving Flag Values /** @@ -694,6 +692,9 @@ public class LDClient { self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) } } + case .upToDate: + connectionInformation.lastKnownFlagValidity = Date() + flagChangeNotifier.notifyUnchanged() case .error(let synchronizingError): process(synchronizingError, logPrefix: typeName(and: #function, appending: ": ")) } @@ -713,7 +714,7 @@ public class LDClient { private func updateCacheAndReportChanges(user: LDUser, oldFlags: [LDFlagKey: FeatureFlag]) { flagCache.storeFeatureFlags(flagStore.featureFlags, userKey: user.key, mobileKey: config.mobileKey, lastUpdated: Date(), storeMode: .async) - flagChangeNotifier.notifyObservers(flagStore: flagStore, oldFlags: oldFlags) + flagChangeNotifier.notifyObservers(oldFlags: oldFlags, newFlags: flagStore.featureFlags) } // MARK: Events @@ -828,8 +829,6 @@ public class LDClient { return } - HTTPHeaders.removeFlagRequestEtags() - let internalUser = user LDClient.instances = [:] diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift index 97ae45bb..5d5a853d 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift @@ -9,7 +9,7 @@ import Foundation struct ConnectionModeChangedObserver { private(set) weak var owner: LDObserverOwner? - let connectionModeChangedHandler: LDConnectionModeChangedHandler? + let connectionModeChangedHandler: LDConnectionModeChangedHandler init(owner: LDObserverOwner, connectionModeChangedHandler: @escaping LDConnectionModeChangedHandler) { self.owner = owner diff --git a/LaunchDarkly/LaunchDarkly/Models/User/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift similarity index 100% rename from LaunchDarkly/LaunchDarkly/Models/User/LDUser.swift rename to LaunchDarkly/LaunchDarkly/Models/LDUser.swift diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 08797263..31048af9 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -52,16 +52,13 @@ final class DarklyService: DarklyServiceProvider { static let report = "REPORT" } - struct ReasonsPath { - static let reasons = URLQueryItem(name: "withReasons", value: "true") - } - let config: LDConfig var user: LDUser let httpHeaders: HTTPHeaders let diagnosticCache: DiagnosticCaching? private (set) var serviceFactory: ClientServiceCreating private var session: URLSession + var flagRequestEtag: String? init(config: LDConfig, user: LDUser, serviceFactory: ClientServiceCreating) { self.config = config @@ -75,85 +72,66 @@ final class DarklyService: DarklyServiceProvider { } self.httpHeaders = HTTPHeaders(config: config, environmentReporter: serviceFactory.makeEnvironmentReporter()) - self.session = URLSession(configuration: URLSessionConfiguration.default) + // URLSessionConfiguration is a class, but `.default` creates a new instance. This does not effect other session configuration. + let sessionConfig = URLSessionConfiguration.default + // We always revalidate the cache which we handle manually + sessionConfig.requestCachePolicy = .reloadIgnoringLocalCacheData + sessionConfig.urlCache = nil + self.session = URLSession(configuration: sessionConfig) } // MARK: Feature Flags - private func requestTask(with: URLRequest, - completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void - ) -> URLSessionDataTask { - // copying the request is needed because swift passes by const reference without any real way of changing that - var req = with - if let headerDelegate = config.headerDelegate { - req.allHTTPHeaderFields = headerDelegate(with.url!, req.allHTTPHeaderFields ?? [:]) - } - return self.session.dataTask(with: req, completionHandler: completionHandler) + func clearFlagResponseCache() { + flagRequestEtag = nil } func getFeatureFlags(useReport: Bool, completion: ServiceCompletionHandler?) { - guard !config.mobileKey.isEmpty, - let flagRequest = flagRequest(useReport: useReport) + guard hasMobileKey(#function) else { return } + guard let userJson = user.dictionaryValue(includePrivateAttributes: true, config: config).jsonData else { - if config.mobileKey.isEmpty { - Log.debug(typeName(and: #function, appending: ": ") + "Aborting. No mobileKey.") - } else { - Log.debug(typeName(and: #function, appending: ": ") + "Aborting. Unable to create flagRequest.") - } + Log.debug(typeName(and: #function, appending: ": ") + "Aborting. Unable to create flagRequest.") return } - let dataTask = requestTask(with: flagRequest) { [weak self] data, response, error in - DispatchQueue.main.async { - self?.processEtag(from: (data, response, error)) - completion?((data, response, error)) - } - } - dataTask.resume() - } - private func flagRequest(useReport: Bool) -> URLRequest? { - guard let flagRequestUrl = flagRequestUrl(useReport: useReport) - else { return nil } - var request = URLRequest(url: flagRequestUrl, cachePolicy: flagRequestCachePolicy, timeoutInterval: config.connectionTimeout) - request.appendHeaders(httpHeaders.flagRequestHeaders) + var headers = httpHeaders.flagRequestHeaders + if let etag = flagRequestEtag { + headers.merge([HTTPHeaders.HeaderKey.ifNoneMatch: etag]) { orig, _ in orig } + } + var request = URLRequest(url: flagRequestUrl(useReport: useReport, getData: userJson), + ldHeaders: headers, + ldConfig: config) if useReport { - guard let userData = user.dictionaryValue(includePrivateAttributes: true, config: config).jsonData - else { return nil } request.httpMethod = URLRequest.HTTPMethods.report - request.httpBody = userData + request.httpBody = userJson } - return request + self.session.dataTask(with: request) { [weak self] data, response, error in + DispatchQueue.main.async { + self?.processEtag(from: (data, response, error)) + completion?((data, response, error)) + } + }.resume() } - // The flagRequestCachePolicy varies to allow the SDK to force a reload from the source on a user change. Both the SDK and iOS keep the etag from the last request. On a user change if we use .useProtocolCachePolicy, even though the SDK doesn't supply the etag, iOS does (despite clearing the URLCache!!!). In order to force iOS to ignore the etag, change the policy to .reloadIgnoringLocalCache when there is no etag. - // Note that after setting .reloadRevalidatingCacheData on the request, the property appears not to accept it, and instead sets .reloadIgnoringLocalCacheData. Despite this, there does appear to be a difference in cache policy, because the SDK behaves as expected: on a new user it requests flags without the cache, and on a request with an etag it requests flags allowing the cache. Although testing shows that we could always set .reloadIgnoringLocalCacheData here, because that is NOT symantecally the desired behavior, the method distinguishes between the use cases. - // watchOS logs an error when .useProtocolCachePolicy is set for flag requests with an etag. By setting .reloadRevalidatingCacheData, the SDK behaves correctly, but watchOS does not log an error. - private var flagRequestCachePolicy: URLRequest.CachePolicy { - return httpHeaders.hasFlagRequestEtag ? .reloadRevalidatingCacheData : .reloadIgnoringLocalCacheData - } - - private func flagRequestUrl(useReport: Bool) -> URL? { - if useReport { - return shouldGetReasons(url: config.baseUrl.appendingPathComponent(FlagRequestPath.report)) - } - guard let encodedUser = user - .dictionaryValue(includePrivateAttributes: true, config: config) - .base64UrlEncodedString - else { - return nil + private func flagRequestUrl(useReport: Bool, getData: Data) -> URL { + var flagRequestUrl = config.baseUrl + if !useReport { + flagRequestUrl.appendPathComponent(FlagRequestPath.get, isDirectory: true) + flagRequestUrl.appendPathComponent(getData.base64UrlEncodedString, isDirectory: false) + } else { + flagRequestUrl.appendPathComponent(FlagRequestPath.report, isDirectory: false) } - return shouldGetReasons(url: config.baseUrl.appendingPathComponent(FlagRequestPath.get).appendingPathComponent(encodedUser)) + return shouldGetReasons(url: flagRequestUrl) } - + private func shouldGetReasons(url: URL) -> URL { - if config.evaluationReasons { - var urlComponent = URLComponents(url: url, resolvingAgainstBaseURL: false) - urlComponent?.queryItems = [ReasonsPath.reasons] - return urlComponent?.url ?? url - } else { - return url - } + guard config.evaluationReasons + else { return url } + + var urlComponent = URLComponents(url: url, resolvingAgainstBaseURL: false) + urlComponent?.queryItems = [URLQueryItem(name: "withReasons", value: "true")] + return urlComponent?.url ?? url } private func processEtag(from serviceResponse: ServiceResponse) { @@ -162,17 +140,11 @@ final class DarklyService: DarklyServiceProvider { serviceResponse.data?.jsonDictionary != nil else { if serviceResponse.urlResponse?.httpStatusCode != HTTPURLResponse.StatusCodes.notModified { - HTTPHeaders.setFlagRequestEtag(nil, for: config.mobileKey) + flagRequestEtag = nil } return } - HTTPHeaders.setFlagRequestEtag(serviceResponse.urlResponse?.httpHeaderEtag, for: config.mobileKey) - } - - // Although this does not need any info stored in the DarklyService instance, LDClient shouldn't have to distinguish between an actual and a mock. Making this an instance method does that. - func clearFlagResponseCache() { - URLCache.shared.removeAllCachedResponses() - HTTPHeaders.removeFlagRequestEtags() + flagRequestEtag = serviceResponse.urlResponse?.httpHeaderEtag } // MARK: Streaming @@ -180,98 +152,76 @@ final class DarklyService: DarklyServiceProvider { func createEventSource(useReport: Bool, handler: EventHandler, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { + let userJsonData = user.dictionaryValue(includePrivateAttributes: true, config: config).jsonData + + var streamRequestUrl = config.streamUrl.appendingPathComponent(StreamRequestPath.meval) + var connectMethod = HTTPRequestMethod.get + var connectBody: Data? + if useReport { - return serviceFactory.makeStreamingProvider(url: reportStreamRequestUrl, - httpHeaders: httpHeaders.eventSourceHeaders, - connectMethod: DarklyService.HTTPRequestMethod.report, - connectBody: user - .dictionaryValue(includePrivateAttributes: true, config: config) - .jsonData, - handler: handler, - delegate: config.headerDelegate, - errorHandler: errorHandler) + connectMethod = HTTPRequestMethod.report + connectBody = userJsonData + } else { + streamRequestUrl.appendPathComponent(userJsonData?.base64UrlEncodedString ?? "", isDirectory: false) } - return serviceFactory.makeStreamingProvider(url: getStreamRequestUrl, + + return serviceFactory.makeStreamingProvider(url: shouldGetReasons(url: streamRequestUrl), httpHeaders: httpHeaders.eventSourceHeaders, + connectMethod: connectMethod, + connectBody: connectBody, handler: handler, delegate: config.headerDelegate, errorHandler: errorHandler) } - private var getStreamRequestUrl: URL { - shouldGetReasons(url: config.streamUrl.appendingPathComponent(StreamRequestPath.meval) - .appendingPathComponent(user - .dictionaryValue(includePrivateAttributes: true, config: config) - .base64UrlEncodedString ?? "")) - } - private var reportStreamRequestUrl: URL { - shouldGetReasons(url: config.streamUrl.appendingPathComponent(StreamRequestPath.meval)) - } - // MARK: Publish Events func publishEventDictionaries(_ eventDictionaries: [[String: Any]], _ payloadId: String, completion: ServiceCompletionHandler?) { - guard !config.mobileKey.isEmpty, - !eventDictionaries.isEmpty + guard hasMobileKey(#function) else { return } + guard !eventDictionaries.isEmpty, let eventData = eventDictionaries.jsonData else { - if config.mobileKey.isEmpty { - Log.debug(typeName(and: #function, appending: ": ") + "Aborting. No mobileKey.") - } else { - Log.debug(typeName(and: #function, appending: ": ") + "Aborting. No event dictionary.") - } - return + return Log.debug(typeName(and: #function, appending: ": ") + "Aborting. No event dictionary.") } - let dataTask = requestTask(with: eventRequest(eventDictionaries: eventDictionaries, payloadId: payloadId)) { (data, response, error) in - completion?((data, response, error)) - } - dataTask.resume() - } - private func eventRequest(eventDictionaries: [[String: Any]], payloadId: String) -> URLRequest { - var request = URLRequest(url: eventUrl, cachePolicy: .useProtocolCachePolicy, timeoutInterval: config.connectionTimeout) - request.appendHeaders([HTTPHeaders.HeaderKey.eventPayloadIDHeader: payloadId]) - request.appendHeaders(httpHeaders.eventRequestHeaders) - request.httpMethod = URLRequest.HTTPMethods.post - request.httpBody = eventDictionaries.jsonData - - return request - } - - private var eventUrl: URL { - config.eventsUrl.appendingPathComponent(EventRequestPath.bulk) + let url = config.eventsUrl.appendingPathComponent(EventRequestPath.bulk) + let headers = [HTTPHeaders.HeaderKey.eventPayloadIDHeader: payloadId].merging(httpHeaders.eventRequestHeaders) { $1 } + doPublish(url: url, headers: headers, body: eventData, completion: completion) } func publishDiagnostic(diagnosticEvent: T, completion: ServiceCompletionHandler?) { - guard !config.mobileKey.isEmpty - else { - Log.debug(typeName(and: #function, appending: ": ") + "Aborting. No mobile key.") - return - } - let dataTask = requestTask(with: diagnosticRequest(diagnosticEvent: diagnosticEvent)) { data, response, error in - completion?((data, response, error)) - } - dataTask.resume() + guard hasMobileKey(#function), + let bodyData = try? JSONEncoder().encode(diagnosticEvent) + else { return } + + let url = config.eventsUrl.appendingPathComponent(EventRequestPath.diagnostic) + doPublish(url: url, headers: httpHeaders.diagnosticRequestHeaders, body: bodyData, completion: completion) } - private func diagnosticRequest(diagnosticEvent: T) -> URLRequest { - var request = URLRequest(url: diagnosticUrl, cachePolicy: .useProtocolCachePolicy, timeoutInterval: config.connectionTimeout) - request.appendHeaders(httpHeaders.diagnosticRequestHeaders) + private func doPublish(url: URL, headers: [String: String], body: Data, completion: ServiceCompletionHandler?) { + var request = URLRequest(url: url, ldHeaders: headers, ldConfig: config) request.httpMethod = URLRequest.HTTPMethods.post - request.httpBody = try? JSONEncoder().encode(diagnosticEvent) - return request + request.httpBody = body + + session.dataTask(with: request) { data, response, error in + completion?((data, response, error)) + }.resume() } - private var diagnosticUrl: URL { - config.eventsUrl.appendingPathComponent(EventRequestPath.diagnostic) + private func hasMobileKey(_ location: String) -> Bool { + if config.mobileKey.isEmpty { + Log.debug(typeName(and: location, appending: ": ") + "Aborting. No mobile key.") + } + return !config.mobileKey.isEmpty } } extension DarklyService: TypeIdentifying { } extension URLRequest { - mutating func appendHeaders(_ newHeaders: [String: String]) { + init(url: URL, ldHeaders: [String: String], ldConfig: LDConfig) { + self.init(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: ldConfig.connectionTimeout) var headers = self.allHTTPHeaderFields ?? [:] - headers.merge(newHeaders) { $1 } - self.allHTTPHeaderFields = headers + headers.merge(ldHeaders) { $1 } + self.allHTTPHeaderFields = ldConfig.headerDelegate?(url, headers) ?? headers } } diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift index e2669908..a257488c 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift @@ -26,16 +26,6 @@ struct HTTPHeaders { static let eventSchema3 = "3" } - private(set) static var flagRequestEtags = [String: String]() - - static func removeFlagRequestEtags() { - flagRequestEtags.removeAll() - } - - static func setFlagRequestEtag(_ etag: String?, for mobileKey: String) { - flagRequestEtags[mobileKey] = etag - } - private let mobileKey: String private let additionalHeaders: [String: String] private let authKey: String @@ -71,18 +61,7 @@ struct HTTPHeaders { } var eventSourceHeaders: [String: String] { withAdditionalHeaders(baseHeaders) } - - var flagRequestHeaders: [String: String] { - var headers = baseHeaders - if let etag = HTTPHeaders.flagRequestEtags[mobileKey] { - headers[HeaderKey.ifNoneMatch] = etag - } - return withAdditionalHeaders(headers) - } - - var hasFlagRequestEtag: Bool { - HTTPHeaders.flagRequestEtags[mobileKey] != nil - } + var flagRequestHeaders: [String: String] { withAdditionalHeaders(baseHeaders) } var eventRequestHeaders: [String: String] { var headers = baseHeaders diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index e6b3346f..30e749fc 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -23,8 +23,7 @@ protocol ClientServiceCreating { func makeFlagChangeNotifier() -> FlagChangeNotifying func makeEventReporter(service: DarklyServiceProvider) -> EventReporting func makeEventReporter(service: DarklyServiceProvider, onSyncComplete: EventSyncCompleteClosure?) -> EventReporting - func makeStreamingProvider(url: URL, httpHeaders: [String: String], handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider - func makeStreamingProvider(url: URL, httpHeaders: [String: String], connectMethod: String?, connectBody: Data?, handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider + func makeStreamingProvider(url: URL, httpHeaders: [String: String], connectMethod: String, connectBody: Data?, handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider func makeEnvironmentReporter() -> EnvironmentReporting func makeThrottler(environmentReporter: EnvironmentReporting) -> Throttling func makeErrorNotifier() -> ErrorNotifying @@ -84,23 +83,9 @@ final class ClientServiceFactory: ClientServiceCreating { EventReporter(service: service, onSyncComplete: onSyncComplete) } - func makeStreamingProvider(url: URL, - httpHeaders: [String: String], - handler: EventHandler, - delegate: RequestHeaderTransform?, - errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { - var config: EventSource.Config = EventSource.Config(handler: handler, url: url) - config.headers = httpHeaders - config.headerTransform = { delegate?(url, $0) ?? $0 } - if let errorHandler = errorHandler { - config.connectionErrorHandler = errorHandler - } - return EventSource(config: config) - } - func makeStreamingProvider(url: URL, httpHeaders: [String: String], - connectMethod: String?, + connectMethod: String, connectBody: Data?, handler: EventHandler, delegate: RequestHeaderTransform?, @@ -108,12 +93,10 @@ final class ClientServiceFactory: ClientServiceCreating { var config: EventSource.Config = EventSource.Config(handler: handler, url: url) config.headerTransform = { delegate?(url, $0) ?? $0 } config.headers = httpHeaders + config.method = connectMethod if let errorHandler = errorHandler { config.connectionErrorHandler = errorHandler } - if let method = connectMethod { - config.method = method - } if let body = connectBody { config.body = body } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift index d1681f82..f11138c5 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift @@ -14,13 +14,15 @@ protocol FlagChangeNotifying { func addConnectionModeChangedObserver(_ observer: ConnectionModeChangedObserver) func removeObserver(owner: LDObserverOwner) func notifyConnectionModeChangedObservers(connectionMode: ConnectionInformation.ConnectionMode) - func notifyObservers(flagStore: FlagMaintaining, oldFlags: [LDFlagKey: FeatureFlag]) + func notifyUnchanged() + func notifyObservers(oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) } final class FlagChangeNotifier: FlagChangeNotifying { - private var flagChangeObservers = [FlagChangeObserver]() - private var flagsUnchangedObservers = [FlagsUnchangedObserver]() - private var connectionModeChangedObservers = [ConnectionModeChangedObserver]() + // Exposed for testing + private (set) var flagChangeObservers = [FlagChangeObserver]() + private (set) var flagsUnchangedObservers = [FlagsUnchangedObserver]() + private (set) var connectionModeChangedObservers = [ConnectionModeChangedObserver]() private var flagChangeQueue = DispatchQueue(label: "com.launchdarkly.FlagChangeNotifier.FlagChangeQueue") private var flagsUnchangedQueue = DispatchQueue(label: "com.launchdarkly.FlagChangeNotifier.FlagsUnchangedQueue") private var connectionModeChangedQueue = DispatchQueue(label: "com.launchdarkly.FlagChangeNotifier.ConnectionModeChangedQueue") @@ -50,37 +52,41 @@ final class FlagChangeNotifier: FlagChangeNotifying { func notifyConnectionModeChangedObservers(connectionMode: ConnectionInformation.ConnectionMode) { connectionModeChangedQueue.sync { - connectionModeChangedObservers.forEach { connectionModeChangedObserver in - if let connectionModeChangedHandler = connectionModeChangedObserver.connectionModeChangedHandler { - DispatchQueue.main.async { - connectionModeChangedHandler(connectionMode) - } + connectionModeChangedObservers.removeAll { $0.owner == nil } + connectionModeChangedObservers.forEach { observer in + DispatchQueue.main.async { + observer.connectionModeChangedHandler(connectionMode) } } } } - func notifyObservers(flagStore: FlagMaintaining, oldFlags: [LDFlagKey: FeatureFlag]) { + func notifyUnchanged() { removeOldObservers() - let changedFlagKeys = findChangedFlagKeys(oldFlags: oldFlags, newFlags: flagStore.featureFlags) - guard !changedFlagKeys.isEmpty - else { - if flagsUnchangedObservers.isEmpty { - Log.debug(typeName(and: #function) + "aborted. Flags unchanged and no flagsUnchanged observers set.") - } else { - Log.debug(typeName(and: #function) + "notifying observers that flags are unchanged.") - } - flagsUnchangedQueue.sync { - flagsUnchangedObservers.forEach { flagsUnchangedObserver in - DispatchQueue.main.async { - flagsUnchangedObserver.flagsUnchangedHandler() - } + if flagsUnchangedObservers.isEmpty { + Log.debug(typeName(and: #function) + "aborted. Flags unchanged and no flagsUnchanged observers set.") + } else { + Log.debug(typeName(and: #function) + "notifying observers that flags are unchanged.") + } + flagsUnchangedQueue.sync { + flagsUnchangedObservers.forEach { flagsUnchangedObserver in + DispatchQueue.main.async { + flagsUnchangedObserver.flagsUnchangedHandler() } } + } + } + + func notifyObservers(oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) { + let changedFlagKeys = findChangedFlagKeys(oldFlags: oldFlags, newFlags: newFlags) + guard !changedFlagKeys.isEmpty + else { + notifyUnchanged() return } + removeOldObservers() let selectedObservers = flagChangeQueue.sync { flagChangeObservers.filter { $0.flagKeys == LDFlagKey.anyKey || $0.flagKeys.contains { changedFlagKeys.contains($0) } } } @@ -90,10 +96,8 @@ final class FlagChangeNotifier: FlagChangeNotifying { return } - let changedFlags = [LDFlagKey: LDChangedFlag](uniqueKeysWithValues: changedFlagKeys.map { flagKey in - (flagKey, LDChangedFlag(key: flagKey, - oldValue: oldFlags[flagKey]?.value, - newValue: flagStore.featureFlags[flagKey]?.value)) + let changedFlags = [LDFlagKey: LDChangedFlag](uniqueKeysWithValues: changedFlagKeys.map { + ($0, LDChangedFlag(key: $0, oldValue: oldFlags[$0]?.value, newValue: newFlags[$0]?.value)) }) Log.debug(typeName(and: #function) + "notifying observers for changes to flags: \(changedFlags.keys.joined(separator: ", ")).") selectedObservers.forEach { observer in @@ -108,46 +112,21 @@ final class FlagChangeNotifier: FlagChangeNotifying { } } } - + private func removeOldObservers() { Log.debug(typeName(and: #function)) flagChangeQueue.sync { flagChangeObservers.removeAll { $0.owner == nil } } flagsUnchangedQueue.sync { flagsUnchangedObservers.removeAll { $0.owner == nil } } - connectionModeChangedQueue.sync { connectionModeChangedObservers.removeAll { $0.owner == nil } } } private func findChangedFlagKeys(oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey] { - oldFlags.symmetricDifference(newFlags) // symmetricDifference tests for equality, which includes version. Exclude version here. - .filter { flagKey in - guard let oldFeatureFlag = oldFlags[flagKey], - let newFeatureFlag = newFlags[flagKey] - else { - return true - } - return !(oldFeatureFlag.variation == newFeatureFlag.variation && - AnyComparer.isEqual(oldFeatureFlag.value, to: newFeatureFlag.value)) + oldFlags.symmetricDifference(newFlags) // symmetricDifference tests for equality, which includes version. Exclude version here. + .filter { + guard let old = oldFlags[$0], let new = newFlags[$0] + else { return true } + return !(old.variation == new.variation && AnyComparer.isEqual(old.value, to: new.value)) } } } extension FlagChangeNotifier: TypeIdentifying { } -// Test support -#if DEBUG - extension FlagChangeNotifier { - var flagObservers: [FlagChangeObserver] { flagChangeObservers } - var noChangeObservers: [FlagsUnchangedObserver] { flagsUnchangedObservers } - - convenience init(flagChangeObservers: [FlagChangeObserver], flagsUnchangedObservers: [FlagsUnchangedObserver]) { - self.init() - self.flagChangeObservers = flagChangeObservers - self.flagsUnchangedObservers = flagsUnchangedObservers - } - - func notifyObservers(flagStore: FlagMaintaining, oldFlags: [LDFlagKey: FeatureFlag], completion: @escaping () -> Void) { - notifyObservers(flagStore: flagStore, oldFlags: oldFlags) - DispatchQueue.main.async { - completion() - } - } - } -#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift index cb517408..c5d012dd 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift @@ -43,6 +43,7 @@ enum SynchronizingError: Error { enum FlagSyncResult { case success([String: Any], FlagUpdateType?) + case upToDate case error(SynchronizingError) } @@ -98,8 +99,6 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { self.useReport = useReport self.service = service self.onSyncComplete = onSyncComplete - - configureCommunications(isOnline: isOnline) } private func configureCommunications(isOnline: Bool) { @@ -231,6 +230,10 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { reportSyncComplete(.error(.request(serviceResponseError))) return } + if serviceResponse.urlResponse?.httpStatusCode == HTTPURLResponse.StatusCodes.notModified { + reportSyncComplete(.upToDate) + return + } guard serviceResponse.urlResponse?.httpStatusCode == HTTPURLResponse.StatusCodes.ok else { Log.debug(typeName(and: #function) + "response: \(String(describing: serviceResponse.urlResponse))") diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 05cc0bf6..fc29a0f0 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -1158,7 +1158,7 @@ final class LDClientSpec: QuickSpec { let receivedObserver = mockNotifier.addConnectionModeChangedObserverReceivedObserver expect(mockNotifier.addConnectionModeChangedObserverCallCount) == 1 expect(receivedObserver?.owner) === self - receivedObserver?.connectionModeChangedHandler?(ConnectionInformation.ConnectionMode.offline) + receivedObserver?.connectionModeChangedHandler(ConnectionInformation.ConnectionMode.offline) expect(callCount) == 1 } it("observeError") { @@ -1236,7 +1236,7 @@ final class LDClientSpec: QuickSpec { } it("informs the flag change notifier of the changed flags") { expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == testContext.cachedFlags).to(beTrue()) } } @@ -1275,7 +1275,7 @@ final class LDClientSpec: QuickSpec { } it("informs the flag change notifier of the changed flag") { expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) } } @@ -1311,7 +1311,7 @@ final class LDClientSpec: QuickSpec { } it("informs the flag change notifier of the changed flag") { expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) } } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index ed3cf5e4..1706b94c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -95,13 +95,7 @@ final class ClientServiceMockFactory: ClientServiceCreating { handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?)? - func makeStreamingProvider(url: URL, httpHeaders: [String: String], handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { - makeStreamingProviderCallCount += 1 - makeStreamingProviderReceivedArguments = (url, httpHeaders, nil, nil, handler, delegate, errorHandler) - return DarklyStreamingProviderMock() - } - - func makeStreamingProvider(url: URL, httpHeaders: [String: String], connectMethod: String?, connectBody: Data?, handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { + func makeStreamingProvider(url: URL, httpHeaders: [String: String], connectMethod: String, connectBody: Data?, handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { makeStreamingProviderCallCount += 1 makeStreamingProviderReceivedArguments = (url, httpHeaders, connectMethod, connectBody, handler, delegate, errorHandler) return DarklyStreamingProviderMock() diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 9143b991..8d2a27fd 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -98,8 +98,6 @@ final class DarklyServiceMock: DarklyServiceProvider { static let mockEventsUrl = URL(string: "https://dummy.events.com")! static let mockStreamUrl = URL(string: "https://dummy.stream.com")! - static let requestPathStream = "/mping" - static let stubNameFlag = "Flag Request Stub" static let stubNameStream = "Stream Connect Stub" static let stubNameEvent = "Event Report Stub" @@ -287,21 +285,22 @@ extension DarklyServiceMock { featureFlags: [LDFlagKey: FeatureFlag]? = nil, useReport: Bool, flagResponseEtag: String? = nil, - onActivation activate: ((URLRequest, HTTPStubsDescriptor, HTTPStubsResponse) -> Void)? = nil) { - + onActivation activate: ((URLRequest) -> Void)? = nil) { let stubbedFeatureFlags = featureFlags ?? Constants.stubFeatureFlags() let responseData = statusCode == HTTPURLResponse.StatusCodes.ok ? stubbedFeatureFlags.dictionaryValue.jsonData! : Data() let stubResponse: HTTPStubsResponseBlock = { _ in - var headers = [String: String]() + var headers: [String: String] = [:] if let flagResponseEtag = flagResponseEtag { headers = [HTTPURLResponse.HeaderKeys.etag: flagResponseEtag, - HTTPURLResponse.HeaderKeys.cacheControl: HTTPURLResponse.HeaderValues.maxAge] + "Cache-Control": "max-age=0"] } return HTTPStubsResponse(data: responseData, statusCode: Int32(statusCode), headers: headers) } stubRequest(passingTest: useReport ? reportFlagRequestStubTest : getFlagRequestStubTest, stub: stubResponse, - name: flagStubName(statusCode: statusCode, useReport: useReport), onActivation: activate) + name: flagStubName(statusCode: statusCode, useReport: useReport)) { request, _, _ in + activate?(request) + } } /// Use when testing requires the mock service to simulate a service response to the flag request callback @@ -365,13 +364,15 @@ extension DarklyServiceMock { } /// Use when testing requires the mock service to actually make an event request - func stubEventRequest(success: Bool, onActivation activate: ((URLRequest, HTTPStubsDescriptor, HTTPStubsResponse) -> Void)? = nil) { + func stubEventRequest(success: Bool, onActivation activate: ((URLRequest) -> Void)? = nil) { let stubResponse: HTTPStubsResponseBlock = success ? { _ in HTTPStubsResponse(data: Data(), statusCode: Int32(HTTPURLResponse.StatusCodes.accepted), headers: nil) } : { _ in HTTPStubsResponse(error: Constants.error) } - stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: Constants.stubNameEvent, onActivation: activate) + stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: Constants.stubNameEvent) { request, _, _ in + activate?(request) + } } /// Use when testing requires the mock service to provide a service response to the event request callback @@ -411,8 +412,6 @@ extension DarklyServiceMock { // MARK: Stub - var anyRequestStubTest: HTTPStubsTestBlock { { _ in true } } - private func stubRequest(passingTest test: @escaping HTTPStubsTestBlock, stub: @escaping HTTPStubsResponseBlock, name: String, @@ -435,12 +434,6 @@ extension DarklyServiceMock { } } -extension HTTPStubs { - class func stub(named name: String) -> HTTPStubsDescriptor? { - (HTTPStubs.allStubs() as? [HTTPStubsDescriptor])?.first { $0.name == name } - } -} - /** * Matcher testing that the `NSURLRequest` is using the **REPORT** `HTTPMethod` * diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index ab029e2d..a7f9e506 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -18,10 +18,6 @@ final class DarklyServiceSpec: QuickSpec { struct Constants { static let eventCount = 3 - static let mobileKeyCount = 3 - - static let emptyMobileKey = "" - static let useGetMethod = false static let useReportMethod = true } @@ -29,47 +25,39 @@ final class DarklyServiceSpec: QuickSpec { struct TestContext { let user = LDUser.stub() var config: LDConfig! - let mockEventDictionaries: [[String: Any]]? var serviceMock: DarklyServiceMock! - var serviceFactoryMock: ClientServiceMockFactory? { - service.serviceFactory as? ClientServiceMockFactory - } + var serviceFactoryMock: ClientServiceMockFactory = ClientServiceMockFactory() var service: DarklyService! - var flagRequestEtag: String? - var flagRequestEtags = [String: String]() var httpHeaders: HTTPHeaders - var flagStore: FlagMaintaining + let stubFlags = FlagMaintainingMock.stubFlags() init(mobileKey: String = LDConfig.Constants.mockMobileKey, useReport: Bool = Constants.useGetMethod, includeMockEventDictionaries: Bool = false, operatingSystemName: String? = nil, - flagRequestEtag: String? = nil, - mobileKeyCount: Int = 0, diagnosticOptOut: Bool = false) { - let serviceFactoryMock = ClientServiceMockFactory() if let operatingSystemName = operatingSystemName { serviceFactoryMock.makeEnvironmentReporterReturnValue.systemName = operatingSystemName } - flagStore = FlagStore(featureFlagDictionary: FlagMaintainingMock.stubFlags()) config = LDConfig.stub(mobileKey: mobileKey, environmentReporter: EnvironmentReportingMock()) config.useReport = useReport config.diagnosticOptOut = diagnosticOptOut - mockEventDictionaries = includeMockEventDictionaries ? Event.stubEventDictionaries(Constants.eventCount, user: user, config: config) : nil serviceMock = DarklyServiceMock(config: config) service = DarklyService(config: config, user: user, serviceFactory: serviceFactoryMock) httpHeaders = HTTPHeaders(config: config, environmentReporter: config.environmentReporter) - self.flagRequestEtag = flagRequestEtag - if let etag = flagRequestEtag { - HTTPHeaders.setFlagRequestEtag(etag, for: mobileKey) - } - while flagRequestEtags.count < mobileKeyCount { - if flagRequestEtags.isEmpty { - flagRequestEtags[mobileKey] = flagRequestEtag ?? UUID().uuidString - } else { - flagRequestEtags[UUID().uuidString] = UUID().uuidString - } + } + + func mockEventDictionaries() -> [[String: Any]] { + Event.stubEventDictionaries(Constants.eventCount, user: user, config: config) + } + + func runStubbedGet(statusCode: Int, featureFlags: [LDFlagKey: FeatureFlag]? = nil, flagResponseEtag: String? = nil) { + serviceMock.stubFlagRequest(statusCode: statusCode, useReport: config.useReport, flagResponseEtag: flagResponseEtag) + waitUntil { done in + self.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in + done() + }) } } } @@ -85,12 +73,12 @@ final class DarklyServiceSpec: QuickSpec { afterEach { HTTPStubs.removeAllStubs() - HTTPHeaders.removeFlagRequestEtags() } } private func getFeatureFlagsSpec() { var testContext: TestContext! + var requestEtag: String! describe("getFeatureFlags") { var responses: ServiceResponses? @@ -98,6 +86,7 @@ final class DarklyServiceSpec: QuickSpec { var reportRequestCount = 0 var urlRequest: URLRequest? beforeEach { + requestEtag = UUID().uuidString (responses, getRequestCount, reportRequestCount, urlRequest) = (nil, 0, 0, nil) } @@ -110,15 +99,15 @@ final class DarklyServiceSpec: QuickSpec { beforeEach { waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.flagStore.featureFlags, + featureFlags: testContext.stubFlags, useReport: Constants.useReportMethod, - onActivation: { _, _, _ in + onActivation: { _ in reportRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.flagStore.featureFlags, + featureFlags: testContext.stubFlags, useReport: Constants.useGetMethod, - onActivation: { request, _, _ in + onActivation: { request in getRequestCount += 1 urlRequest = request }) @@ -151,37 +140,31 @@ final class DarklyServiceSpec: QuickSpec { expect(urlRequest?.httpMethod) == URLRequest.HTTPMethods.get expect(urlRequest?.httpBody).to(beNil()) expect(urlRequest?.httpBodyStream).to(beNil()) - guard let headers = urlRequest?.allHTTPHeaderFields - else { - fail("request is missing HTTP headers") - return - } - expect(headers[HTTPHeaders.HeaderKey.authorization]) == "\(HTTPHeaders.HeaderValue.apiKey) \(testContext.config.mobileKey)" - expect(headers[HTTPHeaders.HeaderKey.userAgent]) == "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)" - expect(headers[HTTPHeaders.HeaderKey.ifNoneMatch]).to(beNil()) + expect(urlRequest?.allHTTPHeaderFields) == testContext.httpHeaders.flagRequestHeaders } it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.flagStore.featureFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } } context("with flag request etag") { beforeEach { - testContext = TestContext(mobileKey: LDConfig.Constants.mockMobileKey, useReport: Constants.useGetMethod, flagRequestEtag: UUID().uuidString) + testContext = TestContext(mobileKey: LDConfig.Constants.mockMobileKey, useReport: Constants.useGetMethod) + testContext.service.flagRequestEtag = requestEtag waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.flagStore.featureFlags, + featureFlags: testContext.stubFlags, useReport: Constants.useReportMethod, - onActivation: { _, _, _ in + onActivation: { _ in reportRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.flagStore.featureFlags, + featureFlags: testContext.stubFlags, useReport: Constants.useGetMethod, - onActivation: { request, _, _ in + onActivation: { request in getRequestCount += 1 urlRequest = request }) @@ -209,24 +192,19 @@ final class DarklyServiceSpec: QuickSpec { } else { fail("request path is missing") } - expect([.reloadIgnoringLocalCacheData, .reloadRevalidatingCacheData]).to(contain(urlRequest?.cachePolicy)) + expect(urlRequest?.cachePolicy) == .reloadIgnoringLocalCacheData expect(urlRequest?.timeoutInterval) == testContext.config.connectionTimeout expect(urlRequest?.httpMethod) == URLRequest.HTTPMethods.get expect(urlRequest?.httpBody).to(beNil()) expect(urlRequest?.httpBodyStream).to(beNil()) - guard let headers = urlRequest?.allHTTPHeaderFields - else { - fail("request is missing HTTP headers") - return - } - expect(headers[HTTPHeaders.HeaderKey.authorization]) == "\(HTTPHeaders.HeaderValue.apiKey) \(testContext.config.mobileKey)" - expect(headers[HTTPHeaders.HeaderKey.userAgent]) == "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)" - expect(headers[HTTPHeaders.HeaderKey.ifNoneMatch]) == testContext.flagRequestEtag + var headers = urlRequest?.allHTTPHeaderFields + expect(headers?.removeValue(forKey: HTTPHeaders.HeaderKey.ifNoneMatch)) == requestEtag + expect(headers) == testContext.httpHeaders.flagRequestHeaders } it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.flagStore.featureFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } @@ -237,12 +215,12 @@ final class DarklyServiceSpec: QuickSpec { waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.internalServerError, useReport: Constants.useReportMethod, - onActivation: { _, _, _ in + onActivation: { _ in reportRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.internalServerError, useReport: Constants.useGetMethod, - onActivation: { _, _, _ in + onActivation: { _ in getRequestCount += 1 }) testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { (data, response, error) in @@ -265,15 +243,15 @@ final class DarklyServiceSpec: QuickSpec { } context("empty mobile key") { beforeEach { - testContext = TestContext(mobileKey: Constants.emptyMobileKey, useReport: Constants.useGetMethod) + testContext = TestContext(mobileKey: "", useReport: Constants.useGetMethod) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, useReport: Constants.useReportMethod, - onActivation: { _, _, _ in + onActivation: { _ in reportRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, useReport: Constants.useGetMethod, - onActivation: { _, _, _ in + onActivation: { _ in getRequestCount += 1 }) testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { (data, response, error) in @@ -292,19 +270,19 @@ final class DarklyServiceSpec: QuickSpec { testContext = TestContext(mobileKey: LDConfig.Constants.mockMobileKey, useReport: Constants.useReportMethod) } context("success") { - context("without a flag requesst etag") { + context("without a flag request etag") { beforeEach { waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.flagStore.featureFlags, + featureFlags: testContext.stubFlags, useReport: Constants.useGetMethod, - onActivation: { _, _, _ in + onActivation: { _ in getRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.flagStore.featureFlags, + featureFlags: testContext.stubFlags, useReport: Constants.useReportMethod, - onActivation: { request, _, _ in + onActivation: { request in reportRequestCount += 1 urlRequest = request }) @@ -330,37 +308,33 @@ final class DarklyServiceSpec: QuickSpec { expect(urlRequest?.timeoutInterval) == testContext.config.connectionTimeout expect(urlRequest?.httpMethod) == URLRequest.HTTPMethods.report expect(urlRequest?.httpBodyStream).toNot(beNil()) // Although the service sets the httpBody, OHHTTPStubs seems to convert that into an InputStream, which should be ok - guard let headers = urlRequest?.allHTTPHeaderFields - else { - fail("request is missing HTTP headers") - return - } - expect(headers[HTTPHeaders.HeaderKey.authorization]) == "\(HTTPHeaders.HeaderValue.apiKey) \(testContext.config.mobileKey)" - expect(headers[HTTPHeaders.HeaderKey.userAgent]) == "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)" - expect(headers[HTTPHeaders.HeaderKey.ifNoneMatch]).to(beNil()) + var headers = urlRequest?.allHTTPHeaderFields + expect(headers?.removeValue(forKey: "Content-Length")).toNot(beNil()) + expect(headers) == testContext.httpHeaders.flagRequestHeaders } it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.flagStore.featureFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } } context("with a flag requesst etag") { beforeEach { - testContext = TestContext(mobileKey: LDConfig.Constants.mockMobileKey, useReport: Constants.useReportMethod, flagRequestEtag: UUID().uuidString) + testContext = TestContext(mobileKey: LDConfig.Constants.mockMobileKey, useReport: Constants.useReportMethod) + testContext.service.flagRequestEtag = requestEtag waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.flagStore.featureFlags, + featureFlags: testContext.stubFlags, useReport: Constants.useGetMethod, - onActivation: { _, _, _ in + onActivation: { _ in getRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.flagStore.featureFlags, + featureFlags: testContext.stubFlags, useReport: Constants.useReportMethod, - onActivation: { request, _, _ in + onActivation: { request in reportRequestCount += 1 urlRequest = request }) @@ -382,23 +356,19 @@ final class DarklyServiceSpec: QuickSpec { } else { fail("request path is missing") } - expect([.reloadIgnoringLocalCacheData, .reloadRevalidatingCacheData]).to(contain(urlRequest?.cachePolicy)) + expect(urlRequest?.cachePolicy) == .reloadIgnoringLocalCacheData expect(urlRequest?.timeoutInterval) == testContext.config.connectionTimeout expect(urlRequest?.httpMethod) == URLRequest.HTTPMethods.report expect(urlRequest?.httpBodyStream).toNot(beNil()) // Although the service sets the httpBody, OHHTTPStubs seems to convert that into an InputStream, which should be ok - guard let headers = urlRequest?.allHTTPHeaderFields - else { - fail("request is missing HTTP headers") - return - } - expect(headers[HTTPHeaders.HeaderKey.authorization]) == "\(HTTPHeaders.HeaderValue.apiKey) \(testContext.config.mobileKey)" - expect(headers[HTTPHeaders.HeaderKey.userAgent]) == "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)" - expect(headers[HTTPHeaders.HeaderKey.ifNoneMatch]) == testContext.flagRequestEtag + var headers = urlRequest?.allHTTPHeaderFields + expect(headers?.removeValue(forKey: "Content-Length")).toNot(beNil()) + expect(headers?.removeValue(forKey: HTTPHeaders.HeaderKey.ifNoneMatch)) == requestEtag + expect(headers) == testContext.httpHeaders.flagRequestHeaders } it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.flagStore.featureFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } @@ -409,12 +379,12 @@ final class DarklyServiceSpec: QuickSpec { waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.internalServerError, useReport: Constants.useReportMethod, - onActivation: { _, _, _ in + onActivation: { _ in reportRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.internalServerError, useReport: Constants.useGetMethod, - onActivation: { _, _, _ in + onActivation: { _ in getRequestCount += 1 }) testContext.service.getFeatureFlags(useReport: Constants.useReportMethod, completion: { data, response, error in @@ -437,15 +407,15 @@ final class DarklyServiceSpec: QuickSpec { } context("empty mobile key") { beforeEach { - testContext = TestContext(mobileKey: Constants.emptyMobileKey, useReport: Constants.useReportMethod) + testContext = TestContext(mobileKey: "", useReport: Constants.useReportMethod) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, useReport: Constants.useGetMethod, - onActivation: { _, _, _ in + onActivation: { _ in getRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, useReport: Constants.useReportMethod, - onActivation: { _, _, _ in + onActivation: { _ in reportRequestCount += 1 }) testContext.service.getFeatureFlags(useReport: Constants.useReportMethod, completion: { data, response, error in @@ -463,302 +433,101 @@ final class DarklyServiceSpec: QuickSpec { } private func flagRequestEtagSpec() { + var originalFlagRequestEtag: String! var testContext: TestContext! - var flagRequestEtag: String? describe("flagRequestEtag") { - context("no original etag") { + beforeEach { + testContext = TestContext() + } + context("no request etag") { context("on success") { - context("response has etag") { - beforeEach { - testContext = TestContext() - flagRequestEtag = UUID().uuidString - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - useReport: Constants.useGetMethod, - flagResponseEtag: flagRequestEtag, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("sets the response etag") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]) == flagRequestEtag - } + it("sets the etag") { + let flagRequestEtag = UUID().uuidString + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.ok, flagResponseEtag: flagRequestEtag) + expect(testContext.service.flagRequestEtag) == flagRequestEtag } - context("response has no etag") { - beforeEach { - testContext = TestContext() - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - useReport: Constants.useGetMethod, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("leaves the etag empty") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]).to(beNil()) - } + it("clears the etag") { + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.ok) + expect(testContext.service.flagRequestEtag).to(beNil()) } } + // This should never happen, without an original etag the server should not send a 304 NOT MODIFIED. If it does ignore it. context("on not modified") { - context("response has etag") { - // This should never happen, without an original etag the server should not send a 304 NOT MODIFIED. If it does ignore it. - beforeEach { - testContext = TestContext() - flagRequestEtag = UUID().uuidString - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.notModified, - featureFlags: [:], - useReport: Constants.useGetMethod, - flagResponseEtag: flagRequestEtag, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("leaves the etag empty") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]).to(beNil()) - } + it("leaves empty when set in response") { + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.notModified, + featureFlags: [:], + flagResponseEtag: UUID().uuidString) + expect(testContext.service.flagRequestEtag).to(beNil()) } - context("response has no etag") { - // This should never happen, without an original etag the server should not send a 304 NOT MODIFIED. If it does ignore it. - beforeEach { - testContext = TestContext() - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.notModified, - featureFlags: [:], - useReport: Constants.useGetMethod, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("leaves the etag empty") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]).to(beNil()) - } + it("leaves empty") { + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.notModified, + featureFlags: [:]) + expect(testContext.service.flagRequestEtag).to(beNil()) } } context("on failure") { - context("response has etag") { + it("leaves empty when set in response") { // This should never happen. The server should not send an etag with a failure status code If it does ignore it. - beforeEach { - testContext = TestContext() - flagRequestEtag = UUID().uuidString - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.internalServerError, - useReport: Constants.useGetMethod, - flagResponseEtag: flagRequestEtag, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("leaves the etag empty") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]).to(beNil()) - } + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.internalServerError, + flagResponseEtag: UUID().uuidString) + expect(testContext.service.flagRequestEtag).to(beNil()) } - context("response has no etag") { - beforeEach { - testContext = TestContext() - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.internalServerError, - useReport: Constants.useGetMethod, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("leaves the etag empty") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]).to(beNil()) - } + it("leaves the etag empty") { + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.internalServerError) + expect(testContext.service.flagRequestEtag).to(beNil()) } } } - context("with original etag") { - var originalFlagRequestEtag: String! + context("with request etag") { + beforeEach { + originalFlagRequestEtag = UUID().uuidString + testContext.service.flagRequestEtag = originalFlagRequestEtag + } context("on success") { - context("response has an etag") { - context("same as original etag") { - beforeEach { - testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) - HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) - originalFlagRequestEtag = HTTPHeaders.flagRequestEtags[testContext.config.mobileKey] - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - useReport: Constants.useGetMethod, - flagResponseEtag: originalFlagRequestEtag, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("retains the original etag") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]) == originalFlagRequestEtag - } - } - context("different from the original etag") { - beforeEach { - testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) - HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) - flagRequestEtag = UUID().uuidString - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - useReport: Constants.useGetMethod, - flagResponseEtag: flagRequestEtag, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("replaces the etag") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]) == flagRequestEtag - } - } + it("response has same etag") { + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.ok, + flagResponseEtag: originalFlagRequestEtag) + expect(testContext.service.flagRequestEtag) == originalFlagRequestEtag } - context("response has no etag") { - beforeEach { - testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) - HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - useReport: Constants.useGetMethod, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("clears the etag") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]).to(beNil()) - } + it("response has different etag") { + let flagRequestEtag = UUID().uuidString + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.ok, + flagResponseEtag: flagRequestEtag) + expect(testContext.service.flagRequestEtag) == flagRequestEtag + } + it("response has no etag") { + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.ok) + expect(testContext.service.flagRequestEtag).to(beNil()) } } context("on not modified") { - context("response has etag") { - context("that matches the original etag") { - beforeEach { - testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) - HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) - originalFlagRequestEtag = HTTPHeaders.flagRequestEtags[testContext.config.mobileKey] - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.notModified, - useReport: Constants.useGetMethod, - flagResponseEtag: originalFlagRequestEtag, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("retains the etag") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]) == originalFlagRequestEtag - } - } - context("that differs from the original etag") { - // This should not happen. If the response was not modified then the etags should match. In that case ignore the new etag - beforeEach { - testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) - HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) - originalFlagRequestEtag = HTTPHeaders.flagRequestEtags[testContext.config.mobileKey] - flagRequestEtag = UUID().uuidString - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.notModified, - useReport: Constants.useGetMethod, - flagResponseEtag: flagRequestEtag, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("retains the original etag") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]) == originalFlagRequestEtag - } - } + it("response has same etag") { + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.notModified, + flagResponseEtag: originalFlagRequestEtag) + expect(testContext.service.flagRequestEtag) == originalFlagRequestEtag } - context("response has no etag") { + it("response has different etag") { // This should not happen. If the response was not modified then the etags should match. In that case ignore the new etag - beforeEach { - testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) - HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) - originalFlagRequestEtag = HTTPHeaders.flagRequestEtags[testContext.config.mobileKey] - flagRequestEtag = UUID().uuidString - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.notModified, - useReport: Constants.useGetMethod, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("retains the original etag") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]) == originalFlagRequestEtag - } + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.notModified, + flagResponseEtag: UUID().uuidString) + expect(testContext.service.flagRequestEtag) == originalFlagRequestEtag + } + it("response has no etag") { + // This should not happen. If the response was not modified then the etags should match. In that case ignore the new etag + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.notModified) + expect(testContext.service.flagRequestEtag) == originalFlagRequestEtag } } context("on failure") { - context("response has etag") { + it("response has etag") { // This should not happen. If the response was an error then there should be no new etag. Because of the error, clear the etag - beforeEach { - testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) - HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) - originalFlagRequestEtag = HTTPHeaders.flagRequestEtags[testContext.config.mobileKey] - flagRequestEtag = UUID().uuidString - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.internalServerError, - useReport: Constants.useGetMethod, - flagResponseEtag: flagRequestEtag, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("clears the etag") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]).to(beNil()) - } + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.internalServerError, + flagResponseEtag: UUID().uuidString) + expect(testContext.service.flagRequestEtag).to(beNil()) } - context("response has no etag") { - beforeEach { - testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) - HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) - originalFlagRequestEtag = HTTPHeaders.flagRequestEtags[testContext.config.mobileKey] - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.internalServerError, - useReport: Constants.useGetMethod, - onActivation: { _, _, _ in - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { _, _, _ in - done() - }) - } - } - it("clears the etag") { - expect(HTTPHeaders.flagRequestEtags[testContext.config.mobileKey]).to(beNil()) - } + it("response has no etag") { + testContext.runStubbedGet(statusCode: HTTPURLResponse.StatusCodes.internalServerError) + expect(testContext.service.flagRequestEtag).to(beNil()) } } } @@ -766,37 +535,12 @@ final class DarklyServiceSpec: QuickSpec { } private func clearFlagRequestCacheSpec() { - var testContext: TestContext! - var flagRequestEtag: String! - var urlRequest: URLRequest! - var serviceResponse: ServiceResponse! describe("clearFlagResponseCache") { - context("cached responses and etags exist") { - beforeEach { - URLCache.shared.diskCapacity = 0 - testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) - HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) - flagRequestEtag = UUID().uuidString - testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - useReport: Constants.useGetMethod, - flagResponseEtag: flagRequestEtag, - onActivation: { request, _, _ in - urlRequest = request - }) - waitUntil { done in - testContext.service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { error, response, data in - serviceResponse = (error, response, data) - done() - }) - } - URLCache.shared.storeResponse(serviceResponse, for: urlRequest) - - testContext.service.clearFlagResponseCache() - } - it("removes cached responses and etags") { - expect(HTTPHeaders.flagRequestEtags.isEmpty).to(beTrue()) - expect(URLCache.shared.cachedResponse(for: urlRequest)).to(beNil()) - } + it("clears cached etag") { + let testContext = TestContext() + testContext.service.flagRequestEtag = UUID().uuidString + testContext.service.clearFlagResponseCache() + expect(testContext.service.flagRequestEtag).to(beNil()) } } } @@ -813,15 +557,14 @@ final class DarklyServiceSpec: QuickSpec { } it("creates an event source that makes valid GET request") { expect(eventSource).toNot(beNil()) - expect(testContext.serviceFactoryMock?.makeStreamingProviderCallCount) == 1 - expect(testContext.serviceFactoryMock?.makeStreamingProviderReceivedArguments).toNot(beNil()) - let receivedArguments = testContext.serviceFactoryMock?.makeStreamingProviderReceivedArguments + expect(testContext.serviceFactoryMock.makeStreamingProviderCallCount) == 1 + expect(testContext.serviceFactoryMock.makeStreamingProviderReceivedArguments).toNot(beNil()) + let receivedArguments = testContext.serviceFactoryMock.makeStreamingProviderReceivedArguments expect(receivedArguments!.url.host) == testContext.config.streamUrl.host expect(receivedArguments!.url.pathComponents.contains(DarklyService.StreamRequestPath.meval)).to(beTrue()) - expect(receivedArguments!.url.pathComponents.contains(DarklyService.StreamRequestPath.mping)).to(beFalse()) expect(LDUser(base64urlEncodedString: receivedArguments!.url.lastPathComponent)?.isEqual(to: testContext.user)) == true expect(receivedArguments!.httpHeaders).toNot(beEmpty()) - expect(receivedArguments!.connectMethod).to(beNil()) + expect(receivedArguments!.connectMethod).to(be("GET")) expect(receivedArguments!.connectBody).to(beNil()) } } @@ -832,12 +575,11 @@ final class DarklyServiceSpec: QuickSpec { } it("creates an event source that makes valid REPORT request") { expect(eventSource).toNot(beNil()) - expect(testContext.serviceFactoryMock?.makeStreamingProviderCallCount) == 1 - expect(testContext.serviceFactoryMock?.makeStreamingProviderReceivedArguments).toNot(beNil()) - let receivedArguments = testContext.serviceFactoryMock?.makeStreamingProviderReceivedArguments + expect(testContext.serviceFactoryMock.makeStreamingProviderCallCount) == 1 + expect(testContext.serviceFactoryMock.makeStreamingProviderReceivedArguments).toNot(beNil()) + let receivedArguments = testContext.serviceFactoryMock.makeStreamingProviderReceivedArguments expect(receivedArguments!.url.host) == testContext.config.streamUrl.host expect(receivedArguments!.url.lastPathComponent) == DarklyService.StreamRequestPath.meval - expect(receivedArguments!.url.pathComponents.contains(DarklyService.StreamRequestPath.mping)).to(beFalse()) expect(receivedArguments!.httpHeaders).toNot(beEmpty()) expect(receivedArguments!.connectMethod) == DarklyService.HTTPRequestMethod.report expect(LDUser(data: receivedArguments!.connectBody)?.isEqual(to: testContext.user)) == true @@ -860,10 +602,8 @@ final class DarklyServiceSpec: QuickSpec { var responses: ServiceResponses! beforeEach { waitUntil { done in - testContext.serviceMock.stubEventRequest(success: true) { request, _, _ in - eventRequest = request - } - testContext.service.publishEventDictionaries(testContext.mockEventDictionaries!, UUID().uuidString) { data, response, error in + testContext.serviceMock.stubEventRequest(success: true) { eventRequest = $0 } + testContext.service.publishEventDictionaries(testContext.mockEventDictionaries(), UUID().uuidString) { data, response, error in responses = (data, response, error) done() } @@ -884,10 +624,8 @@ final class DarklyServiceSpec: QuickSpec { var responses: ServiceResponses! beforeEach { waitUntil { done in - testContext.serviceMock.stubEventRequest(success: false) { request, _, _ in - eventRequest = request - } - testContext.service.publishEventDictionaries(testContext.mockEventDictionaries!, UUID().uuidString) { data, response, error in + testContext.serviceMock.stubEventRequest(success: false) { eventRequest = $0 } + testContext.service.publishEventDictionaries(testContext.mockEventDictionaries(), UUID().uuidString) { data, response, error in responses = (data, response, error) done() } @@ -908,11 +646,9 @@ final class DarklyServiceSpec: QuickSpec { var responses: ServiceResponses! var eventsPublished = false beforeEach { - testContext = TestContext(mobileKey: Constants.emptyMobileKey, useReport: Constants.useGetMethod, includeMockEventDictionaries: true) - testContext.serviceMock.stubEventRequest(success: true) { request, _, _ in - eventRequest = request - } - testContext.service.publishEventDictionaries(testContext.mockEventDictionaries!, UUID().uuidString) { data, response, error in + testContext = TestContext(mobileKey: "", useReport: Constants.useGetMethod, includeMockEventDictionaries: true) + testContext.serviceMock.stubEventRequest(success: true) { eventRequest = $0 } + testContext.service.publishEventDictionaries(testContext.mockEventDictionaries(), UUID().uuidString) { data, response, error in responses = (data, response, error) eventsPublished = true } @@ -929,9 +665,7 @@ final class DarklyServiceSpec: QuickSpec { let emptyEventDictionaryList: [[String: Any]] = [] beforeEach { testContext = TestContext(mobileKey: LDConfig.Constants.mockMobileKey, useReport: Constants.useGetMethod, includeMockEventDictionaries: true) - testContext.serviceMock.stubEventRequest(success: true) { request, _, _ in - eventRequest = request - } + testContext.serviceMock.stubEventRequest(success: true) { eventRequest = $0 } testContext.service.publishEventDictionaries(emptyEventDictionaryList, "") { data, response, error in responses = (data, response, error) eventsPublished = true @@ -947,29 +681,22 @@ final class DarklyServiceSpec: QuickSpec { } private func diagnosticCacheSpec() { - var testContext: TestContext! describe("diagnosticCache") { - context("empty mobileKey") { - it("does not create cache") { - testContext = TestContext(mobileKey: "") - expect(testContext.service.diagnosticCache).to(beNil()) - expect(testContext.serviceFactoryMock?.makeDiagnosticCacheCallCount) == 0 - } + it("does not create cache with empty mobile key") { + let testContext = TestContext(mobileKey: "") + expect(testContext.service.diagnosticCache).to(beNil()) + expect(testContext.serviceFactoryMock.makeDiagnosticCacheCallCount) == 0 } - context("diagnosticOptOut true") { - it("does not create cache") { - testContext = TestContext(diagnosticOptOut: true) - expect(testContext.service.diagnosticCache).to(beNil()) - expect(testContext.serviceFactoryMock?.makeDiagnosticCacheCallCount) == 0 - } + it("does not create cache when diagnosticOptOut set") { + let testContext = TestContext(diagnosticOptOut: true) + expect(testContext.service.diagnosticCache).to(beNil()) + expect(testContext.serviceFactoryMock.makeDiagnosticCacheCallCount) == 0 } - context("diagnosticOptOut false") { - it("creates a cache with the mobile key") { - testContext = TestContext(diagnosticOptOut: false) - expect(testContext.service.diagnosticCache).toNot(beNil()) - expect(testContext.serviceFactoryMock?.makeDiagnosticCacheCallCount) == 1 - expect(testContext.serviceFactoryMock?.makeDiagnosticCacheReceivedSdkKey) == LDConfig.Constants.mockMobileKey - } + it("creates a cache with the mobile key") { + let testContext = TestContext(diagnosticOptOut: false) + expect(testContext.service.diagnosticCache).toNot(beNil()) + expect(testContext.serviceFactoryMock.makeDiagnosticCacheCallCount) == 1 + expect(testContext.serviceFactoryMock.makeDiagnosticCacheReceivedSdkKey) == LDConfig.Constants.mockMobileKey } } } @@ -1023,9 +750,7 @@ final class DarklyServiceSpec: QuickSpec { var responses: ServiceResponses! beforeEach { waitUntil { done in - testContext.serviceMock.stubEventRequest(success: false) { request, _, _ in - diagnosticRequest = request - } + testContext.serviceMock.stubEventRequest(success: false) { diagnosticRequest = $0 } testContext.service.publishDiagnostic(diagnosticEvent: self.stubDiagnostic()) { data, response, error in responses = (data, response, error) done() @@ -1053,7 +778,7 @@ final class DarklyServiceSpec: QuickSpec { context("empty mobile key") { var diagnosticPublished = false beforeEach { - testContext = TestContext(mobileKey: Constants.emptyMobileKey) + testContext = TestContext(mobileKey: "") testContext.serviceMock.stubDiagnosticRequest(success: true) { request, _, _ in diagnosticRequest = request } @@ -1070,8 +795,12 @@ final class DarklyServiceSpec: QuickSpec { } } -extension DarklyService.StreamRequestPath { - static let mping = "mping" +private extension Data { + var flagCollection: [LDFlagKey: FeatureFlag]? { + guard let flagDictionary = try? JSONSerialization.jsonDictionary(with: self, options: .allowFragments) + else { return nil } + return flagDictionary.flagCollection + } } extension LDUser { @@ -1087,11 +816,3 @@ extension LDUser { self.init(userDictionary: userDictionary) } } - -extension HTTPHeaders { - static func loadFlagRequestEtags(_ flagRequestEtags: [String: String]) { - flagRequestEtags.forEach { mobileKey, etag in - HTTPHeaders.setFlagRequestEtag(etag, for: mobileKey) - } - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift index 44df03c1..563099d3 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift @@ -12,50 +12,6 @@ import XCTest final class HTTPHeadersSpec: XCTestCase { - private static var stubEtags: [String: String] = { - var etags: [String: String] = [:] - (0..<3).forEach { _ in etags[UUID().uuidString] = UUID().uuidString } - return etags - }() - - override func tearDown() { - HTTPHeaders.removeFlagRequestEtags() - } - - func testSettingFlagRequestEtags() { - HTTPHeadersSpec.stubEtags.forEach { mobileKey, etag in - HTTPHeaders.setFlagRequestEtag(etag, for: mobileKey) - } - XCTAssertEqual(HTTPHeaders.flagRequestEtags, HTTPHeadersSpec.stubEtags) - } - - func testClearingIndividialFlagRequestEtags() { - HTTPHeadersSpec.stubEtags.forEach { mobileKey, etag in - HTTPHeaders.setFlagRequestEtag(etag, for: mobileKey) - } - HTTPHeadersSpec.stubEtags.forEach { mobileKey, _ in - HTTPHeaders.setFlagRequestEtag(nil, for: mobileKey) - } - XCTAssert(HTTPHeaders.flagRequestEtags.isEmpty) - } - - func testClearingAllFlagRequestEtags() { - HTTPHeadersSpec.stubEtags.forEach { mobileKey, etag in - HTTPHeaders.setFlagRequestEtag(etag, for: mobileKey) - } - HTTPHeaders.removeFlagRequestEtags() - XCTAssert(HTTPHeaders.flagRequestEtags.isEmpty) - } - - func testHasFlagRequestEtag() { - let config = LDConfig(mobileKey: "with-etag") - HTTPHeaders.setFlagRequestEtag("foo", for: "with-etag") - let withoutEtag = HTTPHeaders(config: LDConfig.stub, environmentReporter: EnvironmentReportingMock()) - let withEtag = HTTPHeaders(config: config, environmentReporter: EnvironmentReportingMock()) - XCTAssertFalse(withoutEtag.hasFlagRequestEtag) - XCTAssert(withEtag.hasFlagRequestEtag) - } - func testFlagRequestDefaultHeaders() { let config = LDConfig.stub let httpHeaders = HTTPHeaders(config: config, environmentReporter: EnvironmentReportingMock()) @@ -67,18 +23,6 @@ final class HTTPHeadersSpec: XCTestCase { XCTAssertNil(headers[HTTPHeaders.HeaderKey.ifNoneMatch]) } - func testFlagRequestHeadersWithEtag() { - let config = LDConfig.stub - HTTPHeaders.setFlagRequestEtag("etag", for: config.mobileKey) - let httpHeaders = HTTPHeaders(config: config, environmentReporter: EnvironmentReportingMock()) - let headers = httpHeaders.flagRequestHeaders - XCTAssertEqual(headers[HTTPHeaders.HeaderKey.authorization], - "\(HTTPHeaders.HeaderValue.apiKey) \(config.mobileKey)") - XCTAssertEqual(headers[HTTPHeaders.HeaderKey.userAgent], - "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)") - XCTAssertEqual(headers[HTTPHeaders.HeaderKey.ifNoneMatch], "etag") - } - func testEventSourceDefaultHeaders() { let config = LDConfig.stub let httpHeaders = HTTPHeaders(config: config, environmentReporter: EnvironmentReportingMock()) diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift index 204100f4..f03a0ae4 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift @@ -15,14 +15,3 @@ extension HTTPURLResponse.StatusCodes { !LDConfig.reportRetryStatusCodes.contains(statusCode) && statusCode != ok } } - -extension HTTPURLResponse.HeaderKeys { - static let cacheControl = "Cache-Control" -} - -extension HTTPURLResponse { - struct HeaderValues { - static let etagStub = "4806e" - static let maxAge = "max-age=0" - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/URLCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/URLCacheSpec.swift deleted file mode 100644 index d4b1aa27..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Networking/URLCacheSpec.swift +++ /dev/null @@ -1,183 +0,0 @@ -// -// URLCache.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation -import Quick -import Nimble -import OHHTTPStubs -@testable import LaunchDarkly - -// Normally we would not build an AT for system provided services, like URLCache. The SDK uses the URLCache in a non-standard way, sending HTTP requests with a custom verb REPORT. So building this test validates that the URLCache behaves as expected for GET and REPORT requests. Retaining these tests helps provide that assurance through future revisions. -final class URLCacheSpec: QuickSpec { - - struct Constants { - static let userCount = 3 - static let useReportMethod = true - static let useGetMethod = false - } - - struct TestContext { - var config: LDConfig - var serviceFactoryMock: ClientServiceMockFactory - var flagStore: FlagMaintaining - - // per user - var userServiceObjects = [String: (user: LDUser, service: DarklyService, serviceMock: DarklyServiceMock)]() - var userKeys: Dictionary.Keys { - userServiceObjects.keys - } - - init(userCount: Int = 1, useReport: Bool = false) { - config = LDConfig.stub - config.useReport = useReport - - flagStore = FlagStore(featureFlagDictionary: FlagMaintainingMock.stubFlags()) - - serviceFactoryMock = ClientServiceMockFactory() - - while userServiceObjects.count < userCount { - let user = LDUser.stub() - let service = DarklyService(config: config, user: user, serviceFactory: serviceFactoryMock) - let serviceMock = DarklyServiceMock(config: config, user: user) - userServiceObjects[user.key] = (user, service, serviceMock) - } - } - - func user(for key: String) -> LDUser? { - userServiceObjects[key]?.user - } - - func service(for key: String) -> DarklyService? { - userServiceObjects[key]?.service - } - - func serviceMock(for key: String) -> DarklyServiceMock? { - userServiceObjects[key]?.serviceMock - } - } - - override func spec() { - cacheReportRequestSpec() - } - - private func cacheReportRequestSpec() { - var testContext: TestContext! - - describe("storeCachedResponse") { - context("when flag request uses the get method") { - var urlRequests = [String: URLRequest]() - beforeEach { - testContext = TestContext(userCount: Constants.userCount, useReport: Constants.useGetMethod) - for userKey in testContext.userKeys { - guard let service = testContext.service(for: userKey), - let serviceMock = testContext.serviceMock(for: userKey) - else { - fail("test setup failed to create user service objects") - return - } - var urlRequest: URLRequest! - serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.flagStore.featureFlags, - useReport: Constants.useGetMethod, - onActivation: { request, _, _ in - urlRequest = request - }) - var serviceResponse: ServiceResponse! - waitUntil { done in - service.getFeatureFlags(useReport: Constants.useGetMethod, completion: { response in - serviceResponse = response - done() - }) - } - urlRequests[userKey] = urlRequest - URLCache.shared.storeResponse(serviceResponse, for: urlRequest) - } - } - it("caches the flag request response") { - for userKey in testContext.userKeys { - guard let urlRequest = urlRequests[userKey] - else { - fail("test setup failed to set user or urlRequest for user: \(userKey)") - return - } - expect(URLCache.shared.cachedResponse(for: urlRequest)?.flagCollection) == testContext.flagStore.featureFlags - } - } - } - context("when flag request uses the report method") { - var urlRequests = [String: URLRequest]() - beforeEach { - testContext = TestContext(userCount: Constants.userCount, useReport: Constants.useReportMethod) - for userKey in testContext.userKeys { - guard let service = testContext.service(for: userKey), - let serviceMock = testContext.serviceMock(for: userKey) - else { - fail("test setup failed to create user service objects") - return - } - var urlRequest: URLRequest! - serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.flagStore.featureFlags, - useReport: Constants.useReportMethod, - onActivation: { request, _, _ in - urlRequest = request - }) - var serviceResponse: ServiceResponse! - waitUntil { done in - service.getFeatureFlags(useReport: Constants.useReportMethod, completion: { response in - serviceResponse = response - done() - }) - } - urlRequests[userKey] = urlRequest - URLCache.shared.storeResponse(serviceResponse, for: urlRequest) - } - } - it("caches the flag request response") { - for userKey in testContext.userKeys { - guard let urlRequest = urlRequests[userKey] - else { - fail("test setup failed to set user or urlRequest for user: \(userKey)") - return - } - expect(URLCache.shared.cachedResponse(for: urlRequest)?.flagCollection) == testContext.flagStore.featureFlags - } - } - } - } - } -} - -extension Data { - var flagCollection: [LDFlagKey: FeatureFlag]? { - guard let flagDictionary = try? JSONSerialization.jsonDictionary(with: self, options: .allowFragments) - else { return nil } - return flagDictionary.flagCollection - } -} - -extension CachedURLResponse { - var flagCollection: [LDFlagKey: FeatureFlag]? { - data.flagCollection - } -} - -extension URLCache { - func storeResponse(_ serviceResponse: ServiceResponse?, for request: URLRequest?) { - guard let urlResponse = serviceResponse?.urlResponse, - let data = serviceResponse?.data, - let request = request - else { return } - URLCache.shared.storeCachedResponse(CachedURLResponse(response: urlResponse, data: data), for: request) - } - - func cachedResponse(for request: URLRequest?) -> CachedURLResponse? { - guard let request = request - else { return nil } - return URLCache.shared.cachedResponse(for: request) - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift index 00bf6e56..e07b3244 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift @@ -11,23 +11,24 @@ import XCTest @testable import LaunchDarkly final class URLRequestSpec: XCTestCase { - func testAppendHeadersNoInitial() { - var request: URLRequest = URLRequest(url: URL(string: "https://dummy.urlRequest.com")!) - request.appendHeaders(["headerA": "valueA", "headerB": "valueB"]) - XCTAssertEqual(request.allHTTPHeaderFields, ["headerA": "valueA", "headerB": "valueB"]) - } + func testInitExtension() { + var delegateArgs: (url: URL, headers: [String: String])? - func testAppendHeaders() { - var request: URLRequest = URLRequest(url: URL(string: "https://dummy.urlRequest.com")!) - request.allHTTPHeaderFields = ["header1": "value1"] - request.appendHeaders(["headerA": "valueA"]) - XCTAssertEqual(request.allHTTPHeaderFields, ["header1": "value1", "headerA": "valueA"]) - } + let url = URL(string: "https://dummy.urlRequest.com")! + var config = LDConfig(mobileKey: "testkey") + config.connectionTimeout = 15 + config.headerDelegate = { url, headers in + delegateArgs = (url, headers) + return ["Proxy": "Other"] + } + let request: URLRequest = URLRequest(url: url, + ldHeaders: ["Authorization": "api_key foo"], + ldConfig: config) - func testAppendHeadersOverrides() { - var request: URLRequest = URLRequest(url: URL(string: "https://dummy.urlRequest.com")!) - request.allHTTPHeaderFields = ["header1": "value1", "header2": "value2"] - request.appendHeaders(["header1": "value3"]) - XCTAssertEqual(request.allHTTPHeaderFields, ["header1": "value3", "header2": "value2"]) + XCTAssertEqual(request.timeoutInterval, 15) + XCTAssertEqual(request.url, url) + XCTAssertEqual(delegateArgs?.url, url) + XCTAssertEqual(delegateArgs?.headers, ["Authorization": "api_key foo"]) + XCTAssertEqual(request.allHTTPHeaderFields, ["Proxy": "Other"]) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift index b03169df..44024fca 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift @@ -10,664 +10,412 @@ import Quick import Nimble @testable import LaunchDarkly -final class FlagChangeNotifierSpec: QuickSpec { - enum ObserverType { - case singleKey, multipleKey, any +private class CallTracker { + var callCount = 0 + var lastCallArg: T? +} + +private class MockFlagChangeObserver { + let key: LDFlagKey + let observer: FlagChangeObserver + var owner: LDObserverOwner? + + private var tracker: CallTracker? + var callCount: Int { tracker!.callCount } + var lastCallArg: LDChangedFlag? { tracker!.lastCallArg } + + init(_ key: LDFlagKey, owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { + self.key = key + self.owner = owner + let tracker = CallTracker() + self.observer = FlagChangeObserver(key: key, owner: owner) { + tracker.callCount += 1 + tracker.lastCallArg = $0 + } + self.tracker = tracker } +} - struct TestContext { - var subject: FlagChangeNotifier! - var originalFlagChangeObservers = [FlagChangeObserver]() - var owners = [String: LDObserverOwner?]() - var changedFlag: LDChangedFlag? - var flagChangeHandlerCallCount = 0 - var changedFlags: [LDFlagKey: LDChangedFlag]? - var flagCollectionChangeHandlerCallCount = 0 - var flagsUnchangedHandlerCallCount = 0 - var flagsUnchangedOwnerKey: String? - var featureFlags: [LDFlagKey: FeatureFlag] = DarklyServiceMock.Constants.stubFeatureFlags() - var flagStore = FlagMaintainingMock(flags: FlagMaintainingMock.stubFlags(includeNullValue: true)) +private class MockFlagCollectionChangeObserver { + let keys: [LDFlagKey] + let observer: FlagChangeObserver + var owner: LDObserverOwner? + + private var tracker: CallTracker<[LDFlagKey: LDChangedFlag]>? + var callCount: Int { tracker!.callCount } + var lastCallArg: [LDFlagKey: LDChangedFlag]? { tracker!.lastCallArg } + + init(_ keys: [LDFlagKey], owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { + self.keys = keys + self.owner = owner + let tracker = CallTracker<[LDFlagKey: LDChangedFlag]>() + self.observer = FlagChangeObserver(keys: keys, owner: owner) { + tracker.callCount += 1 + tracker.lastCallArg = $0 + } + self.tracker = tracker + } +} - let alternateFlagKeys = ["flag-key-1", "flag-key-2", "flag-key-3"] +private class MockFlagsUnchangedObserver { + let observer: FlagsUnchangedObserver + var owner: LDObserverOwner? - // Use this initializer when stubbing observers for observer add & remove tests - init(observers observerCount: Int = 0, observerType: ObserverType = .any, repeatFirstObserver: Bool = false) { - subject = FlagChangeNotifier() - guard observerCount > 0 - else { return } - var flagChangeObservers = [FlagChangeObserver]() - while flagChangeObservers.count < observerCount { - if observerType == .singleKey || (observerType == .any && flagChangeObservers.count.isMultiple(of: 2)) { - flagChangeObservers.append(FlagChangeObserver(key: DarklyServiceMock.FlagKeys.bool, - owner: stubOwner(key: DarklyServiceMock.FlagKeys.bool), - flagChangeHandler: flagChangeHandler)) - } else { - flagChangeObservers.append(FlagChangeObserver(keys: DarklyServiceMock.FlagKeys.knownFlags, - owner: stubOwner(keys: DarklyServiceMock.FlagKeys.knownFlags), - flagCollectionChangeHandler: flagCollectionChangeHandler)) - } - } - if repeatFirstObserver { - flagChangeObservers[observerCount - 1] = flagChangeObservers.first! - } - flagsUnchangedOwnerKey = flagChangeObservers.first!.flagKeys.observerKey + private var tracker: CallTracker? + var callCount: Int { tracker!.callCount } - var flagsUnchangedObservers = [FlagsUnchangedObserver]() - // use the flag change observer owners to own the flagsUnchangedObservers - flagChangeObservers.forEach { flagChangeObserver in - flagsUnchangedObservers.append(FlagsUnchangedObserver(owner: flagChangeObserver.owner!, flagsUnchangedHandler: flagsUnchangedHandler)) - } - subject = FlagChangeNotifier(flagChangeObservers: flagChangeObservers, flagsUnchangedObservers: flagsUnchangedObservers) - originalFlagChangeObservers = subject.flagObservers + init(owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { + self.owner = owner + let tracker = CallTracker() + self.observer = FlagsUnchangedObserver(owner: owner) { + tracker.callCount += 1 } + self.tracker = tracker + } +} - // Use this initializer when stubbing observers that should execute a LDFlagChangeHandler during the test - init(keys: [LDFlagKey], flagChangeHandler: @escaping LDFlagChangeHandler, flagsUnchangedHandler: @escaping LDFlagsUnchangedHandler) { - subject = FlagChangeNotifier() - guard !keys.isEmpty - else { return } - var flagChangeObservers = [FlagChangeObserver]() - keys.forEach { key in - flagChangeObservers.append(FlagChangeObserver(key: key, - owner: self.stubOwner(key: key), - flagChangeHandler: flagChangeHandler)) - } - flagsUnchangedOwnerKey = flagChangeObservers.first!.flagKeys.observerKey - var flagsUnchangedObservers = [FlagsUnchangedObserver]() - // use the flag change observer owners to own the flagsUnchangedObservers - flagChangeObservers.forEach { flagChangeObserver in - flagsUnchangedObservers.append(FlagsUnchangedObserver(owner: flagChangeObserver.owner!, flagsUnchangedHandler: flagsUnchangedHandler)) - } - subject = FlagChangeNotifier(flagChangeObservers: flagChangeObservers, flagsUnchangedObservers: flagsUnchangedObservers) - originalFlagChangeObservers = subject.flagObservers - } +private class MockConnectionModeChangedObserver { + let observer: ConnectionModeChangedObserver + var owner: LDObserverOwner? + + private var tracker: CallTracker? + var callCount: Int { tracker!.callCount } + var lastCallArg: ConnectionInformation.ConnectionMode? { tracker!.lastCallArg } - // Use this initializer when stubbing observers that should execute a LDFlagCollectionChangeHandler during the test - // This initializer sets 2 observers, one for the specified flags, and a second for a disjoint set of flags. That way tests verify the notifier is choosing the correct observers - init(keys: [LDFlagKey], flagCollectionChangeHandler: @escaping LDFlagCollectionChangeHandler, flagsUnchangedHandler: @escaping LDFlagsUnchangedHandler) { - subject = FlagChangeNotifier() - guard !keys.isEmpty - else { return } - var observers = [FlagChangeObserver]() - observers.append(FlagChangeObserver(keys: keys, - owner: self.stubOwner(keys: keys), - flagCollectionChangeHandler: flagCollectionChangeHandler)) - observers.append(FlagChangeObserver(keys: alternateFlagKeys, - owner: self.stubOwner(keys: alternateFlagKeys), - flagCollectionChangeHandler: flagCollectionChangeHandler)) - flagsUnchangedOwnerKey = observers.first!.flagKeys.observerKey - let flagsUnchangedObservers = [FlagsUnchangedObserver(owner: observers.first!.owner!, flagsUnchangedHandler: flagsUnchangedHandler)] - subject = FlagChangeNotifier(flagChangeObservers: observers, flagsUnchangedObservers: flagsUnchangedObservers) - originalFlagChangeObservers = subject.flagObservers + init(owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { + self.owner = owner + let tracker = CallTracker() + self.observer = ConnectionModeChangedObserver(owner: owner) { + tracker.callCount += 1 + tracker.lastCallArg = $0 } + self.tracker = tracker + } +} - fileprivate mutating func stubOwner(key: String) -> FlagChangeHandlerOwnerMock { - let owner = FlagChangeHandlerOwnerMock() - owners[key] = owner +final class FlagChangeNotifierSpec: QuickSpec { + struct TestContext { + let subject: FlagChangeNotifier = FlagChangeNotifier() + fileprivate var flagChangeObservers: [LDFlagKey: MockFlagChangeObserver] = [:] + fileprivate var flagCollectionChangeObservers: [MockFlagCollectionChangeObserver] = [] + fileprivate var flagsUnchangedObservers: [MockFlagsUnchangedObserver] = [] + fileprivate var connectionModeObservers: [MockConnectionModeChangedObserver] = [] + + fileprivate mutating func addChangeObserver(forKey key: LDFlagKey, owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { + let changeObserver = MockFlagChangeObserver(key, owner: owner) + flagChangeObservers[key] = changeObserver + subject.addFlagChangeObserver(changeObserver.observer) + } - return owner + fileprivate mutating func addChangeObservers(forKeys keys: [LDFlagKey], owner: LDObserverOwner? = nil) { + keys.forEach { self.addChangeObserver(forKey: $0, owner: owner ?? FlagChangeHandlerOwnerMock()) } } - fileprivate mutating func stubOwner(keys: [String]) -> FlagChangeHandlerOwnerMock { - stubOwner(key: keys.observerKey) + fileprivate mutating func addCollectionChangeObserver(forKeys keys: [LDFlagKey], owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { + let changeObserver = MockFlagCollectionChangeObserver(keys, owner: owner) + flagCollectionChangeObservers.append(changeObserver) + subject.addFlagChangeObserver(changeObserver.observer) } - // Flag change handler stubs - func flagChangeHandler(changedFlag: LDChangedFlag) { } + fileprivate mutating func addUnchangedObserver(owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { + let unchangedObserver = MockFlagsUnchangedObserver(owner: owner) + flagsUnchangedObservers.append(unchangedObserver) + subject.addFlagsUnchangedObserver(unchangedObserver.observer) + } - func flagCollectionChangeHandler(changedFlags: [LDFlagKey: LDChangedFlag]) { } + fileprivate mutating func addConnectionModeObserver(owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { + let connectionModeObserver = MockConnectionModeChangedObserver(owner: owner) + connectionModeObservers.append(connectionModeObserver) + subject.addConnectionModeChangedObserver(connectionModeObserver.observer) + } - func flagsUnchangedHandler() { } - } + func awaitNotify(oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) { + subject.notifyObservers(oldFlags: oldFlags, newFlags: newFlags) + awaitNotifications() + } - struct Constants { - static let observerCount = 3 - static let userKey = "flagChangeNotifierSpecUserKey" + func awaitNotifications() { + // Notifications run on the main thread, so if there are still queued notifications, they will run before this + waitUntil { DispatchQueue.main.async(execute: $0) } + } } override func spec() { - addObserverSpec() + describe("init") { + it("no initial observers") { + let notifier = FlagChangeNotifier() + expect(notifier.flagChangeObservers).to(beEmpty()) + expect(notifier.flagsUnchangedObservers).to(beEmpty()) + expect(notifier.connectionModeChangedObservers).to(beEmpty()) + } + } + removeObserverSpec() notifyObserverSpec() + notifyConnectionSpec() } - private func addObserverSpec() { - var testContext: TestContext! + private func removeObserverSpec() { + describe("removeObserver") { + var testContext: TestContext! + var removedOwner: FlagChangeHandlerOwnerMock! + beforeEach { + testContext = TestContext() + removedOwner = FlagChangeHandlerOwnerMock() + } + it("works when no observers exist") { + testContext.subject.removeObserver(owner: removedOwner) + } + it("does not remove any when owner unused") { + testContext.addConnectionModeObserver() + testContext.addUnchangedObserver() + testContext.addChangeObservers(forKeys: ["a", "b", "c"]) + testContext.addCollectionChangeObserver(forKeys: LDFlagKey.anyKey) + testContext.addCollectionChangeObserver(forKeys: ["a", "b"]) + testContext.subject.removeObserver(owner: removedOwner) + expect(testContext.subject.connectionModeChangedObservers.count) == 1 + expect(testContext.subject.flagsUnchangedObservers.count) == 1 + expect(testContext.subject.flagChangeObservers.count) == 5 + } + it("can remove all observers") { + testContext.addConnectionModeObserver(owner: removedOwner) + testContext.addUnchangedObserver(owner: removedOwner) + testContext.addChangeObservers(forKeys: ["a", "b", "c"], owner: removedOwner) + testContext.addCollectionChangeObserver(forKeys: LDFlagKey.anyKey, owner: removedOwner) + testContext.addCollectionChangeObserver(forKeys: ["a", "b"], owner: removedOwner) + testContext.subject.removeObserver(owner: removedOwner) + expect(testContext.subject.connectionModeChangedObservers.count) == 0 + expect(testContext.subject.flagsUnchangedObservers.count) == 0 + expect(testContext.subject.flagChangeObservers.count) == 0 + } + it("can remove selected observers") { + testContext.addUnchangedObserver() + testContext.addChangeObserver(forKey: "a", owner: removedOwner) + testContext.addCollectionChangeObserver(forKeys: LDFlagKey.anyKey) + testContext.addCollectionChangeObserver(forKeys: ["a", "b"], owner: removedOwner) + testContext.addConnectionModeObserver() + testContext.subject.removeObserver(owner: removedOwner) + expect(testContext.subject.connectionModeChangedObservers.count) == 1 + expect(testContext.subject.flagsUnchangedObservers.count) == 1 + expect(testContext.subject.flagChangeObservers.count) == 1 + expect(testContext.subject.flagChangeObservers.first!.flagKeys) == LDFlagKey.anyKey + } + } + } - describe("add flag change observer") { - var observer: FlagChangeObserver! - context("when no observers exist") { + private func notifyObserverSpec() { + describe("notifyObservers") { + var testContext: TestContext! + beforeEach { + testContext = TestContext() + } + context("singular flag observer") { beforeEach { - testContext = TestContext() - observer = FlagChangeObserver(key: DarklyServiceMock.FlagKeys.bool, - owner: testContext.stubOwner(key: DarklyServiceMock.FlagKeys.bool), - flagChangeHandler: testContext.flagChangeHandler) - testContext.subject.addFlagChangeObserver(observer) + testContext.addChangeObservers(forKeys: ["a", "b"]) + testContext.addCollectionChangeObserver(forKeys: LDFlagKey.anyKey) } - it("adds the observer") { - expect(testContext.subject.flagObservers.count) == 1 - expect(testContext.subject.flagObservers.first) == observer + it("is not called on unchanged") { + testContext.awaitNotify(oldFlags: [:], newFlags: [:]) + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)], + newFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 2, flagVersion: 1)]) + testContext.flagChangeObservers.forEach { expect($0.value.callCount) == 0 } } - } - context("when observers exist") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount) - observer = FlagChangeObserver(key: DarklyServiceMock.FlagKeys.bool, - owner: testContext.stubOwner(key: DarklyServiceMock.FlagKeys.bool), - flagChangeHandler: testContext.flagChangeHandler) - testContext.subject.addFlagChangeObserver(observer) + it("is called on creation") { + testContext.awaitNotify(oldFlags: [:], newFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)]) + let expectedChange = LDChangedFlag(key: "a", oldValue: nil, newValue: 1) + expect(testContext.flagChangeObservers["a"]!.callCount) == 1 + expect(testContext.flagChangeObservers["a"]!.lastCallArg) == expectedChange } - it("adds the observer") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount + 1 - expect(testContext.subject.flagObservers.last) == observer + it("is called on deletion") { + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)], newFlags: [:]) + let expectedChange = LDChangedFlag(key: "a", oldValue: 1, newValue: nil) + expect(testContext.flagChangeObservers["a"]!.callCount) == 1 + expect(testContext.flagChangeObservers["a"]!.lastCallArg) == expectedChange } - } - } - describe("add flags unchanged observer") { - var observer: FlagsUnchangedObserver! - context("when no observers exist") { - beforeEach { - testContext = TestContext() - observer = FlagsUnchangedObserver(owner: testContext.stubOwner(key: DarklyServiceMock.FlagKeys.bool), - flagsUnchangedHandler: testContext.flagsUnchangedHandler) - testContext.subject.addFlagsUnchangedObserver(observer) + it("is called on update") { + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)], + newFlags: ["a": FeatureFlag(flagKey: "a", value: 2, variation: 1, version: 2, flagVersion: 1)]) + let expectedChange = LDChangedFlag(key: "a", oldValue: 1, newValue: 2) + expect(testContext.flagChangeObservers["a"]!.callCount) == 1 + expect(testContext.flagChangeObservers["a"]!.lastCallArg) == expectedChange } - it("adds the observer") { - expect(testContext.subject.noChangeObservers.count) == 1 - expect(testContext.subject.noChangeObservers.first?.owner) === observer.owner + it("calls multiple singular observers") { + let changeObserver = MockFlagChangeObserver("a") + testContext.subject.addFlagChangeObserver(changeObserver.observer) + testContext.awaitNotify(oldFlags: [:], newFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)]) + let expectedChange = LDChangedFlag(key: "a", oldValue: nil, newValue: 1) + expect(testContext.flagChangeObservers["a"]!.callCount) == 1 + expect(testContext.flagChangeObservers["a"]!.lastCallArg) == expectedChange + expect(changeObserver.callCount) == 1 + expect(changeObserver.lastCallArg) == expectedChange + } + afterEach { + expect(testContext.flagChangeObservers["b"]!.callCount) == 0 } } - context("when observers exist") { + context("multi flag observer") { beforeEach { - testContext = TestContext(observers: Constants.observerCount) - observer = FlagsUnchangedObserver(owner: testContext.stubOwner(key: DarklyServiceMock.FlagKeys.bool), - flagsUnchangedHandler: testContext.flagsUnchangedHandler) - testContext.subject.addFlagsUnchangedObserver(observer) + testContext.addCollectionChangeObserver(forKeys: ["a", "b"]) } - it("adds the observer") { - expect(testContext.subject.noChangeObservers.count) == Constants.observerCount + 1 - expect(testContext.subject.noChangeObservers.last?.owner) === observer.owner + it("is not called on unchanged") { + testContext.awaitNotify(oldFlags: [:], newFlags: [:]) + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)], + newFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 2, flagVersion: 1)]) + expect(testContext.flagCollectionChangeObservers.first!.callCount) == 0 } - } - } - } - - private func removeObserverSpec() { - var testContext: TestContext! - var targetObserver: FlagChangeObserver! - var targetOwner: FlagChangeHandlerOwnerMock! - - context("remove observer") { - context("when several observers exist") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount) - targetObserver = testContext.subject.flagObservers[Constants.observerCount - 2] // Take the middle one - - testContext.subject.removeObserver(owner: targetObserver.owner!) + it("is not called on unrelated") { + testContext.awaitNotify(oldFlags: [:], newFlags: ["c": FeatureFlag(flagKey: "c", value: 1, variation: 1, version: 1, flagVersion: 1)]) + expect(testContext.flagCollectionChangeObservers.first!.callCount) == 0 } - it("removes the observer") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - 1 - expect(testContext.subject.flagObservers.contains(targetObserver)).to(beFalse()) - expect(testContext.subject.noChangeObservers.count) == Constants.observerCount - 1 + it("is called on creation") { + testContext.awaitNotify(oldFlags: [:], newFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)]) + let expectedChanges = ["a": LDChangedFlag(key: "a", oldValue: nil, newValue: 1)] + expect(testContext.flagCollectionChangeObservers.first!.callCount) == 1 + expect(testContext.flagCollectionChangeObservers.first!.lastCallArg) == expectedChanges } - } - context("when 1 observer exists") { - beforeEach { - testContext = TestContext(observers: 1) - targetObserver = testContext.subject.flagObservers.first! - - testContext.subject.removeObserver(owner: targetObserver.owner!) + it("is called on deletion") { + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)], newFlags: [:]) + let expectedChanges = ["a": LDChangedFlag(key: "a", oldValue: 1, newValue: nil)] + expect(testContext.flagCollectionChangeObservers.first!.callCount) == 1 + expect(testContext.flagCollectionChangeObservers.first!.lastCallArg) == expectedChanges } - it("removes the observer") { - expect(testContext.subject.flagObservers.isEmpty).to(beTrue()) - expect(testContext.subject.noChangeObservers.isEmpty).to(beTrue()) + it("is called on update") { + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)], + newFlags: ["a": FeatureFlag(flagKey: "a", value: 2, variation: 1, version: 2, flagVersion: 1)]) + let expectedChanges = ["a": LDChangedFlag(key: "a", oldValue: 1, newValue: 2)] + expect(testContext.flagCollectionChangeObservers.first!.callCount) == 1 + expect(testContext.flagCollectionChangeObservers.first!.lastCallArg) == expectedChanges + } + it("called once with all updates") { + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1), + "b": FeatureFlag(flagKey: "b", value: "a", variation: 1, version: 1, flagVersion: 1)], + newFlags: ["b": FeatureFlag(flagKey: "b", value: "b", variation: 1, version: 2, flagVersion: 1), + "c": FeatureFlag(flagKey: "c", value: false, variation: 1, version: 1, flagVersion: 1)]) + let expectedChanges = ["a": LDChangedFlag(key: "a", oldValue: 1, newValue: nil), + "b": LDChangedFlag(key: "b", oldValue: "a", newValue: "b")] + expect(testContext.flagCollectionChangeObservers.first!.callCount) == 1 + expect(testContext.flagCollectionChangeObservers.first!.lastCallArg) == expectedChanges } } - context("when the target observer doesnt exist") { + context("any flag observer") { beforeEach { - testContext = TestContext(observers: Constants.observerCount) - targetOwner = FlagChangeHandlerOwnerMock() - - testContext.subject.removeObserver(owner: targetOwner!) + testContext.addCollectionChangeObserver(forKeys: LDFlagKey.anyKey) + } + it("is not called on unchanged") { + testContext.awaitNotify(oldFlags: [:], newFlags: [:]) + testContext.flagCollectionChangeObservers.forEach { expect($0.callCount) == 0 } + } + it("is called on creation") { + testContext.awaitNotify(oldFlags: [:], newFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)]) + let expectedChanges = ["a": LDChangedFlag(key: "a", oldValue: nil, newValue: 1)] + expect(testContext.flagCollectionChangeObservers.first!.callCount) == 1 + expect(testContext.flagCollectionChangeObservers.first!.lastCallArg) == expectedChanges } - it("leaves the observers unchanged") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - expect(testContext.subject.flagObservers) == testContext.originalFlagChangeObservers - expect(testContext.subject.noChangeObservers.count) == Constants.observerCount + it("is called on deletion") { + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)], newFlags: [:]) + let expectedChanges = ["a": LDChangedFlag(key: "a", oldValue: 1, newValue: nil)] + expect(testContext.flagCollectionChangeObservers.first!.callCount) == 1 + expect(testContext.flagCollectionChangeObservers.first!.lastCallArg) == expectedChanges + } + it("is called on update") { + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)], + newFlags: ["a": FeatureFlag(flagKey: "a", value: 2, variation: 1, version: 2, flagVersion: 1)]) + let expectedChanges = ["a": LDChangedFlag(key: "a", oldValue: 1, newValue: 2)] + expect(testContext.flagCollectionChangeObservers.first!.callCount) == 1 + expect(testContext.flagCollectionChangeObservers.first!.lastCallArg) == expectedChanges + } + it("called once with all updates") { + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1), + "b": FeatureFlag(flagKey: "b", value: "a", variation: 1, version: 1, flagVersion: 1)], + newFlags: ["b": FeatureFlag(flagKey: "b", value: "b", variation: 1, version: 2, flagVersion: 1), + "c": FeatureFlag(flagKey: "c", value: false, variation: 1, version: 1, flagVersion: 1)]) + let expectedChanges = ["a": LDChangedFlag(key: "a", oldValue: 1, newValue: nil), + "b": LDChangedFlag(key: "b", oldValue: "a", newValue: "b"), + "c": LDChangedFlag(key: "c", oldValue: nil, newValue: false)] + expect(testContext.flagCollectionChangeObservers.first!.callCount) == 1 + expect(testContext.flagCollectionChangeObservers.first!.lastCallArg) == expectedChanges } } - context("when 2 target observers exist") { + context("unchanged observer") { beforeEach { - testContext = TestContext(observers: Constants.observerCount, repeatFirstObserver: true) - targetObserver = testContext.subject.flagObservers.first! - - testContext.subject.removeObserver(owner: targetObserver.owner!) + testContext.addChangeObservers(forKeys: ["a", "b"]) + testContext.addCollectionChangeObserver(forKeys: ["a", "b"]) + testContext.addCollectionChangeObserver(forKeys: LDFlagKey.anyKey) + testContext.addUnchangedObserver() + testContext.addUnchangedObserver() } - it("removes both observers") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - 2 - expect(testContext.subject.flagObservers.contains(targetObserver)).to(beFalse()) - expect(testContext.subject.noChangeObservers.count) == Constants.observerCount - 2 + it("is not called on changes") { + testContext.awaitNotify(oldFlags: [:], newFlags: ["c": FeatureFlag(flagKey: "c", value: 1, variation: 1, version: 1, flagVersion: 1)]) + testContext.awaitNotify(oldFlags: ["c": FeatureFlag(flagKey: "c", value: 1, variation: 1, version: 1, flagVersion: 1)], newFlags: [:]) + testContext.awaitNotify(oldFlags: ["c": FeatureFlag(flagKey: "c", value: 1, variation: 1, version: 1, flagVersion: 1)], + newFlags: ["c": FeatureFlag(flagKey: "c", value: 2, variation: 1, version: 2, flagVersion: 1)]) + testContext.flagsUnchangedObservers.forEach { expect($0.callCount) == 0 } } - } - } - } - - private func notifyObserverSpec() { - describe("notify observers") { - notifyObserversWithSingleFlagObserverSpec() - notifyObserversWithMultipleFlagsObserverSpec() - notifyObserversWithAllFlagsObserverSpec() - } - } - - private func notifyObserversWithSingleFlagObserverSpec() { - var testContext: TestContext! - var targetChangedFlag: LDChangedFlag? - var oldFlags: [LDFlagKey: FeatureFlag]! - - context("with single flag observers") { - context("that are active") { - context("and different flags") { - it("activates the change handler") { - DarklyServiceMock.FlagKeys.flagsWithAnAlternateValue.forEach { key in - testContext = TestContext( - keys: DarklyServiceMock.FlagKeys.knownFlags, - flagChangeHandler: { changedFlag in - testContext.flagChangeHandlerCallCount += 1 - testContext.changedFlag = changedFlag - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = DarklyServiceMock.Constants.stubFeatureFlags(alternateValuesForKeys: [key]) - targetChangedFlag = LDChangedFlag.stub(key: key, oldFlags: oldFlags, newFlags: testContext.flagStore.featureFlags) - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - - expect(testContext.flagChangeHandlerCallCount) == 1 - expect(testContext.changedFlag) == targetChangedFlag - let newValue = testContext.changedFlag?.newValue as? String - let newValueFromChangedFlag = targetChangedFlag?.newValue as? String - if newValue != nil || newValueFromChangedFlag != nil { - expect(newValue) == newValueFromChangedFlag - } - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } - } - it("activates the change handler when the value changes but not the variation number") { - DarklyServiceMock.FlagKeys.flagsWithAnAlternateValue.forEach { key in - testContext = TestContext( - keys: DarklyServiceMock.FlagKeys.knownFlags, - flagChangeHandler: { changedFlag in - testContext.flagChangeHandlerCallCount += 1 - testContext.changedFlag = changedFlag - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = DarklyServiceMock.Constants.stubFeatureFlags(alternateVariationNumber: false, bumpFlagVersions: true, alternateValuesForKeys: [key]) - targetChangedFlag = LDChangedFlag.stub(key: key, oldFlags: oldFlags, newFlags: testContext.flagStore.featureFlags) - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - - expect(testContext.flagChangeHandlerCallCount) == 1 - expect(testContext.changedFlag) == targetChangedFlag - let newValue = testContext.changedFlag?.newValue as? String - let newValueFromChangedFlag = targetChangedFlag?.newValue as? String - if newValue != nil || newValueFromChangedFlag != nil { - expect(newValue) == newValueFromChangedFlag - } - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } - } + it("is called when flags unchanged") { + testContext.awaitNotify(oldFlags: [:], newFlags: [:]) + testContext.flagsUnchangedObservers.forEach { expect($0.callCount) == 1 } } - context("and unchanged flags") { - beforeEach { - testContext = TestContext( - keys: DarklyServiceMock.FlagKeys.knownFlags, - flagChangeHandler: { _ in - testContext.flagChangeHandlerCallCount += 1 - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = testContext.flagStore.featureFlags - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - } - it("activates the flags unchanged handler") { - expect(testContext.flagChangeHandlerCallCount) == 0 - expect(testContext.flagsUnchangedHandlerCallCount) == DarklyServiceMock.FlagKeys.knownFlags.count - } + it("is called when explicitly unchanged") { + testContext.subject.notifyUnchanged() + testContext.awaitNotifications() + testContext.flagsUnchangedObservers.forEach { expect($0.callCount) == 1 } } } - context("that are inactive") { - context("and different flags") { - it("does nothing") { - DarklyServiceMock.FlagKeys.flagsWithAnAlternateValue.forEach { key in - testContext = TestContext( - keys: DarklyServiceMock.FlagKeys.knownFlags, - flagChangeHandler: { changedFlag in - testContext.flagChangeHandlerCallCount += 1 - testContext.changedFlag = changedFlag - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = DarklyServiceMock.Constants.stubFeatureFlags(alternateValuesForKeys: [key]) - testContext.owners[key] = nil - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - - expect(testContext.flagChangeHandlerCallCount) == 0 - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } - } + context("removes and does not notify expired observers") { + beforeEach { + testContext.addChangeObservers(forKeys: ["a", "b"]) + testContext.addCollectionChangeObserver(forKeys: ["a", "b"]) + testContext.addCollectionChangeObserver(forKeys: LDFlagKey.anyKey) + testContext.addUnchangedObserver() + // Set expired + testContext.flagChangeObservers.forEach { $0.value.owner = nil } + testContext.flagCollectionChangeObservers.forEach { $0.owner = nil } + testContext.flagsUnchangedObservers.forEach { $0.owner = nil } } - context("and unchanged flags") { - beforeEach { - testContext = TestContext( - keys: DarklyServiceMock.FlagKeys.knownFlags, - flagChangeHandler: { _ in - testContext.flagChangeHandlerCallCount += 1 - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = testContext.flagStore.featureFlags - testContext.owners[testContext.flagsUnchangedOwnerKey!] = nil - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - } - it("does nothing") { - expect(testContext.flagChangeHandlerCallCount) == 0 - expect(testContext.flagsUnchangedHandlerCallCount) == DarklyServiceMock.FlagKeys.knownFlags.count - 1 - } + it("for added") { + testContext.awaitNotify(oldFlags: [:], newFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)]) } - } - } - } - - private func notifyObserversWithMultipleFlagsObserverSpec() { - var testContext: TestContext! - var targetChangedFlags: [LDFlagKey: LDChangedFlag]? - var oldFlags: [LDFlagKey: FeatureFlag]! - - context("with multiple flag observers") { - context("that are active") { - context("and different single flags") { - it("activates the change handler") { - DarklyServiceMock.FlagKeys.flagsWithAnAlternateValue.forEach { key in - testContext = TestContext( - keys: DarklyServiceMock.FlagKeys.knownFlags, - flagCollectionChangeHandler: { changedFlags in - testContext.flagCollectionChangeHandlerCallCount += 1 - testContext.changedFlags = changedFlags - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = DarklyServiceMock.Constants.stubFeatureFlags(alternateValuesForKeys: [key]) - targetChangedFlags = [key: LDChangedFlag.stub(key: key, oldFlags: oldFlags, newFlags: testContext.flagStore.featureFlags)] - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - - expect(testContext.flagCollectionChangeHandlerCallCount) == 1 - expect(testContext.changedFlags) == targetChangedFlags - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } - } + it("for removed") { + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)], newFlags: [:]) } - context("and different multiple flags") { - beforeEach { - testContext = TestContext( - keys: DarklyServiceMock.FlagKeys.knownFlags, - flagCollectionChangeHandler: { changedFlags in - testContext.flagCollectionChangeHandlerCallCount += 1 - testContext.changedFlags = changedFlags - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - let changedFlagKeys = [DarklyServiceMock.FlagKeys.bool, DarklyServiceMock.FlagKeys.int, DarklyServiceMock.FlagKeys.double] - oldFlags = DarklyServiceMock.Constants.stubFeatureFlags(alternateValuesForKeys: changedFlagKeys) - targetChangedFlags = [LDFlagKey: LDChangedFlag](uniqueKeysWithValues: changedFlagKeys.map { flagKey in - (flagKey, LDChangedFlag.stub(key: flagKey, oldFlags: oldFlags, newFlags: testContext.flagStore.featureFlags)) - }) - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - } - it("activates the change handler") { - expect(testContext.flagCollectionChangeHandlerCallCount) == 1 - expect(testContext.changedFlags) == targetChangedFlags - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } + it("for updated") { + testContext.awaitNotify(oldFlags: ["a": FeatureFlag(flagKey: "a", value: 1, variation: 1, version: 1, flagVersion: 1)], + newFlags: ["a": FeatureFlag(flagKey: "a", value: 2, variation: 1, version: 1, flagVersion: 1)]) } - context("and unchanged flags") { - beforeEach { - testContext = TestContext( - keys: DarklyServiceMock.FlagKeys.knownFlags, - flagCollectionChangeHandler: { changedFlags in - testContext.flagCollectionChangeHandlerCallCount += 1 - testContext.changedFlags = changedFlags - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = testContext.flagStore.featureFlags - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - } - it("activates the flags unchanged handler") { - expect(testContext.flagCollectionChangeHandlerCallCount) == 0 - expect(testContext.flagsUnchangedHandlerCallCount) == 1 - } + it("for unchanged") { + testContext.awaitNotify(oldFlags: [:], newFlags: [:]) } - } - context("that are inactive") { - context("and different flags") { - beforeEach { - testContext = TestContext( - keys: DarklyServiceMock.FlagKeys.knownFlags, - flagCollectionChangeHandler: { changedFlags in - testContext.flagCollectionChangeHandlerCallCount += 1 - testContext.changedFlags = changedFlags - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - let changedFlagKeys = [DarklyServiceMock.FlagKeys.bool, DarklyServiceMock.FlagKeys.int, DarklyServiceMock.FlagKeys.double] - oldFlags = DarklyServiceMock.Constants.stubFeatureFlags(alternateValuesForKeys: changedFlagKeys) - testContext.owners[DarklyServiceMock.FlagKeys.knownFlags.observerKey] = nil - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - } - it("does nothing") { - expect(testContext.flagCollectionChangeHandlerCallCount) == 0 - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } + it("for explicit unchanged") { + testContext.subject.notifyUnchanged() + testContext.awaitNotifications() } - context("and unchanged flags") { - beforeEach { - testContext = TestContext( - keys: DarklyServiceMock.FlagKeys.knownFlags, - flagCollectionChangeHandler: { changedFlags in - testContext.flagCollectionChangeHandlerCallCount += 1 - testContext.changedFlags = changedFlags - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = testContext.flagStore.featureFlags - testContext.owners[testContext.flagsUnchangedOwnerKey!] = nil + afterEach { + testContext.flagChangeObservers.forEach { expect($0.value.callCount) == 0 } + testContext.flagCollectionChangeObservers.forEach { expect($0.callCount) == 0 } + testContext.flagsUnchangedObservers.forEach { expect($0.callCount) == 0 } - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - } - it("does nothing") { - expect(testContext.flagCollectionChangeHandlerCallCount) == 0 - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } + expect(testContext.subject.flagChangeObservers).to(beEmpty()) + expect(testContext.subject.flagsUnchangedObservers).to(beEmpty()) } } } } - private func notifyObserversWithAllFlagsObserverSpec() { - var testContext: TestContext! - var targetChangedFlags: [LDFlagKey: LDChangedFlag]? - var oldFlags: [LDFlagKey: FeatureFlag]! - - context("with all flags observers") { - context("that are active") { - context("and different single flags") { - it("activates the change handler") { - DarklyServiceMock.FlagKeys.flagsWithAnAlternateValue.forEach { key in - testContext = TestContext( - keys: LDFlagKey.anyKey, - flagCollectionChangeHandler: { changedFlags in - testContext.flagCollectionChangeHandlerCallCount += 1 - testContext.changedFlags = changedFlags - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = DarklyServiceMock.Constants.stubFeatureFlags(alternateValuesForKeys: [key]) - targetChangedFlags = [key: LDChangedFlag.stub(key: key, oldFlags: oldFlags, newFlags: testContext.flagStore.featureFlags)] - targetChangedFlags![LDUser.StubConstants.userKey] = LDChangedFlag.stub(key: LDUser.StubConstants.userKey, - oldFlags: oldFlags, - newFlags: testContext.flagStore.featureFlags) - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - - expect(testContext.flagCollectionChangeHandlerCallCount) == 1 - expect(testContext.changedFlags) == targetChangedFlags - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } - } - } - context("and different multiple flags") { - beforeEach { - testContext = TestContext( - keys: LDFlagKey.anyKey, - flagCollectionChangeHandler: { changedFlags in - testContext.flagCollectionChangeHandlerCallCount += 1 - testContext.changedFlags = changedFlags - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - let changedFlagKeys = [DarklyServiceMock.FlagKeys.bool, DarklyServiceMock.FlagKeys.int, DarklyServiceMock.FlagKeys.double] - oldFlags = DarklyServiceMock.Constants.stubFeatureFlags(alternateValuesForKeys: changedFlagKeys) - targetChangedFlags = [LDFlagKey: LDChangedFlag](uniqueKeysWithValues: changedFlagKeys.map { flagKey in - (flagKey, LDChangedFlag.stub(key: flagKey, oldFlags: oldFlags, newFlags: testContext.flagStore.featureFlags)) - }) - targetChangedFlags![LDUser.StubConstants.userKey] = LDChangedFlag.stub(key: LDUser.StubConstants.userKey, - oldFlags: oldFlags, - newFlags: testContext.flagStore.featureFlags) - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - } - it("activates the change handler") { - expect(testContext.flagCollectionChangeHandlerCallCount) == 1 - expect(testContext.changedFlags) == targetChangedFlags - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } - } - context("and unchanged flags") { - beforeEach { - testContext = TestContext( - keys: LDFlagKey.anyKey, - flagCollectionChangeHandler: { changedFlags in - testContext.flagCollectionChangeHandlerCallCount += 1 - testContext.changedFlags = changedFlags - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = testContext.flagStore.featureFlags - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - } - it("activates the flags unchanged handler") { - expect(testContext.flagCollectionChangeHandlerCallCount) == 0 - expect(testContext.flagsUnchangedHandlerCallCount) == 1 - } - } - } - context("that are inactive") { - context("and different flags") { - beforeEach { - testContext = TestContext( - keys: LDFlagKey.anyKey, - flagCollectionChangeHandler: { changedFlags in - testContext.flagCollectionChangeHandlerCallCount += 1 - testContext.changedFlags = changedFlags - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - let changedFlagKeys = [DarklyServiceMock.FlagKeys.bool, DarklyServiceMock.FlagKeys.int, DarklyServiceMock.FlagKeys.double] - oldFlags = DarklyServiceMock.Constants.stubFeatureFlags(alternateValuesForKeys: changedFlagKeys) - testContext.owners[LDFlagKey.anyKey.observerKey] = nil - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - } - it("does nothing") { - expect(testContext.flagCollectionChangeHandlerCallCount) == 0 - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } - } - context("and unchanged flags") { - beforeEach { - testContext = TestContext( - keys: LDFlagKey.anyKey, - flagCollectionChangeHandler: { changedFlags in - testContext.flagCollectionChangeHandlerCallCount += 1 - testContext.changedFlags = changedFlags - }, - flagsUnchangedHandler: { - testContext.flagsUnchangedHandlerCallCount += 1 - }) - oldFlags = testContext.flagStore.featureFlags - testContext.owners[testContext.flagsUnchangedOwnerKey!] = nil - - waitUntil { done in - testContext.subject.notifyObservers(flagStore: testContext.flagStore, oldFlags: oldFlags, completion: done) - } - } - it("does nothing") { - expect(testContext.flagCollectionChangeHandlerCallCount) == 0 - expect(testContext.flagsUnchangedHandlerCallCount) == 0 - } - } + private func notifyConnectionSpec() { + describe("notifyConnectionModeChangedObservers") { + it("removes and does not notify expired observers") { + let testContext = TestContext() + let nonExpiredObserver = MockConnectionModeChangedObserver() + let expiredObserver = MockConnectionModeChangedObserver() + testContext.subject.addConnectionModeChangedObserver(nonExpiredObserver.observer) + testContext.subject.addConnectionModeChangedObserver(expiredObserver.observer) + expiredObserver.owner = nil + testContext.subject.notifyConnectionModeChangedObservers(connectionMode: .polling) + testContext.awaitNotifications() + expect(expiredObserver.callCount) == 0 + expect(testContext.subject.connectionModeChangedObservers.count) == 1 + expect(nonExpiredObserver.callCount) == 1 + expect(nonExpiredObserver.lastCallArg) == .polling } } } @@ -675,20 +423,6 @@ final class FlagChangeNotifierSpec: QuickSpec { private final class FlagChangeHandlerOwnerMock { } -fileprivate extension DarklyServiceMock.FlagKeys { - static let extra = "extra-key" -} - -fileprivate extension DarklyServiceMock.FlagValues { - static let extra = "extra-key-value" -} - -fileprivate extension LDChangedFlag { - static func stub(key: LDFlagKey, oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) -> LDChangedFlag { - LDChangedFlag(key: key, oldValue: oldFlags[key]?.value, newValue: newFlags[key]?.value) - } -} - extension LDChangedFlag: Equatable { public static func == (lhs: LDChangedFlag, rhs: LDChangedFlag) -> Bool { lhs.key == rhs.key @@ -696,7 +430,3 @@ extension LDChangedFlag: Equatable { && AnyComparer.isEqual(lhs.newValue, to: rhs.newValue) } } - -fileprivate extension Array where Element == LDFlagKey { - var observerKey: String { joined(separator: ".") } -} From 5c04f08d879de605822728a93b5e5eaf9927ff31 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 13 Aug 2021 22:52:41 +0000 Subject: [PATCH 08/50] [ch115552] Filter deep nulls in flag data. (#159) --- .../LaunchDarkly/Extensions/Dictionary.swift | 25 ++++++++- .../Cache/CacheableEnvironmentFlags.swift | 8 --- .../Cache/CacheableEnvironmentFlagsSpec.swift | 54 ++++++++----------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift b/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift index 377006f7..5ff12c8c 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift @@ -38,10 +38,33 @@ extension Dictionary where Key == String { extension Dictionary where Key == String, Value == Any { var withNullValuesRemoved: [String: Any] { - self.filter { !($1 is NSNull) }.mapValues { value in + (self as [String: Any?]).compactMapValues { value in + if value is NSNull { + return nil + } if let dictionary = value as? [String: Any] { return dictionary.withNullValuesRemoved } + if let arr = value as? [Any] { + return arr.withNullValuesRemoved + } + return value + } + } +} + +private extension Array where Element == Any { + var withNullValuesRemoved: [Any] { + (self as [Any?]).compactMap { value in + if value is NSNull { + return nil + } + if let arr = value as? [Any] { + return arr.withNullValuesRemoved + } + if let dict = value as? [String: Any] { + return dict.withNullValuesRemoved + } return value } } diff --git a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift index 243305fd..0f924177 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift @@ -35,11 +35,3 @@ struct CacheableEnvironmentFlags { self.init(userKey: userKey, mobileKey: mobileKey, featureFlags: featureFlags) } } - -extension CacheableEnvironmentFlags: Equatable { - static func == (lhs: CacheableEnvironmentFlags, rhs: CacheableEnvironmentFlags) -> Bool { - lhs.userKey == rhs.userKey - && lhs.mobileKey == rhs.mobileKey - && lhs.featureFlags == rhs.featureFlags - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift index 8574d878..cf86824c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift @@ -26,7 +26,6 @@ final class CacheableEnvironmentFlagsSpec: QuickSpec { initWithElementsSpec() initWithDictionarySpec() dictionaryValueSpec() - equalsSpec() } private func initWithElementsSpec() { @@ -86,41 +85,30 @@ final class CacheableEnvironmentFlagsSpec: QuickSpec { expect(cacheDictionary["mobileKey"] as? String) == TestValues.mobKey expect(AnyComparer.isEqual(cacheDictionary["featureFlags"], to: [:])) == true } + // Ultimately, this is not desired behavior, but currently we are unable to store internal nil/null values + // inside of the `KeyedValueCache`. When we update our cache format, we can encode all data to get around this. + it("removes internal nulls") { + let flags = ["flag1": FeatureFlag(flagKey: "flag1", value: ["abc": [1, nil, 3]]), + "flag2": FeatureFlag(flagKey: "flag2", value: [1, ["abc": nil], 3])] + let cacheable = CacheableEnvironmentFlags(userKey: "user", mobileKey: "mobile", featureFlags: flags) + let dictionaryFlags = cacheable.dictionaryValue["featureFlags"] as! [String: [String: Any]] + let flag1 = FeatureFlag(dictionary: dictionaryFlags["flag1"]) + let flag2 = FeatureFlag(dictionary: dictionaryFlags["flag2"]) + // Manually comparing fields, `==` on `FeatureFlag` does not compare values. + expect(flag1?.flagKey) == "flag1" + expect(AnyComparer.isEqual(flag1?.value, to: ["abc": [1, 3]])).to(beTrue()) + expect(flag2?.flagKey) == "flag2" + expect(AnyComparer.isEqual(flag2?.value, to: [1, [:], 3])).to(beTrue()) + } } } } +} - private func equalsSpec() { - let environmentFlags = TestValues.defaultEnvironment() - describe("equals") { - it("returns true when elements are equal") { - let other = CacheableEnvironmentFlags(userKey: environmentFlags.userKey, - mobileKey: environmentFlags.mobileKey, - featureFlags: environmentFlags.featureFlags) - expect(environmentFlags == other) == true - } - context("returns false") { - it("when the userKey differs") { - let other = CacheableEnvironmentFlags(userKey: UUID().uuidString, - mobileKey: environmentFlags.mobileKey, - featureFlags: environmentFlags.featureFlags) - expect(environmentFlags == other) == false - } - it("when the mobileKey differs") { - let other = CacheableEnvironmentFlags(userKey: environmentFlags.userKey, - mobileKey: UUID().uuidString, - featureFlags: environmentFlags.featureFlags) - expect(environmentFlags == other) == false - } - it("when the featureFlags differ") { - var otherFlags = environmentFlags.featureFlags - otherFlags.removeValue(forKey: otherFlags.first!.key) - let other = CacheableEnvironmentFlags(userKey: environmentFlags.userKey, - mobileKey: environmentFlags.mobileKey, - featureFlags: otherFlags) - expect(environmentFlags == other) == false - } - } - } +extension CacheableEnvironmentFlags: Equatable { + public static func == (lhs: CacheableEnvironmentFlags, rhs: CacheableEnvironmentFlags) -> Bool { + lhs.userKey == rhs.userKey + && lhs.mobileKey == rhs.mobileKey + && lhs.featureFlags == rhs.featureFlags } } From a2bf12b353e4b5049da100d3685f81b0a3e69d61 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 1 Oct 2021 16:29:34 -0700 Subject: [PATCH 09/50] Add CI job for Xcode 13. (#160) --- .circleci/config.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f036abc7..3a4aabc6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -116,12 +116,16 @@ workflows: build: jobs: + - build: + name: Xcode 13.0 - Swift 5.5 + xcode-version: '13.0.0' + ios-sim: 'platform=iOS Simulator,name=iPhone 11,OS=15.0' + build-doc: true + run-lint: true - build: name: Xcode 12.5 - Swift 5.4 xcode-version: '12.5.0' ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=14.5' - build-doc: true - run-lint: true - build: name: Xcode 12.0 - Swift 5.3 xcode-version: '12.0.1' From d2e99bd83e5365564eeb6cd660d29e24064dd52e Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 19 Nov 2021 12:11:58 -0600 Subject: [PATCH 10/50] Remove Cartfile that specifies redundant framework dependency on LDSwiftEventSource (#161) --- Cartfile | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Cartfile diff --git a/Cartfile b/Cartfile deleted file mode 100644 index f234102f..00000000 --- a/Cartfile +++ /dev/null @@ -1 +0,0 @@ -github "launchdarkly/swift-eventsource" == 1.2.1 \ No newline at end of file From be3ff00321e81bd795d9352cd6bce20d4608e688 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Wed, 8 Dec 2021 15:31:22 -0600 Subject: [PATCH 11/50] Fix CircleCI and improve build time of documentation step (#163) --- .circleci/config.yml | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3a4aabc6..9b7b0e10 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: type: string ssh-fix: type: boolean - default: true + default: false build-doc: type: boolean default: false @@ -24,19 +24,15 @@ jobs: steps: - checkout - # This hack shouldn't be necessary, as we don't actually use SSH - # to get any dependencies, but for some reason starting in the - # '12.0.0' Xcode image it's become necessary. + # There's an XCode bug present in the 12.0.1 CircleCI image that prevents fetching SSH + # dependencies from working in some cases, so we disable CircleCI's rewriting of the HTTPS + # GitHub URLs to SSH. - when: condition: <> steps: - run: - name: SSH fingerprint fix - command: | - sudo defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM YES - rm ~/.ssh/id_rsa || true - for ip in $(dig @8.8.8.8 bitbucket.org +short); do ssh-keyscan bitbucket.org,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts || true - for ip in $(dig @8.8.8.8 github.com +short); do ssh-keyscan github.com,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts || true + name: SSH fix + command: git config --global --unset url.ssh://git@github.com.insteadof - run: name: Setup for builds @@ -87,11 +83,25 @@ jobs: - when: condition: <> steps: + - restore_cache: + key: v1-gem-cache-<>- + - run: - name: Build Documentation + name: Install jazzy gem command: | - sudo gem install jazzy - jazzy -o artifacts/docs + gem install jazzy + gem cleanup + # Used as cache key to prevent storing redundant caches + gem list > /tmp/cache-key.txt + + - save_cache: + key: v1-gem-cache-<>-{{ checksum "/tmp/cache-key.txt" }} + paths: + - ~/.gem + + - run: + name: Build Documentation + command: jazzy -o artifacts/docs - when: condition: <> @@ -117,8 +127,8 @@ workflows: build: jobs: - build: - name: Xcode 13.0 - Swift 5.5 - xcode-version: '13.0.0' + name: Xcode 13.1 - Swift 5.5 + xcode-version: '13.1.0' ios-sim: 'platform=iOS Simulator,name=iPhone 11,OS=15.0' build-doc: true run-lint: true @@ -130,8 +140,8 @@ workflows: name: Xcode 12.0 - Swift 5.3 xcode-version: '12.0.1' ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=14.0' + ssh-fix: true - build: name: Xcode 11.4 - Swift 5.2 xcode-version: '11.4.1' ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=12.2' - ssh-fix: false From bfe48df90e73ae936f7572db4f6cf49a77a04563 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Tue, 28 Dec 2021 16:14:03 -0600 Subject: [PATCH 12/50] Permit additional fields on delete dictionary. (#164) --- .../LaunchDarkly/ServiceObjects/FlagStore.swift | 3 +-- .../ServiceObjects/FlagStoreSpec.swift | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift index a31b68cc..4866a317 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -99,8 +99,7 @@ final class FlagStore: FlagMaintaining { } } } - guard deleteDictionary.keys.sorted() == [Keys.flagKey, FeatureFlag.CodingKeys.version.rawValue], - let flagKey = deleteDictionary[Keys.flagKey] as? String, + guard let flagKey = deleteDictionary[Keys.flagKey] as? String, let newVersion = deleteDictionary[FeatureFlag.CodingKeys.version.rawValue] as? Int else { Log.debug(self.typeName(and: #function) + "aborted. Malformed delete dictionary. deleteDictionary: \(String(describing: deleteDictionary))") diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift index 04c251d6..a37bc73b 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift @@ -175,9 +175,19 @@ final class FlagStoreSpec: QuickSpec { beforeEach { subject = FlagStore(featureFlags: DarklyServiceMock.Constants.stubFeatureFlags()) } - it("removes the feature flag from the store") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1)) - expect(subject.featureFlags[DarklyServiceMock.FlagKeys.int]).to(beNil()) + context("removes flag") { + it("with exact dictionary") { + deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1)) + expect(subject.featureFlags.count) == self.stubFlags.count - 1 + expect(subject.featureFlags[DarklyServiceMock.FlagKeys.int]).to(beNil()) + } + it("with extra fields on dictionary") { + var deleteDictionary = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) + deleteDictionary["new-field"] = 10 + deleteFlag(deleteDictionary) + expect(subject.featureFlags.count) == self.stubFlags.count - 1 + expect(subject.featureFlags[DarklyServiceMock.FlagKeys.int]).to(beNil()) + } } context("makes no changes to the flag store") { it("when the version is the same") { From 22f1aff505037cd0697f5f10b09f4a0130e42d78 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Tue, 28 Dec 2021 16:15:50 -0600 Subject: [PATCH 13/50] Cleanup error notifier (#165) --- .../LaunchDarkly/Models/ErrorObserver.swift | 2 +- .../ServiceObjects/ErrorNotifier.swift | 16 +-- .../LaunchDarklyTests/LDClientSpec.swift | 2 +- .../Models/ErrorObserverSpec.swift | 24 ++-- .../ServiceObjects/ErrorNotifierSpec.swift | 104 +++++++----------- 5 files changed, 57 insertions(+), 91 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/ErrorObserver.swift b/LaunchDarkly/LaunchDarkly/Models/ErrorObserver.swift index 94f9da83..ff475d93 100644 --- a/LaunchDarkly/LaunchDarkly/Models/ErrorObserver.swift +++ b/LaunchDarkly/LaunchDarkly/Models/ErrorObserver.swift @@ -9,7 +9,7 @@ import Foundation struct ErrorObserver { weak var owner: LDObserverOwner? - var errorHandler: LDErrorHandler? + let errorHandler: LDErrorHandler init(owner: LDObserverOwner, errorHandler: @escaping LDErrorHandler) { self.owner = owner diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift index 0bfe1578..70e559bd 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift @@ -22,27 +22,15 @@ final class ErrorNotifier: ErrorNotifying { } func removeObservers(for owner: LDObserverOwner) { - errorObservers = errorObservers.filter { $0.owner !== owner } + errorObservers.removeAll { $0.owner === owner } } func notifyObservers(of error: Error) { removeOldObservers() - errorObservers.forEach { $0.errorHandler?(error) } + errorObservers.forEach { $0.errorHandler(error) } } private func removeOldObservers() { errorObservers = errorObservers.filter { $0.owner != nil } } } - -#if DEBUG -extension ErrorNotifier { - func erase(owner: LDObserverOwner) { - for index in 0.. ErrorObserver { ErrorObserver(owner: owner!, errorHandler: handler) } } +class ErrorObserverOwner { } private class ErrorMock: Error { } final class ErrorObserverSpec: XCTestCase { func testInit() { - let errorOwner = ErrorOwnerMock() - let errorObserver = ErrorObserver(owner: errorOwner, errorHandler: errorOwner.handle) - XCTAssert(errorObserver.owner === errorOwner) + let context = ErrorObserverContext() + let errorObserver = context.observer() + XCTAssert(errorObserver.owner === context.owner) + XCTAssertNotNil(errorObserver.errorHandler) let errorMock = ErrorMock() - XCTAssertNotNil(errorObserver.errorHandler) - errorObserver.errorHandler?(errorMock) - XCTAssertEqual(errorOwner.errors.count, 1) - XCTAssert(errorOwner.errors[0] as? ErrorMock === errorMock) + errorObserver.errorHandler(errorMock) + XCTAssertEqual(context.errors.count, 1) + XCTAssert(context.errors[0] as? ErrorMock === errorMock) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ErrorNotifierSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ErrorNotifierSpec.swift index fda59daa..fb96c63b 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ErrorNotifierSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ErrorNotifierSpec.swift @@ -1,89 +1,65 @@ -// -// ErrorNotifyingSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation import XCTest @testable import LaunchDarkly final class ErrorNotifierSpec: XCTestCase { - - func testInit() { - let errorNotifier = ErrorNotifier() - XCTAssertEqual(errorNotifier.errorObservers.count, 0) - } - - func testAddErrorObserver() { - let errorNotifier = ErrorNotifier() - for index in 0..<4 { - let owner = ErrorOwnerMock() - errorNotifier.addErrorObserver(ErrorObserver(owner: owner, errorHandler: { _ in })) - XCTAssertEqual(errorNotifier.errorObservers.count, index + 1) - XCTAssert(errorNotifier.errorObservers[index].owner === owner) - } - } - - func testRemoveObserversNoObservers() { + func testAddAndRemoveObservers() { let errorNotifier = ErrorNotifier() - let owner = ErrorOwnerMock() - errorNotifier.removeObservers(for: owner) XCTAssertEqual(errorNotifier.errorObservers.count, 0) - } - func testRemoveObserversMatchingAll() { - let errorNotifier = ErrorNotifier() - let owner = ErrorOwnerMock() - errorNotifier.addErrorObserver(ErrorObserver(owner: owner, errorHandler: { _ in })) - errorNotifier.removeObservers(for: owner) + errorNotifier.removeObservers(for: ErrorObserverOwner()) XCTAssertEqual(errorNotifier.errorObservers.count, 0) - } - func testRemoveObserversMatchingNone() { - let errorNotifier = ErrorNotifier() - (0..<4).forEach { _ in - errorNotifier.addErrorObserver(ErrorObserver(owner: ErrorOwnerMock(), errorHandler: { _ in })) - } - let owner = ErrorOwnerMock() - errorNotifier.removeObservers(for: owner) + let firstContext = ErrorObserverContext() + let secondContext = ErrorObserverContext() + errorNotifier.addErrorObserver(firstContext.observer()) + errorNotifier.addErrorObserver(secondContext.observer()) + errorNotifier.addErrorObserver(firstContext.observer()) + errorNotifier.addErrorObserver(secondContext.observer()) XCTAssertEqual(errorNotifier.errorObservers.count, 4) - } - func testRemoveObserversMatchingSome() { - let errorNotifier = ErrorNotifier() - let owner = ErrorOwnerMock() - (0..<4).forEach { _ in - errorNotifier.addErrorObserver(ErrorObserver(owner: ErrorOwnerMock(), errorHandler: { _ in })) - errorNotifier.addErrorObserver(ErrorObserver(owner: owner, errorHandler: { _ in })) - } - errorNotifier.removeObservers(for: owner) + errorNotifier.removeObservers(for: ErrorObserverOwner()) XCTAssertEqual(errorNotifier.errorObservers.count, 4) - XCTAssert(!errorNotifier.errorObservers.contains { $0.owner === owner }) + + errorNotifier.removeObservers(for: firstContext.owner!) + XCTAssertEqual(errorNotifier.errorObservers.count, 2) + XCTAssert(!errorNotifier.errorObservers.contains { $0.owner !== secondContext.owner }) + + errorNotifier.removeObservers(for: secondContext.owner!) + XCTAssertEqual(errorNotifier.errorObservers.count, 0) + + XCTAssertEqual(firstContext.errors.count, 0) + XCTAssertEqual(secondContext.errors.count, 0) } func testNotifyObservers() { let errorNotifier = ErrorNotifier() - let owner = ErrorOwnerMock() - var otherOwners: [ErrorOwnerMock] = [] - (0..<4).forEach { _ in - let newOwner = ErrorOwnerMock() - otherOwners.append(newOwner) - errorNotifier.addErrorObserver(ErrorObserver(owner: newOwner, errorHandler: newOwner.handle)) - errorNotifier.addErrorObserver(ErrorObserver(owner: owner, errorHandler: owner.handle)) + let firstContext = ErrorObserverContext() + let secondContext = ErrorObserverContext() + let thirdContext = ErrorObserverContext() + + (0..<2).forEach { _ in + [firstContext, secondContext, thirdContext].forEach { + errorNotifier.addErrorObserver($0.observer()) + } } - errorNotifier.erase(owner: owner) + // remove reference to owner in secondContext + secondContext.owner = nil + let errorMock = ErrorMock() errorNotifier.notifyObservers(of: errorMock) - XCTAssertEqual(errorNotifier.errorObservers.count, 4) - XCTAssert(!errorNotifier.errorObservers.contains { $0.owner === owner }) - XCTAssertEqual(owner.errors.count, 0) - for owner in otherOwners { - XCTAssertEqual(owner.errors.count, 1) - XCTAssert(owner.errors[0] as? ErrorMock === errorMock) + [firstContext, thirdContext].forEach { + XCTAssertEqual($0.errors.count, 2) + XCTAssert($0.errors[0] as? ErrorMock === errorMock) + XCTAssert($0.errors[1] as? ErrorMock === errorMock) } + + // Ownerless observer should not be notified + XCTAssertEqual(secondContext.errors.count, 0) + // Should remove the observers that no longer have an owner + XCTAssertEqual(errorNotifier.errorObservers.count, 4) + XCTAssert(!errorNotifier.errorObservers.contains { $0.owner !== firstContext.owner && $0.owner !== thirdContext.owner }) } } From 425154eb29af7adb5f4ffb8a0f67ad1534dd5ad9 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 6 Jan 2022 12:48:50 -0600 Subject: [PATCH 14/50] Remove oldest deprecated caches. --- LaunchDarkly.xcodeproj/project.pbxproj | 42 ---------- .../Cache/DeprecatedCache.swift | 2 +- .../Cache/DeprecatedCacheModelV2.swift | 58 -------------- .../Cache/DeprecatedCacheModelV3.swift | 66 --------------- .../Cache/DeprecatedCacheModelV4.swift | 74 ----------------- .../ServiceObjects/ClientServiceFactory.swift | 3 - .../Cache/CacheConverterSpec.swift | 2 +- .../Cache/DeprecatedCacheModelSpec.swift | 13 ++- .../Cache/DeprecatedCacheModelV2Spec.swift | 54 ------------- .../Cache/DeprecatedCacheModelV3Spec.swift | 73 ----------------- .../Cache/DeprecatedCacheModelV4Spec.swift | 80 ------------------- .../Cache/DeprecatedCacheModelV5Spec.swift | 1 - 12 files changed, 7 insertions(+), 461 deletions(-) delete mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift delete mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift delete mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV2Spec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV3Spec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV4Spec.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 2d75e252..92b06d2f 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -107,10 +107,6 @@ 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; - 832D68A7224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */; }; - 832D68A8224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */; }; - 832D68A9224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */; }; - 832D68AA224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */; }; 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68AB224B3321005F052A /* CacheConverterSpec.swift */; }; 832EA061203D03B700A93C0E /* AnyComparerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832EA060203D03B700A93C0E /* AnyComparerSpec.swift */; }; 8335299E1FC37727001166F8 /* FlagMaintainingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */; }; @@ -191,17 +187,6 @@ 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */; }; 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */; }; 83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */; }; - 83D1522B224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */; }; - 83D1522C224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */; }; - 83D1522D224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */; }; - 83D1522E224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */; }; - 83D15230224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */; }; - 83D15231224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */; }; - 83D15232224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */; }; - 83D15233224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */; }; - 83D15235225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D15234225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift */; }; - 83D15237225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D15236225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift */; }; - 83D15239225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D15238225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift */; }; 83D1523B22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */; }; 83D17EAA1FCDA18C00B2823C /* DictionarySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */; }; 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */; }; @@ -391,7 +376,6 @@ 832307A91F7ECA630029815A /* LDConfigStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDConfigStub.swift; sourceTree = ""; }; 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV5.swift; sourceTree = ""; }; 832D68A1224A38FC005F052A /* CacheConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverter.swift; sourceTree = ""; }; - 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV2.swift; sourceTree = ""; }; 832D68AB224B3321005F052A /* CacheConverterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverterSpec.swift; sourceTree = ""; }; 832EA060203D03B700A93C0E /* AnyComparerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyComparerSpec.swift; sourceTree = ""; }; 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagMaintainingMock.swift; sourceTree = ""; }; @@ -448,11 +432,6 @@ 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagsUnchangedObserver.swift; sourceTree = ""; }; 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventReporterSpec.swift; sourceTree = ""; }; 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarklyServiceMock.swift; sourceTree = ""; }; - 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV3.swift; sourceTree = ""; }; - 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV4.swift; sourceTree = ""; }; - 83D15234225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV2Spec.swift; sourceTree = ""; }; - 83D15236225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV3Spec.swift; sourceTree = ""; }; - 83D15238225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV4Spec.swift; sourceTree = ""; }; 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV5Spec.swift; sourceTree = ""; }; 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionarySpec.swift; sourceTree = ""; }; 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedValueCache.swift; sourceTree = ""; }; @@ -622,9 +601,6 @@ 832D68A1224A38FC005F052A /* CacheConverter.swift */, 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */, 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */, - 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */, - 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */, - 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */, B4C9D4322489C8FD004A9B03 /* DiagnosticCache.swift */, ); path = Cache; @@ -638,9 +614,6 @@ 832D68AB224B3321005F052A /* CacheConverterSpec.swift */, B43D5ACF25FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift */, 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */, - 83D15238225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift */, - 83D15236225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift */, - 83D15234225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift */, B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */, ); path = Cache; @@ -1255,12 +1228,10 @@ 831188672113AE4D00D77CB5 /* Thread.swift in Sources */, 832D68A0224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 8354AC642241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, - 83D1522E224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, C443A40823145FEE00145710 /* ConnectionInformationStore.swift in Sources */, 831188662113AE4A00D77CB5 /* AnyComparer.swift in Sources */, 831188492113ADD400D77CB5 /* LDFlagBaseTypeConvertible.swift in Sources */, 8311885C2113AE2200D77CB5 /* HTTPHeaders.swift in Sources */, - 832D68AA224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */, 831188562113AE0800D77CB5 /* FlagSynchronizer.swift in Sources */, 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */, 8311885A2113AE1500D77CB5 /* Log.swift in Sources */, @@ -1275,7 +1246,6 @@ 8311884E2113ADE500D77CB5 /* Event.swift in Sources */, 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */, 831188482113ADD100D77CB5 /* LDFlagValueConvertible.swift in Sources */, - 83D15233224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */, 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */, B4C9D4312489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 831188462113ADCA00D77CB5 /* LDUser.swift in Sources */, @@ -1300,7 +1270,6 @@ 832D689F224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 831EF34620655E730001C643 /* LDUser.swift in Sources */, 831EF34720655E730001C643 /* LDFlagValue.swift in Sources */, - 83D15232224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */, 830DB3B02239B54900D65D25 /* URLResponse.swift in Sources */, 831EF34820655E730001C643 /* LDFlagValueConvertible.swift in Sources */, 831EF34920655E730001C643 /* LDFlagBaseTypeConvertible.swift in Sources */, @@ -1321,7 +1290,6 @@ 831EF35520655E730001C643 /* FlagSynchronizer.swift in Sources */, B4C9D4302489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 831EF35620655E730001C643 /* FlagChangeNotifier.swift in Sources */, - 832D68A9224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */, 831EF35720655E730001C643 /* EventReporter.swift in Sources */, 831EF35820655E730001C643 /* FlagStore.swift in Sources */, 83883DD7220B68A000EEAB95 /* ErrorObserver.swift in Sources */, @@ -1351,7 +1319,6 @@ 831EF36820655E730001C643 /* ObjcLDUser.swift in Sources */, B4C9D43A2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, 831EF36A20655E730001C643 /* ObjcLDChangedFlag.swift in Sources */, - 83D1522D224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1391,11 +1358,9 @@ 832D689D224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 838838411F5EFADF0023D11B /* LDFlagValue.swift in Sources */, 838838431F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift in Sources */, - 83D1522B224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, 835E1D411F63450A00184DB4 /* ObjcLDUser.swift in Sources */, 8354AC612241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */, - 832D68A7224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */, 831D2AAF2061AAA000B4AC3C /* Thread.swift in Sources */, 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */, 8354EFE01F26380700C05156 /* LDClient.swift in Sources */, @@ -1411,7 +1376,6 @@ 83DDBEFC1FA24B2700E428B6 /* JSONSerialization.swift in Sources */, 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */, 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */, - 83D15230224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */, 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */, B4C9D42E2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 83B8C2471FE4071F0082B8A9 /* HTTPURLResponse.swift in Sources */, @@ -1433,7 +1397,6 @@ 831CE0661F853A1700A13A3A /* Match.swift in Sources */, 83DDBF001FA2589900E428B6 /* FlagStoreSpec.swift in Sources */, B4F689142497B2FC004D3CE0 /* DiagnosticEventSpec.swift in Sources */, - 83D15237225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift in Sources */, 83396BC91F7C3711000E256E /* DarklyServiceSpec.swift in Sources */, 83EF67931F9945E800403126 /* EventSpec.swift in Sources */, 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */, @@ -1465,14 +1428,12 @@ 8354AC6E22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift in Sources */, 83B9A080204F56F4000C3F17 /* FlagChangeObserverSpec.swift in Sources */, 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, - 83D15235225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift in Sources */, 831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */, 838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */, 83883DDF220B6D4B00EEAB95 /* ErrorObserverSpec.swift in Sources */, 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */, 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */, 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */, - 83D15239225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift in Sources */, 838F967A1FBA551A009CFC45 /* ClientServiceMockFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1514,12 +1475,10 @@ 832D689E224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 8354AC622241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83D9EC8C2062DEAB004D7FA6 /* HTTPHeaders.swift in Sources */, - 83D1522C224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, C443A40623145FED00145710 /* ConnectionInformationStore.swift in Sources */, 83D9EC8D2062DEAB004D7FA6 /* DarklyService.swift in Sources */, 83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */, 83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */, - 832D68A8224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */, 83D9EC902062DEAB004D7FA6 /* Dictionary.swift in Sources */, 831425B2206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */, @@ -1533,7 +1492,6 @@ 83D9EC962062DEAB004D7FA6 /* AnyComparer.swift in Sources */, 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */, 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */, - 83D15231224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */, 83D9EC982062DEAB004D7FA6 /* ObjcLDClient.swift in Sources */, B4C9D42F2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 83D9EC992062DEAB004D7FA6 /* ObjcLDConfig.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift index 911a3134..ea7f56de 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift @@ -36,7 +36,7 @@ extension DeprecatedCache { } enum DeprecatedCacheModel: String, CaseIterable { - case version5, version4, version3, version2 // version1 is not supported + case version5 // earlier versions are not supported } // updatedAt in cached data was used as the LDUser.lastUpdated, which is deprecated in the Swift SDK diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift deleted file mode 100644 index 16ac8c7c..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// DeprecatedCacheModelV2.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation - -// Cache model in use from 2.3.3 up to 2.11.0 -/* Cache model v2 schema -[: [ - “key: , //LDUserModel dictionary - “ip”: , - “country”: , - “email”: , - “name”: , - “firstName”: , - “lastName”: , - “avatar”: , - “custom”: [ - “device”: , - “os”: , - ...], - “anonymous”: , - “updatedAt: , - ”config”: [: ] - ] -] - */ -final class DeprecatedCacheModelV2: DeprecatedCache { - let keyedValueCache: KeyedValueCaching - let cachedDataKey = CacheConverter.CacheKeys.ldUserModelDictionary - - init(keyedValueCache: KeyedValueCaching) { - self.keyedValueCache = keyedValueCache - } - - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) { - guard let cachedUserDictionaries = keyedValueCache.dictionary(forKey: cachedDataKey), !cachedUserDictionaries.isEmpty, - let cachedUserDictionary = cachedUserDictionaries[userKey] as? [String: Any], !cachedUserDictionary.isEmpty, - let featureFlagDictionaries = cachedUserDictionary[LDUser.CodingKeys.config.rawValue] as? [LDFlagKey: Any] - else { - return (nil, nil) - } - let featureFlags = Dictionary(uniqueKeysWithValues: featureFlagDictionaries.compactMap { flagKey, value in - (flagKey, FeatureFlag(flagKey: flagKey, value: value)) - }) - return (featureFlags, cachedUserDictionary.lastUpdated) - } - - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan expirationDate: Date) -> [UserKey] { - cachedUserData.compactMap { userKey, userDictionary in - let lastUpdated = userDictionary.lastUpdated ?? Date.distantFuture - return lastUpdated.isExpired(expirationDate: expirationDate) ? userKey : nil - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift deleted file mode 100644 index 6afb78f6..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// DeprecatedCacheModelV3.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation - -// Cache model in use from 2.11.0 up to 2.13.0 -/* Cache model v3 schema -[: [ - “key: , //LDUserModel dictionary - “ip”: , - “country”: , - “email”: , - “name”: , - “firstName”: , - “lastName”: , - “avatar”: , - “custom”: [ - “device”: , - “os”: , - ...], - “anonymous”: , - “updatedAt: , - ”config”: [ //LDFlagConfigModel dictionary - : [ //LDFlagConfigValue dictionary - “value”: , - “version”: - ] - ], - “privateAttrs”: (from 2.10.0 forward) - ] -] - */ -final class DeprecatedCacheModelV3: DeprecatedCache { - let keyedValueCache: KeyedValueCaching - let cachedDataKey = CacheConverter.CacheKeys.ldUserModelDictionary - - init(keyedValueCache: KeyedValueCaching) { - self.keyedValueCache = keyedValueCache - } - - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) { - guard let cachedUserDictionaries = keyedValueCache.dictionary(forKey: cachedDataKey), !cachedUserDictionaries.isEmpty, - let cachedUserDictionary = cachedUserDictionaries[userKey] as? [String: Any], !cachedUserDictionary.isEmpty, - let featureFlagDictionaries = cachedUserDictionary[LDUser.CodingKeys.config.rawValue] as? [LDFlagKey: [String: Any]] - else { - return (nil, nil) - } - let featureFlags = Dictionary(uniqueKeysWithValues: featureFlagDictionaries.compactMap { flagKey, flagValueDictionary in - (flagKey, FeatureFlag(flagKey: flagKey, - value: flagValueDictionary.value, - version: flagValueDictionary.version)) - }) - return (featureFlags, cachedUserDictionary.lastUpdated) - } - - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan expirationDate: Date) -> [UserKey] { - cachedUserData.compactMap { userKey, userDictionary in - let lastUpdated = userDictionary.lastUpdated ?? Date.distantFuture - return lastUpdated.isExpired(expirationDate: expirationDate) ? userKey : nil - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift deleted file mode 100644 index a0f18d98..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// DeprecatedCacheModelV4.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation - -// Cache model in use from 2.13.0 up to 2.14.0 -/* Cache model v4 schema -[: [ - “key: , //LDUserModel dictionary - “ip”: , - “country”: , - “email”: , - “name”: , - “firstName”: , - “lastName”: , - “avatar”: , - “custom”: [ - “device”: , - “os”: , - ...], - “anonymous”: , - “updatedAt: , - ”config”: [ //LDFlagConfigModel dictionary - : [ //LDFlagConfigValue dictionary - “value”: , - “version”: , - “flagVersion”: , - “variation”: , - “trackEvents”: , - “debugEventsUntilDate”: - ] - ], - “privateAttrs”: - ] -] - */ -final class DeprecatedCacheModelV4: DeprecatedCache { - let keyedValueCache: KeyedValueCaching - let cachedDataKey = CacheConverter.CacheKeys.ldUserModelDictionary - - init(keyedValueCache: KeyedValueCaching) { - self.keyedValueCache = keyedValueCache - } - - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) { - guard let cachedUserDictionaries = keyedValueCache.dictionary(forKey: cachedDataKey), !cachedUserDictionaries.isEmpty, - let cachedUserDictionary = cachedUserDictionaries[userKey] as? [String: Any], !cachedUserDictionary.isEmpty, - let featureFlagDictionaries = cachedUserDictionary[LDUser.CodingKeys.config.rawValue] as? [LDFlagKey: [String: Any]] - else { - return (nil, nil) - } - let featureFlags = Dictionary(uniqueKeysWithValues: featureFlagDictionaries.compactMap { flagKey, flagValueDictionary in - (flagKey, FeatureFlag(flagKey: flagKey, - value: flagValueDictionary.value, - variation: flagValueDictionary.variation, - version: flagValueDictionary.version, - flagVersion: flagValueDictionary.flagVersion, - trackEvents: flagValueDictionary.trackEvents, - debugEventsUntilDate: Date(millisSince1970: flagValueDictionary.debugEventsUntilDate))) - }) - return (featureFlags, cachedUserDictionary.lastUpdated) - } - - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan expirationDate: Date) -> [UserKey] { - cachedUserData.compactMap { userKey, userDictionary in - let lastUpdated = userDictionary.lastUpdated ?? Date.distantFuture - return lastUpdated.isExpired(expirationDate: expirationDate) ? userKey : nil - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index 30e749fc..b5d82812 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -48,9 +48,6 @@ final class ClientServiceFactory: ClientServiceCreating { func makeDeprecatedCacheModel(_ model: DeprecatedCacheModel) -> DeprecatedCache { switch model { - case .version2: return DeprecatedCacheModelV2(keyedValueCache: makeKeyedValueCache()) - case .version3: return DeprecatedCacheModelV3(keyedValueCache: makeKeyedValueCache()) - case .version4: return DeprecatedCacheModelV4(keyedValueCache: makeKeyedValueCache()) case .version5: return DeprecatedCacheModelV5(keyedValueCache: makeKeyedValueCache()) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift index 8fc6e45d..b6be7e76 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift @@ -69,7 +69,7 @@ final class CacheConverterSpec: QuickSpec { } private func convertCacheDataSpec() { - let cacheCases: [DeprecatedCacheModel?] = [.version5, .version4, .version3, .version2, nil] // Nil for no deprecated cache + let cacheCases: [DeprecatedCacheModel?] = [.version5, nil] // Nil for no deprecated cache var testContext: TestContext! describe("convertCacheData") { afterEach { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift index ae48c41d..6fdc1092 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift @@ -5,7 +5,6 @@ import Nimble protocol CacheModelTestInterface { var cacheKey: String { get } - var supportsMultiEnv: Bool { get } func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] @@ -96,13 +95,11 @@ class DeprecatedCacheModelSpec { } } } - if self.cacheModelInterface.supportsMultiEnv { - it("returns nil for uncached environment") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - cachedData = testContext.deprecatedCache.retrieveFlags(for: testContext.users.first!.key, and: UUID().uuidString) - expect(cachedData.featureFlags).to(beNil()) - expect(cachedData.lastUpdated).to(beNil()) - } + it("returns nil for uncached environment") { + testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) + cachedData = testContext.deprecatedCache.retrieveFlags(for: testContext.users.first!.key, and: UUID().uuidString) + expect(cachedData.featureFlags).to(beNil()) + expect(cachedData.lastUpdated).to(beNil()) } it("returns nil for uncached user") { testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV2Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV2Spec.swift deleted file mode 100644 index 3b705d8c..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV2Spec.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// DeprecatedCacheModelV2Spec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DeprecatedCacheModelV2Spec: QuickSpec, CacheModelTestInterface { - let cacheKey = CacheConverter.CacheKeys.ldUserModelDictionary - var supportsMultiEnv = false - - func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache { - DeprecatedCacheModelV2(keyedValueCache: keyedValueCache) - } - - func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? { - guard let mobileKey = mobileKeys.first, !users.isEmpty - else { return nil } - - return Dictionary(uniqueKeysWithValues: users.map { user in - let featureFlags = userEnvironmentsCollection[user.key]?.environmentFlags[mobileKey]?.featureFlags - let lastUpdated = userEnvironmentsCollection[user.key]?.lastUpdated - return (user.key, user.modelV2DictionaryValue(including: featureFlags!, using: lastUpdated)) - }) - } - - func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] { - originalFlags.filter { $0.value.value != nil }.compactMapValues { orig in - FeatureFlag(flagKey: orig.flagKey, value: orig.value) - } - } - - override func spec() { - DeprecatedCacheModelSpec(cacheModelInterface: self).spec() - } -} - -// MARK: Dictionary value to cache - -extension LDUser { - func modelV2DictionaryValue(including featureFlags: [LDFlagKey: FeatureFlag], using lastUpdated: Date?) -> [String: Any] { - var userDictionary = dictionaryValueWithAllAttributes() - userDictionary.removeValue(forKey: LDUser.CodingKeys.privateAttributes.rawValue) - userDictionary.setLastUpdated(lastUpdated) - userDictionary[LDUser.CodingKeys.config.rawValue] = featureFlags.allFlagValues.withNullValuesRemoved - - return userDictionary - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV3Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV3Spec.swift deleted file mode 100644 index 0d0c5fdf..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV3Spec.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// DeprecatedCacheModelV3Spec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DeprecatedCacheModelV3Spec: QuickSpec, CacheModelTestInterface { - - let cacheKey = CacheConverter.CacheKeys.ldUserModelDictionary - let supportsMultiEnv = false - - func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache { - DeprecatedCacheModelV3(keyedValueCache: keyedValueCache) - } - - func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? { - guard let mobileKey = mobileKeys.first, !users.isEmpty - else { return nil } - - return Dictionary(uniqueKeysWithValues: users.map { user in - let featureFlags = userEnvironmentsCollection[user.key]?.environmentFlags[mobileKey]?.featureFlags - let lastUpdated = userEnvironmentsCollection[user.key]?.lastUpdated - return (user.key, user.modelV3DictionaryValue(including: featureFlags!, using: lastUpdated)) - }) - } - - func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] { - originalFlags.filter { $0.value.value != nil }.compactMapValues { orig in - FeatureFlag(flagKey: orig.flagKey, value: orig.value, version: orig.version) - } - } - - override func spec() { - DeprecatedCacheModelSpec(cacheModelInterface: self).spec() - } -} - -// MARK: Dictionary value to cache - -extension LDUser { - func modelV3DictionaryValue(including featureFlags: [LDFlagKey: FeatureFlag], using lastUpdated: Date?) -> [String: Any] { - var userDictionary = dictionaryValueWithAllAttributes() - userDictionary.setLastUpdated(lastUpdated) - userDictionary[LDUser.CodingKeys.config.rawValue] = featureFlags.compactMapValues { $0.modelV3dictionaryValue } - - return userDictionary - } -} - -extension FeatureFlag { -/* - [“version”: , - “value”: ] -*/ - var modelV3dictionaryValue: [String: Any]? { - guard value != nil - else { return nil } - var flagDictionary = dictionaryValue - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.flagKey.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.variation.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.flagVersion.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.trackEvents.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.debugEventsUntilDate.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.reason.rawValue) - return flagDictionary - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV4Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV4Spec.swift deleted file mode 100644 index d3461b69..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV4Spec.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// DeprecatedCacheModelV4Spec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DeprecatedCacheModelV4Spec: QuickSpec, CacheModelTestInterface { - - let cacheKey = CacheConverter.CacheKeys.ldUserModelDictionary - let supportsMultiEnv = false - - func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache { - DeprecatedCacheModelV4(keyedValueCache: keyedValueCache) - } - - func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? { - guard let mobileKey = mobileKeys.first, !users.isEmpty - else { return nil } - - return Dictionary(uniqueKeysWithValues: users.map { user in - let featureFlags = userEnvironmentsCollection[user.key]?.environmentFlags[mobileKey]?.featureFlags - let lastUpdated = userEnvironmentsCollection[user.key]?.lastUpdated - return (user.key, user.modelV4DictionaryValue(including: featureFlags!, using: lastUpdated)) - }) - } - - func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] { - originalFlags.filter { $0.value.value != nil }.compactMapValues { orig in - FeatureFlag(flagKey: orig.flagKey, - value: orig.value, - variation: orig.variation, - version: orig.version, - flagVersion: orig.flagVersion, - trackEvents: orig.trackEvents, - debugEventsUntilDate: orig.debugEventsUntilDate) - } - } - - override func spec() { - DeprecatedCacheModelSpec(cacheModelInterface: self).spec() - } -} - -// MARK: Dictionary value to cache - -extension LDUser { - func modelV4DictionaryValue(including featureFlags: [LDFlagKey: FeatureFlag], using lastUpdated: Date?) -> [String: Any] { - var userDictionary = dictionaryValueWithAllAttributes() - userDictionary.setLastUpdated(lastUpdated) - userDictionary[LDUser.CodingKeys.config.rawValue] = featureFlags.compactMapValues { $0.modelV4dictionaryValue } - - return userDictionary - } -} - -extension FeatureFlag { -/* - [“version”: , - “flagVersion”: , - “variation”: , - “value”: , - “trackEvents”: , - “debugEventsUntilDate”: ] -*/ - var modelV4dictionaryValue: [String: Any]? { - guard value != nil - else { return nil } - var flagDictionary = dictionaryValue - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.flagKey.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.reason.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.trackReason.rawValue) - return flagDictionary - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift index a274c01d..5424dc5d 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift @@ -12,7 +12,6 @@ import Nimble final class DeprecatedCacheModelV5Spec: QuickSpec, CacheModelTestInterface { let cacheKey = DeprecatedCacheModelV5.CacheKeys.userEnvironments - let supportsMultiEnv = true func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache { DeprecatedCacheModelV5(keyedValueCache: keyedValueCache) From 15949b671ee2262401b65e3c1a1c74c48f9354ae Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 6 Jan 2022 13:02:57 -0600 Subject: [PATCH 15/50] Remove LDFlagBaseTypeConvertible we never used. --- LaunchDarkly.xcodeproj/project.pbxproj | 10 -- .../FlagValue/LDFlagBaseTypeConvertible.swift | 110 ------------------ 2 files changed, 120 deletions(-) delete mode 100644 LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagBaseTypeConvertible.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 92b06d2f..4a95ce37 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -19,7 +19,6 @@ 831188462113ADCA00D77CB5 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; 831188472113ADCD00D77CB5 /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; 831188482113ADD100D77CB5 /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; - 831188492113ADD400D77CB5 /* LDFlagBaseTypeConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838421F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift */; }; 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 8311884B2113ADDA00D77CB5 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -69,7 +68,6 @@ 831EF34620655E730001C643 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; 831EF34720655E730001C643 /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; 831EF34820655E730001C643 /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; - 831EF34920655E730001C643 /* LDFlagBaseTypeConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838421F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift */; }; 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -158,7 +156,6 @@ 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */; }; 837EF3742059C237009D628A /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837EF3732059C237009D628A /* Log.swift */; }; 838838411F5EFADF0023D11B /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; - 838838431F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838421F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift */; }; 838838451F5EFBAF0023D11B /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; 83883DD5220B68A000EEAB95 /* ErrorObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */; }; 83883DD6220B68A000EEAB95 /* ErrorObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */; }; @@ -197,7 +194,6 @@ 83D9EC782062DEAB004D7FA6 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; 83D9EC792062DEAB004D7FA6 /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; 83D9EC7A2062DEAB004D7FA6 /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; - 83D9EC7B2062DEAB004D7FA6 /* LDFlagBaseTypeConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838421F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift */; }; 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 83D9EC7E2062DEAB004D7FA6 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -411,7 +407,6 @@ 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentReporterSpec.swift; sourceTree = ""; }; 837EF3732059C237009D628A /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 838838401F5EFADF0023D11B /* LDFlagValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDFlagValue.swift; sourceTree = ""; }; - 838838421F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDFlagBaseTypeConvertible.swift; sourceTree = ""; }; 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDFlagValueConvertible.swift; sourceTree = ""; }; 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorObserver.swift; sourceTree = ""; }; 83883DD9220B6A9A00EEAB95 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = ""; }; @@ -778,7 +773,6 @@ children = ( 838838401F5EFADF0023D11B /* LDFlagValue.swift */, 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */, - 838838421F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift */, ); path = FlagValue; sourceTree = ""; @@ -1230,7 +1224,6 @@ 8354AC642241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, C443A40823145FEE00145710 /* ConnectionInformationStore.swift in Sources */, 831188662113AE4A00D77CB5 /* AnyComparer.swift in Sources */, - 831188492113ADD400D77CB5 /* LDFlagBaseTypeConvertible.swift in Sources */, 8311885C2113AE2200D77CB5 /* HTTPHeaders.swift in Sources */, 831188562113AE0800D77CB5 /* FlagSynchronizer.swift in Sources */, 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */, @@ -1272,7 +1265,6 @@ 831EF34720655E730001C643 /* LDFlagValue.swift in Sources */, 830DB3B02239B54900D65D25 /* URLResponse.swift in Sources */, 831EF34820655E730001C643 /* LDFlagValueConvertible.swift in Sources */, - 831EF34920655E730001C643 /* LDFlagBaseTypeConvertible.swift in Sources */, B4C9D4352489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */, C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */, @@ -1357,7 +1349,6 @@ 838838451F5EFBAF0023D11B /* LDFlagValueConvertible.swift in Sources */, 832D689D224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 838838411F5EFADF0023D11B /* LDFlagValue.swift in Sources */, - 838838431F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift in Sources */, 835E1D411F63450A00184DB4 /* ObjcLDUser.swift in Sources */, 8354AC612241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */, @@ -1453,7 +1444,6 @@ 83D9EC7A2062DEAB004D7FA6 /* LDFlagValueConvertible.swift in Sources */, 83883DD6220B68A000EEAB95 /* ErrorObserver.swift in Sources */, B4C9D4342489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, - 83D9EC7B2062DEAB004D7FA6 /* LDFlagBaseTypeConvertible.swift in Sources */, 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */, 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagBaseTypeConvertible.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagBaseTypeConvertible.swift deleted file mode 100644 index ab3a6a85..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagBaseTypeConvertible.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// LDFlagBaseTypeConvertible.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - -import Foundation - -/// Protocol to convert LDFlagValue into it's Base Type. -protocol LDFlagBaseTypeConvertible { - /// Failable initializer. Client app developers should not use LDFlagBaseTypeConvertible. The SDK uses this protocol to limit feature flag types to those defined in `LDFlagValue`. - init?(_ flag: LDFlagValue?) -} - -// MARK: - LDFlagValue - -extension LDFlagValue { - var baseValue: LDFlagBaseTypeConvertible? { - switch self { - case let .bool(value): return value - case let .int(value): return value - case let .double(value): return value - case let .string(value): return value - case .array: return self.baseArray - case .dictionary: return self.baseDictionary - default: return nil - } - } -} - -// MARK: - Bool - -extension Bool: LDFlagBaseTypeConvertible { - init?(_ flag: LDFlagValue?) { - guard case let .bool(bool) = flag - else { return nil } - self = bool - } -} - -// MARK: - Int - -extension Int: LDFlagBaseTypeConvertible { - init?(_ flag: LDFlagValue?) { - guard case let .int(value) = flag - else { return nil } - self = value - } -} - -// MARK: - Double - -extension Double: LDFlagBaseTypeConvertible { - init?(_ flag: LDFlagValue?) { - guard case let .double(value) = flag - else { return nil } - self = value - } -} - -// MARK: - String - -extension String: LDFlagBaseTypeConvertible { - init?(_ flag: LDFlagValue?) { - guard case let .string(value) = flag - else { return nil } - self = value - } -} - -// MARK: - Array - -extension Array: LDFlagBaseTypeConvertible { - init?(_ flag: LDFlagValue?) { - guard let flagArray = flag?.baseArray as? [Element] - else { return nil } - self = flagArray - } -} - -extension LDFlagValue { - func toBaseTypeArray() -> [BaseType]? { - self.flagValueArray?.compactMap { BaseType($0) } - } - - var baseArray: [LDFlagBaseTypeConvertible]? { - self.flagValueArray?.compactMap { $0.baseValue } - } -} - -// MARK: - Dictionary - -extension LDFlagValue { - func toBaseTypeDictionary() -> [LDFlagKey: Value]? { - baseDictionary as? [LDFlagKey: Value] - } - - var baseDictionary: [String: LDFlagBaseTypeConvertible]? { - flagValueDictionary?.compactMapValues { $0.baseValue } - } -} - -extension Dictionary: LDFlagBaseTypeConvertible { - init?(_ flag: LDFlagValue?) { - guard let flagValue = flag?.baseDictionary as? [Key: Value] - else { return nil } - self = flagValue - } -} From 302a5d5d20ce985efbb0f23e57d7f6640a38e94e Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 6 Jan 2022 13:23:03 -0600 Subject: [PATCH 16/50] Break out variaiton methods from LDClient.swift. --- LaunchDarkly.xcodeproj/project.pbxproj | 10 + LaunchDarkly/LaunchDarkly/LDClient.swift | 186 ------------------ .../LaunchDarkly/LDClientVariation.swift | 179 +++++++++++++++++ 3 files changed, 189 insertions(+), 186 deletions(-) create mode 100644 LaunchDarkly/LaunchDarkly/LDClientVariation.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 4a95ce37..ba2a7170 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -252,6 +252,10 @@ B4903D9824BD61B200F087C4 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B4903D9724BD61B200F087C4 /* OHHTTPStubsSwift */; }; B4903D9B24BD61D000F087C4 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = B4903D9A24BD61D000F087C4 /* Nimble */; }; B4903D9E24BD61EF00F087C4 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = B4903D9D24BD61EF00F087C4 /* Quick */; }; + B495A8A22787762C0051977C /* LDClientVariation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B495A8A12787762C0051977C /* LDClientVariation.swift */; }; + B495A8A32787762C0051977C /* LDClientVariation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B495A8A12787762C0051977C /* LDClientVariation.swift */; }; + B495A8A42787762C0051977C /* LDClientVariation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B495A8A12787762C0051977C /* LDClientVariation.swift */; }; + B495A8A52787762C0051977C /* LDClientVariation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B495A8A12787762C0051977C /* LDClientVariation.swift */; }; B4C9D42E2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */; }; B4C9D42F2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */; }; B4C9D4302489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */; }; @@ -452,6 +456,7 @@ B43D5ACF25FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelSpec.swift; sourceTree = ""; }; B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = ""; }; B46F344025E6DB7D0078D45F /* DiagnosticReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticReporterSpec.swift; sourceTree = ""; }; + B495A8A12787762C0051977C /* LDClientVariation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDClientVariation.swift; sourceTree = ""; }; B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticEvent.swift; sourceTree = ""; }; B4C9D4322489C8FD004A9B03 /* DiagnosticCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCache.swift; sourceTree = ""; }; B4C9D4372489E20A004A9B03 /* DiagnosticReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticReporter.swift; sourceTree = ""; }; @@ -642,6 +647,7 @@ children = ( 83B6C4B51F4DE7630055351C /* LDCommon.swift */, 8354EFDC1F26380700C05156 /* LDClient.swift */, + B495A8A12787762C0051977C /* LDClientVariation.swift */, 8354EFE61F263E4200C05156 /* Models */, 83FEF8D91F2666BF001CF12C /* ServiceObjects */, 831D8B701F71D3A600ED65E8 /* Networking */, @@ -1247,6 +1253,7 @@ 831188472113ADCD00D77CB5 /* LDFlagValue.swift in Sources */, 831188442113ADC200D77CB5 /* LDConfig.swift in Sources */, 83906A7721190B1900D7D3C5 /* FlagRequestTracker.swift in Sources */, + B495A8A52787762C0051977C /* LDClientVariation.swift in Sources */, 831188622113AE3A00D77CB5 /* Data.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1302,6 +1309,7 @@ 83EBCBB520DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, 831EF36220655E730001C643 /* JSONSerialization.swift in Sources */, 8347BB0E21F147E100E56BCD /* LDTimer.swift in Sources */, + B495A8A42787762C0051977C /* LDClientVariation.swift in Sources */, 831EF36320655E730001C643 /* Date.swift in Sources */, 831EF36420655E730001C643 /* AnyComparer.swift in Sources */, 831EF36520655E730001C643 /* Thread.swift in Sources */, @@ -1372,6 +1380,7 @@ 83B8C2471FE4071F0082B8A9 /* HTTPURLResponse.swift in Sources */, 830DB3AE2239B54900D65D25 /* URLResponse.swift in Sources */, 83DDBEF61FA24A7E00E428B6 /* Data.swift in Sources */, + B495A8A22787762C0051977C /* LDClientVariation.swift in Sources */, 838F96781FBA504A009CFC45 /* ClientServiceFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1487,6 +1496,7 @@ 83D9EC992062DEAB004D7FA6 /* ObjcLDConfig.swift in Sources */, 830DB3AF2239B54900D65D25 /* URLResponse.swift in Sources */, 83D9EC9A2062DEAB004D7FA6 /* ObjcLDUser.swift in Sources */, + B495A8A32787762C0051977C /* LDClientVariation.swift in Sources */, 83D9EC9C2062DEAB004D7FA6 /* ObjcLDChangedFlag.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index e26ce92d..4ae67d71 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -1,18 +1,9 @@ -// -// LDClient.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation enum LDClientRunMode { case foreground, background } -// swiftlint:disable type_body_length - /** The LDClient is the heart of the SDK, providing client apps running iOS, watchOS, macOS, or tvOS access to LaunchDarkly services. This singleton provides the ability to set a configuration (LDConfig) that controls how the LDClient talks to LaunchDarkly servers, and a user (LDUser) that provides finer control on the feature flag values delivered to LDClient. Once the LDClient has started, it connects to LaunchDarkly's servers to get the feature flag values you set in the Dashboard. ## Usage @@ -332,173 +323,6 @@ public class LDClient { private let internalIdentifyQueue: DispatchQueue = DispatchQueue(label: "InternalIdentifyQueue") - // MARK: Retrieving Flag Values - - /** - Returns the variation for the given feature flag. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns the default value. Use this method when the default value is a non-Optional type. See `variation` with the Optional return value when the default value can be nil. - - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *true* and *false*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. - - The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. - - When online, the LDClient has two modes for maintaining feature flag values: *streaming* and *polling*. The client app requests the mode by setting the `config.streamingMode`, see `LDConfig` for details. - - In streaming mode, the LDClient opens a long-running connection to LaunchDarkly's streaming server (called *clientstream*). When a flag value changes on the server, the clientstream notifies the SDK to update the value. Streaming mode is not available on watchOS. On iOS and tvOS, the client app must be running in the foreground to connect to clientstream. On macOS the client app may run in either foreground or background to connect to clientstream. If streaming mode is not available, the SDK reverts to polling mode. - - In polling mode, the LDClient requests feature flags from LaunchDarkly's app server at regular intervals defined in the LDConfig. When a flag value changes on the server, the LDClient will learn of the change the next time the SDK requests feature flags. - - When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. - - A call to `variation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. - - ### Usage - ```` - let boolFeatureFlagValue = LDClient.get()!.variation(forKey: "bool-flag-key", defaultValue: false) //boolFeatureFlagValue is a Bool - ```` - **Important** The default value tells the SDK the type of the feature flag. In several cases, the feature flag type cannot be determined by the values sent from the server. It is possible to provide a default value with a type that does not match the feature flag value's type. The SDK will attempt to convert the feature flag's value into the type of the default value in the variation request. If that cast fails, the SDK will not be able to determine the correct return type, and will always return the default value. - - Pay close attention to the type of the default value for collections. If the default value collection type is more restrictive than the feature flag, the sdk will return the default value even though the feature flag is present because it cannot convert the feature flag into the type requested via the default value. For example, if the feature flag has the type `[String: Any]`, but the default value has the type `[String: Int]`, the sdk will not be able to convert the flags into the requested type, and will return the default value. - - To avoid this, make sure the default value type matches the expected feature flag type. Either specify the default value type to be the feature flag type, or cast the default value to the feature flag type prior to making the variation request. In the above example, either specify that the default value's type is [String: Any]: - ```` - let defaultValue: [String: Any] = ["a": 1, "b": 2] //dictionary type would be [String: Int] without the type specifier - ```` - or cast the default value into the feature flag type prior to calling variation: - ```` - let dictionaryFlagValue = LDClient.get()!.variation(forKey: "dictionary-key", defaultValue: ["a": 1, "b": 2] as [String: Any]) - ```` - - - parameter forKey: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. - - - returns: The requested feature flag value, or the default value if the flag is missing or cannot be cast to the default value type, or the client is not started - */ - /// - Tag: variationWithdefaultValue - public func variation(forKey flagKey: LDFlagKey, defaultValue: T) -> T { - // the defaultValue cast to 'as T?' directs the call to the Optional-returning variation method - variation(forKey: flagKey, defaultValue: defaultValue as T?) ?? defaultValue - } - - /** - Returns the LDEvaluationDetail for the given feature flag. LDEvaluationDetail gives you more insight into why your variation contains the specified value. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns an LDEvaluationDetail with the default value. Use this method when the default value is a non-Optional type. See `variationDetail` with the Optional return value when the default value can be nil. See [variationWithdefaultValue](x-source-tag://variationWithdefaultValue) - - - parameter forKey: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value value to return if the feature flag key does not exist. - - - returns: LDEvaluationDetail which wraps the requested feature flag value, or the default value, which variation was served, and the evaluation reason. - */ - public func variationDetail(forKey flagKey: LDFlagKey, defaultValue: T) -> LDEvaluationDetail { - let featureFlag = flagStore.featureFlag(for: flagKey) - let reason = checkErrorKinds(featureFlag: featureFlag) ?? featureFlag?.reason - let value = variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: true) - return LDEvaluationDetail(value: value ?? defaultValue, variationIndex: featureFlag?.variation, reason: reason) - } - - private func checkErrorKinds(featureFlag: FeatureFlag?) -> [String: Any]? { - if !hasStarted { - return ["kind": "ERROR", "errorKind": "CLIENT_NOT_READY"] - } else if featureFlag == nil { - return ["kind": "ERROR", "errorKind": "FLAG_NOT_FOUND"] - } else { - return nil - } - } - - /** - Returns the variation for the given feature flag. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns the default value, which may be `nil`. Use this method when the default value is an Optional type. See `variation` with the non-Optional return value when the default value cannot be nil. - - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *true* and *false*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. - - The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. - - When online, the LDClient has two modes for maintaining feature flag values: *streaming* and *polling*. The client app requests the mode by setting the `config.streamingMode`, see `LDConfig` for details. - - In streaming mode, the LDClient opens a long-running connection to LaunchDarkly's streaming server (called *clientstream*). When a flag value changes on the server, the clientstream notifies the SDK to update the value. Streaming mode is not available on watchOS. On iOS and tvOS, the client app must be running in the foreground to connect to clientstream. On macOS the client app may run in either foreground or background to connect to clientstream. If streaming mode is not available, the SDK reverts to polling mode. - - In polling mode, the LDClient requests feature flags from LaunchDarkly's app server at regular intervals defined in the LDConfig. When a flag value changes on the server, the LDClient will learn of the change the next time the SDK requests feature flags. - - When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. - - A call to `variation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. - - ### Usage - ```` - let boolFeatureFlagValue: Bool? = LDClient.get()!.variation(forKey: "bool-flag-key", defaultValue: nil) //boolFeatureFlagValue is a Bool? - ```` - **Important** The default value tells the SDK the type of the feature flag. In several cases, the feature flag type cannot be determined by the values sent from the server. It is possible to provide a default value with a type that does not match the feature flag value's type. The SDK will attempt to convert the feature flag's value into the type of the default value in the variation request. If that cast fails, the SDK will not be able to determine the correct return type, and will always return the default value. - - When specifying `nil` as the default value, the compiler must also know the type of the optional. Without this information, the compiler will give the error "'nil' requires a contextual type". There are several ways to provide this information, by setting the type on the item holding the return value, by casting the return value to the desired type, or by casting `nil` to the desired type. We recommend following the above example and setting the type on the return value item. - - For this method, the default value is defaulted to `nil`, allowing the call site to omit the default value. - - Pay close attention to the type of the default value for collections. If the default value collection type is more restrictive than the feature flag, the sdk will return the default value even though the feature flag is present because it cannot convert the feature flag into the type requested via the default value. For example, if the feature flag has the type `[String: Any]`, but the default value has the type `[String: Int]`, the sdk will not be able to convert the flags into the requested type, and will return the default value. - - To avoid this, make sure the default value type matches the expected feature flag type. Either specify the default value value type to be the feature flag type, or cast the default value value to the feature flag type prior to making the variation request. In the above example, either specify that the default value's type is [String: Any]: - ```` - let defaultValue: [String: Any]? = ["a": 1, "b": 2] //dictionary type would be [String: Int] without the type specifier - ```` - or cast the default value into the feature flag type prior to calling variation: - ```` - let dictionaryFlagValue = LDClient.get()!.variation(forKey: "dictionary-key", defaultValue: ["a": 1, "b": 2] as [String: Any]?) - ```` - - - parameter forKey: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. If omitted, the default value is `nil`. (Optional) - - - returns: The requested feature flag value, or the default value if the flag is missing or cannot be cast to the default value type, or the client is not started - */ - /// - Tag: variationWithoutdefaultValue - public func variation(forKey flagKey: LDFlagKey, defaultValue: T? = nil) -> T? { - variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: false) - } - - /** - Returns the LDEvaluationDetail for the given feature flag. LDEvaluationDetail gives you more insight into why your variation contains the specified value. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns an LDEvaluationDetail with the default value, which may be `nil`. Use this method when the default value is a Optional type. See [variationWithoutdefaultValue](x-source-tag://variationWithoutdefaultValue) - - - parameter forKey: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. If omitted, the default value is `nil`. (Optional) - - - returns: LDEvaluationDetail which wraps the requested feature flag value, or the default value, which variation was served, and the evaluation reason. - */ - public func variationDetail(forKey flagKey: LDFlagKey, defaultValue: T? = nil) -> LDEvaluationDetail { - let featureFlag = flagStore.featureFlag(for: flagKey) - let reason = checkErrorKinds(featureFlag: featureFlag) ?? featureFlag?.reason - let value = variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: true) - return LDEvaluationDetail(value: value, variationIndex: featureFlag?.variation, reason: reason) - } - - internal func variationInternal(forKey flagKey: LDFlagKey, defaultValue: T? = nil, includeReason: Bool? = false) -> T? { - guard hasStarted - else { - Log.debug(typeName(and: #function) + "returning defaultValue: \(defaultValue.stringValue)." + " LDClient not started.") - return defaultValue - } - let featureFlag = flagStore.featureFlag(for: flagKey) - let value = (featureFlag?.value as? T) ?? defaultValue - let failedConversionMessage = self.failedConversionMessage(featureFlag: featureFlag, defaultValue: defaultValue) - Log.debug(typeName(and: #function) + "flagKey: \(flagKey), value: \(value.stringValue), defaultValue: \(defaultValue.stringValue), featureFlag: \(featureFlag.stringValue), reason: \(featureFlag?.reason?.description ?? "No evaluation reason")." - + "\(failedConversionMessage)") - eventReporter.recordFlagEvaluationEvents(flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason ?? false) - return value - } - - private func failedConversionMessage(featureFlag: FeatureFlag?, defaultValue: T?) -> String { - if featureFlag == nil { - return " Feature flag not found." - } - if featureFlag?.value is T { - return "" - } - return " LDClient was unable to convert the feature flag to the requested type (\(T.self))." - + (isCollection(defaultValue) ? " The defaultValue type is a collection. Make sure the element of the defaultValue's type is not too restrictive for the actual feature flag type." : "") - } - - private func isCollection(_ object: T) -> Bool { - let collectionsTypes = ["Set", "Array", "Dictionary"] - let typeString = String(describing: type(of: object)) - - for type in collectionsTypes { - if typeString.contains(type) { return true } - } - return false - } - /** Returns a dictionary with the flag keys and their values. If the LDClient is not started, returns nil. @@ -990,16 +814,6 @@ public class LDClient { extension LDClient: TypeIdentifying { } -private extension Optional { - var stringValue: String { - guard let value = self - else { - return "" - } - return "\(value)" - } -} - #if DEBUG extension LDClient { func setRunMode(_ runMode: LDClientRunMode) { diff --git a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift new file mode 100644 index 00000000..d896f2ee --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -0,0 +1,179 @@ +import Foundation + +extension LDClient { + // MARK: Retrieving Flag Values + /** + Returns the variation for the given feature flag. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns the default value. Use this method when the default value is a non-Optional type. See `variation` with the Optional return value when the default value can be nil. + + A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *true* and *false*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. + + The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. + + When online, the LDClient has two modes for maintaining feature flag values: *streaming* and *polling*. The client app requests the mode by setting the `config.streamingMode`, see `LDConfig` for details. + - In streaming mode, the LDClient opens a long-running connection to LaunchDarkly's streaming server (called *clientstream*). When a flag value changes on the server, the clientstream notifies the SDK to update the value. Streaming mode is not available on watchOS. On iOS and tvOS, the client app must be running in the foreground to connect to clientstream. On macOS the client app may run in either foreground or background to connect to clientstream. If streaming mode is not available, the SDK reverts to polling mode. + - In polling mode, the LDClient requests feature flags from LaunchDarkly's app server at regular intervals defined in the LDConfig. When a flag value changes on the server, the LDClient will learn of the change the next time the SDK requests feature flags. + + When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. + + A call to `variation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. + + ### Usage + ```` + let boolFeatureFlagValue = LDClient.get()!.variation(forKey: "bool-flag-key", defaultValue: false) //boolFeatureFlagValue is a Bool + ```` + **Important** The default value tells the SDK the type of the feature flag. In several cases, the feature flag type cannot be determined by the values sent from the server. It is possible to provide a default value with a type that does not match the feature flag value's type. The SDK will attempt to convert the feature flag's value into the type of the default value in the variation request. If that cast fails, the SDK will not be able to determine the correct return type, and will always return the default value. + + Pay close attention to the type of the default value for collections. If the default value collection type is more restrictive than the feature flag, the sdk will return the default value even though the feature flag is present because it cannot convert the feature flag into the type requested via the default value. For example, if the feature flag has the type `[String: Any]`, but the default value has the type `[String: Int]`, the sdk will not be able to convert the flags into the requested type, and will return the default value. + + To avoid this, make sure the default value type matches the expected feature flag type. Either specify the default value type to be the feature flag type, or cast the default value to the feature flag type prior to making the variation request. In the above example, either specify that the default value's type is [String: Any]: + ```` + let defaultValue: [String: Any] = ["a": 1, "b": 2] //dictionary type would be [String: Int] without the type specifier + ```` + or cast the default value into the feature flag type prior to calling variation: + ```` + let dictionaryFlagValue = LDClient.get()!.variation(forKey: "dictionary-key", defaultValue: ["a": 1, "b": 2] as [String: Any]) + ```` + + - parameter forKey: The LDFlagKey for the requested feature flag. + - parameter defaultValue: The default value to return if the feature flag key does not exist. + + - returns: The requested feature flag value, or the default value if the flag is missing or cannot be cast to the default value type, or the client is not started + */ + /// - Tag: variationWithdefaultValue + public func variation(forKey flagKey: LDFlagKey, defaultValue: T) -> T { + // the defaultValue cast to 'as T?' directs the call to the Optional-returning variation method + variation(forKey: flagKey, defaultValue: defaultValue as T?) ?? defaultValue + } + + /** + Returns the LDEvaluationDetail for the given feature flag. LDEvaluationDetail gives you more insight into why your variation contains the specified value. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns an LDEvaluationDetail with the default value. Use this method when the default value is a non-Optional type. See `variationDetail` with the Optional return value when the default value can be nil. See [variationWithdefaultValue](x-source-tag://variationWithdefaultValue) + + - parameter forKey: The LDFlagKey for the requested feature flag. + - parameter defaultValue: The default value value to return if the feature flag key does not exist. + + - returns: LDEvaluationDetail which wraps the requested feature flag value, or the default value, which variation was served, and the evaluation reason. + */ + public func variationDetail(forKey flagKey: LDFlagKey, defaultValue: T) -> LDEvaluationDetail { + let featureFlag = flagStore.featureFlag(for: flagKey) + let reason = checkErrorKinds(featureFlag: featureFlag) ?? featureFlag?.reason + let value = variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: true) + return LDEvaluationDetail(value: value ?? defaultValue, variationIndex: featureFlag?.variation, reason: reason) + } + + private func checkErrorKinds(featureFlag: FeatureFlag?) -> [String: Any]? { + if !hasStarted { + return ["kind": "ERROR", "errorKind": "CLIENT_NOT_READY"] + } else if featureFlag == nil { + return ["kind": "ERROR", "errorKind": "FLAG_NOT_FOUND"] + } else { + return nil + } + } + + /** + Returns the variation for the given feature flag. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns the default value, which may be `nil`. Use this method when the default value is an Optional type. See `variation` with the non-Optional return value when the default value cannot be nil. + + A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *true* and *false*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. + + The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. + + When online, the LDClient has two modes for maintaining feature flag values: *streaming* and *polling*. The client app requests the mode by setting the `config.streamingMode`, see `LDConfig` for details. + - In streaming mode, the LDClient opens a long-running connection to LaunchDarkly's streaming server (called *clientstream*). When a flag value changes on the server, the clientstream notifies the SDK to update the value. Streaming mode is not available on watchOS. On iOS and tvOS, the client app must be running in the foreground to connect to clientstream. On macOS the client app may run in either foreground or background to connect to clientstream. If streaming mode is not available, the SDK reverts to polling mode. + - In polling mode, the LDClient requests feature flags from LaunchDarkly's app server at regular intervals defined in the LDConfig. When a flag value changes on the server, the LDClient will learn of the change the next time the SDK requests feature flags. + + When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. + + A call to `variation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. + + ### Usage + ```` + let boolFeatureFlagValue: Bool? = LDClient.get()!.variation(forKey: "bool-flag-key", defaultValue: nil) //boolFeatureFlagValue is a Bool? + ```` + **Important** The default value tells the SDK the type of the feature flag. In several cases, the feature flag type cannot be determined by the values sent from the server. It is possible to provide a default value with a type that does not match the feature flag value's type. The SDK will attempt to convert the feature flag's value into the type of the default value in the variation request. If that cast fails, the SDK will not be able to determine the correct return type, and will always return the default value. + + When specifying `nil` as the default value, the compiler must also know the type of the optional. Without this information, the compiler will give the error "'nil' requires a contextual type". There are several ways to provide this information, by setting the type on the item holding the return value, by casting the return value to the desired type, or by casting `nil` to the desired type. We recommend following the above example and setting the type on the return value item. + + For this method, the default value is defaulted to `nil`, allowing the call site to omit the default value. + + Pay close attention to the type of the default value for collections. If the default value collection type is more restrictive than the feature flag, the sdk will return the default value even though the feature flag is present because it cannot convert the feature flag into the type requested via the default value. For example, if the feature flag has the type `[String: Any]`, but the default value has the type `[String: Int]`, the sdk will not be able to convert the flags into the requested type, and will return the default value. + + To avoid this, make sure the default value type matches the expected feature flag type. Either specify the default value value type to be the feature flag type, or cast the default value value to the feature flag type prior to making the variation request. In the above example, either specify that the default value's type is [String: Any]: + ```` + let defaultValue: [String: Any]? = ["a": 1, "b": 2] //dictionary type would be [String: Int] without the type specifier + ```` + or cast the default value into the feature flag type prior to calling variation: + ```` + let dictionaryFlagValue = LDClient.get()!.variation(forKey: "dictionary-key", defaultValue: ["a": 1, "b": 2] as [String: Any]?) + ```` + + - parameter forKey: The LDFlagKey for the requested feature flag. + - parameter defaultValue: The default value to return if the feature flag key does not exist. If omitted, the default value is `nil`. (Optional) + + - returns: The requested feature flag value, or the default value if the flag is missing or cannot be cast to the default value type, or the client is not started + */ + /// - Tag: variationWithoutdefaultValue + public func variation(forKey flagKey: LDFlagKey, defaultValue: T? = nil) -> T? { + variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: false) + } + + /** + Returns the LDEvaluationDetail for the given feature flag. LDEvaluationDetail gives you more insight into why your variation contains the specified value. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns an LDEvaluationDetail with the default value, which may be `nil`. Use this method when the default value is a Optional type. See [variationWithoutdefaultValue](x-source-tag://variationWithoutdefaultValue) + + - parameter forKey: The LDFlagKey for the requested feature flag. + - parameter defaultValue: The default value to return if the feature flag key does not exist. If omitted, the default value is `nil`. (Optional) + + - returns: LDEvaluationDetail which wraps the requested feature flag value, or the default value, which variation was served, and the evaluation reason. + */ + public func variationDetail(forKey flagKey: LDFlagKey, defaultValue: T? = nil) -> LDEvaluationDetail { + let featureFlag = flagStore.featureFlag(for: flagKey) + let reason = checkErrorKinds(featureFlag: featureFlag) ?? featureFlag?.reason + let value = variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: true) + return LDEvaluationDetail(value: value, variationIndex: featureFlag?.variation, reason: reason) + } + + private func variationInternal(forKey flagKey: LDFlagKey, defaultValue: T? = nil, includeReason: Bool? = false) -> T? { + guard hasStarted + else { + Log.debug(typeName(and: #function) + "returning defaultValue: \(defaultValue.stringValue)." + " LDClient not started.") + return defaultValue + } + let featureFlag = flagStore.featureFlag(for: flagKey) + let value = (featureFlag?.value as? T) ?? defaultValue + let failedConversionMessage = self.failedConversionMessage(featureFlag: featureFlag, defaultValue: defaultValue) + Log.debug(typeName(and: #function) + "flagKey: \(flagKey), value: \(value.stringValue), defaultValue: \(defaultValue.stringValue), featureFlag: \(featureFlag.stringValue), reason: \(featureFlag?.reason?.description ?? "No evaluation reason")." + + "\(failedConversionMessage)") + eventReporter.recordFlagEvaluationEvents(flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason ?? false) + return value + } + + private func failedConversionMessage(featureFlag: FeatureFlag?, defaultValue: T?) -> String { + if featureFlag == nil { + return " Feature flag not found." + } + if featureFlag?.value is T { + return "" + } + return " LDClient was unable to convert the feature flag to the requested type (\(T.self))." + + (isCollection(defaultValue) ? " The defaultValue type is a collection. Make sure the element of the defaultValue's type is not too restrictive for the actual feature flag type." : "") + } + + private func isCollection(_ object: T) -> Bool { + let collectionsTypes = ["Set", "Array", "Dictionary"] + let typeString = String(describing: type(of: object)) + + for type in collectionsTypes { + if typeString.contains(type) { return true } + } + return false + } +} + +private extension Optional { + var stringValue: String { + guard let value = self + else { + return "" + } + return "\(value)" + } +} From a97c363b4e9c11f966d11064997121ddfa906092 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 6 Jan 2022 14:25:05 -0600 Subject: [PATCH 17/50] Remove FlagStore from within LDUser. --- LaunchDarkly/LaunchDarkly/LDClient.swift | 6 +----- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 7 ------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 4ae67d71..b4b6ba3c 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -298,8 +298,7 @@ public class LDClient { if let cachedFlags = self.flagCache.retrieveFeatureFlags(forUserWithKey: self.user.key, andMobileKey: self.config.mobileKey), !cachedFlags.isEmpty { flagStore.replaceStore(newFlags: cachedFlags, completion: nil) } else { - // Deprecated behavior of setting flags from user init dictionary - flagStore.replaceStore(newFlags: user.flagStore?.featureFlags ?? [:], completion: nil) + flagStore.replaceStore(newFlags: [:], completion: nil) } self.service.user = self.user self.service.clearFlagResponseCache() @@ -759,9 +758,6 @@ public class LDClient { environmentReporter = self.serviceFactory.makeEnvironmentReporter() flagCache = self.serviceFactory.makeFeatureFlagCache(maxCachedUsers: configuration.maxCachedUsers) flagStore = self.serviceFactory.makeFlagStore() - if let userFlagStore = startUser?.flagStore { - flagStore.replaceStore(newFlags: userFlagStore.featureFlags, completion: nil) - } LDUserWrapper.configureKeyedArchiversToHandleVersion2_3_0AndOlderUserCacheFormat() cacheConverter = self.serviceFactory.makeCacheConverter(maxCachedUsers: configuration.maxCachedUsers) flagChangeNotifier = self.serviceFactory.makeFlagChangeNotifier() diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index c501fe51..4b5b9ddc 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -76,8 +76,6 @@ public struct LDUser { /// An NSObject wrapper for the Swift LDUser struct. Intended for use in mixed apps when Swift code needs to pass a user into an Objective-C method. public var objcLdUser: ObjcLDUser { ObjcLDUser(self) } - internal var flagStore: FlagMaintaining? - /** Initializer to create a LDUser. Client configurable attributes each have an optional parameter to facilitate setting user information into the LDUser. The SDK will automatically set `key`, `device`, `operatingSystem`, and `isAnonymous` attributes if the client does not provide them. The SDK embeds `device` and `operatingSystem` into the `custom` dictionary for transmission to LaunchDarkly. - parameter key: String that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. @@ -150,7 +148,6 @@ public struct LDUser { device = custom?[CodingKeys.device.rawValue] as? String operatingSystem = custom?[CodingKeys.operatingSystem.rawValue] as? String - flagStore = FlagStore(featureFlagDictionary: userDictionary[CodingKeys.config.rawValue] as? [String: Any]) Log.debug(typeName(and: #function) + "user: \(self)") } @@ -177,7 +174,6 @@ public struct LDUser { case CodingKeys.custom.rawValue: return custom case CodingKeys.device.rawValue: return device case CodingKeys.operatingSystem.rawValue: return operatingSystem - case CodingKeys.config.rawValue: return flagStore?.featureFlags case CodingKeys.privateAttributes.rawValue: return privateAttributes default: return nil } @@ -282,7 +278,6 @@ extension LDUserWrapper: NSCoding { encoder.encode(wrapped.device, forKey: LDUser.CodingKeys.device.rawValue) encoder.encode(wrapped.operatingSystem, forKey: LDUser.CodingKeys.operatingSystem.rawValue) encoder.encode(wrapped.privateAttributes, forKey: LDUser.CodingKeys.privateAttributes.rawValue) - encoder.encode([Keys.featureFlags: wrapped.flagStore?.featureFlags.dictionaryValue.withNullValuesRemoved], forKey: LDUser.CodingKeys.config.rawValue) } convenience init?(coder decoder: NSCoder) { @@ -301,8 +296,6 @@ extension LDUserWrapper: NSCoding { ) user.device = decoder.decodeObject(forKey: LDUser.CodingKeys.device.rawValue) as? String user.operatingSystem = decoder.decodeObject(forKey: LDUser.CodingKeys.operatingSystem.rawValue) as? String - let wrappedFlags = decoder.decodeObject(forKey: LDUser.CodingKeys.config.rawValue) as? [String: Any] - user.flagStore = FlagStore(featureFlagDictionary: wrappedFlags?[Keys.featureFlags] as? [String: Any]) self.init(user: user) } From 15d68faaa60eb9178c9dd1640aafc98f1e98858d Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Sun, 9 Jan 2022 11:55:36 -0600 Subject: [PATCH 18/50] Simplify cache load in identify. --- LaunchDarkly/LaunchDarkly/LDClient.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index b4b6ba3c..66537ced 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -295,11 +295,8 @@ public class LDClient { self.internalSetOnline(false) cacheConverter.convertCacheData(for: user, and: config) - if let cachedFlags = self.flagCache.retrieveFeatureFlags(forUserWithKey: self.user.key, andMobileKey: self.config.mobileKey), !cachedFlags.isEmpty { - flagStore.replaceStore(newFlags: cachedFlags, completion: nil) - } else { - flagStore.replaceStore(newFlags: [:], completion: nil) - } + let cachedUserFlags = self.flagCache.retrieveFeatureFlags(forUserWithKey: self.user.key, andMobileKey: self.config.mobileKey) ?? [:] + flagStore.replaceStore(newFlags: cachedUserFlags, completion: nil) self.service.user = self.user self.service.clearFlagResponseCache() flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), From 000ca611c8a146dcc802594530c18d042e7fb1e7 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Wed, 12 Jan 2022 15:26:58 -0600 Subject: [PATCH 19/50] Improvements to LDTimer and Throttler cleanup. (#170) --- .../ServiceObjects/DiagnosticReporter.swift | 2 +- .../ServiceObjects/EventReporter.swift | 2 +- .../ServiceObjects/FlagSynchronizer.swift | 2 +- .../LaunchDarkly/ServiceObjects/LDTimer.swift | 22 +-- .../ServiceObjects/Throttler.swift | 58 +++---- .../ServiceObjects/LDTimerSpec.swift | 152 ++++-------------- .../ServiceObjects/ThrottlerSpec.swift | 46 +++--- 7 files changed, 81 insertions(+), 203 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift index 8d1f2a79..38c72498 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift @@ -47,7 +47,7 @@ class DiagnosticReporter: DiagnosticReporting { sendDiagnosticEventAsync(diagnosticEvent: initEvent) } - timer = LDTimer(withTimeInterval: service.config.diagnosticRecordingInterval, repeats: true, fireQueue: workQueue) { + timer = LDTimer(withTimeInterval: service.config.diagnosticRecordingInterval, fireQueue: workQueue) { self.sendDiagnosticEventSync(diagnosticEvent: cache.getCurrentStatsAndReset()) } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index d67e9835..527cca16 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -97,7 +97,7 @@ class EventReporter: EventReporting { private func startReporting(isOnline: Bool) { guard isOnline && !isReportingActive else { return } - eventReportTimer = LDTimer(withTimeInterval: service.config.eventFlushInterval, repeats: true, fireQueue: eventQueue, execute: reportEvents) + eventReportTimer = LDTimer(withTimeInterval: service.config.eventFlushInterval, fireQueue: eventQueue, execute: reportEvents) } private func stopReporting() { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift index c5d012dd..74213bb9 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift @@ -176,7 +176,7 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { return } Log.debug(typeName(and: #function)) - flagRequestTimer = LDTimer(withTimeInterval: pollingInterval, repeats: true, fireQueue: syncQueue, execute: processTimer) + flagRequestTimer = LDTimer(withTimeInterval: pollingInterval, fireQueue: syncQueue, execute: processTimer) makeFlagRequest(isOnline: isOnline) } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/LDTimer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/LDTimer.swift index b88268e2..42cecabe 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/LDTimer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/LDTimer.swift @@ -1,17 +1,9 @@ -// -// LDTimer.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation protocol TimeResponding { - var isRepeating: Bool { get } var fireDate: Date? { get } - init(withTimeInterval: TimeInterval, repeats: Bool, fireQueue: DispatchQueue, execute: @escaping () -> Void) + init(withTimeInterval: TimeInterval, fireQueue: DispatchQueue, execute: @escaping () -> Void) func cancel() } @@ -20,17 +12,15 @@ final class LDTimer: TimeResponding { private (set) weak var timer: Timer? private let fireQueue: DispatchQueue private let execute: () -> Void - private (set) var isRepeating: Bool private (set) var isCancelled: Bool = false var fireDate: Date? { timer?.fireDate } - init(withTimeInterval timeInterval: TimeInterval, repeats: Bool, fireQueue: DispatchQueue = DispatchQueue.main, execute: @escaping () -> Void) { - isRepeating = repeats + init(withTimeInterval timeInterval: TimeInterval, fireQueue: DispatchQueue = DispatchQueue.main, execute: @escaping () -> Void) { self.fireQueue = fireQueue self.execute = execute // the run loop retains the timer, so the property is weak to avoid a retain cycle. Setting the timer to a strong reference is important so that the timer doesn't get nil'd before it's added to the run loop. - let timer = Timer(timeInterval: timeInterval, target: self, selector: #selector(timerFired), userInfo: nil, repeats: repeats) + let timer = Timer(timeInterval: timeInterval, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true) self.timer = timer RunLoop.main.add(timer, forMode: RunLoop.Mode.default) } @@ -52,9 +42,3 @@ final class LDTimer: TimeResponding { isCancelled = true } } - -#if DEBUG -extension LDTimer { - var testFireQueue: DispatchQueue { fireQueue } -} -#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift index 68b8188b..16b71b78 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift @@ -1,10 +1,3 @@ -// -// Throttler.swift -// LaunchDarkly -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation typealias RunClosure = () -> Void @@ -28,8 +21,7 @@ final class Throttler: Throttling { let maxDelay: TimeInterval private (set) var runAttempts = -1 - private (set) var delayTimer: TimeResponding? - private var runClosure: RunClosure? + private (set) var workItem: DispatchWorkItem? init(maxDelay: TimeInterval = Constants.defaultDelay, environmentReporter: EnvironmentReporting = EnvironmentReporter(), @@ -44,17 +36,18 @@ final class Throttler: Throttling { } func runThrottledSync(_ runClosure: @escaping RunClosure) -> String? { - runQueue.sync { - if !throttlingEnabled { - dispatcher(runClosure) - return typeName(and: #function) + "Executing run closure unthrottled, as throttling is disabled." - } + if !throttlingEnabled { + dispatcher(runClosure) + return typeName(and: #function) + "Executing run closure unthrottled, as throttling is disabled." + } + return runQueue.sync { runAttempts += 1 let resetDelay = min(maxDelay, TimeInterval(pow(2.0, Double(runAttempts - 1)))) - if runAttempts > 0 { - runQueue.asyncAfter(deadline: .now() + resetDelay) { self.decrementRunAttempts() } + runQueue.asyncAfter(deadline: .now() + resetDelay) { [weak self] in + guard let self = self else { return } + self.runAttempts = max(0, self.runAttempts - 1) } if runAttempts <= 1 { @@ -63,36 +56,23 @@ final class Throttler: Throttling { } let jittered = resetDelay / 2 + Double.random(in: 0.0...(resetDelay / 2)) - self.runClosure = runClosure - self.delayTimer?.cancel() - self.delayTimer = LDTimer(withTimeInterval: jittered, repeats: false, fireQueue: runQueue, execute: timerFired) + let workItem = DispatchWorkItem { [weak self] in + guard let self = self else { return } + self.dispatcher(runClosure) + self.workItem = nil + } + self.workItem?.cancel() + self.workItem = workItem + runQueue.asyncAfter(deadline: .now() + jittered, execute: workItem) return typeName(and: #function) + "Throttling run closure. Run attempts: \(runAttempts), Delay: \(jittered)" } } func cancelThrottledRun() { runQueue.sync { - delayTimer?.cancel() - reset() - } - } - - private func reset() { - delayTimer = nil - runClosure = nil - } - - private func decrementRunAttempts() { - if runAttempts > 0 { - runAttempts -= 1 - } - } - - @objc func timerFired() { - if let run = runClosure { - dispatcher(run) + self.workItem?.cancel() + self.workItem = nil } - reset() } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift index 2e11a23d..d97cdece 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift @@ -1,10 +1,3 @@ -// -// LDTimerSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble @@ -12,25 +5,16 @@ import Nimble final class LDTimerSpec: QuickSpec { - struct Constants { - static let oneMinute: TimeInterval = 60.0 - static let oneMilli: TimeInterval = 0.001 - static let fireQueueLabel = "LaunchDarkly.LDTimerSpec.TestContext.fireQueue" - static let targetFireCount = 5 - } - struct TestContext { var ldTimer: LDTimer - let fireQueue: DispatchQueue = DispatchQueue(label: Constants.fireQueueLabel) + let fireQueue: DispatchQueue = DispatchQueue(label: "LaunchDarkly.LDTimerSpec.TestContext.fireQueue") let timeInterval: TimeInterval - let repeats: Bool let fireDate: Date - init(timeInterval: TimeInterval = Constants.oneMinute, repeats: Bool, execute: @escaping () -> Void) { + init(timeInterval: TimeInterval = 60.0, execute: @escaping () -> Void) { self.timeInterval = timeInterval - self.repeats = repeats self.fireDate = Date().addingTimeInterval(timeInterval) - ldTimer = LDTimer(withTimeInterval: timeInterval, repeats: repeats, fireQueue: fireQueue, execute: execute) + ldTimer = LDTimer(withTimeInterval: timeInterval, fireQueue: fireQueue, execute: execute) } } @@ -41,123 +25,53 @@ final class LDTimerSpec: QuickSpec { } private func initSpec() { - var testContext: TestContext! describe("init") { - afterEach { + it("creates a repeating timer") { + let testContext = TestContext(execute: { }) + + expect(testContext.ldTimer.timer).toNot(beNil()) + expect(testContext.ldTimer.isCancelled) == false + expect(testContext.ldTimer.fireDate?.isWithin(1.0, of: testContext.fireDate)).to(beTrue()) // 1 second is arbitrary...just want it to be "close" + testContext.ldTimer.cancel() } - context("repeating timer") { - beforeEach { - testContext = TestContext(repeats: true, execute: { }) - } - it("creates a repeating timer") { - expect(testContext.ldTimer).toNot(beNil()) - expect(testContext.ldTimer.timer).toNot(beNil()) - expect(testContext.ldTimer.testFireQueue.label) == Constants.fireQueueLabel - expect(testContext.ldTimer.isRepeating) == testContext.repeats // true - expect(testContext.ldTimer.isCancelled) == false - expect(testContext.ldTimer.fireDate?.isWithin(1.0, of: testContext.fireDate)).to(beTrue()) // 1 second is arbitrary...just want it to be "close" - } - } - context("one-time timer") { - beforeEach { - testContext = TestContext(repeats: false, execute: { }) - } - it("creates a one-time timer") { - expect(testContext.ldTimer).toNot(beNil()) - expect(testContext.ldTimer.timer).toNot(beNil()) - expect(testContext.ldTimer.testFireQueue.label) == Constants.fireQueueLabel - expect(testContext.ldTimer.isRepeating) == testContext.repeats // false - expect(testContext.ldTimer.isCancelled) == false - expect(testContext.ldTimer.fireDate?.isWithin(1.0, of: testContext.fireDate)).to(beTrue()) // 1 second is arbitrary...just want it to be "close" - } - } } } private func timerFiredSpec() { - var testContext: TestContext! - var fireQueueLabel: String? - var fireCount = 0 describe("timerFired") { - context("one-time timer") { - beforeEach { - waitUntil { done in - // timeInterval is arbitrary here. "Fast" so the test doesn't take a long time. - testContext = TestContext(timeInterval: Constants.oneMilli, repeats: false, execute: { - fireQueueLabel = DispatchQueue.currentQueueLabel + it("calls execute on the fireQueue multiple times") { + var fireCount = 0 + var testContext: TestContext! + waitUntil { done in + // timeInterval is arbitrary here. "Fast" so the test doesn't take a long time. + testContext = TestContext(timeInterval: 0.01, execute: { + dispatchPrecondition(condition: .onQueue(testContext.fireQueue)) + if fireCount < 2 { + fireCount += 1 // If the timer fires again before the test is done, that's ok. This just measures an arbitrary point in time. + } else { done() - }) - } - } - it("calls execute on the fireQueue one time") { - expect(testContext.ldTimer.timer).to(beNil()) - expect(fireQueueLabel).toNot(beNil()) - expect(fireQueueLabel) == Constants.fireQueueLabel - } - } - context("repeating timer") { - beforeEach { - waitUntil { done in - // timeInterval is arbitrary here. "Fast" so the test doesn't take a long time. - testContext = TestContext(timeInterval: Constants.oneMilli, repeats: true, execute: { - if fireQueueLabel == nil { - fireQueueLabel = DispatchQueue.currentQueueLabel - } - if fireCount < Constants.targetFireCount { - fireCount += 1 // If the timer fires again before the test is done, that's ok. This just measures an arbitrary point in time. - if fireCount == Constants.targetFireCount { - done() - } - } - }) - } - } - afterEach { - testContext.ldTimer.cancel() - } - it("calls execute on the fireQueue multiple times") { - expect(testContext.ldTimer.timer).toNot(beNil()) - expect(testContext.ldTimer.timer?.isValid) == true - expect(fireQueueLabel).toNot(beNil()) - expect(fireQueueLabel) == Constants.fireQueueLabel - expect(fireCount) == Constants.targetFireCount // targetFireCount is 5, and totally arbitrary. Want to measure that the repeating timer does in fact repeat. + } + }) } + + expect(testContext.ldTimer.timer?.isValid) == true + expect(testContext.ldTimer.isCancelled) == false + expect(fireCount) == 2 + + testContext.ldTimer.cancel() } } } private func cancelSpec() { - var testContext: TestContext! describe("cancel") { - context("one-time timer") { - beforeEach { - testContext = TestContext(repeats: false, execute: { }) - - testContext.ldTimer.cancel() - } - it("cancels the timer") { - expect(testContext.ldTimer.timer?.isValid ?? false) == false // the timer either doesn't exist or is invalid...could be either depending on timing - expect(testContext.ldTimer.isCancelled) == true - } - } - context("repeating timer") { - beforeEach { - testContext = TestContext(repeats: true, execute: { }) - - testContext.ldTimer.cancel() - } - it("cancels the timer") { - expect(testContext.ldTimer.timer?.isValid ?? false) == false // the timer either doesn't exist or is invalid...could be either depending on timing - expect(testContext.ldTimer.isCancelled) == true - } + it("cancels the timer") { + let testContext = TestContext(execute: { }) + testContext.ldTimer.cancel() + expect(testContext.ldTimer.timer?.isValid ?? false) == false // the timer either doesn't exist or is invalid...could be either depending on timing + expect(testContext.ldTimer.isCancelled) == true } } } } - -extension DispatchQueue { - class var currentQueueLabel: String? { - String(validatingUTF8: __dispatch_queue_get_label(nil)) // from https://gitlab.com/theswiftdev/swift/snippets/1741827/raw - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift index c1713b19..49717f9f 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift @@ -1,10 +1,3 @@ -// -// ThrottlerSpec.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble @@ -38,13 +31,13 @@ final class ThrottlerSpec: QuickSpec { let throttler = Throttler(maxDelay: Constants.maxDelay) expect(throttler.maxDelay) == Constants.maxDelay expect(throttler.runAttempts) == -1 - expect(throttler.delayTimer).to(beNil()) + expect(throttler.workItem).to(beNil()) } it("without a maxDelay parameter") { let throttler = Throttler() expect(throttler.maxDelay) == Throttler.Constants.defaultDelay expect(throttler.runAttempts) == -1 - expect(throttler.delayTimer).to(beNil()) + expect(throttler.workItem).to(beNil()) } it("throttling controlled by environment reporter") { expect(self.testThrottler(throttlingDisabled: false).throttlingEnabled) == true @@ -76,14 +69,14 @@ final class ThrottlerSpec: QuickSpec { } expect(hasRun) == true expect(throttler.safeRunAttempts) == 0 - expect(throttler.delayTimer).to(beNil()) + expect(throttler.workItem).to(beNil()) hasRun = false throttler.runThrottled { hasRun = true } expect(hasRun) == true expect(throttler.safeRunAttempts) == 1 - expect(throttler.delayTimer).to(beNil()) + expect(throttler.workItem).to(beNil()) } } @@ -113,28 +106,35 @@ final class ThrottlerSpec: QuickSpec { throttler.runThrottled { } throttler.runThrottled { } expect(throttler.safeRunAttempts) == 1 - var hasRun = false + let callDate = Date() + var runDate: Date? waitUntil(timeout: .seconds(3)) { done in - let callDate = Date() throttler.runThrottled { - hasRun = true + runDate = Date() done() } - expect(hasRun) == false - expect(throttler.delayTimer?.fireDate) >= callDate + 1 - expect(throttler.delayTimer?.fireDate) <= callDate + 2.5 + expect(runDate).to(beNil()) } - expect(hasRun) == true + expect(runDate) >= callDate + 1 + expect(runDate) <= callDate + 2.5 } } func maxDelaySpec() { it("limits delay to maxDelay") { - let throttler = self.testThrottler() + let throttler = Throttler(maxDelay: 1.0) (0..<10).forEach { _ in throttler.runThrottled { } } - let now = Date() - expect(throttler.delayTimer?.fireDate) <= now.addingTimeInterval(Constants.maxDelay) - expect(throttler.delayTimer?.fireDate) >= now.addingTimeInterval(Constants.maxDelay / 2) - 0.5 + let callDate = Date() + var runDate: Date? + waitUntil(timeout: .seconds(2)) { done in + throttler.runThrottled { + runDate = Date() + done() + } + expect(runDate).to(beNil()) + } + expect(runDate) >= callDate + 0.5 + expect(runDate) <= callDate + 1.5 throttler.cancelThrottledRun() } } @@ -166,7 +166,7 @@ final class ThrottlerSpec: QuickSpec { } throttler.cancelThrottledRun() expect(throttler.safeRunAttempts) == 2 - expect(throttler.delayTimer).to(beNil()) + expect(throttler.workItem).to(beNil()) // Wait until run would have occured Thread.sleep(forTimeInterval: 1.0) expect(hasRun).to(beFalse()) From 53799f2ca0c734e5edc4080bd431d6f95a959f89 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Mon, 17 Jan 2022 21:25:23 -0600 Subject: [PATCH 20/50] Remove variation methods that allowed optional default values, and improve some documentation comments. --- .../LaunchDarkly/LDClientVariation.swift | 124 ++++++------------ .../LaunchDarkly/Models/LDConfig.swift | 50 ++++--- .../ObjectiveC/ObjcLDClient.swift | 36 ++--- .../LaunchDarklyTests/LDClientSpec.swift | 115 ++-------------- 4 files changed, 94 insertions(+), 231 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift index d896f2ee..67602b59 100644 --- a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -3,29 +3,38 @@ import Foundation extension LDClient { // MARK: Retrieving Flag Values /** - Returns the variation for the given feature flag. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns the default value. Use this method when the default value is a non-Optional type. See `variation` with the Optional return value when the default value can be nil. + Returns the variation for the given feature flag. If the flag does not exist, cannot be cast to the correct return + type, or the LDClient is not started, returns the default value. - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *true* and *false*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. + A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *true* and *false*. + You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the + available types. - The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. + When online, the LDClient has two modes for maintaining feature flag values: *streaming* and *polling*. The client + app requests the mode by setting the `config.streamingMode`, see `LDConfig` for details. - When online, the LDClient has two modes for maintaining feature flag values: *streaming* and *polling*. The client app requests the mode by setting the `config.streamingMode`, see `LDConfig` for details. - - In streaming mode, the LDClient opens a long-running connection to LaunchDarkly's streaming server (called *clientstream*). When a flag value changes on the server, the clientstream notifies the SDK to update the value. Streaming mode is not available on watchOS. On iOS and tvOS, the client app must be running in the foreground to connect to clientstream. On macOS the client app may run in either foreground or background to connect to clientstream. If streaming mode is not available, the SDK reverts to polling mode. - - In polling mode, the LDClient requests feature flags from LaunchDarkly's app server at regular intervals defined in the LDConfig. When a flag value changes on the server, the LDClient will learn of the change the next time the SDK requests feature flags. - - When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. - - A call to `variation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. + A call to `variation` records events reported later. Recorded events allow clients to analyze usage and assist in + debugging issues. ### Usage ```` let boolFeatureFlagValue = LDClient.get()!.variation(forKey: "bool-flag-key", defaultValue: false) //boolFeatureFlagValue is a Bool ```` - **Important** The default value tells the SDK the type of the feature flag. In several cases, the feature flag type cannot be determined by the values sent from the server. It is possible to provide a default value with a type that does not match the feature flag value's type. The SDK will attempt to convert the feature flag's value into the type of the default value in the variation request. If that cast fails, the SDK will not be able to determine the correct return type, and will always return the default value. - - Pay close attention to the type of the default value for collections. If the default value collection type is more restrictive than the feature flag, the sdk will return the default value even though the feature flag is present because it cannot convert the feature flag into the type requested via the default value. For example, if the feature flag has the type `[String: Any]`, but the default value has the type `[String: Int]`, the sdk will not be able to convert the flags into the requested type, and will return the default value. - - To avoid this, make sure the default value type matches the expected feature flag type. Either specify the default value type to be the feature flag type, or cast the default value to the feature flag type prior to making the variation request. In the above example, either specify that the default value's type is [String: Any]: + **Important** The default value tells the SDK the type of the feature flag. In several cases, the feature flag type + cannot be determined by the values sent from the server. It is possible to provide a default value with a type that + does not match the feature flag value's type. The SDK will attempt to convert the feature flag's value into the + type of the default value in the variation request. If that cast fails, the SDK will not be able to determine the + correct return type, and will always return the default value. + + Pay close attention to the type of the default value for collections. If the default value collection type is more + restrictive than the feature flag, the sdk will return the default value even though the feature flag is present + because it cannot convert the feature flag into the type requested via the default value. For example, if the + feature flag has the type `[String: Any]`, but the default value has the type `[String: Int]`, the sdk will not be + able to convert the flags into the requested type, and will return the default value. + + To avoid this, make sure the default value type matches the expected feature flag type. Either specify the default + value type to be the feature flag type, or cast the default value to the feature flag type prior to making the + variation request. In the above example, either specify that the default value's type is `[String: Any]`: ```` let defaultValue: [String: Any] = ["a": 1, "b": 2] //dictionary type would be [String: Int] without the type specifier ```` @@ -39,14 +48,16 @@ extension LDClient { - returns: The requested feature flag value, or the default value if the flag is missing or cannot be cast to the default value type, or the client is not started */ - /// - Tag: variationWithdefaultValue + /// - Tag: variation public func variation(forKey flagKey: LDFlagKey, defaultValue: T) -> T { - // the defaultValue cast to 'as T?' directs the call to the Optional-returning variation method - variation(forKey: flagKey, defaultValue: defaultValue as T?) ?? defaultValue + variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: false) } /** - Returns the LDEvaluationDetail for the given feature flag. LDEvaluationDetail gives you more insight into why your variation contains the specified value. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns an LDEvaluationDetail with the default value. Use this method when the default value is a non-Optional type. See `variationDetail` with the Optional return value when the default value can be nil. See [variationWithdefaultValue](x-source-tag://variationWithdefaultValue) + Returns the LDEvaluationDetail for the given feature flag. LDEvaluationDetail gives you more insight into why your + variation contains the specified value. If the flag does not exist, cannot be cast to the correct return type, or + the LDClient is not started, returns an LDEvaluationDetail with the default value. + See [variation](x-source-tag://variation) - parameter forKey: The LDFlagKey for the requested feature flag. - parameter defaultValue: The default value value to return if the feature flag key does not exist. @@ -57,7 +68,7 @@ extension LDClient { let featureFlag = flagStore.featureFlag(for: flagKey) let reason = checkErrorKinds(featureFlag: featureFlag) ?? featureFlag?.reason let value = variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: true) - return LDEvaluationDetail(value: value ?? defaultValue, variationIndex: featureFlag?.variation, reason: reason) + return LDEvaluationDetail(value: value, variationIndex: featureFlag?.variation, reason: reason) } private func checkErrorKinds(featureFlag: FeatureFlag?) -> [String: Any]? { @@ -70,83 +81,22 @@ extension LDClient { } } - /** - Returns the variation for the given feature flag. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns the default value, which may be `nil`. Use this method when the default value is an Optional type. See `variation` with the non-Optional return value when the default value cannot be nil. - - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *true* and *false*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. - - The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. - - When online, the LDClient has two modes for maintaining feature flag values: *streaming* and *polling*. The client app requests the mode by setting the `config.streamingMode`, see `LDConfig` for details. - - In streaming mode, the LDClient opens a long-running connection to LaunchDarkly's streaming server (called *clientstream*). When a flag value changes on the server, the clientstream notifies the SDK to update the value. Streaming mode is not available on watchOS. On iOS and tvOS, the client app must be running in the foreground to connect to clientstream. On macOS the client app may run in either foreground or background to connect to clientstream. If streaming mode is not available, the SDK reverts to polling mode. - - In polling mode, the LDClient requests feature flags from LaunchDarkly's app server at regular intervals defined in the LDConfig. When a flag value changes on the server, the LDClient will learn of the change the next time the SDK requests feature flags. - - When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. - - A call to `variation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. - - ### Usage - ```` - let boolFeatureFlagValue: Bool? = LDClient.get()!.variation(forKey: "bool-flag-key", defaultValue: nil) //boolFeatureFlagValue is a Bool? - ```` - **Important** The default value tells the SDK the type of the feature flag. In several cases, the feature flag type cannot be determined by the values sent from the server. It is possible to provide a default value with a type that does not match the feature flag value's type. The SDK will attempt to convert the feature flag's value into the type of the default value in the variation request. If that cast fails, the SDK will not be able to determine the correct return type, and will always return the default value. - - When specifying `nil` as the default value, the compiler must also know the type of the optional. Without this information, the compiler will give the error "'nil' requires a contextual type". There are several ways to provide this information, by setting the type on the item holding the return value, by casting the return value to the desired type, or by casting `nil` to the desired type. We recommend following the above example and setting the type on the return value item. - - For this method, the default value is defaulted to `nil`, allowing the call site to omit the default value. - - Pay close attention to the type of the default value for collections. If the default value collection type is more restrictive than the feature flag, the sdk will return the default value even though the feature flag is present because it cannot convert the feature flag into the type requested via the default value. For example, if the feature flag has the type `[String: Any]`, but the default value has the type `[String: Int]`, the sdk will not be able to convert the flags into the requested type, and will return the default value. - - To avoid this, make sure the default value type matches the expected feature flag type. Either specify the default value value type to be the feature flag type, or cast the default value value to the feature flag type prior to making the variation request. In the above example, either specify that the default value's type is [String: Any]: - ```` - let defaultValue: [String: Any]? = ["a": 1, "b": 2] //dictionary type would be [String: Int] without the type specifier - ```` - or cast the default value into the feature flag type prior to calling variation: - ```` - let dictionaryFlagValue = LDClient.get()!.variation(forKey: "dictionary-key", defaultValue: ["a": 1, "b": 2] as [String: Any]?) - ```` - - - parameter forKey: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. If omitted, the default value is `nil`. (Optional) - - - returns: The requested feature flag value, or the default value if the flag is missing or cannot be cast to the default value type, or the client is not started - */ - /// - Tag: variationWithoutdefaultValue - public func variation(forKey flagKey: LDFlagKey, defaultValue: T? = nil) -> T? { - variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: false) - } - - /** - Returns the LDEvaluationDetail for the given feature flag. LDEvaluationDetail gives you more insight into why your variation contains the specified value. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns an LDEvaluationDetail with the default value, which may be `nil`. Use this method when the default value is a Optional type. See [variationWithoutdefaultValue](x-source-tag://variationWithoutdefaultValue) - - - parameter forKey: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. If omitted, the default value is `nil`. (Optional) - - - returns: LDEvaluationDetail which wraps the requested feature flag value, or the default value, which variation was served, and the evaluation reason. - */ - public func variationDetail(forKey flagKey: LDFlagKey, defaultValue: T? = nil) -> LDEvaluationDetail { - let featureFlag = flagStore.featureFlag(for: flagKey) - let reason = checkErrorKinds(featureFlag: featureFlag) ?? featureFlag?.reason - let value = variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: true) - return LDEvaluationDetail(value: value, variationIndex: featureFlag?.variation, reason: reason) - } - - private func variationInternal(forKey flagKey: LDFlagKey, defaultValue: T? = nil, includeReason: Bool? = false) -> T? { + private func variationInternal(forKey flagKey: LDFlagKey, defaultValue: T, includeReason: Bool) -> T { guard hasStarted else { - Log.debug(typeName(and: #function) + "returning defaultValue: \(defaultValue.stringValue)." + " LDClient not started.") + Log.debug(typeName(and: #function) + "returning defaultValue: \(defaultValue)." + " LDClient not started.") return defaultValue } let featureFlag = flagStore.featureFlag(for: flagKey) let value = (featureFlag?.value as? T) ?? defaultValue let failedConversionMessage = self.failedConversionMessage(featureFlag: featureFlag, defaultValue: defaultValue) - Log.debug(typeName(and: #function) + "flagKey: \(flagKey), value: \(value.stringValue), defaultValue: \(defaultValue.stringValue), featureFlag: \(featureFlag.stringValue), reason: \(featureFlag?.reason?.description ?? "No evaluation reason")." - + "\(failedConversionMessage)") - eventReporter.recordFlagEvaluationEvents(flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason ?? false) + Log.debug(typeName(and: #function) + "flagKey: \(flagKey), value: \(value), defaultValue: \(defaultValue), " + + "featureFlag: \(String(describing: featureFlag)), reason: \(featureFlag?.reason?.description ?? "nil"). \(failedConversionMessage)") + eventReporter.recordFlagEvaluationEvents(flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason) return value } - private func failedConversionMessage(featureFlag: FeatureFlag?, defaultValue: T?) -> String { + private func failedConversionMessage(featureFlag: FeatureFlag?, defaultValue: T) -> String { if featureFlag == nil { return " Feature flag not found." } diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index ad7715f6..3894a894 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -1,17 +1,20 @@ -// -// LDConfig.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation -/// Defines the connection modes available to set into LDClient. +/// Defines the connection modes the SDK may be configured to use to retrieve feature flag data from LaunchDarkly. public enum LDStreamingMode { - /// In streaming mode, the LDClient opens a long-running connection to LaunchDarkly's streaming server (called *clientstream*). When a flag value changes on the server, the clientstream notifies the SDK to update the value. Streaming mode is not available on watchOS. On iOS and tvOS, the client app must be running in the foreground to connect to clientstream. On macOS the client app may run in either foreground or background to connect to clientstream. If streaming mode is not available, the SDK reverts to polling mode. + /** + In streaming mode, the SDK uses a streaming connection to receive feature flag data from LaunchDarkly. When a flag + is updated in the dashboard, the stream notifies the SDK of changes to the evaluation result for the current user. + + Streaming mode is not available on watchOS. On iOS and tvOS, the client app must be running in the foreground to + use a streaming connection. If streaming mode is not available, the SDK reverts to polling mode. + */ case streaming - /// In polling mode, the LDClient requests feature flags from LaunchDarkly's app server at regular intervals defined in the LDConfig. When a flag value changes on the server, the LDClient will learn of the change the next time the SDK requests feature flags. + /** + In polling mode, the SDK requests feature flag data from the LaunchDarkly service at regular intervals defined in + the `LDConfig`. When a flag is updated in the dashboard, the SDK will not show the change until the next time the + it requests the feature flag data. + */ case polling } @@ -31,18 +34,16 @@ public typealias RequestHeaderTransform = (_ url: URL, _ headers: [String: Strin /** Use LDConfig to configure the LDClient. When initialized, a LDConfig contains the default values which can be changed as needed. - - The client app can change the LDConfig by getting the `config` from `LDClient`, adjusting the values, and setting it back into the `LDClient`. */ public struct LDConfig { /// The default values set when a LDConfig is initialized struct Defaults { - /// The default url for making feature flag requests + /// The default base url for making feature flag requests static let baseUrl = URL(string: "https://app.launchdarkly.com")! - /// The default url for making event reports + /// The default base url for making event reports static let eventsUrl = URL(string: "https://mobile.launchdarkly.com")! - /// The default url for connecting to the *clientstream* + /// The default base url for connecting to streaming service static let streamUrl = URL(string: "https://clientstream.launchdarkly.com")! /// The default maximum number of events the LDClient can store @@ -69,7 +70,7 @@ public struct LDConfig { /// The default private user attribute list (nil) static let privateUserAttributes: [String]? = nil - /// The default HTTP request method for `clientstream` connection and feature flag requests. When true, these requests will use the non-standard verb `REPORT`. When false, these requests will use the standard verb `GET`. (false) + /// The default HTTP request method for stream connections and feature flag requests. When true, these requests will use the non-standard verb `REPORT`. When false, these requests will use the standard verb `GET`. (false) static let useReport = false /// The default setting controlling the amount of user data sent in events. When true the SDK will generate events using the full LDUser, excluding private attributes. When false the SDK will generate events using only the LDUser.key. (false) @@ -150,11 +151,11 @@ public struct LDConfig { /// The Mobile key from your [LaunchDarkly Account](app.launchdarkly.com) settings (on the left at the bottom). If you have multiple projects be sure to choose the correct Mobile key. public var mobileKey: String - /// The url for making feature flag requests. Do not change unless instructed by LaunchDarkly. + /// The base url for making feature flag requests. Do not change unless instructed by LaunchDarkly. public var baseUrl: URL = Defaults.baseUrl - /// The url for making event reports. Do not change unless instructed by LaunchDarkly. + /// The base url for making event reports. Do not change unless instructed by LaunchDarkly. public var eventsUrl: URL = Defaults.eventsUrl - /// The url for connecting to the *clientstream*. Do not change unless instructed by LaunchDarkly. + /// The base url for connecting to the streaming service. Do not change unless instructed by LaunchDarkly. public var streamUrl: URL = Defaults.streamUrl /// The maximum number of analytics events the LDClient can store. When the LDClient event store reaches the eventCapacity, the SDK discards events until it successfully reports them to LaunchDarkly. (Default: 100) @@ -169,7 +170,11 @@ public struct LDConfig { /// The time interval between feature flag requests while running in the background. Used only for polling mode. (Default: 60 minutes) public var backgroundFlagPollingInterval: TimeInterval = Defaults.backgroundFlagPollingInterval - /// Controls the method the SDK uses to keep feature flags updated. When set to .streaming, connects to `clientstream` which notifies the SDK of feature flag changes. When set to .polling, an efficient polling mechanism is used to periodically request feature flag values. Ignored for watchOS, which always uses .polling. See `LDStreamingMode` for more details. (Default: .streaming) + /** + Controls the method the SDK uses to keep feature flags updated. (Default: `.streaming`) + + See `LDStreamingMode` for more details. + */ public var streamingMode: LDStreamingMode = Defaults.streamingMode /// Indicates whether streaming mode is allowed for the operating system private(set) var allowStreamingMode: Bool @@ -211,7 +216,10 @@ public struct LDConfig { public var privateUserAttributes: [String]? = Defaults.privateUserAttributes /** - Directs the SDK to use REPORT for HTTP requests to connect to `clientstream` and make feature flag requests. When false the SDK uses GET for these requests. Do not use unless advised by LaunchDarkly. (Default: false) + Directs the SDK to use REPORT for HTTP requests for feature flag data. (Default: `false`) + + This setting applies both to requests to the streaming service, as well as flag requests when the SDK is in polling + mode. When false the SDK uses GET for these requests. Do not use unless advised by LaunchDarkly. */ public var useReport: Bool = Defaults.useReport private static let flagRetryStatusCodes = [HTTPURLResponse.StatusCodes.methodNotAllowed, HTTPURLResponse.StatusCodes.badRequest, HTTPURLResponse.StatusCodes.notImplemented] diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index f8a9a8c2..2bce560f 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -294,7 +294,7 @@ public final class ObjcLDClient: NSObject { } /** - Returns the NSString variation for the given feature flag. If the flag does not exist, cannot be cast to a NSString, or the LDClient is not started, returns the default value, which may be nil. + Returns the NSString variation for the given feature flag. If the flag does not exist, cannot be cast to a NSString, or the LDClient is not started, returns the default value. A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. @@ -312,12 +312,12 @@ public final class ObjcLDClient: NSObject { ```` - parameter key: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. The default value may be nil. + - parameter defaultValue: The default value to return if the feature flag key does not exist. - - returns: The requested NSString feature flag value, or the default value (which may be nil) if the flag is missing or cannot be cast to a NSString, or the client is not started. + - returns: The requested NSString feature flag value, or the default value if the flag is missing or cannot be cast to a NSString, or the client is not started. */ /// - Tag: stringVariation - @objc public func stringVariation(forKey key: LDFlagKey, defaultValue: String?) -> String? { + @objc public func stringVariation(forKey key: LDFlagKey, defaultValue: String) -> String { ldClient.variation(forKey: key, defaultValue: defaultValue) } @@ -325,17 +325,17 @@ public final class ObjcLDClient: NSObject { See [stringVariation](x-source-tag://stringVariation) for more information on variation methods. - parameter key: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. The default value may be nil. + - parameter defaultValue: The default value to return if the feature flag key does not exist. - returns: ObjcLDStringEvaluationDetail containing your value as well as useful information on why that value was returned. */ - @objc public func stringVariationDetail(forKey key: LDFlagKey, defaultValue: String?) -> ObjcLDStringEvaluationDetail { + @objc public func stringVariationDetail(forKey key: LDFlagKey, defaultValue: String) -> ObjcLDStringEvaluationDetail { let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) return ObjcLDStringEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) } /** - Returns the NSArray variation for the given feature flag. If the flag does not exist, cannot be cast to a NSArray, or the LDClient is not started, returns the default value, which may be nil.. + Returns the NSArray variation for the given feature flag. If the flag does not exist, cannot be cast to a NSArray, or the LDClient is not started, returns the default value. A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. @@ -353,12 +353,12 @@ public final class ObjcLDClient: NSObject { ```` - parameter key: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. The default value may be nil. + - parameter defaultValue: The default value to return if the feature flag key does not exist. - - returns: The requested NSArray feature flag value, or the default value (which may be nil) if the flag is missing or cannot be cast to a NSArray, or the client is not started + - returns: The requested NSArray feature flag value, or the default value if the flag is missing or cannot be cast to a NSArray, or the client is not started */ /// - Tag: arrayVariation - @objc public func arrayVariation(forKey key: LDFlagKey, defaultValue: [Any]?) -> [Any]? { + @objc public func arrayVariation(forKey key: LDFlagKey, defaultValue: [Any]) -> [Any] { ldClient.variation(forKey: key, defaultValue: defaultValue) } @@ -366,17 +366,17 @@ public final class ObjcLDClient: NSObject { See [arrayVariation](x-source-tag://arrayVariation) for more information on variation methods. - parameter key: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. The default value may be nil. + - parameter defaultValue: The default value to return if the feature flag key does not exist. - returns: ObjcLDArrayEvaluationDetail containing your value as well as useful information on why that value was returned. */ - @objc public func arrayVariationDetail(forKey key: LDFlagKey, defaultValue: [Any]?) -> ObjcLDArrayEvaluationDetail { + @objc public func arrayVariationDetail(forKey key: LDFlagKey, defaultValue: [Any]) -> ObjcLDArrayEvaluationDetail { let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) return ObjcLDArrayEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) } /** - Returns the NSDictionary variation for the given feature flag. If the flag does not exist, cannot be cast to a NSDictionary, or the LDClient is not started, returns the default value, which may be nil.. + Returns the NSDictionary variation for the given feature flag. If the flag does not exist, cannot be cast to a NSDictionary, or the LDClient is not started, returns the default value. A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. @@ -394,12 +394,12 @@ public final class ObjcLDClient: NSObject { ```` - parameter key: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. The default value may be nil. + - parameter defaultValue: The default value to return if the feature flag key does not exist. - - returns: The requested NSDictionary feature flag value, or the default value (which may be nil) if the flag is missing or cannot be cast to a NSDictionary, or the client is not started + - returns: The requested NSDictionary feature flag value, or the default value if the flag is missing or cannot be cast to a NSDictionary, or the client is not started */ /// - Tag: dictionaryVariation - @objc public func dictionaryVariation(forKey key: LDFlagKey, defaultValue: [String: Any]?) -> [String: Any]? { + @objc public func dictionaryVariation(forKey key: LDFlagKey, defaultValue: [String: Any]) -> [String: Any] { ldClient.variation(forKey: key, defaultValue: defaultValue) } @@ -407,11 +407,11 @@ public final class ObjcLDClient: NSObject { See [dictionaryVariation](x-source-tag://dictionaryVariation) for more information on variation methods. - parameter key: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. The default value may be nil. + - parameter defaultValue: The default value to return if the feature flag key does not exist. - returns: ObjcLDDictionaryEvaluationDetail containing your value as well as useful information on why that value was returned. */ - @objc public func dictionaryVariationDetail(forKey key: LDFlagKey, defaultValue: [String: Any]?) -> ObjcLDDictionaryEvaluationDetail { + @objc public func dictionaryVariationDetail(forKey key: LDFlagKey, defaultValue: [String: Any]) -> ObjcLDDictionaryEvaluationDetail { let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) return ObjcLDDictionaryEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) } diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 4f7f8f33..b71b3240 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -1,10 +1,3 @@ -// -// LDClientSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble @@ -973,13 +966,12 @@ final class LDClientSpec: QuickSpec { } context("non-Optional default value") { it("returns the flag value") { - // The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) as Bool) == DarklyServiceMock.FlagValues.bool - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int) as Int) == DarklyServiceMock.FlagValues.int - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double) as Double) == DarklyServiceMock.FlagValues.double - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string) as String) == DarklyServiceMock.FlagValues.string + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DarklyServiceMock.FlagValues.bool + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DarklyServiceMock.FlagValues.int + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DarklyServiceMock.FlagValues.double + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DarklyServiceMock.FlagValues.string expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array) == DarklyServiceMock.FlagValues.array).to(beTrue()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary) as [String: Any] + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary) == DarklyServiceMock.FlagValues.dictionary).to(beTrue()) } it("records a flag evaluation event") { @@ -992,61 +984,16 @@ final class LDClientSpec: QuickSpec { expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user } } - context("Optional default value") { - it("returns the flag value") { - // The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the Optional variation method - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?)) == DarklyServiceMock.FlagValues.bool - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int as Int?)) == DarklyServiceMock.FlagValues.int - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double as Double?)) == DarklyServiceMock.FlagValues.double - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string as String?)) == DarklyServiceMock.FlagValues.string - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array as Array?) == DarklyServiceMock.FlagValues.array).to(beTrue()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary as [String: Any]?) - == DarklyServiceMock.FlagValues.dictionary).to(beTrue()) - } - it("records a flag evaluation event") { - // The cast in the variation call directs the compiler to the Optional variation method - _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool - expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value, to: DarklyServiceMock.FlagValues.bool)).to(beTrue()) - expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue, to: DefaultFlagValues.bool)).to(beTrue()) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag) == testContext.flagStoreMock.featureFlags[DarklyServiceMock.FlagKeys.bool] - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user - } - } - context("No default value") { - it("returns the flag value") { - // The casts in the expect() calls allow the compiler to determine the return type. - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: nil as Bool?)) == DarklyServiceMock.FlagValues.bool - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: nil as Int?)) == DarklyServiceMock.FlagValues.int - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: nil as Double?)) == DarklyServiceMock.FlagValues.double - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: nil as String?)) == DarklyServiceMock.FlagValues.string - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: nil as Array?) == DarklyServiceMock.FlagValues.array).to(beTrue()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: nil as [String: Any]?) - == DarklyServiceMock.FlagValues.dictionary).to(beTrue()) - } - it("records a flag evaluation event") { - // The cast in the variation call allows the compiler to determine the return type - _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: nil as Bool?) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool - expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value, to: DarklyServiceMock.FlagValues.bool)).to(beTrue()) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue).to(beNil()) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag) == testContext.flagStoreMock.featureFlags[DarklyServiceMock.FlagKeys.bool] - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user - } - } } context("flag store does not contain the requested value") { context("non-Optional default value") { it("returns the default value") { - // The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) as Bool) == DefaultFlagValues.bool - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int) as Int) == DefaultFlagValues.int - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double) as Double) == DefaultFlagValues.double - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string) as String) == DefaultFlagValues.string + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DefaultFlagValues.bool + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DefaultFlagValues.int + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DefaultFlagValues.double + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DefaultFlagValues.string expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array) == DefaultFlagValues.array).to(beTrue()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary) as [String: Any] == DefaultFlagValues.dictionary).to(beTrue()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary) == DefaultFlagValues.dictionary).to(beTrue()) } it("records a flag evaluation event") { _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) @@ -1058,48 +1005,6 @@ final class LDClientSpec: QuickSpec { expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user } } - context("Optional default value") { - it("returns the default value") { - // The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?)) == DefaultFlagValues.bool - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int as Int?)) == DefaultFlagValues.int - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double as Double?)) == DefaultFlagValues.double - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string as String?)) == DefaultFlagValues.string - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array as Array?) == DefaultFlagValues.array).to(beTrue()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary as [String: Any]?) == DefaultFlagValues.dictionary).to(beTrue()) - } - it("records a flag evaluation event") { - // The cast in the variation call directs the compiler to the Optional variation method - _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool - expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value, to: DefaultFlagValues.bool)).to(beTrue()) - expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue, to: DefaultFlagValues.bool)).to(beTrue()) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag).to(beNil()) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user - } - } - context("no default value") { - it("returns nil") { - // The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: nil as Bool?)).to(beNil()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: nil as Int?)).to(beNil()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: nil as Double?)).to(beNil()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: nil as String?)).to(beNil()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: nil as [Any]?)).to(beNil()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: nil as [String: Any]?)).to(beNil()) - } - it("records a flag evaluation event") { - // The cast in the variation call directs the compiler to the Optional variation method - _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: nil as Bool?) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value).to(beNil()) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue).to(beNil()) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag).to(beNil()) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user - } - } } } } From b732e7af0c12d7503cf5070d332b09e4a7bf44a6 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Wed, 19 Jan 2022 16:10:02 -0600 Subject: [PATCH 21/50] Update LDSwiftEventSource version and test dependencies. (#172) --- LaunchDarkly.podspec | 2 +- LaunchDarkly.xcodeproj/project.pbxproj | 6 +++--- Package.swift | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 7b163b04..3cfc44f1 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -35,6 +35,6 @@ Pod::Spec.new do |ld| ld.swift_version = '5.0' ld.subspec 'Core' do |es| - es.dependency 'LDSwiftEventSource', '1.2.1' + es.dependency 'LDSwiftEventSource', '1.3.0' end end diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 2d75e252..191fa74b 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -1962,7 +1962,7 @@ repositoryURL = "https://github.com/LaunchDarkly/swift-eventsource.git"; requirement = { kind = exactVersion; - version = 1.2.1; + version = 1.3.0; }; }; B4903D9624BD61B200F087C4 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { @@ -1978,7 +1978,7 @@ repositoryURL = "https://github.com/Quick/Nimble.git"; requirement = { kind = exactVersion; - version = 9.2.0; + version = 9.2.1; }; }; B4903D9C24BD61EF00F087C4 /* XCRemoteSwiftPackageReference "Quick" */ = { @@ -1986,7 +1986,7 @@ repositoryURL = "https://github.com/Quick/Quick.git"; requirement = { kind = exactVersion; - version = 3.1.2; + version = 4.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Package.swift b/Package.swift index 223e25f2..79631e0f 100644 --- a/Package.swift +++ b/Package.swift @@ -17,9 +17,9 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", .exact("9.1.0")), - .package(url: "https://github.com/Quick/Quick.git", .exact("3.1.2")), - .package(url: "https://github.com/Quick/Nimble.git", .exact("9.2.0")), - .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("1.2.1")) + .package(url: "https://github.com/Quick/Quick.git", .exact("4.0.0")), + .package(url: "https://github.com/Quick/Nimble.git", .exact("9.2.1")), + .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("1.3.0")) ], targets: [ .target( From 822703a1f5e9372a7450b6473eccff242aca708d Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Wed, 19 Jan 2022 17:42:13 -0600 Subject: [PATCH 22/50] (v6) Remove oldest deprecated caches. (#166) --- LaunchDarkly.xcodeproj/project.pbxproj | 42 ---------- .../Cache/DeprecatedCache.swift | 2 +- .../Cache/DeprecatedCacheModelV2.swift | 58 -------------- .../Cache/DeprecatedCacheModelV3.swift | 66 --------------- .../Cache/DeprecatedCacheModelV4.swift | 74 ----------------- .../ServiceObjects/ClientServiceFactory.swift | 3 - .../Cache/CacheConverterSpec.swift | 2 +- .../Cache/DeprecatedCacheModelSpec.swift | 13 ++- .../Cache/DeprecatedCacheModelV2Spec.swift | 54 ------------- .../Cache/DeprecatedCacheModelV3Spec.swift | 73 ----------------- .../Cache/DeprecatedCacheModelV4Spec.swift | 80 ------------------- .../Cache/DeprecatedCacheModelV5Spec.swift | 1 - 12 files changed, 7 insertions(+), 461 deletions(-) delete mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift delete mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift delete mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV2Spec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV3Spec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV4Spec.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 2d75e252..92b06d2f 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -107,10 +107,6 @@ 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; - 832D68A7224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */; }; - 832D68A8224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */; }; - 832D68A9224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */; }; - 832D68AA224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */; }; 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68AB224B3321005F052A /* CacheConverterSpec.swift */; }; 832EA061203D03B700A93C0E /* AnyComparerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832EA060203D03B700A93C0E /* AnyComparerSpec.swift */; }; 8335299E1FC37727001166F8 /* FlagMaintainingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */; }; @@ -191,17 +187,6 @@ 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */; }; 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */; }; 83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */; }; - 83D1522B224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */; }; - 83D1522C224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */; }; - 83D1522D224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */; }; - 83D1522E224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */; }; - 83D15230224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */; }; - 83D15231224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */; }; - 83D15232224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */; }; - 83D15233224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */; }; - 83D15235225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D15234225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift */; }; - 83D15237225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D15236225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift */; }; - 83D15239225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D15238225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift */; }; 83D1523B22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */; }; 83D17EAA1FCDA18C00B2823C /* DictionarySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */; }; 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */; }; @@ -391,7 +376,6 @@ 832307A91F7ECA630029815A /* LDConfigStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDConfigStub.swift; sourceTree = ""; }; 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV5.swift; sourceTree = ""; }; 832D68A1224A38FC005F052A /* CacheConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverter.swift; sourceTree = ""; }; - 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV2.swift; sourceTree = ""; }; 832D68AB224B3321005F052A /* CacheConverterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverterSpec.swift; sourceTree = ""; }; 832EA060203D03B700A93C0E /* AnyComparerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyComparerSpec.swift; sourceTree = ""; }; 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagMaintainingMock.swift; sourceTree = ""; }; @@ -448,11 +432,6 @@ 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagsUnchangedObserver.swift; sourceTree = ""; }; 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventReporterSpec.swift; sourceTree = ""; }; 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarklyServiceMock.swift; sourceTree = ""; }; - 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV3.swift; sourceTree = ""; }; - 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV4.swift; sourceTree = ""; }; - 83D15234225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV2Spec.swift; sourceTree = ""; }; - 83D15236225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV3Spec.swift; sourceTree = ""; }; - 83D15238225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV4Spec.swift; sourceTree = ""; }; 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV5Spec.swift; sourceTree = ""; }; 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionarySpec.swift; sourceTree = ""; }; 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedValueCache.swift; sourceTree = ""; }; @@ -622,9 +601,6 @@ 832D68A1224A38FC005F052A /* CacheConverter.swift */, 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */, 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */, - 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */, - 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */, - 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */, B4C9D4322489C8FD004A9B03 /* DiagnosticCache.swift */, ); path = Cache; @@ -638,9 +614,6 @@ 832D68AB224B3321005F052A /* CacheConverterSpec.swift */, B43D5ACF25FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift */, 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */, - 83D15238225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift */, - 83D15236225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift */, - 83D15234225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift */, B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */, ); path = Cache; @@ -1255,12 +1228,10 @@ 831188672113AE4D00D77CB5 /* Thread.swift in Sources */, 832D68A0224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 8354AC642241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, - 83D1522E224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, C443A40823145FEE00145710 /* ConnectionInformationStore.swift in Sources */, 831188662113AE4A00D77CB5 /* AnyComparer.swift in Sources */, 831188492113ADD400D77CB5 /* LDFlagBaseTypeConvertible.swift in Sources */, 8311885C2113AE2200D77CB5 /* HTTPHeaders.swift in Sources */, - 832D68AA224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */, 831188562113AE0800D77CB5 /* FlagSynchronizer.swift in Sources */, 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */, 8311885A2113AE1500D77CB5 /* Log.swift in Sources */, @@ -1275,7 +1246,6 @@ 8311884E2113ADE500D77CB5 /* Event.swift in Sources */, 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */, 831188482113ADD100D77CB5 /* LDFlagValueConvertible.swift in Sources */, - 83D15233224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */, 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */, B4C9D4312489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 831188462113ADCA00D77CB5 /* LDUser.swift in Sources */, @@ -1300,7 +1270,6 @@ 832D689F224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 831EF34620655E730001C643 /* LDUser.swift in Sources */, 831EF34720655E730001C643 /* LDFlagValue.swift in Sources */, - 83D15232224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */, 830DB3B02239B54900D65D25 /* URLResponse.swift in Sources */, 831EF34820655E730001C643 /* LDFlagValueConvertible.swift in Sources */, 831EF34920655E730001C643 /* LDFlagBaseTypeConvertible.swift in Sources */, @@ -1321,7 +1290,6 @@ 831EF35520655E730001C643 /* FlagSynchronizer.swift in Sources */, B4C9D4302489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 831EF35620655E730001C643 /* FlagChangeNotifier.swift in Sources */, - 832D68A9224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */, 831EF35720655E730001C643 /* EventReporter.swift in Sources */, 831EF35820655E730001C643 /* FlagStore.swift in Sources */, 83883DD7220B68A000EEAB95 /* ErrorObserver.swift in Sources */, @@ -1351,7 +1319,6 @@ 831EF36820655E730001C643 /* ObjcLDUser.swift in Sources */, B4C9D43A2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, 831EF36A20655E730001C643 /* ObjcLDChangedFlag.swift in Sources */, - 83D1522D224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1391,11 +1358,9 @@ 832D689D224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 838838411F5EFADF0023D11B /* LDFlagValue.swift in Sources */, 838838431F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift in Sources */, - 83D1522B224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, 835E1D411F63450A00184DB4 /* ObjcLDUser.swift in Sources */, 8354AC612241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */, - 832D68A7224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */, 831D2AAF2061AAA000B4AC3C /* Thread.swift in Sources */, 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */, 8354EFE01F26380700C05156 /* LDClient.swift in Sources */, @@ -1411,7 +1376,6 @@ 83DDBEFC1FA24B2700E428B6 /* JSONSerialization.swift in Sources */, 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */, 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */, - 83D15230224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */, 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */, B4C9D42E2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 83B8C2471FE4071F0082B8A9 /* HTTPURLResponse.swift in Sources */, @@ -1433,7 +1397,6 @@ 831CE0661F853A1700A13A3A /* Match.swift in Sources */, 83DDBF001FA2589900E428B6 /* FlagStoreSpec.swift in Sources */, B4F689142497B2FC004D3CE0 /* DiagnosticEventSpec.swift in Sources */, - 83D15237225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift in Sources */, 83396BC91F7C3711000E256E /* DarklyServiceSpec.swift in Sources */, 83EF67931F9945E800403126 /* EventSpec.swift in Sources */, 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */, @@ -1465,14 +1428,12 @@ 8354AC6E22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift in Sources */, 83B9A080204F56F4000C3F17 /* FlagChangeObserverSpec.swift in Sources */, 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, - 83D15235225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift in Sources */, 831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */, 838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */, 83883DDF220B6D4B00EEAB95 /* ErrorObserverSpec.swift in Sources */, 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */, 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */, 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */, - 83D15239225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift in Sources */, 838F967A1FBA551A009CFC45 /* ClientServiceMockFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1514,12 +1475,10 @@ 832D689E224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 8354AC622241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83D9EC8C2062DEAB004D7FA6 /* HTTPHeaders.swift in Sources */, - 83D1522C224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, C443A40623145FED00145710 /* ConnectionInformationStore.swift in Sources */, 83D9EC8D2062DEAB004D7FA6 /* DarklyService.swift in Sources */, 83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */, 83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */, - 832D68A8224A4668005F052A /* DeprecatedCacheModelV2.swift in Sources */, 83D9EC902062DEAB004D7FA6 /* Dictionary.swift in Sources */, 831425B2206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */, @@ -1533,7 +1492,6 @@ 83D9EC962062DEAB004D7FA6 /* AnyComparer.swift in Sources */, 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */, 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */, - 83D15231224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */, 83D9EC982062DEAB004D7FA6 /* ObjcLDClient.swift in Sources */, B4C9D42F2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 83D9EC992062DEAB004D7FA6 /* ObjcLDConfig.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift index 911a3134..ea7f56de 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift @@ -36,7 +36,7 @@ extension DeprecatedCache { } enum DeprecatedCacheModel: String, CaseIterable { - case version5, version4, version3, version2 // version1 is not supported + case version5 // earlier versions are not supported } // updatedAt in cached data was used as the LDUser.lastUpdated, which is deprecated in the Swift SDK diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift deleted file mode 100644 index 16ac8c7c..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV2.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// DeprecatedCacheModelV2.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation - -// Cache model in use from 2.3.3 up to 2.11.0 -/* Cache model v2 schema -[: [ - “key: , //LDUserModel dictionary - “ip”: , - “country”: , - “email”: , - “name”: , - “firstName”: , - “lastName”: , - “avatar”: , - “custom”: [ - “device”: , - “os”: , - ...], - “anonymous”: , - “updatedAt: , - ”config”: [: ] - ] -] - */ -final class DeprecatedCacheModelV2: DeprecatedCache { - let keyedValueCache: KeyedValueCaching - let cachedDataKey = CacheConverter.CacheKeys.ldUserModelDictionary - - init(keyedValueCache: KeyedValueCaching) { - self.keyedValueCache = keyedValueCache - } - - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) { - guard let cachedUserDictionaries = keyedValueCache.dictionary(forKey: cachedDataKey), !cachedUserDictionaries.isEmpty, - let cachedUserDictionary = cachedUserDictionaries[userKey] as? [String: Any], !cachedUserDictionary.isEmpty, - let featureFlagDictionaries = cachedUserDictionary[LDUser.CodingKeys.config.rawValue] as? [LDFlagKey: Any] - else { - return (nil, nil) - } - let featureFlags = Dictionary(uniqueKeysWithValues: featureFlagDictionaries.compactMap { flagKey, value in - (flagKey, FeatureFlag(flagKey: flagKey, value: value)) - }) - return (featureFlags, cachedUserDictionary.lastUpdated) - } - - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan expirationDate: Date) -> [UserKey] { - cachedUserData.compactMap { userKey, userDictionary in - let lastUpdated = userDictionary.lastUpdated ?? Date.distantFuture - return lastUpdated.isExpired(expirationDate: expirationDate) ? userKey : nil - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift deleted file mode 100644 index 6afb78f6..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV3.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// DeprecatedCacheModelV3.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation - -// Cache model in use from 2.11.0 up to 2.13.0 -/* Cache model v3 schema -[: [ - “key: , //LDUserModel dictionary - “ip”: , - “country”: , - “email”: , - “name”: , - “firstName”: , - “lastName”: , - “avatar”: , - “custom”: [ - “device”: , - “os”: , - ...], - “anonymous”: , - “updatedAt: , - ”config”: [ //LDFlagConfigModel dictionary - : [ //LDFlagConfigValue dictionary - “value”: , - “version”: - ] - ], - “privateAttrs”: (from 2.10.0 forward) - ] -] - */ -final class DeprecatedCacheModelV3: DeprecatedCache { - let keyedValueCache: KeyedValueCaching - let cachedDataKey = CacheConverter.CacheKeys.ldUserModelDictionary - - init(keyedValueCache: KeyedValueCaching) { - self.keyedValueCache = keyedValueCache - } - - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) { - guard let cachedUserDictionaries = keyedValueCache.dictionary(forKey: cachedDataKey), !cachedUserDictionaries.isEmpty, - let cachedUserDictionary = cachedUserDictionaries[userKey] as? [String: Any], !cachedUserDictionary.isEmpty, - let featureFlagDictionaries = cachedUserDictionary[LDUser.CodingKeys.config.rawValue] as? [LDFlagKey: [String: Any]] - else { - return (nil, nil) - } - let featureFlags = Dictionary(uniqueKeysWithValues: featureFlagDictionaries.compactMap { flagKey, flagValueDictionary in - (flagKey, FeatureFlag(flagKey: flagKey, - value: flagValueDictionary.value, - version: flagValueDictionary.version)) - }) - return (featureFlags, cachedUserDictionary.lastUpdated) - } - - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan expirationDate: Date) -> [UserKey] { - cachedUserData.compactMap { userKey, userDictionary in - let lastUpdated = userDictionary.lastUpdated ?? Date.distantFuture - return lastUpdated.isExpired(expirationDate: expirationDate) ? userKey : nil - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift deleted file mode 100644 index a0f18d98..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV4.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// DeprecatedCacheModelV4.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation - -// Cache model in use from 2.13.0 up to 2.14.0 -/* Cache model v4 schema -[: [ - “key: , //LDUserModel dictionary - “ip”: , - “country”: , - “email”: , - “name”: , - “firstName”: , - “lastName”: , - “avatar”: , - “custom”: [ - “device”: , - “os”: , - ...], - “anonymous”: , - “updatedAt: , - ”config”: [ //LDFlagConfigModel dictionary - : [ //LDFlagConfigValue dictionary - “value”: , - “version”: , - “flagVersion”: , - “variation”: , - “trackEvents”: , - “debugEventsUntilDate”: - ] - ], - “privateAttrs”: - ] -] - */ -final class DeprecatedCacheModelV4: DeprecatedCache { - let keyedValueCache: KeyedValueCaching - let cachedDataKey = CacheConverter.CacheKeys.ldUserModelDictionary - - init(keyedValueCache: KeyedValueCaching) { - self.keyedValueCache = keyedValueCache - } - - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) { - guard let cachedUserDictionaries = keyedValueCache.dictionary(forKey: cachedDataKey), !cachedUserDictionaries.isEmpty, - let cachedUserDictionary = cachedUserDictionaries[userKey] as? [String: Any], !cachedUserDictionary.isEmpty, - let featureFlagDictionaries = cachedUserDictionary[LDUser.CodingKeys.config.rawValue] as? [LDFlagKey: [String: Any]] - else { - return (nil, nil) - } - let featureFlags = Dictionary(uniqueKeysWithValues: featureFlagDictionaries.compactMap { flagKey, flagValueDictionary in - (flagKey, FeatureFlag(flagKey: flagKey, - value: flagValueDictionary.value, - variation: flagValueDictionary.variation, - version: flagValueDictionary.version, - flagVersion: flagValueDictionary.flagVersion, - trackEvents: flagValueDictionary.trackEvents, - debugEventsUntilDate: Date(millisSince1970: flagValueDictionary.debugEventsUntilDate))) - }) - return (featureFlags, cachedUserDictionary.lastUpdated) - } - - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan expirationDate: Date) -> [UserKey] { - cachedUserData.compactMap { userKey, userDictionary in - let lastUpdated = userDictionary.lastUpdated ?? Date.distantFuture - return lastUpdated.isExpired(expirationDate: expirationDate) ? userKey : nil - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index 30e749fc..b5d82812 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -48,9 +48,6 @@ final class ClientServiceFactory: ClientServiceCreating { func makeDeprecatedCacheModel(_ model: DeprecatedCacheModel) -> DeprecatedCache { switch model { - case .version2: return DeprecatedCacheModelV2(keyedValueCache: makeKeyedValueCache()) - case .version3: return DeprecatedCacheModelV3(keyedValueCache: makeKeyedValueCache()) - case .version4: return DeprecatedCacheModelV4(keyedValueCache: makeKeyedValueCache()) case .version5: return DeprecatedCacheModelV5(keyedValueCache: makeKeyedValueCache()) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift index 8fc6e45d..b6be7e76 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift @@ -69,7 +69,7 @@ final class CacheConverterSpec: QuickSpec { } private func convertCacheDataSpec() { - let cacheCases: [DeprecatedCacheModel?] = [.version5, .version4, .version3, .version2, nil] // Nil for no deprecated cache + let cacheCases: [DeprecatedCacheModel?] = [.version5, nil] // Nil for no deprecated cache var testContext: TestContext! describe("convertCacheData") { afterEach { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift index ae48c41d..6fdc1092 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift @@ -5,7 +5,6 @@ import Nimble protocol CacheModelTestInterface { var cacheKey: String { get } - var supportsMultiEnv: Bool { get } func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] @@ -96,13 +95,11 @@ class DeprecatedCacheModelSpec { } } } - if self.cacheModelInterface.supportsMultiEnv { - it("returns nil for uncached environment") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - cachedData = testContext.deprecatedCache.retrieveFlags(for: testContext.users.first!.key, and: UUID().uuidString) - expect(cachedData.featureFlags).to(beNil()) - expect(cachedData.lastUpdated).to(beNil()) - } + it("returns nil for uncached environment") { + testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) + cachedData = testContext.deprecatedCache.retrieveFlags(for: testContext.users.first!.key, and: UUID().uuidString) + expect(cachedData.featureFlags).to(beNil()) + expect(cachedData.lastUpdated).to(beNil()) } it("returns nil for uncached user") { testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV2Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV2Spec.swift deleted file mode 100644 index 3b705d8c..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV2Spec.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// DeprecatedCacheModelV2Spec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DeprecatedCacheModelV2Spec: QuickSpec, CacheModelTestInterface { - let cacheKey = CacheConverter.CacheKeys.ldUserModelDictionary - var supportsMultiEnv = false - - func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache { - DeprecatedCacheModelV2(keyedValueCache: keyedValueCache) - } - - func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? { - guard let mobileKey = mobileKeys.first, !users.isEmpty - else { return nil } - - return Dictionary(uniqueKeysWithValues: users.map { user in - let featureFlags = userEnvironmentsCollection[user.key]?.environmentFlags[mobileKey]?.featureFlags - let lastUpdated = userEnvironmentsCollection[user.key]?.lastUpdated - return (user.key, user.modelV2DictionaryValue(including: featureFlags!, using: lastUpdated)) - }) - } - - func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] { - originalFlags.filter { $0.value.value != nil }.compactMapValues { orig in - FeatureFlag(flagKey: orig.flagKey, value: orig.value) - } - } - - override func spec() { - DeprecatedCacheModelSpec(cacheModelInterface: self).spec() - } -} - -// MARK: Dictionary value to cache - -extension LDUser { - func modelV2DictionaryValue(including featureFlags: [LDFlagKey: FeatureFlag], using lastUpdated: Date?) -> [String: Any] { - var userDictionary = dictionaryValueWithAllAttributes() - userDictionary.removeValue(forKey: LDUser.CodingKeys.privateAttributes.rawValue) - userDictionary.setLastUpdated(lastUpdated) - userDictionary[LDUser.CodingKeys.config.rawValue] = featureFlags.allFlagValues.withNullValuesRemoved - - return userDictionary - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV3Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV3Spec.swift deleted file mode 100644 index 0d0c5fdf..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV3Spec.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// DeprecatedCacheModelV3Spec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DeprecatedCacheModelV3Spec: QuickSpec, CacheModelTestInterface { - - let cacheKey = CacheConverter.CacheKeys.ldUserModelDictionary - let supportsMultiEnv = false - - func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache { - DeprecatedCacheModelV3(keyedValueCache: keyedValueCache) - } - - func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? { - guard let mobileKey = mobileKeys.first, !users.isEmpty - else { return nil } - - return Dictionary(uniqueKeysWithValues: users.map { user in - let featureFlags = userEnvironmentsCollection[user.key]?.environmentFlags[mobileKey]?.featureFlags - let lastUpdated = userEnvironmentsCollection[user.key]?.lastUpdated - return (user.key, user.modelV3DictionaryValue(including: featureFlags!, using: lastUpdated)) - }) - } - - func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] { - originalFlags.filter { $0.value.value != nil }.compactMapValues { orig in - FeatureFlag(flagKey: orig.flagKey, value: orig.value, version: orig.version) - } - } - - override func spec() { - DeprecatedCacheModelSpec(cacheModelInterface: self).spec() - } -} - -// MARK: Dictionary value to cache - -extension LDUser { - func modelV3DictionaryValue(including featureFlags: [LDFlagKey: FeatureFlag], using lastUpdated: Date?) -> [String: Any] { - var userDictionary = dictionaryValueWithAllAttributes() - userDictionary.setLastUpdated(lastUpdated) - userDictionary[LDUser.CodingKeys.config.rawValue] = featureFlags.compactMapValues { $0.modelV3dictionaryValue } - - return userDictionary - } -} - -extension FeatureFlag { -/* - [“version”: , - “value”: ] -*/ - var modelV3dictionaryValue: [String: Any]? { - guard value != nil - else { return nil } - var flagDictionary = dictionaryValue - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.flagKey.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.variation.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.flagVersion.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.trackEvents.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.debugEventsUntilDate.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.reason.rawValue) - return flagDictionary - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV4Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV4Spec.swift deleted file mode 100644 index d3461b69..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV4Spec.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// DeprecatedCacheModelV4Spec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DeprecatedCacheModelV4Spec: QuickSpec, CacheModelTestInterface { - - let cacheKey = CacheConverter.CacheKeys.ldUserModelDictionary - let supportsMultiEnv = false - - func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache { - DeprecatedCacheModelV4(keyedValueCache: keyedValueCache) - } - - func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? { - guard let mobileKey = mobileKeys.first, !users.isEmpty - else { return nil } - - return Dictionary(uniqueKeysWithValues: users.map { user in - let featureFlags = userEnvironmentsCollection[user.key]?.environmentFlags[mobileKey]?.featureFlags - let lastUpdated = userEnvironmentsCollection[user.key]?.lastUpdated - return (user.key, user.modelV4DictionaryValue(including: featureFlags!, using: lastUpdated)) - }) - } - - func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] { - originalFlags.filter { $0.value.value != nil }.compactMapValues { orig in - FeatureFlag(flagKey: orig.flagKey, - value: orig.value, - variation: orig.variation, - version: orig.version, - flagVersion: orig.flagVersion, - trackEvents: orig.trackEvents, - debugEventsUntilDate: orig.debugEventsUntilDate) - } - } - - override func spec() { - DeprecatedCacheModelSpec(cacheModelInterface: self).spec() - } -} - -// MARK: Dictionary value to cache - -extension LDUser { - func modelV4DictionaryValue(including featureFlags: [LDFlagKey: FeatureFlag], using lastUpdated: Date?) -> [String: Any] { - var userDictionary = dictionaryValueWithAllAttributes() - userDictionary.setLastUpdated(lastUpdated) - userDictionary[LDUser.CodingKeys.config.rawValue] = featureFlags.compactMapValues { $0.modelV4dictionaryValue } - - return userDictionary - } -} - -extension FeatureFlag { -/* - [“version”: , - “flagVersion”: , - “variation”: , - “value”: , - “trackEvents”: , - “debugEventsUntilDate”: ] -*/ - var modelV4dictionaryValue: [String: Any]? { - guard value != nil - else { return nil } - var flagDictionary = dictionaryValue - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.flagKey.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.reason.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.trackReason.rawValue) - return flagDictionary - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift index a274c01d..5424dc5d 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift @@ -12,7 +12,6 @@ import Nimble final class DeprecatedCacheModelV5Spec: QuickSpec, CacheModelTestInterface { let cacheKey = DeprecatedCacheModelV5.CacheKeys.userEnvironments - let supportsMultiEnv = true func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache { DeprecatedCacheModelV5(keyedValueCache: keyedValueCache) From dcce03ed9043228fed1693d42a649e05eb151319 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 4 Feb 2022 15:05:42 -0600 Subject: [PATCH 23/50] Use CircleCI macOS Gen2 resource class. (#173) --- .circleci/config.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9b7b0e10..da14498c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,6 +20,7 @@ jobs: macos: xcode: <> + resource_class: macos.x86.medium.gen2 steps: - checkout @@ -134,7 +135,7 @@ workflows: run-lint: true - build: name: Xcode 12.5 - Swift 5.4 - xcode-version: '12.5.0' + xcode-version: '12.5.1' ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=14.5' - build: name: Xcode 12.0 - Swift 5.3 @@ -142,6 +143,6 @@ workflows: ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=14.0' ssh-fix: true - build: - name: Xcode 11.4 - Swift 5.2 - xcode-version: '11.4.1' - ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=12.2' + name: Xcode 11.7 - Swift 5.2 + xcode-version: '11.7.0' + ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=12.4' From e4073adee6db8bd48dda663ec13fbe9c1a1da3d6 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Mon, 7 Mar 2022 09:33:01 -0600 Subject: [PATCH 24/50] Remove ErrorNotifier and file headers. --- LaunchDarkly.xcodeproj/project.pbxproj | 38 ++--------- .../GeneratedCode/mocks.generated.swift | 31 --------- .../LaunchDarkly/Extensions/AnyComparer.swift | 7 -- .../LaunchDarkly/Extensions/Data.swift | 7 -- .../LaunchDarkly/Extensions/Date.swift | 7 -- .../Extensions/DateFormatter.swift | 7 -- .../LaunchDarkly/Extensions/Dictionary.swift | 7 -- .../Extensions/JSONSerialization.swift | 7 -- .../LaunchDarkly/Extensions/Thread.swift | 7 -- LaunchDarkly/LaunchDarkly/LDClient.swift | 12 ---- LaunchDarkly/LaunchDarkly/LDCommon.swift | 7 -- .../Cache/CacheableEnvironmentFlags.swift | 7 -- .../Cache/CacheableUserEnvironmentFlags.swift | 7 -- .../Models/ConnectionInformation.swift | 7 -- .../LaunchDarkly/Models/DiagnosticEvent.swift | 7 -- .../LaunchDarkly/Models/ErrorObserver.swift | 18 ----- LaunchDarkly/LaunchDarkly/Models/Event.swift | 7 -- .../Models/FeatureFlag/FeatureFlag.swift | 7 -- .../ConnectionModeChangeObserver.swift | 7 -- .../FlagChange/FlagChangeObserver.swift | 7 -- .../FlagChange/FlagsUnchangedObserver.swift | 7 -- .../FlagChange/LDChangedFlag.swift | 7 -- .../FeatureFlag/FlagRequestTracker.swift | 7 -- .../FeatureFlag/FlagValue/LDFlagValue.swift | 7 -- .../FlagValue/LDFlagValueConvertible.swift | 7 -- .../FeatureFlag/LDEvaluationDetail.swift | 7 -- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 6 -- .../Networking/DarklyService.swift | 7 -- .../LaunchDarkly/Networking/HTTPHeaders.swift | 7 -- .../Networking/HTTPURLRequest.swift | 7 -- .../Networking/HTTPURLResponse.swift | 7 -- .../LaunchDarkly/Networking/URLResponse.swift | 7 -- .../ObjectiveC/ObjcLDChangedFlag.swift | 7 -- .../ObjectiveC/ObjcLDClient.swift | 32 --------- .../ObjectiveC/ObjcLDConfig.swift | 7 -- .../ObjectiveC/ObjcLDEvaluationDetail.swift | 7 -- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 7 -- .../ServiceObjects/Cache/CacheConverter.swift | 7 -- .../Cache/ConnectionInformationStore.swift | 7 -- .../Cache/DeprecatedCache.swift | 7 -- .../Cache/DeprecatedCacheModelV5.swift | 7 -- .../Cache/DiagnosticCache.swift | 7 -- .../Cache/KeyedValueCache.swift | 7 -- .../Cache/UserEnvironmentFlagCache.swift | 7 -- .../ServiceObjects/ClientServiceFactory.swift | 12 ---- .../ServiceObjects/DiagnosticReporter.swift | 7 -- .../ServiceObjects/EnvironmentReporter.swift | 7 -- .../ServiceObjects/ErrorNotifier.swift | 36 ---------- .../ServiceObjects/EventReporter.swift | 7 -- .../ServiceObjects/FlagChangeNotifier.swift | 7 -- .../ServiceObjects/FlagStore.swift | 7 -- .../ServiceObjects/FlagSynchronizer.swift | 7 -- .../LaunchDarkly/ServiceObjects/Log.swift | 7 -- .../ServiceObjects/NetworkReporter.swift | 7 -- .../LaunchDarkly/Support/LaunchDarkly.h | 7 -- .../Extensions/AnyComparerSpec.swift | 7 -- .../Extensions/DictionarySpec.swift | 7 -- .../Extensions/ThreadSpec.swift | 7 -- .../LaunchDarklyTests/LDClientSpec.swift | 65 ++++++------------ .../LaunchDarklyTests/Matcher/Match.swift | 7 -- .../Mocks/ClientServiceMockFactory.swift | 11 ---- .../Mocks/DarklyServiceMock.swift | 7 -- .../Mocks/DeprecatedCacheMock.swift | 6 -- .../Mocks/EnvironmentReportingMock.swift | 7 -- .../Mocks/FlagMaintainingMock.swift | 7 -- .../Mocks/LDConfigStub.swift | 7 -- .../Mocks/LDEventSourceMock.swift | 7 -- .../LaunchDarklyTests/Mocks/LDUserStub.swift | 7 -- .../Cache/CacheableEnvironmentFlagsSpec.swift | 7 -- .../CacheableUserEnvironmentFlagsSpec.swift | 7 -- .../Models/DiagnosticEventSpec.swift | 7 -- .../Models/ErrorObserverSpec.swift | 36 ---------- .../LaunchDarklyTests/Models/EventSpec.swift | 7 -- .../Models/FeatureFlag/FeatureFlagSpec.swift | 7 -- .../FlagChange/FlagChangeObserverSpec.swift | 7 -- .../FlagRequestTracking/FlagCounterSpec.swift | 7 -- .../FlagRequestTrackerSpec.swift | 7 -- .../Models/LDConfigSpec.swift | 7 -- .../Models/User/LDUserSpec.swift | 7 -- .../Networking/DarklyServiceSpec.swift | 7 -- .../Networking/HTTPHeadersSpec.swift | 7 -- .../Networking/HTTPURLResponse.swift | 7 -- .../Networking/URLRequestSpec.swift | 7 -- .../Cache/CacheConverterSpec.swift | 7 -- .../Cache/DeprecatedCacheModelV5Spec.swift | 7 -- .../Cache/DiagnosticCacheSpec.swift | 7 -- .../Cache/KeyedValueCacheSpec.swift | 7 -- .../Cache/UserEnvironmentFlagCacheSpec.swift | 7 -- .../EnvironmentReporterSpec.swift | 7 -- .../ServiceObjects/ErrorNotifierSpec.swift | 66 ------------------- .../ServiceObjects/EventReporterSpec.swift | 7 -- .../FlagChangeNotifierSpec.swift | 7 -- .../ServiceObjects/FlagStoreSpec.swift | 7 -- .../ServiceObjects/FlagSynchronizerSpec.swift | 7 -- .../SynchronizingErrorSpec.swift | 7 -- LaunchDarkly/LaunchDarklyTests/TestUtil.swift | 7 -- 96 files changed, 24 insertions(+), 926 deletions(-) delete mode 100644 LaunchDarkly/LaunchDarkly/Models/ErrorObserver.swift delete mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/Models/ErrorObserverSpec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/ErrorNotifierSpec.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 34c0ecd3..67d96487 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -108,7 +108,6 @@ 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68AB224B3321005F052A /* CacheConverterSpec.swift */; }; 832EA061203D03B700A93C0E /* AnyComparerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832EA060203D03B700A93C0E /* AnyComparerSpec.swift */; }; 8335299E1FC37727001166F8 /* FlagMaintainingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */; }; - 833631CB221B5DFA00BA53EE /* ErrorNotifierSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833631CA221B5DFA00BA53EE /* ErrorNotifierSpec.swift */; }; 83383A5120460DD30024D975 /* SynchronizingErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83383A5020460DD30024D975 /* SynchronizingErrorSpec.swift */; }; 83396BC91F7C3711000E256E /* DarklyServiceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83396BC81F7C3711000E256E /* DarklyServiceSpec.swift */; }; 83411A5F1FABDA8700E5CF39 /* mocks.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83411A5D1FABDA8700E5CF39 /* mocks.generated.swift */; }; @@ -157,15 +156,6 @@ 837EF3742059C237009D628A /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837EF3732059C237009D628A /* Log.swift */; }; 838838411F5EFADF0023D11B /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; 838838451F5EFBAF0023D11B /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; - 83883DD5220B68A000EEAB95 /* ErrorObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */; }; - 83883DD6220B68A000EEAB95 /* ErrorObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */; }; - 83883DD7220B68A000EEAB95 /* ErrorObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */; }; - 83883DD8220B68A000EEAB95 /* ErrorObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */; }; - 83883DDA220B6A9A00EEAB95 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DD9220B6A9A00EEAB95 /* ErrorNotifier.swift */; }; - 83883DDB220B6A9A00EEAB95 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DD9220B6A9A00EEAB95 /* ErrorNotifier.swift */; }; - 83883DDC220B6A9A00EEAB95 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DD9220B6A9A00EEAB95 /* ErrorNotifier.swift */; }; - 83883DDD220B6A9A00EEAB95 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DD9220B6A9A00EEAB95 /* ErrorNotifier.swift */; }; - 83883DDF220B6D4B00EEAB95 /* ErrorObserverSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83883DDE220B6D4B00EEAB95 /* ErrorObserverSpec.swift */; }; 838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D8B761F72A4B400ED65E8 /* FlagSynchronizerSpec.swift */; }; 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838F96731FB9F024009CFC45 /* LDClientSpec.swift */; }; 838F96781FBA504A009CFC45 /* ClientServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838F96771FBA504A009CFC45 /* ClientServiceFactory.swift */; }; @@ -379,7 +369,6 @@ 832D68AB224B3321005F052A /* CacheConverterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverterSpec.swift; sourceTree = ""; }; 832EA060203D03B700A93C0E /* AnyComparerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyComparerSpec.swift; sourceTree = ""; }; 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagMaintainingMock.swift; sourceTree = ""; }; - 833631CA221B5DFA00BA53EE /* ErrorNotifierSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifierSpec.swift; sourceTree = ""; }; 83383A5020460DD30024D975 /* SynchronizingErrorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizingErrorSpec.swift; sourceTree = ""; }; 83396BC81F7C3711000E256E /* DarklyServiceSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarklyServiceSpec.swift; sourceTree = ""; }; 83411A5D1FABDA8700E5CF39 /* mocks.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = mocks.generated.swift; path = LaunchDarkly/GeneratedCode/mocks.generated.swift; sourceTree = SOURCE_ROOT; }; @@ -412,9 +401,6 @@ 837EF3732059C237009D628A /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 838838401F5EFADF0023D11B /* LDFlagValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDFlagValue.swift; sourceTree = ""; }; 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDFlagValueConvertible.swift; sourceTree = ""; }; - 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorObserver.swift; sourceTree = ""; }; - 83883DD9220B6A9A00EEAB95 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = ""; }; - 83883DDE220B6D4B00EEAB95 /* ErrorObserverSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorObserverSpec.swift; sourceTree = ""; }; 838F96731FB9F024009CFC45 /* LDClientSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDClientSpec.swift; sourceTree = ""; }; 838F96771FBA504A009CFC45 /* ClientServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientServiceFactory.swift; sourceTree = ""; }; 838F96791FBA551A009CFC45 /* ClientServiceMockFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientServiceMockFactory.swift; sourceTree = ""; }; @@ -541,7 +527,6 @@ children = ( B46F344025E6DB7D0078D45F /* DiagnosticReporterSpec.swift */, 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */, - 833631CA221B5DFA00BA53EE /* ErrorNotifierSpec.swift */, 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */, 83B8C2441FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift */, 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */, @@ -682,7 +667,6 @@ 8354AC5F224150C300CDE602 /* Cache */, C408884823033B7500420721 /* ConnectionInformation.swift */, B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */, - 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */, 8354EFDE1F26380700C05156 /* Event.swift */, 83EBCB9D20D9A0A1003A7142 /* FeatureFlag */, 8354EFDD1F26380700C05156 /* LDConfig.swift */, @@ -825,7 +809,6 @@ 83EBCBA620D9A23E003A7142 /* User */, 83EBCBA720D9A251003A7142 /* FeatureFlag */, 83EF67921F9945E800403126 /* EventSpec.swift */, - 83883DDE220B6D4B00EEAB95 /* ErrorObserverSpec.swift */, 8354AC672241586D00CDE602 /* Cache */, B4F689132497B2FC004D3CE0 /* DiagnosticEventSpec.swift */, ); @@ -840,7 +823,6 @@ 83B1D7C82073F354006D1B1C /* CwlSysctl.swift */, B4C9D4372489E20A004A9B03 /* DiagnosticReporter.swift */, 831425B0206B030100F2EF36 /* EnvironmentReporter.swift */, - 83883DD9220B6A9A00EEAB95 /* ErrorNotifier.swift */, 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */, 8358F25F1F476AD800ECE1AF /* FlagChangeNotifier.swift */, 831D8B731F72994600ED65E8 /* FlagStore.swift */, @@ -1134,7 +1116,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run realm/SwiftLint\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; + shellScript = "# Adds support for Apple Silicon brew directory\nexport PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run realm/SwiftLint\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; }; 830C2AC2207416A5001D645D /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1147,7 +1129,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run realm/SwiftLint\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; + shellScript = "# Adds support for Apple Silicon brew directory\nexport PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run realm/SwiftLint\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; }; 833FD9F821C01333001F80EB /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1164,7 +1146,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run realm/SwiftLint\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; + shellScript = "# Adds support for Apple Silicon brew directory\nexport PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run realm/SwiftLint\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; }; 83411A561FABCA2200E5CF39 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1177,7 +1159,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run krzysztofzablocki/Sourcery\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; + shellScript = "# Adds support for Apple Silicon brew directory\nexport PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run krzysztofzablocki/Sourcery\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; }; 835E1CFE1F61AC0600184DB4 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1190,7 +1172,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run realm/SwiftLint\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; + shellScript = "# Adds support for Apple Silicon brew directory\nexport PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run realm/SwiftLint\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -1203,12 +1185,10 @@ 8311886A2113AE5D00D77CB5 /* ObjcLDUser.swift in Sources */, 8370DF6F225E40B800F84810 /* DeprecatedCache.swift in Sources */, 831188502113ADEF00D77CB5 /* EnvironmentReporter.swift in Sources */, - 83883DDD220B6A9A00EEAB95 /* ErrorNotifier.swift in Sources */, 831188682113AE5600D77CB5 /* ObjcLDClient.swift in Sources */, 831188572113AE0B00D77CB5 /* FlagChangeNotifier.swift in Sources */, 8311884D2113ADE200D77CB5 /* FlagsUnchangedObserver.swift in Sources */, 8311885F2113AE2D00D77CB5 /* HTTPURLRequest.swift in Sources */, - 83883DD8220B68A000EEAB95 /* ErrorObserver.swift in Sources */, B4C9D4362489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 831188452113ADC500D77CB5 /* LDClient.swift in Sources */, 831188522113ADF700D77CB5 /* KeyedValueCache.swift in Sources */, @@ -1291,10 +1271,8 @@ 831EF35620655E730001C643 /* FlagChangeNotifier.swift in Sources */, 831EF35720655E730001C643 /* EventReporter.swift in Sources */, 831EF35820655E730001C643 /* FlagStore.swift in Sources */, - 83883DD7220B68A000EEAB95 /* ErrorObserver.swift in Sources */, 831EF35920655E730001C643 /* Log.swift in Sources */, 831EF35A20655E730001C643 /* HTTPHeaders.swift in Sources */, - 83883DDC220B6A9A00EEAB95 /* ErrorNotifier.swift in Sources */, 831EF35B20655E730001C643 /* DarklyService.swift in Sources */, 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */, 8354AC6B22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, @@ -1330,10 +1308,8 @@ 835E1D3F1F63450A00184DB4 /* ObjcLDClient.swift in Sources */, 8370DF6C225E40B800F84810 /* DeprecatedCache.swift in Sources */, 83EBCBB320DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, - 83883DDA220B6A9A00EEAB95 /* ErrorNotifier.swift in Sources */, 837EF3742059C237009D628A /* Log.swift in Sources */, 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */, - 83883DD5220B68A000EEAB95 /* ErrorObserver.swift in Sources */, 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */, B4C9D4332489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 8354EFE51F263DAC00C05156 /* FeatureFlag.swift in Sources */, @@ -1390,7 +1366,6 @@ buildActionMask = 2147483647; files = ( 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */, - 833631CB221B5DFA00BA53EE /* ErrorNotifierSpec.swift in Sources */, 8392FFA32033565700320914 /* HTTPURLResponse.swift in Sources */, 83411A5F1FABDA8700E5CF39 /* mocks.generated.swift in Sources */, 83D5597E1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift in Sources */, @@ -1430,7 +1405,6 @@ 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, 831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */, 838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */, - 83883DDF220B6D4B00EEAB95 /* ErrorObserverSpec.swift in Sources */, 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */, 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */, 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */, @@ -1446,12 +1420,10 @@ 83D9EC762062DEAB004D7FA6 /* LDConfig.swift in Sources */, 8370DF6D225E40B800F84810 /* DeprecatedCache.swift in Sources */, 83EBCBB420DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, - 83883DDB220B6A9A00EEAB95 /* ErrorNotifier.swift in Sources */, 83D9EC772062DEAB004D7FA6 /* LDClient.swift in Sources */, 83D9EC782062DEAB004D7FA6 /* LDUser.swift in Sources */, 83D9EC792062DEAB004D7FA6 /* LDFlagValue.swift in Sources */, 83D9EC7A2062DEAB004D7FA6 /* LDFlagValueConvertible.swift in Sources */, - 83883DD6220B68A000EEAB95 /* ErrorObserver.swift in Sources */, B4C9D4342489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */, diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index 7a9069f3..c5f448a1 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -210,37 +210,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } -// MARK: - ErrorNotifyingMock -final class ErrorNotifyingMock: ErrorNotifying { - - var addErrorObserverCallCount = 0 - var addErrorObserverCallback: (() -> Void)? - var addErrorObserverReceivedObserver: ErrorObserver? - func addErrorObserver(_ observer: ErrorObserver) { - addErrorObserverCallCount += 1 - addErrorObserverReceivedObserver = observer - addErrorObserverCallback?() - } - - var removeObserversCallCount = 0 - var removeObserversCallback: (() -> Void)? - var removeObserversReceivedOwner: LDObserverOwner? - func removeObservers(for owner: LDObserverOwner) { - removeObserversCallCount += 1 - removeObserversReceivedOwner = owner - removeObserversCallback?() - } - - var notifyObserversCallCount = 0 - var notifyObserversCallback: (() -> Void)? - var notifyObserversReceivedError: Error? - func notifyObservers(of error: Error) { - notifyObserversCallCount += 1 - notifyObserversReceivedError = error - notifyObserversCallback?() - } -} - // MARK: - EventReportingMock final class EventReportingMock: EventReporting { diff --git a/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift b/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift index ac863ba1..bca2a855 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift @@ -1,10 +1,3 @@ -// -// Any.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation struct AnyComparer { diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Data.swift b/LaunchDarkly/LaunchDarkly/Extensions/Data.swift index 506a922d..fe3e8a32 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/Data.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/Data.swift @@ -1,10 +1,3 @@ -// -// Data.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation extension Data { diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Date.swift b/LaunchDarkly/LaunchDarkly/Extensions/Date.swift index b1e84ac7..8b04c280 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/Date.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/Date.swift @@ -1,10 +1,3 @@ -// -// Date.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation extension Date { diff --git a/LaunchDarkly/LaunchDarkly/Extensions/DateFormatter.swift b/LaunchDarkly/LaunchDarkly/Extensions/DateFormatter.swift index 89160cc4..4112f47c 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/DateFormatter.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/DateFormatter.swift @@ -1,10 +1,3 @@ -// -// DateFormatter.swift -// LaunchDarkly -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation extension DateFormatter { diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift b/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift index 5ff12c8c..be516003 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift @@ -1,10 +1,3 @@ -// -// Dictionary.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation extension Dictionary where Key == String { diff --git a/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift b/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift index a1289e1b..aad475c1 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift @@ -1,10 +1,3 @@ -// -// JSONSerialization.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation extension JSONSerialization { diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Thread.swift b/LaunchDarkly/LaunchDarkly/Extensions/Thread.swift index 7742dca5..4f958c14 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/Thread.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/Thread.swift @@ -1,10 +1,3 @@ -// -// Thread.swift -// LaunchDarkly -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation extension Thread { diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 66537ced..072c2d95 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -482,14 +482,6 @@ public class LDClient { public func stopObserving(owner: LDObserverOwner) { Log.debug(typeName(and: #function) + " owner: \(String(describing: owner))") flagChangeNotifier.removeObserver(owner: owner) - errorNotifier.removeObservers(for: owner) - } - - private(set) var errorNotifier: ErrorNotifying - - public func observeError(owner: LDObserverOwner, handler: @escaping LDErrorHandler) { - Log.debug(typeName(and: #function) + " owner: \(String(describing: owner))") - errorNotifier.addErrorObserver(ErrorObserver(owner: owner, errorHandler: handler)) } private func onFlagSyncComplete(result: FlagSyncResult) { @@ -526,9 +518,6 @@ public class LDClient { internalSetOnline(false) } connectionInformation = ConnectionInformation.synchronizingErrorCheck(synchronizingError: synchronizingError, connectionInformation: connectionInformation) - DispatchQueue.main.async { - self.errorNotifier.notifyObservers(of: synchronizingError) - } } private func updateCacheAndReportChanges(user: LDUser, @@ -766,7 +755,6 @@ public class LDClient { service = self.serviceFactory.makeDarklyServiceProvider(config: config, user: user) diagnosticReporter = self.serviceFactory.makeDiagnosticReporter(service: service) eventReporter = self.serviceFactory.makeEventReporter(service: service) - errorNotifier = self.serviceFactory.makeErrorNotifier() connectionInformation = self.serviceFactory.makeConnectionInformation() flagSynchronizer = self.serviceFactory.makeFlagSynchronizer(streamingMode: config.allowStreamingMode ? config.streamingMode : .polling, pollingInterval: config.flagPollingInterval(runMode: runMode), diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index 094284f1..054c8d41 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -1,10 +1,3 @@ -// -// LDCommon.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation /// The feature flag key is a String. This typealias helps define where the SDK expects the string to be a feature flag key. diff --git a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift index 0f924177..3aaa3923 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift @@ -1,10 +1,3 @@ -// -// CacheableEnvironmentFlags.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation // Data structure used to cache feature flags for a specific user from a specific environment diff --git a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift index 56cb65f9..d6a3d0d7 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift @@ -1,10 +1,3 @@ -// -// CacheableUserEnvironments.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation // Data structure used to cache feature flags for a specific user for multiple environments diff --git a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift index 415285b7..c80981b6 100644 --- a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift +++ b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift @@ -1,10 +1,3 @@ -// -// ConnectionInformation.swift -// LaunchDarkly_iOS -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation public struct ConnectionInformation: Codable, CustomStringConvertible { diff --git a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift index 657fb340..3df691e7 100644 --- a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift +++ b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift @@ -1,10 +1,3 @@ -// -// DiagnosticEvent.swift -// LaunchDarkly -// -// Copyright © 2020 Catamorphic Co. All rights reserved. -// - import Foundation enum DiagnosticKind: String, Codable { diff --git a/LaunchDarkly/LaunchDarkly/Models/ErrorObserver.swift b/LaunchDarkly/LaunchDarkly/Models/ErrorObserver.swift deleted file mode 100644 index ff475d93..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/ErrorObserver.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ErrorObserver.swift -// Darkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation - -struct ErrorObserver { - weak var owner: LDObserverOwner? - let errorHandler: LDErrorHandler - - init(owner: LDObserverOwner, errorHandler: @escaping LDErrorHandler) { - self.owner = owner - self.errorHandler = errorHandler - } -} diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index 3168e45c..eb0f3262 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -1,10 +1,3 @@ -// -// Event.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation func userType(_ user: LDUser) -> String { diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift index cf1f07b8..8f29ba57 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift @@ -1,10 +1,3 @@ -// -// FeatureFlag.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation struct FeatureFlag { diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift index 5d5a853d..1fb0e44d 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift @@ -1,10 +1,3 @@ -// -// ConnectionModeChangeObserver.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation struct ConnectionModeChangedObserver { diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagChangeObserver.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagChangeObserver.swift index f4304d2d..6ba669fa 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagChangeObserver.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagChangeObserver.swift @@ -1,10 +1,3 @@ -// -// LDFlagObserver.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation struct FlagChangeObserver { diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagsUnchangedObserver.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagsUnchangedObserver.swift index f7806264..495dfeeb 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagsUnchangedObserver.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagsUnchangedObserver.swift @@ -1,10 +1,3 @@ -// -// FlagsUnchangedObserver.swift -// LaunchDarkly -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation struct FlagsUnchangedObserver { diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift index d07afa87..581c0790 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift @@ -1,10 +1,3 @@ -// -// LDChangedFlag.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation /** diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift index 191da4a7..53182997 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift @@ -1,10 +1,3 @@ -// -// FlagRequestTracker.swift -// LaunchDarkly -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation struct FlagRequestTracker { diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift index 138858c2..6c888ba7 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift @@ -1,10 +1,3 @@ -// -// LDFlagValue.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation /// Defines the types and values of a feature flag. The SDK limits feature flags to these types by use of the `LDFlagValueConvertible` protocol, which uses this type. Client app developers should not construct an LDFlagValue. diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift index fb778d45..2f3cf1cb 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift @@ -1,10 +1,3 @@ -// -// LDFlagValueConvertible.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation /// Protocol used by the SDK to limit feature flag types to those representable on LaunchDarkly servers. Client app developers should not need to use this protocol. The protocol is public because `LDClient.variation(forKey:defaultValue:)` and `LDClient.variationDetail(forKey:defaultValue:)` return a type that conforms to this protocol. See `LDFlagValue` for types that LaunchDarkly feature flags can take. diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift index 749a971d..ceb12c10 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift @@ -1,10 +1,3 @@ -// -// LDEvaluationDetail.swift -// LaunchDarkly_iOS -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation /** diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 4b5b9ddc..85723156 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -1,9 +1,3 @@ -// -// LDUser.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// import Foundation typealias UserKey = String // use for identifying semantics for strings, particularly in dictionaries diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 31048af9..62e48861 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -1,10 +1,3 @@ -// -// DarklyService.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import LDSwiftEventSource diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift index a257488c..d37fadee 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift @@ -1,10 +1,3 @@ -// -// HTTPHeaders.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation struct HTTPHeaders { diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPURLRequest.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPURLRequest.swift index 7d91fbfb..4ece8e2b 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPURLRequest.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPURLRequest.swift @@ -1,10 +1,3 @@ -// -// HTTPURLRequest.swift -// LaunchDarkly -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation extension URLRequest { diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift index c11bd342..d51d76af 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift @@ -1,10 +1,3 @@ -// -// HTTPURLResponse.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation extension HTTPURLResponse { diff --git a/LaunchDarkly/LaunchDarkly/Networking/URLResponse.swift b/LaunchDarkly/LaunchDarkly/Networking/URLResponse.swift index c1af3228..6469bd36 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/URLResponse.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/URLResponse.swift @@ -1,10 +1,3 @@ -// -// URLResponse.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation extension URLResponse { diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift index dcc95c5b..56065e63 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift @@ -1,10 +1,3 @@ -// -// LDChangedFlagObject.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation /** diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index 2bce560f..da955e07 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -1,10 +1,3 @@ -// -// LDClientWrapper.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation /** @@ -685,31 +678,6 @@ public final class ObjcLDClient: NSObject { ldClient.stopObserving(owner: owner) } - /** - Sets a handler executed when an error occurs while processing flag or event responses. - - The SDK retains only weak references to owner, which allows the client app to freely destroy change owners without issues. Client apps should capture a strong self reference from a weak reference immediately inside the handler to avoid retain cycles causing a memory leak. - - The SDK executes handlers on the main thread. - - SeeAlso: `stopObserving(owner:)` - - ### Usage - ```` - __weak typeof(self) weakSelf = self; - [[LDClient sharedInstance] observeErrorWithOwner:self handler:^(NSError * _Nonnull error){ - __strong typeof(weakSelf) strongSelf = weakSelf; - [self doSomethingWithError:error]; - }]; - ```` - - - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. - - parameter handler: The LDErrorHandler the SDK will execute when a network request results in an error. - */ - @objc public func observeError(owner: LDObserverOwner, handler: @escaping LDErrorHandler) { - ldClient.observeError(owner: owner, handler: handler) - } - /** Handler passed to the client app when a BOOL feature flag value changes diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index 6276bb93..4244abfe 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -1,10 +1,3 @@ -// -// LDConfigObject.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation /** diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift index 97bd7a8e..a13ce6ba 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift @@ -1,10 +1,3 @@ -// -// ObjcLDEvaluationDetail.swift -// LaunchDarkly_iOS -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation @objc(LDBoolEvaluationDetail) diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index 35eea6c8..cbc1a245 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -1,10 +1,3 @@ -// -// LDUserObject.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation /** diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index 7f562c64..dfa8f53d 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -1,10 +1,3 @@ -// -// CacheConverter.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation // sourcery: autoMockable diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/ConnectionInformationStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/ConnectionInformationStore.swift index e510c912..6a3e6fe9 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/ConnectionInformationStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/ConnectionInformationStore.swift @@ -1,10 +1,3 @@ -// -// ConnectionInformationStore.swift -// LaunchDarkly_iOS -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation final class ConnectionInformationStore { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift index ea7f56de..b9668f05 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift @@ -1,10 +1,3 @@ -// -// DeprecatedCache.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation protocol DeprecatedCache { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift index 3c0103fe..dd45518e 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift @@ -1,10 +1,3 @@ -// -// DeprecatedCacheModelV5.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation // Cache model in use from 2.14.0 up to 4.0.0 diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift index 81c08401..9355c9e4 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift @@ -1,10 +1,3 @@ -// -// DiagnosticCache.swift -// LaunchDarkly -// -// Copyright © 2020 Catamorphic Co. All rights reserved. -// - import Foundation // sourcery: autoMockable diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift index 64bc88e9..5598594f 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift @@ -1,10 +1,3 @@ -// -// KeyedValueCache.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation // sourcery: autoMockable diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift index db7a4bee..5162da79 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift @@ -1,10 +1,3 @@ -// -// UserEnvironmentCache.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation enum FlagCachingStoreMode: CaseIterable { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index b5d82812..51112d0e 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -1,10 +1,3 @@ -// -// ClientServiceFactory.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import LDSwiftEventSource @@ -26,7 +19,6 @@ protocol ClientServiceCreating { func makeStreamingProvider(url: URL, httpHeaders: [String: String], connectMethod: String, connectBody: Data?, handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider func makeEnvironmentReporter() -> EnvironmentReporting func makeThrottler(environmentReporter: EnvironmentReporting) -> Throttling - func makeErrorNotifier() -> ErrorNotifying func makeConnectionInformation() -> ConnectionInformation func makeDiagnosticCache(sdkKey: String) -> DiagnosticCaching func makeDiagnosticReporter(service: DarklyServiceProvider) -> DiagnosticReporting @@ -107,10 +99,6 @@ final class ClientServiceFactory: ClientServiceCreating { func makeThrottler(environmentReporter: EnvironmentReporting) -> Throttling { Throttler(environmentReporter: environmentReporter) } - - func makeErrorNotifier() -> ErrorNotifying { - ErrorNotifier() - } func makeConnectionInformation() -> ConnectionInformation { ConnectionInformation(currentConnectionMode: .offline, lastConnectionFailureReason: .none) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift index 38c72498..9cb188e5 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift @@ -1,10 +1,3 @@ -// -// DiagnosticEventProcessor.swift -// LaunchDarkly -// -// Copyright © 2020 Catamorphic Co. All rights reserved. -// - import Foundation // sourcery: autoMockable diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift index c1ddb285..2e82a0bc 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -1,10 +1,3 @@ -// -// EnvironmentReporter.swift -// LaunchDarkly -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation #if os(iOS) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift deleted file mode 100644 index 70e559bd..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ErrorNotifier.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ErrorNotifier.swift -// Darkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation - -// sourcery: autoMockable -protocol ErrorNotifying { - func addErrorObserver(_ observer: ErrorObserver) - func removeObservers(for owner: LDObserverOwner) - func notifyObservers(of error: Error) -} - -final class ErrorNotifier: ErrorNotifying { - private(set) var errorObservers = [ErrorObserver]() - - func addErrorObserver(_ observer: ErrorObserver) { - errorObservers.append(observer) - } - - func removeObservers(for owner: LDObserverOwner) { - errorObservers.removeAll { $0.owner === owner } - } - - func notifyObservers(of error: Error) { - removeOldObservers() - errorObservers.forEach { $0.errorHandler(error) } - } - - private func removeOldObservers() { - errorObservers = errorObservers.filter { $0.owner != nil } - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index 527cca16..5482da04 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -1,10 +1,3 @@ -// -// EventReporter.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation enum EventSyncResult { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift index f11138c5..c06fc9eb 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift @@ -1,10 +1,3 @@ -// -// FlagChangeNotifier.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation // sourcery: autoMockable diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift index 4866a317..b18ab10e 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -1,10 +1,3 @@ -// -// FlagStore.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation protocol FlagMaintaining { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift index 74213bb9..741358c2 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift @@ -1,10 +1,3 @@ -// -// FlagSynchronizer.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Dispatch import LDSwiftEventSource diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Log.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Log.swift index 535abb37..4e8c9ed1 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Log.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Log.swift @@ -1,10 +1,3 @@ -// -// Log.swift -// LaunchDarkly -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation protocol Logger { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/NetworkReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/NetworkReporter.swift index 8f044956..82583e70 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/NetworkReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/NetworkReporter.swift @@ -1,10 +1,3 @@ -// -// File.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation #if canImport(SystemConfiguration) import SystemConfiguration diff --git a/LaunchDarkly/LaunchDarkly/Support/LaunchDarkly.h b/LaunchDarkly/LaunchDarkly/Support/LaunchDarkly.h index 3e607d20..149daed2 100644 --- a/LaunchDarkly/LaunchDarkly/Support/LaunchDarkly.h +++ b/LaunchDarkly/LaunchDarkly/Support/LaunchDarkly.h @@ -1,10 +1,3 @@ -// -// LaunchDarkly.h -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - #import //! Project version number for Darkly. diff --git a/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift b/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift index a0d7c2f6..ddd5aa6c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift @@ -1,10 +1,3 @@ -// -// AnyComparerSpec.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift b/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift index a5fc6819..f8d53f42 100644 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift @@ -1,10 +1,3 @@ -// -// DictionarySpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/Extensions/ThreadSpec.swift b/LaunchDarkly/LaunchDarklyTests/Extensions/ThreadSpec.swift index 53cb2de1..37c67ad1 100644 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/ThreadSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Extensions/ThreadSpec.swift @@ -1,10 +1,3 @@ -// -// ThreadSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index b71b3240..e6abcc6b 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -51,9 +51,6 @@ final class LDClientSpec: QuickSpec { var changeNotifierMock: FlagChangeNotifyingMock! { subject.flagChangeNotifier as? FlagChangeNotifyingMock } - var errorNotifierMock: ErrorNotifyingMock! { - subject.errorNotifier as? ErrorNotifyingMock - } var environmentReporterMock: EnvironmentReportingMock! { subject.environmentReporter as? EnvironmentReportingMock } @@ -66,9 +63,6 @@ final class LDClientSpec: QuickSpec { var makeFlagSynchronizerService: DarklyServiceProvider? { serviceFactoryMock.makeFlagSynchronizerReceivedParameters?.service } - var observedError: Error? { - errorNotifierMock.notifyObserversReceivedError - } var onSyncComplete: FlagSyncCompleteClosure? { serviceFactoryMock.onFlagSyncComplete } @@ -1066,19 +1060,10 @@ final class LDClientSpec: QuickSpec { receivedObserver?.connectionModeChangedHandler(ConnectionInformation.ConnectionMode.offline) expect(callCount) == 1 } - it("observeError") { - testContext.subject.observeError(owner: self) { _ in callCount += 1 } - expect(testContext.errorNotifierMock.addErrorObserverCallCount) == 1 - expect(testContext.errorNotifierMock.addErrorObserverReceivedObserver?.owner) === self - testContext.errorNotifierMock.addErrorObserverReceivedObserver?.errorHandler(ErrorMock()) - expect(callCount) == 1 - } it("stopObserving") { testContext.subject.stopObserving(owner: self) expect(mockNotifier.removeObserverCallCount) == 1 expect(mockNotifier.removeObserverReceivedOwner) === self - expect(testContext.errorNotifierMock.removeObserversCallCount) == 1 - expect(testContext.errorNotifierMock.removeObserversReceivedOwner) === self } } } @@ -1222,17 +1207,16 @@ final class LDClientSpec: QuickSpec { } func onSyncCompleteErrorSpec() { - func runTest(_ ctx: String, _ err: SynchronizingError, testError: @escaping ((SynchronizingError) -> Void)) { + func runTest(_ ctx: String, + _ err: SynchronizingError, + testError: @escaping ((ConnectionInformation.LastConnectionFailureReason) -> Void)) { var testContext: TestContext! context(ctx) { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true) - testContext.start() - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - testContext.errorNotifierMock.notifyObserversCallback = done - testContext.onSyncComplete?(.error(err)) - } + testContext = TestContext(startOnline: true) + testContext.start() + testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() + testContext.onSyncComplete?(.error(err)) } it("takes the client offline when unauthed") { expect(testContext.subject.isOnline) == !err.isClientUnauthorized @@ -1243,10 +1227,9 @@ final class LDClientSpec: QuickSpec { it("does not call the flag change notifier") { expect(testContext.changeNotifierMock.notifyObserversCallCount) == 0 } - it("informs the error notifier") { - expect(testContext.errorNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.observedError).to(beAnInstanceOf(SynchronizingError.self)) - if let err = testContext.observedError as? SynchronizingError { testError(err) } + it("Updates the connection information") { + expect(testContext.subject.getConnectionInformation().lastFailedConnection).to(beCloseTo(Date(), within: 5.0)) + testError(testContext.subject.getConnectionInformation().lastConnectionFailureReason) } } } @@ -1256,9 +1239,9 @@ final class LDClientSpec: QuickSpec { httpVersion: DarklyServiceMock.Constants.httpVersion, headerFields: nil) runTest("there was an internal server error", .response(serverError)) { error in - if case .response(let urlResponse as HTTPURLResponse) = error { - expect(urlResponse).to(beIdenticalTo(serverError)) - } else { fail("Incorrect error given to error notifier") } + if case .httpError(let errCode) = error { + expect(errCode) == 500 + } else { fail("Incorrect error in connection information") } } let unauthedError = HTTPURLResponse(url: DarklyServiceMock.Constants.mockBaseUrl, @@ -1266,25 +1249,15 @@ final class LDClientSpec: QuickSpec { httpVersion: DarklyServiceMock.Constants.httpVersion, headerFields: nil) runTest("there was a client unauthorized error", .response(unauthedError)) { error in - if case .response(let urlResponse as HTTPURLResponse) = error { - expect(urlResponse).to(beIdenticalTo(unauthedError)) - } else { fail("Incorrect error given to error notifier") } + if case .unauthorized = error { + } else { fail("Incorrect error in connection information") } } runTest("there was a request error", .request(DarklyServiceMock.Constants.error)) { error in - if case .request(let nsError as NSError) = error { - expect(nsError).to(beIdenticalTo(DarklyServiceMock.Constants.error)) - } else { fail("Incorrect error given to error notifier") } - } - runTest("there was a data error", .data(DarklyServiceMock.Constants.errorData)) { error in - if case .data(let data) = error { - expect(data) == DarklyServiceMock.Constants.errorData - } else { fail("Incorrect error given to error notifier") } - } - runTest("there was a non-NSError error", .streamError(DummyError())) { error in - if case .streamError(let dummy) = error { - expect(dummy is DummyError).to(beTrue()) - } else { fail("Incorrect error given to error notifier") } + if case .unknownError = error { + } else { fail("Incorrect error in connection information") } } + runTest("there was a data error", .data(DarklyServiceMock.Constants.errorData)) { _ in } + runTest("there was a non-NSError error", .streamError(DummyError())) { _ in } } private func runModeSpec() { diff --git a/LaunchDarkly/LaunchDarklyTests/Matcher/Match.swift b/LaunchDarkly/LaunchDarklyTests/Matcher/Match.swift index 60c15100..59e611b9 100644 --- a/LaunchDarkly/LaunchDarklyTests/Matcher/Match.swift +++ b/LaunchDarkly/LaunchDarklyTests/Matcher/Match.swift @@ -1,10 +1,3 @@ -// -// Match.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index 1706b94c..405efe0d 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -1,10 +1,3 @@ -// -// ClientServiceMockFactory.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import LDSwiftEventSource @testable import LaunchDarkly @@ -129,10 +122,6 @@ final class ClientServiceMockFactory: ClientServiceCreating { } return throttlingMock } - - func makeErrorNotifier() -> ErrorNotifying { - ErrorNotifyingMock() - } func makeConnectionInformation() -> ConnectionInformation { ConnectionInformation(currentConnectionMode: .offline, lastConnectionFailureReason: .none) diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 8d2a27fd..232407c9 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -1,10 +1,3 @@ -// -// DarklyServiceMock.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift index 88c5d44f..80934705 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift @@ -1,9 +1,3 @@ -// -// DeprecatedCacheMock.swift -// LaunchDarklyTests -// -// Copyright © 2020 Catamorphic Co. All rights reserved. -// import Foundation @testable import LaunchDarkly diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift index 79e5c088..359141ec 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift @@ -1,10 +1,3 @@ -// -// EnvironmentReportingMock.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation extension EnvironmentReportingMock { diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift index 7f0b4503..d9b40800 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift @@ -1,10 +1,3 @@ -// -// FlagMaintainingMock.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation @testable import LaunchDarkly diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDConfigStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDConfigStub.swift index 4c8472f6..2c956804 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDConfigStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDConfigStub.swift @@ -1,10 +1,3 @@ -// -// LDConfigStub.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation @testable import LaunchDarkly diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift index c09d6c34..8cd5a488 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift @@ -1,10 +1,3 @@ -// -// LDEventSourceMock.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import LDSwiftEventSource @testable import LaunchDarkly diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift index 4a1b51e9..93b7872b 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift @@ -1,10 +1,3 @@ -// -// LDUserStub.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation @testable import LaunchDarkly diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift index cf86824c..134b6464 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift @@ -1,10 +1,3 @@ -// -// CacheableEnvironmentFlagsSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift index b3b1b897..066035b8 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift @@ -1,10 +1,3 @@ -// -// CacheableUserEnvironmentsSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index 42270dd2..f04f6d9c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -1,10 +1,3 @@ -// -// DiagnosticEventSpec.swift -// LaunchDarklyTests -// -// Copyright © 2020 Catamorphic Co. All rights reserved. -// - import Foundation import XCTest import Quick diff --git a/LaunchDarkly/LaunchDarklyTests/Models/ErrorObserverSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/ErrorObserverSpec.swift deleted file mode 100644 index f56b36e4..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Models/ErrorObserverSpec.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ErrorObserverSpec.swift -// DarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation -import XCTest - -@testable import LaunchDarkly - -final class ErrorObserverContext { - var owner: ErrorObserverOwner? = ErrorObserverOwner() - var errors = [Error]() - - func handler(error: Error) { errors.append(error) } - func observer() -> ErrorObserver { ErrorObserver(owner: owner!, errorHandler: handler) } -} - -class ErrorObserverOwner { } -private class ErrorMock: Error { } - -final class ErrorObserverSpec: XCTestCase { - func testInit() { - let context = ErrorObserverContext() - let errorObserver = context.observer() - XCTAssert(errorObserver.owner === context.owner) - XCTAssertNotNil(errorObserver.errorHandler) - - let errorMock = ErrorMock() - errorObserver.errorHandler(errorMock) - XCTAssertEqual(context.errors.count, 1) - XCTAssert(context.errors[0] as? ErrorMock === errorMock) - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 1862d8b8..0cb05d5c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -1,10 +1,3 @@ -// -// EventSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift index 7415338b..1befd42f 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift @@ -1,10 +1,3 @@ -// -// FeatureFlagSpec.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagChange/FlagChangeObserverSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagChange/FlagChangeObserverSpec.swift index 5c30ae87..477194aa 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagChange/FlagChangeObserverSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagChange/FlagChangeObserverSpec.swift @@ -1,10 +1,3 @@ -// -// FlagObserverSpec.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation import XCTest diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift index 68a161af..3fc83097 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift @@ -1,10 +1,3 @@ -// -// FlagCounterSpec.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation import XCTest diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift index 7f8184cd..eb6f2fef 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift @@ -1,10 +1,3 @@ -// -// FlagRequestTrackerSpec.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation import XCTest diff --git a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index 4f3c3baa..107ab2fe 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -1,10 +1,3 @@ -// -// LDConfigSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import XCTest diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index a557f156..58da3f0c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -1,10 +1,3 @@ -// -// LDUserSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index a7f9e506..3fb870db 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -1,10 +1,3 @@ -// -// DarklyServiceSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift index 563099d3..11cce882 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift @@ -1,10 +1,3 @@ -// -// HTTPHeadersSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation import XCTest diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift index f03a0ae4..ffb5910f 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift @@ -1,10 +1,3 @@ -// -// HTTPURLResponse.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation @testable import LaunchDarkly diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift index e07b3244..5a186dc7 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift @@ -1,10 +1,3 @@ -// -// URLRequestSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import XCTest diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift index b6be7e76..25a57327 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift @@ -1,10 +1,3 @@ -// -// CacheConverterSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift index 5424dc5d..0b56d4fb 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift @@ -1,10 +1,3 @@ -// -// DeprecatedCacheModelV5Spec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift index fb233483..1df77c31 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift @@ -1,10 +1,3 @@ -// -// DiagnosticCacheSpec.swift -// LaunchDarklyTests -// -// Copyright © 2020 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift index 88f58642..3b59c37d 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift @@ -1,10 +1,3 @@ -// -// KeyedValueCacheSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import XCTest diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift index 19654032..1e4d73e0 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift @@ -1,10 +1,3 @@ -// -// UserEnvironmentCacheSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift index d7183e79..548b647f 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift @@ -1,10 +1,3 @@ -// -// EnvironmentReporterSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ErrorNotifierSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ErrorNotifierSpec.swift deleted file mode 100644 index fb96c63b..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ErrorNotifierSpec.swift +++ /dev/null @@ -1,66 +0,0 @@ -import Foundation -import XCTest - -@testable import LaunchDarkly - -final class ErrorNotifierSpec: XCTestCase { - func testAddAndRemoveObservers() { - let errorNotifier = ErrorNotifier() - XCTAssertEqual(errorNotifier.errorObservers.count, 0) - - errorNotifier.removeObservers(for: ErrorObserverOwner()) - XCTAssertEqual(errorNotifier.errorObservers.count, 0) - - let firstContext = ErrorObserverContext() - let secondContext = ErrorObserverContext() - errorNotifier.addErrorObserver(firstContext.observer()) - errorNotifier.addErrorObserver(secondContext.observer()) - errorNotifier.addErrorObserver(firstContext.observer()) - errorNotifier.addErrorObserver(secondContext.observer()) - XCTAssertEqual(errorNotifier.errorObservers.count, 4) - - errorNotifier.removeObservers(for: ErrorObserverOwner()) - XCTAssertEqual(errorNotifier.errorObservers.count, 4) - - errorNotifier.removeObservers(for: firstContext.owner!) - XCTAssertEqual(errorNotifier.errorObservers.count, 2) - XCTAssert(!errorNotifier.errorObservers.contains { $0.owner !== secondContext.owner }) - - errorNotifier.removeObservers(for: secondContext.owner!) - XCTAssertEqual(errorNotifier.errorObservers.count, 0) - - XCTAssertEqual(firstContext.errors.count, 0) - XCTAssertEqual(secondContext.errors.count, 0) - } - - func testNotifyObservers() { - let errorNotifier = ErrorNotifier() - let firstContext = ErrorObserverContext() - let secondContext = ErrorObserverContext() - let thirdContext = ErrorObserverContext() - - (0..<2).forEach { _ in - [firstContext, secondContext, thirdContext].forEach { - errorNotifier.addErrorObserver($0.observer()) - } - } - // remove reference to owner in secondContext - secondContext.owner = nil - - let errorMock = ErrorMock() - errorNotifier.notifyObservers(of: errorMock) - [firstContext, thirdContext].forEach { - XCTAssertEqual($0.errors.count, 2) - XCTAssert($0.errors[0] as? ErrorMock === errorMock) - XCTAssert($0.errors[1] as? ErrorMock === errorMock) - } - - // Ownerless observer should not be notified - XCTAssertEqual(secondContext.errors.count, 0) - // Should remove the observers that no longer have an owner - XCTAssertEqual(errorNotifier.errorObservers.count, 4) - XCTAssert(!errorNotifier.errorObservers.contains { $0.owner !== firstContext.owner && $0.owner !== thirdContext.owner }) - } -} - -private class ErrorMock: Error { } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index 4c192675..3d2686bf 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -1,10 +1,3 @@ -// -// EventReporterSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift index 44024fca..548adfea 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift @@ -1,10 +1,3 @@ -// -// FlagChangeNotifierSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift index a37bc73b..12cf793d 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift @@ -1,10 +1,3 @@ -// -// FlagStoreSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift index 507d5060..31125317 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift @@ -1,10 +1,3 @@ -// -// FlagSynchronizerSpec.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SynchronizingErrorSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SynchronizingErrorSpec.swift index a7629d94..94dae68d 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SynchronizingErrorSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SynchronizingErrorSpec.swift @@ -1,10 +1,3 @@ -// -// SynchronizingErrorSpec.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation import XCTest diff --git a/LaunchDarkly/LaunchDarklyTests/TestUtil.swift b/LaunchDarkly/LaunchDarklyTests/TestUtil.swift index 6fe9e6fd..3d102856 100644 --- a/LaunchDarkly/LaunchDarklyTests/TestUtil.swift +++ b/LaunchDarkly/LaunchDarklyTests/TestUtil.swift @@ -1,10 +1,3 @@ -// -// TestUtil.swift -// LaunchDarklyTests -// -// Copyright © 2020 Catamorphic Co. All rights reserved. -// - import XCTest import Foundation From 916c185dfd6cf9429e83cfadd153b5049d6b6265 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Wed, 9 Mar 2022 13:19:12 -0600 Subject: [PATCH 25/50] (V6) Remove Date extensions, isWithin and isEarlierThan. (#175) --- .../LaunchDarkly/Extensions/Date.swift | 10 ----- .../Models/FeatureFlag/FeatureFlag.swift | 5 +-- .../ServiceObjects/Cache/CacheConverter.swift | 4 +- .../Cache/UserEnvironmentFlagCache.swift | 4 +- .../LaunchDarklyTests/LDClientSpec.swift | 6 +-- .../LaunchDarklyTests/Models/EventSpec.swift | 39 ++++++++++--------- .../FlagRequestTrackerSpec.swift | 2 +- .../Cache/CacheConverterSpec.swift | 4 +- .../Cache/DeprecatedCacheModelSpec.swift | 8 ++-- .../Cache/UserEnvironmentFlagCacheSpec.swift | 2 +- .../ServiceObjects/LDTimerSpec.swift | 2 +- 11 files changed, 36 insertions(+), 50 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Date.swift b/LaunchDarkly/LaunchDarkly/Extensions/Date.swift index 8b04c280..238f81bd 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/Date.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/Date.swift @@ -10,14 +10,4 @@ extension Date { else { return nil } self = Date(timeIntervalSince1970: Double(millisSince1970) / 1_000) } - - func isWithin(_ timeInterval: TimeInterval, of otherDate: Date?) -> Bool { - guard let otherDate = otherDate - else { return false } - return fabs(self.timeIntervalSince(otherDate)) <= timeInterval - } - - func isEarlierThan(_ otherDate: Date) -> Bool { - self.timeIntervalSince(otherDate) < 0.0 - } } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift index 8f29ba57..e3528ed8 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift @@ -70,10 +70,7 @@ struct FeatureFlag { } func shouldCreateDebugEvents(lastEventReportResponseTime: Date?) -> Bool { - guard let debugEventsUntilDate = debugEventsUntilDate - else { return false } - let comparisonDate = lastEventReportResponseTime ?? Date() - return comparisonDate.isEarlierThan(debugEventsUntilDate) || comparisonDate == debugEventsUntilDate + (lastEventReportResponseTime ?? Date()) <= (debugEventsUntilDate ?? Date.distantPast) } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index dfa8f53d..070f5a66 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -55,8 +55,6 @@ final class CacheConverter: CacheConverting { extension Date { func isExpired(expirationDate: Date) -> Bool { - let stringEquivalentDate = self.stringEquivalentDate - let stringEquivalentExpirationDate = expirationDate.stringEquivalentDate - return stringEquivalentDate.isEarlierThan(stringEquivalentExpirationDate) + self.stringEquivalentDate < expirationDate.stringEquivalentDate } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift index 5162da79..81db8dde 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift @@ -83,8 +83,8 @@ final class UserEnvironmentFlagCache: FeatureFlagCaching { return cacheableUserEnvironmentsCollection } // sort collection into key-value pairs in descending order...youngest to oldest - var userEnvironmentsCollection = cacheableUserEnvironmentsCollection.sorted { pair1, pair2 -> Bool in - pair2.value.lastUpdated.isEarlierThan(pair1.value.lastUpdated) + var userEnvironmentsCollection = cacheableUserEnvironmentsCollection.sorted { + $1.value.lastUpdated < $0.value.lastUpdated } while userEnvironmentsCollection.count > maxCachedUsers && maxCachedUsers >= 0 { userEnvironmentsCollection.removeLast() diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index e6abcc6b..92f0466e 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -1121,7 +1121,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async } it("informs the flag change notifier of the changed flags") { @@ -1160,7 +1160,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async } it("informs the flag change notifier of the changed flag") { @@ -1196,7 +1196,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async } it("informs the flag change notifier of the changed flag") { diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 0cb05d5c..9d3c34df 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -319,7 +319,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary with matching non-user elements") { expect(eventDictionary.eventKind) == .feature expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation @@ -344,7 +344,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary with matching non-user elements") { expect(eventDictionary.eventKind) == .feature expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation @@ -366,7 +366,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary with the version") { expect(eventDictionary.eventKind) == .feature expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventUserKey) == user.key @@ -385,7 +385,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary without the version") { expect(eventDictionary.eventKind) == .feature expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventUserKey) == user.key @@ -404,7 +404,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary with matching non-user elements") { expect(eventDictionary.eventKind) == .feature expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: NSNull())).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: NSNull())).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation @@ -441,7 +441,7 @@ final class EventSpec: QuickSpec { expect(eventDictionary.eventKind) == .identify expect(eventDictionary.eventKey) == user.key - expect(eventDictionary.eventCreationDate?.isWithin(0.1, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.1)) expect(eventDictionary.eventValue).to(beNil()) expect(eventDictionary.eventDefaultValue).to(beNil()) expect(eventDictionary.eventVariation).to(beNil()) @@ -513,7 +513,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary with matching custom data") { expect(eventDictionary.eventKind) == .custom expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventData, to: eventData)).to(beTrue()) expect(eventDictionary.eventValue).to(beNil()) expect(eventDictionary.eventDefaultValue).to(beNil()) @@ -538,7 +538,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary with matching custom data") { expect(eventDictionary.eventKind) == .custom expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(eventDictionary.eventData).to(beNil()) expect(eventDictionary.eventValue).to(beNil()) expect(eventDictionary.eventDefaultValue).to(beNil()) @@ -562,7 +562,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary with matching non-user elements") { expect(eventDictionary.eventKind) == .custom expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventData, to: CustomEvent.dictionaryData)).to(beTrue()) expect(eventDictionary.eventValue).to(beNil()) expect(eventDictionary.eventDefaultValue).to(beNil()) @@ -588,7 +588,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary with matching non-user elements") { expect(eventDictionary.eventKind) == .custom expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventData, to: CustomEvent.dictionaryData)).to(beTrue()) expect(eventDictionary.eventValue).to(beNil()) expect(eventDictionary.eventDefaultValue).to(beNil()) @@ -638,7 +638,7 @@ final class EventSpec: QuickSpec { expect(eventDictionary.eventKind) == .debug expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation @@ -663,7 +663,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary with the version") { expect(eventDictionary.eventKind) == .debug expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) @@ -682,7 +682,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary without the version") { expect(eventDictionary.eventKind) == .debug expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) @@ -701,7 +701,7 @@ final class EventSpec: QuickSpec { it("creates a dictionary with matching non-user elements") { expect(eventDictionary.eventKind) == .debug expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) + expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: NSNull())).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: NSNull())).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) @@ -727,8 +727,8 @@ final class EventSpec: QuickSpec { } it("creates a summary dictionary with matching elements") { expect(eventDictionary.eventKind) == .summary - expect(eventDictionary.eventStartDate?.isWithin(0.001, of: event.flagRequestTracker?.startDate)).to(beTrue()) - expect(eventDictionary.eventEndDate?.isWithin(0.001, of: event.endDate)).to(beTrue()) + expect(eventDictionary.eventStartDate).to(beCloseTo(event.flagRequestTracker!.startDate, within: 0.001)) + expect(eventDictionary.eventEndDate).to(beCloseTo(event.endDate!, within: 0.001)) guard let features = eventDictionary.eventFeatures else { fail("expected eventDictionary features to not be nil, got nil") @@ -824,7 +824,7 @@ final class EventSpec: QuickSpec { eventDictionary = event.dictionaryValue(config: config) } it("returns the event kind when the dictionary contains the event endDate") { - expect(eventDictionary.eventEndDate?.isWithin(0.001, of: event.endDate)).to(beTrue()) + expect(eventDictionary.eventEndDate).to(beCloseTo(event.endDate!, within: 0.001)) } it("returns nil when the dictionary does not contain the event kind") { eventDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) @@ -976,8 +976,9 @@ extension Dictionary where Key == String, Value == Any { else { return false } if kind == .summary { guard kind == other.eventKind, - let eventEndDate = eventEndDate, eventEndDate.isWithin(0.001, of: other.eventEndDate) - else { return false } + let eventEndDate = eventEndDate, let otherEndDate = other.eventEndDate, + fabs(eventEndDate.timeIntervalSince(otherEndDate)) <= 0.001 + else { return false } return true } guard let key = eventKey, let creationDateMillis = eventCreationDateMillis, diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift index eb6f2fef..82aacd45 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift @@ -84,7 +84,7 @@ extension FlagRequestTracker { extension FlagRequestTracker: Equatable { public static func == (lhs: FlagRequestTracker, rhs: FlagRequestTracker) -> Bool { - if !lhs.startDate.isWithin(0.001, of: rhs.startDate) { + if fabs(lhs.startDate.timeIntervalSince(rhs.startDate)) > 0.001 { return false } return lhs.flagCounters == rhs.flagCounters diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift index 25a57327..372e2984 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift @@ -69,8 +69,8 @@ final class CacheConverterSpec: QuickSpec { // The CacheConverter should always remove all expired data DeprecatedCacheModel.allCases.forEach { model in expect(testContext.deprecatedCacheMock(for: model).removeDataCallCount) == 1 - expect(testContext.deprecatedCacheMock(for: model).removeDataReceivedExpirationDate? - .isWithin(0.5, of: testContext.expiredCacheThreshold)) == true + expect(testContext.deprecatedCacheMock(for: model).removeDataReceivedExpirationDate) + .to(beCloseTo(testContext.expiredCacheThreshold, within: 0.5)) } } for deprecatedData in cacheCases { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift index 6fdc1092..9e6df446 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift @@ -26,8 +26,8 @@ class DeprecatedCacheModelSpec { var userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags] var mobileKeys: [MobileKey] var sortedLastUpdatedDates: [(userKey: UserKey, lastUpdated: Date)] { - userEnvironmentsCollection.map { ($0, $1.lastUpdated) }.sorted { tuple1, tuple2 in - tuple1.lastUpdated.isEarlierThan(tuple2.lastUpdated) + userEnvironmentsCollection.map { ($0, $1.lastUpdated) }.sorted { + $0.lastUpdated < $1.lastUpdated } } var userKeys: [UserKey] { users.map { $0.key } } @@ -46,8 +46,8 @@ class DeprecatedCacheModelSpec { } func expiredUserKeys(for expirationDate: Date) -> [UserKey] { - sortedLastUpdatedDates.compactMap { tuple in - tuple.lastUpdated.isEarlierThan(expirationDate) ? tuple.userKey : nil + sortedLastUpdatedDates.compactMap { + $0.lastUpdated < expirationDate ? $0.userKey : nil } } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift index 1e4d73e0..10145d3e 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift @@ -26,7 +26,7 @@ final class UserEnvironmentFlagCacheSpec: QuickSpec { } var oldestUser: String { userEnvironmentsCollection.compactMapValues { $0.lastUpdated } - .max { $1.value.isEarlierThan($0.value) }! + .max { $1.value < $0.value }! .key } var setUserEnvironments: [UserKey: CacheableUserEnvironmentFlags]? { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift index d97cdece..9022da1a 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift @@ -31,7 +31,7 @@ final class LDTimerSpec: QuickSpec { expect(testContext.ldTimer.timer).toNot(beNil()) expect(testContext.ldTimer.isCancelled) == false - expect(testContext.ldTimer.fireDate?.isWithin(1.0, of: testContext.fireDate)).to(beTrue()) // 1 second is arbitrary...just want it to be "close" + expect(testContext.ldTimer.fireDate).to(beCloseTo(testContext.fireDate, within: 1.0)) // 1 second is arbitrary...just want it to be "close" testContext.ldTimer.cancel() } From 466d2a8b932aebcf9092b74a00136dd1d4a2a7c3 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Wed, 9 Mar 2022 13:33:12 -0600 Subject: [PATCH 26/50] (V6) Remove test helper and tests for test helper that was not used. (#176) --- .../LaunchDarklyTests/Models/EventSpec.swift | 96 ------------------- 1 file changed, 96 deletions(-) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 9d3c34df..e53f756e 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -831,75 +831,6 @@ final class EventSpec: QuickSpec { expect(eventDictionary.eventEndDate).to(beNil()) } } - - describe("matches") { - var eventDictionary: [String: Any]! - var otherDictionary: [String: Any]! - beforeEach { - eventDictionary = Event.stub(.custom, with: user).dictionaryValue(config: config) - otherDictionary = eventDictionary - } - it("returns true when keys and creationDateMillis are equal") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == true - } - it("returns false when keys differ") { - otherDictionary[Event.CodingKeys.key.rawValue] = otherDictionary.eventKey! + "dummy" - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } - it("returns false when creationDateMillis differ") { - otherDictionary[Event.CodingKeys.creationDate.rawValue] = otherDictionary.eventCreationDateMillis! + 1 - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } - it("returns false when dictionary key is nil") { - eventDictionary.removeValue(forKey: Event.CodingKeys.key.rawValue) - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } - it("returns false when other dictionary key is nil") { - otherDictionary.removeValue(forKey: Event.CodingKeys.key.rawValue) - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } - it("returns false when dictionary creationDateMillis is nil") { - eventDictionary.removeValue(forKey: Event.CodingKeys.creationDate.rawValue) - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } - it("returns false when other dictionary creationDateMillis is nil") { - otherDictionary.removeValue(forKey: Event.CodingKeys.creationDate.rawValue) - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } - context("for summary event dictionaries") { - var event: Event! - beforeEach { - event = Event.stub(.summary, with: user) - eventDictionary = event.dictionaryValue(config: config) - } - it("when the kinds and endDates match returns true") { - otherDictionary = event.dictionaryValue(config: config) - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == true - } - it("when the kinds do not match returns false") { - otherDictionary = event.dictionaryValue(config: config) - otherDictionary[Event.CodingKeys.kind.rawValue] = Event.Kind.feature.rawValue - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } - context("when the endDates do not match") { - it("and endDates differ returns false") { - otherDictionary = event.dictionaryValue(config: config) - otherDictionary[Event.CodingKeys.endDate.rawValue] = event.endDate!.addingTimeInterval(0.002).millisSince1970 - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } - it("and endDate is nil returns false") { - eventDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) - otherDictionary = event.dictionaryValue(config: config) - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } - it("and other endDate is nil returns false") { - otherDictionary = event.dictionaryValue(config: config) - otherDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } - } - } - } } } } @@ -970,33 +901,6 @@ extension Dictionary where Key == String, Value == Any { var eventPreviousContextKind: String? { self[Event.CodingKeys.previousContextKind.rawValue] as? String } - - func matches(eventDictionary other: [String: Any]) -> Bool { - guard let kind = eventKind - else { return false } - if kind == .summary { - guard kind == other.eventKind, - let eventEndDate = eventEndDate, let otherEndDate = other.eventEndDate, - fabs(eventEndDate.timeIntervalSince(otherEndDate)) <= 0.001 - else { return false } - return true - } - guard let key = eventKey, let creationDateMillis = eventCreationDateMillis, - let otherKey = other.eventKey, let otherCreationDateMillis = other.eventCreationDateMillis - else { return false } - return key == otherKey && creationDateMillis == otherCreationDateMillis - } -} - -extension Array where Element == [String: Any] { - func eventDictionary(for event: Event) -> [String: Any]? { - let selectedDictionaries = self.filter { eventDictionary -> Bool in - event.key == eventDictionary.eventKey - } - guard selectedDictionaries.count == 1 - else { return nil } - return selectedDictionaries.first - } } extension Event { From cebd6f2c4f7aaa93e22536cfe2865125e6c10c30 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 10 Mar 2022 09:05:54 -0600 Subject: [PATCH 27/50] Remove functionality of privatizing all custom attributes with the private attribute name "custom". Remove LDUserWrapper NSCoding functionality for old caches. Remove top level device and operating system attributes from LDUser. Simplify LDUserSpec. --- LaunchDarkly/LaunchDarkly/LDClient.swift | 1 - LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 111 +-- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 13 +- .../LaunchDarklyTests/Mocks/LDUserStub.swift | 2 - .../LaunchDarklyTests/Models/EventSpec.swift | 4 +- .../Models/User/LDUserSpec.swift | 929 ++++-------------- 6 files changed, 205 insertions(+), 855 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 072c2d95..a61bcb0f 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -744,7 +744,6 @@ public class LDClient { environmentReporter = self.serviceFactory.makeEnvironmentReporter() flagCache = self.serviceFactory.makeFeatureFlagCache(maxCachedUsers: configuration.maxCachedUsers) flagStore = self.serviceFactory.makeFlagStore() - LDUserWrapper.configureKeyedArchiversToHandleVersion2_3_0AndOlderUserCacheFormat() cacheConverter = self.serviceFactory.makeCacheConverter(maxCachedUsers: configuration.maxCachedUsers) flagChangeNotifier = self.serviceFactory.makeFlagChangeNotifier() throttler = self.serviceFactory.makeThrottler(environmentReporter: environmentReporter) diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 85723156..92b78b1c 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -19,7 +19,7 @@ public struct LDUser { The SDK will not include private attribute values in analytics events, but private attribute names will be sent. See Also: `LDConfig.allUserAttributesPrivate`, `LDConfig.privateUserAttributes`, and `privateAttributes`. */ - public static var privatizableAttributes: [String] { optionalAttributes + [CodingKeys.custom.rawValue] } + public static var privatizableAttributes: [String] { optionalAttributes } static let optionalAttributes = [CodingKeys.name.rawValue, CodingKeys.firstName.rawValue, CodingKeys.lastName.rawValue, CodingKeys.country.rawValue, @@ -51,13 +51,9 @@ public struct LDUser { /// Client app defined avatar for the user. (Default: nil) public var avatar: String? /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) - public var custom: [String: Any]? + public var custom: [String: Any] /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: true) public var isAnonymous: Bool - /// Client app defined device for the user. The SDK will determine the device automatically, however the client app can override the value. The SDK will insert the device into the `custom` dictionary. The device cannot be made private. (Default: the system identified device) - public var device: String? - /// Client app defined operatingSystem for the user. The SDK will determine the operatingSystem automatically, however the client app can override the value. The SDK will insert the operatingSystem into the `custom` dictionary. The operatingSystem cannot be made private. (Default: the system identified operating system) - public var operatingSystem: String? /** Client app defined privateAttributes for the user. @@ -97,8 +93,6 @@ public struct LDUser { avatar: String? = nil, custom: [String: Any]? = nil, isAnonymous: Bool? = nil, - device: String? = nil, - operatingSystem: String? = nil, privateAttributes: [String]? = nil, secondary: String? = nil) { let environmentReporter = EnvironmentReporter() @@ -112,10 +106,10 @@ public struct LDUser { self.ipAddress = ipAddress self.email = email self.avatar = avatar - self.custom = custom self.isAnonymous = isAnonymous ?? (selectedKey == LDUser.defaultKey(environmentReporter: environmentReporter)) - self.device = device ?? custom?[CodingKeys.device.rawValue] as? String ?? environmentReporter.deviceModel - self.operatingSystem = operatingSystem ?? custom?[CodingKeys.operatingSystem.rawValue] as? String ?? environmentReporter.systemVersion + self.custom = custom ?? [:] + self.custom.merge([CodingKeys.device.rawValue: environmentReporter.deviceModel, + CodingKeys.operatingSystem.rawValue: environmentReporter.systemVersion]) { lhs, _ in lhs } self.privateAttributes = privateAttributes Log.debug(typeName(and: #function) + "user: \(self)") } @@ -137,10 +131,7 @@ public struct LDUser { email = userDictionary[CodingKeys.email.rawValue] as? String avatar = userDictionary[CodingKeys.avatar.rawValue] as? String privateAttributes = userDictionary[CodingKeys.privateAttributes.rawValue] as? [String] - - custom = userDictionary[CodingKeys.custom.rawValue] as? [String: Any] - device = custom?[CodingKeys.device.rawValue] as? String - operatingSystem = custom?[CodingKeys.operatingSystem.rawValue] as? String + custom = userDictionary[CodingKeys.custom.rawValue] as? [String: Any] ?? [:] Log.debug(typeName(and: #function) + "user: \(self)") } @@ -149,7 +140,10 @@ public struct LDUser { Internal initializer that accepts an environment reporter, used for testing */ init(environmentReporter: EnvironmentReporting) { - self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), isAnonymous: true, device: environmentReporter.deviceModel, operatingSystem: environmentReporter.systemVersion) + self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), + custom: [CodingKeys.device.rawValue: environmentReporter.deviceModel, + CodingKeys.operatingSystem.rawValue: environmentReporter.systemVersion], + isAnonymous: true) } // swiftlint:disable:next cyclomatic_complexity @@ -166,53 +160,52 @@ public struct LDUser { case CodingKeys.email.rawValue: return email case CodingKeys.avatar.rawValue: return avatar case CodingKeys.custom.rawValue: return custom - case CodingKeys.device.rawValue: return device - case CodingKeys.operatingSystem.rawValue: return operatingSystem + case CodingKeys.device.rawValue: return custom[CodingKeys.device.rawValue] + case CodingKeys.operatingSystem.rawValue: return custom[CodingKeys.operatingSystem.rawValue] case CodingKeys.privateAttributes.rawValue: return privateAttributes default: return nil } } /// Returns the custom dictionary without the SDK set device and operatingSystem attributes var customWithoutSdkSetAttributes: [String: Any] { - custom?.filter { key, _ in !LDUser.sdkSetAttributes.contains(key) } ?? [:] + custom.filter { key, _ in !LDUser.sdkSetAttributes.contains(key) } } /// Dictionary with LDUser attribute keys and values, with options to include feature flags and private attributes. LDConfig object used to help resolving what attributes should be private. /// - parameter includePrivateAttributes: Controls whether the resulting dictionary includes private attributes /// - parameter config: Provides supporting information for defining private attributes func dictionaryValue(includePrivateAttributes includePrivate: Bool, config: LDConfig) -> [String: Any] { - var dictionary = [String: Any]() - var redactedAttributes = [String]() - let combinedPrivateAttributes = config.allUserAttributesPrivate ? LDUser.privatizableAttributes - : (privateAttributes ?? []) + (config.privateUserAttributes ?? []) + let allPrivate = !includePrivate && config.allUserAttributesPrivate + let privateAttributeNames = includePrivate ? [] : (privateAttributes ?? []) + (config.privateUserAttributes ?? []) + + var dictionary: [String: Any] = [:] + var redactedAttributes: [String] = [] dictionary[CodingKeys.key.rawValue] = key dictionary[CodingKeys.isAnonymous.rawValue] = isAnonymous LDUser.optionalAttributes.forEach { attribute in - let value = self.value(for: attribute) - if !includePrivate && combinedPrivateAttributes.contains(attribute) && value != nil { - redactedAttributes.append(attribute) - } else { - dictionary[attribute] = value + if let value = self.value(for: attribute) { + if allPrivate || privateAttributeNames.contains(attribute) { + redactedAttributes.append(attribute) + } else { + dictionary[attribute] = value + } } } - var customDictionary = [String: Any]() - customWithoutSdkSetAttributes.forEach { attrName, attrVal in - if !includePrivate && combinedPrivateAttributes.contains(where: [CodingKeys.custom.rawValue, attrName].contains ) { + var customDictionary: [String: Any] = [:] + custom.forEach { attrName, attrVal in + if allPrivate || privateAttributeNames.contains(attrName) { redactedAttributes.append(attrName) } else { customDictionary[attrName] = attrVal } } - customDictionary[CodingKeys.device.rawValue] = device - customDictionary[CodingKeys.operatingSystem.rawValue] = operatingSystem dictionary[CodingKeys.custom.rawValue] = customDictionary.isEmpty ? nil : customDictionary - if !includePrivate && !redactedAttributes.isEmpty { - let redactedAttributeSet: Set = Set(redactedAttributes) - dictionary[CodingKeys.privateAttributes.rawValue] = redactedAttributeSet.sorted() + if !redactedAttributes.isEmpty { + dictionary[CodingKeys.privateAttributes.rawValue] = Set(redactedAttributes).sorted() } return dictionary @@ -252,52 +245,10 @@ extension LDUser: Equatable { } } -extension LDUserWrapper: NSCoding { +extension LDUserWrapper { struct Keys { fileprivate static let featureFlags = "featuresJsonDictionary" } - - func encode(with encoder: NSCoder) { - encoder.encode(wrapped.key, forKey: LDUser.CodingKeys.key.rawValue) - encoder.encode(wrapped.secondary, forKey: LDUser.CodingKeys.secondary.rawValue) - encoder.encode(wrapped.name, forKey: LDUser.CodingKeys.name.rawValue) - encoder.encode(wrapped.firstName, forKey: LDUser.CodingKeys.firstName.rawValue) - encoder.encode(wrapped.lastName, forKey: LDUser.CodingKeys.lastName.rawValue) - encoder.encode(wrapped.country, forKey: LDUser.CodingKeys.country.rawValue) - encoder.encode(wrapped.ipAddress, forKey: LDUser.CodingKeys.ipAddress.rawValue) - encoder.encode(wrapped.email, forKey: LDUser.CodingKeys.email.rawValue) - encoder.encode(wrapped.avatar, forKey: LDUser.CodingKeys.avatar.rawValue) - encoder.encode(wrapped.custom, forKey: LDUser.CodingKeys.custom.rawValue) - encoder.encode(wrapped.isAnonymous, forKey: LDUser.CodingKeys.isAnonymous.rawValue) - encoder.encode(wrapped.device, forKey: LDUser.CodingKeys.device.rawValue) - encoder.encode(wrapped.operatingSystem, forKey: LDUser.CodingKeys.operatingSystem.rawValue) - encoder.encode(wrapped.privateAttributes, forKey: LDUser.CodingKeys.privateAttributes.rawValue) - } - - convenience init?(coder decoder: NSCoder) { - var user = LDUser(key: decoder.decodeObject(forKey: LDUser.CodingKeys.key.rawValue) as? String, - name: decoder.decodeObject(forKey: LDUser.CodingKeys.name.rawValue) as? String, - firstName: decoder.decodeObject(forKey: LDUser.CodingKeys.firstName.rawValue) as? String, - lastName: decoder.decodeObject(forKey: LDUser.CodingKeys.lastName.rawValue) as? String, - country: decoder.decodeObject(forKey: LDUser.CodingKeys.country.rawValue) as? String, - ipAddress: decoder.decodeObject(forKey: LDUser.CodingKeys.ipAddress.rawValue) as? String, - email: decoder.decodeObject(forKey: LDUser.CodingKeys.email.rawValue) as? String, - avatar: decoder.decodeObject(forKey: LDUser.CodingKeys.avatar.rawValue) as? String, - custom: decoder.decodeObject(forKey: LDUser.CodingKeys.custom.rawValue) as? [String: Any], - isAnonymous: decoder.decodeBool(forKey: LDUser.CodingKeys.isAnonymous.rawValue), - privateAttributes: decoder.decodeObject(forKey: LDUser.CodingKeys.privateAttributes.rawValue) as? [String], - secondary: decoder.decodeObject(forKey: LDUser.CodingKeys.secondary.rawValue) as? String - ) - user.device = decoder.decodeObject(forKey: LDUser.CodingKeys.device.rawValue) as? String - user.operatingSystem = decoder.decodeObject(forKey: LDUser.CodingKeys.operatingSystem.rawValue) as? String - self.init(user: user) - } - - /// Method to configure NSKeyed(Un)Archivers to convert version 2.3.0 and older user caches to 2.3.1 and later user cache formats. Note that the v3 SDK no longer caches LDUsers, rather only feature flags and the LDUser.key are cached. - class func configureKeyedArchiversToHandleVersion2_3_0AndOlderUserCacheFormat() { - NSKeyedUnarchiver.setClass(LDUserWrapper.self, forClassName: "LDUserModel") - NSKeyedArchiver.setClassName("LDUserModel", for: LDUserWrapper.self) - } } extension LDUser: TypeIdentifying { } @@ -322,8 +273,6 @@ extension LDUser: TypeIdentifying { } && avatar == otherUser.avatar && AnyComparer.isEqual(custom, to: otherUser.custom) && isAnonymous == otherUser.isAnonymous - && device == otherUser.device - && operatingSystem == otherUser.operatingSystem && privateAttributes == otherUser.privateAttributes } } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index cbc1a245..46cc2985 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -103,7 +103,7 @@ public final class ObjcLDUser: NSObject { set { user.avatar = newValue } } /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) - @objc public var custom: [String: Any]? { + @objc public var custom: [String: Any] { get { user.custom } set { user.custom = newValue } } @@ -112,16 +112,7 @@ public final class ObjcLDUser: NSObject { get { user.isAnonymous } set { user.isAnonymous = newValue } } - /// Client app defined device for the user. The SDK will determine the device automatically, however the client app can override the value. The SDK will insert the device into the `custom` dictionary. The device cannot be made private. (Default: the system identified device) - @objc public var device: String? { - get { user.device } - set { user.device = newValue } - } - /// Client app defined operatingSystem for the user. The SDK will determine the operatingSystem automatically, however the client app can override the value. The SDK will insert the operatingSystem into the `custom` dictionary. The operatingSystem cannot be made private. (Default: the system identified operating system) - @objc public var operatingSystem: String? { - get { user.operatingSystem } - set { user.operatingSystem = newValue } - } + /** Client app defined privateAttributes for the user. diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift index 93b7872b..7f057711 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift @@ -45,8 +45,6 @@ extension LDUser { avatar: StubConstants.avatar, custom: StubConstants.custom(includeSystemValues: true), isAnonymous: StubConstants.isAnonymous, - device: environmentReporter?.deviceModel, - operatingSystem: environmentReporter?.systemVersion, secondary: StubConstants.secondary) return user } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index e53f756e..75b4efe8 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -842,13 +842,13 @@ extension Dictionary where Key == String, Value == Any { var eventUserKey: String? { self[Event.CodingKeys.userKey.rawValue] as? String } - var eventUser: LDUser? { + fileprivate var eventUser: LDUser? { if let userDictionary = eventUserDictionary { return LDUser(userDictionary: userDictionary) } return nil } - var eventUserDictionary: [String: Any]? { + fileprivate var eventUserDictionary: [String: Any]? { self[Event.CodingKeys.user.rawValue] as? [String: Any] } var eventValue: Any? { diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 58da3f0c..28b2791f 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -5,10 +5,6 @@ import Nimble final class LDUserSpec: QuickSpec { - struct Constants { - fileprivate static let userCount = 3 - } - override func spec() { initSpec() dictionaryValueSpec() @@ -24,62 +20,31 @@ final class LDUserSpec: QuickSpec { private func initSubSpec() { var user: LDUser! describe("init") { - context("called with optional elements") { - context("including system values") { - it("creates a LDUser with optional elements") { - user = LDUser(key: LDUser.StubConstants.key, name: LDUser.StubConstants.name, firstName: LDUser.StubConstants.firstName, lastName: LDUser.StubConstants.lastName, - country: LDUser.StubConstants.country, ipAddress: LDUser.StubConstants.ipAddress, email: LDUser.StubConstants.email, avatar: LDUser.StubConstants.avatar, - custom: LDUser.StubConstants.custom(includeSystemValues: true), isAnonymous: LDUser.StubConstants.isAnonymous, - privateAttributes: LDUser.privatizableAttributes, secondary: LDUser.StubConstants.secondary) - expect(user.key) == LDUser.StubConstants.key - expect(user.secondary) == LDUser.StubConstants.secondary - expect(user.name) == LDUser.StubConstants.name - expect(user.firstName) == LDUser.StubConstants.firstName - expect(user.lastName) == LDUser.StubConstants.lastName - expect(user.isAnonymous) == LDUser.StubConstants.isAnonymous - expect(user.country) == LDUser.StubConstants.country - expect(user.ipAddress) == LDUser.StubConstants.ipAddress - expect(user.email) == LDUser.StubConstants.email - expect(user.avatar) == LDUser.StubConstants.avatar - expect(user.device) == LDUser.StubConstants.device - expect(user.operatingSystem) == LDUser.StubConstants.operatingSystem - expect(user.custom).toNot(beNil()) - if let subjectCustom = user.custom { - expect(subjectCustom == LDUser.StubConstants.custom(includeSystemValues: true)).to(beTrue()) - } - expect(user.privateAttributes).toNot(beNil()) - if let privateAttributes = user.privateAttributes { - expect(privateAttributes) == LDUser.privatizableAttributes - } - } - } - context("excluding system values") { - it("creates a LDUser with optional elements") { - user = LDUser(key: LDUser.StubConstants.key, name: LDUser.StubConstants.name, firstName: LDUser.StubConstants.firstName, lastName: LDUser.StubConstants.lastName, - country: LDUser.StubConstants.country, ipAddress: LDUser.StubConstants.ipAddress, email: LDUser.StubConstants.email, avatar: LDUser.StubConstants.avatar, - custom: LDUser.StubConstants.custom(includeSystemValues: false), isAnonymous: LDUser.StubConstants.isAnonymous, device: LDUser.StubConstants.device, operatingSystem: LDUser.StubConstants.operatingSystem, privateAttributes: LDUser.privatizableAttributes, secondary: LDUser.StubConstants.secondary) - expect(user.key) == LDUser.StubConstants.key - expect(user.secondary) == LDUser.StubConstants.secondary - expect(user.name) == LDUser.StubConstants.name - expect(user.firstName) == LDUser.StubConstants.firstName - expect(user.lastName) == LDUser.StubConstants.lastName - expect(user.isAnonymous) == LDUser.StubConstants.isAnonymous - expect(user.country) == LDUser.StubConstants.country - expect(user.ipAddress) == LDUser.StubConstants.ipAddress - expect(user.email) == LDUser.StubConstants.email - expect(user.avatar) == LDUser.StubConstants.avatar - expect(user.device) == LDUser.StubConstants.device - expect(user.operatingSystem) == LDUser.StubConstants.operatingSystem - expect(user.custom).toNot(beNil()) - if let subjectCustom = user.custom { - expect(subjectCustom == LDUser.StubConstants.custom(includeSystemValues: false)).to(beTrue()) - } - expect(user.privateAttributes).toNot(beNil()) - if let privateAttributes = user.privateAttributes { - expect(privateAttributes) == LDUser.privatizableAttributes - } - } - } + it("with all fields and custom overriding system values") { + user = LDUser(key: LDUser.StubConstants.key, + name: LDUser.StubConstants.name, + firstName: LDUser.StubConstants.firstName, + lastName: LDUser.StubConstants.lastName, + country: LDUser.StubConstants.country, + ipAddress: LDUser.StubConstants.ipAddress, + email: LDUser.StubConstants.email, + avatar: LDUser.StubConstants.avatar, + custom: LDUser.StubConstants.custom(includeSystemValues: true), + isAnonymous: LDUser.StubConstants.isAnonymous, + privateAttributes: LDUser.privatizableAttributes, + secondary: LDUser.StubConstants.secondary) + expect(user.key) == LDUser.StubConstants.key + expect(user.secondary) == LDUser.StubConstants.secondary + expect(user.name) == LDUser.StubConstants.name + expect(user.firstName) == LDUser.StubConstants.firstName + expect(user.lastName) == LDUser.StubConstants.lastName + expect(user.isAnonymous) == LDUser.StubConstants.isAnonymous + expect(user.country) == LDUser.StubConstants.country + expect(user.ipAddress) == LDUser.StubConstants.ipAddress + expect(user.email) == LDUser.StubConstants.email + expect(user.avatar) == LDUser.StubConstants.avatar + expect(user.custom == LDUser.StubConstants.custom(includeSystemValues: true)).to(beTrue()) + expect(user.privateAttributes) == LDUser.privatizableAttributes } context("called without optional elements") { var environmentReporter: EnvironmentReporter! @@ -98,9 +63,9 @@ final class LDUserSpec: QuickSpec { expect(user.ipAddress).to(beNil()) expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) - expect(user.device) == environmentReporter.deviceModel - expect(user.operatingSystem) == environmentReporter.systemVersion - expect(user.custom).to(beNil()) + expect(user.custom.count) == 2 + expect(user.custom[LDUser.CodingKeys.device.rawValue] as? String) == environmentReporter.deviceModel + expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue] as? String) == environmentReporter.systemVersion expect(user.privateAttributes).to(beNil()) expect(user.secondary).to(beNil()) } @@ -108,7 +73,7 @@ final class LDUserSpec: QuickSpec { context("called without a key multiple times") { var users = [LDUser]() beforeEach { - while users.count < Constants.userCount { + while users.count < 3 { users.append(LDUser()) } } @@ -145,17 +110,7 @@ final class LDUserSpec: QuickSpec { expect(user.ipAddress) == originalUser.ipAddress expect(user.email) == originalUser.email expect(user.avatar) == originalUser.avatar - - expect(originalUser.custom).toNot(beNil()) - expect(user.custom).toNot(beNil()) - if let originalCustom = originalUser.custom, - let subjectCustom = user.custom { - expect(subjectCustom == originalCustom).to(beTrue()) - } - - expect(user.device) == originalUser.device - expect(user.operatingSystem) == originalUser.operatingSystem - + expect(user.custom == originalUser.custom).to(beTrue()) expect(user.privateAttributes) == LDUser.privatizableAttributes } } @@ -178,11 +133,10 @@ final class LDUserSpec: QuickSpec { expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) expect(user.secondary).to(beNil()) - expect(user.device).toNot(beNil()) - expect(user.operatingSystem).toNot(beNil()) - expect(user.custom).toNot(beNil()) - expect(user.customWithoutSdkSetAttributes.isEmpty) == true + expect(user.custom.count) == 2 + expect(user.custom[LDUser.CodingKeys.device.rawValue] as? String) == EnvironmentReporter().deviceModel + expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue] as? String) == EnvironmentReporter().systemVersion expect(user.privateAttributes).to(beNil()) } } @@ -201,9 +155,7 @@ final class LDUserSpec: QuickSpec { expect(user.ipAddress).to(beNil()) expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) - expect(user.device).to(beNil()) - expect(user.operatingSystem).to(beNil()) - expect(user.custom).to(beNil()) + expect(user.custom).to(beEmpty()) expect(user.privateAttributes).to(beNil()) } } @@ -230,480 +182,178 @@ final class LDUserSpec: QuickSpec { expect(user.ipAddress).to(beNil()) expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) - expect(user.device) == environmentReporter.deviceModel - expect(user.operatingSystem) == environmentReporter.systemVersion + expect(user.custom.count) == 2 + expect(user.custom[LDUser.CodingKeys.device.rawValue] as? String) == environmentReporter.deviceModel + expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue] as? String) == environmentReporter.systemVersion - expect(user.custom).to(beNil()) expect(user.privateAttributes).to(beNil()) } } } - private func dictionaryValueInvariants(user: LDUser, userDictionary: [String: Any]) { - // Always has required attributes - expect({ user.requiredAttributeKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - // Optional attributes with nil value should never be included in user dictionary - expect({ user.optionalAttributeMissingValueKeysDontExist(userDictionary: userDictionary) }).to(match()) - // Flag config is legacy, shouldn't be included - expect(userDictionary.flagConfig).to(beNil()) - } - private func dictionaryValueSpec() { + let allCustomPrivitizable = Array(LDUser.StubConstants.custom(includeSystemValues: true).keys) + describe("dictionaryValue") { var user: LDUser! var config: LDConfig! var userDictionary: [String: Any]! - var privateAttributes: [String]! beforeEach { config = LDConfig.stub user = LDUser.stub() } - context("including private attributes") { - context("with individual private attributes") { - let assertions = { - it("creates a matching dictionary") { - // creates a dictionary with matching key value pairs - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - expect({ user.customDictionaryPublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - - // creates a dictionary without redacted attributes - expect(userDictionary.redactedAttributes).to(beNil()) - - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } - } - (LDUser.privatizableAttributes + LDUser.StubConstants.custom.keys).forEach { attribute in - context("\(attribute) in the config") { - beforeEach { - user.privateAttributes = [attribute] - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - assertions() - } - context("\(attribute) in the user") { - context("that is populated") { - beforeEach { - user.privateAttributes = [attribute] - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - assertions() - } - context("that is empty") { - beforeEach { - user = LDUser() - user.privateAttributes = [attribute] - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - assertions() - } - } - } + context("with an empty user") { + beforeEach { + user = LDUser() + // Remove SDK set attributes + user.custom = [:] } - context("with all private attributes") { - let allPrivateAssertions = { - it("creates a dictionary with matching key value pairs") { - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - expect({ user.customDictionaryPublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - } - it("creates a dictionary without redacted attributes") { - expect(userDictionary.redactedAttributes).to(beNil()) - } - it("maintains invariants") { - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } - } - context("using the config flag") { - beforeEach { - config.allUserAttributesPrivate = true - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - allPrivateAssertions() - } - context("contained in the config") { - beforeEach { - config.privateUserAttributes = LDUser.privatizableAttributes - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - allPrivateAssertions() - } - context("contained in the user") { - beforeEach { - user.privateAttributes = LDUser.privatizableAttributes - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - allPrivateAssertions() + // Should be the same regardless of including/privitizing attributes + let testCase = { + it("creates expected user dictionary") { + expect(userDictionary.count) == 2 + // Required attributes + expect(userDictionary[LDUser.CodingKeys.key.rawValue] as? String) == user.key + expect(userDictionary[LDUser.CodingKeys.isAnonymous.rawValue] as? Bool) == user.isAnonymous } } - context("with no private attributes") { - let noPrivateAssertions = { - it("creates a dictionary with matching key value pairs") { - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - expect({ user.customDictionaryPublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - } - it("creates a dictionary without redacted attributes") { - expect(userDictionary.redactedAttributes).to(beNil()) - } - it("maintains invariants") { - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } + context("including private attributes") { + beforeEach { + userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) } - context("by setting private attributes to nil") { - beforeEach { - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - noPrivateAssertions() - } - context("by setting config private attributes to empty") { - beforeEach { - config.privateUserAttributes = [] - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - noPrivateAssertions() - } - context("by setting user private attributes to empty") { - beforeEach { - user.privateAttributes = [] - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - noPrivateAssertions() + testCase() + } + context("privatizing all globally") { + beforeEach { + config.allUserAttributesPrivate = true + userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) } + testCase() } - context("with custom as the private attribute") { - context("on a user with no custom dictionary") { - context("with a device and os") { - beforeEach { - user.custom = nil - user.privateAttributes = [LDUser.CodingKeys.custom.rawValue] - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - it("creates a dictionary with matching key value pairs") { - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - expect({ user.customDictionaryContainsOnlySdkSetAttributes(userDictionary: userDictionary) }).to(match()) - } - it("creates a dictionary without redacted attributes") { - expect(userDictionary.redactedAttributes).to(beNil()) - } - it("maintains invariants") { - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } - } - context("without a device and os") { - beforeEach { - user.custom = nil - user.operatingSystem = nil - user.device = nil - user.privateAttributes = [LDUser.CodingKeys.custom.rawValue] - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - it("creates a dictionary with matching key value pairs") { - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - } - it("creates a dictionary without redacted attributes") { - expect(userDictionary.redactedAttributes).to(beNil()) - } - it("creates a dictionary without a custom dictionary") { - expect(userDictionary.customDictionary(includeSdkSetAttributes: true)).to(beNil()) - } - it("maintains invariants") { - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } - } + context("privatizing all individually in config") { + beforeEach { + config.privateUserAttributes = LDUser.privatizableAttributes + ["customAttr"] + userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) } - context("on a user with a custom dictionary") { - context("without a device and os") { - beforeEach { - user.custom = user.customWithoutSdkSetAttributes - user.device = nil - user.operatingSystem = nil - user.privateAttributes = [LDUser.CodingKeys.custom.rawValue] - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - it("creates a dictionary with matching key value pairs") { - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - expect({ user.sdkSetAttributesDontExist(userDictionary: userDictionary) }).to(match()) - expect({ user.customDictionaryPublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - } - it("creates a dictionary without redacted attributes") { - expect(userDictionary.redactedAttributes).to(beNil()) - } - it("maintains invariants") { - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } - } + testCase() + } + context("privatizing all individually on user") { + beforeEach { + user.privateAttributes = LDUser.privatizableAttributes + ["customAttr"] + userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) } + testCase() } } - context("excluding private attributes") { - context("with individual private attributes") { - context("contained in the config") { - beforeEach { - privateAttributes = LDUser.privatizableAttributes + user.customAttributes! - } - it("creates a matching dictionary") { - privateAttributes.forEach { attribute in - let privateAttributesForTest = [attribute] - config.privateUserAttributes = privateAttributesForTest - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - - // creates a dictionary with matching key value pairs - expect({ user.requiredAttributeKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) - expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - - // creates a dictionary without private keys - expect({ user.optionalAttributePrivateKeysDontExist(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) - - // creates a dictionary with redacted attributes - expect({ user.optionalAttributePrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - expect({ user.optionalAttributePublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - // creates a custom dictionary with matching key value pairs, without private keys, and with redacted attributes - if attribute == LDUser.CodingKeys.custom.rawValue { - expect({ user.customDictionaryContainsOnlySdkSetAttributes(userDictionary: userDictionary) }).to(match()) - expect(user.customWithoutSdkSetAttributes.allSatisfy { k, _ in userDictionary.redactedAttributes!.contains(k) }).to(beTrue()) - } else { - expect({ user.customDictionaryPublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) - expect({ user.customDictionaryPrivateKeysDontExist(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) + it("includePrivateAttributes always includes attributes") { + config.allUserAttributesPrivate = true + config.privateUserAttributes = LDUser.privatizableAttributes + allCustomPrivitizable + user.privateAttributes = LDUser.privatizableAttributes + allCustomPrivitizable + let userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) + + expect(userDictionary.count) == 11 + + // Required attributes + expect(userDictionary[LDUser.CodingKeys.key.rawValue] as? String) == user.key + expect(userDictionary[LDUser.CodingKeys.isAnonymous.rawValue] as? Bool) == user.isAnonymous + + // Built-in optional attributes + expect(userDictionary[LDUser.CodingKeys.name.rawValue] as? String) == user.name + expect(userDictionary[LDUser.CodingKeys.firstName.rawValue] as? String) == user.firstName + expect(userDictionary[LDUser.CodingKeys.lastName.rawValue] as? String) == user.lastName + expect(userDictionary[LDUser.CodingKeys.email.rawValue] as? String) == user.email + expect(userDictionary[LDUser.CodingKeys.ipAddress.rawValue] as? String) == user.ipAddress + expect(userDictionary[LDUser.CodingKeys.avatar.rawValue] as? String) == user.avatar + expect(userDictionary[LDUser.CodingKeys.secondary.rawValue] as? String) == user.secondary + expect(userDictionary[LDUser.CodingKeys.country.rawValue] as? String) == user.country + + let customDictionary = userDictionary.customDictionary()! + expect(customDictionary.count) == allCustomPrivitizable.count + + // Custom attributes + allCustomPrivitizable.forEach { attr in + expect(AnyComparer.isEqual(customDictionary[attr], to: user.custom[attr])).to(beTrue()) + } - expect({ user.customPrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - expect({ user.customPublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - } + // Redacted attributes is empty + expect(userDictionary[LDUser.CodingKeys.privateAttributes.rawValue]).to(beNil()) + } - // creates a dictionary without flag config - expect(userDictionary.flagConfig).to(beNil()) - } - } - } - context("contained in the user") { - context("on a populated user") { - beforeEach { - privateAttributes = LDUser.privatizableAttributes + user.customAttributes! + [false, true].forEach { isCustomAttr in + (isCustomAttr ? Array(LDUser.StubConstants.custom(includeSystemValues: true).keys) + : LDUser.privatizableAttributes).forEach { privateAttr in + [false, true].forEach { inConfig in + it("with \(privateAttr) private in \(inConfig ? "config" : "user")") { + if inConfig { + config.privateUserAttributes = [privateAttr] + } else { + user.privateAttributes = [privateAttr] } - it("creates a matching dictionary") { - privateAttributes.forEach { attribute in - let privateAttributesForTest = [attribute] - user.privateAttributes = privateAttributesForTest - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - - // creates a dictionary with matching key value pairs - expect({ user.requiredAttributeKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) - expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - - // creates a dictionary without private keys - expect({ user.optionalAttributePrivateKeysDontExist(userDictionary: userDictionary, privateAttributes: privateAttributesForTest) }).to(match()) - // creates a dictionary with redacted attributes - expect({ user.optionalAttributePrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - expect({ user.optionalAttributePublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - - // creates a custom dictionary with matching key value pairs, without private keys, and with redacted attributes - if attribute == LDUser.CodingKeys.custom.rawValue { - expect({ user.customDictionaryContainsOnlySdkSetAttributes(userDictionary: userDictionary) }).to(match()) - expect(user.customWithoutSdkSetAttributes.allSatisfy { k, _ in userDictionary.redactedAttributes!.contains(k) }).to(beTrue()) - } else { - expect({ user.customDictionaryPublicKeyValuePairsMatch(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - expect({ user.customDictionaryPrivateKeysDontExist(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) + userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - expect({ user.customPrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - expect({ user.customPublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - } + expect(userDictionary.redactedAttributes) == [privateAttr] - // creates a dictionary without flag config - expect(userDictionary.flagConfig).to(beNil()) - } + let includingDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) + if !isCustomAttr { + let userDictionaryWithoutRedacted = userDictionary.filter { $0.key != "privateAttrs" } + let includingDictionaryWithoutRedacted = includingDictionary.filter { $0.key != privateAttr && $0.key != "privateAttrs" } + expect(AnyComparer.isEqual(userDictionaryWithoutRedacted, to: includingDictionaryWithoutRedacted)) == true + } else { + let userDictionaryWithoutRedacted = userDictionary.filter { $0.key != "custom" && $0.key != "privateAttrs" } + let includingDictionaryWithoutRedacted = includingDictionary.filter { $0.key != "custom" && $0.key != "privateAttrs" } + expect(AnyComparer.isEqual(userDictionaryWithoutRedacted, to: includingDictionaryWithoutRedacted)) == true + let expectedCustom = (includingDictionary["custom"] as! [String: Any]).filter { $0.key != privateAttr } + expect(AnyComparer.isEqual(userDictionary["custom"], to: expectedCustom)) == true } } - context("on an empty user") { - beforeEach { - user = LDUser() - privateAttributes = LDUser.privatizableAttributes - } - it("creates a matching dictionary") { - privateAttributes.forEach { attribute in - let privateAttributesForTest = [attribute] - user.privateAttributes = privateAttributesForTest - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - - // creates a dictionary with matching key value pairs - expect({ user.requiredAttributeKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - expect({ user.optionalAttributeMissingValueKeysDontExist(userDictionary: userDictionary) }).to(match()) - expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) + } + } + } - // creates a dictionary without private keys - expect({ user.customDictionaryContainsOnlySdkSetAttributes(userDictionary: userDictionary) }).to(match()) + context("with allUserAttributesPrivate") { + beforeEach { + config.allUserAttributesPrivate = true + userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) + } + it("creates expected dictionary") { + expect(userDictionary.count) == 3 + // Required attributes + expect(userDictionary[LDUser.CodingKeys.key.rawValue] as? String) == user.key + expect(userDictionary[LDUser.CodingKeys.isAnonymous.rawValue] as? Bool) == user.isAnonymous - // creates a dictionary without redacted attributes - expect(userDictionary.redactedAttributes).to(beNil()) + expect(Set(userDictionary.redactedAttributes!)) == Set(LDUser.privatizableAttributes + allCustomPrivitizable) + } + } - // creates a dictionary without flag config - expect(userDictionary.flagConfig).to(beNil()) - } - } - } + context("with no private attributes") { + let noPrivateAssertions = { + it("matches dictionary including private") { + expect(AnyComparer.isEqual(userDictionary, to: user.dictionaryValue(includePrivateAttributes: true, config: config))) == true } } - context("with all private attributes") { - let allPrivateAssertions = { - it("creates a dictionary with matching key value pairs") { - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: LDUser.privatizableAttributes) }).to(match()) - expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - } - it("creates a dictionary without private keys") { - expect({ user.optionalAttributePrivateKeysDontExist(userDictionary: userDictionary, privateAttributes: LDUser.privatizableAttributes) }).to(match()) - expect({ user.customDictionaryContainsOnlySdkSetAttributes(userDictionary: userDictionary) }).to(match()) - } - it("creates a dictionary with redacted attributes") { - expect({ user.optionalAttributePrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: userDictionary, - privateAttributes: LDUser.privatizableAttributes) }).to(match()) - expect({ user.optionalAttributePublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: userDictionary, - privateAttributes: LDUser.privatizableAttributes) }).to(match()) - expect(user.customWithoutSdkSetAttributes.allSatisfy { k, _ in userDictionary.redactedAttributes!.contains(k) }).to(beTrue()) - } - it("maintains invariants") { - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } - } - context("using the config flag") { - beforeEach { - config.allUserAttributesPrivate = true - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - allPrivateAssertions() - } - context("contained in the config") { - beforeEach { - config.privateUserAttributes = LDUser.privatizableAttributes - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - allPrivateAssertions() - } - context("contained in the user") { - beforeEach { - user.privateAttributes = LDUser.privatizableAttributes - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - allPrivateAssertions() + context("by setting private attributes to nil") { + beforeEach { + userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) } + noPrivateAssertions() } - context("with no private attributes") { - let noPrivateAssertions = { - it("creates a dictionary with matching key value pairs") { - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - expect({ user.customDictionaryPublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - } - it("creates a dictionary without redacted attributes") { - expect(userDictionary.redactedAttributes).to(beNil()) - } - it("maintains invariants") { - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } - } - context("by setting private attributes to nil") { - beforeEach { - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - noPrivateAssertions() - } - context("by setting config private attributes to empty") { - beforeEach { - config.privateUserAttributes = [] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - noPrivateAssertions() - } - context("by setting user private attributes to empty") { - beforeEach { - user.privateAttributes = [] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - noPrivateAssertions() + context("by setting config private attributes to empty") { + beforeEach { + config.privateUserAttributes = [] + userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) } + noPrivateAssertions() } - context("with custom as the private attribute") { - context("on a user with no custom dictionary") { - context("with a device and os") { - beforeEach { - user.custom = nil - user.privateAttributes = [LDUser.CodingKeys.custom.rawValue] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - it("creates a dictionary with matching key value pairs") { - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - expect({ user.sdkSetAttributesKeyValuePairsMatch(userDictionary: userDictionary) }).to(match()) - expect({ user.customDictionaryContainsOnlySdkSetAttributes(userDictionary: userDictionary) }).to(match()) - } - it("creates a dictionary without redacted attributes") { - expect(userDictionary.redactedAttributes).to(beNil()) - } - it("maintains invariants") { - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } - } - context("without a device and os") { - beforeEach { - user.custom = nil - user.operatingSystem = nil - user.device = nil - user.privateAttributes = [LDUser.CodingKeys.custom.rawValue] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - it("creates a dictionary with matching key value pairs") { - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - } - it("creates a dictionary without redacted attributes") { - expect(userDictionary.redactedAttributes).to(beNil()) - } - it("creates a dictionary without a custom dictionary") { - expect(userDictionary.customDictionary(includeSdkSetAttributes: true)).to(beNil()) - } - it("maintains invariants") { - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } - } - } - context("on a user with a custom dictionary") { - context("without a device and os") { - beforeEach { - user.custom = user.customWithoutSdkSetAttributes - user.device = nil - user.operatingSystem = nil - user.privateAttributes = [LDUser.CodingKeys.custom.rawValue] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - it("creates a dictionary with matching key value pairs") { - expect({ user.optionalAttributePublicKeyValuePairsMatch(userDictionary: userDictionary, privateAttributes: []) }).to(match()) - expect({ user.sdkSetAttributesDontExist(userDictionary: userDictionary) }).to(match()) - } - it("creates a dictionary private attrs include custom attributes") { - expect(userDictionary.redactedAttributes?.count) == user.custom?.count - expect(userDictionary.redactedAttributes?.contains { user.custom?[$0] != nil }).to(beTrue()) - } - it("creates a dictionary without a custom dictionary") { - expect(userDictionary.customDictionary(includeSdkSetAttributes: true)).to(beNil()) - } - it("maintains invariants") { - self.dictionaryValueInvariants(user: user, userDictionary: userDictionary) - } - } + context("by setting user private attributes to empty") { + beforeEach { + user.privateAttributes = [] + userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) } + noPrivateAssertions() } } } @@ -715,19 +365,15 @@ final class LDUserSpec: QuickSpec { describe("isEqual") { context("when users are equal") { - context("with all properties set") { - it("returns true") { - user = LDUser.stub() - otherUser = user - expect(user.isEqual(to: otherUser)) == true - } + it("returns true with all properties set") { + user = LDUser.stub() + otherUser = user + expect(user.isEqual(to: otherUser)) == true } - context("with no properties set") { - it("returns true") { - user = LDUser() - otherUser = user - expect(user.isEqual(to: otherUser)) == true - } + it("returns true with no properties set") { + user = LDUser() + otherUser = user + expect(user.isEqual(to: otherUser)) == true } } context("when users are not equal") { @@ -741,10 +387,8 @@ final class LDUserSpec: QuickSpec { ("ipAddress", true, "dummy", { u, v in u.ipAddress = v as! String? }), ("email address", true, "dummy", { u, v in u.email = v as! String? }), ("avatar", true, "dummy", { u, v in u.avatar = v as! String? }), - ("custom", true, ["dummy": true], { u, v in u.custom = v as! [String: Any]? }), + ("custom", false, ["dummy": true], { u, v in u.custom = v as! [String: Any] }), ("isAnonymous", false, true, { u, v in u.isAnonymous = v as! Bool }), - ("device", true, "dummy", { u, v in u.device = v as! String? }), - ("operatingSystem", true, "dummy", { u, v in u.operatingSystem = v as! String? }), ("privateAttributes", false, ["dummy"], { u, v in u.privateAttributes = v as! [String]? })] testFields.forEach { name, isOptional, otherVal, setter in context("\(name) differs") { @@ -781,224 +425,6 @@ final class LDUserSpec: QuickSpec { } extension LDUser { - static var requiredAttributes: [String] { - [CodingKeys.key.rawValue, CodingKeys.isAnonymous.rawValue] - } - var customAttributes: [String]? { - custom?.keys.filter { key in !LDUser.sdkSetAttributes.contains(key) } - } - - struct MatcherMessages { - static let valuesDontMatch = "dictionary does not match attribute " - static let dictionaryShouldNotContain = "dictionary contains attribute " - static let dictionaryShouldContain = "dictionary does not contain attribute " - static let attributeListShouldNotContain = "private attributes list contains attribute " - static let attributeListShouldContain = "private attributes list does not contain attribute " - } - - private func failsToMatch(fails: [String]) -> ToMatchResult { - fails.isEmpty ? .matched : .failed(reason: fails.joined(separator: ", ")) - } - - fileprivate func requiredAttributeKeyValuePairsMatch(userDictionary: [String: Any]) -> ToMatchResult { - failsToMatch(fails: LDUser.requiredAttributes.compactMap { attribute in - messageIfMissingValue(in: userDictionary, for: attribute) - }) - } - - fileprivate func optionalAttributePublicKeyValuePairsMatch(userDictionary: [String: Any], privateAttributes: [String]) -> ToMatchResult { - failsToMatch(fails: LDUser.optionalAttributes.compactMap { attribute in - privateAttributes.contains(attribute) ? nil : messageIfValueDoesntMatch(value: value(forAttribute: attribute), in: userDictionary, for: attribute) - }) - } - - fileprivate func optionalAttributePrivateKeysDontExist(userDictionary: [String: Any], privateAttributes: [String]) -> ToMatchResult { - failsToMatch(fails: LDUser.optionalAttributes.compactMap { attribute in - !privateAttributes.contains(attribute) ? nil : messageIfAttributeExists(in: userDictionary, for: attribute) - }) - } - - fileprivate func optionalAttributeMissingValueKeysDontExist(userDictionary: [String: Any]) -> ToMatchResult { - failsToMatch(fails: LDUser.optionalAttributes.compactMap { attribute in - value(forAttribute: attribute) != nil ? nil : messageIfAttributeExists(in: userDictionary, for: attribute) - }) - } - - fileprivate func optionalAttributePrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: [String: Any], privateAttributes: [String]) -> ToMatchResult { - let redactedAttributes = userDictionary.redactedAttributes - let messages: [String] = LDUser.optionalAttributes.compactMap { attribute in - if value(forAttribute: attribute) != nil && privateAttributes.contains(attribute) { - return messageIfRedactedAttributeDoesNotExist(in: redactedAttributes, for: attribute) - } - return nil - } - return failsToMatch(fails: messages) - } - - fileprivate func optionalAttributeKeysDontAppearInPrivateAttrs(userDictionary: [String: Any]) -> ToMatchResult { - let redactedAttributes = userDictionary.redactedAttributes - return failsToMatch(fails: LDUser.optionalAttributes.compactMap { attribute in - messageIfAttributeExists(in: redactedAttributes, for: attribute) - }) - } - - fileprivate func optionalAttributePublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: [String: Any], privateAttributes: [String]) -> ToMatchResult { - let redactedAttributes = userDictionary.redactedAttributes - let messages: [String] = LDUser.optionalAttributes.compactMap { attribute in - if value(forAttribute: attribute) == nil || !privateAttributes.contains(attribute) { - return messageIfPublicOrMissingAttributeIsRedacted(in: redactedAttributes, for: attribute) - } - return nil - } - return failsToMatch(fails: messages) - } - - fileprivate func sdkSetAttributesKeyValuePairsMatch(userDictionary: [String: Any]) -> ToMatchResult { - guard let customDictionary = userDictionary.customDictionary(includeSdkSetAttributes: true) - else { - return .failed(reason: MatcherMessages.dictionaryShouldContain + CodingKeys.custom.rawValue) - } - - var messages = [String]() - - LDUser.sdkSetAttributes.forEach { attribute in - if let message = messageIfMissingValue(in: customDictionary, for: attribute) { - messages.append(message) - } - if let message = messageIfValueDoesntMatch(value: value(forAttribute: attribute), in: customDictionary, for: attribute) { - messages.append(message) - } - } - - return failsToMatch(fails: messages) - } - - fileprivate func sdkSetAttributesDontExist(userDictionary: [String: Any]) -> ToMatchResult { - guard let customDictionary = userDictionary.customDictionary(includeSdkSetAttributes: true) else { - return .matched - } - - let messages = LDUser.sdkSetAttributes.compactMap { attribute in - messageIfAttributeExists(in: customDictionary, for: attribute) - } - - return failsToMatch(fails: messages) - } - - fileprivate func customDictionaryContainsOnlySdkSetAttributes(userDictionary: [String: Any]) -> ToMatchResult { - guard let customDictionary = userDictionary.customDictionary(includeSdkSetAttributes: false) - else { - return .failed(reason: MatcherMessages.dictionaryShouldContain + CodingKeys.custom.rawValue) - } - - if !customDictionary.isEmpty { - return .failed(reason: MatcherMessages.dictionaryShouldNotContain + CodingKeys.custom.rawValue) - } - - return .matched - } - - fileprivate func customDictionaryPublicKeyValuePairsMatch(userDictionary: [String: Any], privateAttributes: [String]) -> ToMatchResult { - guard let custom = custom - else { - return userDictionary.customDictionary(includeSdkSetAttributes: false).isNilOrEmpty ? .matched - : .failed(reason: MatcherMessages.dictionaryShouldNotContain + CodingKeys.custom.rawValue) - } - guard let customDictionary = userDictionary.customDictionary(includeSdkSetAttributes: false) - else { - return .failed(reason: MatcherMessages.dictionaryShouldContain + CodingKeys.custom.rawValue) - } - - var messages = [String]() - - customAttributes?.forEach { customAttribute in - if !privateAttributes.contains(customAttribute) { - if let message = messageIfMissingValue(in: customDictionary, for: customAttribute) { - messages.append(message) - } - if let message = messageIfValueDoesntMatch(value: custom[customAttribute], in: customDictionary, for: customAttribute) { - messages.append(message) - } - } - } - - return failsToMatch(fails: messages) - } - - fileprivate func customDictionaryPrivateKeysDontExist(userDictionary: [String: Any], privateAttributes: [String]) -> ToMatchResult { - guard let customDictionary = userDictionary.customDictionary(includeSdkSetAttributes: false) - else { - return .failed(reason: MatcherMessages.dictionaryShouldContain + CodingKeys.custom.rawValue) - } - - let messages = customAttributes?.compactMap { customAttribute in - if privateAttributes.contains(customAttribute) { - return messageIfAttributeExists(in: customDictionary, for: customAttribute) - } - return nil - } ?? [String]() - - return failsToMatch(fails: messages) - } - - fileprivate func customPrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: [String: Any], privateAttributes: [String]) -> ToMatchResult { - guard let custom = custom - else { - return userDictionary.customDictionary(includeSdkSetAttributes: false).isNilOrEmpty ? .matched - : .failed(reason: MatcherMessages.dictionaryShouldNotContain + CodingKeys.custom.rawValue) - } - - return failsToMatch(fails: customAttributes?.compactMap { customAttribute in - if privateAttributes.contains(customAttribute) && custom[customAttribute] != nil { - return messageIfRedactedAttributeDoesNotExist(in: userDictionary.redactedAttributes, for: customAttribute) - } - return nil - } ?? [String]()) - } - - fileprivate func customPublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: [String: Any], privateAttributes: [String]) -> ToMatchResult { - guard let custom = custom - else { - return userDictionary.customDictionary(includeSdkSetAttributes: false).isNilOrEmpty ? .matched - : .failed(reason: MatcherMessages.dictionaryShouldNotContain + CodingKeys.custom.rawValue) - } - - return failsToMatch(fails: customAttributes?.compactMap { customAttribute in - if !privateAttributes.contains(customAttribute) || custom[customAttribute] == nil { - return messageIfPublicOrMissingAttributeIsRedacted(in: userDictionary.redactedAttributes, for: customAttribute) - } - return nil - } ?? [String]()) - } - - private func messageIfMissingValue(in dictionary: [String: Any], for attribute: String) -> String? { - dictionary[attribute] != nil ? nil : MatcherMessages.dictionaryShouldContain + attribute - } - - private func messageIfValueDoesntMatch(value: Any?, in dictionary: [String: Any], for attribute: String) -> String? { - AnyComparer.isEqual(value, to: dictionary[attribute]) ? nil : MatcherMessages.valuesDontMatch + attribute - } - - private func messageIfAttributeExists(in dictionary: [String: Any], for attribute: String) -> String? { - dictionary[attribute] == nil ? nil : MatcherMessages.dictionaryShouldNotContain + attribute - } - - private func messageIfRedactedAttributeDoesNotExist(in redactedAttributes: [String]?, for attribute: String) -> String? { - guard let redactedAttributes = redactedAttributes - else { - return MatcherMessages.dictionaryShouldContain + CodingKeys.privateAttributes.rawValue - } - return redactedAttributes.contains(attribute) ? nil : MatcherMessages.attributeListShouldContain + attribute - } - - private func messageIfAttributeExists(in redactedAttributes: [String]?, for attribute: String) -> String? { - redactedAttributes?.contains(attribute) != true ? nil : MatcherMessages.attributeListShouldNotContain + attribute - } - - private func messageIfPublicOrMissingAttributeIsRedacted(in redactedAttributes: [String]?, for attribute: String) -> String? { - redactedAttributes?.contains(attribute) != true ? nil : MatcherMessages.attributeListShouldNotContain + attribute - } - public func dictionaryValueWithAllAttributes() -> [String: Any] { var dictionary = dictionaryValue(includePrivateAttributes: true, config: LDConfig.stub) dictionary[CodingKeys.privateAttributes.rawValue] = privateAttributes @@ -1006,24 +432,11 @@ extension LDUser { } } -extension Optional where Wrapped: Collection { - var isNilOrEmpty: Bool { self?.isEmpty ?? true } -} - extension Dictionary where Key == String, Value == Any { fileprivate var redactedAttributes: [String]? { self[LDUser.CodingKeys.privateAttributes.rawValue] as? [String] } - fileprivate func customDictionary(includeSdkSetAttributes: Bool) -> [String: Any]? { - var customDictionary = self[LDUser.CodingKeys.custom.rawValue] as? [String: Any] - if !includeSdkSetAttributes { - customDictionary = customDictionary?.filter { key, _ in - !LDUser.sdkSetAttributes.contains(key) - } - } - return customDictionary - } - fileprivate var flagConfig: [String: Any]? { - self[LDUser.CodingKeys.config.rawValue] as? [LDFlagKey: Any] + fileprivate func customDictionary() -> [String: Any]? { + self[LDUser.CodingKeys.custom.rawValue] as? [String: Any] } } From 1cb2067cff4578faf15c2586c052d508f6e60ef5 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 10 Mar 2022 09:26:44 -0600 Subject: [PATCH 28/50] Simplify event reporting complete callback to not include published dictionary. --- LaunchDarkly/LaunchDarkly/LDClient.swift | 11 ++--- .../ServiceObjects/EventReporter.swift | 21 ++++----- .../ServiceObjects/EventReporterSpec.swift | 46 ++++--------------- 3 files changed, 22 insertions(+), 56 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index a61bcb0f..235d72f0 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -596,13 +596,12 @@ public class LDClient { eventReporter.flush(completion: nil) } - private func onEventSyncComplete(result: EventSyncResult) { - Log.debug(typeName(and: #function) + "result: \(result)") - switch result { - case .success: - break // EventReporter handles removing events from the event store, so there's nothing to do here. It's here in case we want to do something in the future. - case .error(let synchronizingError): + private func onEventSyncComplete(result: SynchronizingError?) { + if let synchronizingError = result { + Log.debug(typeName(and: #function) + "result: \(synchronizingError)") process(synchronizingError, logPrefix: typeName(and: #function, appending: ": ")) + } else { + Log.debug(typeName(and: #function) + "result: success") } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index 5482da04..b799134d 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -1,11 +1,6 @@ import Foundation -enum EventSyncResult { - case success([[String: Any]]) - case error(SynchronizingError) -} - -typealias EventSyncCompleteClosure = ((EventSyncResult) -> Void) +typealias EventSyncCompleteClosure = ((SynchronizingError?) -> Void) // sourcery: autoMockable protocol EventReporting { // sourcery: defaultMockValue = false @@ -114,7 +109,7 @@ class EventReporter: EventReporting { guard isOnline else { Log.debug(typeName(and: #function) + "aborted. EventReporter is offline") - reportSyncComplete(.error(.isOffline)) + reportSyncComplete(.isOffline) completion?() return } @@ -126,7 +121,7 @@ class EventReporter: EventReporting { guard !eventStore.isEmpty else { Log.debug(typeName(and: #function) + "aborted. Event store is empty") - reportSyncComplete(.success([])) + reportSyncComplete(nil) completion?() return } @@ -164,13 +159,13 @@ class EventReporter: EventReporting { if error == nil && (200..<300).contains(response?.statusCode ?? 0) { self.lastEventResponseDate = response?.headerDate ?? self.lastEventResponseDate Log.debug(self.typeName(and: #function) + "Completed sending \(sentEvents.count) event(s)") - self.reportSyncComplete(.success(sentEvents)) + self.reportSyncComplete(nil) return false } if let statusCode = response?.statusCode, (400..<500).contains(statusCode) && ![400, 408, 429].contains(statusCode) { Log.debug(typeName(and: #function) + "dropping events due to non-retriable response: \(String(describing: response))") - self.reportSyncComplete(.error(.response(response))) + self.reportSyncComplete(.response(response)) return false } @@ -179,9 +174,9 @@ class EventReporter: EventReporting { if isRetry { Log.debug(typeName(and: #function) + "dropping events due to failed retry") if let error = error { - reportSyncComplete(.error(.request(error))) + reportSyncComplete(.request(error)) } else { - reportSyncComplete(.error(.response(response))) + reportSyncComplete(.response(response)) } return false } @@ -189,7 +184,7 @@ class EventReporter: EventReporting { return true } - private func reportSyncComplete(_ result: EventSyncResult) { + private func reportSyncComplete(_ result: SynchronizingError?) { // The eventReporter is created when the LDClient singleton is created, and kept for the app's lifetime. So while the use of self in the async block does setup a retain cycle, it's not going to cause a memory leak guard let onSyncComplete = onSyncComplete else { return } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index 3d2686bf..41535605 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -25,7 +25,7 @@ final class EventReporterSpec: QuickSpec { var featureFlagWithReasonAndTrackReason: FeatureFlag! var eventStubResponseDate: Date? var flagRequestTracker: FlagRequestTracker? { eventReporter.flagRequestTracker } - var syncResult: EventSyncResult? = nil + var syncResult: SynchronizingError? = nil var diagnosticCache: DiagnosticCachingMock init(eventCount: Int = 0, @@ -264,12 +264,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.eventStore.isEmpty) == true expect(testContext.eventReporter.lastEventResponseDate) == testContext.eventStubResponseDate expect(testContext.eventReporter.flagRequestTracker.hasLoggedRequests) == false - guard case let .success(result) = testContext.syncResult - else { - fail("Expected event dictionaries in sync result") - return - } - expect(result == testContext.serviceMock.publishedEventDictionaries!).to(beTrue()) + expect(testContext.syncResult).to(beNil()) } } context("with events only") { @@ -297,12 +292,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.eventStore.isEmpty) == true expect(testContext.eventReporter.lastEventResponseDate) == testContext.eventStubResponseDate expect(testContext.eventReporter.flagRequestTracker.hasLoggedRequests) == false - guard case let .success(result) = testContext.syncResult - else { - fail("Expected event dictionaries in sync result") - return - } - expect(result == testContext.serviceMock.publishedEventDictionaries!).to(beTrue()) + expect(testContext.syncResult).to(beNil()) } } context("with tracked requests only") { @@ -329,12 +319,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.eventStore.isEmpty) == true expect(testContext.eventReporter.lastEventResponseDate) == testContext.eventStubResponseDate expect(testContext.eventReporter.flagRequestTracker.hasLoggedRequests) == false - guard case let .success(result) = testContext.syncResult - else { - fail("Expected event dictionaries in sync result") - return - } - expect(result == testContext.serviceMock.publishedEventDictionaries!).to(beTrue()) + expect(testContext.syncResult).to(beNil()) } } context("without events or tracked requests") { @@ -356,12 +341,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.eventStore.isEmpty) == true expect(testContext.eventReporter.lastEventResponseDate).to(beNil()) expect(testContext.eventReporter.flagRequestTracker.hasLoggedRequests) == false - guard case let .success(result) = testContext.syncResult - else { - fail("Expected event dictionaries in sync result") - return - } - expect(result == [[String: Any]]()).to(beTrue()) + expect(testContext.syncResult).to(beNil()) } } } @@ -391,7 +371,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == Event.Kind.nonSummaryKinds.count + 1 expect(testContext.eventReporter.lastEventResponseDate).to(beNil()) expect(testContext.eventReporter.flagRequestTracker.hasLoggedRequests) == false - guard case let .error(.request(error)) = testContext.syncResult + guard case let .request(error) = testContext.syncResult else { fail("Expected error result for event send") return @@ -425,7 +405,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.lastEventResponseDate).to(beNil()) expect(testContext.eventReporter.flagRequestTracker.hasLoggedRequests) == false let expectedError = testContext.serviceMock.errorEventHTTPURLResponse - guard case let .error(.response(error)) = testContext.syncResult + guard case let .response(error) = testContext.syncResult else { fail("Expected error result for event send") return @@ -460,7 +440,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == Event.Kind.nonSummaryKinds.count + 1 expect(testContext.eventReporter.lastEventResponseDate).to(beNil()) expect(testContext.eventReporter.flagRequestTracker.hasLoggedRequests) == false - guard case let .error(.request(error)) = testContext.syncResult + guard case let .request(error) = testContext.syncResult else { fail("Expected error result for event send") return @@ -491,7 +471,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.eventStoreKinds.contains(.summary)) == false expect(testContext.eventReporter.lastEventResponseDate).to(beNil()) expect(testContext.eventReporter.flagRequestTracker.hasLoggedRequests) == true - guard case .error(.isOffline) = testContext.syncResult + guard case .isOffline = testContext.syncResult else { fail("Expected error .isOffline result for event send") return @@ -861,11 +841,3 @@ extension Event.Kind { [feature, debug, identify, custom] } } - -// Performs set-wise equality, without ordering -extension Array where Element == [String: Any] { - static func == (_ lhs: [[String: Any]], _ rhs: [[String: Any]]) -> Bool { - // Same length and the left hand side does not contain any elements not in the right hand side - lhs.count == rhs.count && !lhs.contains { lhse in !rhs.contains { AnyComparer.isEqual($0, to: lhse) } } - } -} From 096ad3e31f1ffd644194321be6e5e3dce75f1924 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 10 Mar 2022 10:47:23 -0600 Subject: [PATCH 29/50] Add UserAttribute class. --- LaunchDarkly.xcodeproj/project.pbxproj | 10 +++ .../LaunchDarkly/Models/LDConfig.swift | 7 +- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 70 +++++-------------- .../LaunchDarkly/Models/UserAttribute.swift | 50 +++++++++++++ .../ObjectiveC/ObjcLDConfig.swift | 6 +- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 16 +---- .../Models/LDConfigSpec.swift | 6 +- .../Models/User/LDUserSpec.swift | 39 ++++++----- 8 files changed, 111 insertions(+), 93 deletions(-) create mode 100644 LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 67d96487..f50ec76a 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 29A4C47527DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; + 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; + 29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; + 29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */; }; 830DB3AE2239B54900D65D25 /* URLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AD2239B54900D65D25 /* URLResponse.swift */; }; @@ -346,6 +350,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 29A4C47427DA6266005B8D34 /* UserAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAttribute.swift; sourceTree = ""; }; 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLRequest.swift; sourceTree = ""; }; 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersSpec.swift; sourceTree = ""; }; 830DB3AD2239B54900D65D25 /* URLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponse.swift; sourceTree = ""; }; @@ -671,6 +676,7 @@ 83EBCB9D20D9A0A1003A7142 /* FeatureFlag */, 8354EFDD1F26380700C05156 /* LDConfig.swift */, 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */, + 29A4C47427DA6266005B8D34 /* UserAttribute.swift */, ); path = Models; sourceTree = ""; @@ -1216,6 +1222,7 @@ 8311885A2113AE1500D77CB5 /* Log.swift in Sources */, 8311884B2113ADDA00D77CB5 /* LDChangedFlag.swift in Sources */, 8311885E2113AE2900D77CB5 /* HTTPURLResponse.swift in Sources */, + 29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */, 8347BB0F21F147E100E56BCD /* LDTimer.swift in Sources */, B4C9D43B2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */, @@ -1275,6 +1282,7 @@ 831EF35A20655E730001C643 /* HTTPHeaders.swift in Sources */, 831EF35B20655E730001C643 /* DarklyService.swift in Sources */, 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */, + 29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */, 8354AC6B22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, C443A40723145FEE00145710 /* ConnectionInformationStore.swift in Sources */, 831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */, @@ -1339,6 +1347,7 @@ 831D2AAF2061AAA000B4AC3C /* Thread.swift in Sources */, 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */, 8354EFE01F26380700C05156 /* LDClient.swift in Sources */, + 29A4C47527DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B1206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, C408884723033B3600420721 /* ConnectionInformationStore.swift in Sources */, 83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */, @@ -1451,6 +1460,7 @@ 83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */, 83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */, 83D9EC902062DEAB004D7FA6 /* Dictionary.swift in Sources */, + 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B2206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */, 8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 3894a894..fe98ba7a 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -68,7 +68,7 @@ public struct LDConfig { /// The default setting for private user attributes. (false) static let allUserAttributesPrivate = false /// The default private user attribute list (nil) - static let privateUserAttributes: [String]? = nil + static let privateUserAttributes: [UserAttribute] = [] /// The default HTTP request method for stream connections and feature flag requests. When true, these requests will use the non-standard verb `REPORT`. When false, these requests will use the standard verb `GET`. (false) static let useReport = false @@ -213,7 +213,7 @@ public struct LDConfig { See Also: `allUserAttributesPrivate`, `LDUser.privatizableAttributes`, and `LDUser.privateAttributes`. */ - public var privateUserAttributes: [String]? = Defaults.privateUserAttributes + public var privateUserAttributes: [UserAttribute] = Defaults.privateUserAttributes /** Directs the SDK to use REPORT for HTTP requests for feature flag data. (Default: `false`) @@ -368,8 +368,7 @@ extension LDConfig: Equatable { && lhs.enableBackgroundUpdates == rhs.enableBackgroundUpdates && lhs.startOnline == rhs.startOnline && lhs.allUserAttributesPrivate == rhs.allUserAttributesPrivate - && (lhs.privateUserAttributes == nil && rhs.privateUserAttributes == nil - || (lhs.privateUserAttributes != nil && rhs.privateUserAttributes != nil && lhs.privateUserAttributes! == rhs.privateUserAttributes!)) + && Set(lhs.privateUserAttributes) == Set(rhs.privateUserAttributes) && lhs.useReport == rhs.useReport && lhs.inlineUserInEvents == rhs.inlineUserInEvents && lhs.isDebugMode == rhs.isDebugMode diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 92b78b1c..ddca111d 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -1,6 +1,7 @@ import Foundation typealias UserKey = String // use for identifying semantics for strings, particularly in dictionaries + /** LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. For example, the client app may launch with the SDK defined anonymous user. As the user works with the client app, information may be collected as needed and sent to LaunchDarkly. The client app controls the information collected, which LaunchDarkly does not use except as the client directs to refine feature flags. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. The SDK caches last known feature flags for use on app startup to provide continuity with the last app run. Provided the LDClient is online and can establish a connection with LaunchDarkly servers, cached information will only be used a very short time. Once the latest feature flags arrive at the SDK, the SDK no longer uses cached feature flags. The SDK retains feature flags on the last 5 client defined users. The SDK will retain feature flags until they are overwritten by a different user's feature flags, or until the user removes the app from the device. @@ -14,21 +15,7 @@ public struct LDUser { case key, name, firstName, lastName, country, ipAddress = "ip", email, avatar, custom, isAnonymous = "anonymous", device, operatingSystem = "os", config, privateAttributes = "privateAttrs", secondary } - /** - LDUser attributes that can be marked private. - The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - See Also: `LDConfig.allUserAttributesPrivate`, `LDConfig.privateUserAttributes`, and `privateAttributes`. - */ - public static var privatizableAttributes: [String] { optionalAttributes } - - static let optionalAttributes = [CodingKeys.name.rawValue, CodingKeys.firstName.rawValue, - CodingKeys.lastName.rawValue, CodingKeys.country.rawValue, - CodingKeys.ipAddress.rawValue, CodingKeys.email.rawValue, - CodingKeys.avatar.rawValue, CodingKeys.secondary.rawValue] - - static var sdkSetAttributes: [String] { - [CodingKeys.device.rawValue, CodingKeys.operatingSystem.rawValue] - } + static let optionalAttributes = UserAttribute.BuiltIn.allBuiltIns.filter { $0.name != "key" && $0.name != "anonymous"} static let storedIdKey: String = "ldDeviceIdentifier" @@ -61,7 +48,7 @@ public struct LDUser { This attribute is ignored if `LDConfig.allUserAttributesPrivate` is true. Combined with `LDConfig.privateUserAttributes`. The SDK considers attributes appearing in either list as private. Client apps may define attributes found in `privatizableAttributes` and top level `custom` dictionary keys here. (Default: nil) See Also: `LDConfig.allUserAttributesPrivate` and `LDConfig.privateUserAttributes`. */ - public var privateAttributes: [String]? + public var privateAttributes: [UserAttribute] /// An NSObject wrapper for the Swift LDUser struct. Intended for use in mixed apps when Swift code needs to pass a user into an Objective-C method. public var objcLdUser: ObjcLDUser { ObjcLDUser(self) } @@ -93,7 +80,7 @@ public struct LDUser { avatar: String? = nil, custom: [String: Any]? = nil, isAnonymous: Bool? = nil, - privateAttributes: [String]? = nil, + privateAttributes: [UserAttribute]? = nil, secondary: String? = nil) { let environmentReporter = EnvironmentReporter() let selectedKey = key ?? LDUser.defaultKey(environmentReporter: environmentReporter) @@ -110,12 +97,12 @@ public struct LDUser { self.custom = custom ?? [:] self.custom.merge([CodingKeys.device.rawValue: environmentReporter.deviceModel, CodingKeys.operatingSystem.rawValue: environmentReporter.systemVersion]) { lhs, _ in lhs } - self.privateAttributes = privateAttributes + self.privateAttributes = privateAttributes ?? [] Log.debug(typeName(and: #function) + "user: \(self)") } /** - Initializer that takes a [String: Any] and creates a LDUser from the contents. Uses any keys present to define corresponding attribute values. Initializes attributes not present in the dictionary to their default value. Attempts to set `device` and `operatingSystem` from corresponding values embedded in `custom`. DEPRECATED: Attempts to set feature flags from values set in `config`. + Initializer that takes a [String: Any] and creates a LDUser from the contents. Uses any keys present to define corresponding attribute values. Initializes attributes not present in the dictionary to their default value. Attempts to set `device` and `operatingSystem` from corresponding values embedded in `custom`. - parameter userDictionary: Dictionary with LDUser attribute keys and values. */ public init(userDictionary: [String: Any]) { @@ -130,7 +117,11 @@ public struct LDUser { ipAddress = userDictionary[CodingKeys.ipAddress.rawValue] as? String email = userDictionary[CodingKeys.email.rawValue] as? String avatar = userDictionary[CodingKeys.avatar.rawValue] as? String - privateAttributes = userDictionary[CodingKeys.privateAttributes.rawValue] as? [String] + if let privateAttrs = (userDictionary[CodingKeys.privateAttributes.rawValue] as? [String]) { + privateAttributes = privateAttrs.map { UserAttribute.forName($0) } + } else { + privateAttributes = [] + } custom = userDictionary[CodingKeys.custom.rawValue] as? [String: Any] ?? [:] Log.debug(typeName(and: #function) + "user: \(self)") @@ -146,29 +137,11 @@ public struct LDUser { isAnonymous: true) } - // swiftlint:disable:next cyclomatic_complexity - private func value(for attribute: String) -> Any? { - switch attribute { - case CodingKeys.key.rawValue: return key - case CodingKeys.secondary.rawValue: return secondary - case CodingKeys.isAnonymous.rawValue: return isAnonymous - case CodingKeys.name.rawValue: return name - case CodingKeys.firstName.rawValue: return firstName - case CodingKeys.lastName.rawValue: return lastName - case CodingKeys.country.rawValue: return country - case CodingKeys.ipAddress.rawValue: return ipAddress - case CodingKeys.email.rawValue: return email - case CodingKeys.avatar.rawValue: return avatar - case CodingKeys.custom.rawValue: return custom - case CodingKeys.device.rawValue: return custom[CodingKeys.device.rawValue] - case CodingKeys.operatingSystem.rawValue: return custom[CodingKeys.operatingSystem.rawValue] - case CodingKeys.privateAttributes.rawValue: return privateAttributes - default: return nil + private func value(for attribute: UserAttribute) -> Any? { + if let builtInGetter = attribute.builtInGetter { + return builtInGetter(self) } - } - /// Returns the custom dictionary without the SDK set device and operatingSystem attributes - var customWithoutSdkSetAttributes: [String: Any] { - custom.filter { key, _ in !LDUser.sdkSetAttributes.contains(key) } + return custom[attribute.name] } /// Dictionary with LDUser attribute keys and values, with options to include feature flags and private attributes. LDConfig object used to help resolving what attributes should be private. @@ -176,7 +149,7 @@ public struct LDUser { /// - parameter config: Provides supporting information for defining private attributes func dictionaryValue(includePrivateAttributes includePrivate: Bool, config: LDConfig) -> [String: Any] { let allPrivate = !includePrivate && config.allUserAttributesPrivate - let privateAttributeNames = includePrivate ? [] : (privateAttributes ?? []) + (config.privateUserAttributes ?? []) + let privateAttributeNames = includePrivate ? [] : (privateAttributes + config.privateUserAttributes).map { $0.name } var dictionary: [String: Any] = [:] var redactedAttributes: [String] = [] @@ -186,10 +159,10 @@ public struct LDUser { LDUser.optionalAttributes.forEach { attribute in if let value = self.value(for: attribute) { - if allPrivate || privateAttributeNames.contains(attribute) { - redactedAttributes.append(attribute) + if allPrivate || privateAttributeNames.contains(attribute.name) { + redactedAttributes.append(attribute.name) } else { - dictionary[attribute] = value + dictionary[attribute.name] = value } } } @@ -255,11 +228,6 @@ extension LDUser: TypeIdentifying { } #if DEBUG extension LDUser { - /// Testing method to get the user attribute value from a LDUser struct - func value(forAttribute attribute: String) -> Any? { - value(for: attribute) - } - // Compares all user properties. Excludes the composed FlagStore, which contains the users feature flags func isEqual(to otherUser: LDUser) -> Bool { key == otherUser.key diff --git a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift new file mode 100644 index 00000000..cf08e89f --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift @@ -0,0 +1,50 @@ +import Foundation + +public class UserAttribute: Equatable, Hashable { + + public struct BuiltIn { + public static let key = UserAttribute("key") { $0.key } + public static let secondaryKey = UserAttribute("secondary") { $0.secondary } + // swiftlint:disable:next identifier_name + public static let ip = UserAttribute("ip") { $0.ipAddress } + public static let email = UserAttribute("email") { $0.email } + public static let name = UserAttribute("name") { $0.name } + public static let avatar = UserAttribute("avatar") { $0.avatar } + public static let firstName = UserAttribute("firstName") { $0.firstName } + public static let lastName = UserAttribute("lastName") { $0.lastName } + public static let country = UserAttribute("country") { $0.country } + public static let anonymous = UserAttribute("anonymous") { $0.isAnonymous } + + static let allBuiltIns = [key, secondaryKey, ip, email, name, avatar, firstName, lastName, country, anonymous] + } + + static var builtInMap = { return BuiltIn.allBuiltIns.reduce(into: [:]) { $0[$1.name] = $1 } }() + + public static func forName(_ name: String) -> UserAttribute { + if let builtIn = builtInMap[name] { + return builtIn + } + return UserAttribute(name) + } + + let name: String + let builtInGetter: ((LDUser) -> Any?)? + + init(_ name: String, builtInGetter: ((LDUser) -> Any?)? = nil) { + self.name = name + self.builtInGetter = builtInGetter + } + + public var isBuiltIn: Bool { builtInGetter != nil } + + public static func == (lhs: UserAttribute, rhs: UserAttribute) -> Bool { + if lhs.isBuiltIn || rhs.isBuiltIn { + return lhs === rhs + } + return lhs.name == rhs.name + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + } +} diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index 4244abfe..b1e7ec64 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -105,9 +105,9 @@ public final class ObjcLDConfig: NSObject { See Also: `allUserAttributesPrivate`, `LDUser.privatizableAttributes` (`ObjcLDUser.privatizableAttributes`), and `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). */ - @objc public var privateUserAttributes: [String]? { - get { config.privateUserAttributes } - set { config.privateUserAttributes = newValue } + @objc public var privateUserAttributes: [String] { + get { config.privateUserAttributes.map { $0.name } } + set { config.privateUserAttributes = newValue.map { UserAttribute.forName($0) } } } /** diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index 46cc2985..9dfe4892 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -11,16 +11,6 @@ import Foundation public final class ObjcLDUser: NSObject { var user: LDUser - /** - LDUser attributes that can be marked private. - - The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - - See Also: `ObjcLDConfig.allUserAttributesPrivate`, `ObjcLDConfig.privateUserAttributes`, and `privateAttributes`. - */ - @objc public class var privatizableAttributes: [String] { - LDUser.privatizableAttributes - } /// LDUser secondary attribute used to make `secondary` private @objc public class var attributeSecondary: String { LDUser.CodingKeys.secondary.rawValue @@ -123,9 +113,9 @@ public final class ObjcLDUser: NSObject { See Also: `ObjcLDConfig.allUserAttributesPrivate` and `ObjcLDConfig.privateUserAttributes`. */ - @objc public var privateAttributes: [String]? { - get { user.privateAttributes } - set { user.privateAttributes = newValue } + @objc public var privateAttributes: [String] { + get { user.privateAttributes.map { $0.name } } + set { user.privateAttributes = newValue.map { UserAttribute.forName($0) } } } /** diff --git a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index 107ab2fe..baca06dc 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -17,7 +17,7 @@ final class LDConfigSpec: XCTestCase { fileprivate static let startOnline = false fileprivate static let allUserAttributesPrivate = true - fileprivate static let privateUserAttributes: [String] = ["dummy"] + fileprivate static let privateUserAttributes: [UserAttribute] = [UserAttribute.forName("dummy")] fileprivate static let useReport = true @@ -49,7 +49,7 @@ final class LDConfigSpec: XCTestCase { ("start online", Constants.startOnline, { c, v in c.startOnline = v as! Bool }), ("debug mode", Constants.debugMode, { c, v in c.isDebugMode = v as! Bool }), ("all user attributes private", Constants.allUserAttributesPrivate, { c, v in c.allUserAttributesPrivate = v as! Bool }), - ("private user attributes", Constants.privateUserAttributes, { c, v in c.privateUserAttributes = (v as! [String])}), + ("private user attributes", Constants.privateUserAttributes, { c, v in c.privateUserAttributes = (v as! [UserAttribute])}), ("use report", Constants.useReport, { c, v in c.useReport = v as! Bool }), ("inline user in events", Constants.inlineUserInEvents, { c, v in c.inlineUserInEvents = v as! Bool }), ("evaluation reasons", Constants.evaluationReasons, { c, v in c.evaluationReasons = v as! Bool }), @@ -76,7 +76,7 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.enableBackgroundUpdates, LDConfig.Defaults.enableBackgroundUpdates) XCTAssertEqual(config.startOnline, LDConfig.Defaults.startOnline) XCTAssertEqual(config.allUserAttributesPrivate, LDConfig.Defaults.allUserAttributesPrivate) - XCTAssertEqual(config.privateUserAttributes, nil) + XCTAssertEqual(config.privateUserAttributes, LDConfig.Defaults.privateUserAttributes) XCTAssertEqual(config.useReport, LDConfig.Defaults.useReport) XCTAssertEqual(config.inlineUserInEvents, LDConfig.Defaults.inlineUserInEvents) XCTAssertEqual(config.isDebugMode, LDConfig.Defaults.debugMode) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 28b2791f..3f94d819 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -31,7 +31,7 @@ final class LDUserSpec: QuickSpec { avatar: LDUser.StubConstants.avatar, custom: LDUser.StubConstants.custom(includeSystemValues: true), isAnonymous: LDUser.StubConstants.isAnonymous, - privateAttributes: LDUser.privatizableAttributes, + privateAttributes: LDUser.optionalAttributes, secondary: LDUser.StubConstants.secondary) expect(user.key) == LDUser.StubConstants.key expect(user.secondary) == LDUser.StubConstants.secondary @@ -44,7 +44,7 @@ final class LDUserSpec: QuickSpec { expect(user.email) == LDUser.StubConstants.email expect(user.avatar) == LDUser.StubConstants.avatar expect(user.custom == LDUser.StubConstants.custom(includeSystemValues: true)).to(beTrue()) - expect(user.privateAttributes) == LDUser.privatizableAttributes + expect(user.privateAttributes) == LDUser.optionalAttributes } context("called without optional elements") { var environmentReporter: EnvironmentReporter! @@ -66,7 +66,7 @@ final class LDUserSpec: QuickSpec { expect(user.custom.count) == 2 expect(user.custom[LDUser.CodingKeys.device.rawValue] as? String) == environmentReporter.deviceModel expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue] as? String) == environmentReporter.systemVersion - expect(user.privateAttributes).to(beNil()) + expect(user.privateAttributes).to(beEmpty()) expect(user.secondary).to(beNil()) } } @@ -96,7 +96,7 @@ final class LDUserSpec: QuickSpec { beforeEach { originalUser = LDUser.stub() var userDictionary = originalUser.dictionaryValue(includePrivateAttributes: true, config: LDConfig.stub) - userDictionary[LDUser.CodingKeys.privateAttributes.rawValue] = LDUser.privatizableAttributes + userDictionary[LDUser.CodingKeys.privateAttributes.rawValue] = LDUser.optionalAttributes.map { $0.name } user = LDUser(userDictionary: userDictionary) } it("creates a user with optional elements and feature flags") { @@ -111,7 +111,7 @@ final class LDUserSpec: QuickSpec { expect(user.email) == originalUser.email expect(user.avatar) == originalUser.avatar expect(user.custom == originalUser.custom).to(beTrue()) - expect(user.privateAttributes) == LDUser.privatizableAttributes + expect(user.privateAttributes) == LDUser.optionalAttributes } } context("without optional elements") { @@ -137,7 +137,7 @@ final class LDUserSpec: QuickSpec { expect(user.custom.count) == 2 expect(user.custom[LDUser.CodingKeys.device.rawValue] as? String) == EnvironmentReporter().deviceModel expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue] as? String) == EnvironmentReporter().systemVersion - expect(user.privateAttributes).to(beNil()) + expect(user.privateAttributes).to(beEmpty()) } } context("with empty dictionary") { @@ -156,7 +156,7 @@ final class LDUserSpec: QuickSpec { expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) expect(user.custom).to(beEmpty()) - expect(user.privateAttributes).to(beNil()) + expect(user.privateAttributes).to(beEmpty()) } } } @@ -186,12 +186,13 @@ final class LDUserSpec: QuickSpec { expect(user.custom[LDUser.CodingKeys.device.rawValue] as? String) == environmentReporter.deviceModel expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue] as? String) == environmentReporter.systemVersion - expect(user.privateAttributes).to(beNil()) + expect(user.privateAttributes).to(beEmpty()) } } } private func dictionaryValueSpec() { + let optionalNames = LDUser.optionalAttributes.map { $0.name } let allCustomPrivitizable = Array(LDUser.StubConstants.custom(includeSystemValues: true).keys) describe("dictionaryValue") { @@ -234,14 +235,14 @@ final class LDUserSpec: QuickSpec { } context("privatizing all individually in config") { beforeEach { - config.privateUserAttributes = LDUser.privatizableAttributes + ["customAttr"] + config.privateUserAttributes = LDUser.optionalAttributes + [UserAttribute.forName("customAttr")] userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) } testCase() } context("privatizing all individually on user") { beforeEach { - user.privateAttributes = LDUser.privatizableAttributes + ["customAttr"] + user.privateAttributes = LDUser.optionalAttributes + [UserAttribute.forName("customAttr")] userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) } testCase() @@ -250,8 +251,8 @@ final class LDUserSpec: QuickSpec { it("includePrivateAttributes always includes attributes") { config.allUserAttributesPrivate = true - config.privateUserAttributes = LDUser.privatizableAttributes + allCustomPrivitizable - user.privateAttributes = LDUser.privatizableAttributes + allCustomPrivitizable + config.privateUserAttributes = LDUser.optionalAttributes + allCustomPrivitizable.map { UserAttribute.forName($0) } + user.privateAttributes = LDUser.optionalAttributes + allCustomPrivitizable.map { UserAttribute.forName($0) } let userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) expect(userDictionary.count) == 11 @@ -283,8 +284,8 @@ final class LDUserSpec: QuickSpec { } [false, true].forEach { isCustomAttr in - (isCustomAttr ? Array(LDUser.StubConstants.custom(includeSystemValues: true).keys) - : LDUser.privatizableAttributes).forEach { privateAttr in + (isCustomAttr ? LDUser.StubConstants.custom(includeSystemValues: true).keys.map { UserAttribute.forName($0) } + : LDUser.optionalAttributes).forEach { privateAttr in [false, true].forEach { inConfig in it("with \(privateAttr) private in \(inConfig ? "config" : "user")") { if inConfig { @@ -295,18 +296,18 @@ final class LDUserSpec: QuickSpec { userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - expect(userDictionary.redactedAttributes) == [privateAttr] + expect(userDictionary.redactedAttributes) == [privateAttr.name] let includingDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) if !isCustomAttr { let userDictionaryWithoutRedacted = userDictionary.filter { $0.key != "privateAttrs" } - let includingDictionaryWithoutRedacted = includingDictionary.filter { $0.key != privateAttr && $0.key != "privateAttrs" } + let includingDictionaryWithoutRedacted = includingDictionary.filter { $0.key != privateAttr.name && $0.key != "privateAttrs" } expect(AnyComparer.isEqual(userDictionaryWithoutRedacted, to: includingDictionaryWithoutRedacted)) == true } else { let userDictionaryWithoutRedacted = userDictionary.filter { $0.key != "custom" && $0.key != "privateAttrs" } let includingDictionaryWithoutRedacted = includingDictionary.filter { $0.key != "custom" && $0.key != "privateAttrs" } expect(AnyComparer.isEqual(userDictionaryWithoutRedacted, to: includingDictionaryWithoutRedacted)) == true - let expectedCustom = (includingDictionary["custom"] as! [String: Any]).filter { $0.key != privateAttr } + let expectedCustom = (includingDictionary["custom"] as! [String: Any]).filter { $0.key != privateAttr.name } expect(AnyComparer.isEqual(userDictionary["custom"], to: expectedCustom)) == true } } @@ -325,7 +326,7 @@ final class LDUserSpec: QuickSpec { expect(userDictionary[LDUser.CodingKeys.key.rawValue] as? String) == user.key expect(userDictionary[LDUser.CodingKeys.isAnonymous.rawValue] as? Bool) == user.isAnonymous - expect(Set(userDictionary.redactedAttributes!)) == Set(LDUser.privatizableAttributes + allCustomPrivitizable) + expect(Set(userDictionary.redactedAttributes!)) == Set(optionalNames + allCustomPrivitizable) } } @@ -389,7 +390,7 @@ final class LDUserSpec: QuickSpec { ("avatar", true, "dummy", { u, v in u.avatar = v as! String? }), ("custom", false, ["dummy": true], { u, v in u.custom = v as! [String: Any] }), ("isAnonymous", false, true, { u, v in u.isAnonymous = v as! Bool }), - ("privateAttributes", false, ["dummy"], { u, v in u.privateAttributes = v as! [String]? })] + ("privateAttributes", false, [UserAttribute.forName("dummy")], { u, v in u.privateAttributes = v as! [UserAttribute] })] testFields.forEach { name, isOptional, otherVal, setter in context("\(name) differs") { beforeEach { From 99256d5d9598c476d7cc4918726285c8c4b97783 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 10 Mar 2022 11:23:54 -0600 Subject: [PATCH 30/50] Remove Dictionary initializer for LDUser. --- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 26 ------- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 9 --- .../LaunchDarklyTests/Models/EventSpec.swift | 22 ++---- .../Models/User/LDUserSpec.swift | 75 ------------------- .../Networking/DarklyServiceSpec.swift | 37 +++------ 5 files changed, 20 insertions(+), 149 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index ddca111d..33224580 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -101,32 +101,6 @@ public struct LDUser { Log.debug(typeName(and: #function) + "user: \(self)") } - /** - Initializer that takes a [String: Any] and creates a LDUser from the contents. Uses any keys present to define corresponding attribute values. Initializes attributes not present in the dictionary to their default value. Attempts to set `device` and `operatingSystem` from corresponding values embedded in `custom`. - - parameter userDictionary: Dictionary with LDUser attribute keys and values. - */ - public init(userDictionary: [String: Any]) { - key = userDictionary[CodingKeys.key.rawValue] as? String ?? LDUser.defaultKey(environmentReporter: EnvironmentReporter()) - secondary = userDictionary[CodingKeys.secondary.rawValue] as? String - isAnonymous = userDictionary[CodingKeys.isAnonymous.rawValue] as? Bool ?? false - - name = userDictionary[CodingKeys.name.rawValue] as? String - firstName = userDictionary[CodingKeys.firstName.rawValue] as? String - lastName = userDictionary[CodingKeys.lastName.rawValue] as? String - country = userDictionary[CodingKeys.country.rawValue] as? String - ipAddress = userDictionary[CodingKeys.ipAddress.rawValue] as? String - email = userDictionary[CodingKeys.email.rawValue] as? String - avatar = userDictionary[CodingKeys.avatar.rawValue] as? String - if let privateAttrs = (userDictionary[CodingKeys.privateAttributes.rawValue] as? [String]) { - privateAttributes = privateAttrs.map { UserAttribute.forName($0) } - } else { - privateAttributes = [] - } - custom = userDictionary[CodingKeys.custom.rawValue] as? [String: Any] ?? [:] - - Log.debug(typeName(and: #function) + "user: \(self)") - } - /** Internal initializer that accepts an environment reporter, used for testing */ diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index 9dfe4892..6541773d 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -139,15 +139,6 @@ public final class ObjcLDUser: NSObject { self.user = user } - /** - Initializer that takes a NSDictionary and creates a LDUser from the contents. Uses any keys present to define corresponding attribute values. Initializes attributes not present in the dictionary to their default value. The initializer attempts to set `device` and `operatingSystem` from corresponding values embedded in `custom`. The initializer attempts to set feature flags from values set in `config`. - - - parameter userDictionary: NSDictionary with LDUser attribute keys and values. - */ - @objc public init(userDictionary: [String: Any]) { - self.user = LDUser(userDictionary: userDictionary) - } - /// Compares users by comparing their user keys only, to allow the client app to collect user information over time @objc public func isEqual(object: Any) -> Bool { guard let otherUser = object as? ObjcLDUser diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 75b4efe8..49af62bd 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -332,7 +332,7 @@ final class EventSpec: QuickSpec { } it("creates a dictionary with the user key only") { expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUser).to(beNil()) + expect(eventDictionary.eventUserDictionary).to(beNil()) } } context("inlining user and without reason") { @@ -370,7 +370,7 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUser).to(beNil()) + expect(eventDictionary.eventUserDictionary).to(beNil()) expect(eventDictionary.eventVariation) == featureFlag.variation expect(eventDictionary.eventVersion) == featureFlag.version expect(eventDictionary.eventData).to(beNil()) @@ -389,7 +389,7 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUser).to(beNil()) + expect(eventDictionary.eventUserDictionary).to(beNil()) expect(eventDictionary.eventVariation) == featureFlag.variation expect(eventDictionary.eventVersion).to(beNil()) expect(eventDictionary.eventData).to(beNil()) @@ -413,7 +413,7 @@ final class EventSpec: QuickSpec { } it("creates a dictionary with the user key only") { expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUser).to(beNil()) + expect(eventDictionary.eventUserDictionary).to(beNil()) } } it("creates a dictionary with contextKind for anonymous user") { @@ -519,7 +519,7 @@ final class EventSpec: QuickSpec { expect(eventDictionary.eventDefaultValue).to(beNil()) expect(eventDictionary.eventVariation).to(beNil()) expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUser).to(beNil()) + expect(eventDictionary.eventUserDictionary).to(beNil()) expect(eventDictionary.eventMetricValue) == metricValue } } @@ -544,7 +544,7 @@ final class EventSpec: QuickSpec { expect(eventDictionary.eventDefaultValue).to(beNil()) expect(eventDictionary.eventVariation).to(beNil()) expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUser).to(beNil()) + expect(eventDictionary.eventUserDictionary).to(beNil()) } } context("without inlining user") { @@ -570,7 +570,7 @@ final class EventSpec: QuickSpec { } it("creates a dictionary with the user key only") { expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUser).to(beNil()) + expect(eventDictionary.eventUserDictionary).to(beNil()) } } context("inlining user") { @@ -746,7 +746,7 @@ final class EventSpec: QuickSpec { expect(eventDictionary.eventKey).to(beNil()) expect(eventDictionary.eventCreationDate).to(beNil()) - expect(eventDictionary.eventUser).to(beNil()) + expect(eventDictionary.eventUserDictionary).to(beNil()) expect(eventDictionary.eventUserKey).to(beNil()) expect(eventDictionary.eventValue).to(beNil()) expect(eventDictionary.eventDefaultValue).to(beNil()) @@ -842,12 +842,6 @@ extension Dictionary where Key == String, Value == Any { var eventUserKey: String? { self[Event.CodingKeys.userKey.rawValue] as? String } - fileprivate var eventUser: LDUser? { - if let userDictionary = eventUserDictionary { - return LDUser(userDictionary: userDictionary) - } - return nil - } fileprivate var eventUserDictionary: [String: Any]? { self[Event.CodingKeys.user.rawValue] as? [String: Any] } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 3f94d819..1b6193ec 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -13,7 +13,6 @@ final class LDUserSpec: QuickSpec { private func initSpec() { initSubSpec() - initFromDictionarySpec() initWithEnvironmentReporterSpec() } @@ -88,80 +87,6 @@ final class LDUserSpec: QuickSpec { } } - private func initFromDictionarySpec() { - describe("init from dictionary") { - var user: LDUser! - var originalUser: LDUser! - context("and optional elements") { - beforeEach { - originalUser = LDUser.stub() - var userDictionary = originalUser.dictionaryValue(includePrivateAttributes: true, config: LDConfig.stub) - userDictionary[LDUser.CodingKeys.privateAttributes.rawValue] = LDUser.optionalAttributes.map { $0.name } - user = LDUser(userDictionary: userDictionary) - } - it("creates a user with optional elements and feature flags") { - expect(user.key) == originalUser.key - expect(user.secondary) == originalUser.secondary - expect(user.name) == originalUser.name - expect(user.firstName) == originalUser.firstName - expect(user.lastName) == originalUser.lastName - expect(user.isAnonymous) == originalUser.isAnonymous - expect(user.country) == originalUser.country - expect(user.ipAddress) == originalUser.ipAddress - expect(user.email) == originalUser.email - expect(user.avatar) == originalUser.avatar - expect(user.custom == originalUser.custom).to(beTrue()) - expect(user.privateAttributes) == LDUser.optionalAttributes - } - } - context("without optional elements") { - beforeEach { - originalUser = LDUser(isAnonymous: true) - var userDictionary = originalUser.dictionaryValue(includePrivateAttributes: true, config: LDConfig.stub) - userDictionary[LDUser.CodingKeys.privateAttributes.rawValue] = originalUser.privateAttributes - user = LDUser(userDictionary: userDictionary) - } - it("creates a user without optional elements") { - expect(user.key) == originalUser.key - expect(user.isAnonymous) == originalUser.isAnonymous - - expect(user.name).to(beNil()) - expect(user.firstName).to(beNil()) - expect(user.lastName).to(beNil()) - expect(user.country).to(beNil()) - expect(user.ipAddress).to(beNil()) - expect(user.email).to(beNil()) - expect(user.avatar).to(beNil()) - expect(user.secondary).to(beNil()) - - expect(user.custom.count) == 2 - expect(user.custom[LDUser.CodingKeys.device.rawValue] as? String) == EnvironmentReporter().deviceModel - expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue] as? String) == EnvironmentReporter().systemVersion - expect(user.privateAttributes).to(beEmpty()) - } - } - context("with empty dictionary") { - it("creates a user without optional elements or feature flags") { - user = LDUser(userDictionary: [:]) - expect(user.key).toNot(beNil()) - expect(user.key.isEmpty).to(beFalse()) - expect(user.isAnonymous) == false - - expect(user.secondary).to(beNil()) - expect(user.name).to(beNil()) - expect(user.firstName).to(beNil()) - expect(user.lastName).to(beNil()) - expect(user.country).to(beNil()) - expect(user.ipAddress).to(beNil()) - expect(user.email).to(beNil()) - expect(user.avatar).to(beNil()) - expect(user.custom).to(beEmpty()) - expect(user.privateAttributes).to(beEmpty()) - } - } - } - } - private func initWithEnvironmentReporterSpec() { describe("initWithEnvironmentReporter") { var user: LDUser! diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index 3fb870db..6a2262ac 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -119,12 +119,8 @@ final class DarklyServiceSpec: QuickSpec { expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) - if let encodedUserString = urlRequest?.url?.lastPathComponent, - let decodedUser = LDUser(base64urlEncodedString: encodedUserString) { - expect(decodedUser.isEqual(to: testContext.user)) == true - } else { - fail("encoded user string did not create a user") - } + let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) + expect(AnyComparer.isEqual(urlRequest?.url?.lastPathComponent.jsonDictionary, to: expectedUser)) == true } else { fail("request path is missing") } @@ -176,12 +172,8 @@ final class DarklyServiceSpec: QuickSpec { expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) - if let encodedUserString = urlRequest?.url?.lastPathComponent, - let decodedUser = LDUser(base64urlEncodedString: encodedUserString) { - expect(decodedUser.isEqual(to: testContext.user)) == true - } else { - fail("encoded user string did not create a user") - } + let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) + expect(AnyComparer.isEqual(urlRequest?.url?.lastPathComponent.jsonDictionary, to: expectedUser)) == true } else { fail("request path is missing") } @@ -555,7 +547,8 @@ final class DarklyServiceSpec: QuickSpec { let receivedArguments = testContext.serviceFactoryMock.makeStreamingProviderReceivedArguments expect(receivedArguments!.url.host) == testContext.config.streamUrl.host expect(receivedArguments!.url.pathComponents.contains(DarklyService.StreamRequestPath.meval)).to(beTrue()) - expect(LDUser(base64urlEncodedString: receivedArguments!.url.lastPathComponent)?.isEqual(to: testContext.user)) == true + let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) + expect(AnyComparer.isEqual(receivedArguments!.url.lastPathComponent.jsonDictionary, to: expectedUser)) == true expect(receivedArguments!.httpHeaders).toNot(beEmpty()) expect(receivedArguments!.connectMethod).to(be("GET")) expect(receivedArguments!.connectBody).to(beNil()) @@ -575,7 +568,8 @@ final class DarklyServiceSpec: QuickSpec { expect(receivedArguments!.url.lastPathComponent) == DarklyService.StreamRequestPath.meval expect(receivedArguments!.httpHeaders).toNot(beEmpty()) expect(receivedArguments!.connectMethod) == DarklyService.HTTPRequestMethod.report - expect(LDUser(data: receivedArguments!.connectBody)?.isEqual(to: testContext.user)) == true + let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) + expect(AnyComparer.isEqual(receivedArguments!.connectBody?.jsonDictionary, to: expectedUser)) == true } } } @@ -796,16 +790,9 @@ private extension Data { } } -extension LDUser { - init?(base64urlEncodedString: String) { - let base64encodedString = base64urlEncodedString.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") - self.init(data: Data(base64Encoded: base64encodedString)) - } - - init?(data: Data?) { - guard let data = data, - let userDictionary = try? JSONSerialization.jsonDictionary(with: data) - else { return nil } - self.init(userDictionary: userDictionary) +private extension String { + var jsonDictionary: [String: Any]? { + let base64encodedString = self.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") + return Data(base64Encoded: base64encodedString)?.jsonDictionary } } From 121eda5914c5e7199aedb3bbd296b5d9d62e02c1 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 11 Mar 2022 11:46:18 -0600 Subject: [PATCH 31/50] Use LDValue for LDUser custom attributes and add Encodable implementation for LDUser. --- LaunchDarkly/LaunchDarkly/LDCommon.swift | 173 ++++++++++++++++++ LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 67 +++++-- .../Networking/DarklyService.swift | 12 +- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 4 +- .../LaunchDarklyTests/Mocks/LDUserStub.swift | 8 +- .../Models/User/LDUserSpec.swift | 36 ++-- 6 files changed, 261 insertions(+), 39 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index 054c8d41..afd2b173 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -28,3 +28,176 @@ extension LDFlagKey { self.localizedDescription = description } } + +struct DynamicKey: CodingKey { + let intValue: Int? = nil + let stringValue: String + + init?(intValue: Int) { + return nil + } + + init?(stringValue: String) { + self.stringValue = stringValue + } +} + +public enum LDValue: Codable, + Equatable, + ExpressibleByNilLiteral, + ExpressibleByBooleanLiteral, + ExpressibleByIntegerLiteral, + ExpressibleByFloatLiteral, + ExpressibleByStringLiteral, + ExpressibleByArrayLiteral, + ExpressibleByDictionaryLiteral { + + public typealias StringLiteralType = String + + public typealias ArrayLiteralElement = LDValue + + public typealias Key = String + public typealias Value = LDValue + + public typealias IntegerLiteralType = Double + public typealias FloatLiteralType = Double + + case null + case bool(Bool) + case number(Double) + case string(String) + case array([LDValue]) + case object([String: LDValue]) + + public init(nilLiteral: ()) { + self = .null + } + + public init(booleanLiteral: Bool) { + self = .bool(booleanLiteral) + } + + public init(integerLiteral: Double) { + self = .number(integerLiteral) + } + + public init(floatLiteral: Double) { + self = .number(floatLiteral) + } + + public init(stringLiteral: String) { + self = .string(stringLiteral) + } + + public init(arrayLiteral: LDValue...) { + self = .array(arrayLiteral) + } + + public init(dictionaryLiteral: (String, LDValue)...) { + self = .object(dictionaryLiteral.reduce(into: [:]) { $0[$1.0] = $1.1 }) + } + + public init(from decoder: Decoder) throws { + if var array = try? decoder.unkeyedContainer() { + var valueArr: [LDValue] = [] + while !array.isAtEnd { + valueArr.append(try array.decode(LDValue.self)) + } + self = .array(valueArr) + } else if let dict = try? decoder.container(keyedBy: DynamicKey.self) { + var valueDict: [String: LDValue] = [:] + let keys = dict.allKeys + for key in keys { + valueDict[key.stringValue] = try dict.decode(LDValue.self, forKey: key) + } + self = .object(valueDict) + } else { + let single = try decoder.singleValueContainer() + if let str = try? single.decode(String.self) { + self = .string(str) + } else if let num = try? single.decode(Double.self) { + self = .number(num) + } else if let bool = try? single.decode(Bool.self) { + self = .bool(bool) + } else if single.decodeNil() { + self = .null + } else { + throw DecodingError.dataCorruptedError(in: single, debugDescription: "Unexpected type when decoding LDValue") + } + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .null: + var sve = encoder.singleValueContainer() + try sve.encodeNil() + case .bool(let boolValue): + var sve = encoder.singleValueContainer() + try sve.encode(boolValue) + case .number(let doubleValue): + var sve = encoder.singleValueContainer() + try sve.encode(doubleValue) + case .string(let stringValue): + var sve = encoder.singleValueContainer() + try sve.encode(stringValue) + case .array(let arrayValue): + var unkeyedEncoder = encoder.unkeyedContainer() + try arrayValue.forEach { try unkeyedEncoder.encode($0) } + case .object(let dictValue): + var keyedEncoder = encoder.container(keyedBy: DynamicKey.self) + try dictValue.forEach { try keyedEncoder.encode($1, forKey: DynamicKey(stringValue: $0)!) } + } + } + + func booleanValue() -> Bool { + if case .bool(let val) = self { + return val + } + return false + } + + func intValue() -> Int { + if case .number(let val) = self { + // TODO check + return Int.init(val) + } + return 0 + } + + func doubleValue() -> Double { + if case .number(let val) = self { + return val + } + return 0 + } + + func stringValue() -> String { + if case .string(let val) = self { + return val + } + return "" + } + + func toAny() -> Any? { + switch self { + case .null: return nil + case .bool(let boolValue): return boolValue + case .number(let doubleValue): return doubleValue + case .string(let stringValue): return stringValue + case .array(let arrayValue): return arrayValue.map { $0.toAny() } + case .object(let dictValue): return dictValue.mapValues { $0.toAny() } + } + } + + static func fromAny(_ value: Any?) -> LDValue { + guard let value = value, !(value is NSNull) + else { return .null } + if let boolValue = value as? Bool { return .bool(boolValue) } + if let numValue = value as? NSNumber { return .number(Double(truncating: numValue)) } + if let stringValue = value as? String { return .string(stringValue) } + if let arrayValue = value as? [Any?] { return .array(arrayValue.map { LDValue.fromAny($0) }) } + if let dictValue = value as? [String: Any?] { return .object(dictValue.mapValues { LDValue.fromAny($0) }) } + return .null + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 33224580..91716ed7 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -7,7 +7,7 @@ typealias UserKey = String // use for identifying semantics for strings, partic The SDK caches last known feature flags for use on app startup to provide continuity with the last app run. Provided the LDClient is online and can establish a connection with LaunchDarkly servers, cached information will only be used a very short time. Once the latest feature flags arrive at the SDK, the SDK no longer uses cached feature flags. The SDK retains feature flags on the last 5 client defined users. The SDK will retain feature flags until they are overwritten by a different user's feature flags, or until the user removes the app from the device. The SDK does not cache user information collected, except for the user key. The user key is used to identify the cached feature flags for that user. Client app developers should use caution not to use sensitive user information as the user-key. */ -public struct LDUser { +public struct LDUser: Encodable { /// String keys associated with LDUser properties. public enum CodingKeys: String, CodingKey { @@ -38,7 +38,7 @@ public struct LDUser { /// Client app defined avatar for the user. (Default: nil) public var avatar: String? /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) - public var custom: [String: Any] + public var custom: [String: LDValue] /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: true) public var isAnonymous: Bool @@ -65,8 +65,6 @@ public struct LDUser { - parameter avatar: Client app defined avatar for the user. (Default: nil) - parameter custom: Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) - parameter isAnonymous: Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. (Default: nil) - - parameter device: Client app defined device for the user. The SDK will determine the device automatically, however the client app can override the value. The SDK will insert the device into the `custom` dictionary. (Default: nil) - - parameter operatingSystem: Client app defined operatingSystem for the user. The SDK will determine the operatingSystem automatically, however the client app can override the value. The SDK will insert the operatingSystem into the `custom` dictionary. (Default: nil) - parameter privateAttributes: Client app defined privateAttributes for the user. (Default: nil) - parameter secondary: Secondary attribute value. (Default: nil) */ @@ -78,7 +76,7 @@ public struct LDUser { ipAddress: String? = nil, email: String? = nil, avatar: String? = nil, - custom: [String: Any]? = nil, + custom: [String: LDValue]? = nil, isAnonymous: Bool? = nil, privateAttributes: [UserAttribute]? = nil, secondary: String? = nil) { @@ -95,8 +93,8 @@ public struct LDUser { self.avatar = avatar self.isAnonymous = isAnonymous ?? (selectedKey == LDUser.defaultKey(environmentReporter: environmentReporter)) self.custom = custom ?? [:] - self.custom.merge([CodingKeys.device.rawValue: environmentReporter.deviceModel, - CodingKeys.operatingSystem.rawValue: environmentReporter.systemVersion]) { lhs, _ in lhs } + self.custom.merge([CodingKeys.device.rawValue: .string(environmentReporter.deviceModel), + CodingKeys.operatingSystem.rawValue: .string(environmentReporter.systemVersion)]) { lhs, _ in lhs } self.privateAttributes = privateAttributes ?? [] Log.debug(typeName(and: #function) + "user: \(self)") } @@ -106,8 +104,8 @@ public struct LDUser { */ init(environmentReporter: EnvironmentReporting) { self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), - custom: [CodingKeys.device.rawValue: environmentReporter.deviceModel, - CodingKeys.operatingSystem.rawValue: environmentReporter.systemVersion], + custom: [CodingKeys.device.rawValue: .string(environmentReporter.deviceModel), + CodingKeys.operatingSystem.rawValue: .string(environmentReporter.systemVersion)], isAnonymous: true) } @@ -146,7 +144,7 @@ public struct LDUser { if allPrivate || privateAttributeNames.contains(attrName) { redactedAttributes.append(attrName) } else { - customDictionary[attrName] = attrVal + customDictionary[attrName] = attrVal.toAny() } } dictionary[CodingKeys.custom.rawValue] = customDictionary.isEmpty ? nil : customDictionary @@ -158,6 +156,53 @@ public struct LDUser { return dictionary } + struct UserInfoKeys { + static let includePrivateAttributes = CodingUserInfoKey(rawValue: "LD_includePrivateAttributes")! + static let allAttributesPrivate = CodingUserInfoKey(rawValue: "LD_allAttributesPrivate")! + static let globalPrivateAttributes = CodingUserInfoKey(rawValue: "LD_globalPrivateAttributes")! + } + + public func encode(to encoder: Encoder) throws { + let includePrivateAttributes = encoder.userInfo[UserInfoKeys.includePrivateAttributes] as? Bool ?? false + let allAttributesPrivate = encoder.userInfo[UserInfoKeys.allAttributesPrivate] as? Bool ?? false + let globalPrivateAttributes = encoder.userInfo[UserInfoKeys.globalPrivateAttributes] as? [String] ?? [] + + let allPrivate = !includePrivateAttributes && allAttributesPrivate + let privateAttributeNames = includePrivateAttributes ? [] : (privateAttributes.map { $0.name } + globalPrivateAttributes) + + var redactedAttributes: [String] = [] + + var container = encoder.container(keyedBy: DynamicKey.self) + try container.encode(key, forKey: DynamicKey(stringValue: "key")!) + try container.encode(isAnonymous, forKey: DynamicKey(stringValue: "anonymous")!) + + try LDUser.optionalAttributes.forEach { attribute in + if let value = self.value(for: attribute) as? String { + if allPrivate || privateAttributeNames.contains(attribute.name) { + redactedAttributes.append(attribute.name) + } else { + try container.encode(value, forKey: DynamicKey(stringValue: attribute.name)!) + } + } + } + + var nestedContainer: KeyedEncodingContainer? + try custom.forEach { attrName, attrVal in + if allPrivate || privateAttributeNames.contains(attrName) { + redactedAttributes.append(attrName) + } else { + if nestedContainer == nil { + nestedContainer = container.nestedContainer(keyedBy: DynamicKey.self, forKey: DynamicKey(stringValue: "custom")!) + } + try nestedContainer!.encode(attrVal, forKey: DynamicKey(stringValue: attrName)!) + } + } + + if !redactedAttributes.isEmpty { + try container.encode(Set(redactedAttributes).sorted(), forKey: DynamicKey(stringValue: "privateAttrs")!) + } + } + /// Default key is the LDUser.key the SDK provides when any intializer is called without defining the key. The key should be constant with respect to the client app installation on a specific device. (The key may change if the client app is uninstalled and then reinstalled on the same device.) /// - parameter environmentReporter: The environmentReporter provides selected information that varies between OS regarding how it's determined static func defaultKey(environmentReporter: EnvironmentReporting) -> String { @@ -213,7 +258,7 @@ extension LDUser: TypeIdentifying { } && ipAddress == otherUser.ipAddress && email == otherUser.email && avatar == otherUser.avatar - && AnyComparer.isEqual(custom, to: otherUser.custom) + && custom == otherUser.custom && isAnonymous == otherUser.isAnonymous && privateAttributes == otherUser.privateAttributes } diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 62e48861..55391266 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -81,7 +81,9 @@ final class DarklyService: DarklyServiceProvider { func getFeatureFlags(useReport: Bool, completion: ServiceCompletionHandler?) { guard hasMobileKey(#function) else { return } - guard let userJson = user.dictionaryValue(includePrivateAttributes: true, config: config).jsonData + let encoder = JSONEncoder() + encoder.userInfo[LDUser.UserInfoKeys.includePrivateAttributes] = true + guard let userJsonData = try? encoder.encode(user) else { Log.debug(typeName(and: #function, appending: ": ") + "Aborting. Unable to create flagRequest.") return @@ -91,12 +93,12 @@ final class DarklyService: DarklyServiceProvider { if let etag = flagRequestEtag { headers.merge([HTTPHeaders.HeaderKey.ifNoneMatch: etag]) { orig, _ in orig } } - var request = URLRequest(url: flagRequestUrl(useReport: useReport, getData: userJson), + var request = URLRequest(url: flagRequestUrl(useReport: useReport, getData: userJsonData), ldHeaders: headers, ldConfig: config) if useReport { request.httpMethod = URLRequest.HTTPMethods.report - request.httpBody = userJson + request.httpBody = userJsonData } self.session.dataTask(with: request) { [weak self] data, response, error in @@ -145,7 +147,9 @@ final class DarklyService: DarklyServiceProvider { func createEventSource(useReport: Bool, handler: EventHandler, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { - let userJsonData = user.dictionaryValue(includePrivateAttributes: true, config: config).jsonData + let encoder = JSONEncoder() + encoder.userInfo[LDUser.UserInfoKeys.includePrivateAttributes] = true + let userJsonData = try? encoder.encode(user) var streamRequestUrl = config.streamUrl.appendingPathComponent(StreamRequestPath.meval) var connectMethod = HTTPRequestMethod.get diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index 6541773d..4e1da577 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -94,8 +94,8 @@ public final class ObjcLDUser: NSObject { } /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) @objc public var custom: [String: Any] { - get { user.custom } - set { user.custom = newValue } + get { user.custom.mapValues { $0.toAny() } } + set { user.custom = newValue.mapValues { LDValue.fromAny($0) } } } /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: YES) @objc public var isAnonymous: Bool { diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift index 7f057711..5a83b3e1 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift @@ -14,16 +14,16 @@ extension LDUser { static let ipAddress = "stub.user.ipAddress" static let email = "stub.user@email.com" static let avatar = "stub.user.avatar" - static let device = "stub.user.custom.device" - static let operatingSystem = "stub.user.custom.operatingSystem" - static let custom: [String: Any] = ["stub.user.custom.keyA": "stub.user.custom.valueA", + static let device: LDValue = "stub.user.custom.device" + static let operatingSystem: LDValue = "stub.user.custom.operatingSystem" + static let custom: [String: LDValue] = ["stub.user.custom.keyA": "stub.user.custom.valueA", "stub.user.custom.keyB": true, "stub.user.custom.keyC": 1027, "stub.user.custom.keyD": 2.71828, "stub.user.custom.keyE": [0, 1, 2], "stub.user.custom.keyF": ["1": 1, "2": 2, "3": 3]] - static func custom(includeSystemValues: Bool) -> [String: Any] { + static func custom(includeSystemValues: Bool) -> [String: LDValue] { var custom = StubConstants.custom if includeSystemValues { custom[CodingKeys.device.rawValue] = StubConstants.device diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 1b6193ec..7bf06860 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -63,8 +63,8 @@ final class LDUserSpec: QuickSpec { expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) expect(user.custom.count) == 2 - expect(user.custom[LDUser.CodingKeys.device.rawValue] as? String) == environmentReporter.deviceModel - expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue] as? String) == environmentReporter.systemVersion + expect(user.custom[LDUser.CodingKeys.device.rawValue]) == .string(environmentReporter.deviceModel) + expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue]) == .string(environmentReporter.systemVersion) expect(user.privateAttributes).to(beEmpty()) expect(user.secondary).to(beNil()) } @@ -108,8 +108,8 @@ final class LDUserSpec: QuickSpec { expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) expect(user.custom.count) == 2 - expect(user.custom[LDUser.CodingKeys.device.rawValue] as? String) == environmentReporter.deviceModel - expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue] as? String) == environmentReporter.systemVersion + expect(user.custom[LDUser.CodingKeys.device.rawValue]) == .string(environmentReporter.deviceModel) + expect(user.custom[LDUser.CodingKeys.operatingSystem.rawValue]) == .string(environmentReporter.systemVersion) expect(user.privateAttributes).to(beEmpty()) } @@ -201,7 +201,7 @@ final class LDUserSpec: QuickSpec { // Custom attributes allCustomPrivitizable.forEach { attr in - expect(AnyComparer.isEqual(customDictionary[attr], to: user.custom[attr])).to(beTrue()) + expect(LDValue.fromAny(customDictionary[attr])) == user.custom[attr] } // Redacted attributes is empty @@ -303,19 +303,19 @@ final class LDUserSpec: QuickSpec { } } context("when users are not equal") { - let testFields: [(String, Bool, Any, (inout LDUser, Any?) -> Void)] = - [("key", false, "dummy", { u, v in u.key = v as! String }), - ("secondary", true, "dummy", { u, v in u.secondary = v as! String? }), - ("name", true, "dummy", { u, v in u.name = v as! String? }), - ("firstName", true, "dummy", { u, v in u.firstName = v as! String? }), - ("lastName", true, "dummy", { u, v in u.lastName = v as! String? }), - ("country", true, "dummy", { u, v in u.country = v as! String? }), - ("ipAddress", true, "dummy", { u, v in u.ipAddress = v as! String? }), - ("email address", true, "dummy", { u, v in u.email = v as! String? }), - ("avatar", true, "dummy", { u, v in u.avatar = v as! String? }), - ("custom", false, ["dummy": true], { u, v in u.custom = v as! [String: Any] }), - ("isAnonymous", false, true, { u, v in u.isAnonymous = v as! Bool }), - ("privateAttributes", false, [UserAttribute.forName("dummy")], { u, v in u.privateAttributes = v as! [UserAttribute] })] + let testFields: [(String, Bool, LDValue, (inout LDUser, LDValue?) -> Void)] = + [("key", false, "dummy", { u, v in u.key = v!.stringValue() }), + ("secondary", true, "dummy", { u, v in u.secondary = v?.stringValue() }), + ("name", true, "dummy", { u, v in u.name = v?.stringValue() }), + ("firstName", true, "dummy", { u, v in u.firstName = v?.stringValue() }), + ("lastName", true, "dummy", { u, v in u.lastName = v?.stringValue() }), + ("country", true, "dummy", { u, v in u.country = v?.stringValue() }), + ("ipAddress", true, "dummy", { u, v in u.ipAddress = v?.stringValue() }), + ("email address", true, "dummy", { u, v in u.email = v?.stringValue() }), + ("avatar", true, "dummy", { u, v in u.avatar = v?.stringValue() }), + ("custom", false, ["dummy": true], { u, v in u.custom = (v!.toAny() as! [String: Any]).mapValues { LDValue.fromAny($0) } }), + ("isAnonymous", false, true, { u, v in u.isAnonymous = v!.booleanValue() }), + ("privateAttributes", false, "dummy", { u, v in u.privateAttributes = [UserAttribute.forName(v!.stringValue())] })] testFields.forEach { name, isOptional, otherVal, setter in context("\(name) differs") { beforeEach { From 9fb18b667dbdc9e19ceab98802a67b050f02a28c Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 11 Mar 2022 13:56:03 -0600 Subject: [PATCH 32/50] Bump LDSwiftEventSource to 1.3.1 to fix race condition. (#182) --- LaunchDarkly.podspec | 2 +- LaunchDarkly.xcodeproj/project.pbxproj | 2 +- Package.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index c0417369..8bd88770 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -35,6 +35,6 @@ Pod::Spec.new do |ld| ld.swift_version = '5.0' ld.subspec 'Core' do |es| - es.dependency 'LDSwiftEventSource', '1.3.0' + es.dependency 'LDSwiftEventSource', '1.3.1' end end diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index dae24c38..a0f3e429 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -1962,7 +1962,7 @@ repositoryURL = "https://github.com/LaunchDarkly/swift-eventsource.git"; requirement = { kind = exactVersion; - version = 1.3.0; + version = 1.3.1; }; }; B4903D9624BD61B200F087C4 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { diff --git a/Package.swift b/Package.swift index 79631e0f..30d74f3b 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ let package = Package( .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", .exact("9.1.0")), .package(url: "https://github.com/Quick/Quick.git", .exact("4.0.0")), .package(url: "https://github.com/Quick/Nimble.git", .exact("9.2.1")), - .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("1.3.0")) + .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("1.3.1")) ], targets: [ .target( From 61f92be177cbf36a2b0212de304487c9b87fef9b Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Mon, 14 Mar 2022 02:37:21 -0500 Subject: [PATCH 33/50] Silence warning so CI validation passes. --- LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index 4e1da577..e417b1e3 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -94,7 +94,7 @@ public final class ObjcLDUser: NSObject { } /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) @objc public var custom: [String: Any] { - get { user.custom.mapValues { $0.toAny() } } + get { user.custom.mapValues { $0.toAny() as Any } } set { user.custom = newValue.mapValues { LDValue.fromAny($0) } } } /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: YES) From 8f4d721deb92b6e18b8bfddeed70016840ea3246 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Tue, 15 Mar 2022 14:10:09 -0500 Subject: [PATCH 34/50] Use LDValue for Event fields and summarization. --- .../GeneratedCode/mocks.generated.swift | 4 +- LaunchDarkly/LaunchDarkly/LDClient.swift | 6 +- .../LaunchDarkly/LDClientVariation.swift | 2 +- LaunchDarkly/LaunchDarkly/Models/Event.swift | 38 +- .../FeatureFlag/FlagRequestTracker.swift | 20 +- .../ObjectiveC/ObjcLDClient.swift | 8 +- .../ServiceObjects/EventReporter.swift | 4 +- .../LaunchDarklyTests/LDClientSpec.swift | 35 +- .../LaunchDarklyTests/Models/EventSpec.swift | 336 +++++------------- .../FlagRequestTracking/FlagCounterSpec.swift | 123 +++---- .../FlagRequestTrackerSpec.swift | 9 - .../ServiceObjects/EventReporterSpec.swift | 160 ++++----- 12 files changed, 270 insertions(+), 475 deletions(-) diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index c5f448a1..dbfdd106 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -242,8 +242,8 @@ final class EventReportingMock: EventReporting { var recordFlagEvaluationEventsCallCount = 0 var recordFlagEvaluationEventsCallback: (() -> Void)? - var recordFlagEvaluationEventsReceivedArguments: (flagKey: LDFlagKey, value: Any?, defaultValue: Any?, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool)? - func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: Any?, defaultValue: Any?, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) { + var recordFlagEvaluationEventsReceivedArguments: (flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool)? + func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) { recordFlagEvaluationEventsCallCount += 1 recordFlagEvaluationEventsReceivedArguments = (flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason) recordFlagEvaluationEventsCallback?() diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 235d72f0..6802e2d6 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -544,16 +544,14 @@ public class LDClient { - parameter key: The key for the event. The SDK does nothing with the key, which can be any string the client app sends - parameter data: The data for the event. The SDK does nothing with the data, which can be any valid JSON item the client app sends. (Optional) - parameter metricValue: A numeric value used by the LaunchDarkly experimentation feature in numeric custom metrics. Can be omitted if this event is used by only non-numeric metrics. This field will also be returned as part of the custom event for Data Export. (Optional) - - - throws: LDInvalidArgumentError if the data is not a valid JSON item */ - public func track(key: String, data: Any? = nil, metricValue: Double? = nil) throws { + public func track(key: String, data: LDValue? = nil, metricValue: Double? = nil) { guard hasStarted else { Log.debug(typeName(and: #function) + "aborted. LDClient not started") return } - let event = try Event.customEvent(key: key, user: user, data: data, metricValue: metricValue) + let event = Event.customEvent(key: key, user: user, data: data ?? .null, metricValue: metricValue) Log.debug(typeName(and: #function) + "event: \(event), data: \(String(describing: data)), metricValue: \(String(describing: metricValue))") eventReporter.record(event) } diff --git a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift index 67602b59..41e0bfe1 100644 --- a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -92,7 +92,7 @@ extension LDClient { let failedConversionMessage = self.failedConversionMessage(featureFlag: featureFlag, defaultValue: defaultValue) Log.debug(typeName(and: #function) + "flagKey: \(flagKey), value: \(value), defaultValue: \(defaultValue), " + "featureFlag: \(String(describing: featureFlag)), reason: \(featureFlag?.reason?.description ?? "nil"). \(failedConversionMessage)") - eventReporter.recordFlagEvaluationEvents(flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason) + eventReporter.recordFlagEvaluationEvents(flagKey: flagKey, value: LDValue.fromAny(value), defaultValue: LDValue.fromAny(defaultValue), featureFlag: featureFlag, user: user, includeReason: includeReason) return value } diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index eb0f3262..42d1d854 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -38,10 +38,10 @@ struct Event { let previousKey: String? let creationDate: Date? let user: LDUser? - let value: Any? - let defaultValue: Any? + let value: LDValue + let defaultValue: LDValue let featureFlag: FeatureFlag? - let data: Any? + let data: LDValue let flagRequestTracker: FlagRequestTracker? let endDate: Date? let includeReason: Bool @@ -55,10 +55,10 @@ struct Event { contextKind: String? = nil, previousContextKind: String? = nil, user: LDUser? = nil, - value: Any? = nil, - defaultValue: Any? = nil, + value: LDValue = .null, + defaultValue: LDValue = .null, featureFlag: FeatureFlag? = nil, - data: Any? = nil, + data: LDValue = .null, flagRequestTracker: FlagRequestTracker? = nil, endDate: Date? = nil, includeReason: Bool = false, @@ -81,27 +81,19 @@ struct Event { } // swiftlint:disable:next function_parameter_count - static func featureEvent(key: String, value: Any?, defaultValue: Any?, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) -> Event { - Log.debug(typeName(and: #function) + "key: " + key + ", value: \(String(describing: value)), " + "defaultValue: \(String(describing: defaultValue) + "reason: \(String(describing: includeReason))"), " - + "featureFlag: \(String(describing: featureFlag))") + static func featureEvent(key: String, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) -> Event { + Log.debug(typeName(and: #function) + "key: \(key), value: \(value), defaultValue: \(defaultValue), includeReason: \(includeReason), featureFlag: \(String(describing: featureFlag))") return Event(kind: .feature, key: key, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason) } // swiftlint:disable:next function_parameter_count - static func debugEvent(key: String, value: Any?, defaultValue: Any?, featureFlag: FeatureFlag, user: LDUser, includeReason: Bool) -> Event { - Log.debug(typeName(and: #function) + "key: " + key + ", value: \(String(describing: value)), " + "defaultValue: \(String(describing: defaultValue) + "reason: \(String(describing: includeReason))"), " - + "featureFlag: \(String(describing: featureFlag))") + static func debugEvent(key: String, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag, user: LDUser, includeReason: Bool) -> Event { + Log.debug(typeName(and: #function) + "key: \(key), value: \(value), defaultValue: \(defaultValue), includeReason: \(includeReason), featureFlag: \(String(describing: featureFlag))") return Event(kind: .debug, key: key, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason) } - static func customEvent(key: String, user: LDUser, data: Any? = nil, metricValue: Double? = nil) throws -> Event { - Log.debug(typeName(and: #function) + "key: " + key + ", data: \(String(describing: data)), metricValue: \(String(describing: metricValue))") - if let data = data { - guard JSONSerialization.isValidJSONObject([CodingKeys.data.rawValue: data]) // the top level object must be either an array or an object for isValidJSONObject to work correctly - else { - throw LDInvalidArgumentError("data is not a JSON convertible value") - } - } + static func customEvent(key: String, user: LDUser, data: LDValue, metricValue: Double? = nil) -> Event { + Log.debug(typeName(and: #function) + "key: " + key + ", data: \(data), metricValue: \(String(describing: metricValue))") return Event(kind: .custom, key: key, user: user, data: data, metricValue: metricValue) } @@ -134,13 +126,13 @@ struct Event { eventDictionary[CodingKeys.userKey.rawValue] = user?.key } if kind.isAlwaysIncludeValueKinds { - eventDictionary[CodingKeys.value.rawValue] = value ?? NSNull() - eventDictionary[CodingKeys.defaultValue.rawValue] = defaultValue ?? NSNull() + eventDictionary[CodingKeys.value.rawValue] = value.toAny() ?? NSNull() + eventDictionary[CodingKeys.defaultValue.rawValue] = defaultValue.toAny() ?? NSNull() } eventDictionary[CodingKeys.variation.rawValue] = featureFlag?.variation // If the flagVersion exists, it is reported as the "version". If not, the version is reported using the "version" key. eventDictionary[CodingKeys.version.rawValue] = featureFlag?.flagVersion ?? featureFlag?.version - eventDictionary[CodingKeys.data.rawValue] = data + eventDictionary[CodingKeys.data.rawValue] = data.toAny() if let flagRequestTracker = flagRequestTracker { eventDictionary.merge(flagRequestTracker.dictionaryValue) { _, trackerItem in trackerItem // This should never happen because the eventDictionary does not use any conflicting keys with the flagRequestTracker diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift index 53182997..98a03cb9 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift @@ -6,9 +6,9 @@ struct FlagRequestTracker { } let startDate = Date() - var flagCounters = [LDFlagKey: FlagCounter]() + var flagCounters: [LDFlagKey: FlagCounter] = [:] - mutating func trackRequest(flagKey: LDFlagKey, reportedValue: Any?, featureFlag: FeatureFlag?, defaultValue: Any?) { + mutating func trackRequest(flagKey: LDFlagKey, reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue) { if flagCounters[flagKey] == nil { flagCounters[flagKey] = FlagCounter() } @@ -17,10 +17,10 @@ struct FlagRequestTracker { flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) Log.debug(typeName(and: #function) + "\n\tflagKey: \(flagKey)" - + "\n\treportedValue: \(String(describing: reportedValue)), " + + "\n\treportedValue: \(reportedValue), " + "\n\tvariation: \(String(describing: featureFlag?.variation)), " + "\n\tversion: \(String(describing: featureFlag?.flagVersion ?? featureFlag?.version)), " - + "\n\tdefaultValue: \(String(describing: defaultValue))\n") + + "\n\tdefaultValue: \(defaultValue)\n") } var dictionaryValue: [String: Any] { @@ -38,10 +38,10 @@ final class FlagCounter { case defaultValue = "default", counters, value, variation, version, unknown, count } - var defaultValue: Any? + var defaultValue: LDValue = .null var flagValueCounters: [CounterKey: CounterValue] = [:] - func trackRequest(reportedValue: Any?, featureFlag: FeatureFlag?, defaultValue: Any?) { + func trackRequest(reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue) { self.defaultValue = defaultValue let key = CounterKey(variation: featureFlag?.variation, version: featureFlag?.versionForEvents) if let counter = flagValueCounters[key] { @@ -53,7 +53,7 @@ final class FlagCounter { var dictionaryValue: [String: Any] { let counters: [[String: Any]] = flagValueCounters.map { (key, value) in - var res: [String: Any] = [CodingKeys.value.rawValue: value.value ?? NSNull(), + var res: [String: Any] = [CodingKeys.value.rawValue: value.value.toAny() ?? NSNull(), CodingKeys.count.rawValue: value.count, CodingKeys.variation.rawValue: key.variation ?? NSNull()] if let version = key.version { @@ -63,7 +63,7 @@ final class FlagCounter { } return res } - return [CodingKeys.defaultValue.rawValue: defaultValue ?? NSNull(), + return [CodingKeys.defaultValue.rawValue: defaultValue.toAny() ?? NSNull(), CodingKeys.counters.rawValue: counters] } } @@ -74,10 +74,10 @@ struct CounterKey: Equatable, Hashable { } class CounterValue { - let value: Any? + let value: LDValue var count: Int = 1 - init(value: Any?) { + init(value: LDValue) { self.value = value } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index da955e07..cc0fb65d 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -746,8 +746,8 @@ public final class ObjcLDClient: NSObject { - parameter error: NSError object to hold the invalidJsonObject error if the data is not a valid JSON item. (Optional) */ /// - Tag: track - @objc public func track(key: String, data: Any? = nil) throws { - try ldClient.track(key: key, data: data, metricValue: nil) + @objc public func track(key: String, data: Any? = nil) { + ldClient.track(key: key, data: LDValue.fromAny(data), metricValue: nil) } /** @@ -758,8 +758,8 @@ public final class ObjcLDClient: NSObject { - parameter metricValue: A numeric value used by the LaunchDarkly experimentation feature in numeric custom metrics. Can be omitted if this event is used by only non-numeric metrics. This field will also be returned as part of the custom event for Data Export. - parameter error: NSError object to hold the invalidJsonObject error if the data is not a valid JSON item. (Optional) */ - @objc public func track(key: String, data: Any? = nil, metricValue: Double) throws { - try ldClient.track(key: key, data: data, metricValue: metricValue) + @objc public func track(key: String, data: Any? = nil, metricValue: Double) { + ldClient.track(key: key, data: LDValue.fromAny(data), metricValue: metricValue) } /** diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index b799134d..e2f6f9cd 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -9,7 +9,7 @@ protocol EventReporting { func record(_ event: Event) // swiftlint:disable:next function_parameter_count - func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: Any?, defaultValue: Any?, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) + func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) func flush(completion: CompletionClosure?) } @@ -65,7 +65,7 @@ class EventReporter: EventReporting { } // swiftlint:disable:next function_parameter_count - func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: Any?, defaultValue: Any?, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) { + func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) { let recordingFeatureEvent = featureFlag?.trackEvents == true let recordingDebugEvent = featureFlag?.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate) ?? false diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 92f0466e..cfe147b3 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -855,7 +855,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.isOnline) == false } it("stops recording events") { - expect(try testContext.subject.track(key: event.key!)).toNot(throwError()) + testContext.subject.track(key: event.key!) expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents } it("flushes the event reporter") { @@ -875,7 +875,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.isOnline) == false } it("stops recording events") { - expect(try testContext.subject.track(key: event.key!)).toNot(throwError()) + testContext.subject.track(key: event.key!) expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents } it("flushes the event reporter") { @@ -897,7 +897,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.isOnline) == false } it("stops recording events") { - expect(try testContext.subject.track(key: event.key!)).toNot(throwError()) + testContext.subject.track(key: event.key!) expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents } it("flushes the event reporter") { @@ -911,22 +911,17 @@ final class LDClientSpec: QuickSpec { var testContext: TestContext! describe("track event") { - var event: LaunchDarkly.Event! beforeEach { testContext = TestContext() testContext.start() - event = Event.stub(.custom, with: testContext.user) } - context("when client was started") { - beforeEach { - try! testContext.subject.track(key: event.key!, data: event.data) - } - it("records a custom event") { - expect(testContext.eventReporterMock.recordReceivedEvent?.key) == event.key - expect(testContext.eventReporterMock.recordReceivedEvent?.user) == event.user - expect(testContext.eventReporterMock.recordReceivedEvent?.data).toNot(beNil()) - expect(AnyComparer.isEqual(testContext.eventReporterMock.recordReceivedEvent?.data, to: event.data)).to(beTrue()) - } + it("records a custom event when client was started") { + testContext.subject.track(key: "customEvent", data: "abc", metricValue: 5.0) + let receivedEvent = testContext.eventReporterMock.recordReceivedEvent + expect(receivedEvent?.key) == "customEvent" + expect(receivedEvent?.user) == testContext.user + expect(receivedEvent?.data) == "abc" + expect(receivedEvent?.metricValue) == 5.0 } context("when client was stopped") { var priorRecordedEvents: Int! @@ -934,7 +929,7 @@ final class LDClientSpec: QuickSpec { testContext.subject.close() priorRecordedEvents = testContext.eventReporterMock.recordCallCount - try! testContext.subject.track(key: event.key!, data: event.data) + testContext.subject.track(key: "abc") } it("does not record any more events") { expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents @@ -972,8 +967,8 @@ final class LDClientSpec: QuickSpec { _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool - expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value, to: DarklyServiceMock.FlagValues.bool)).to(beTrue()) - expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue, to: DefaultFlagValues.bool)).to(beTrue()) + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == LDValue.fromAny(DarklyServiceMock.FlagValues.bool) + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue) == LDValue.fromAny(DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag) == testContext.flagStoreMock.featureFlags[DarklyServiceMock.FlagKeys.bool] expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user } @@ -993,8 +988,8 @@ final class LDClientSpec: QuickSpec { _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool - expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value, to: DefaultFlagValues.bool)).to(beTrue()) - expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue, to: DefaultFlagValues.bool)).to(beTrue()) + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == LDValue.fromAny(DefaultFlagValues.bool) + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue) == LDValue.fromAny(DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag).to(beNil()) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 49af62bd..290ca545 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -9,21 +9,21 @@ final class EventSpec: QuickSpec { } struct CustomEvent { - static let intData = 3 - static let doubleData = 1.414 - static let boolData = true - static let stringData = "custom event string data" - static let arrayData: [Any] = [12, 1.61803, true, "custom event array data"] - static let nestedArrayData = [1, 3, 7, 12] - static let nestedDictionaryData = ["one": 1.0, "three": 3.0, "seven": 7.0, "twelve": 12.0] - static let dictionaryData: [String: Any] = ["dozen": 12, - "phi": 1.61803, - "true": true, - "data string": "custom event dictionary data", - "nestedArray": nestedArrayData, - "nestedDictionary": nestedDictionaryData] - - static let allData: [Any] = [intData, doubleData, boolData, stringData, arrayData, dictionaryData] + static let intData: LDValue = 3 + static let doubleData: LDValue = 1.414 + static let boolData: LDValue = true + static let stringData: LDValue = "custom event string data" + static let arrayData: LDValue = [12, 1.61803, true, "custom event array data"] + static let nestedArrayData: LDValue = [1, 3, 7, 12] + static let nestedDictionaryData: LDValue = ["one": 1.0, "three": 3.0, "seven": 7.0, "twelve": 12.0] + static let dictionaryData: LDValue = ["dozen": 12, + "phi": 1.61803, + "true": true, + "data string": "custom event dictionary data", + "nestedArray": nestedArrayData, + "nestedDictionary": nestedDictionaryData] + + static let allData: [LDValue] = [intData, doubleData, boolData, stringData, arrayData, dictionaryData] } override func spec() { @@ -35,7 +35,6 @@ final class EventSpec: QuickSpec { identifyEventSpec() summaryEventSpec() dictionaryValueSpec() - eventDictionarySpec() } private func initSpec() { @@ -57,11 +56,10 @@ final class EventSpec: QuickSpec { expect(event.key) == Constants.eventKey expect(event.creationDate).toNot(beNil()) expect(event.user) == user - expect(AnyComparer.isEqual(event.value, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(event.defaultValue, to: false)).to(beTrue()) + expect(event.value) == true + expect(event.defaultValue) == false expect(event.featureFlag?.allPropertiesMatch(featureFlag)).to(beTrue()) - expect(event.data).toNot(beNil()) - expect(AnyComparer.isEqual(event.data, to: CustomEvent.dictionaryData)).to(beTrue()) + expect(event.data) == CustomEvent.dictionaryData expect(event.flagRequestTracker).toNot(beNil()) expect(event.endDate).toNot(beNil()) } @@ -75,10 +73,10 @@ final class EventSpec: QuickSpec { expect(event.key).to(beNil()) expect(event.creationDate).toNot(beNil()) expect(event.user).to(beNil()) - expect(event.value).to(beNil()) - expect(event.defaultValue).to(beNil()) + expect(event.value) == .null + expect(event.defaultValue) == .null expect(event.featureFlag).to(beNil()) - expect(event.data).to(beNil()) + expect(event.data) == .null expect(event.flagRequestTracker).to(beNil()) expect(event.endDate).to(beNil()) } @@ -134,11 +132,11 @@ final class EventSpec: QuickSpec { expect(event.key) == Constants.eventKey expect(event.creationDate).toNot(beNil()) expect(event.user) == user - expect(AnyComparer.isEqual(event.value, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(event.defaultValue, to: false)).to(beTrue()) + expect(event.value) == true + expect(event.defaultValue) == false expect(event.featureFlag?.allPropertiesMatch(featureFlag)).to(beTrue()) - expect(event.data).to(beNil()) + expect(event.data) == .null expect(event.endDate).to(beNil()) expect(event.flagRequestTracker).to(beNil()) } @@ -162,11 +160,11 @@ final class EventSpec: QuickSpec { expect(event.key) == Constants.eventKey expect(event.creationDate).toNot(beNil()) expect(event.user) == user - expect(AnyComparer.isEqual(event.value, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(event.defaultValue, to: false)).to(beTrue()) + expect(event.value) == true + expect(event.defaultValue) == false expect(event.featureFlag?.allPropertiesMatch(featureFlag)).to(beTrue()) - expect(event.data).to(beNil()) + expect(event.data) == .null expect(event.endDate).to(beNil()) expect(event.flagRequestTracker).to(beNil()) } @@ -175,7 +173,6 @@ final class EventSpec: QuickSpec { private func customEventSpec() { var user: LDUser! - var event: Event! beforeEach { user = LDUser.stub() } @@ -183,38 +180,33 @@ final class EventSpec: QuickSpec { for eventData in CustomEvent.allData { context("with valid json data") { it("creates a custom event with matching data") { - expect(event = try Event.customEvent(key: Constants.eventKey, user: user, data: eventData)).toNot(throwError()) + let event = Event.customEvent(key: Constants.eventKey, user: user, data: eventData) expect(event.kind) == Event.Kind.custom expect(event.key) == Constants.eventKey expect(event.creationDate).toNot(beNil()) expect(event.user) == user - expect(AnyComparer.isEqual(event.data, to: eventData)).to(beTrue()) + expect(event.data) == eventData - expect(event.value).to(beNil()) - expect(event.defaultValue).to(beNil()) + expect(event.value) == .null + expect(event.defaultValue) == .null expect(event.endDate).to(beNil()) expect(event.flagRequestTracker).to(beNil()) } } } - context("with invalid json data") { - it("throws an invalidJsonObject error") { - expect(event = try Event.customEvent(key: Constants.eventKey, user: user, data: Date())).to(throwError(errorType: LDInvalidArgumentError.self)) - } - } context("without data") { it("creates a custom event with matching data") { - expect(event = try Event.customEvent(key: Constants.eventKey, user: user, data: nil)).toNot(throwError()) + let event = Event.customEvent(key: Constants.eventKey, user: user, data: nil) expect(event.kind) == Event.Kind.custom expect(event.key) == Constants.eventKey expect(event.creationDate).toNot(beNil()) expect(event.user) == user - expect(event.data).to(beNil()) + expect(event.data) == .null - expect(event.value).to(beNil()) - expect(event.defaultValue).to(beNil()) + expect(event.value) == .null + expect(event.defaultValue) == .null expect(event.endDate).to(beNil()) expect(event.flagRequestTracker).to(beNil()) } @@ -238,9 +230,9 @@ final class EventSpec: QuickSpec { expect(event.creationDate).toNot(beNil()) expect(event.user) == user - expect(event.value).to(beNil()) - expect(event.defaultValue).to(beNil()) - expect(event.data).to(beNil()) + expect(event.value) == .null + expect(event.defaultValue) == .null + expect(event.data) == .null expect(event.endDate).to(beNil()) expect(event.flagRequestTracker).to(beNil()) } @@ -262,15 +254,16 @@ final class EventSpec: QuickSpec { it("creates a summary event with matching data") { expect(event.kind) == Event.Kind.summary expect(event.endDate) == endDate - expect(event.flagRequestTracker) == flagRequestTracker + expect(event.flagRequestTracker?.startDate) == flagRequestTracker.startDate + expect(event.flagRequestTracker?.flagCounters) == flagRequestTracker.flagCounters expect(event.key).to(beNil()) expect(event.creationDate).to(beNil()) expect(event.user).to(beNil()) - expect(event.value).to(beNil()) - expect(event.defaultValue).to(beNil()) + expect(event.value) == .null + expect(event.defaultValue) == .null expect(event.featureFlag).to(beNil()) - expect(event.data).to(beNil()) + expect(event.data) == .null } } context("without tracked requests") { @@ -316,7 +309,8 @@ final class EventSpec: QuickSpec { config.inlineUserInEvents = false // Default value, here for clarity eventDictionary = event.dictionaryValue(config: config) } - it("creates a dictionary with matching non-user elements") { + it("creates a dictionary with matching elements") { + expect(eventDictionary.count) == 9 expect(eventDictionary.eventKind) == .feature expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) @@ -324,15 +318,8 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. - expect(eventDictionary.eventData).to(beNil()) expect(AnyComparer.isEqual(eventDictionary.reason, to: DarklyServiceMock.Constants.reason)).to(beTrue()) - expect(eventDictionary.eventPreviousKey).to(beNil()) - expect(eventDictionary.eventContextKind).to(beNil()) - expect(eventDictionary.eventPreviousContextKind).to(beNil()) - } - it("creates a dictionary with the user key only") { expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUserDictionary).to(beNil()) } } context("inlining user and without reason") { @@ -341,7 +328,8 @@ final class EventSpec: QuickSpec { config.inlineUserInEvents = true eventDictionary = event.dictionaryValue(config: config) } - it("creates a dictionary with matching non-user elements") { + it("creates a dictionary with matching elements") { + expect(eventDictionary.count) == 8 expect(eventDictionary.eventKind) == .feature expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) @@ -349,12 +337,7 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. - expect(eventDictionary.eventData).to(beNil()) - expect(eventDictionary.reason).to(beNil()) - } - it("creates a dictionary with the full user") { expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - expect(eventDictionary.eventUserKey).to(beNil()) } } context("omitting the flagVersion") { @@ -364,16 +347,15 @@ final class EventSpec: QuickSpec { eventDictionary = event.dictionaryValue(config: config) } it("creates a dictionary with the version") { + expect(eventDictionary.count) == 8 expect(eventDictionary.eventKind) == .feature expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUserDictionary).to(beNil()) expect(eventDictionary.eventVariation) == featureFlag.variation expect(eventDictionary.eventVersion) == featureFlag.version - expect(eventDictionary.eventData).to(beNil()) } } context("omitting flagVersion and version") { @@ -383,16 +365,14 @@ final class EventSpec: QuickSpec { eventDictionary = event.dictionaryValue(config: config) } it("creates a dictionary without the version") { + expect(eventDictionary.count) == 7 expect(eventDictionary.eventKind) == .feature expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUserDictionary).to(beNil()) expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion).to(beNil()) - expect(eventDictionary.eventData).to(beNil()) } } context("without value or defaultValue") { @@ -402,6 +382,7 @@ final class EventSpec: QuickSpec { eventDictionary = event.dictionaryValue(config: config) } it("creates a dictionary with matching non-user elements") { + expect(eventDictionary.count) == 8 expect(eventDictionary.eventKind) == .feature expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) @@ -409,11 +390,7 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: NSNull())).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. - expect(eventDictionary.eventData).to(beNil()) - } - it("creates a dictionary with the user key only") { expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUserDictionary).to(beNil()) } } it("creates a dictionary with contextKind for anonymous user") { @@ -439,15 +416,11 @@ final class EventSpec: QuickSpec { config.inlineUserInEvents = inlineUser eventDictionary = event.dictionaryValue(config: config) + expect(eventDictionary.count) == 4 expect(eventDictionary.eventKind) == .identify expect(eventDictionary.eventKey) == user.key expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.1)) - expect(eventDictionary.eventValue).to(beNil()) - expect(eventDictionary.eventDefaultValue).to(beNil()) - expect(eventDictionary.eventVariation).to(beNil()) - expect(eventDictionary.eventData).to(beNil()) expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - expect(eventDictionary.eventUserKey).to(beNil()) } } } @@ -462,27 +435,33 @@ final class EventSpec: QuickSpec { context("alias event") { it("known to known") { let eventDictionary = Event.aliasEvent(newUser: user1, oldUser: user2).dictionaryValue(config: config) + expect(eventDictionary.count) == 6 expect(eventDictionary.eventKind) == .alias expect(eventDictionary.eventKey) == user1.key expect(eventDictionary.eventPreviousKey) == user2.key expect(eventDictionary.eventContextKind) == "user" expect(eventDictionary.eventPreviousContextKind) == "user" + expect(eventDictionary.eventCreationDate).toNot(beNil()) } it("unknown to known") { let eventDictionary = Event.aliasEvent(newUser: user1, oldUser: anonUser1).dictionaryValue(config: config) + expect(eventDictionary.count) == 6 expect(eventDictionary.eventKind) == .alias expect(eventDictionary.eventKey) == user1.key expect(eventDictionary.eventPreviousKey) == anonUser1.key expect(eventDictionary.eventContextKind) == "user" expect(eventDictionary.eventPreviousContextKind) == "anonymousUser" + expect(eventDictionary.eventCreationDate).toNot(beNil()) } it("unknown to unknown") { let eventDictionary = Event.aliasEvent(newUser: anonUser1, oldUser: anonUser2).dictionaryValue(config: config) + expect(eventDictionary.count) == 6 expect(eventDictionary.eventKind) == .alias expect(eventDictionary.eventKey) == anonUser1.key expect(eventDictionary.eventPreviousKey) == anonUser2.key expect(eventDictionary.eventContextKind) == "anonymousUser" expect(eventDictionary.eventPreviousContextKind) == "anonymousUser" + expect(eventDictionary.eventCreationDate).toNot(beNil()) } } } @@ -501,114 +480,66 @@ final class EventSpec: QuickSpec { for eventData in CustomEvent.allData { context("with valid json data") { beforeEach { - do { - event = try Event.customEvent(key: Constants.eventKey, user: user, data: eventData, metricValue: metricValue) - } catch is LDInvalidArgumentError { - fail("customEvent threw an invalid argument exception") - } catch { - fail("customEvent threw an exception") - } + event = Event.customEvent(key: Constants.eventKey, user: user, data: eventData, metricValue: metricValue) eventDictionary = event.dictionaryValue(config: config) } it("creates a dictionary with matching custom data") { + expect(eventDictionary.count) == 6 expect(eventDictionary.eventKind) == .custom expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventData, to: eventData)).to(beTrue()) - expect(eventDictionary.eventValue).to(beNil()) - expect(eventDictionary.eventDefaultValue).to(beNil()) - expect(eventDictionary.eventVariation).to(beNil()) + expect(LDValue.fromAny(eventDictionary.eventData)) == eventData expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUserDictionary).to(beNil()) expect(eventDictionary.eventMetricValue) == metricValue } } } context("without data") { beforeEach { - do { - event = try Event.customEvent(key: Constants.eventKey, user: user, data: nil) - } catch is LDInvalidArgumentError { - fail("customEvent threw an invalid argument exception") - } catch { - fail("customEvent threw an exception") - } + event = Event.customEvent(key: Constants.eventKey, user: user, data: nil) eventDictionary = event.dictionaryValue(config: config) } it("creates a dictionary with matching custom data") { + expect(eventDictionary.count) == 4 expect(eventDictionary.eventKind) == .custom expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(eventDictionary.eventData).to(beNil()) - expect(eventDictionary.eventValue).to(beNil()) - expect(eventDictionary.eventDefaultValue).to(beNil()) - expect(eventDictionary.eventVariation).to(beNil()) expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUserDictionary).to(beNil()) } } context("without inlining user") { beforeEach { - do { - event = try Event.customEvent(key: Constants.eventKey, user: user, data: CustomEvent.dictionaryData) - } catch is LDInvalidArgumentError { - fail("customEvent threw an invalid argument exception") - } catch { - fail("customEvent threw an exception") - } + event = Event.customEvent(key: Constants.eventKey, user: user, data: CustomEvent.dictionaryData) config.inlineUserInEvents = false // Default value, here for clarity eventDictionary = event.dictionaryValue(config: config) } - it("creates a dictionary with matching non-user elements") { + it("creates a dictionary with matching elements") { + expect(eventDictionary.count) == 5 expect(eventDictionary.eventKind) == .custom expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventData, to: CustomEvent.dictionaryData)).to(beTrue()) - expect(eventDictionary.eventValue).to(beNil()) - expect(eventDictionary.eventDefaultValue).to(beNil()) - expect(eventDictionary.eventVariation).to(beNil()) - } - it("creates a dictionary with the user key only") { + expect(LDValue.fromAny(eventDictionary.eventData)) == CustomEvent.dictionaryData expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUserDictionary).to(beNil()) } } context("inlining user") { beforeEach { - do { - event = try Event.customEvent(key: Constants.eventKey, user: user, data: CustomEvent.dictionaryData) - } catch is LDInvalidArgumentError { - fail("customEvent threw an invalid argument exception") - } catch { - fail("customEvent threw an exception") - } + event = Event.customEvent(key: Constants.eventKey, user: user, data: CustomEvent.dictionaryData) config.inlineUserInEvents = true eventDictionary = event.dictionaryValue(config: config) } - it("creates a dictionary with matching non-user elements") { + it("creates a dictionary with matching elements") { + expect(eventDictionary.count) == 5 expect(eventDictionary.eventKind) == .custom expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventData, to: CustomEvent.dictionaryData)).to(beTrue()) - expect(eventDictionary.eventValue).to(beNil()) - expect(eventDictionary.eventDefaultValue).to(beNil()) - expect(eventDictionary.eventVariation).to(beNil()) - expect(eventDictionary.eventContextKind).to(beNil()) - } - it("creates a dictionary with the full user") { + expect(LDValue.fromAny(eventDictionary.eventData)) == CustomEvent.dictionaryData expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - expect(eventDictionary.eventUserKey).to(beNil()) } } context("with anonymous user") { it("sets contextKind field") { - do { - event = try Event.customEvent(key: Constants.eventKey, user: LDUser()) - } catch is LDInvalidArgumentError { - fail("customEvent threw an invalid argument exception") - } catch { - fail("customEvent threw an exception") - } + event = Event.customEvent(key: Constants.eventKey, user: LDUser(), data: nil) eventDictionary = event.dictionaryValue(config: config) expect(eventDictionary.eventContextKind) == "anonymousUser" } @@ -632,10 +563,11 @@ final class EventSpec: QuickSpec { event = Event.debugEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) } [true, false].forEach { inlineUser in - it("creates a dictionary with matching non-user elements") { + it("creates a dictionary with matching elements") { config.inlineUserInEvents = inlineUser eventDictionary = event.dictionaryValue(config: config) + expect(eventDictionary.count) == 8 expect(eventDictionary.eventKind) == .debug expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) @@ -643,14 +575,7 @@ final class EventSpec: QuickSpec { expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(eventDictionary.eventVariation) == featureFlag.variation expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. - expect(eventDictionary.eventData).to(beNil()) - } - it("creates a dictionary with the full user") { - config.inlineUserInEvents = inlineUser - eventDictionary = event.dictionaryValue(config: config) - expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - expect(eventDictionary.eventUserKey).to(beNil()) } } } @@ -661,16 +586,15 @@ final class EventSpec: QuickSpec { eventDictionary = event.dictionaryValue(config: config) } it("creates a dictionary with the version") { + expect(eventDictionary.count) == 8 expect(eventDictionary.eventKind) == .debug expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - expect(eventDictionary.eventUserKey).to(beNil()) expect(eventDictionary.eventVariation) == featureFlag.variation expect(eventDictionary.eventVersion) == featureFlag.version - expect(eventDictionary.eventData).to(beNil()) } } context("omitting flagVersion and version") { @@ -680,16 +604,14 @@ final class EventSpec: QuickSpec { eventDictionary = event.dictionaryValue(config: config) } it("creates a dictionary without the version") { + expect(eventDictionary.count) == 7 expect(eventDictionary.eventKind) == .debug expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - expect(eventDictionary.eventUserKey).to(beNil()) expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion).to(beNil()) - expect(eventDictionary.eventData).to(beNil()) } } context("without value or defaultValue") { @@ -699,16 +621,15 @@ final class EventSpec: QuickSpec { eventDictionary = event.dictionaryValue(config: config) } it("creates a dictionary with matching non-user elements") { + expect(eventDictionary.count) == 8 expect(eventDictionary.eventKind) == .debug expect(eventDictionary.eventKey) == Constants.eventKey expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) expect(AnyComparer.isEqual(eventDictionary.eventValue, to: NSNull())).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: NSNull())).to(beTrue()) expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - expect(eventDictionary.eventUserKey).to(beNil()) expect(eventDictionary.eventVariation) == featureFlag.variation expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. - expect(eventDictionary.eventData).to(beNil()) } } } @@ -726,6 +647,7 @@ final class EventSpec: QuickSpec { eventDictionary = event.dictionaryValue(config: config) } it("creates a summary dictionary with matching elements") { + expect(eventDictionary.count) == 4 expect(eventDictionary.eventKind) == .summary expect(eventDictionary.eventStartDate).to(beCloseTo(event.flagRequestTracker!.startDate, within: 0.001)) expect(eventDictionary.eventEndDate).to(beCloseTo(event.endDate!, within: 0.001)) @@ -743,93 +665,6 @@ final class EventSpec: QuickSpec { } expect(AnyComparer.isEqual(flagCounterDictionary, to: flagCounter.dictionaryValue, considerNilAndNullEqual: true)).to(beTrue()) } - - expect(eventDictionary.eventKey).to(beNil()) - expect(eventDictionary.eventCreationDate).to(beNil()) - expect(eventDictionary.eventUserDictionary).to(beNil()) - expect(eventDictionary.eventUserKey).to(beNil()) - expect(eventDictionary.eventValue).to(beNil()) - expect(eventDictionary.eventDefaultValue).to(beNil()) - expect(eventDictionary.eventVariation).to(beNil()) - expect(eventDictionary.eventVersion).to(beNil()) - expect(eventDictionary.eventData).to(beNil()) - } - } - } - - // Dictionary extension methods that extract an event key, or creationDateMillis, and compare them with another dictionary - private func eventDictionarySpec() { - let config = LDConfig.stub - let user = LDUser.stub() - describe("event dictionary") { - describe("eventKind") { - context("when the dictionary contains the event kind") { - var events: [Event]! - var eventDictionary: [String: Any]! - beforeEach { - events = Event.stubEvents(for: user) - } - it("returns the event kind") { - events.forEach { event in - eventDictionary = event.dictionaryValue(config: config) - expect(eventDictionary.eventKind) == event.kind - } - } - } - it("returns nil when the dictionary does not contain the event kind") { - let event = Event.stub(.custom, with: user) - var eventDictionary = event.dictionaryValue(config: config) - eventDictionary.removeValue(forKey: Event.CodingKeys.kind.rawValue) - expect(eventDictionary.eventKind).to(beNil()) - } - } - - describe("eventKey") { - var event: Event! - var eventDictionary: [String: Any]! - beforeEach { - event = Event.stub(.custom, with: user) - eventDictionary = event.dictionaryValue(config: config) - } - it("returns the key when the dictionary contains a key") { - expect(eventDictionary.eventKey) == event.key - } - it("returns nil when the dictionary does not contain a key") { - eventDictionary.removeValue(forKey: Event.CodingKeys.key.rawValue) - expect(eventDictionary.eventKey).to(beNil()) - } - } - - describe("eventCreationDateMillis") { - var event: Event! - var eventDictionary: [String: Any]! - beforeEach { - event = Event.stub(.custom, with: user) - eventDictionary = event.dictionaryValue(config: config) - } - it("returns the creation date millis when the dictionary contains a creation date") { - expect(eventDictionary.eventCreationDateMillis) == event.creationDate?.millisSince1970 - } - it("returns nil when the dictionary does not contain a creation date") { - eventDictionary.removeValue(forKey: Event.CodingKeys.creationDate.rawValue) - expect(eventDictionary.eventCreationDateMillis).to(beNil()) - } - } - - describe("eventEndDate") { - var event: Event! - var eventDictionary: [String: Any]! - beforeEach { - event = Event.stub(.summary, with: user) - eventDictionary = event.dictionaryValue(config: config) - } - it("returns the event kind when the dictionary contains the event endDate") { - expect(eventDictionary.eventEndDate).to(beCloseTo(event.endDate!, within: 0.001)) - } - it("returns nil when the dictionary does not contain the event kind") { - eventDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) - expect(eventDictionary.eventEndDate).to(beNil()) - } } } } @@ -886,9 +721,6 @@ extension Dictionary where Key == String, Value == Any { var eventPreviousKey: String? { self[Event.CodingKeys.previousKey.rawValue] as? String } - var eventCreationDateMillis: Int64? { - self[Event.CodingKeys.creationDate.rawValue] as? Int64 - } var eventContextKind: String? { self[Event.CodingKeys.contextKind.rawValue] as? String } @@ -907,7 +739,7 @@ extension Event { let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) return Event.debugEvent(key: UUID().uuidString, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) case .identify: return Event.identifyEvent(user: user) - case .custom: return (try? Event.customEvent(key: UUID().uuidString, user: user, data: ["custom": UUID().uuidString]))! + case .custom: return Event.customEvent(key: UUID().uuidString, user: user, data: ["custom": .string(UUID().uuidString)]) case .summary: return Event.summaryEvent(flagRequestTracker: FlagRequestTracker.stub())! case .alias: return Event.aliasEvent(newUser: LDUser(), oldUser: LDUser()) } @@ -932,3 +764,15 @@ extension Event { } } } + +extension CounterValue: Equatable { + public static func == (lhs: CounterValue, rhs: CounterValue) -> Bool { + lhs.value == rhs.value && lhs.count == rhs.count + } +} + +extension FlagCounter: Equatable { + public static func == (lhs: FlagCounter, rhs: FlagCounter) -> Bool { + lhs.defaultValue == rhs.defaultValue && lhs.flagValueCounters == rhs.flagValueCounters + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift index 3fc83097..83d2c8b0 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift @@ -6,22 +6,22 @@ import XCTest final class FlagCounterSpec: XCTestCase { func testInit() { let flagCounter = FlagCounter() - XCTAssertNil(flagCounter.defaultValue) + XCTAssertEqual(flagCounter.defaultValue, .null) XCTAssert(flagCounter.flagValueCounters.isEmpty) } func testTrackRequestInitialKnown() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() + let reportedValue: LDValue = "a" + let defaultValue: LDValue = "b" let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, flagVersion: 3) let flagCounter = FlagCounter() flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) let result = flagCounter.dictionaryValue - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) + XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) let counters = result.flagCounterFlagValueCounters XCTAssertEqual(counters?.count, 1) let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter.valueCounterReportedValue, reportedValue) XCTAssertEqual(counter.valueCounterVersion, 3) XCTAssertEqual(counter.valueCounterVariation, 2) XCTAssertNil(counter.valueCounterIsUnknown) @@ -29,19 +29,19 @@ final class FlagCounterSpec: XCTestCase { } func testTrackRequestKnownMatching() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() + let reportedValue: LDValue = "a" + let defaultValue: LDValue = "b" let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 5, flagVersion: 3) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 7, flagVersion: 3) let flagCounter = FlagCounter() flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: secondFeatureFlag, defaultValue: defaultValue) let result = flagCounter.dictionaryValue - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) + XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) let counters = result.flagCounterFlagValueCounters XCTAssertEqual(counters?.count, 1) let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter.valueCounterReportedValue, reportedValue) XCTAssertEqual(counter.valueCounterVersion, 3) XCTAssertEqual(counter.valueCounterVariation, 2) XCTAssertNil(counter.valueCounterIsUnknown) @@ -49,26 +49,26 @@ final class FlagCounterSpec: XCTestCase { } func testTrackRequestKnownDifferentVariations() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() + let reportedValue: LDValue = "a" + let defaultValue: LDValue = "b" let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10, flagVersion: 5) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 3, version: 10, flagVersion: 5) let flagCounter = FlagCounter() flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: secondFeatureFlag, defaultValue: defaultValue) let result = flagCounter.dictionaryValue - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) + XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) let counters = result.flagCounterFlagValueCounters XCTAssertEqual(counters?.count, 2) let counter1 = counters!.first { $0.valueCounterVariation == 2 }! let counter2 = counters!.first { $0.valueCounterVariation == 3 }! - XCTAssert(counter1.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter1.valueCounterReportedValue, reportedValue) XCTAssertEqual(counter1.valueCounterVersion, 5) XCTAssertEqual(counter1.valueCounterVariation, 2) XCTAssertNil(counter1.valueCounterIsUnknown) XCTAssertEqual(counter1.valueCounterCount, 1) - XCTAssert(counter2.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter2.valueCounterReportedValue, reportedValue) XCTAssertEqual(counter2.valueCounterVersion, 5) XCTAssertEqual(counter2.valueCounterVariation, 3) XCTAssertNil(counter2.valueCounterIsUnknown) @@ -76,26 +76,26 @@ final class FlagCounterSpec: XCTestCase { } func testTrackRequestKnownDifferentFlagVersions() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() + let reportedValue: LDValue = "a" + let defaultValue: LDValue = "b" let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10, flagVersion: 3) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10, flagVersion: 5) let flagCounter = FlagCounter() flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: secondFeatureFlag, defaultValue: defaultValue) let result = flagCounter.dictionaryValue - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) + XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) let counters = result.flagCounterFlagValueCounters XCTAssertEqual(counters?.count, 2) let counter1 = counters!.first { $0.valueCounterVersion == 3 }! let counter2 = counters!.first { $0.valueCounterVersion == 5 }! - XCTAssert(counter1.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter1.valueCounterReportedValue, reportedValue) XCTAssertEqual(counter1.valueCounterVersion, 3) XCTAssertEqual(counter1.valueCounterVariation, 2) XCTAssertNil(counter1.valueCounterIsUnknown) XCTAssertEqual(counter1.valueCounterCount, 1) - XCTAssert(counter2.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter2.valueCounterReportedValue, reportedValue) XCTAssertEqual(counter2.valueCounterVersion, 5) XCTAssertEqual(counter2.valueCounterVariation, 2) XCTAssertNil(counter2.valueCounterIsUnknown) @@ -103,19 +103,19 @@ final class FlagCounterSpec: XCTestCase { } func testTrackRequestKnownMissingFlagVersionsMatchingVersions() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() + let reportedValue: LDValue = "a" + let defaultValue: LDValue = "b" let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10) let flagCounter = FlagCounter() flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: secondFeatureFlag, defaultValue: defaultValue) let result = flagCounter.dictionaryValue - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) + XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) let counters = result.flagCounterFlagValueCounters XCTAssertEqual(counters?.count, 1) let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter.valueCounterReportedValue, reportedValue) XCTAssertEqual(counter.valueCounterVersion, 10) XCTAssertEqual(counter.valueCounterVariation, 2) XCTAssertNil(counter.valueCounterIsUnknown) @@ -123,26 +123,26 @@ final class FlagCounterSpec: XCTestCase { } func testTrackRequestKnownMissingFlagVersionsDifferentVersions() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() + let reportedValue: LDValue = "a" + let defaultValue: LDValue = "b" let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 5) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10) let flagCounter = FlagCounter() flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: secondFeatureFlag, defaultValue: defaultValue) let result = flagCounter.dictionaryValue - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) + XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) let counters = result.flagCounterFlagValueCounters XCTAssertEqual(counters?.count, 2) let counter1 = counters!.first { $0.valueCounterVersion == 5 }! let counter2 = counters!.first { $0.valueCounterVersion == 10 }! - XCTAssert(counter1.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter1.valueCounterReportedValue, reportedValue) XCTAssertEqual(counter1.valueCounterVersion, 5) XCTAssertEqual(counter1.valueCounterVariation, 2) XCTAssertNil(counter1.valueCounterIsUnknown) XCTAssertEqual(counter1.valueCounterCount, 1) - XCTAssert(counter2.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter2.valueCounterReportedValue, reportedValue) XCTAssertEqual(counter2.valueCounterVersion, 10) XCTAssertEqual(counter2.valueCounterVariation, 2) XCTAssertNil(counter2.valueCounterIsUnknown) @@ -150,16 +150,16 @@ final class FlagCounterSpec: XCTestCase { } func testTrackRequestInitialUnknown() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() + let reportedValue: LDValue = "a" + let defaultValue: LDValue = "b" let flagCounter = FlagCounter() flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: nil, defaultValue: defaultValue) let result = flagCounter.dictionaryValue - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) + XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) let counters = result.flagCounterFlagValueCounters XCTAssertEqual(counters?.count, 1) let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter.valueCounterReportedValue, reportedValue) XCTAssertNil(counter.valueCounterVersion) XCTAssertNil(counter.valueCounterVariation) XCTAssertEqual(counter.valueCounterIsUnknown, true) @@ -167,17 +167,17 @@ final class FlagCounterSpec: XCTestCase { } func testTrackRequestSecondUnknown() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() + let reportedValue: LDValue = "a" + let defaultValue: LDValue = "b" let flagCounter = FlagCounter() flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: nil, defaultValue: defaultValue) flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: nil, defaultValue: defaultValue) let result = flagCounter.dictionaryValue - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) + XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) let counters = result.flagCounterFlagValueCounters XCTAssertEqual(counters?.count, 1) let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === reportedValue) + XCTAssertEqual(counter.valueCounterReportedValue, reportedValue) XCTAssertNil(counter.valueCounterVersion) XCTAssertNil(counter.valueCounterVariation) XCTAssertEqual(counter.valueCounterIsUnknown, true) @@ -185,19 +185,19 @@ final class FlagCounterSpec: XCTestCase { } func testTrackRequestSecondUnknownWithDifferentValues() { - let initialReportedValue = Placeholder() - let initialDefaultValue = Placeholder() - let secondReportedValue = Placeholder() - let secondDefaultValue = Placeholder() + let initialReportedValue: LDValue = "a" + let initialDefaultValue: LDValue = "b" + let secondReportedValue: LDValue = "c" + let secondDefaultValue: LDValue = "d" let flagCounter = FlagCounter() flagCounter.trackRequest(reportedValue: initialReportedValue, featureFlag: nil, defaultValue: initialDefaultValue) flagCounter.trackRequest(reportedValue: secondReportedValue, featureFlag: nil, defaultValue: secondDefaultValue) let result = flagCounter.dictionaryValue - XCTAssert(result.flagCounterDefaultValue as! Placeholder === secondDefaultValue) + XCTAssertEqual(result.flagCounterDefaultValue, secondDefaultValue) let counters = result.flagCounterFlagValueCounters XCTAssertEqual(counters?.count, 1) let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === initialReportedValue) + XCTAssertEqual(counter.valueCounterReportedValue, initialReportedValue) XCTAssertNil(counter.valueCounterVersion) XCTAssertNil(counter.valueCounterVariation) XCTAssertEqual(counter.valueCounterIsUnknown, true) @@ -205,20 +205,18 @@ final class FlagCounterSpec: XCTestCase { } } -private class Placeholder { } - extension FlagCounter { struct Constants { static let requestCount = 5 } - class func stub(flagKey: LDFlagKey, includeVersion: Bool = true, includeFlagVersion: Bool = true) -> FlagCounter { + class func stub(flagKey: LDFlagKey) -> FlagCounter { let flagCounter = FlagCounter() var featureFlag: FeatureFlag? = nil if flagKey.isKnown { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey, includeVersion: includeVersion, includeFlagVersion: includeFlagVersion) + featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey, includeVersion: true, includeFlagVersion: true) for _ in 0.. Bool { - AnyComparer.isEqual(lhs.value, to: rhs.value) && lhs.count == rhs.count - } -} - -extension FlagCounter: Equatable { - public static func == (lhs: FlagCounter, rhs: FlagCounter) -> Bool { - AnyComparer.isEqual(lhs.defaultValue, to: rhs.defaultValue, considerNilAndNullEqual: true) && - lhs.flagValueCounters == rhs.flagValueCounters - } -} - extension Dictionary where Key == String, Value == Any { - var valueCounterReportedValue: Any? { - self[FlagCounter.CodingKeys.value.rawValue] + fileprivate var valueCounterReportedValue: LDValue { + LDValue.fromAny(self[FlagCounter.CodingKeys.value.rawValue]) } - var valueCounterVariation: Int? { + fileprivate var valueCounterVariation: Int? { self[FlagCounter.CodingKeys.variation.rawValue] as? Int } - var valueCounterVersion: Int? { + fileprivate var valueCounterVersion: Int? { self[FlagCounter.CodingKeys.version.rawValue] as? Int } - var valueCounterIsUnknown: Bool? { + fileprivate var valueCounterIsUnknown: Bool? { self[FlagCounter.CodingKeys.unknown.rawValue] as? Bool } - var valueCounterCount: Int? { + fileprivate var valueCounterCount: Int? { self[FlagCounter.CodingKeys.count.rawValue] as? Int } - var flagCounterDefaultValue: Any? { - self[FlagCounter.CodingKeys.defaultValue.rawValue] + fileprivate var flagCounterDefaultValue: LDValue { + LDValue.fromAny(self[FlagCounter.CodingKeys.defaultValue.rawValue]) } - var flagCounterFlagValueCounters: [[String: Any]]? { + fileprivate var flagCounterFlagValueCounters: [[String: Any]]? { self[FlagCounter.CodingKeys.counters.rawValue] as? [[String: Any]] } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift index 82aacd45..5365a7bc 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift @@ -82,15 +82,6 @@ extension FlagRequestTracker { } } -extension FlagRequestTracker: Equatable { - public static func == (lhs: FlagRequestTracker, rhs: FlagRequestTracker) -> Bool { - if fabs(lhs.startDate.timeIntervalSince(rhs.startDate)) > 0.001 { - return false - } - return lhs.flagCounters == rhs.flagCounters - } -} - extension Dictionary where Key == String, Value == Any { var flagRequestTrackerStartDateMillis: Int64? { self[FlagRequestTracker.CodingKeys.startDate.rawValue] as? Int64 diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index 41535605..0cf4d5fb 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -8,7 +8,7 @@ final class EventReporterSpec: QuickSpec { struct Constants { static let eventFlushInterval: TimeInterval = 10.0 static let eventFlushIntervalHalfSecond: TimeInterval = 0.5 - static let defaultValue = false + static let defaultValue: LDValue = false } struct TestContext { @@ -490,12 +490,17 @@ final class EventReporterSpec: QuickSpec { private func recordFeatureAndDebugEventsSpec() { var testContext: TestContext! + let summarizesRequest = { it("summarizes the flag request") { + let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) + expect(flagValueCounter?.value) == LDValue.fromAny(testContext.featureFlag.value) + expect(flagValueCounter?.count) == 1 + }} context("record feature and debug events") { context("when trackEvents is on and a reason is present") { beforeEach { testContext = TestContext(trackEvents: true) testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: testContext.featureFlag.value!, + value: LDValue.fromAny(testContext.featureFlag.value), defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlagWithReason, user: testContext.user, @@ -506,18 +511,13 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.eventStoreKeys.contains(testContext.flagKey)).to(beTrue()) expect(testContext.eventReporter.eventStoreKinds.contains(.feature)).to(beTrue()) } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 1 - } + summarizesRequest() } context("when a reason is present and reason is false but trackReason is true") { beforeEach { testContext = TestContext(trackEvents: true) testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: testContext.featureFlag.value!, + value: LDValue.fromAny(testContext.featureFlag.value), defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlagWithReasonAndTrackReason, user: testContext.user, @@ -528,18 +528,13 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.eventStoreKeys.contains(testContext.flagKey)).to(beTrue()) expect(testContext.eventReporter.eventStoreKinds.contains(.feature)).to(beTrue()) } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 1 - } + summarizesRequest() } context("when trackEvents is off") { beforeEach { testContext = TestContext(trackEvents: false) testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: testContext.featureFlag.value!, + value: LDValue.fromAny(testContext.featureFlag.value), defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlag, user: testContext.user, @@ -548,19 +543,19 @@ final class EventReporterSpec: QuickSpec { it("does not record a feature event") { expect(testContext.eventReporter.eventStore).to(beEmpty()) } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 1 - } + summarizesRequest() } context("when debugEventsUntilDate exists") { context("lastEventResponseDate exists") { context("and debugEventsUntilDate is later") { beforeEach { testContext = TestContext(lastEventResponseDate: Date(), trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, value: testContext.featureFlag.value!, defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlag, user: testContext.user, includeReason: false) + testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, + value: LDValue.fromAny(testContext.featureFlag.value), + defaultValue: Constants.defaultValue, + featureFlag: testContext.featureFlag, + user: testContext.user, + includeReason: false) } it("records a debug event") { expect(testContext.eventReporter.eventStore.count) == 1 @@ -569,66 +564,70 @@ final class EventReporterSpec: QuickSpec { } it("tracks the flag request") { let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) + expect(flagValueCounter?.value) == LDValue.fromAny(testContext.featureFlag.value) expect(flagValueCounter?.count) == 1 } } context("and debugEventsUntilDate is earlier") { beforeEach { testContext = TestContext(lastEventResponseDate: Date(), trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(-TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, value: testContext.featureFlag.value!, defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlag, user: testContext.user, includeReason: false) + testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, + value: LDValue.fromAny(testContext.featureFlag.value), + defaultValue: Constants.defaultValue, + featureFlag: testContext.featureFlag, + user: testContext.user, + includeReason: false) } it("does not record a debug event") { expect(testContext.eventReporter.eventStore).to(beEmpty()) } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 1 - } + summarizesRequest() } } context("lastEventResponseDate is nil") { context("and debugEventsUntilDate is later than current time") { beforeEach { testContext = TestContext(lastEventResponseDate: nil, trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, value: testContext.featureFlag.value!, defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlag, user: testContext.user, includeReason: false) + testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, + value: LDValue.fromAny(testContext.featureFlag.value), + defaultValue: Constants.defaultValue, + featureFlag: testContext.featureFlag, + user: testContext.user, + includeReason: false) } it("records a debug event") { expect(testContext.eventReporter.eventStore.count) == 1 expect(testContext.eventReporter.eventStoreKeys.contains(testContext.flagKey)).to(beTrue()) expect(testContext.eventReporter.eventStoreKinds.contains(.debug)).to(beTrue()) } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 1 - } + summarizesRequest() } context("and debugEventsUntilDate is earlier than current time") { beforeEach { testContext = TestContext(lastEventResponseDate: nil, trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(-TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, value: testContext.featureFlag.value!, defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlag, user: testContext.user, includeReason: false) + testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, + value: LDValue.fromAny(testContext.featureFlag.value), + defaultValue: Constants.defaultValue, + featureFlag: testContext.featureFlag, + user: testContext.user, + includeReason: false) } it("does not record a debug event") { expect(testContext.eventReporter.eventStore).to(beEmpty()) } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 1 - } + summarizesRequest() } } } context("when both trackEvents is true and debugEventsUntilDate is later than lastEventResponseDate") { beforeEach { testContext = TestContext(lastEventResponseDate: Date(), trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, value: testContext.featureFlag.value!, defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlag, user: testContext.user, includeReason: false) + testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, + value: LDValue.fromAny(testContext.featureFlag.value), + defaultValue: Constants.defaultValue, + featureFlag: testContext.featureFlag, + user: testContext.user, + includeReason: false) } it("records a feature and debug event") { expect(testContext.eventReporter.eventStore.count == 2).to(beTrue()) @@ -637,17 +636,17 @@ final class EventReporterSpec: QuickSpec { }.count == 2).to(beTrue()) expect(testContext.eventReporter.eventStoreKinds).to(contain([.feature, .debug])) } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 1 - } + summarizesRequest() } context("when both trackEvents is true, debugEventsUntilDate is later than lastEventResponseDate, reason is false, and track reason is true") { beforeEach { testContext = TestContext(lastEventResponseDate: Date(), trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, value: testContext.featureFlag.value!, defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlagWithReasonAndTrackReason, user: testContext.user, includeReason: false) + testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, + value: LDValue.fromAny(testContext.featureFlag.value), + defaultValue: Constants.defaultValue, + featureFlag: testContext.featureFlagWithReasonAndTrackReason, + user: testContext.user, + includeReason: false) } it("records a feature and debug event") { expect(testContext.eventReporter.eventStore.count == 2).to(beTrue()) @@ -656,33 +655,28 @@ final class EventReporterSpec: QuickSpec { }.count == 2).to(beTrue()) expect(testContext.eventReporter.eventStoreKinds).to(contain([.feature, .debug])) } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 1 - } + summarizesRequest() } context("when debugEventsUntilDate is nil") { beforeEach { testContext = TestContext(lastEventResponseDate: Date(), trackEvents: false, debugEventsUntilDate: nil) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, value: testContext.featureFlag.value!, defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlag, user: testContext.user, includeReason: false) + testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, + value: LDValue.fromAny(testContext.featureFlag.value), + defaultValue: Constants.defaultValue, + featureFlag: testContext.featureFlag, + user: testContext.user, + includeReason: false) } it("does not record an event") { expect(testContext.eventReporter.eventStore).to(beEmpty()) } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 1 - } + summarizesRequest() } context("when eventTrackingContext is nil") { beforeEach { testContext = TestContext(trackEvents: nil) testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: testContext.featureFlag.value!, + value: LDValue.fromAny(testContext.featureFlag.value), defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlag, user: testContext.user, @@ -691,12 +685,7 @@ final class EventReporterSpec: QuickSpec { it("does not record an event") { expect(testContext.eventReporter.eventStore).to(beEmpty()) } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 1 - } + summarizesRequest() } context("when multiple flag requests are made") { context("serially") { @@ -704,7 +693,7 @@ final class EventReporterSpec: QuickSpec { testContext = TestContext(trackEvents: false) for _ in 1...3 { testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: testContext.featureFlag.value!, + value: LDValue.fromAny(testContext.featureFlag.value), defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlag, user: testContext.user, @@ -713,8 +702,7 @@ final class EventReporterSpec: QuickSpec { } it("tracks the flag request") { let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) + expect(flagValueCounter?.value) == LDValue.fromAny(testContext.featureFlag.value) expect(flagValueCounter?.count) == 3 } } @@ -738,7 +726,7 @@ final class EventReporterSpec: QuickSpec { for _ in 1...5 { requestQueue.asyncAfter(deadline: fireTime) { testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: testContext.featureFlag.value!, + value: LDValue.fromAny(testContext.featureFlag.value), defaultValue: Constants.defaultValue, featureFlag: testContext.featureFlag, user: testContext.user, @@ -750,8 +738,7 @@ final class EventReporterSpec: QuickSpec { } it("tracks the flag request") { let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) + expect(flagValueCounter?.value) == LDValue.fromAny(testContext.featureFlag.value) expect(flagValueCounter?.count) == 5 } } @@ -764,17 +751,20 @@ final class EventReporterSpec: QuickSpec { var testContext: TestContext! beforeEach { testContext = TestContext() - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, value: testContext.featureFlag.value, defaultValue: testContext.featureFlag.value, featureFlag: testContext.featureFlag, user: testContext.user, includeReason: false) + testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, + value: LDValue.fromAny(testContext.featureFlag.value), + defaultValue: LDValue.fromAny(testContext.featureFlag.value), + featureFlag: testContext.featureFlag, + user: testContext.user, + includeReason: false) } it("tracks flag requests") { let flagCounter = testContext.flagCounter(for: testContext.flagKey) - expect(flagCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagCounter?.defaultValue, to: testContext.featureFlag.value, considerNilAndNullEqual: true)).to(beTrue()) + expect(flagCounter?.defaultValue) == LDValue.fromAny(testContext.featureFlag.value) expect(flagCounter?.flagValueCounters.count) == 1 let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) + expect(flagValueCounter?.value) == LDValue.fromAny(testContext.featureFlag.value) expect(flagValueCounter?.count) == 1 } } From f6ebd8688a5d814cc40eaba120b011de66ca6d0a Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Tue, 15 Mar 2022 20:40:43 -0500 Subject: [PATCH 35/50] Use Encodable instances for Event serialization. --- .../LaunchDarkly/Extensions/Dictionary.swift | 4 - LaunchDarkly/LaunchDarkly/Models/Event.swift | 72 +- .../FeatureFlag/FlagRequestTracker.swift | 47 +- .../Networking/DarklyService.swift | 9 +- .../ServiceObjects/EventReporter.swift | 29 +- .../Mocks/DarklyServiceMock.swift | 52 +- .../LaunchDarklyTests/Models/EventSpec.swift | 755 ++++++------------ .../FlagRequestTracking/FlagCounterSpec.swift | 316 +++----- .../FlagRequestTrackerSpec.swift | 57 +- .../Networking/DarklyServiceSpec.swift | 44 +- .../ServiceObjects/EventReporterSpec.swift | 110 ++- LaunchDarkly/LaunchDarklyTests/TestUtil.swift | 34 + 12 files changed, 593 insertions(+), 936 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift b/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift index be516003..e70fce44 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift @@ -23,10 +23,6 @@ extension Dictionary where Key == String { } return differingKeys.union(matchingKeysWithDifferentValues).sorted() } - - var base64UrlEncodedString: String? { - jsonData?.base64UrlEncodedString - } } extension Dictionary where Key == String, Value == Any { diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index 42d1d854..d028204d 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -4,11 +4,11 @@ func userType(_ user: LDUser) -> String { return user.isAnonymous ? "anonymousUser" : "user" } -struct Event { +struct Event: Encodable { enum CodingKeys: String, CodingKey { case key, previousKey, kind, creationDate, user, userKey, value, defaultValue = "default", variation, version, - data, endDate, reason, metricValue, + data, startDate, endDate, features, reason, metricValue, // for aliasing contextKind, previousContextKind } @@ -114,52 +114,48 @@ struct Event { return Event(kind: .alias, key: new.key, previousKey: old.key, contextKind: userType(new), previousContextKind: userType(old)) } - func dictionaryValue(config: LDConfig) -> [String: Any] { - var eventDictionary = [String: Any]() - eventDictionary[CodingKeys.kind.rawValue] = kind.rawValue - eventDictionary[CodingKeys.key.rawValue] = key - eventDictionary[CodingKeys.previousKey.rawValue] = previousKey - eventDictionary[CodingKeys.creationDate.rawValue] = creationDate?.millisSince1970 - if kind.isAlwaysInlineUserKind || config.inlineUserInEvents { - eventDictionary[CodingKeys.user.rawValue] = user?.dictionaryValue(includePrivateAttributes: false, config: config) + struct UserInfoKeys { + static let inlineUserInEvents = CodingUserInfoKey(rawValue: "LD_inlineUserInEvents")! + } + + func encode(to encoder: Encoder) throws { + let inlineUserInEvents = encoder.userInfo[UserInfoKeys.inlineUserInEvents] as? Bool ?? false + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(kind.rawValue, forKey: .kind) + try container.encodeIfPresent(key, forKey: .key) + try container.encodeIfPresent(previousKey, forKey: .previousKey) + try container.encodeIfPresent(creationDate, forKey: .creationDate) + if kind.isAlwaysInlineUserKind || inlineUserInEvents { + try container.encodeIfPresent(user, forKey: .user) } else { - eventDictionary[CodingKeys.userKey.rawValue] = user?.key + try container.encodeIfPresent(user?.key, forKey: .userKey) } if kind.isAlwaysIncludeValueKinds { - eventDictionary[CodingKeys.value.rawValue] = value.toAny() ?? NSNull() - eventDictionary[CodingKeys.defaultValue.rawValue] = defaultValue.toAny() ?? NSNull() + try container.encode(value, forKey: .value) + try container.encode(defaultValue, forKey: .defaultValue) + } + try container.encodeIfPresent(featureFlag?.variation, forKey: .variation) + try container.encodeIfPresent(featureFlag?.versionForEvents, forKey: .version) + if data != .null { + try container.encode(data, forKey: .data) } - eventDictionary[CodingKeys.variation.rawValue] = featureFlag?.variation - // If the flagVersion exists, it is reported as the "version". If not, the version is reported using the "version" key. - eventDictionary[CodingKeys.version.rawValue] = featureFlag?.flagVersion ?? featureFlag?.version - eventDictionary[CodingKeys.data.rawValue] = data.toAny() if let flagRequestTracker = flagRequestTracker { - eventDictionary.merge(flagRequestTracker.dictionaryValue) { _, trackerItem in - trackerItem // This should never happen because the eventDictionary does not use any conflicting keys with the flagRequestTracker - } + try container.encode(flagRequestTracker.startDate, forKey: .startDate) + try container.encode(flagRequestTracker.flagCounters, forKey: .features) } - eventDictionary[CodingKeys.endDate.rawValue] = endDate?.millisSince1970 - eventDictionary[CodingKeys.reason.rawValue] = includeReason || featureFlag?.trackReason ?? false ? featureFlag?.reason : nil - eventDictionary[CodingKeys.metricValue.rawValue] = metricValue - + try container.encodeIfPresent(endDate, forKey: .endDate) + if let reason = includeReason || featureFlag?.trackReason ?? false ? featureFlag?.reason : nil { + try container.encode(LDValue.fromAny(reason), forKey: .reason) + } + try container.encodeIfPresent(metricValue, forKey: .metricValue) if kind.needsContextKind && (user?.isAnonymous == true) { - eventDictionary[CodingKeys.contextKind.rawValue] = "anonymousUser" + try container.encode("anonymousUser", forKey: .contextKind) } - if kind == .alias { - eventDictionary[CodingKeys.contextKind.rawValue] = self.contextKind - eventDictionary[CodingKeys.previousContextKind.rawValue] = self.previousContextKind + try container.encodeIfPresent(self.contextKind, forKey: .contextKind) + try container.encodeIfPresent(self.previousContextKind, forKey: .previousContextKind) } - - return eventDictionary - } -} - -extension Array where Element == [String: Any] { - var jsonData: Data? { - guard JSONSerialization.isValidJSONObject(self) - else { return nil } - return try? JSONSerialization.data(withJSONObject: self, options: []) } } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift index 98a03cb9..909696c8 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift @@ -1,10 +1,6 @@ import Foundation struct FlagRequestTracker { - enum CodingKeys: String, CodingKey { - case startDate, features - } - let startDate = Date() var flagCounters: [LDFlagKey: FlagCounter] = [:] @@ -23,23 +19,22 @@ struct FlagRequestTracker { + "\n\tdefaultValue: \(defaultValue)\n") } - var dictionaryValue: [String: Any] { - [CodingKeys.startDate.rawValue: startDate.millisSince1970, - CodingKeys.features.rawValue: flagCounters.mapValues { $0.dictionaryValue }] - } - var hasLoggedRequests: Bool { !flagCounters.isEmpty } } extension FlagRequestTracker: TypeIdentifying { } -final class FlagCounter { +final class FlagCounter: Encodable { enum CodingKeys: String, CodingKey { - case defaultValue = "default", counters, value, variation, version, unknown, count + case defaultValue = "default", counters + } + + enum CounterCodingKeys: String, CodingKey { + case value, variation, version, unknown, count } - var defaultValue: LDValue = .null - var flagValueCounters: [CounterKey: CounterValue] = [:] + private(set) var defaultValue: LDValue = .null + private(set) var flagValueCounters: [CounterKey: CounterValue] = [:] func trackRequest(reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue) { self.defaultValue = defaultValue @@ -51,20 +46,20 @@ final class FlagCounter { } } - var dictionaryValue: [String: Any] { - let counters: [[String: Any]] = flagValueCounters.map { (key, value) in - var res: [String: Any] = [CodingKeys.value.rawValue: value.value.toAny() ?? NSNull(), - CodingKeys.count.rawValue: value.count, - CodingKeys.variation.rawValue: key.variation ?? NSNull()] - if let version = key.version { - res[CodingKeys.version.rawValue] = version - } else { - res[CodingKeys.unknown.rawValue] = true + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(defaultValue, forKey: .defaultValue) + var countersContainer = container.nestedUnkeyedContainer(forKey: .counters) + try flagValueCounters.forEach { (key, value) in + var counterContainer = countersContainer.nestedContainer(keyedBy: CounterCodingKeys.self) + try counterContainer.encodeIfPresent(key.version, forKey: .version) + try counterContainer.encodeIfPresent(key.variation, forKey: .variation) + try counterContainer.encode(value.count, forKey: .count) + try counterContainer.encode(value.value, forKey: .value) + if key.version == nil { + try counterContainer.encode(true, forKey: .unknown) } - return res } - return [CodingKeys.defaultValue.rawValue: defaultValue.toAny() ?? NSNull(), - CodingKeys.counters.rawValue: counters] } } @@ -75,7 +70,7 @@ struct CounterKey: Equatable, Hashable { class CounterValue { let value: LDValue - var count: Int = 1 + private(set) var count: Int = 1 init(value: LDValue) { self.value = value diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 55391266..7cc30e85 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -20,7 +20,7 @@ protocol DarklyServiceProvider: AnyObject { func getFeatureFlags(useReport: Bool, completion: ServiceCompletionHandler?) func clearFlagResponseCache() func createEventSource(useReport: Bool, handler: EventHandler, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider - func publishEventDictionaries(_ eventDictionaries: [[String: Any]], _ payloadId: String, completion: ServiceCompletionHandler?) + func publishEventData(_ eventData: Data, _ payloadId: String, completion: ServiceCompletionHandler?) func publishDiagnostic(diagnosticEvent: T, completion: ServiceCompletionHandler?) } @@ -173,13 +173,8 @@ final class DarklyService: DarklyServiceProvider { // MARK: Publish Events - func publishEventDictionaries(_ eventDictionaries: [[String: Any]], _ payloadId: String, completion: ServiceCompletionHandler?) { + func publishEventData(_ eventData: Data, _ payloadId: String, completion: ServiceCompletionHandler?) { guard hasMobileKey(#function) else { return } - guard !eventDictionaries.isEmpty, let eventData = eventDictionaries.jsonData - else { - return Log.debug(typeName(and: #function, appending: ": ") + "Aborting. No event dictionary.") - } - let url = config.eventsUrl.appendingPathComponent(EventRequestPath.bulk) let headers = [HTTPHeaders.HeaderKey.eventPayloadIDHeader: payloadId].merging(httpHeaders.eventRequestHeaders) { $1 } doPublish(url: url, headers: headers, body: eventData, completion: completion) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index e2f6f9cd..4925aa83 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -138,14 +138,29 @@ class EventReporter: EventReporting { } private func publish(_ events: [Event], _ payloadId: String, _ completion: CompletionClosure?) { - let eventDictionaries = events.map { $0.dictionaryValue(config: service.config) } - self.service.publishEventDictionaries(eventDictionaries, payloadId) { _, urlResponse, error in - let shouldRetry = self.processEventResponse(sentEvents: eventDictionaries, response: urlResponse as? HTTPURLResponse, error: error, isRetry: false) + let encodingConfig: [CodingUserInfoKey: Any] = + [Event.UserInfoKeys.inlineUserInEvents: service.config.inlineUserInEvents, + LDUser.UserInfoKeys.allAttributesPrivate: service.config.allUserAttributesPrivate, + LDUser.UserInfoKeys.globalPrivateAttributes: service.config.privateUserAttributes.map { $0.name }] + let encoder = JSONEncoder() + encoder.userInfo = encodingConfig + encoder.dateEncodingStrategy = .custom { date, encoder in + var container = encoder.singleValueContainer() + try container.encode(date.millisSince1970) + } + guard let eventData = try? encoder.encode(events) + else { + Log.debug(self.typeName(and: #function) + "Failed to serialize event(s) for publication: \(events)") + completion?() + return + } + self.service.publishEventData(eventData, payloadId) { _, urlResponse, error in + let shouldRetry = self.processEventResponse(sentEvents: events.count, response: urlResponse as? HTTPURLResponse, error: error, isRetry: false) if shouldRetry { Log.debug("Retrying event post after delay.") DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) { - self.service.publishEventDictionaries(eventDictionaries, payloadId) { _, urlResponse, error in - _ = self.processEventResponse(sentEvents: eventDictionaries, response: urlResponse as? HTTPURLResponse, error: error, isRetry: true) + self.service.publishEventData(eventData, payloadId) { _, urlResponse, error in + _ = self.processEventResponse(sentEvents: events.count, response: urlResponse as? HTTPURLResponse, error: error, isRetry: true) completion?() } } @@ -155,10 +170,10 @@ class EventReporter: EventReporting { } } - private func processEventResponse(sentEvents: [[String: Any]], response: HTTPURLResponse?, error: Error?, isRetry: Bool) -> Bool { + private func processEventResponse(sentEvents: Int, response: HTTPURLResponse?, error: Error?, isRetry: Bool) -> Bool { if error == nil && (200..<300).contains(response?.statusCode ?? 0) { self.lastEventResponseDate = response?.headerDate ?? self.lastEventResponseDate - Log.debug(self.typeName(and: #function) + "Completed sending \(sentEvents.count) event(s)") + Log.debug(self.typeName(and: #function) + "Completed sending \(sentEvents) event(s)") self.reportSyncComplete(nil) return false } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 232407c9..698470e3 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -34,10 +34,6 @@ final class DarklyServiceMock: DarklyServiceProvider { static let dictionary: [String: Any] = ["sub-flag-a": false, "sub-flag-b": 3, "sub-flag-c": 2.71828] static let null = NSNull() - static var knownFlags: [Any] { - [bool, int, double, string, array, dictionary, null] - } - static func value(from flagKey: LDFlagKey) -> Any? { switch flagKey { case FlagKeys.bool: return FlagValues.bool @@ -73,13 +69,6 @@ final class DarklyServiceMock: DarklyServiceProvider { } struct Constants { - static var streamData: Data { - let featureFlags = stubFeatureFlags(includeNullValue: false) - let featureFlagDictionaries = featureFlags.dictionaryValue - let eventStreamString = "event: put\ndata:\(featureFlagDictionaries.jsonString!)" - - return eventStreamString.data(using: .utf8)! - } static let error = NSError(domain: NSURLErrorDomain, code: Int(CFNetworkErrors.cfurlErrorResourceUnavailable.rawValue), userInfo: nil) static let jsonErrorString = "Bad json data" static let errorData = jsonErrorString.data(using: .utf8)! @@ -230,17 +219,11 @@ final class DarklyServiceMock: DarklyServiceProvider { } var stubbedEventResponse: ServiceResponse? - var publishEventDictionariesCallCount = 0 - var publishedEventDictionaries: [[String: Any]]? - var publishedEventDictionaryKeys: [String]? { - publishedEventDictionaries?.compactMap { $0.eventKey } - } - var publishedEventDictionaryKinds: [Event.Kind]? { - publishedEventDictionaries?.compactMap { $0.eventKind } - } - func publishEventDictionaries(_ eventDictionaries: [[String: Any]], _ payloadId: String, completion: ServiceCompletionHandler?) { - publishEventDictionariesCallCount += 1 - publishedEventDictionaries = eventDictionaries + var publishEventDataCallCount = 0 + var publishedEventData: Data? + func publishEventData(_ eventData: Data, _ payloadId: String, completion: ServiceCompletionHandler?) { + publishEventDataCallCount += 1 + publishedEventData = eventData completion?(stubbedEventResponse ?? (nil, nil, nil)) } @@ -322,31 +305,6 @@ extension DarklyServiceMock { "\(Constants.stubNameFlag) using method \(useReport ? URLRequest.HTTPMethods.report : URLRequest.HTTPMethods.get) with response status code \(statusCode)" } - // MARK: Stream - - var streamHost: String? { - config.streamUrl.host - } - var getStreamRequestStubTest: HTTPStubsTestBlock { - isScheme(Constants.schemeHttps) && isHost(streamHost!) && isMethodGET() - } - var reportStreamRequestStubTest: HTTPStubsTestBlock { - isScheme(Constants.schemeHttps) && isHost(streamHost!) && isMethodREPORT() - } - - /// Use when testing requires the mock service to actually make an event source connection request - func stubStreamRequest(useReport: Bool, success: Bool, onActivation activate: ((URLRequest, HTTPStubsDescriptor, HTTPStubsResponse) -> Void)? = nil) { - var stubResponse: HTTPStubsResponseBlock = { _ in - HTTPStubsResponse(error: Constants.error) - } - if success { - stubResponse = { _ in - HTTPStubsResponse(data: Constants.streamData, statusCode: Int32(HTTPURLResponse.StatusCodes.ok), headers: nil) - } - } - stubRequest(passingTest: useReport ? reportStreamRequestStubTest : getStreamRequestStubTest, stub: stubResponse, name: Constants.stubNameStream, onActivation: activate) - } - // MARK: Publish Event var eventHost: String? { diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 290ca545..3627b36e 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -9,21 +9,12 @@ final class EventSpec: QuickSpec { } struct CustomEvent { - static let intData: LDValue = 3 - static let doubleData: LDValue = 1.414 - static let boolData: LDValue = true - static let stringData: LDValue = "custom event string data" - static let arrayData: LDValue = [12, 1.61803, true, "custom event array data"] - static let nestedArrayData: LDValue = [1, 3, 7, 12] - static let nestedDictionaryData: LDValue = ["one": 1.0, "three": 3.0, "seven": 7.0, "twelve": 12.0] static let dictionaryData: LDValue = ["dozen": 12, "phi": 1.61803, "true": true, "data string": "custom event dictionary data", - "nestedArray": nestedArrayData, - "nestedDictionary": nestedDictionaryData] - - static let allData: [LDValue] = [intData, doubleData, boolData, stringData, arrayData, dictionaryData] + "nestedArray": [1, 3, 7, 12], + "nestedDictionary": ["one": 1.0, "three": 3.0]] } override func spec() { @@ -34,7 +25,12 @@ final class EventSpec: QuickSpec { customEventSpec() identifyEventSpec() summaryEventSpec() - dictionaryValueSpec() + testAliasEventEncoding() + testCustomEventEncoding() + testDebugEventEncoding() + testFeatureEventEncoding() + testIdentifyEventEncoding() + testSummaryEventEncoding() } private func initSpec() { @@ -86,48 +82,33 @@ final class EventSpec: QuickSpec { private func aliasSpec() { describe("alias events") { - var event: Event! - context("aliasing users") { - it("has correct fields") { - event = Event.aliasEvent(newUser: LDUser(), oldUser: LDUser()) - - expect(event.kind) == Event.Kind.alias - } - - it("from user to user") { - event = Event.aliasEvent(newUser: LDUser(key: "new"), oldUser: LDUser(key: "old")) - - expect(event.key) == "new" - expect(event.previousKey) == "old" - expect(event.contextKind) == "user" - expect(event.previousContextKind) == "user" - } - - it("from anon to anon") { - event = Event.aliasEvent(newUser: LDUser(key: "new", isAnonymous: true), oldUser: LDUser(key: "old", isAnonymous: true)) - - expect(event.key) == "new" - expect(event.previousKey) == "old" - expect(event.contextKind) == "anonymousUser" - expect(event.previousContextKind) == "anonymousUser" - } + it("has correct fields") { + let event = Event.aliasEvent(newUser: LDUser(), oldUser: LDUser()) + expect(event.kind) == Event.Kind.alias + } + it("from user to user") { + let event = Event.aliasEvent(newUser: LDUser(key: "new"), oldUser: LDUser(key: "old")) + expect(event.key) == "new" + expect(event.previousKey) == "old" + expect(event.contextKind) == "user" + expect(event.previousContextKind) == "user" + } + it("from anon to anon") { + let event = Event.aliasEvent(newUser: LDUser(key: "new", isAnonymous: true), oldUser: LDUser(key: "old", isAnonymous: true)) + expect(event.key) == "new" + expect(event.previousKey) == "old" + expect(event.contextKind) == "anonymousUser" + expect(event.previousContextKind) == "anonymousUser" } } } private func featureEventSpec() { - var user: LDUser! - var event: Event! - var featureFlag: FeatureFlag! - beforeEach { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - user = LDUser.stub() - } describe("featureEvent") { - beforeEach { - event = Event.featureEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - } it("creates a feature event with matching data") { + let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) + let user = LDUser.stub() + let event = Event.featureEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) expect(event.kind) == Event.Kind.feature expect(event.key) == Constants.eventKey expect(event.creationDate).toNot(beNil()) @@ -144,18 +125,12 @@ final class EventSpec: QuickSpec { } private func debugEventSpec() { - var user: LDUser! - var event: Event! - var featureFlag: FeatureFlag! - beforeEach { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - user = LDUser.stub() - } describe("debugEvent") { - beforeEach { - event = Event.debugEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - } it("creates a debug event with matching data") { + let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) + let user = LDUser.stub() + let event = Event.debugEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) + expect(event.kind) == Event.Kind.debug expect(event.key) == Constants.eventKey expect(event.creationDate).toNot(beNil()) @@ -177,22 +152,18 @@ final class EventSpec: QuickSpec { user = LDUser.stub() } describe("customEvent") { - for eventData in CustomEvent.allData { - context("with valid json data") { - it("creates a custom event with matching data") { - let event = Event.customEvent(key: Constants.eventKey, user: user, data: eventData) - - expect(event.kind) == Event.Kind.custom - expect(event.key) == Constants.eventKey - expect(event.creationDate).toNot(beNil()) - expect(event.user) == user - expect(event.data) == eventData - - expect(event.value) == .null - expect(event.defaultValue) == .null - expect(event.endDate).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - } + context("with valid json data") { + it("creates a custom event with matching data") { + let event = Event.customEvent(key: Constants.eventKey, user: user, data: ["abc": 123]) + expect(event.kind) == Event.Kind.custom + expect(event.key) == Constants.eventKey + expect(event.creationDate).toNot(beNil()) + expect(event.user) == user + expect(event.data) == ["abc": 123] + expect(event.value) == .null + expect(event.defaultValue) == .null + expect(event.endDate).to(beNil()) + expect(event.flagRequestTracker).to(beNil()) } } context("without data") { @@ -280,455 +251,246 @@ final class EventSpec: QuickSpec { } } - private func dictionaryValueSpec() { - describe("dictionaryValue") { - dictionaryValueFeatureEventSpec() - dictionaryValueIdentifyEventSpec() - dictionaryValueAliasEventSpec() - dictionaryValueCustomEventSpec() - dictionaryValueDebugEventSpec() - dictionaryValueSummaryEventSpec() + private func testAliasEventEncoding() { + it("alias event encoding") { + let user = LDUser(key: "abc") + let anonUser = LDUser(key: "anon", isAnonymous: true) + let event = Event.aliasEvent(newUser: user, oldUser: anonUser) + encodesToObject(event) { dict in + expect(dict.count) == 6 + expect(dict["kind"]) == "alias" + expect(dict["key"]) == .string(user.key) + expect(dict["previousKey"]) == .string(anonUser.key) + expect(dict["contextKind"]) == "user" + expect(dict["previousContextKind"]) == "anonymousUser" + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) + } } } - private func dictionaryValueFeatureEventSpec() { - var config: LDConfig! + private func testCustomEventEncoding() { let user = LDUser.stub() - var featureFlag: FeatureFlag! - var event: Event! - var eventDictionary: [String: Any]! - context("feature event") { - beforeEach { - config = LDConfig.stub - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - } - context("without inlining user and with reason") { - beforeEach { - let featureFlagWithReason = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool, includeEvaluationReason: true, includeTrackReason: true) - event = Event.featureEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlagWithReason, user: user, includeReason: true) - config.inlineUserInEvents = false // Default value, here for clarity - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary with matching elements") { - expect(eventDictionary.count) == 9 - expect(eventDictionary.eventKind) == .feature - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) - expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. - expect(AnyComparer.isEqual(eventDictionary.reason, to: DarklyServiceMock.Constants.reason)).to(beTrue()) - expect(eventDictionary.eventUserKey) == user.key - } - } - context("inlining user and without reason") { - beforeEach { - event = Event.featureEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - config.inlineUserInEvents = true - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary with matching elements") { - expect(eventDictionary.count) == 8 - expect(eventDictionary.eventKind) == .feature - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) - expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. - expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - } - } - context("omitting the flagVersion") { - beforeEach { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool, includeFlagVersion: false, includeEvaluationReason: true) - event = Event.featureEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary with the version") { - expect(eventDictionary.count) == 8 - expect(eventDictionary.eventKind) == .feature - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) - expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.version - } - } - context("omitting flagVersion and version") { - beforeEach { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool, includeVersion: false, includeFlagVersion: false, includeEvaluationReason: true, includeTrackReason: false) - event = Event.featureEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary without the version") { - expect(eventDictionary.count) == 7 - expect(eventDictionary.eventKind) == .feature - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) - expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventVariation) == featureFlag.variation - } - } - context("without value or defaultValue") { - beforeEach { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.null) - event = Event.featureEvent(key: Constants.eventKey, value: nil, defaultValue: nil, featureFlag: featureFlag, user: user, includeReason: false) - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary with matching non-user elements") { - expect(eventDictionary.count) == 8 - expect(eventDictionary.eventKind) == .feature - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventValue, to: NSNull())).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: NSNull())).to(beTrue()) - expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. - expect(eventDictionary.eventUserKey) == user.key + context("custom event") { + it("encodes with data and metric") { + let event = Event.customEvent(key: "event-key", user: user, data: ["abc", 12], metricValue: 0.5) + encodesToObject(event) { dict in + expect(dict.count) == 6 + expect(dict["kind"]) == "custom" + expect(dict["key"]) == "event-key" + expect(dict["data"]) == ["abc", 12] + expect(dict["metricValue"]) == 0.5 + expect(dict["userKey"]) == .string(user.key) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) + } + } + it("encodes with only data and anon user") { + let anonUser = LDUser() + let event = Event.customEvent(key: "event-key", user: anonUser, data: ["key": "val"]) + encodesToObject(event) { dict in + expect(dict.count) == 6 + expect(dict["kind"]) == "custom" + expect(dict["key"]) == "event-key" + expect(dict["data"]) == ["key": "val"] + expect(dict["userKey"]) == .string(anonUser.key) + expect(dict["contextKind"]) == "anonymousUser" + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) + } + } + it("encodes inlining user") { + let event = Event.customEvent(key: "event-key", user: user, data: nil, metricValue: 2.5) + encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: true]) { dict in + expect(dict.count) == 5 + expect(dict["kind"]) == "custom" + expect(dict["key"]) == "event-key" + expect(dict["metricValue"]) == 2.5 + expect(dict["user"]) == encodeToLDValue(user) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } } - it("creates a dictionary with contextKind for anonymous user") { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.null) - event = Event.featureEvent(key: Constants.eventKey, value: nil, defaultValue: nil, featureFlag: featureFlag, user: LDUser(), includeReason: false) - expect(event.dictionaryValue(config: config).eventContextKind) == "anonymousUser" - } } } - private func dictionaryValueIdentifyEventSpec() { - var config: LDConfig! + private func testDebugEventEncoding() { let user = LDUser.stub() - var event: Event! - var eventDictionary: [String: Any]! - context("identify event") { - beforeEach { - config = LDConfig.stub - event = Event.identifyEvent(user: user) - } - it("creates a dictionary with the full user and matching non-user elements") { - for inlineUser in [true, false] { - config.inlineUserInEvents = inlineUser - eventDictionary = event.dictionaryValue(config: config) - - expect(eventDictionary.count) == 4 - expect(eventDictionary.eventKind) == .identify - expect(eventDictionary.eventKey) == user.key - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.1)) - expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - } + it("encodes without reason by default") { + let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, flagVersion: 3, reason: ["kind": "OFF"]) + let event = Event.debugEvent(key: "event-key", value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) + encodesToObject(event) { dict in + expect(dict.count) == 8 + expect(dict["kind"]) == "debug" + expect(dict["key"]) == "event-key" + expect(dict["value"]) == true + expect(dict["default"]) == false + expect(dict["variation"]) == 2 + expect(dict["version"]) == 3 + expect(dict["user"]) == encodeToLDValue(user) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } } - } - - private func dictionaryValueAliasEventSpec() { - let config = LDConfig.stub - let user1 = LDUser(key: "abc") - let user2 = LDUser(key: "def") - let anonUser1 = LDUser(key: "anon1", isAnonymous: true) - let anonUser2 = LDUser(key: "anon2", isAnonymous: true) - context("alias event") { - it("known to known") { - let eventDictionary = Event.aliasEvent(newUser: user1, oldUser: user2).dictionaryValue(config: config) - expect(eventDictionary.count) == 6 - expect(eventDictionary.eventKind) == .alias - expect(eventDictionary.eventKey) == user1.key - expect(eventDictionary.eventPreviousKey) == user2.key - expect(eventDictionary.eventContextKind) == "user" - expect(eventDictionary.eventPreviousContextKind) == "user" - expect(eventDictionary.eventCreationDate).toNot(beNil()) + it("encodes with reason when includeReason is true") { + let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, version: 2, flagVersion: 3, reason: ["kind": "OFF"]) + let event = Event.debugEvent(key: "event-key", value: 3, defaultValue: 4, featureFlag: featureFlag, user: user, includeReason: true) + encodesToObject(event) { dict in + expect(dict.count) == 9 + expect(dict["kind"]) == "debug" + expect(dict["key"]) == "event-key" + expect(dict["value"]) == 3 + expect(dict["default"]) == 4 + expect(dict["variation"]) == 2 + expect(dict["version"]) == 3 + expect(dict["reason"]) == ["kind": "OFF"] + expect(dict["user"]) == encodeToLDValue(user) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } - it("unknown to known") { - let eventDictionary = Event.aliasEvent(newUser: user1, oldUser: anonUser1).dictionaryValue(config: config) - expect(eventDictionary.count) == 6 - expect(eventDictionary.eventKind) == .alias - expect(eventDictionary.eventKey) == user1.key - expect(eventDictionary.eventPreviousKey) == anonUser1.key - expect(eventDictionary.eventContextKind) == "user" - expect(eventDictionary.eventPreviousContextKind) == "anonymousUser" - expect(eventDictionary.eventCreationDate).toNot(beNil()) + } + it("encodes with reason when trackReason is true") { + let featureFlag = FeatureFlag(flagKey: "flag-key", reason: ["kind": "OFF"], trackReason: true) + let event = Event.debugEvent(key: "event-key", value: nil, defaultValue: nil, featureFlag: featureFlag, user: user, includeReason: false) + encodesToObject(event) { dict in + expect(dict.count) == 7 + expect(dict["kind"]) == "debug" + expect(dict["key"]) == "event-key" + expect(dict["value"]) == .null + expect(dict["default"]) == .null + expect(dict["reason"]) == ["kind": "OFF"] + expect(dict["user"]) == encodeToLDValue(user) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } - it("unknown to unknown") { - let eventDictionary = Event.aliasEvent(newUser: anonUser1, oldUser: anonUser2).dictionaryValue(config: config) - expect(eventDictionary.count) == 6 - expect(eventDictionary.eventKind) == .alias - expect(eventDictionary.eventKey) == anonUser1.key - expect(eventDictionary.eventPreviousKey) == anonUser2.key - expect(eventDictionary.eventContextKind) == "anonymousUser" - expect(eventDictionary.eventPreviousContextKind) == "anonymousUser" - expect(eventDictionary.eventCreationDate).toNot(beNil()) + } + it("encodes inlined user always") { + let anonUser = LDUser() + let featureFlag = FeatureFlag(flagKey: "flag-key", version: 3) + let event = Event.debugEvent(key: "event-key", value: true, defaultValue: false, featureFlag: featureFlag, user: anonUser, includeReason: false) + encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: false]) { dict in + expect(dict.count) == 7 + expect(dict["kind"]) == "debug" + expect(dict["key"]) == "event-key" + expect(dict["value"]) == true + expect(dict["default"]) == false + expect(dict["version"]) == 3 + expect(dict["user"]) == encodeToLDValue(anonUser) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } } } - private func dictionaryValueCustomEventSpec() { - var config: LDConfig! + private func testFeatureEventEncoding() { let user = LDUser.stub() - var event: Event! - var eventDictionary: [String: Any]! - var metricValue: Double! - context("custom event") { - beforeEach { - config = LDConfig.stub + it("encodes without reason by default") { + let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, flagVersion: 3, reason: ["kind": "OFF"]) + let event = Event.featureEvent(key: "event-key", value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) + encodesToObject(event) { dict in + expect(dict.count) == 8 + expect(dict["kind"]) == "feature" + expect(dict["key"]) == "event-key" + expect(dict["value"]) == true + expect(dict["default"]) == false + expect(dict["variation"]) == 2 + expect(dict["version"]) == 3 + expect(dict["userKey"]) == .string(user.key) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } - metricValue = 0.5 - for eventData in CustomEvent.allData { - context("with valid json data") { - beforeEach { - event = Event.customEvent(key: Constants.eventKey, user: user, data: eventData, metricValue: metricValue) - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary with matching custom data") { - expect(eventDictionary.count) == 6 - expect(eventDictionary.eventKind) == .custom - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(LDValue.fromAny(eventDictionary.eventData)) == eventData - expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventMetricValue) == metricValue - } - } - } - context("without data") { - beforeEach { - event = Event.customEvent(key: Constants.eventKey, user: user, data: nil) - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary with matching custom data") { - expect(eventDictionary.count) == 4 - expect(eventDictionary.eventKind) == .custom - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(eventDictionary.eventUserKey) == user.key - } + } + it("encodes with reason when includeReason is true") { + let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, version: 2, flagVersion: 3, reason: ["kind": "OFF"]) + let event = Event.featureEvent(key: "event-key", value: 3, defaultValue: 4, featureFlag: featureFlag, user: user, includeReason: true) + encodesToObject(event) { dict in + expect(dict.count) == 9 + expect(dict["kind"]) == "feature" + expect(dict["key"]) == "event-key" + expect(dict["value"]) == 3 + expect(dict["default"]) == 4 + expect(dict["variation"]) == 2 + expect(dict["version"]) == 3 + expect(dict["reason"]) == ["kind": "OFF"] + expect(dict["userKey"]) == .string(user.key) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } - context("without inlining user") { - beforeEach { - event = Event.customEvent(key: Constants.eventKey, user: user, data: CustomEvent.dictionaryData) - config.inlineUserInEvents = false // Default value, here for clarity - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary with matching elements") { - expect(eventDictionary.count) == 5 - expect(eventDictionary.eventKind) == .custom - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(LDValue.fromAny(eventDictionary.eventData)) == CustomEvent.dictionaryData - expect(eventDictionary.eventUserKey) == user.key - } + } + it("encodes with reason when trackReason is true") { + let featureFlag = FeatureFlag(flagKey: "flag-key", reason: ["kind": "OFF"], trackReason: true) + let event = Event.featureEvent(key: "event-key", value: nil, defaultValue: nil, featureFlag: featureFlag, user: user, includeReason: false) + encodesToObject(event) { dict in + expect(dict.count) == 7 + expect(dict["kind"]) == "feature" + expect(dict["key"]) == "event-key" + expect(dict["value"]) == .null + expect(dict["default"]) == .null + expect(dict["reason"]) == ["kind": "OFF"] + expect(dict["userKey"]) == .string(user.key) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } - context("inlining user") { - beforeEach { - event = Event.customEvent(key: Constants.eventKey, user: user, data: CustomEvent.dictionaryData) - config.inlineUserInEvents = true - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary with matching elements") { - expect(eventDictionary.count) == 5 - expect(eventDictionary.eventKind) == .custom - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(LDValue.fromAny(eventDictionary.eventData)) == CustomEvent.dictionaryData - expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - } + } + it("encodes inlined user when configured") { + let featureFlag = FeatureFlag(flagKey: "flag-key", version: 3) + let event = Event.featureEvent(key: "event-key", value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) + encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: true]) { dict in + expect(dict.count) == 7 + expect(dict["kind"]) == "feature" + expect(dict["key"]) == "event-key" + expect(dict["value"]) == true + expect(dict["default"]) == false + expect(dict["version"]) == 3 + expect(dict["user"]) == encodeToLDValue(user) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } - context("with anonymous user") { - it("sets contextKind field") { - event = Event.customEvent(key: Constants.eventKey, user: LDUser(), data: nil) - eventDictionary = event.dictionaryValue(config: config) - expect(eventDictionary.eventContextKind) == "anonymousUser" - } + } + it("encodes with contextKind for anon user") { + let anonUser = LDUser() + let event = Event.featureEvent(key: "event-key", value: true, defaultValue: false, featureFlag: nil, user: anonUser, includeReason: false) + encodesToObject(event) { dict in + expect(dict.count) == 7 + expect(dict["kind"]) == "feature" + expect(dict["key"]) == "event-key" + expect(dict["value"]) == true + expect(dict["default"]) == false + expect(dict["userKey"]) == .string(anonUser.key) + expect(dict["contextKind"]) == "anonymousUser" + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } } } - private func dictionaryValueDebugEventSpec() { - var config: LDConfig! + private func testIdentifyEventEncoding() { let user = LDUser.stub() - var featureFlag: FeatureFlag! - var event: Event! - var eventDictionary: [String: Any]! - context("debug event") { - beforeEach { - config = LDConfig.stub - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - } - context("regardless of inlining user") { - beforeEach { - event = Event.debugEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - } - [true, false].forEach { inlineUser in - it("creates a dictionary with matching elements") { - config.inlineUserInEvents = inlineUser - eventDictionary = event.dictionaryValue(config: config) - - expect(eventDictionary.count) == 8 - expect(eventDictionary.eventKind) == .debug - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) - expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. - expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - } - } - } - context("omitting the flagVersion") { - beforeEach { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool, includeFlagVersion: false) - event = Event.debugEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary with the version") { - expect(eventDictionary.count) == 8 - expect(eventDictionary.eventKind) == .debug - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.version - } - } - context("omitting flagVersion and version") { - beforeEach { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool, includeVersion: false, includeFlagVersion: false) - event = Event.debugEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary without the version") { - expect(eventDictionary.count) == 7 - expect(eventDictionary.eventKind) == .debug - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventValue, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: false)).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - expect(eventDictionary.eventVariation) == featureFlag.variation - } - } - context("without value or defaultValue") { - beforeEach { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.null) - event = Event.debugEvent(key: Constants.eventKey, value: nil, defaultValue: nil, featureFlag: featureFlag, user: user, includeReason: false) - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a dictionary with matching non-user elements") { - expect(eventDictionary.count) == 8 - expect(eventDictionary.eventKind) == .debug - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate).to(beCloseTo(event.creationDate!, within: 0.001)) - expect(AnyComparer.isEqual(eventDictionary.eventValue, to: NSNull())).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: NSNull())).to(beTrue()) - expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) - expect(eventDictionary.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.flagVersion // Since feature flags include the flag version, it should be used. + it("identify event encoding") { + for inlineUser in [true, false] { + let event = Event.identifyEvent(user: user) + encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: inlineUser]) { dict in + expect(dict.count) == 4 + expect(dict["kind"]) == "identify" + expect(dict["key"]) == .string(user.key) + expect(dict["user"]) == encodeToLDValue(user) + expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) } } } } - private func dictionaryValueSummaryEventSpec() { - var config: LDConfig! - var event: Event! - var eventDictionary: [String: Any]! - context("summary event") { - beforeEach { - config = LDConfig.stub - event = Event.summaryEvent(flagRequestTracker: FlagRequestTracker.stub(), endDate: Date()) - - eventDictionary = event.dictionaryValue(config: config) - } - it("creates a summary dictionary with matching elements") { - expect(eventDictionary.count) == 4 - expect(eventDictionary.eventKind) == .summary - expect(eventDictionary.eventStartDate).to(beCloseTo(event.flagRequestTracker!.startDate, within: 0.001)) - expect(eventDictionary.eventEndDate).to(beCloseTo(event.endDate!, within: 0.001)) - guard let features = eventDictionary.eventFeatures - else { - fail("expected eventDictionary features to not be nil, got nil") - return - } - expect(features.count) == event.flagRequestTracker?.flagCounters.count - event.flagRequestTracker?.flagCounters.forEach { flagKey, flagCounter in - guard let flagCounterDictionary = features[flagKey] as? [String: Any] - else { - fail("expected features to contain flag counter for \(flagKey), got nil") - return - } - expect(AnyComparer.isEqual(flagCounterDictionary, to: flagCounter.dictionaryValue, considerNilAndNullEqual: true)).to(beTrue()) + private func testSummaryEventEncoding() { + it("summary event encoding") { + let flag = FeatureFlag(flagKey: "bool-flag", variation: 1, version: 5, flagVersion: 2) + var flagRequestTracker = FlagRequestTracker() + flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) + flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) + let event = Event.summaryEvent(flagRequestTracker: flagRequestTracker, endDate: Date()) + encodesToObject(event) { dict in + expect(dict.count) == 4 + expect(dict["kind"]) == "summary" + expect(dict["startDate"]) == LDValue.fromAny(flagRequestTracker.startDate.millisSince1970) + expect(dict["endDate"]) == LDValue.fromAny(event?.endDate?.millisSince1970) + valueIsObject(dict["features"]) { features in + expect(features.count) == 1 + let counter = FlagCounter() + counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) + counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) + expect(features["bool-flag"]) == encodeToLDValue(counter) } } } } } -extension Dictionary where Key == String, Value == Any { - var eventCreationDate: Date? { - Date(millisSince1970: self[Event.CodingKeys.creationDate.rawValue] as? Int64) - } - var eventUserKey: String? { - self[Event.CodingKeys.userKey.rawValue] as? String - } - fileprivate var eventUserDictionary: [String: Any]? { - self[Event.CodingKeys.user.rawValue] as? [String: Any] - } - var eventValue: Any? { - self[Event.CodingKeys.value.rawValue] - } - var eventDefaultValue: Any? { - self[Event.CodingKeys.defaultValue.rawValue] - } - var eventVariation: Int? { - self[Event.CodingKeys.variation.rawValue] as? Int - } - var eventVersion: Int? { - self[Event.CodingKeys.version.rawValue] as? Int - } - var eventData: Any? { - self[Event.CodingKeys.data.rawValue] - } - var eventStartDate: Date? { - Date(millisSince1970: self[FlagRequestTracker.CodingKeys.startDate.rawValue] as? Int64) - } - var eventEndDate: Date? { - Date(millisSince1970: self[Event.CodingKeys.endDate.rawValue] as? Int64) - } - var eventFeatures: [String: Any]? { - self[FlagRequestTracker.CodingKeys.features.rawValue] as? [String: Any] - } - var eventMetricValue: Double? { - self[Event.CodingKeys.metricValue.rawValue] as? Double - } - private var eventKindString: String? { - self[Event.CodingKeys.kind.rawValue] as? String - } - var eventKind: Event.Kind? { - guard let eventKindString = eventKindString - else { return nil } - return Event.Kind(rawValue: eventKindString) - } - var eventKey: String? { - self[Event.CodingKeys.key.rawValue] as? String - } - var eventPreviousKey: String? { - self[Event.CodingKeys.previousKey.rawValue] as? String - } - var eventContextKind: String? { - self[Event.CodingKeys.contextKind.rawValue] as? String - } - var eventPreviousContextKind: String? { - self[Event.CodingKeys.previousContextKind.rawValue] as? String - } -} - extension Event { static func stub(_ eventKind: Kind, with user: LDUser) -> Event { switch eventKind { @@ -745,34 +507,7 @@ extension Event { } } - static func stubEvents(eventCount: Int = Event.Kind.allKinds.count, for user: LDUser) -> [Event] { - var eventStubs = [Event]() - while eventStubs.count < eventCount { - eventStubs.append(Event.stub(eventKind(for: eventStubs.count), with: user)) - } - return eventStubs - } - static func eventKind(for count: Int) -> Kind { Event.Kind.allKinds[count % Event.Kind.allKinds.count] } - - static func stubEventDictionaries(_ eventCount: Int, user: LDUser, config: LDConfig) -> [[String: Any]] { - let eventStubs = stubEvents(eventCount: eventCount, for: user) - return eventStubs.map { event in - event.dictionaryValue(config: config) - } - } -} - -extension CounterValue: Equatable { - public static func == (lhs: CounterValue, rhs: CounterValue) -> Bool { - lhs.value == rhs.value && lhs.count == rhs.count - } -} - -extension FlagCounter: Equatable { - public static func == (lhs: FlagCounter, rhs: FlagCounter) -> Bool { - lhs.defaultValue == rhs.defaultValue && lhs.flagValueCounters == rhs.flagValueCounters - } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift index 83d2c8b0..f37372c7 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift @@ -4,6 +4,9 @@ import XCTest @testable import LaunchDarkly final class FlagCounterSpec: XCTestCase { + private let testDefaultValue: LDValue = "d" + private let testValue: LDValue = 5.5 + func testInit() { let flagCounter = FlagCounter() XCTAssertEqual(flagCounter.defaultValue, .null) @@ -11,197 +14,164 @@ final class FlagCounterSpec: XCTestCase { } func testTrackRequestInitialKnown() { - let reportedValue: LDValue = "a" - let defaultValue: LDValue = "b" - let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, flagVersion: 3) + let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 2, flagVersion: 3) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) - let result = flagCounter.dictionaryValue - XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssertEqual(counter.valueCounterReportedValue, reportedValue) - XCTAssertEqual(counter.valueCounterVersion, 3) - XCTAssertEqual(counter.valueCounterVariation, 2) - XCTAssertNil(counter.valueCounterIsUnknown) - XCTAssertEqual(counter.valueCounterCount, 1) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) + XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) + XCTAssertEqual(flagCounter.flagValueCounters.count, 1) + let counter = flagCounter.flagValueCounters.first! + XCTAssertEqual(counter.key.version, 3) + XCTAssertEqual(counter.key.variation, 2) + XCTAssertEqual(counter.value.value, testValue) + XCTAssertEqual(counter.value.count, 1) } func testTrackRequestKnownMatching() { - let reportedValue: LDValue = "a" - let defaultValue: LDValue = "b" let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 5, flagVersion: 3) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 7, flagVersion: 3) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: secondFeatureFlag, defaultValue: defaultValue) - let result = flagCounter.dictionaryValue - XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssertEqual(counter.valueCounterReportedValue, reportedValue) - XCTAssertEqual(counter.valueCounterVersion, 3) - XCTAssertEqual(counter.valueCounterVariation, 2) - XCTAssertNil(counter.valueCounterIsUnknown) - XCTAssertEqual(counter.valueCounterCount, 2) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: "e") + flagCounter.trackRequest(reportedValue: "b", featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) + XCTAssertEqual(flagCounter.flagValueCounters.count, 1) + let counter = flagCounter.flagValueCounters.first! + XCTAssertEqual(counter.key.version, 3) + XCTAssertEqual(counter.key.variation, 2) + XCTAssertEqual(counter.value.value, testValue) + XCTAssertEqual(counter.value.count, 2) } func testTrackRequestKnownDifferentVariations() { - let reportedValue: LDValue = "a" - let defaultValue: LDValue = "b" let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10, flagVersion: 5) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 3, version: 10, flagVersion: 5) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: secondFeatureFlag, defaultValue: defaultValue) - let result = flagCounter.dictionaryValue - XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 2) - let counter1 = counters!.first { $0.valueCounterVariation == 2 }! - let counter2 = counters!.first { $0.valueCounterVariation == 3 }! - XCTAssertEqual(counter1.valueCounterReportedValue, reportedValue) - XCTAssertEqual(counter1.valueCounterVersion, 5) - XCTAssertEqual(counter1.valueCounterVariation, 2) - XCTAssertNil(counter1.valueCounterIsUnknown) - XCTAssertEqual(counter1.valueCounterCount, 1) - - XCTAssertEqual(counter2.valueCounterReportedValue, reportedValue) - XCTAssertEqual(counter2.valueCounterVersion, 5) - XCTAssertEqual(counter2.valueCounterVariation, 3) - XCTAssertNil(counter2.valueCounterIsUnknown) - XCTAssertEqual(counter2.valueCounterCount, 1) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) + XCTAssertEqual(flagCounter.flagValueCounters.count, 2) + let counter1 = flagCounter.flagValueCounters.first { key, _ in key.variation == 2 }! + XCTAssertEqual(counter1.key.version, 5) + XCTAssertEqual(counter1.value.value, testValue) + XCTAssertEqual(counter1.value.count, 1) + let counter2 = flagCounter.flagValueCounters.first { key, _ in key.variation == 3 }! + XCTAssertEqual(counter2.key.version, 5) + XCTAssertEqual(counter2.value.value, testValue) + XCTAssertEqual(counter2.value.count, 1) } func testTrackRequestKnownDifferentFlagVersions() { - let reportedValue: LDValue = "a" - let defaultValue: LDValue = "b" let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10, flagVersion: 3) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10, flagVersion: 5) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: secondFeatureFlag, defaultValue: defaultValue) - let result = flagCounter.dictionaryValue - XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 2) - let counter1 = counters!.first { $0.valueCounterVersion == 3 }! - let counter2 = counters!.first { $0.valueCounterVersion == 5 }! - XCTAssertEqual(counter1.valueCounterReportedValue, reportedValue) - XCTAssertEqual(counter1.valueCounterVersion, 3) - XCTAssertEqual(counter1.valueCounterVariation, 2) - XCTAssertNil(counter1.valueCounterIsUnknown) - XCTAssertEqual(counter1.valueCounterCount, 1) - - XCTAssertEqual(counter2.valueCounterReportedValue, reportedValue) - XCTAssertEqual(counter2.valueCounterVersion, 5) - XCTAssertEqual(counter2.valueCounterVariation, 2) - XCTAssertNil(counter2.valueCounterIsUnknown) - XCTAssertEqual(counter2.valueCounterCount, 1) - } - - func testTrackRequestKnownMissingFlagVersionsMatchingVersions() { - let reportedValue: LDValue = "a" - let defaultValue: LDValue = "b" + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) + XCTAssertEqual(flagCounter.flagValueCounters.count, 2) + let counter1 = flagCounter.flagValueCounters.first { key, _ in key.version == 3 }! + XCTAssertEqual(counter1.key.variation, 2) + XCTAssertEqual(counter1.value.value, testValue) + XCTAssertEqual(counter1.value.count, 1) + let counter2 = flagCounter.flagValueCounters.first { key, _ in key.version == 5 }! + XCTAssertEqual(counter2.key.variation, 2) + XCTAssertEqual(counter2.value.value, testValue) + XCTAssertEqual(counter2.value.count, 1) + } + + func testTrackRequestKnownMissingFlagVersionMatchingVersions() { let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10) - let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10) - let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: secondFeatureFlag, defaultValue: defaultValue) - let result = flagCounter.dictionaryValue - XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssertEqual(counter.valueCounterReportedValue, reportedValue) - XCTAssertEqual(counter.valueCounterVersion, 10) - XCTAssertEqual(counter.valueCounterVariation, 2) - XCTAssertNil(counter.valueCounterIsUnknown) - XCTAssertEqual(counter.valueCounterCount, 2) - } - - func testTrackRequestKnownMissingFlagVersionsDifferentVersions() { - let reportedValue: LDValue = "a" - let defaultValue: LDValue = "b" - let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 5) - let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10) + let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 5, flagVersion: 10) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: secondFeatureFlag, defaultValue: defaultValue) - let result = flagCounter.dictionaryValue - XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 2) - let counter1 = counters!.first { $0.valueCounterVersion == 5 }! - let counter2 = counters!.first { $0.valueCounterVersion == 10 }! - XCTAssertEqual(counter1.valueCounterReportedValue, reportedValue) - XCTAssertEqual(counter1.valueCounterVersion, 5) - XCTAssertEqual(counter1.valueCounterVariation, 2) - XCTAssertNil(counter1.valueCounterIsUnknown) - XCTAssertEqual(counter1.valueCounterCount, 1) - - XCTAssertEqual(counter2.valueCounterReportedValue, reportedValue) - XCTAssertEqual(counter2.valueCounterVersion, 10) - XCTAssertEqual(counter2.valueCounterVariation, 2) - XCTAssertNil(counter2.valueCounterIsUnknown) - XCTAssertEqual(counter2.valueCounterCount, 1) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) + XCTAssertEqual(flagCounter.flagValueCounters.count, 1) + let counter = flagCounter.flagValueCounters.first! + XCTAssertEqual(counter.key.version, 10) + XCTAssertEqual(counter.key.variation, 2) + XCTAssertEqual(counter.value.value, testValue) + XCTAssertEqual(counter.value.count, 2) } func testTrackRequestInitialUnknown() { - let reportedValue: LDValue = "a" - let defaultValue: LDValue = "b" let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: nil, defaultValue: defaultValue) - let result = flagCounter.dictionaryValue - XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssertEqual(counter.valueCounterReportedValue, reportedValue) - XCTAssertNil(counter.valueCounterVersion) - XCTAssertNil(counter.valueCounterVariation) - XCTAssertEqual(counter.valueCounterIsUnknown, true) - XCTAssertEqual(counter.valueCounterCount, 1) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue) + XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) + XCTAssertEqual(flagCounter.flagValueCounters.count, 1) + let counter = flagCounter.flagValueCounters.first! + XCTAssertNil(counter.key.version) + XCTAssertNil(counter.key.variation) + XCTAssertEqual(counter.value.value, testValue) + XCTAssertEqual(counter.value.count, 1) } func testTrackRequestSecondUnknown() { - let reportedValue: LDValue = "a" - let defaultValue: LDValue = "b" let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: nil, defaultValue: defaultValue) - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: nil, defaultValue: defaultValue) - let result = flagCounter.dictionaryValue - XCTAssertEqual(result.flagCounterDefaultValue, defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssertEqual(counter.valueCounterReportedValue, reportedValue) - XCTAssertNil(counter.valueCounterVersion) - XCTAssertNil(counter.valueCounterVariation) - XCTAssertEqual(counter.valueCounterIsUnknown, true) - XCTAssertEqual(counter.valueCounterCount, 2) - } - - func testTrackRequestSecondUnknownWithDifferentValues() { - let initialReportedValue: LDValue = "a" - let initialDefaultValue: LDValue = "b" - let secondReportedValue: LDValue = "c" - let secondDefaultValue: LDValue = "d" + flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue) + XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) + XCTAssertEqual(flagCounter.flagValueCounters.count, 1) + let counter = flagCounter.flagValueCounters.first! + XCTAssertNil(counter.key.version) + XCTAssertNil(counter.key.variation) + XCTAssertEqual(counter.value.value, testValue) + XCTAssertEqual(counter.value.count, 2) + } + + func testTrackRequestSecondUnknownWithDifferentVariations() { + let unknownFlag1 = FeatureFlag(flagKey: "unused", variation: 1) + let unknownFlag2 = FeatureFlag(flagKey: "unused", variation: 2) + let flagCounter = FlagCounter() + flagCounter.trackRequest(reportedValue: testValue, featureFlag: unknownFlag1, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: unknownFlag2, defaultValue: testDefaultValue) + XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) + XCTAssertEqual(flagCounter.flagValueCounters.count, 2) + let counter1 = flagCounter.flagValueCounters.first { key, _ in key.variation == 1 }! + XCTAssertNil(counter1.key.version) + XCTAssertEqual(counter1.key.variation, 1) + XCTAssertEqual(counter1.value.value, testValue) + XCTAssertEqual(counter1.value.count, 1) + let counter2 = flagCounter.flagValueCounters.first { key, _ in key.variation == 2 }! + XCTAssertNil(counter2.key.version) + XCTAssertEqual(counter2.key.variation, 2) + XCTAssertEqual(counter2.value.value, testValue) + XCTAssertEqual(counter2.value.count, 1) + } + + func testEncoding() { + let featureFlag = FeatureFlag(flagKey: "unused", variation: 3, version: 2, flagVersion: 5) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: initialReportedValue, featureFlag: nil, defaultValue: initialDefaultValue) - flagCounter.trackRequest(reportedValue: secondReportedValue, featureFlag: nil, defaultValue: secondDefaultValue) - let result = flagCounter.dictionaryValue - XCTAssertEqual(result.flagCounterDefaultValue, secondDefaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssertEqual(counter.valueCounterReportedValue, initialReportedValue) - XCTAssertNil(counter.valueCounterVersion) - XCTAssertNil(counter.valueCounterVariation) - XCTAssertEqual(counter.valueCounterIsUnknown, true) - XCTAssertEqual(counter.valueCounterCount, 2) + flagCounter.trackRequest(reportedValue: "a", featureFlag: featureFlag, defaultValue: "b") + flagCounter.trackRequest(reportedValue: "a", featureFlag: featureFlag, defaultValue: "b") + encodesToObject(flagCounter) { dict in + XCTAssertEqual(dict.count, 2) + XCTAssertEqual(dict["default"], "b") + valueIsArray(dict["counters"]) { counters in + XCTAssertEqual(counters.count, 1) + valueIsObject(counters[0]) { counter in + XCTAssertEqual(counter.count, 4) + XCTAssertEqual(counter["value"], "a") + XCTAssertEqual(counter["count"], 2) + XCTAssertEqual(counter["version"], 5) + XCTAssertEqual(counter["variation"], 3) + } + } + } + + let flagCounterNulls = FlagCounter() + flagCounterNulls.trackRequest(reportedValue: nil, featureFlag: nil, defaultValue: nil) + encodesToObject(flagCounterNulls) { dict in + XCTAssertEqual(dict.count, 2) + XCTAssertEqual(dict["default"], .null) + valueIsArray(dict["counters"]) { counters in + XCTAssertEqual(counters.count, 1) + valueIsObject(counters[0]) { counter in + XCTAssertEqual(counter.count, 3) + XCTAssertEqual(counter["value"], .null) + XCTAssertEqual(counter["count"], 1) + XCTAssertEqual(counter["unknown"], true) + } + } + } } } @@ -227,27 +197,3 @@ extension FlagCounter { return flagCounter } } - -extension Dictionary where Key == String, Value == Any { - fileprivate var valueCounterReportedValue: LDValue { - LDValue.fromAny(self[FlagCounter.CodingKeys.value.rawValue]) - } - fileprivate var valueCounterVariation: Int? { - self[FlagCounter.CodingKeys.variation.rawValue] as? Int - } - fileprivate var valueCounterVersion: Int? { - self[FlagCounter.CodingKeys.version.rawValue] as? Int - } - fileprivate var valueCounterIsUnknown: Bool? { - self[FlagCounter.CodingKeys.unknown.rawValue] as? Bool - } - fileprivate var valueCounterCount: Int? { - self[FlagCounter.CodingKeys.count.rawValue] as? Int - } - fileprivate var flagCounterDefaultValue: LDValue { - LDValue.fromAny(self[FlagCounter.CodingKeys.defaultValue.rawValue]) - } - fileprivate var flagCounterFlagValueCounters: [[String: Any]]? { - self[FlagCounter.CodingKeys.counters.rawValue] as? [[String: Any]] - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift index 5365a7bc..55d7a69e 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagRequestTrackerSpec.swift @@ -6,6 +6,7 @@ import XCTest final class FlagRequestTrackerSpec: XCTestCase { func testInit() { let flagRequestTracker = FlagRequestTracker() + XCTAssertEqual(flagRequestTracker.flagCounters, [:]) XCTAssertFalse(flagRequestTracker.hasLoggedRequests) let now = Date() XCTAssert(flagRequestTracker.startDate <= now) @@ -15,53 +16,38 @@ final class FlagRequestTrackerSpec: XCTestCase { func testTrackRequestInitial() { let flag = FeatureFlag(flagKey: "bool-flag", variation: 1, version: 5, flagVersion: 2) var flagRequestTracker = FlagRequestTracker() - let now = Date().millisSince1970 flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) - let dictionaryValue = flagRequestTracker.dictionaryValue - XCTAssert(dictionaryValue.flagRequestTrackerStartDateMillis! <= now) - XCTAssert(dictionaryValue.flagRequestTrackerStartDateMillis! >= now - 1000) - let features = dictionaryValue.flagRequestTrackerFeatures! - XCTAssertEqual(features.count, 1) + XCTAssertEqual(flagRequestTracker.flagCounters.count, 1) let counter = FlagCounter() counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) - XCTAssert(AnyComparer.isEqual(features["bool-flag"], to: counter.dictionaryValue)) + XCTAssertEqual(flagRequestTracker.flagCounters["bool-flag"], counter) } func testTrackRequestSameFlagKey() { let flag = FeatureFlag(flagKey: "bool-flag", variation: 1, version: 5, flagVersion: 2) var flagRequestTracker = FlagRequestTracker() - let now = Date().millisSince1970 flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) - let dictionaryValue = flagRequestTracker.dictionaryValue - XCTAssert(dictionaryValue.flagRequestTrackerStartDateMillis! <= now) - XCTAssert(dictionaryValue.flagRequestTrackerStartDateMillis! >= now - 1000) - let features = dictionaryValue.flagRequestTrackerFeatures! - XCTAssertEqual(features.count, 1) + XCTAssertEqual(flagRequestTracker.flagCounters.count, 1) let counter = FlagCounter() counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) - XCTAssert(AnyComparer.isEqual(features["bool-flag"], to: counter.dictionaryValue)) + XCTAssertEqual(flagRequestTracker.flagCounters["bool-flag"], counter) } func testTrackRequestDifferentFlagKey() { let flag = FeatureFlag(flagKey: "bool-flag", variation: 1, version: 5, flagVersion: 2) let secondFlag = FeatureFlag(flagKey: "alt-flag", variation: 2, version: 6, flagVersion: 3) var flagRequestTracker = FlagRequestTracker() - let now = Date().millisSince1970 flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) flagRequestTracker.trackRequest(flagKey: "alt-flag", reportedValue: true, featureFlag: secondFlag, defaultValue: false) - let dictionaryValue = flagRequestTracker.dictionaryValue - XCTAssert(dictionaryValue.flagRequestTrackerStartDateMillis! <= now) - XCTAssert(dictionaryValue.flagRequestTrackerStartDateMillis! >= now - 1000) - let features = dictionaryValue.flagRequestTrackerFeatures! - XCTAssertEqual(features.count, 2) - let counter = FlagCounter() - counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) - let secondCounter = FlagCounter() - secondCounter.trackRequest(reportedValue: true, featureFlag: secondFlag, defaultValue: false) - XCTAssert(AnyComparer.isEqual(features["bool-flag"], to: counter.dictionaryValue)) - XCTAssert(AnyComparer.isEqual(features["alt-flag"], to: secondCounter.dictionaryValue)) + XCTAssertEqual(flagRequestTracker.flagCounters.count, 2) + let counter1 = FlagCounter() + counter1.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) + let counter2 = FlagCounter() + counter2.trackRequest(reportedValue: true, featureFlag: secondFlag, defaultValue: false) + XCTAssertEqual(flagRequestTracker.flagCounters["bool-flag"], counter1) + XCTAssertEqual(flagRequestTracker.flagCounters["alt-flag"], counter2) } func testHasLoggedRequests() { @@ -82,17 +68,20 @@ extension FlagRequestTracker { } } -extension Dictionary where Key == String, Value == Any { - var flagRequestTrackerStartDateMillis: Int64? { - self[FlagRequestTracker.CodingKeys.startDate.rawValue] as? Int64 +extension LDFlagKey { + var isKnown: Bool { + DarklyServiceMock.FlagKeys.knownFlags.contains(self) } - var flagRequestTrackerFeatures: [LDFlagKey: Any]? { - self[FlagRequestTracker.CodingKeys.features.rawValue] as? [LDFlagKey: Any] +} + +extension CounterValue: Equatable { + public static func == (lhs: CounterValue, rhs: CounterValue) -> Bool { + lhs.value == rhs.value && lhs.count == rhs.count } } -extension LDFlagKey { - var isKnown: Bool { - DarklyServiceMock.FlagKeys.knownFlags.contains(self) +extension FlagCounter: Equatable { + public static func == (lhs: FlagCounter, rhs: FlagCounter) -> Bool { + lhs.defaultValue == rhs.defaultValue && lhs.flagValueCounters == rhs.flagValueCounters } } diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index 6a2262ac..ac5a5b4d 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -26,13 +26,8 @@ final class DarklyServiceSpec: QuickSpec { init(mobileKey: String = LDConfig.Constants.mockMobileKey, useReport: Bool = Constants.useGetMethod, - includeMockEventDictionaries: Bool = false, - operatingSystemName: String? = nil, diagnosticOptOut: Bool = false) { - if let operatingSystemName = operatingSystemName { - serviceFactoryMock.makeEnvironmentReporterReturnValue.systemName = operatingSystemName - } config = LDConfig.stub(mobileKey: mobileKey, environmentReporter: EnvironmentReportingMock()) config.useReport = useReport config.diagnosticOptOut = diagnosticOptOut @@ -41,10 +36,6 @@ final class DarklyServiceSpec: QuickSpec { httpHeaders = HTTPHeaders(config: config, environmentReporter: config.environmentReporter) } - func mockEventDictionaries() -> [[String: Any]] { - Event.stubEventDictionaries(Constants.eventCount, user: user, config: config) - } - func runStubbedGet(statusCode: Int, featureFlags: [LDFlagKey: FeatureFlag]? = nil, flagResponseEtag: String? = nil) { serviceMock.stubFlagRequest(statusCode: statusCode, useReport: config.useReport, flagResponseEtag: flagResponseEtag) waitUntil { done in @@ -60,7 +51,7 @@ final class DarklyServiceSpec: QuickSpec { flagRequestEtagSpec() clearFlagRequestCacheSpec() createEventSourceSpec() - publishEventDictionariesSpec() + publishEventDataSpec() diagnosticCacheSpec() publishDiagnosticSpec() @@ -575,22 +566,23 @@ final class DarklyServiceSpec: QuickSpec { } } - private func publishEventDictionariesSpec() { + private func publishEventDataSpec() { + let testData = Data("abc".utf8) var testContext: TestContext! - describe("publishEventDictionaries") { + describe("publishEventData") { var eventRequest: URLRequest? beforeEach { eventRequest = nil - testContext = TestContext(mobileKey: LDConfig.Constants.mockMobileKey, useReport: Constants.useGetMethod, includeMockEventDictionaries: true) + testContext = TestContext(mobileKey: LDConfig.Constants.mockMobileKey, useReport: Constants.useGetMethod) } context("success") { var responses: ServiceResponses! beforeEach { waitUntil { done in testContext.serviceMock.stubEventRequest(success: true) { eventRequest = $0 } - testContext.service.publishEventDictionaries(testContext.mockEventDictionaries(), UUID().uuidString) { data, response, error in + testContext.service.publishEventData(testData, UUID().uuidString) { data, response, error in responses = (data, response, error) done() } @@ -612,7 +604,7 @@ final class DarklyServiceSpec: QuickSpec { beforeEach { waitUntil { done in testContext.serviceMock.stubEventRequest(success: false) { eventRequest = $0 } - testContext.service.publishEventDictionaries(testContext.mockEventDictionaries(), UUID().uuidString) { data, response, error in + testContext.service.publishEventData(testData, UUID().uuidString) { data, response, error in responses = (data, response, error) done() } @@ -633,27 +625,9 @@ final class DarklyServiceSpec: QuickSpec { var responses: ServiceResponses! var eventsPublished = false beforeEach { - testContext = TestContext(mobileKey: "", useReport: Constants.useGetMethod, includeMockEventDictionaries: true) - testContext.serviceMock.stubEventRequest(success: true) { eventRequest = $0 } - testContext.service.publishEventDictionaries(testContext.mockEventDictionaries(), UUID().uuidString) { data, response, error in - responses = (data, response, error) - eventsPublished = true - } - } - it("does not make a request") { - expect(eventRequest).to(beNil()) - expect(eventsPublished) == false - expect(responses).to(beNil()) - } - } - context("empty event list") { - var responses: ServiceResponses! - var eventsPublished = false - let emptyEventDictionaryList: [[String: Any]] = [] - beforeEach { - testContext = TestContext(mobileKey: LDConfig.Constants.mockMobileKey, useReport: Constants.useGetMethod, includeMockEventDictionaries: true) + testContext = TestContext(mobileKey: "", useReport: Constants.useGetMethod) testContext.serviceMock.stubEventRequest(success: true) { eventRequest = $0 } - testContext.service.publishEventDictionaries(emptyEventDictionaryList, "") { data, response, error in + testContext.service.publishEventData(testData, UUID().uuidString) { data, response, error in responses = (data, response, error) eventsPublished = true } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index 0cf4d5fb..330570f4 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -105,7 +105,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.service) === testContext.serviceMock expect(testContext.eventReporter.isOnline) == false expect(testContext.eventReporter.isReportingActive) == false - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 } } } @@ -128,7 +128,7 @@ final class EventReporterSpec: QuickSpec { it("goes offline and stops reporting") { expect(testContext.eventReporter.isOnline) == false expect(testContext.eventReporter.isReportingActive) == false - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 } } context("offline to online") { @@ -141,7 +141,7 @@ final class EventReporterSpec: QuickSpec { it("goes online and starts reporting") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 } } context("without events") { @@ -153,7 +153,7 @@ final class EventReporterSpec: QuickSpec { it("goes online and starts reporting") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 } } } @@ -167,7 +167,7 @@ final class EventReporterSpec: QuickSpec { it("stays online and continues reporting") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 } } context("offline to offline") { @@ -179,7 +179,7 @@ final class EventReporterSpec: QuickSpec { it("stays offline and does not start reporting") { expect(testContext.eventReporter.isOnline) == false expect(testContext.eventReporter.isReportingActive) == false - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 expect(testContext.eventReporter.eventStoreKeys) == testContext.eventKeys } } @@ -197,7 +197,7 @@ final class EventReporterSpec: QuickSpec { it("records events up to event capacity") { expect(testContext.eventReporter.isOnline) == false expect(testContext.eventReporter.isReportingActive) == false - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 expect(testContext.eventReporter.eventStoreKeys) == testContext.eventKeys } it("does not record a dropped event to diagnosticCache") { @@ -215,7 +215,7 @@ final class EventReporterSpec: QuickSpec { it("doesn't record any more events") { expect(testContext.eventReporter.isOnline) == false expect(testContext.eventReporter.isReportingActive) == false - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 expect(testContext.eventReporter.eventStoreKeys) == testContext.eventKeys expect(testContext.eventReporter.eventStoreKeys.contains(extraEvent.key!)) == false } @@ -240,7 +240,6 @@ final class EventReporterSpec: QuickSpec { context("success") { context("with events and tracked requests") { beforeEach { - // The EventReporter will try to report events if it's started online with events. By starting online without events, then adding them, we "beat the timer" by reporting them right away waitUntil { syncComplete in testContext = TestContext(eventStubResponseDate: eventStubResponseDate, onSyncComplete: { result in testContext.syncResult = result @@ -255,10 +254,15 @@ final class EventReporterSpec: QuickSpec { it("reports events and a summary event") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 1 - expect(testContext.serviceMock.publishedEventDictionaries?.count) == Event.Kind.nonSummaryKinds.count + 1 - expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys // summary events have no key, this verifies non-summary events - expect(testContext.serviceMock.publishedEventDictionaryKinds?.contains(.summary)) == true + expect(testContext.serviceMock.publishEventDataCallCount) == 1 + let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) + valueIsArray(published) { valueArray in + expect(valueArray.count) == testContext.events.count + 1 + expect(Array(valueArray.prefix(testContext.events.count))) == testContext.events.map { encodeToLDValue($0) } + valueIsObject(valueArray[testContext.events.count]) { summaryObject in + expect(summaryObject["kind"]) == "summary" + } + } expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 1 expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == Event.Kind.nonSummaryKinds.count + 1 expect(testContext.eventReporter.eventStore.isEmpty) == true @@ -269,7 +273,6 @@ final class EventReporterSpec: QuickSpec { } context("with events only") { beforeEach { - // The EventReporter will try to report events if it's started online with events. By starting online without events, then adding them, we "beat the timer" by reporting them right away waitUntil { syncComplete in testContext = TestContext(eventStubResponseDate: eventStubResponseDate, onSyncComplete: { result in testContext.syncResult = result @@ -283,10 +286,9 @@ final class EventReporterSpec: QuickSpec { it("reports events without a summary event") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 1 - expect(testContext.serviceMock.publishedEventDictionaries?.count) == Event.Kind.nonSummaryKinds.count - expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys // summary events have no key, this verifies non-summary events - expect(testContext.serviceMock.publishedEventDictionaryKinds?.contains(.summary)) == false + expect(testContext.serviceMock.publishEventDataCallCount) == 1 + let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) + expect(published) == encodeToLDValue(testContext.events) expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 1 expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == Event.Kind.nonSummaryKinds.count expect(testContext.eventReporter.eventStore.isEmpty) == true @@ -297,7 +299,6 @@ final class EventReporterSpec: QuickSpec { } context("with tracked requests only") { beforeEach { - // The EventReporter will try to report events if it's started online with events. By starting online without events, then adding them, we "beat the timer" by reporting them right away waitUntil { syncComplete in testContext = TestContext(eventStubResponseDate: eventStubResponseDate, onSyncComplete: { result in testContext.syncResult = result @@ -311,9 +312,14 @@ final class EventReporterSpec: QuickSpec { it("reports only a summary event") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 1 - expect(testContext.serviceMock.publishedEventDictionaries?.count) == 1 - expect(testContext.serviceMock.publishedEventDictionaryKinds?.contains(.summary)) == true + expect(testContext.serviceMock.publishEventDataCallCount) == 1 + let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) + valueIsArray(published) { valueArray in + expect(valueArray.count) == 1 + valueIsObject(valueArray[0]) { summaryObject in + expect(summaryObject["kind"]) == "summary" + } + } expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 1 expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == 1 expect(testContext.eventReporter.eventStore.isEmpty) == true @@ -336,7 +342,7 @@ final class EventReporterSpec: QuickSpec { it("does not report events") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 0 expect(testContext.eventReporter.eventStore.isEmpty) == true expect(testContext.eventReporter.lastEventResponseDate).to(beNil()) @@ -362,11 +368,16 @@ final class EventReporterSpec: QuickSpec { it("drops events after the failure") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 2 // 1 retry attempt - expect(testContext.eventReporter.eventStoreKeys) == [] - expect(testContext.eventReporter.eventStoreKinds.contains(.summary)) == false - expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys - expect(testContext.serviceMock.publishedEventDictionaryKinds?.contains(.summary)) == true + expect(testContext.serviceMock.publishEventDataCallCount) == 2 // 1 retry attempt + expect(testContext.eventReporter.eventStore.isEmpty) == true + let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) + valueIsArray(published) { valueArray in + expect(valueArray.count) == testContext.events.count + 1 + expect(Array(valueArray.prefix(testContext.events.count))) == testContext.events.map { encodeToLDValue($0) } + valueIsObject(valueArray[testContext.events.count]) { summaryObject in + expect(summaryObject["kind"]) == "summary" + } + } expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 1 expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == Event.Kind.nonSummaryKinds.count + 1 expect(testContext.eventReporter.lastEventResponseDate).to(beNil()) @@ -395,11 +406,16 @@ final class EventReporterSpec: QuickSpec { it("drops events after the failure") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 2 // 1 retry attempt - expect(testContext.eventReporter.eventStoreKeys) == [] - expect(testContext.eventReporter.eventStoreKinds.contains(.summary)) == false - expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys - expect(testContext.serviceMock.publishedEventDictionaryKinds?.contains(.summary)) == true + expect(testContext.serviceMock.publishEventDataCallCount) == 2 // 1 retry attempt + expect(testContext.eventReporter.eventStore.isEmpty) == true + let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) + valueIsArray(published) { valueArray in + expect(valueArray.count) == testContext.events.count + 1 + expect(Array(valueArray.prefix(testContext.events.count))) == testContext.events.map { encodeToLDValue($0) } + valueIsObject(valueArray[testContext.events.count]) { summaryObject in + expect(summaryObject["kind"]) == "summary" + } + } expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 1 expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == Event.Kind.nonSummaryKinds.count + 1 expect(testContext.eventReporter.lastEventResponseDate).to(beNil()) @@ -431,11 +447,16 @@ final class EventReporterSpec: QuickSpec { it("drops events events after the failure") { expect(testContext.eventReporter.isOnline) == true expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 2 // 1 retry attempt - expect(testContext.eventReporter.eventStoreKeys) == [] - expect(testContext.eventReporter.eventStoreKinds.contains(.summary)) == false - expect(testContext.serviceMock.publishedEventDictionaryKeys) == testContext.eventKeys - expect(testContext.serviceMock.publishedEventDictionaryKinds?.contains(.summary)) == true + expect(testContext.serviceMock.publishEventDataCallCount) == 2 // 1 retry attempt + expect(testContext.eventReporter.eventStore.isEmpty) == true + let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) + valueIsArray(published) { valueArray in + expect(valueArray.count) == testContext.events.count + 1 + expect(Array(valueArray.prefix(testContext.events.count))) == testContext.events.map { encodeToLDValue($0) } + valueIsObject(valueArray[testContext.events.count]) { summaryObject in + expect(summaryObject["kind"]) == "summary" + } + } expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 1 expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == Event.Kind.nonSummaryKinds.count + 1 expect(testContext.eventReporter.lastEventResponseDate).to(beNil()) @@ -465,7 +486,7 @@ final class EventReporterSpec: QuickSpec { it("doesn't report events") { expect(testContext.eventReporter.isOnline) == false expect(testContext.eventReporter.isReportingActive) == false - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 0 expect(testContext.eventReporter.eventStoreKeys) == testContext.eventKeys expect(testContext.eventReporter.eventStoreKinds.contains(.summary)) == false @@ -783,12 +804,15 @@ final class EventReporterSpec: QuickSpec { testContext.recordEvents(Event.Kind.allKinds.count) } it("reports events") { - expect(testContext.serviceMock.publishEventDictionariesCallCount).toEventually(equal(1)) - expect(testContext.serviceMock.publishedEventDictionaries?.count).toEventually(equal(testContext.events.count)) - expect(testContext.serviceMock.publishedEventDictionaryKeys).toEventually(equal(testContext.eventKeys)) + expect(testContext.serviceMock.publishEventDataCallCount).toEventually(equal(1)) expect(testContext.eventReporter.eventStore.isEmpty).toEventually(beTrue()) expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount).toEventually(equal(1)) expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch).toEventually(equal(testContext.events.count)) + let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) + valueIsArray(published) { valueArray in + expect(valueArray.count) == testContext.events.count + expect(valueArray) == testContext.events.map { encodeToLDValue($0) } + } } } context("without events") { @@ -799,7 +823,7 @@ final class EventReporterSpec: QuickSpec { it("doesn't report events") { waitUntil { done in DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Constants.eventFlushIntervalHalfSecond) { - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + expect(testContext.serviceMock.publishEventDataCallCount) == 0 done() } } diff --git a/LaunchDarkly/LaunchDarklyTests/TestUtil.swift b/LaunchDarkly/LaunchDarklyTests/TestUtil.swift index 3d102856..523fa82a 100644 --- a/LaunchDarkly/LaunchDarklyTests/TestUtil.swift +++ b/LaunchDarkly/LaunchDarklyTests/TestUtil.swift @@ -1,6 +1,8 @@ import XCTest import Foundation +@testable import LaunchDarkly + func symmetricAssertEqual(_ exp1: @autoclosure () throws -> T, _ exp2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "") { @@ -14,3 +16,35 @@ func symmetricAssertNotEqual(_ exp1: @autoclosure () throws -> T, XCTAssertNotEqual(try exp1(), try exp2(), message()) XCTAssertNotEqual(try exp2(), try exp1(), message()) } + +func encodeToLDValue(_ value: T, userInfo: [CodingUserInfoKey: Any] = [:]) -> LDValue? { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .custom { date, encoder in + var container = encoder.singleValueContainer() + try container.encode(date.millisSince1970) + } + encoder.userInfo = userInfo + return try? JSONDecoder().decode(LDValue.self, from: encoder.encode(value)) +} + +func encodesToObject(_ value: T, userInfo: [CodingUserInfoKey: Any] = [:], asserts: ([String: LDValue]) -> Void) { + valueIsObject(encodeToLDValue(value, userInfo: userInfo), asserts: asserts) +} + +func valueIsObject(_ value: LDValue?, asserts: ([String: LDValue]) -> Void) { + guard case .object(let dict) = value + else { + XCTFail("expected value to be object got \(String(describing: value))") + return + } + asserts(dict) +} + +func valueIsArray(_ value: LDValue?, asserts: ([LDValue]) -> Void) { + guard case .array(let arr) = value + else { + XCTFail("expected value to be array got \(String(describing: value))") + return + } + asserts(arr) +} From 79705a2b0197b2c1b12a0595d0384f6c38c8ab7a Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Tue, 15 Mar 2022 23:23:14 -0500 Subject: [PATCH 36/50] Break up Event model into multiple SubEvent models. --- LaunchDarkly/LaunchDarkly/LDClient.swift | 8 +- LaunchDarkly/LaunchDarkly/Models/Event.swift | 255 +++---- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 4 +- .../ServiceObjects/EventReporter.swift | 12 +- .../LaunchDarklyTests/LDClientSpec.swift | 30 +- .../LaunchDarklyTests/Models/EventSpec.swift | 667 ++++++------------ .../ServiceObjects/EventReporterSpec.swift | 42 +- 7 files changed, 407 insertions(+), 611 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 6802e2d6..6fb8d06d 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -306,7 +306,7 @@ public class LDClient { onSyncComplete: self.onFlagSyncComplete) if self.hasStarted { - self.eventReporter.record(Event.identifyEvent(user: self.user)) + self.eventReporter.record(IdentifyEvent(user: self.user)) } self.internalSetOnline(wasOnline, completion: completion) @@ -551,7 +551,7 @@ public class LDClient { Log.debug(typeName(and: #function) + "aborted. LDClient not started") return } - let event = Event.customEvent(key: key, user: user, data: data ?? .null, metricValue: metricValue) + let event = CustomEvent(key: key, user: user, data: data ?? .null, metricValue: metricValue) Log.debug(typeName(and: #function) + "event: \(event), data: \(String(describing: data)), metricValue: \(String(describing: metricValue))") eventReporter.record(event) } @@ -576,7 +576,7 @@ public class LDClient { return } - self.eventReporter.record(Event.aliasEvent(newUser: new, oldUser: old)) + self.eventReporter.record(AliasEvent(key: new.key, previousKey: old.key, contextKind: new.contextKind, previousContextKind: old.contextKind)) } /** @@ -779,7 +779,7 @@ public class LDClient { flagStore.replaceStore(newFlags: cachedFlags, completion: nil) } - eventReporter.record(Event.identifyEvent(user: user)) + eventReporter.record(IdentifyEvent(user: user)) self.connectionInformation = ConnectionInformation.uncacheConnectionInformation(config: config, ldClient: self, clientServiceFactory: self.serviceFactory) internalSetOnline(configuration.startOnline) { diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index d028204d..e33b9832 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -1,16 +1,13 @@ import Foundation -func userType(_ user: LDUser) -> String { - return user.isAnonymous ? "anonymousUser" : "user" +private protocol SubEvent { + func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws } -struct Event: Encodable { +class Event: Encodable { enum CodingKeys: String, CodingKey { - case key, previousKey, kind, creationDate, user, userKey, - value, defaultValue = "default", variation, version, - data, startDate, endDate, features, reason, metricValue, - // for aliasing - contextKind, previousContextKind + case key, previousKey, kind, creationDate, user, userKey, value, defaultValue = "default", variation, version, + data, startDate, endDate, features, reason, metricValue, contextKind, previousContextKind } enum Kind: String { @@ -19,144 +16,166 @@ struct Event: Encodable { static var allKinds: [Kind] { [feature, debug, identify, custom, summary, alias] } - - var isAlwaysInlineUserKind: Bool { - [.identify, .debug].contains(self) - } - - var isAlwaysIncludeValueKinds: Bool { - [.feature, .debug].contains(self) - } - - var needsContextKind: Bool { - [.feature, .custom].contains(self) - } } let kind: Kind - let key: String? - let previousKey: String? - let creationDate: Date? - let user: LDUser? - let value: LDValue - let defaultValue: LDValue - let featureFlag: FeatureFlag? - let data: LDValue - let flagRequestTracker: FlagRequestTracker? - let endDate: Date? - let includeReason: Bool - let metricValue: Double? - let contextKind: String? - let previousContextKind: String? - - init(kind: Kind = .custom, - key: String? = nil, - previousKey: String? = nil, - contextKind: String? = nil, - previousContextKind: String? = nil, - user: LDUser? = nil, - value: LDValue = .null, - defaultValue: LDValue = .null, - featureFlag: FeatureFlag? = nil, - data: LDValue = .null, - flagRequestTracker: FlagRequestTracker? = nil, - endDate: Date? = nil, - includeReason: Bool = false, - metricValue: Double? = nil) { + + fileprivate init(kind: Kind) { self.kind = kind - self.key = key - self.previousKey = previousKey - self.creationDate = kind == .summary ? nil : Date() - self.user = user - self.value = value - self.defaultValue = defaultValue - self.featureFlag = featureFlag - self.data = data - self.flagRequestTracker = flagRequestTracker - self.endDate = endDate - self.includeReason = includeReason - self.metricValue = metricValue - self.contextKind = contextKind - self.previousContextKind = previousContextKind } - // swiftlint:disable:next function_parameter_count - static func featureEvent(key: String, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) -> Event { - Log.debug(typeName(and: #function) + "key: \(key), value: \(value), defaultValue: \(defaultValue), includeReason: \(includeReason), featureFlag: \(String(describing: featureFlag))") - return Event(kind: .feature, key: key, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason) + struct UserInfoKeys { + static let inlineUserInEvents = CodingUserInfoKey(rawValue: "LD_inlineUserInEvents")! } - // swiftlint:disable:next function_parameter_count - static func debugEvent(key: String, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag, user: LDUser, includeReason: Bool) -> Event { - Log.debug(typeName(and: #function) + "key: \(key), value: \(value), defaultValue: \(defaultValue), includeReason: \(includeReason), featureFlag: \(String(describing: featureFlag))") - return Event(kind: .debug, key: key, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason) + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(kind.rawValue, forKey: .kind) + switch self.kind { + case .alias: try (self as? AliasEvent)?.encode(to: encoder, container: container) + case .custom: try (self as? CustomEvent)?.encode(to: encoder, container: container) + case .debug, .feature: try (self as? FeatureEvent)?.encode(to: encoder, container: container) + case .identify: try (self as? IdentifyEvent)?.encode(to: encoder, container: container) + case .summary: try (self as? SummaryEvent)?.encode(to: encoder, container: container) + } } +} - static func customEvent(key: String, user: LDUser, data: LDValue, metricValue: Double? = nil) -> Event { - Log.debug(typeName(and: #function) + "key: " + key + ", data: \(data), metricValue: \(String(describing: metricValue))") - return Event(kind: .custom, key: key, user: user, data: data, metricValue: metricValue) - } +class AliasEvent: Event, SubEvent { + let key: String + let previousKey: String + let contextKind: String + let previousContextKind: String + let creationDate: Date - static func identifyEvent(user: LDUser) -> Event { - Log.debug(typeName(and: #function) + "key: " + user.key) - return Event(kind: .identify, key: user.key, user: user) + init(key: String, previousKey: String, contextKind: String, previousContextKind: String, creationDate: Date = Date()) { + self.key = key + self.previousKey = previousKey + self.contextKind = contextKind + self.previousContextKind = previousContextKind + self.creationDate = creationDate + super.init(kind: .alias) } - static func summaryEvent(flagRequestTracker: FlagRequestTracker, endDate: Date = Date()) -> Event? { - Log.debug(typeName(and: #function)) - guard flagRequestTracker.hasLoggedRequests - else { return nil } - return Event(kind: .summary, flagRequestTracker: flagRequestTracker, endDate: endDate) + fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { + var container = container + try container.encode(key, forKey: .key) + try container.encode(previousKey, forKey: .previousKey) + try container.encode(contextKind, forKey: .contextKind) + try container.encode(previousContextKind, forKey: .previousContextKind) + try container.encode(creationDate, forKey: .creationDate) } +} - static func aliasEvent(newUser new: LDUser, oldUser old: LDUser) -> Event { - Log.debug("\(typeName(and: #function)) key: \(new.key), previousKey: \(old.key)") - return Event(kind: .alias, key: new.key, previousKey: old.key, contextKind: userType(new), previousContextKind: userType(old)) - } +class CustomEvent: Event, SubEvent { + let key: String + let user: LDUser + let data: LDValue + let metricValue: Double? + let creationDate: Date - struct UserInfoKeys { - static let inlineUserInEvents = CodingUserInfoKey(rawValue: "LD_inlineUserInEvents")! + init(key: String, user: LDUser, data: LDValue = nil, metricValue: Double? = nil, creationDate: Date = Date()) { + self.key = key + self.user = user + self.data = data + self.metricValue = metricValue + self.creationDate = creationDate + super.init(kind: Event.Kind.custom) } - func encode(to encoder: Encoder) throws { - let inlineUserInEvents = encoder.userInfo[UserInfoKeys.inlineUserInEvents] as? Bool ?? false - - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(kind.rawValue, forKey: .kind) - try container.encodeIfPresent(key, forKey: .key) - try container.encodeIfPresent(previousKey, forKey: .previousKey) - try container.encodeIfPresent(creationDate, forKey: .creationDate) - if kind.isAlwaysInlineUserKind || inlineUserInEvents { - try container.encodeIfPresent(user, forKey: .user) + fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { + var container = container + try container.encode(key, forKey: .key) + if encoder.userInfo[Event.UserInfoKeys.inlineUserInEvents] as? Bool ?? false { + try container.encode(user, forKey: .user) } else { - try container.encodeIfPresent(user?.key, forKey: .userKey) + try container.encode(user.key, forKey: .userKey) } - if kind.isAlwaysIncludeValueKinds { - try container.encode(value, forKey: .value) - try container.encode(defaultValue, forKey: .defaultValue) + if user.isAnonymous == true { + try container.encode("anonymousUser", forKey: .contextKind) } - try container.encodeIfPresent(featureFlag?.variation, forKey: .variation) - try container.encodeIfPresent(featureFlag?.versionForEvents, forKey: .version) if data != .null { try container.encode(data, forKey: .data) } - if let flagRequestTracker = flagRequestTracker { - try container.encode(flagRequestTracker.startDate, forKey: .startDate) - try container.encode(flagRequestTracker.flagCounters, forKey: .features) - } - try container.encodeIfPresent(endDate, forKey: .endDate) - if let reason = includeReason || featureFlag?.trackReason ?? false ? featureFlag?.reason : nil { - try container.encode(LDValue.fromAny(reason), forKey: .reason) - } try container.encodeIfPresent(metricValue, forKey: .metricValue) - if kind.needsContextKind && (user?.isAnonymous == true) { + try container.encode(creationDate, forKey: .creationDate) + } +} + +class FeatureEvent: Event, SubEvent { + let key: String + let user: LDUser + let value: LDValue + let defaultValue: LDValue + let featureFlag: FeatureFlag? + let includeReason: Bool + let creationDate: Date + + init(key: String, user: LDUser, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, includeReason: Bool, isDebug: Bool, creationDate: Date = Date()) { + self.key = key + self.value = value + self.defaultValue = defaultValue + self.featureFlag = featureFlag + self.user = user + self.includeReason = includeReason + self.creationDate = creationDate + super.init(kind: isDebug ? .debug : .feature) + } + + fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { + var container = container + try container.encode(key, forKey: .key) + if kind == .debug || encoder.userInfo[Event.UserInfoKeys.inlineUserInEvents] as? Bool ?? false { + try container.encode(user, forKey: .user) + } else { + try container.encode(user.key, forKey: .userKey) + } + if kind == .feature && user.isAnonymous == true { try container.encode("anonymousUser", forKey: .contextKind) } - if kind == .alias { - try container.encodeIfPresent(self.contextKind, forKey: .contextKind) - try container.encodeIfPresent(self.previousContextKind, forKey: .previousContextKind) + try container.encodeIfPresent(featureFlag?.variation, forKey: .variation) + try container.encodeIfPresent(featureFlag?.versionForEvents, forKey: .version) + try container.encode(value, forKey: .value) + try container.encode(defaultValue, forKey: .defaultValue) + if let reason = includeReason || featureFlag?.trackReason ?? false ? featureFlag?.reason : nil { + try container.encode(LDValue.fromAny(reason), forKey: .reason) } + try container.encode(creationDate, forKey: .creationDate) } } -extension Event: TypeIdentifying { } +class IdentifyEvent: Event, SubEvent { + let user: LDUser + let creationDate: Date + + init(user: LDUser, creationDate: Date = Date()) { + self.user = user + self.creationDate = creationDate + super.init(kind: .identify) + } + + fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { + var container = container + try container.encode(user.key, forKey: .key) + try container.encode(user, forKey: .user) + try container.encode(creationDate, forKey: .creationDate) + } +} + +class SummaryEvent: Event, SubEvent { + let flagRequestTracker: FlagRequestTracker + let endDate: Date + + init(flagRequestTracker: FlagRequestTracker, endDate: Date = Date()) { + self.flagRequestTracker = flagRequestTracker + self.endDate = endDate + super.init(kind: .summary) + } + + fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { + var container = container + try container.encode(flagRequestTracker.startDate, forKey: .startDate) + try container.encode(endDate, forKey: .endDate) + try container.encode(flagRequestTracker.flagCounters, forKey: .features) + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 91716ed7..75420154 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -53,6 +53,8 @@ public struct LDUser: Encodable { /// An NSObject wrapper for the Swift LDUser struct. Intended for use in mixed apps when Swift code needs to pass a user into an Objective-C method. public var objcLdUser: ObjcLDUser { ObjcLDUser(self) } + var contextKind: String { isAnonymous ? "anonymousUser" : "user" } + /** Initializer to create a LDUser. Client configurable attributes each have an optional parameter to facilitate setting user information into the LDUser. The SDK will automatically set `key`, `device`, `operatingSystem`, and `isAnonymous` attributes if the client does not provide them. The SDK embeds `device` and `operatingSystem` into the `custom` dictionary for transmission to LaunchDarkly. - parameter key: String that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. @@ -247,7 +249,7 @@ extension LDUser: TypeIdentifying { } #if DEBUG extension LDUser { - // Compares all user properties. Excludes the composed FlagStore, which contains the users feature flags + // Compares all user properties. func isEqual(to otherUser: LDUser) -> Bool { key == otherUser.key && secondary == otherUser.secondary diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index 4925aa83..8368118d 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -72,11 +72,11 @@ class EventReporter: EventReporting { eventQueue.sync { flagRequestTracker.trackRequest(flagKey: flagKey, reportedValue: value, featureFlag: featureFlag, defaultValue: defaultValue) if recordingFeatureEvent { - let featureEvent = Event.featureEvent(key: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason) + let featureEvent = FeatureEvent(key: flagKey, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: false) recordNoSync(featureEvent) } if recordingDebugEvent, let featureFlag = featureFlag { - let debugEvent = Event.debugEvent(key: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason) + let debugEvent = FeatureEvent(key: flagKey, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: true) recordNoSync(debugEvent) } } @@ -114,9 +114,11 @@ class EventReporter: EventReporting { return } - let summaryEvent = Event.summaryEvent(flagRequestTracker: flagRequestTracker) - if let summaryEvent = summaryEvent { recordNoSync(summaryEvent) } - flagRequestTracker = FlagRequestTracker() + if flagRequestTracker.hasLoggedRequests { + let summaryEvent = SummaryEvent(flagRequestTracker: flagRequestTracker) + self.eventStore.append(summaryEvent) + flagRequestTracker = FlagRequestTracker() + } guard !eventStore.isEmpty else { diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index cfe147b3..0610cf02 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -254,8 +254,7 @@ final class LDClientSpec: QuickSpec { } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect(testContext.recordedEvent?.kind) == .identify - expect(testContext.recordedEvent?.key) == testContext.user.key + expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 @@ -299,8 +298,7 @@ final class LDClientSpec: QuickSpec { } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect(testContext.recordedEvent?.kind) == .identify - expect(testContext.recordedEvent?.key) == testContext.user.key + expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 @@ -343,8 +341,7 @@ final class LDClientSpec: QuickSpec { } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 2 // both start and internalIdentify - expect(testContext.recordedEvent?.kind) == .identify - expect(testContext.recordedEvent?.key) == testContext.user.key + expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 // Both start and internalIdentify @@ -378,8 +375,7 @@ final class LDClientSpec: QuickSpec { } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect(testContext.recordedEvent?.kind) == .identify - expect(testContext.recordedEvent?.key) == testContext.subject.user.key + expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.subject.user } it("converts cached data") { expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 @@ -563,8 +559,7 @@ final class LDClientSpec: QuickSpec { } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect(testContext.recordedEvent?.kind) == .identify - expect(testContext.recordedEvent?.key) == testContext.user.key + expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 @@ -610,8 +605,7 @@ final class LDClientSpec: QuickSpec { } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect(testContext.recordedEvent?.kind) == .identify - expect(testContext.recordedEvent?.key) == testContext.user.key + expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 @@ -836,7 +830,6 @@ final class LDClientSpec: QuickSpec { var testContext: TestContext! describe("stop") { - var event: LaunchDarkly.Event! var priorRecordedEvents: Int! context("when started") { beforeEach { @@ -846,7 +839,6 @@ final class LDClientSpec: QuickSpec { beforeEach { testContext = TestContext(startOnline: true) testContext.start() - event = Event.stub(.custom, with: testContext.user) priorRecordedEvents = testContext.eventReporterMock.recordCallCount testContext.subject.close() @@ -855,7 +847,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.isOnline) == false } it("stops recording events") { - testContext.subject.track(key: event.key!) + testContext.subject.track(key: "abc") expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents } it("flushes the event reporter") { @@ -866,7 +858,6 @@ final class LDClientSpec: QuickSpec { beforeEach { testContext = TestContext() testContext.start() - event = Event.stub(.custom, with: testContext.user) priorRecordedEvents = testContext.eventReporterMock.recordCallCount testContext.subject.close() @@ -875,7 +866,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.isOnline) == false } it("stops recording events") { - testContext.subject.track(key: event.key!) + testContext.subject.track(key: "abc") expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents } it("flushes the event reporter") { @@ -887,7 +878,6 @@ final class LDClientSpec: QuickSpec { beforeEach { testContext = TestContext() testContext.start() - event = Event.stub(.custom, with: testContext.user) testContext.subject.close() priorRecordedEvents = testContext.eventReporterMock.recordCallCount @@ -897,7 +887,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.isOnline) == false } it("stops recording events") { - testContext.subject.track(key: event.key!) + testContext.subject.track(key: "abc") expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents } it("flushes the event reporter") { @@ -917,7 +907,7 @@ final class LDClientSpec: QuickSpec { } it("records a custom event when client was started") { testContext.subject.track(key: "customEvent", data: "abc", metricValue: 5.0) - let receivedEvent = testContext.eventReporterMock.recordReceivedEvent + let receivedEvent = testContext.eventReporterMock.recordReceivedEvent as? CustomEvent expect(receivedEvent?.key) == "customEvent" expect(receivedEvent?.user) == testContext.user expect(receivedEvent?.data) == "abc" diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 3627b36e..efe5b400 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -1,509 +1,298 @@ import Foundation -import Quick -import Nimble +import XCTest + @testable import LaunchDarkly -final class EventSpec: QuickSpec { - struct Constants { - static let eventKey = "EventSpec.Event.Key" +final class EventSpec: XCTestCase { + func testAliasEventInit() { + let testDate = Date() + let event = AliasEvent(key: "abc", previousKey: "def", contextKind: "user", previousContextKind: "anonymousUser", creationDate: testDate) + XCTAssertEqual(event.kind, .alias) + XCTAssertEqual(event.key, "abc") + XCTAssertEqual(event.previousKey, "def") + XCTAssertEqual(event.contextKind, "user") + XCTAssertEqual(event.previousContextKind, "anonymousUser") + XCTAssertEqual(event.creationDate, testDate) } - struct CustomEvent { - static let dictionaryData: LDValue = ["dozen": 12, - "phi": 1.61803, - "true": true, - "data string": "custom event dictionary data", - "nestedArray": [1, 3, 7, 12], - "nestedDictionary": ["one": 1.0, "three": 3.0]] + func testFeatureEventInit() { + let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) + let user = LDUser.stub() + let testDate = Date() + let event = FeatureEvent(key: "abc", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: true, isDebug: false, creationDate: testDate) + XCTAssertEqual(event.kind, Event.Kind.feature) + XCTAssertEqual(event.key, "abc") + XCTAssertEqual(event.user, user) + XCTAssertEqual(event.value, true) + XCTAssertEqual(event.defaultValue, false) + XCTAssertEqual(event.featureFlag?.allPropertiesMatch(featureFlag), true) + XCTAssertEqual(event.includeReason, true) + XCTAssertEqual(event.creationDate, testDate) } - override func spec() { - initSpec() - aliasSpec() - featureEventSpec() - debugEventSpec() - customEventSpec() - identifyEventSpec() - summaryEventSpec() - testAliasEventEncoding() - testCustomEventEncoding() - testDebugEventEncoding() - testFeatureEventEncoding() - testIdentifyEventEncoding() - testSummaryEventEncoding() + func testDebugEventInit() { + let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) + let user = LDUser.stub() + let testDate = Date() + let event = FeatureEvent(key: "abc", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true, creationDate: testDate) + XCTAssertEqual(event.kind, Event.Kind.debug) + XCTAssertEqual(event.key, "abc") + XCTAssertEqual(event.user, user) + XCTAssertEqual(event.value, true) + XCTAssertEqual(event.defaultValue, false) + XCTAssertEqual(event.featureFlag?.allPropertiesMatch(featureFlag), true) + XCTAssertEqual(event.includeReason, false) + XCTAssertEqual(event.creationDate, testDate) } - private func initSpec() { - describe("init") { - var user: LDUser! - var featureFlag: FeatureFlag! - var event: Event! - beforeEach { - user = LDUser.stub() - } - context("with optional items") { - beforeEach { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - - event = Event(kind: .feature, key: Constants.eventKey, user: user, value: true, defaultValue: false, featureFlag: featureFlag, data: CustomEvent.dictionaryData, flagRequestTracker: FlagRequestTracker.stub(), endDate: Date()) - } - it("creates an event with matching data") { - expect(event.kind) == Event.Kind.feature - expect(event.key) == Constants.eventKey - expect(event.creationDate).toNot(beNil()) - expect(event.user) == user - expect(event.value) == true - expect(event.defaultValue) == false - expect(event.featureFlag?.allPropertiesMatch(featureFlag)).to(beTrue()) - expect(event.data) == CustomEvent.dictionaryData - expect(event.flagRequestTracker).toNot(beNil()) - expect(event.endDate).toNot(beNil()) - } - } - context("without optional items") { - beforeEach { - event = Event(kind: .feature) - } - it("creates an event with matching data") { - expect(event.kind) == Event.Kind.feature - expect(event.key).to(beNil()) - expect(event.creationDate).toNot(beNil()) - expect(event.user).to(beNil()) - expect(event.value) == .null - expect(event.defaultValue) == .null - expect(event.featureFlag).to(beNil()) - expect(event.data) == .null - expect(event.flagRequestTracker).to(beNil()) - expect(event.endDate).to(beNil()) - } - } - } + func testCustomEventInit() { + let user = LDUser.stub() + let testDate = Date() + let event = CustomEvent(key: "abc", user: user, data: ["abc": 123], metricValue: 5.0, creationDate: testDate) + XCTAssertEqual(event.kind, Event.Kind.custom) + XCTAssertEqual(event.key, "abc") + XCTAssertEqual(event.user, user) + XCTAssertEqual(event.data, ["abc": 123]) + XCTAssertEqual(event.metricValue, 5.0) + XCTAssertEqual(event.creationDate, testDate) } - private func aliasSpec() { - describe("alias events") { - it("has correct fields") { - let event = Event.aliasEvent(newUser: LDUser(), oldUser: LDUser()) - expect(event.kind) == Event.Kind.alias - } - it("from user to user") { - let event = Event.aliasEvent(newUser: LDUser(key: "new"), oldUser: LDUser(key: "old")) - expect(event.key) == "new" - expect(event.previousKey) == "old" - expect(event.contextKind) == "user" - expect(event.previousContextKind) == "user" - } - it("from anon to anon") { - let event = Event.aliasEvent(newUser: LDUser(key: "new", isAnonymous: true), oldUser: LDUser(key: "old", isAnonymous: true)) - expect(event.key) == "new" - expect(event.previousKey) == "old" - expect(event.contextKind) == "anonymousUser" - expect(event.previousContextKind) == "anonymousUser" - } - } + func testIdentifyEventInit() { + let testDate = Date() + let user = LDUser.stub() + let event = IdentifyEvent(user: user, creationDate: testDate) + XCTAssertEqual(event.kind, Event.Kind.identify) + XCTAssertEqual(event.user, user) + XCTAssertEqual(event.creationDate, testDate) } - private func featureEventSpec() { - describe("featureEvent") { - it("creates a feature event with matching data") { - let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - let user = LDUser.stub() - let event = Event.featureEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - expect(event.kind) == Event.Kind.feature - expect(event.key) == Constants.eventKey - expect(event.creationDate).toNot(beNil()) - expect(event.user) == user - expect(event.value) == true - expect(event.defaultValue) == false - expect(event.featureFlag?.allPropertiesMatch(featureFlag)).to(beTrue()) - - expect(event.data) == .null - expect(event.endDate).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - } - } + func testSummaryEventInit() { + let flagRequestTracker = FlagRequestTracker.stub() + let endDate = Date() + let event = SummaryEvent(flagRequestTracker: flagRequestTracker, endDate: endDate) + XCTAssertEqual(event.kind, Event.Kind.summary) + XCTAssertEqual(event.endDate, endDate) + XCTAssertEqual(event.flagRequestTracker.startDate, flagRequestTracker.startDate) + XCTAssertEqual(event.flagRequestTracker.flagCounters, flagRequestTracker.flagCounters) } - private func debugEventSpec() { - describe("debugEvent") { - it("creates a debug event with matching data") { - let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - let user = LDUser.stub() - let event = Event.debugEvent(key: Constants.eventKey, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - - expect(event.kind) == Event.Kind.debug - expect(event.key) == Constants.eventKey - expect(event.creationDate).toNot(beNil()) - expect(event.user) == user - expect(event.value) == true - expect(event.defaultValue) == false - expect(event.featureFlag?.allPropertiesMatch(featureFlag)).to(beTrue()) - - expect(event.data) == .null - expect(event.endDate).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - } + func testAliasEventEncoding() { + let event = AliasEvent(key: "abc", previousKey: "def", contextKind: "user", previousContextKind: "anonymousUser") + encodesToObject(event) { dict in + XCTAssertEqual(dict.count, 6) + XCTAssertEqual(dict["kind"], "alias") + XCTAssertEqual(dict["key"], "abc") + XCTAssertEqual(dict["previousKey"], "def") + XCTAssertEqual(dict["contextKind"], "user") + XCTAssertEqual(dict["previousContextKind"], "anonymousUser") + XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) } } - private func customEventSpec() { - var user: LDUser! - beforeEach { - user = LDUser.stub() - } - describe("customEvent") { - context("with valid json data") { - it("creates a custom event with matching data") { - let event = Event.customEvent(key: Constants.eventKey, user: user, data: ["abc": 123]) - expect(event.kind) == Event.Kind.custom - expect(event.key) == Constants.eventKey - expect(event.creationDate).toNot(beNil()) - expect(event.user) == user - expect(event.data) == ["abc": 123] - expect(event.value) == .null - expect(event.defaultValue) == .null - expect(event.endDate).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - } - } - context("without data") { - it("creates a custom event with matching data") { - let event = Event.customEvent(key: Constants.eventKey, user: user, data: nil) - - expect(event.kind) == Event.Kind.custom - expect(event.key) == Constants.eventKey - expect(event.creationDate).toNot(beNil()) - expect(event.user) == user - expect(event.data) == .null - - expect(event.value) == .null - expect(event.defaultValue) == .null - expect(event.endDate).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - } - } + func testCustomEventEncodingDataAndMetric() { + let user = LDUser.stub() + let event = CustomEvent(key: "event-key", user: user, data: ["abc", 12], metricValue: 0.5) + encodesToObject(event) { dict in + XCTAssertEqual(dict.count, 6) + XCTAssertEqual(dict["kind"], "custom") + XCTAssertEqual(dict["key"], "event-key") + XCTAssertEqual(dict["data"], ["abc", 12]) + XCTAssertEqual(dict["metricValue"], 0.5) + XCTAssertEqual(dict["userKey"], .string(user.key)) + XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) } } - private func identifyEventSpec() { - var user: LDUser! - var event: Event! - beforeEach { - user = LDUser.stub() - } - describe("identifyEvent") { - beforeEach { - event = Event.identifyEvent(user: user) - } - it("creates an identify event with matching data") { - expect(event.kind) == Event.Kind.identify - expect(event.key) == user.key - expect(event.creationDate).toNot(beNil()) - expect(event.user) == user - - expect(event.value) == .null - expect(event.defaultValue) == .null - expect(event.data) == .null - expect(event.endDate).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - } + func testCustomEventEncodingAnonUser() { + let anonUser = LDUser() + let event = CustomEvent(key: "event-key", user: anonUser, data: ["key": "val"]) + encodesToObject(event) { dict in + XCTAssertEqual(dict.count, 6) + XCTAssertEqual(dict["kind"], "custom") + XCTAssertEqual(dict["key"], "event-key") + XCTAssertEqual(dict["data"], ["key": "val"]) + XCTAssertEqual(dict["userKey"], .string(anonUser.key)) + XCTAssertEqual(dict["contextKind"], "anonymousUser") + XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) } } - private func summaryEventSpec() { - var event: Event! - var flagRequestTracker: FlagRequestTracker! - var endDate: Date! - describe("summaryEvent") { - context("with tracked requests") { - beforeEach { - flagRequestTracker = FlagRequestTracker.stub() - endDate = Date() - - event = Event.summaryEvent(flagRequestTracker: flagRequestTracker, endDate: endDate) - } - it("creates a summary event with matching data") { - expect(event.kind) == Event.Kind.summary - expect(event.endDate) == endDate - expect(event.flagRequestTracker?.startDate) == flagRequestTracker.startDate - expect(event.flagRequestTracker?.flagCounters) == flagRequestTracker.flagCounters - - expect(event.key).to(beNil()) - expect(event.creationDate).to(beNil()) - expect(event.user).to(beNil()) - expect(event.value) == .null - expect(event.defaultValue) == .null - expect(event.featureFlag).to(beNil()) - expect(event.data) == .null - } - } - context("without tracked requests") { - beforeEach { - flagRequestTracker = FlagRequestTracker() - endDate = Date() - - event = Event.summaryEvent(flagRequestTracker: flagRequestTracker, endDate: endDate) - } - it("does not create an event") { - expect(event).to(beNil()) - } - } + func testCustomEventEncodingInlining() { + let user = LDUser.stub() + let event = CustomEvent(key: "event-key", user: user, data: nil, metricValue: 2.5) + encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: true]) { dict in + XCTAssertEqual(dict.count, 5) + XCTAssertEqual(dict["kind"], "custom") + XCTAssertEqual(dict["key"], "event-key") + XCTAssertEqual(dict["metricValue"], 2.5) + XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) } } - private func testAliasEventEncoding() { - it("alias event encoding") { - let user = LDUser(key: "abc") - let anonUser = LDUser(key: "anon", isAnonymous: true) - let event = Event.aliasEvent(newUser: user, oldUser: anonUser) + func testFeatureEventEncodingNoReasonByDefault() { + let user = LDUser.stub() + let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, flagVersion: 3, reason: ["kind": "OFF"]) + [false, true].forEach { isDebug in + let event = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: isDebug) encodesToObject(event) { dict in - expect(dict.count) == 6 - expect(dict["kind"]) == "alias" - expect(dict["key"]) == .string(user.key) - expect(dict["previousKey"]) == .string(anonUser.key) - expect(dict["contextKind"]) == "user" - expect(dict["previousContextKind"]) == "anonymousUser" - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) + XCTAssertEqual(dict.count, 8) + XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") + XCTAssertEqual(dict["key"], "event-key") + XCTAssertEqual(dict["value"], true) + XCTAssertEqual(dict["default"], false) + XCTAssertEqual(dict["variation"], 2) + XCTAssertEqual(dict["version"], 3) + if isDebug { + XCTAssertEqual(dict["user"], encodeToLDValue(user)) + } else { + XCTAssertEqual(dict["userKey"], .string(user.key)) + } + XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) } } } - private func testCustomEventEncoding() { + func testFeatureEventEncodingIncludeReason() { let user = LDUser.stub() - context("custom event") { - it("encodes with data and metric") { - let event = Event.customEvent(key: "event-key", user: user, data: ["abc", 12], metricValue: 0.5) - encodesToObject(event) { dict in - expect(dict.count) == 6 - expect(dict["kind"]) == "custom" - expect(dict["key"]) == "event-key" - expect(dict["data"]) == ["abc", 12] - expect(dict["metricValue"]) == 0.5 - expect(dict["userKey"]) == .string(user.key) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) - } - } - it("encodes with only data and anon user") { - let anonUser = LDUser() - let event = Event.customEvent(key: "event-key", user: anonUser, data: ["key": "val"]) - encodesToObject(event) { dict in - expect(dict.count) == 6 - expect(dict["kind"]) == "custom" - expect(dict["key"]) == "event-key" - expect(dict["data"]) == ["key": "val"] - expect(dict["userKey"]) == .string(anonUser.key) - expect(dict["contextKind"]) == "anonymousUser" - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) - } - } - it("encodes inlining user") { - let event = Event.customEvent(key: "event-key", user: user, data: nil, metricValue: 2.5) - encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: true]) { dict in - expect(dict.count) == 5 - expect(dict["kind"]) == "custom" - expect(dict["key"]) == "event-key" - expect(dict["metricValue"]) == 2.5 - expect(dict["user"]) == encodeToLDValue(user) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) + let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, version: 2, flagVersion: 3, reason: ["kind": "OFF"]) + [false, true].forEach { isDebug in + let event = FeatureEvent(key: "event-key", user: user, value: 3, defaultValue: 4, featureFlag: featureFlag, includeReason: true, isDebug: isDebug) + encodesToObject(event) { dict in + XCTAssertEqual(dict.count, 9) + XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") + XCTAssertEqual(dict["key"], "event-key") + XCTAssertEqual(dict["value"], 3) + XCTAssertEqual(dict["default"], 4) + XCTAssertEqual(dict["variation"], 2) + XCTAssertEqual(dict["version"], 3) + XCTAssertEqual(dict["reason"], ["kind": "OFF"]) + if isDebug { + XCTAssertEqual(dict["user"], encodeToLDValue(user)) + } else { + XCTAssertEqual(dict["userKey"], .string(user.key)) } + XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) } } } - private func testDebugEventEncoding() { + func testFeatureEventEncodingTrackReason() { let user = LDUser.stub() - it("encodes without reason by default") { - let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, flagVersion: 3, reason: ["kind": "OFF"]) - let event = Event.debugEvent(key: "event-key", value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) + let featureFlag = FeatureFlag(flagKey: "flag-key", reason: ["kind": "OFF"], trackReason: true) + [false, true].forEach { isDebug in + let event = FeatureEvent(key: "event-key", user: user, value: nil, defaultValue: nil, featureFlag: featureFlag, includeReason: false, isDebug: isDebug) encodesToObject(event) { dict in - expect(dict.count) == 8 - expect(dict["kind"]) == "debug" - expect(dict["key"]) == "event-key" - expect(dict["value"]) == true - expect(dict["default"]) == false - expect(dict["variation"]) == 2 - expect(dict["version"]) == 3 - expect(dict["user"]) == encodeToLDValue(user) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) - } - } - it("encodes with reason when includeReason is true") { - let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, version: 2, flagVersion: 3, reason: ["kind": "OFF"]) - let event = Event.debugEvent(key: "event-key", value: 3, defaultValue: 4, featureFlag: featureFlag, user: user, includeReason: true) - encodesToObject(event) { dict in - expect(dict.count) == 9 - expect(dict["kind"]) == "debug" - expect(dict["key"]) == "event-key" - expect(dict["value"]) == 3 - expect(dict["default"]) == 4 - expect(dict["variation"]) == 2 - expect(dict["version"]) == 3 - expect(dict["reason"]) == ["kind": "OFF"] - expect(dict["user"]) == encodeToLDValue(user) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) + XCTAssertEqual(dict.count, 7) + XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") + XCTAssertEqual(dict["key"], "event-key") + XCTAssertEqual(dict["value"], .null) + XCTAssertEqual(dict["default"], .null) + XCTAssertEqual(dict["reason"], ["kind": "OFF"]) + if isDebug { + XCTAssertEqual(dict["user"], encodeToLDValue(user)) + } else { + XCTAssertEqual(dict["userKey"], .string(user.key)) + } + XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) } } - it("encodes with reason when trackReason is true") { - let featureFlag = FeatureFlag(flagKey: "flag-key", reason: ["kind": "OFF"], trackReason: true) - let event = Event.debugEvent(key: "event-key", value: nil, defaultValue: nil, featureFlag: featureFlag, user: user, includeReason: false) + } + + func testFeatureEventEncodingAnonContextKind() { + let user = LDUser() + [false, true].forEach { isDebug in + let event = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: nil, includeReason: false, isDebug: isDebug) encodesToObject(event) { dict in - expect(dict.count) == 7 - expect(dict["kind"]) == "debug" - expect(dict["key"]) == "event-key" - expect(dict["value"]) == .null - expect(dict["default"]) == .null - expect(dict["reason"]) == ["kind": "OFF"] - expect(dict["user"]) == encodeToLDValue(user) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) - } - } - it("encodes inlined user always") { - let anonUser = LDUser() - let featureFlag = FeatureFlag(flagKey: "flag-key", version: 3) - let event = Event.debugEvent(key: "event-key", value: true, defaultValue: false, featureFlag: featureFlag, user: anonUser, includeReason: false) - encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: false]) { dict in - expect(dict.count) == 7 - expect(dict["kind"]) == "debug" - expect(dict["key"]) == "event-key" - expect(dict["value"]) == true - expect(dict["default"]) == false - expect(dict["version"]) == 3 - expect(dict["user"]) == encodeToLDValue(anonUser) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) + XCTAssertEqual(dict.count, isDebug ? 6 : 7) + XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") + XCTAssertEqual(dict["key"], "event-key") + XCTAssertEqual(dict["value"], true) + XCTAssertEqual(dict["default"], false) + if isDebug { + XCTAssertEqual(dict["user"], encodeToLDValue(user)) + } else { + XCTAssertEqual(dict["userKey"], .string(user.key)) + XCTAssertEqual(dict["contextKind"], "anonymousUser") + } + XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) } } } - private func testFeatureEventEncoding() { + func testFeatureEventEncodingInlinesUserForDebugOrConfig() { let user = LDUser.stub() - it("encodes without reason by default") { - let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, flagVersion: 3, reason: ["kind": "OFF"]) - let event = Event.featureEvent(key: "event-key", value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - encodesToObject(event) { dict in - expect(dict.count) == 8 - expect(dict["kind"]) == "feature" - expect(dict["key"]) == "event-key" - expect(dict["value"]) == true - expect(dict["default"]) == false - expect(dict["variation"]) == 2 - expect(dict["version"]) == 3 - expect(dict["userKey"]) == .string(user.key) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) - } - } - it("encodes with reason when includeReason is true") { - let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, version: 2, flagVersion: 3, reason: ["kind": "OFF"]) - let event = Event.featureEvent(key: "event-key", value: 3, defaultValue: 4, featureFlag: featureFlag, user: user, includeReason: true) - encodesToObject(event) { dict in - expect(dict.count) == 9 - expect(dict["kind"]) == "feature" - expect(dict["key"]) == "event-key" - expect(dict["value"]) == 3 - expect(dict["default"]) == 4 - expect(dict["variation"]) == 2 - expect(dict["version"]) == 3 - expect(dict["reason"]) == ["kind": "OFF"] - expect(dict["userKey"]) == .string(user.key) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) - } - } - it("encodes with reason when trackReason is true") { - let featureFlag = FeatureFlag(flagKey: "flag-key", reason: ["kind": "OFF"], trackReason: true) - let event = Event.featureEvent(key: "event-key", value: nil, defaultValue: nil, featureFlag: featureFlag, user: user, includeReason: false) - encodesToObject(event) { dict in - expect(dict.count) == 7 - expect(dict["kind"]) == "feature" - expect(dict["key"]) == "event-key" - expect(dict["value"]) == .null - expect(dict["default"]) == .null - expect(dict["reason"]) == ["kind": "OFF"] - expect(dict["userKey"]) == .string(user.key) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) - } - } - it("encodes inlined user when configured") { - let featureFlag = FeatureFlag(flagKey: "flag-key", version: 3) - let event = Event.featureEvent(key: "event-key", value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: true]) { dict in - expect(dict.count) == 7 - expect(dict["kind"]) == "feature" - expect(dict["key"]) == "event-key" - expect(dict["value"]) == true - expect(dict["default"]) == false - expect(dict["version"]) == 3 - expect(dict["user"]) == encodeToLDValue(user) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) - } - } - it("encodes with contextKind for anon user") { - let anonUser = LDUser() - let event = Event.featureEvent(key: "event-key", value: true, defaultValue: false, featureFlag: nil, user: anonUser, includeReason: false) - encodesToObject(event) { dict in - expect(dict.count) == 7 - expect(dict["kind"]) == "feature" - expect(dict["key"]) == "event-key" - expect(dict["value"]) == true - expect(dict["default"]) == false - expect(dict["userKey"]) == .string(anonUser.key) - expect(dict["contextKind"]) == "anonymousUser" - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) - } - } + let featureFlag = FeatureFlag(flagKey: "flag-key", version: 3) + let featureEvent = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: false) + let debugEvent = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true) + let encodedFeature = encodeToLDValue(featureEvent, userInfo: [Event.UserInfoKeys.inlineUserInEvents: true]) + let encodedDebug = encodeToLDValue(debugEvent, userInfo: [Event.UserInfoKeys.inlineUserInEvents: false]) + [encodedFeature, encodedDebug].forEach { valueIsObject($0) { dict in + XCTAssertEqual(dict.count, 7) + XCTAssertEqual(dict["key"], "event-key") + XCTAssertEqual(dict["value"], true) + XCTAssertEqual(dict["default"], false) + XCTAssertEqual(dict["version"], 3) + XCTAssertEqual(dict["user"], encodeToLDValue(user)) + }} } - private func testIdentifyEventEncoding() { + func testIdentifyEventEncoding() { let user = LDUser.stub() - it("identify event encoding") { - for inlineUser in [true, false] { - let event = Event.identifyEvent(user: user) - encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: inlineUser]) { dict in - expect(dict.count) == 4 - expect(dict["kind"]) == "identify" - expect(dict["key"]) == .string(user.key) - expect(dict["user"]) == encodeToLDValue(user) - expect(dict["creationDate"]) == LDValue.fromAny(event.creationDate?.millisSince1970) - } + for inlineUser in [true, false] { + let event = IdentifyEvent(user: user) + encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: inlineUser]) { dict in + XCTAssertEqual(dict.count, 4) + XCTAssertEqual(dict["kind"], "identify") + XCTAssertEqual(dict["key"], .string(user.key)) + XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) } } } - private func testSummaryEventEncoding() { - it("summary event encoding") { - let flag = FeatureFlag(flagKey: "bool-flag", variation: 1, version: 5, flagVersion: 2) - var flagRequestTracker = FlagRequestTracker() - flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) - flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) - let event = Event.summaryEvent(flagRequestTracker: flagRequestTracker, endDate: Date()) - encodesToObject(event) { dict in - expect(dict.count) == 4 - expect(dict["kind"]) == "summary" - expect(dict["startDate"]) == LDValue.fromAny(flagRequestTracker.startDate.millisSince1970) - expect(dict["endDate"]) == LDValue.fromAny(event?.endDate?.millisSince1970) - valueIsObject(dict["features"]) { features in - expect(features.count) == 1 - let counter = FlagCounter() - counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) - counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) - expect(features["bool-flag"]) == encodeToLDValue(counter) - } + func testSummaryEventEncoding() { + let flag = FeatureFlag(flagKey: "bool-flag", variation: 1, version: 5, flagVersion: 2) + var flagRequestTracker = FlagRequestTracker() + flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) + flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) + let event = SummaryEvent(flagRequestTracker: flagRequestTracker, endDate: Date()) + encodesToObject(event) { dict in + XCTAssertEqual(dict.count, 4) + XCTAssertEqual(dict["kind"], "summary") + XCTAssertEqual(dict["startDate"], LDValue.fromAny(flagRequestTracker.startDate.millisSince1970)) + XCTAssertEqual(dict["endDate"], LDValue.fromAny(event.endDate.millisSince1970)) + valueIsObject(dict["features"]) { features in + XCTAssertEqual(features.count, 1) + let counter = FlagCounter() + counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) + counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) + XCTAssertEqual(features["bool-flag"], encodeToLDValue(counter)) } } } } +extension Event: Equatable { + public static func == (_ lhs: Event, _ rhs: Event) -> Bool { + let config = [LDUser.UserInfoKeys.includePrivateAttributes: true, Event.UserInfoKeys.inlineUserInEvents: true] + return encodeToLDValue(lhs, userInfo: config) == encodeToLDValue(rhs, userInfo: config) + } +} + extension Event { static func stub(_ eventKind: Kind, with user: LDUser) -> Event { switch eventKind { case .feature: let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - return Event.featureEvent(key: UUID().uuidString, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) + return FeatureEvent(key: UUID().uuidString, user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: false) case .debug: let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - return Event.debugEvent(key: UUID().uuidString, value: true, defaultValue: false, featureFlag: featureFlag, user: user, includeReason: false) - case .identify: return Event.identifyEvent(user: user) - case .custom: return Event.customEvent(key: UUID().uuidString, user: user, data: ["custom": .string(UUID().uuidString)]) - case .summary: return Event.summaryEvent(flagRequestTracker: FlagRequestTracker.stub())! - case .alias: return Event.aliasEvent(newUser: LDUser(), oldUser: LDUser()) + return FeatureEvent(key: UUID().uuidString, user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true) + case .identify: return IdentifyEvent(user: user) + case .custom: return CustomEvent(key: UUID().uuidString, user: user, data: ["custom": .string(UUID().uuidString)]) + case .summary: return SummaryEvent(flagRequestTracker: FlagRequestTracker.stub()) + case .alias: return AliasEvent(key: UUID().uuidString, previousKey: UUID().uuidString, contextKind: "anonymousUser", previousContextKind: "anonymousUser") } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index 330570f4..4c7d9a05 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -17,7 +17,6 @@ final class EventReporterSpec: QuickSpec { var user: LDUser! var serviceMock: DarklyServiceMock! var events: [Event] = [] - var eventKeys: [String]! { events.compactMap { $0.key } } var lastEventResponseDate: Date? var flagKey: LDFlagKey! var featureFlag: FeatureFlag! @@ -180,7 +179,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.isOnline) == false expect(testContext.eventReporter.isReportingActive) == false expect(testContext.serviceMock.publishEventDataCallCount) == 0 - expect(testContext.eventReporter.eventStoreKeys) == testContext.eventKeys + expect(testContext.eventReporter.eventStore) == testContext.events } } } @@ -198,7 +197,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.isOnline) == false expect(testContext.eventReporter.isReportingActive) == false expect(testContext.serviceMock.publishEventDataCallCount) == 0 - expect(testContext.eventReporter.eventStoreKeys) == testContext.eventKeys + expect(testContext.eventReporter.eventStore) == testContext.events } it("does not record a dropped event to diagnosticCache") { expect(testContext.diagnosticCache.incrementDroppedEventCountCallCount) == 0 @@ -216,8 +215,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.isOnline) == false expect(testContext.eventReporter.isReportingActive) == false expect(testContext.serviceMock.publishEventDataCallCount) == 0 - expect(testContext.eventReporter.eventStoreKeys) == testContext.eventKeys - expect(testContext.eventReporter.eventStoreKeys.contains(extraEvent.key!)) == false + expect(testContext.eventReporter.eventStore) == testContext.events } it("records a dropped event to diagnosticCache") { expect(testContext.diagnosticCache.incrementDroppedEventCountCallCount) == 1 @@ -488,8 +486,7 @@ final class EventReporterSpec: QuickSpec { expect(testContext.eventReporter.isReportingActive) == false expect(testContext.serviceMock.publishEventDataCallCount) == 0 expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 0 - expect(testContext.eventReporter.eventStoreKeys) == testContext.eventKeys - expect(testContext.eventReporter.eventStoreKinds.contains(.summary)) == false + expect(testContext.eventReporter.eventStore) == testContext.events expect(testContext.eventReporter.lastEventResponseDate).to(beNil()) expect(testContext.eventReporter.flagRequestTracker.hasLoggedRequests) == true guard case .isOffline = testContext.syncResult @@ -529,8 +526,8 @@ final class EventReporterSpec: QuickSpec { } it("records a feature event") { expect(testContext.eventReporter.eventStore.count) == 1 - expect(testContext.eventReporter.eventStoreKeys.contains(testContext.flagKey)).to(beTrue()) - expect(testContext.eventReporter.eventStoreKinds.contains(.feature)).to(beTrue()) + expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.kind) == .feature + expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.key) == testContext.flagKey } summarizesRequest() } @@ -546,8 +543,8 @@ final class EventReporterSpec: QuickSpec { } it("records a feature event") { expect(testContext.eventReporter.eventStore.count) == 1 - expect(testContext.eventReporter.eventStoreKeys.contains(testContext.flagKey)).to(beTrue()) - expect(testContext.eventReporter.eventStoreKinds.contains(.feature)).to(beTrue()) + expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.kind) == .feature + expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.key) == testContext.flagKey } summarizesRequest() } @@ -580,8 +577,8 @@ final class EventReporterSpec: QuickSpec { } it("records a debug event") { expect(testContext.eventReporter.eventStore.count) == 1 - expect(testContext.eventReporter.eventStoreKeys.contains(testContext.flagKey)).to(beTrue()) - expect(testContext.eventReporter.eventStoreKinds.contains(.debug)).to(beTrue()) + expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.kind) == .debug + expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.key) == testContext.flagKey } it("tracks the flag request") { let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) @@ -618,8 +615,8 @@ final class EventReporterSpec: QuickSpec { } it("records a debug event") { expect(testContext.eventReporter.eventStore.count) == 1 - expect(testContext.eventReporter.eventStoreKeys.contains(testContext.flagKey)).to(beTrue()) - expect(testContext.eventReporter.eventStoreKinds.contains(.debug)).to(beTrue()) + expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.kind) == .debug + expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.key) == testContext.flagKey } summarizesRequest() } @@ -652,10 +649,9 @@ final class EventReporterSpec: QuickSpec { } it("records a feature and debug event") { expect(testContext.eventReporter.eventStore.count == 2).to(beTrue()) - expect(testContext.eventReporter.eventStoreKeys.filter { eventKey in - eventKey == testContext.flagKey - }.count == 2).to(beTrue()) - expect(testContext.eventReporter.eventStoreKinds).to(contain([.feature, .debug])) + let features = testContext.eventReporter.eventStore.compactMap { $0 as? FeatureEvent } + expect(features.allSatisfy { $0.key == testContext.flagKey }).to(beTrue()) + expect(features.map { $0.kind }).to(contain([.feature, .debug])) } summarizesRequest() } @@ -671,10 +667,9 @@ final class EventReporterSpec: QuickSpec { } it("records a feature and debug event") { expect(testContext.eventReporter.eventStore.count == 2).to(beTrue()) - expect(testContext.eventReporter.eventStoreKeys.filter { eventKey in - eventKey == testContext.flagKey - }.count == 2).to(beTrue()) - expect(testContext.eventReporter.eventStoreKinds).to(contain([.feature, .debug])) + let features = testContext.eventReporter.eventStore.compactMap { $0 as? FeatureEvent } + expect(features.allSatisfy { $0.key == testContext.flagKey }).to(beTrue()) + expect(features.map { $0.kind }).to(contain([.feature, .debug])) } summarizesRequest() } @@ -834,7 +829,6 @@ final class EventReporterSpec: QuickSpec { } extension EventReporter { - var eventStoreKeys: [String] { eventStore.compactMap { $0.key } } var eventStoreKinds: [Event.Kind] { eventStore.compactMap { $0.kind } } } From f537121e5f9aab732f400c1bfcbbf896fb3a1e0a Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Wed, 16 Mar 2022 10:38:35 -0500 Subject: [PATCH 37/50] Clean up EventReporterSpec. --- .../ServiceObjects/EventReporter.swift | 2 +- .../LaunchDarklyTests/Models/EventSpec.swift | 2 +- .../ServiceObjects/EventReporterSpec.swift | 451 ++++++------------ 3 files changed, 140 insertions(+), 315 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index 8368118d..e2e5ed73 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -75,7 +75,7 @@ class EventReporter: EventReporting { let featureEvent = FeatureEvent(key: flagKey, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: false) recordNoSync(featureEvent) } - if recordingDebugEvent, let featureFlag = featureFlag { + if recordingDebugEvent { let debugEvent = FeatureEvent(key: flagKey, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: true) recordNoSync(debugEvent) } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index efe5b400..2e04b9ed 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -202,7 +202,7 @@ final class EventSpec: XCTestCase { func testFeatureEventEncodingAnonContextKind() { let user = LDUser() [false, true].forEach { isDebug in - let event = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: nil, includeReason: false, isDebug: isDebug) + let event = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: nil, includeReason: true, isDebug: isDebug) encodesToObject(event) { dict in XCTAssertEqual(dict.count, isDebug ? 6 : 7) XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index 4c7d9a05..c2529480 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -8,7 +8,6 @@ final class EventReporterSpec: QuickSpec { struct Constants { static let eventFlushInterval: TimeInterval = 10.0 static let eventFlushIntervalHalfSecond: TimeInterval = 0.5 - static let defaultValue: LDValue = false } struct TestContext { @@ -18,12 +17,7 @@ final class EventReporterSpec: QuickSpec { var serviceMock: DarklyServiceMock! var events: [Event] = [] var lastEventResponseDate: Date? - var flagKey: LDFlagKey! - var featureFlag: FeatureFlag! - var featureFlagWithReason: FeatureFlag! - var featureFlagWithReasonAndTrackReason: FeatureFlag! var eventStubResponseDate: Date? - var flagRequestTracker: FlagRequestTracker? { eventReporter.flagRequestTracker } var syncResult: SynchronizingError? = nil var diagnosticCache: DiagnosticCachingMock @@ -34,8 +28,6 @@ final class EventReporterSpec: QuickSpec { stubResponseOnly: Bool = false, stubResponseErrorOnly: Bool = false, eventStubResponseDate: Date? = nil, - trackEvents: Bool? = true, - debugEventsUntilDate: Date? = nil, onSyncComplete: EventSyncCompleteClosure? = nil) { config = LDConfig.stub @@ -60,11 +52,6 @@ final class EventReporterSpec: QuickSpec { eventReporter.record(event) } eventReporter.setLastEventResponseDate(self.lastEventResponseDate) - - flagKey = UUID().uuidString - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool, trackEvents: trackEvents, debugEventsUntilDate: debugEventsUntilDate) - featureFlagWithReason = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool, trackEvents: trackEvents, debugEventsUntilDate: debugEventsUntilDate, includeEvaluationReason: true) - featureFlagWithReasonAndTrackReason = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool, trackEvents: trackEvents, debugEventsUntilDate: debugEventsUntilDate, includeEvaluationReason: true, includeTrackReason: true) } mutating func recordEvents(_ eventCount: Int) { @@ -74,21 +61,13 @@ final class EventReporterSpec: QuickSpec { eventReporter.record(event) } } - - func flagCounter(for key: LDFlagKey) -> FlagCounter? { - flagRequestTracker?.flagCounters[key] - } - - func flagValueCounter(for key: LDFlagKey, and featureFlag: FeatureFlag?) -> CounterValue? { - flagCounter(for: key)?.flagValueCounters[CounterKey(variation: featureFlag?.variation, version: featureFlag?.versionForEvents)] - } } override func spec() { initSpec() isOnlineSpec() recordEventSpec() - recordFlagEvaluationEventsSpec() + testRecordFlagEvaluationEvents() reportEventsSpec() reportTimerSpec() } @@ -234,6 +213,10 @@ final class EventReporterSpec: QuickSpec { afterEach { testContext.eventReporter.isOnline = false } + let erOnline = { + expect(testContext.eventReporter.isOnline) == true + expect(testContext.eventReporter.isReportingActive) == true + } context("online") { context("success") { context("with events and tracked requests") { @@ -250,8 +233,7 @@ final class EventReporterSpec: QuickSpec { } } it("reports events and a summary event") { - expect(testContext.eventReporter.isOnline) == true - expect(testContext.eventReporter.isReportingActive) == true + erOnline() expect(testContext.serviceMock.publishEventDataCallCount) == 1 let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) valueIsArray(published) { valueArray in @@ -282,13 +264,12 @@ final class EventReporterSpec: QuickSpec { } } it("reports events without a summary event") { - expect(testContext.eventReporter.isOnline) == true - expect(testContext.eventReporter.isReportingActive) == true + erOnline() expect(testContext.serviceMock.publishEventDataCallCount) == 1 let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) expect(published) == encodeToLDValue(testContext.events) expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 1 - expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == Event.Kind.nonSummaryKinds.count + expect(testContext.diagnosticCache.recordEventsInLastBatchReceivedEventsInLastBatch) == testContext.events.count expect(testContext.eventReporter.eventStore.isEmpty) == true expect(testContext.eventReporter.lastEventResponseDate) == testContext.eventStubResponseDate expect(testContext.eventReporter.flagRequestTracker.hasLoggedRequests) == false @@ -308,8 +289,7 @@ final class EventReporterSpec: QuickSpec { } } it("reports only a summary event") { - expect(testContext.eventReporter.isOnline) == true - expect(testContext.eventReporter.isReportingActive) == true + erOnline() expect(testContext.serviceMock.publishEventDataCallCount) == 1 let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) valueIsArray(published) { valueArray in @@ -338,8 +318,7 @@ final class EventReporterSpec: QuickSpec { } } it("does not report events") { - expect(testContext.eventReporter.isOnline) == true - expect(testContext.eventReporter.isReportingActive) == true + erOnline() expect(testContext.serviceMock.publishEventDataCallCount) == 0 expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 0 expect(testContext.eventReporter.eventStore.isEmpty) == true @@ -364,8 +343,7 @@ final class EventReporterSpec: QuickSpec { } } it("drops events after the failure") { - expect(testContext.eventReporter.isOnline) == true - expect(testContext.eventReporter.isReportingActive) == true + erOnline() expect(testContext.serviceMock.publishEventDataCallCount) == 2 // 1 retry attempt expect(testContext.eventReporter.eventStore.isEmpty) == true let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) @@ -402,8 +380,7 @@ final class EventReporterSpec: QuickSpec { } } it("drops events after the failure") { - expect(testContext.eventReporter.isOnline) == true - expect(testContext.eventReporter.isReportingActive) == true + erOnline() expect(testContext.serviceMock.publishEventDataCallCount) == 2 // 1 retry attempt expect(testContext.eventReporter.eventStore.isEmpty) == true let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) @@ -443,8 +420,7 @@ final class EventReporterSpec: QuickSpec { } } it("drops events events after the failure") { - expect(testContext.eventReporter.isOnline) == true - expect(testContext.eventReporter.isReportingActive) == true + erOnline() expect(testContext.serviceMock.publishEventDataCallCount) == 2 // 1 retry attempt expect(testContext.eventReporter.eventStore.isEmpty) == true let published = try JSONDecoder().decode(LDValue.self, from: testContext.serviceMock.publishedEventData!) @@ -499,289 +475,146 @@ final class EventReporterSpec: QuickSpec { } } - private func recordFlagEvaluationEventsSpec() { + func testRecordFlagEvaluationEvents() { + let user = LDUser() + let serviceMock = DarklyServiceMock() describe("recordFlagEvaluationEvents") { - recordFeatureAndDebugEventsSpec() - trackFlagRequestSpec() - } - } - - private func recordFeatureAndDebugEventsSpec() { - var testContext: TestContext! - let summarizesRequest = { it("summarizes the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter?.value) == LDValue.fromAny(testContext.featureFlag.value) - expect(flagValueCounter?.count) == 1 - }} - context("record feature and debug events") { - context("when trackEvents is on and a reason is present") { - beforeEach { - testContext = TestContext(trackEvents: true) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlagWithReason, - user: testContext.user, - includeReason: true) - } - it("records a feature event") { - expect(testContext.eventReporter.eventStore.count) == 1 - expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.kind) == .feature - expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.key) == testContext.flagKey - } - summarizesRequest() + it("unknown flag") { + let reporter = EventReporter(service: serviceMock, onSyncComplete: nil) + reporter.recordFlagEvaluationEvents(flagKey: "flag-key", value: "a", defaultValue: "b", featureFlag: nil, user: user, includeReason: true) + expect(reporter.eventStore.count) == 0 + expect(reporter.flagRequestTracker.hasLoggedRequests) == true + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.defaultValue) == "b" + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: nil, version: nil)]?.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: nil, version: nil)]?.value) == "a" } - context("when a reason is present and reason is false but trackReason is true") { - beforeEach { - testContext = TestContext(trackEvents: true) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlagWithReasonAndTrackReason, - user: testContext.user, - includeReason: false) - } - it("records a feature event") { - expect(testContext.eventReporter.eventStore.count) == 1 - expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.kind) == .feature - expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.key) == testContext.flagKey - } - summarizesRequest() - } - context("when trackEvents is off") { - beforeEach { - testContext = TestContext(trackEvents: false) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - } - it("does not record a feature event") { - expect(testContext.eventReporter.eventStore).to(beEmpty()) - } - summarizesRequest() + it("untracked flag") { + let reporter = EventReporter(service: serviceMock, onSyncComplete: nil) + let flag = FeatureFlag(flagKey: "unused", value: nil, variation: 1, flagVersion: 2, trackEvents: false) + reporter.recordFlagEvaluationEvents(flagKey: "flag-key", value: "a", defaultValue: "b", featureFlag: flag, user: user, includeReason: true) + expect(reporter.eventStore.count) == 0 + expect(reporter.flagRequestTracker.hasLoggedRequests) == true + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.defaultValue) == "b" + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.value) == "a" } - context("when debugEventsUntilDate exists") { - context("lastEventResponseDate exists") { - context("and debugEventsUntilDate is later") { - beforeEach { - testContext = TestContext(lastEventResponseDate: Date(), trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - } - it("records a debug event") { - expect(testContext.eventReporter.eventStore.count) == 1 - expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.kind) == .debug - expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.key) == testContext.flagKey - } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter?.value) == LDValue.fromAny(testContext.featureFlag.value) - expect(flagValueCounter?.count) == 1 - } - } - context("and debugEventsUntilDate is earlier") { - beforeEach { - testContext = TestContext(lastEventResponseDate: Date(), trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(-TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - } - it("does not record a debug event") { - expect(testContext.eventReporter.eventStore).to(beEmpty()) - } - summarizesRequest() - } - } - context("lastEventResponseDate is nil") { - context("and debugEventsUntilDate is later than current time") { - beforeEach { - testContext = TestContext(lastEventResponseDate: nil, trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - } - it("records a debug event") { - expect(testContext.eventReporter.eventStore.count) == 1 - expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.kind) == .debug - expect((testContext.eventReporter.eventStore[0] as? FeatureEvent)?.key) == testContext.flagKey - } - summarizesRequest() - } - context("and debugEventsUntilDate is earlier than current time") { - beforeEach { - testContext = TestContext(lastEventResponseDate: nil, trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(-TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - } - it("does not record a debug event") { - expect(testContext.eventReporter.eventStore).to(beEmpty()) - } - summarizesRequest() - } - } + it("tracked flag") { + let reporter = EventReporter(service: serviceMock, onSyncComplete: nil) + let flag = FeatureFlag(flagKey: "unused", value: nil, variation: 1, flagVersion: 2, trackEvents: true) + reporter.recordFlagEvaluationEvents(flagKey: "flag-key", value: "a", defaultValue: "b", featureFlag: flag, user: user, includeReason: true) + expect(reporter.eventStore.count) == 1 + expect((reporter.eventStore[0] as? FeatureEvent)?.kind) == .feature + expect((reporter.eventStore[0] as? FeatureEvent)?.key) == "flag-key" + expect((reporter.eventStore[0] as? FeatureEvent)?.user) == user + expect((reporter.eventStore[0] as? FeatureEvent)?.value) == "a" + expect((reporter.eventStore[0] as? FeatureEvent)?.defaultValue) == "b" + expect((reporter.eventStore[0] as? FeatureEvent)?.featureFlag) == flag + expect((reporter.eventStore[0] as? FeatureEvent)?.includeReason) == true + expect(reporter.flagRequestTracker.hasLoggedRequests) == true + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.defaultValue) == "b" + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.value) == "a" } - context("when both trackEvents is true and debugEventsUntilDate is later than lastEventResponseDate") { - beforeEach { - testContext = TestContext(lastEventResponseDate: Date(), trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - } - it("records a feature and debug event") { - expect(testContext.eventReporter.eventStore.count == 2).to(beTrue()) - let features = testContext.eventReporter.eventStore.compactMap { $0 as? FeatureEvent } - expect(features.allSatisfy { $0.key == testContext.flagKey }).to(beTrue()) - expect(features.map { $0.kind }).to(contain([.feature, .debug])) - } - summarizesRequest() + it("debug until past date") { + let reporter = EventReporter(service: serviceMock, onSyncComplete: nil) + let flag = FeatureFlag(flagKey: "unused", value: nil, variation: 1, flagVersion: 2, trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(-1.0)) + reporter.recordFlagEvaluationEvents(flagKey: "flag-key", value: "a", defaultValue: "b", featureFlag: flag, user: user, includeReason: true) + expect(reporter.eventStore.count) == 0 + expect(reporter.flagRequestTracker.hasLoggedRequests) == true + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.defaultValue) == "b" + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.value) == "a" } - context("when both trackEvents is true, debugEventsUntilDate is later than lastEventResponseDate, reason is false, and track reason is true") { - beforeEach { - testContext = TestContext(lastEventResponseDate: Date(), trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(TimeInterval.oneSecond)) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlagWithReasonAndTrackReason, - user: testContext.user, - includeReason: false) - } - it("records a feature and debug event") { - expect(testContext.eventReporter.eventStore.count == 2).to(beTrue()) - let features = testContext.eventReporter.eventStore.compactMap { $0 as? FeatureEvent } - expect(features.allSatisfy { $0.key == testContext.flagKey }).to(beTrue()) - expect(features.map { $0.kind }).to(contain([.feature, .debug])) - } - summarizesRequest() + it("debug until future date") { + let reporter = EventReporter(service: serviceMock, onSyncComplete: nil) + let flag = FeatureFlag(flagKey: "unused", value: nil, variation: 1, flagVersion: 2, trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(3.0)) + reporter.recordFlagEvaluationEvents(flagKey: "flag-key", value: "a", defaultValue: "b", featureFlag: flag, user: user, includeReason: false) + expect(reporter.eventStore.count) == 1 + expect((reporter.eventStore[0] as? FeatureEvent)?.kind) == .debug + expect((reporter.eventStore[0] as? FeatureEvent)?.key) == "flag-key" + expect((reporter.eventStore[0] as? FeatureEvent)?.user) == user + expect((reporter.eventStore[0] as? FeatureEvent)?.value) == "a" + expect((reporter.eventStore[0] as? FeatureEvent)?.defaultValue) == "b" + expect((reporter.eventStore[0] as? FeatureEvent)?.featureFlag) == flag + expect((reporter.eventStore[0] as? FeatureEvent)?.includeReason) == false + expect(reporter.flagRequestTracker.hasLoggedRequests) == true + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.defaultValue) == "b" + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.value) == "a" } - context("when debugEventsUntilDate is nil") { - beforeEach { - testContext = TestContext(lastEventResponseDate: Date(), trackEvents: false, debugEventsUntilDate: nil) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - } - it("does not record an event") { - expect(testContext.eventReporter.eventStore).to(beEmpty()) - } - summarizesRequest() + it("debug until future date earlier than service date") { + let reporter = EventReporter(service: serviceMock, onSyncComplete: nil) + reporter.setLastEventResponseDate(Date().addingTimeInterval(10.0)) + let flag = FeatureFlag(flagKey: "unused", value: nil, variation: 1, flagVersion: 2, trackEvents: false, debugEventsUntilDate: Date().addingTimeInterval(3.0)) + reporter.recordFlagEvaluationEvents(flagKey: "flag-key", value: "a", defaultValue: "b", featureFlag: flag, user: user, includeReason: true) + expect(reporter.eventStore.count) == 0 + expect(reporter.flagRequestTracker.hasLoggedRequests) == true + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.defaultValue) == "b" + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.value) == "a" } - context("when eventTrackingContext is nil") { - beforeEach { - testContext = TestContext(trackEvents: nil) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - } - it("does not record an event") { - expect(testContext.eventReporter.eventStore).to(beEmpty()) - } - summarizesRequest() + it("tracked flag and debug date in future") { + let reporter = EventReporter(service: serviceMock, onSyncComplete: nil) + reporter.setLastEventResponseDate(Date().addingTimeInterval(-3.0)) + let flag = FeatureFlag(flagKey: "unused", value: nil, variation: 1, flagVersion: 2, trackEvents: true, debugEventsUntilDate: Date()) + reporter.recordFlagEvaluationEvents(flagKey: "flag-key", value: "a", defaultValue: "b", featureFlag: flag, user: user, includeReason: false) + expect(reporter.eventStore.count) == 2 + let featureEvent = reporter.eventStore.first { $0.kind == .feature } as? FeatureEvent + let debugEvent = reporter.eventStore.first { $0.kind == .debug } as? FeatureEvent + expect(featureEvent?.kind) == .feature + expect(featureEvent?.key) == "flag-key" + expect(featureEvent?.user) == user + expect(featureEvent?.value) == "a" + expect(featureEvent?.defaultValue) == "b" + expect(featureEvent?.featureFlag) == flag + expect(featureEvent?.includeReason) == false + expect(debugEvent?.kind) == .debug + expect(debugEvent?.key) == "flag-key" + expect(debugEvent?.user) == user + expect(debugEvent?.value) == "a" + expect(debugEvent?.defaultValue) == "b" + expect(debugEvent?.featureFlag) == flag + expect(debugEvent?.includeReason) == false + expect(reporter.flagRequestTracker.hasLoggedRequests) == true + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.defaultValue) == "b" + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: 1, version: 2)]?.value) == "a" } - context("when multiple flag requests are made") { - context("serially") { - beforeEach { - testContext = TestContext(trackEvents: false) - for _ in 1...3 { - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - } - } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter?.value) == LDValue.fromAny(testContext.featureFlag.value) - expect(flagValueCounter?.count) == 3 - } - } - context("concurrently") { - let requestQueue = DispatchQueue(label: "com.launchdarkly.test.eventReporterSpec.flagRequestTracking.concurrent", qos: .userInitiated, attributes: .concurrent) + it("records events concurrently") { + let reporter = EventReporter(service: serviceMock, onSyncComplete: nil) + reporter.setLastEventResponseDate(Date()) + let flag = FeatureFlag(flagKey: "unused", trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(3.0)) + waitUntil { done in var recordFlagEvaluationCompletionCallCount = 0 - var recordFlagEvaluationCompletion: (() -> Void)! - beforeEach { - testContext = TestContext(trackEvents: false) - - waitUntil { done in - recordFlagEvaluationCompletion = { - DispatchQueue.main.async { - recordFlagEvaluationCompletionCallCount += 1 - if recordFlagEvaluationCompletionCallCount == 5 { - done() - } - } - } - let fireTime = DispatchTime.now() + 0.1 - for _ in 1...5 { - requestQueue.asyncAfter(deadline: fireTime) { - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - recordFlagEvaluationCompletion() - } + let recordFlagEvaluationCompletion = { + DispatchQueue.main.async { + recordFlagEvaluationCompletionCallCount += 1 + if recordFlagEvaluationCompletionCallCount == 10 { + done() } } } - it("tracks the flag request") { - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter?.value) == LDValue.fromAny(testContext.featureFlag.value) - expect(flagValueCounter?.count) == 5 + DispatchQueue.concurrentPerform(iterations: 10) { _ in + reporter.recordFlagEvaluationEvents(flagKey: "flag-key", value: "a", defaultValue: "b", featureFlag: flag, user: user, includeReason: false) + recordFlagEvaluationCompletion() } } - } - } - } - private func trackFlagRequestSpec() { - context("record summary event") { - var testContext: TestContext! - beforeEach { - testContext = TestContext() - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: LDValue.fromAny(testContext.featureFlag.value), - defaultValue: LDValue.fromAny(testContext.featureFlag.value), - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - } - it("tracks flag requests") { - let flagCounter = testContext.flagCounter(for: testContext.flagKey) - expect(flagCounter?.defaultValue) == LDValue.fromAny(testContext.featureFlag.value) - expect(flagCounter?.flagValueCounters.count) == 1 - - let flagValueCounter = testContext.flagValueCounter(for: testContext.flagKey, and: testContext.featureFlag) - expect(flagValueCounter?.value) == LDValue.fromAny(testContext.featureFlag.value) - expect(flagValueCounter?.count) == 1 + expect(reporter.eventStore.count) == 20 + expect(reporter.eventStore.filter { $0.kind == .feature }.count) == 10 + expect(reporter.eventStore.filter { $0.kind == .debug }.count) == 10 + expect(reporter.flagRequestTracker.hasLoggedRequests) == true + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.defaultValue) == "b" + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters.count) == 1 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: nil, version: nil)]?.count) == 10 + expect(reporter.flagRequestTracker.flagCounters["flag-key"]?.flagValueCounters[CounterKey(variation: nil, version: nil)]?.value) == "a" } } } @@ -828,14 +661,6 @@ final class EventReporterSpec: QuickSpec { } } -extension EventReporter { - var eventStoreKinds: [Event.Kind] { eventStore.compactMap { $0.kind } } -} - -extension TimeInterval { - static let oneSecond: TimeInterval = 1.0 -} - private extension Date { var adjustedForHttpUrlHeaderUse: Date { let headerDateFormatter = DateFormatter.httpUrlHeaderFormatter From 1aceb27c66efe4f368e9c64804b2c837f1ef2fcc Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 17 Mar 2022 23:59:14 -0500 Subject: [PATCH 38/50] Use LDValue for LDChangedFlag. --- .../FlagChange/LDChangedFlag.swift | 15 ++++++---- .../ObjectiveC/ObjcLDChangedFlag.swift | 28 +++++++++---------- .../ServiceObjects/FlagChangeNotifier.swift | 2 +- .../FlagChangeNotifierSpec.swift | 20 ++++++------- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift index 581c0790..cd75b60d 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift @@ -1,17 +1,20 @@ import Foundation /** - Collects the elements of a feature flag that changed as a result of a `clientstream` update or feature flag request. The SDK will pass a LDChangedFlag or a collection of LDChangedFlags into feature flag observer closures. The client app will have to convert the old/newValue into the expected type. See `LDClient.observe(key:owner:handler:)`, `LDClient.observe(keys:owner:handler:)`, and `LDClient.observeAll(owner:handler:)` for more details. + Collects the elements of a feature flag that changed as a result of a `clientstream` update or feature flag request. + The SDK will pass a LDChangedFlag or a collection of LDChangedFlags into feature flag observer closures. See + `LDClient.observe(key:owner:handler:)`, `LDClient.observe(keys:owner:handler:)`, and + `LDClient.observeAll(owner:handler:)` for more details. */ public struct LDChangedFlag { /// The key of the changed feature flag public let key: LDFlagKey - /// The feature flag's value before the change. The client app will have to convert the oldValue into the expected type. - public let oldValue: Any? - /// The feature flag's value after the change. The client app will have to convert the newValue into the expected type. - public let newValue: Any? + /// The feature flag's value before the change. + public let oldValue: LDValue + /// The feature flag's value after the change. + public let newValue: LDValue - init(key: LDFlagKey, oldValue: Any?, newValue: Any?) { + init(key: LDFlagKey, oldValue: LDValue, newValue: LDValue) { self.key = key self.oldValue = oldValue self.newValue = newValue diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift index 56065e63..73eba2a7 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift @@ -9,7 +9,7 @@ import Foundation public class ObjcLDChangedFlag: NSObject { fileprivate let changedFlag: LDChangedFlag fileprivate var sourceValue: Any? { - changedFlag.oldValue ?? changedFlag.newValue + changedFlag.oldValue.toAny() ?? changedFlag.newValue.toAny() } /// The changed feature flag's key @@ -29,11 +29,11 @@ public class ObjcLDChangedFlag: NSObject { public final class ObjcLDBoolChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: Bool { - (changedFlag.oldValue as? Bool) ?? false + (changedFlag.oldValue.toAny() as? Bool) ?? false } /// The changed flag's value after it changed @objc public var newValue: Bool { - (changedFlag.newValue as? Bool) ?? false + (changedFlag.newValue.toAny() as? Bool) ?? false } override init(_ changedFlag: LDChangedFlag) { @@ -52,11 +52,11 @@ public final class ObjcLDBoolChangedFlag: ObjcLDChangedFlag { public final class ObjcLDIntegerChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: Int { - (changedFlag.oldValue as? Int) ?? 0 + (changedFlag.oldValue.toAny() as? Int) ?? 0 } /// The changed flag's value after it changed @objc public var newValue: Int { - (changedFlag.newValue as? Int) ?? 0 + (changedFlag.newValue.toAny() as? Int) ?? 0 } override init(_ changedFlag: LDChangedFlag) { @@ -75,11 +75,11 @@ public final class ObjcLDIntegerChangedFlag: ObjcLDChangedFlag { public final class ObjcLDDoubleChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: Double { - (changedFlag.oldValue as? Double) ?? 0.0 + (changedFlag.oldValue.toAny() as? Double) ?? 0.0 } /// The changed flag's value after it changed @objc public var newValue: Double { - (changedFlag.newValue as? Double) ?? 0.0 + (changedFlag.newValue.toAny() as? Double) ?? 0.0 } override init(_ changedFlag: LDChangedFlag) { @@ -98,11 +98,11 @@ public final class ObjcLDDoubleChangedFlag: ObjcLDChangedFlag { public final class ObjcLDStringChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: String? { - (changedFlag.oldValue as? String) + (changedFlag.oldValue.toAny() as? String) } /// The changed flag's value after it changed @objc public var newValue: String? { - (changedFlag.newValue as? String) + (changedFlag.newValue.toAny() as? String) } override init(_ changedFlag: LDChangedFlag) { @@ -121,11 +121,11 @@ public final class ObjcLDStringChangedFlag: ObjcLDChangedFlag { public final class ObjcLDArrayChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: [Any]? { - changedFlag.oldValue as? [Any] + changedFlag.oldValue.toAny() as? [Any] } /// The changed flag's value after it changed @objc public var newValue: [Any]? { - changedFlag.newValue as? [Any] + changedFlag.newValue.toAny() as? [Any] } override init(_ changedFlag: LDChangedFlag) { @@ -144,11 +144,11 @@ public final class ObjcLDArrayChangedFlag: ObjcLDChangedFlag { public final class ObjcLDDictionaryChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: [String: Any]? { - changedFlag.oldValue as? [String: Any] + changedFlag.oldValue.toAny() as? [String: Any] } /// The changed flag's value after it changed @objc public var newValue: [String: Any]? { - changedFlag.newValue as? [String: Any] + changedFlag.newValue.toAny() as? [String: Any] } override init(_ changedFlag: LDChangedFlag) { @@ -163,7 +163,7 @@ public final class ObjcLDDictionaryChangedFlag: ObjcLDChangedFlag { public extension LDChangedFlag { /// An NSObject wrapper for the Swift LDChangeFlag enum. Intended for use in mixed apps when Swift code needs to pass a LDChangeFlag into an Objective-C method. var objcChangedFlag: ObjcLDChangedFlag { - let extantValue = oldValue ?? newValue + let extantValue = oldValue.toAny() ?? newValue.toAny() switch extantValue { case _ as Bool: return ObjcLDBoolChangedFlag(self) case _ as Int: return ObjcLDIntegerChangedFlag(self) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift index c06fc9eb..296c5a00 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift @@ -90,7 +90,7 @@ final class FlagChangeNotifier: FlagChangeNotifying { } let changedFlags = [LDFlagKey: LDChangedFlag](uniqueKeysWithValues: changedFlagKeys.map { - ($0, LDChangedFlag(key: $0, oldValue: oldFlags[$0]?.value, newValue: newFlags[$0]?.value)) + ($0, LDChangedFlag(key: $0, oldValue: LDValue.fromAny(oldFlags[$0]?.value), newValue: LDValue.fromAny(newFlags[$0]?.value))) }) Log.debug(typeName(and: #function) + "notifying observers for changes to flags: \(changedFlags.keys.joined(separator: ", ")).") selectedObservers.forEach { observer in diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift index 548adfea..33576b86 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift @@ -34,9 +34,9 @@ private class MockFlagCollectionChangeObserver { let observer: FlagChangeObserver var owner: LDObserverOwner? - private var tracker: CallTracker<[LDFlagKey: LDChangedFlag]>? - var callCount: Int { tracker!.callCount } - var lastCallArg: [LDFlagKey: LDChangedFlag]? { tracker!.lastCallArg } + private var tracker: CallTracker<[LDFlagKey: LDChangedFlag]> + var callCount: Int { tracker.callCount } + var lastCallArg: [LDFlagKey: LDChangedFlag]? { tracker.lastCallArg } init(_ keys: [LDFlagKey], owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { self.keys = keys @@ -54,8 +54,8 @@ private class MockFlagsUnchangedObserver { let observer: FlagsUnchangedObserver var owner: LDObserverOwner? - private var tracker: CallTracker? - var callCount: Int { tracker!.callCount } + private var tracker: CallTracker + var callCount: Int { tracker.callCount } init(owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { self.owner = owner @@ -71,9 +71,9 @@ private class MockConnectionModeChangedObserver { let observer: ConnectionModeChangedObserver var owner: LDObserverOwner? - private var tracker: CallTracker? - var callCount: Int { tracker!.callCount } - var lastCallArg: ConnectionInformation.ConnectionMode? { tracker!.lastCallArg } + private var tracker: CallTracker + var callCount: Int { tracker.callCount } + var lastCallArg: ConnectionInformation.ConnectionMode? { tracker.lastCallArg } init(owner: LDObserverOwner = FlagChangeHandlerOwnerMock()) { self.owner = owner @@ -418,8 +418,6 @@ private final class FlagChangeHandlerOwnerMock { } extension LDChangedFlag: Equatable { public static func == (lhs: LDChangedFlag, rhs: LDChangedFlag) -> Bool { - lhs.key == rhs.key - && AnyComparer.isEqual(lhs.oldValue, to: rhs.oldValue) - && AnyComparer.isEqual(lhs.newValue, to: rhs.newValue) + lhs.key == rhs.key && lhs.oldValue == rhs.oldValue && lhs.newValue == rhs.newValue } } From 782f82854c962a866fa19ab52583852628ace92f Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 18 Mar 2022 01:01:11 -0500 Subject: [PATCH 39/50] Use LDValue to simplify DiagnosticEventSpec encoding tests. --- .../Models/DiagnosticEventSpec.swift | 436 +++++++----------- .../FlagChange/FlagChangeObserverSpec.swift | 8 - 2 files changed, 158 insertions(+), 286 deletions(-) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index f04f6d9c..ece75e9e 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -6,13 +6,6 @@ import Nimble final class DiagnosticEventSpec: QuickSpec { - // We test against plist as well as JSON. Originally we were going to use plist for - // the device cache and JSON on the wire, but JSON encoding is faster and smaller - // than plist. Leaving testing against it for now. - private let testEncoders: [(String, CodingScheme)] = - [("JSON", CodingScheme(JSONEncoder(), JSONDecoder())), - ("plist", CodingScheme(PropertyListEncoder(), PropertyListDecoder()))] - override func spec() { diagnosticIdSpec() diagnosticSdkSpec() @@ -51,56 +44,58 @@ final class DiagnosticEventSpec: QuickSpec { } } context("DiagnosticId encoding") { - for (desc, scheme) in testEncoders { - context("using \(desc) encoding") { - it("encodes correct values to keys") { - let uuid = UUID().uuidString - let diagnosticId = DiagnosticId(diagnosticId: uuid, sdkKey: "this_is_a_fake_key") - let decoded = self.loadAndRestoreRaw(scheme, diagnosticId) - expect(decoded.count) == 2 - expect((decoded["diagnosticId"] as! String)) == uuid - expect((decoded["sdkKeySuffix"] as! String)) == "ke_key" - } - it("can load and restore through codable protocol") { - let uuid = UUID().uuidString - let diagnosticId = DiagnosticId(diagnosticId: uuid, sdkKey: "this_is_a_fake_key") - let decoded = self.loadAndRestore(scheme, diagnosticId) - expect(decoded?.diagnosticId) == diagnosticId.diagnosticId - expect(decoded?.sdkKeySuffix) == diagnosticId.sdkKeySuffix - } + it("encodes correct values to keys") { + let uuid = UUID().uuidString + let diagnosticId = DiagnosticId(diagnosticId: uuid, sdkKey: "this_is_a_fake_key") + encodesToObject(diagnosticId) { decoded in + expect(decoded.count) == 2 + expect(decoded["diagnosticId"]) == .string(uuid) + expect(decoded["sdkKeySuffix"]) == "ke_key" } } + it("can load and restore through codable protocol") { + let uuid = UUID().uuidString + let diagnosticId = DiagnosticId(diagnosticId: uuid, sdkKey: "this_is_a_fake_key") + let decoded = self.loadAndRestore(diagnosticId) + expect(decoded?.diagnosticId) == diagnosticId.diagnosticId + expect(decoded?.sdkKeySuffix) == diagnosticId.sdkKeySuffix + } } } private func diagnosticSdkSpec() { context("DiagnosticSdk") { - let defaultConfig = LDConfig.stub - var wrapperConfig = LDConfig.stub - wrapperConfig.wrapperName = "ReactNative" - wrapperConfig.wrapperVersion = "0.1.0" - for (title, config) in [("defaults", defaultConfig), ("wrapper set", wrapperConfig)] { - context("with \(title)") { - it("inits with correct values") { - let diagnosticSdk = DiagnosticSdk(config: config) - expect(diagnosticSdk.name) == "ios-client-sdk" - expect(diagnosticSdk.version) == config.environmentReporter.sdkVersion - expect(diagnosticSdk.wrapperName == config.wrapperName) == true - expect(diagnosticSdk.wrapperVersion == config.wrapperVersion) == true + context("without wrapper configured") { + it("has correct values and encoding") { + let config = LDConfig.stub + let diagnosticSdk = DiagnosticSdk(config: config) + expect(diagnosticSdk.name) == "ios-client-sdk" + expect(diagnosticSdk.version) == config.environmentReporter.sdkVersion + expect(diagnosticSdk.wrapperName).to(beNil()) + expect(diagnosticSdk.wrapperVersion).to(beNil()) + encodesToObject(diagnosticSdk) { decoded in + expect(decoded.count) == 2 + expect(decoded["name"]) == "ios-client-sdk" + expect(decoded["version"]) == .string(config.environmentReporter.sdkVersion) } - for (desc, scheme) in testEncoders { - context("using \(desc) encoding") { - it("encodes correct values to keys") { - let diagnosticSdk = DiagnosticSdk(config: config) - let decoded = self.loadAndRestoreRaw(scheme, diagnosticSdk) - expect((decoded["name"] as! String)) == "ios-client-sdk" - expect((decoded["version"] as! String)) == config.environmentReporter.sdkVersion - expect((decoded["wrapperName"] as! String?) == config.wrapperName) == true - expect((decoded["wrapperVersion"] as! String?) == config.wrapperVersion) == true - let expectedKeys = ["name", "version", "wrapperName", "wrapperVersion"] - expect(decoded.keys.allSatisfy(expectedKeys.contains)) == true - } - } + } + } + context("with wrapper configured") { + it("has correct values and encoding") { + var config = LDConfig.stub + config.wrapperName = "ReactNative" + config.wrapperVersion = "0.1.0" + let diagnosticSdk = DiagnosticSdk(config: config) + expect(diagnosticSdk.name) == "ios-client-sdk" + expect(diagnosticSdk.version) == config.environmentReporter.sdkVersion + expect(diagnosticSdk.wrapperName) == config.wrapperName + expect(diagnosticSdk.wrapperVersion) == config.wrapperVersion + encodesToObject(diagnosticSdk) { decoded in + expect(decoded.count) == 4 + expect(decoded["name"]) == "ios-client-sdk" + expect(decoded["version"]) == .string(config.environmentReporter.sdkVersion) + expect(decoded["wrapperName"]) == "ReactNative" + expect(decoded["wrapperVersion"]) == "0.1.0" } } } @@ -127,18 +122,15 @@ final class DiagnosticEventSpec: QuickSpec { expect(diagnosticPlatform.streamingEnabled) == environmentReporter.operatingSystem.isStreamingEnabled expect(diagnosticPlatform.deviceType) == environmentReporter.deviceType } - for (desc, scheme) in testEncoders { - context("using \(desc) encoding") { - it("encodes correct values to keys") { - let decoded = self.loadAndRestoreRaw(scheme, diagnosticPlatform) - expect(decoded.count) == 6 - expect((decoded["name"] as! String)) == diagnosticPlatform.name - expect((decoded["systemName"] as! String)) == diagnosticPlatform.systemName - expect((decoded["systemVersion"] as! String)) == diagnosticPlatform.systemVersion - expect((decoded["backgroundEnabled"] as! Bool)) == diagnosticPlatform.backgroundEnabled - expect((decoded["streamingEnabled"] as! Bool)) == diagnosticPlatform.streamingEnabled - expect((decoded["deviceType"] as! String)) == diagnosticPlatform.deviceType - } + it("encodes correct values to keys") { + encodesToObject(diagnosticPlatform) { decoded in + expect(decoded.count) == 6 + expect(decoded["name"]) == .string(diagnosticPlatform.name) + expect(decoded["systemName"]) == .string(diagnosticPlatform.systemName) + expect(decoded["systemVersion"]) == .string(diagnosticPlatform.systemVersion) + expect(decoded["backgroundEnabled"]) == .bool(diagnosticPlatform.backgroundEnabled) + expect(decoded["streamingEnabled"]) == .bool(diagnosticPlatform.streamingEnabled) + expect(decoded["deviceType"]) == .string(diagnosticPlatform.deviceType) } } } @@ -157,23 +149,20 @@ final class DiagnosticEventSpec: QuickSpec { for streamInit in [DiagnosticStreamInit(timestamp: 1000, durationMillis: 100, failed: true), DiagnosticStreamInit(timestamp: Date().millisSince1970, durationMillis: 0, failed: false)] { context("for init \(String(describing: streamInit))") { - for (desc, scheme) in testEncoders { - context("using \(desc) encoding") { - it("encodes correct values to keys") { - let decoded = self.loadAndRestoreRaw(scheme, streamInit) - expect(decoded.count) == 3 - expect((decoded["timestamp"] as! Int64)) == streamInit.timestamp - expect((decoded["durationMillis"] as! Int64)) == Int64(streamInit.durationMillis) - expect((decoded["failed"] as! Bool) == streamInit.failed) == true - } - it("can load and restore through codable protocol") { - let decoded = self.loadAndRestore(scheme, streamInit) - expect(decoded?.timestamp) == streamInit.timestamp - expect(decoded?.durationMillis) == streamInit.durationMillis - expect(decoded?.failed) == streamInit.failed - } + it("encodes correct values to keys") { + encodesToObject(streamInit) { decoded in + expect(decoded.count) == 3 + expect(decoded["timestamp"]) == .number(Double(streamInit.timestamp)) + expect(decoded["durationMillis"]) == .number(Double(streamInit.durationMillis)) + expect(decoded["failed"]) == .bool(streamInit.failed) } } + it("can load and restore through codable protocol") { + let decoded = self.loadAndRestore(streamInit) + expect(decoded?.timestamp) == streamInit.timestamp + expect(decoded?.durationMillis) == streamInit.durationMillis + expect(decoded?.failed) == streamInit.failed + } } } } @@ -280,52 +269,49 @@ final class DiagnosticEventSpec: QuickSpec { beforeEach { diagnosticConfig = DiagnosticConfig(config: config) } - for (desc, scheme) in testEncoders { - context("using \(desc) encoding") { - it("encodes correct values to keys") { - let decoded = self.loadAndRestoreRaw(scheme, diagnosticConfig) - expect(decoded.count) == 19 - expect((decoded["autoAliasingOptOut"] as! Bool)) == diagnosticConfig.autoAliasingOptOut - expect((decoded["customBaseURI"] as! Bool)) == diagnosticConfig.customBaseURI - expect((decoded["customEventsURI"] as! Bool)) == diagnosticConfig.customEventsURI - expect((decoded["customStreamURI"] as! Bool)) == diagnosticConfig.customStreamURI - expect((decoded["eventsCapacity"] as! Int64)) == Int64(diagnosticConfig.eventsCapacity) - expect((decoded["connectTimeoutMillis"] as! Int64)) == Int64(diagnosticConfig.connectTimeoutMillis) - expect((decoded["eventsFlushIntervalMillis"] as! Int64)) == Int64(diagnosticConfig.eventsFlushIntervalMillis) - expect((decoded["streamingDisabled"] as! Bool)) == diagnosticConfig.streamingDisabled - expect((decoded["allAttributesPrivate"] as! Bool)) == diagnosticConfig.allAttributesPrivate - expect((decoded["pollingIntervalMillis"] as! Int64)) == Int64(diagnosticConfig.pollingIntervalMillis) - expect((decoded["backgroundPollingIntervalMillis"] as! Int64)) == Int64(diagnosticConfig.backgroundPollingIntervalMillis) - expect((decoded["inlineUsersInEvents"] as! Bool)) == diagnosticConfig.inlineUsersInEvents - expect((decoded["useReport"] as! Bool)) == diagnosticConfig.useReport - expect((decoded["backgroundPollingDisabled"] as! Bool)) == diagnosticConfig.backgroundPollingDisabled - expect((decoded["evaluationReasonsRequested"] as! Bool)) == diagnosticConfig.evaluationReasonsRequested - expect((decoded["maxCachedUsers"] as! Int64)) == Int64(diagnosticConfig.maxCachedUsers) - expect((decoded["mobileKeyCount"] as! Int64)) == Int64(diagnosticConfig.mobileKeyCount) - expect((decoded["diagnosticRecordingIntervalMillis"] as! Int64)) == Int64(diagnosticConfig.diagnosticRecordingIntervalMillis) - } - it("can load and restore through codable protocol") { - let decoded = self.loadAndRestore(scheme, diagnosticConfig) - expect(decoded?.customBaseURI) == diagnosticConfig.customBaseURI - expect(decoded?.customEventsURI) == diagnosticConfig.customEventsURI - expect(decoded?.customStreamURI) == diagnosticConfig.customStreamURI - expect(decoded?.eventsCapacity) == diagnosticConfig.eventsCapacity - expect(decoded?.connectTimeoutMillis) == diagnosticConfig.connectTimeoutMillis - expect(decoded?.eventsFlushIntervalMillis) == diagnosticConfig.eventsFlushIntervalMillis - expect(decoded?.streamingDisabled) == diagnosticConfig.streamingDisabled - expect(decoded?.allAttributesPrivate) == diagnosticConfig.allAttributesPrivate - expect(decoded?.pollingIntervalMillis) == diagnosticConfig.pollingIntervalMillis - expect(decoded?.backgroundPollingIntervalMillis) == diagnosticConfig.backgroundPollingIntervalMillis - expect(decoded?.inlineUsersInEvents) == diagnosticConfig.inlineUsersInEvents - expect(decoded?.useReport) == diagnosticConfig.useReport - expect(decoded?.backgroundPollingDisabled) == diagnosticConfig.backgroundPollingDisabled - expect(decoded?.evaluationReasonsRequested) == diagnosticConfig.evaluationReasonsRequested - expect(decoded?.maxCachedUsers) == diagnosticConfig.maxCachedUsers - expect(decoded?.mobileKeyCount) == diagnosticConfig.mobileKeyCount - expect(decoded?.diagnosticRecordingIntervalMillis) == diagnosticConfig.diagnosticRecordingIntervalMillis - } + it("encodes correct values to keys") { + encodesToObject(diagnosticConfig) { decoded in + expect(decoded.count) == 19 + expect(decoded["autoAliasingOptOut"]) == .bool(diagnosticConfig.autoAliasingOptOut) + expect(decoded["customBaseURI"]) == .bool(diagnosticConfig.customBaseURI) + expect(decoded["customEventsURI"]) == .bool(diagnosticConfig.customEventsURI) + expect(decoded["customStreamURI"]) == .bool(diagnosticConfig.customStreamURI) + expect(decoded["eventsCapacity"]) == .number(Double(diagnosticConfig.eventsCapacity)) + expect(decoded["connectTimeoutMillis"]) == .number(Double(diagnosticConfig.connectTimeoutMillis)) + expect(decoded["eventsFlushIntervalMillis"]) == .number(Double(diagnosticConfig.eventsFlushIntervalMillis)) + expect(decoded["streamingDisabled"]) == .bool(diagnosticConfig.streamingDisabled) + expect(decoded["allAttributesPrivate"]) == .bool(diagnosticConfig.allAttributesPrivate) + expect(decoded["pollingIntervalMillis"]) == .number(Double(diagnosticConfig.pollingIntervalMillis)) + expect(decoded["backgroundPollingIntervalMillis"]) == .number(Double(diagnosticConfig.backgroundPollingIntervalMillis)) + expect(decoded["inlineUsersInEvents"]) == .bool(diagnosticConfig.inlineUsersInEvents) + expect(decoded["useReport"]) == .bool(diagnosticConfig.useReport) + expect(decoded["backgroundPollingDisabled"]) == .bool(diagnosticConfig.backgroundPollingDisabled) + expect(decoded["evaluationReasonsRequested"]) == .bool(diagnosticConfig.evaluationReasonsRequested) + expect(decoded["maxCachedUsers"]) == .number(Double(diagnosticConfig.maxCachedUsers)) + expect(decoded["mobileKeyCount"]) == .number(Double(diagnosticConfig.mobileKeyCount)) + expect(decoded["diagnosticRecordingIntervalMillis"]) == .number(Double(diagnosticConfig.diagnosticRecordingIntervalMillis)) } } + it("can load and restore through codable protocol") { + let decoded = self.loadAndRestore(diagnosticConfig) + expect(decoded?.customBaseURI) == diagnosticConfig.customBaseURI + expect(decoded?.customEventsURI) == diagnosticConfig.customEventsURI + expect(decoded?.customStreamURI) == diagnosticConfig.customStreamURI + expect(decoded?.eventsCapacity) == diagnosticConfig.eventsCapacity + expect(decoded?.connectTimeoutMillis) == diagnosticConfig.connectTimeoutMillis + expect(decoded?.eventsFlushIntervalMillis) == diagnosticConfig.eventsFlushIntervalMillis + expect(decoded?.streamingDisabled) == diagnosticConfig.streamingDisabled + expect(decoded?.allAttributesPrivate) == diagnosticConfig.allAttributesPrivate + expect(decoded?.pollingIntervalMillis) == diagnosticConfig.pollingIntervalMillis + expect(decoded?.backgroundPollingIntervalMillis) == diagnosticConfig.backgroundPollingIntervalMillis + expect(decoded?.inlineUsersInEvents) == diagnosticConfig.inlineUsersInEvents + expect(decoded?.useReport) == diagnosticConfig.useReport + expect(decoded?.backgroundPollingDisabled) == diagnosticConfig.backgroundPollingDisabled + expect(decoded?.evaluationReasonsRequested) == diagnosticConfig.evaluationReasonsRequested + expect(decoded?.maxCachedUsers) == diagnosticConfig.maxCachedUsers + expect(decoded?.mobileKeyCount) == diagnosticConfig.mobileKeyCount + expect(decoded?.diagnosticRecordingIntervalMillis) == diagnosticConfig.diagnosticRecordingIntervalMillis + } } } } @@ -333,26 +319,22 @@ final class DiagnosticEventSpec: QuickSpec { private func diagnosticKindSpec() { context("DiagnosticKind") { - // Cannot encode raw primitives in plist. JSONEncoder will encode raw primitives on newer platforms, but not all supported platforms. For these tests we wrap the kind in an array to allow us to test the encoding. - for (desc, scheme) in testEncoders { - context("using \(desc) encoding") { - it("encodes to correct values") { - let encodedInit = try? scheme.encode([DiagnosticKind.diagnosticInit]) - expect(encodedInit).toNot(beNil()) - let decodedInit = (try? scheme.decode(ArrayDecoder.self, from: encodedInit!))!.decoded - expect((decodedInit as! [String])[0]) == "diagnostic-init" - - let encodedStats = try? scheme.encode([DiagnosticKind.diagnosticStats]) - expect(encodedStats).toNot(beNil()) - let decodedStats = (try? scheme.decode(ArrayDecoder.self, from: encodedStats!))!.decoded - expect((decodedStats as! [String])[0]) == "diagnostic" - } - it("can load and restore through codable protocol") { - for kind in [DiagnosticKind.diagnosticInit, DiagnosticKind.diagnosticStats] { - let decoded = self.loadAndRestore(scheme, [kind]) - expect(decoded) == [kind] - } - } + // JSONEncoder will encode raw primitives on newer platforms, but not all supported platforms. For these + // tests we wrap the kind in an object to allow us to test the encoding. + it("encodes to correct values") { + encodesToObject(["abc": DiagnosticKind.diagnosticInit]) { value in + expect(value.count) == 1 + expect(value["abc"]) == "diagnostic-init" + } + encodesToObject(["abc": DiagnosticKind.diagnosticStats]) { value in + expect(value.count) == 1 + expect(value["abc"]) == "diagnostic" + } + } + it("can load and restore through codable protocol") { + for kind in [DiagnosticKind.diagnosticInit, DiagnosticKind.diagnosticStats] { + let decoded = self.loadAndRestore([kind]) + expect(decoded) == [kind] } } } @@ -379,22 +361,19 @@ final class DiagnosticEventSpec: QuickSpec { expect(diagnosticInit.configuration.customBaseURI) == true expect(diagnosticInit.platform.backgroundEnabled) == true } - for (desc, scheme) in testEncoders { - context("using \(desc) encoding") { - it("encodes correct values to keys") { - let expectedId = self.loadAndRestoreRaw(scheme, diagnosticId) - let expectedSdk = self.loadAndRestoreRaw(scheme, diagnosticInit.sdk) - let expectedConfig = self.loadAndRestoreRaw(scheme, diagnosticInit.configuration) - let expectedPlatform = self.loadAndRestoreRaw(scheme, diagnosticInit.platform) - let decoded = self.loadAndRestoreRaw(scheme, diagnosticInit) - expect(decoded.count) == 6 - expect((decoded["kind"] as! String)) == DiagnosticKind.diagnosticInit.rawValue - expect(AnyComparer.isEqual(decoded["id"], to: expectedId)) == true - expect((decoded["creationDate"] as! Int64)) == now - expect(AnyComparer.isEqual(decoded["sdk"], to: expectedSdk)) == true - expect(AnyComparer.isEqual(decoded["configuration"], to: expectedConfig)) == true - expect(AnyComparer.isEqual(decoded["platform"], to: expectedPlatform)) == true - } + it("encodes correct values to keys") { + let expectedId = encodeToLDValue(diagnosticId) + let expectedSdk = encodeToLDValue(diagnosticInit.sdk) + let expectedConfig = encodeToLDValue(diagnosticInit.configuration) + let expectedPlatform = encodeToLDValue(diagnosticInit.platform) + encodesToObject(diagnosticInit) { decoded in + expect(decoded.count) == 6 + expect(decoded["kind"]) == .string(DiagnosticKind.diagnosticInit.rawValue) + expect(decoded["id"]) == expectedId + expect(decoded["creationDate"]) == .number(Double(now)) + expect(decoded["sdk"]) == expectedSdk + expect(decoded["configuration"]) == expectedConfig + expect(decoded["platform"]) == expectedPlatform } } } @@ -411,7 +390,12 @@ final class DiagnosticEventSpec: QuickSpec { beforeEach { now = Date().millisSince1970 diagnosticId = DiagnosticId(diagnosticId: UUID().uuidString, sdkKey: "foobar") - diagnosticStats = DiagnosticStats(id: diagnosticId, creationDate: now, dataSinceDate: now - 60_000, droppedEvents: 5, eventsInLastBatch: 10, streamInits: streamInits) + diagnosticStats = DiagnosticStats(id: diagnosticId, + creationDate: now, + dataSinceDate: now - 60_000, + droppedEvents: 5, + eventsInLastBatch: 10, + streamInits: streamInits) } it("inits with correct values") { expect(diagnosticStats.kind) == DiagnosticKind.diagnosticStats @@ -426,21 +410,18 @@ final class DiagnosticEventSpec: QuickSpec { expect(diagnosticStats.streamInits[i].timestamp) == streamInits[i].timestamp } } - for (desc, scheme) in testEncoders { - context("using \(desc) encoding") { - it("encodes correct values to keys") { - let expectedId = self.loadAndRestoreRaw(scheme, diagnosticId) - let expectedInits = streamInits.map { self.loadAndRestoreRaw(scheme, $0) } - let decoded = self.loadAndRestoreRaw(scheme, diagnosticStats) - expect(decoded.count) == 7 - expect((decoded["kind"] as! String)) == DiagnosticKind.diagnosticStats.rawValue - expect(AnyComparer.isEqual(decoded["id"], to: expectedId)) == true - expect((decoded["creationDate"] as! Int64)) == now - expect((decoded["dataSinceDate"] as! Int64)) == now - 60_000 - expect((decoded["droppedEvents"] as! Int64)) == 5 - expect((decoded["eventsInLastBatch"] as! Int64)) == 10 - expect(AnyComparer.isEqual(decoded["streamInits"], to: expectedInits)) == true - } + it("encodes correct values to keys") { + let expectedId = encodeToLDValue(diagnosticId) + let expectedInits = encodeToLDValue(streamInits) + encodesToObject(diagnosticStats) { decoded in + expect(decoded.count) == 7 + expect(decoded["kind"]) == .string(DiagnosticKind.diagnosticStats.rawValue) + expect(decoded["id"]) == expectedId + expect(decoded["creationDate"]) == .number(Double(now)) + expect(decoded["dataSinceDate"]) == .number(Double(now - 60_000)) + expect(decoded["droppedEvents"]) == 5 + expect(decoded["eventsInLastBatch"]) == 10 + expect(decoded["streamInits"]) == expectedInits } } } @@ -448,112 +429,11 @@ final class DiagnosticEventSpec: QuickSpec { } } - private func loadAndRestore(_ scheme: CodingScheme, _ subject: T?) -> T? { - let encoded = try? scheme.encode(subject) - return try? scheme.decode(T.self, from: encoded!) - } + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() - private func loadAndRestoreRaw(_ scheme: CodingScheme, _ subject: T) -> [String: Any] { - let encoded = try? scheme.encode(subject) - expect(encoded).toNot(beNil()) - return (try? scheme.decode(ObjectDecoder.self, from: encoded!))!.decoded + private func loadAndRestore(_ subject: T?) -> T? { + let encoded = try? encoder.encode(subject) + return try? decoder.decode(T.self, from: encoded!) } } - -private struct DynamicKey: CodingKey { - var intValue: Int? - var stringValue: String - - init?(intValue: Int) { - self.intValue = intValue - self.stringValue = "\(intValue)" - } - - init?(stringValue: String) { - self.stringValue = stringValue - } -} - -private struct ObjectDecoder: Decodable { - let decoded: [String: Any] - - init(from decoder: Decoder) throws { - var decoded: [String: Any] = [:] - let container = try decoder.container(keyedBy: DynamicKey.self) - for key in container.allKeys { - if let prim = try? container.decode(PrimDecoder.self, forKey: key) { - decoded[key.stringValue] = prim.decoded - } else if let arr = try? container.decode(ArrayDecoder.self, forKey: key) { - decoded[key.stringValue] = arr.decoded - } else if let obj = try? container.decode(ObjectDecoder.self, forKey: key) { - decoded[key.stringValue] = obj.decoded - } - } - self.decoded = decoded - } -} - -private struct ArrayDecoder: Decodable { - let decoded: [Any] - - init(from decoder: Decoder) throws { - var decoded: [Any] = [] - var container = try decoder.unkeyedContainer() - while !container.isAtEnd { - if let prim = try? container.decode(PrimDecoder.self) { - decoded.append(prim.decoded) - } else if let arr = try? container.decode(ArrayDecoder.self) { - decoded.append(arr.decoded) - } else if let obj = try? container.decode(ObjectDecoder.self) { - decoded.append(obj.decoded) - } - } - self.decoded = decoded - } -} - -private struct PrimDecoder: Decodable { - let decoded: Any - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let i = try? container.decode(Int64.self) { - decoded = i - } else if let b = try? container.decode(Bool.self) { - decoded = b - } else { - decoded = try container.decode(String.self) - } - } -} - -private class CodingScheme: TopLevelEncoder, TopLevelDecoder { - let encoder: TopLevelEncoder - let decoder: TopLevelDecoder - - init(_ encoder: TopLevelEncoder, _ decoder: TopLevelDecoder) { - self.encoder = encoder - self.decoder = decoder - } - - func encode(_ value: T) throws -> Data where T: Encodable { - try encoder.encode(value) - } - - func decode(_ type: T.Type, from: Data) throws -> T where T: Decodable { - try decoder.decode(type, from: from) - } -} - -protocol TopLevelEncoder { - func encode(_ value: T) throws -> Data where T: Encodable -} - -protocol TopLevelDecoder { - func decode(_ type: T.Type, from: Data) throws -> T where T: Decodable -} - -extension PropertyListEncoder: TopLevelEncoder { } -extension PropertyListDecoder: TopLevelDecoder { } -extension JSONEncoder: TopLevelEncoder { } -extension JSONDecoder: TopLevelDecoder { } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagChange/FlagChangeObserverSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagChange/FlagChangeObserverSpec.swift index 477194aa..e3f6c86b 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagChange/FlagChangeObserverSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagChange/FlagChangeObserverSpec.swift @@ -39,11 +39,3 @@ final class FlagChangeObserverSpec: XCTestCase { XCTAssertEqual(ownerMock.changedCollectionCount, 1) } } - -extension FlagChangeObserver: Equatable { - public static func == (lhs: FlagChangeObserver, rhs: FlagChangeObserver) -> Bool { - lhs.flagKeys == rhs.flagKeys && lhs.owner === rhs.owner && - ((lhs.flagChangeHandler == nil && rhs.flagChangeHandler == nil) || - (lhs.flagCollectionChangeHandler == nil && rhs.flagCollectionChangeHandler == nil)) - } -} From 0b7539cfafc2266313c8fd6d18fbd1d8c1fa63f7 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 18 Mar 2022 01:05:17 -0500 Subject: [PATCH 40/50] Remove unused import. --- LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index ece75e9e..005e882a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -1,5 +1,4 @@ import Foundation -import XCTest import Quick import Nimble @testable import LaunchDarkly From 95d900587c3c221d93b596a6c9546d34b83dd9d0 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 18 Mar 2022 01:27:04 -0500 Subject: [PATCH 41/50] Remove unused LDUser code. --- LaunchDarkly/LaunchDarkly/LDClient.swift | 2 +- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 60 ----- .../Cache/DeprecatedCache.swift | 8 - .../ServiceObjects/FlagStore.swift | 4 - .../Models/User/LDUserSpec.swift | 252 ------------------ .../Networking/DarklyServiceSpec.swift | 22 +- .../Cache/DeprecatedCacheModelV5Spec.swift | 6 +- 7 files changed, 16 insertions(+), 338 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 6fb8d06d..4dcab792 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -329,7 +329,7 @@ public class LDClient { public var allFlags: [LDFlagKey: Any]? { guard hasStarted else { return nil } - return flagStore.featureFlags.allFlagValues + return flagStore.featureFlags.compactMapValues { $0.value } } // MARK: Observing Updates diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 75420154..10257a42 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -118,46 +118,6 @@ public struct LDUser: Encodable { return custom[attribute.name] } - /// Dictionary with LDUser attribute keys and values, with options to include feature flags and private attributes. LDConfig object used to help resolving what attributes should be private. - /// - parameter includePrivateAttributes: Controls whether the resulting dictionary includes private attributes - /// - parameter config: Provides supporting information for defining private attributes - func dictionaryValue(includePrivateAttributes includePrivate: Bool, config: LDConfig) -> [String: Any] { - let allPrivate = !includePrivate && config.allUserAttributesPrivate - let privateAttributeNames = includePrivate ? [] : (privateAttributes + config.privateUserAttributes).map { $0.name } - - var dictionary: [String: Any] = [:] - var redactedAttributes: [String] = [] - - dictionary[CodingKeys.key.rawValue] = key - dictionary[CodingKeys.isAnonymous.rawValue] = isAnonymous - - LDUser.optionalAttributes.forEach { attribute in - if let value = self.value(for: attribute) { - if allPrivate || privateAttributeNames.contains(attribute.name) { - redactedAttributes.append(attribute.name) - } else { - dictionary[attribute.name] = value - } - } - } - - var customDictionary: [String: Any] = [:] - custom.forEach { attrName, attrVal in - if allPrivate || privateAttributeNames.contains(attrName) { - redactedAttributes.append(attrName) - } else { - customDictionary[attrName] = attrVal.toAny() - } - } - dictionary[CodingKeys.custom.rawValue] = customDictionary.isEmpty ? nil : customDictionary - - if !redactedAttributes.isEmpty { - dictionary[CodingKeys.privateAttributes.rawValue] = Set(redactedAttributes).sorted() - } - - return dictionary - } - struct UserInfoKeys { static let includePrivateAttributes = CodingUserInfoKey(rawValue: "LD_includePrivateAttributes")! static let allAttributesPrivate = CodingUserInfoKey(rawValue: "LD_allAttributesPrivate")! @@ -246,23 +206,3 @@ extension LDUserWrapper { } extension LDUser: TypeIdentifying { } - -#if DEBUG - extension LDUser { - // Compares all user properties. - func isEqual(to otherUser: LDUser) -> Bool { - key == otherUser.key - && secondary == otherUser.secondary - && name == otherUser.name - && firstName == otherUser.firstName - && lastName == otherUser.lastName - && country == otherUser.country - && ipAddress == otherUser.ipAddress - && email == otherUser.email - && avatar == otherUser.avatar - && custom == otherUser.custom - && isAnonymous == otherUser.isAnonymous - && privateAttributes == otherUser.privateAttributes - } - } -#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift index b9668f05..e952edfb 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift @@ -42,11 +42,3 @@ extension Dictionary where Key == String, Value == Any { (self[LDUser.CodingKeys.lastUpdated] as? String)?.dateValue } } - -#if DEBUG -extension Dictionary where Key == String, Value == Any { - mutating func setLastUpdated(_ lastUpdated: Date?) { - self[LDUser.CodingKeys.lastUpdated] = lastUpdated?.stringValue - } -} -#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift index b18ab10e..ac9ed9e2 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -127,7 +127,3 @@ final class FlagStore: FlagMaintaining { } extension FlagStore: TypeIdentifying { } - -extension Dictionary where Key == LDFlagKey, Value == FeatureFlag { - var allFlagValues: [LDFlagKey: Any] { compactMapValues { $0.value } } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 7bf06860..13797e2c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -7,8 +7,6 @@ final class LDUserSpec: QuickSpec { override func spec() { initSpec() - dictionaryValueSpec() - isEqualSpec() } private func initSpec() { @@ -115,254 +113,4 @@ final class LDUserSpec: QuickSpec { } } } - - private func dictionaryValueSpec() { - let optionalNames = LDUser.optionalAttributes.map { $0.name } - let allCustomPrivitizable = Array(LDUser.StubConstants.custom(includeSystemValues: true).keys) - - describe("dictionaryValue") { - var user: LDUser! - var config: LDConfig! - var userDictionary: [String: Any]! - - beforeEach { - config = LDConfig.stub - user = LDUser.stub() - } - - context("with an empty user") { - beforeEach { - user = LDUser() - // Remove SDK set attributes - user.custom = [:] - } - // Should be the same regardless of including/privitizing attributes - let testCase = { - it("creates expected user dictionary") { - expect(userDictionary.count) == 2 - // Required attributes - expect(userDictionary[LDUser.CodingKeys.key.rawValue] as? String) == user.key - expect(userDictionary[LDUser.CodingKeys.isAnonymous.rawValue] as? Bool) == user.isAnonymous - } - } - context("including private attributes") { - beforeEach { - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - testCase() - } - context("privatizing all globally") { - beforeEach { - config.allUserAttributesPrivate = true - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - testCase() - } - context("privatizing all individually in config") { - beforeEach { - config.privateUserAttributes = LDUser.optionalAttributes + [UserAttribute.forName("customAttr")] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - testCase() - } - context("privatizing all individually on user") { - beforeEach { - user.privateAttributes = LDUser.optionalAttributes + [UserAttribute.forName("customAttr")] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - testCase() - } - } - - it("includePrivateAttributes always includes attributes") { - config.allUserAttributesPrivate = true - config.privateUserAttributes = LDUser.optionalAttributes + allCustomPrivitizable.map { UserAttribute.forName($0) } - user.privateAttributes = LDUser.optionalAttributes + allCustomPrivitizable.map { UserAttribute.forName($0) } - let userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - - expect(userDictionary.count) == 11 - - // Required attributes - expect(userDictionary[LDUser.CodingKeys.key.rawValue] as? String) == user.key - expect(userDictionary[LDUser.CodingKeys.isAnonymous.rawValue] as? Bool) == user.isAnonymous - - // Built-in optional attributes - expect(userDictionary[LDUser.CodingKeys.name.rawValue] as? String) == user.name - expect(userDictionary[LDUser.CodingKeys.firstName.rawValue] as? String) == user.firstName - expect(userDictionary[LDUser.CodingKeys.lastName.rawValue] as? String) == user.lastName - expect(userDictionary[LDUser.CodingKeys.email.rawValue] as? String) == user.email - expect(userDictionary[LDUser.CodingKeys.ipAddress.rawValue] as? String) == user.ipAddress - expect(userDictionary[LDUser.CodingKeys.avatar.rawValue] as? String) == user.avatar - expect(userDictionary[LDUser.CodingKeys.secondary.rawValue] as? String) == user.secondary - expect(userDictionary[LDUser.CodingKeys.country.rawValue] as? String) == user.country - - let customDictionary = userDictionary.customDictionary()! - expect(customDictionary.count) == allCustomPrivitizable.count - - // Custom attributes - allCustomPrivitizable.forEach { attr in - expect(LDValue.fromAny(customDictionary[attr])) == user.custom[attr] - } - - // Redacted attributes is empty - expect(userDictionary[LDUser.CodingKeys.privateAttributes.rawValue]).to(beNil()) - } - - [false, true].forEach { isCustomAttr in - (isCustomAttr ? LDUser.StubConstants.custom(includeSystemValues: true).keys.map { UserAttribute.forName($0) } - : LDUser.optionalAttributes).forEach { privateAttr in - [false, true].forEach { inConfig in - it("with \(privateAttr) private in \(inConfig ? "config" : "user")") { - if inConfig { - config.privateUserAttributes = [privateAttr] - } else { - user.privateAttributes = [privateAttr] - } - - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - - expect(userDictionary.redactedAttributes) == [privateAttr.name] - - let includingDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - if !isCustomAttr { - let userDictionaryWithoutRedacted = userDictionary.filter { $0.key != "privateAttrs" } - let includingDictionaryWithoutRedacted = includingDictionary.filter { $0.key != privateAttr.name && $0.key != "privateAttrs" } - expect(AnyComparer.isEqual(userDictionaryWithoutRedacted, to: includingDictionaryWithoutRedacted)) == true - } else { - let userDictionaryWithoutRedacted = userDictionary.filter { $0.key != "custom" && $0.key != "privateAttrs" } - let includingDictionaryWithoutRedacted = includingDictionary.filter { $0.key != "custom" && $0.key != "privateAttrs" } - expect(AnyComparer.isEqual(userDictionaryWithoutRedacted, to: includingDictionaryWithoutRedacted)) == true - let expectedCustom = (includingDictionary["custom"] as! [String: Any]).filter { $0.key != privateAttr.name } - expect(AnyComparer.isEqual(userDictionary["custom"], to: expectedCustom)) == true - } - } - } - } - } - - context("with allUserAttributesPrivate") { - beforeEach { - config.allUserAttributesPrivate = true - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - it("creates expected dictionary") { - expect(userDictionary.count) == 3 - // Required attributes - expect(userDictionary[LDUser.CodingKeys.key.rawValue] as? String) == user.key - expect(userDictionary[LDUser.CodingKeys.isAnonymous.rawValue] as? Bool) == user.isAnonymous - - expect(Set(userDictionary.redactedAttributes!)) == Set(optionalNames + allCustomPrivitizable) - } - } - - context("with no private attributes") { - let noPrivateAssertions = { - it("matches dictionary including private") { - expect(AnyComparer.isEqual(userDictionary, to: user.dictionaryValue(includePrivateAttributes: true, config: config))) == true - } - } - context("by setting private attributes to nil") { - beforeEach { - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - noPrivateAssertions() - } - context("by setting config private attributes to empty") { - beforeEach { - config.privateUserAttributes = [] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - noPrivateAssertions() - } - context("by setting user private attributes to empty") { - beforeEach { - user.privateAttributes = [] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - noPrivateAssertions() - } - } - } - } - - private func isEqualSpec() { - var user: LDUser! - var otherUser: LDUser! - - describe("isEqual") { - context("when users are equal") { - it("returns true with all properties set") { - user = LDUser.stub() - otherUser = user - expect(user.isEqual(to: otherUser)) == true - } - it("returns true with no properties set") { - user = LDUser() - otherUser = user - expect(user.isEqual(to: otherUser)) == true - } - } - context("when users are not equal") { - let testFields: [(String, Bool, LDValue, (inout LDUser, LDValue?) -> Void)] = - [("key", false, "dummy", { u, v in u.key = v!.stringValue() }), - ("secondary", true, "dummy", { u, v in u.secondary = v?.stringValue() }), - ("name", true, "dummy", { u, v in u.name = v?.stringValue() }), - ("firstName", true, "dummy", { u, v in u.firstName = v?.stringValue() }), - ("lastName", true, "dummy", { u, v in u.lastName = v?.stringValue() }), - ("country", true, "dummy", { u, v in u.country = v?.stringValue() }), - ("ipAddress", true, "dummy", { u, v in u.ipAddress = v?.stringValue() }), - ("email address", true, "dummy", { u, v in u.email = v?.stringValue() }), - ("avatar", true, "dummy", { u, v in u.avatar = v?.stringValue() }), - ("custom", false, ["dummy": true], { u, v in u.custom = (v!.toAny() as! [String: Any]).mapValues { LDValue.fromAny($0) } }), - ("isAnonymous", false, true, { u, v in u.isAnonymous = v!.booleanValue() }), - ("privateAttributes", false, "dummy", { u, v in u.privateAttributes = [UserAttribute.forName(v!.stringValue())] })] - testFields.forEach { name, isOptional, otherVal, setter in - context("\(name) differs") { - beforeEach { - user = LDUser.stub() - otherUser = user - } - context("and both exist") { - it("returns false") { - setter(&otherUser, otherVal) - expect(user.isEqual(to: otherUser)) == false - expect(otherUser.isEqual(to: user)) == false - } - } - if isOptional { - context("self \(name) nil") { - it("returns false") { - setter(&user, nil) - expect(user.isEqual(to: otherUser)) == false - } - } - context("other \(name) nil") { - it("returns false") { - setter(&otherUser, nil) - expect(user.isEqual(to: otherUser)) == false - } - } - } - } - } - } - } - } -} - -extension LDUser { - public func dictionaryValueWithAllAttributes() -> [String: Any] { - var dictionary = dictionaryValue(includePrivateAttributes: true, config: LDConfig.stub) - dictionary[CodingKeys.privateAttributes.rawValue] = privateAttributes - return dictionary - } -} - -extension Dictionary where Key == String, Value == Any { - fileprivate var redactedAttributes: [String]? { - self[LDUser.CodingKeys.privateAttributes.rawValue] as? [String] - } - fileprivate func customDictionary() -> [String: Any]? { - self[LDUser.CodingKeys.custom.rawValue] as? [String: Any] - } } diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index ac5a5b4d..292c2c97 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -110,8 +110,8 @@ final class DarklyServiceSpec: QuickSpec { expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) - let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) - expect(AnyComparer.isEqual(urlRequest?.url?.lastPathComponent.jsonDictionary, to: expectedUser)) == true + let expectedUser = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedUser } else { fail("request path is missing") } @@ -163,8 +163,8 @@ final class DarklyServiceSpec: QuickSpec { expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) - let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) - expect(AnyComparer.isEqual(urlRequest?.url?.lastPathComponent.jsonDictionary, to: expectedUser)) == true + let expectedUser = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedUser } else { fail("request path is missing") } @@ -538,8 +538,8 @@ final class DarklyServiceSpec: QuickSpec { let receivedArguments = testContext.serviceFactoryMock.makeStreamingProviderReceivedArguments expect(receivedArguments!.url.host) == testContext.config.streamUrl.host expect(receivedArguments!.url.pathComponents.contains(DarklyService.StreamRequestPath.meval)).to(beTrue()) - let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) - expect(AnyComparer.isEqual(receivedArguments!.url.lastPathComponent.jsonDictionary, to: expectedUser)) == true + let expectedUser = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(receivedArguments!.url.lastPathComponent.jsonValue) == expectedUser expect(receivedArguments!.httpHeaders).toNot(beEmpty()) expect(receivedArguments!.connectMethod).to(be("GET")) expect(receivedArguments!.connectBody).to(beNil()) @@ -559,8 +559,8 @@ final class DarklyServiceSpec: QuickSpec { expect(receivedArguments!.url.lastPathComponent) == DarklyService.StreamRequestPath.meval expect(receivedArguments!.httpHeaders).toNot(beEmpty()) expect(receivedArguments!.connectMethod) == DarklyService.HTTPRequestMethod.report - let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) - expect(AnyComparer.isEqual(receivedArguments!.connectBody?.jsonDictionary, to: expectedUser)) == true + let expectedUser = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(try? JSONDecoder().decode(LDValue.self, from: receivedArguments!.connectBody!)) == expectedUser } } } @@ -765,8 +765,10 @@ private extension Data { } private extension String { - var jsonDictionary: [String: Any]? { + var jsonValue: LDValue? { let base64encodedString = self.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") - return Data(base64Encoded: base64encodedString)?.jsonDictionary + guard let data = Data(base64Encoded: base64encodedString) + else { return nil } + return try? JSONDecoder().decode(LDValue.self, from: data) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift index 0b56d4fb..9a75b969 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift @@ -52,10 +52,10 @@ final class DeprecatedCacheModelV5Spec: QuickSpec, CacheModelTestInterface { extension LDUser { func modelV5DictionaryValue(including featureFlags: [LDFlagKey: FeatureFlag], using lastUpdated: Date?) -> [String: Any] { - var userDictionary = dictionaryValueWithAllAttributes() - userDictionary.setLastUpdated(lastUpdated) + var userDictionary = encodeToLDValue(self, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true])?.toAny() as! [String: Any] + userDictionary[CodingKeys.privateAttributes.rawValue] = privateAttributes + userDictionary["updatedAt"] = lastUpdated?.stringValue userDictionary[LDUser.CodingKeys.config.rawValue] = featureFlags.compactMapValues { $0.modelV5dictionaryValue } - return userDictionary } } From 26cef8174f68add84ddab38bb7014d154f04a715 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Mon, 21 Mar 2022 11:07:17 -0500 Subject: [PATCH 42/50] Make trackEvents and trackReason non-optional in FeatureFlag and spec cleanup. --- LaunchDarkly.xcodeproj/project.pbxproj | 4 - .../Models/FeatureFlag/FeatureFlag.swift | 16 +- .../Cache/DeprecatedCacheModelV5.swift | 2 +- .../Extensions/AnyComparerSpec.swift | 283 ---- .../Extensions/DictionarySpec.swift | 136 +- .../LaunchDarklyTests/LDClientSpec.swift | 1334 +++++++---------- .../Mocks/DarklyServiceMock.swift | 4 +- .../Models/FeatureFlag/FeatureFlagSpec.swift | 114 +- .../Cache/DiagnosticCacheSpec.swift | 27 +- .../DiagnosticReporterSpec.swift | 2 +- .../ServiceObjects/EventReporterSpec.swift | 39 +- .../ServiceObjects/FlagSynchronizerSpec.swift | 12 +- 12 files changed, 633 insertions(+), 1340 deletions(-) delete mode 100644 LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 6a5acfce..afb3c46e 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -110,7 +110,6 @@ 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68AB224B3321005F052A /* CacheConverterSpec.swift */; }; - 832EA061203D03B700A93C0E /* AnyComparerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832EA060203D03B700A93C0E /* AnyComparerSpec.swift */; }; 8335299E1FC37727001166F8 /* FlagMaintainingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */; }; 83383A5120460DD30024D975 /* SynchronizingErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83383A5020460DD30024D975 /* SynchronizingErrorSpec.swift */; }; 83396BC91F7C3711000E256E /* DarklyServiceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83396BC81F7C3711000E256E /* DarklyServiceSpec.swift */; }; @@ -372,7 +371,6 @@ 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV5.swift; sourceTree = ""; }; 832D68A1224A38FC005F052A /* CacheConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverter.swift; sourceTree = ""; }; 832D68AB224B3321005F052A /* CacheConverterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverterSpec.swift; sourceTree = ""; }; - 832EA060203D03B700A93C0E /* AnyComparerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyComparerSpec.swift; sourceTree = ""; }; 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagMaintainingMock.swift; sourceTree = ""; }; 83383A5020460DD30024D975 /* SynchronizingErrorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizingErrorSpec.swift; sourceTree = ""; }; 83396BC81F7C3711000E256E /* DarklyServiceSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarklyServiceSpec.swift; sourceTree = ""; }; @@ -721,7 +719,6 @@ isa = PBXGroup; children = ( 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */, - 832EA060203D03B700A93C0E /* AnyComparerSpec.swift */, 83B6E3F0222EFA3800FF2A6A /* ThreadSpec.swift */, ); path = Extensions; @@ -1401,7 +1398,6 @@ B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */, 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */, 83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */, - 832EA061203D03B700A93C0E /* AnyComparerSpec.swift in Sources */, B43D5AD025FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift in Sources */, 832307AA1F7ECA630029815A /* LDConfigStub.swift in Sources */, 8354AC77224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift index e3528ed8..b8c1b41d 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift @@ -13,10 +13,10 @@ struct FeatureFlag { let version: Int? /// The feature flag version. It changes whenever this feature flag changes. Used for event reporting only. Server json lists this as "flagVersion". Event json lists this as "version". let flagVersion: Int? - let trackEvents: Bool? + let trackEvents: Bool let debugEventsUntilDate: Date? let reason: [String: Any]? - let trackReason: Bool? + let trackReason: Bool var versionForEvents: Int? { flagVersion ?? version } @@ -25,10 +25,10 @@ struct FeatureFlag { variation: Int? = nil, version: Int? = nil, flagVersion: Int? = nil, - trackEvents: Bool? = nil, + trackEvents: Bool = false, debugEventsUntilDate: Date? = nil, reason: [String: Any]? = nil, - trackReason: Bool? = nil) { + trackReason: Bool = false) { self.flagKey = flagKey self.value = value is NSNull ? nil : value self.variation = variation @@ -49,10 +49,10 @@ struct FeatureFlag { variation: dictionary.variation, version: dictionary.version, flagVersion: dictionary.flagVersion, - trackEvents: dictionary.trackEvents, + trackEvents: dictionary.trackEvents ?? false, debugEventsUntilDate: Date(millisSince1970: dictionary.debugEventsUntilDate), reason: dictionary.reason, - trackReason: dictionary.trackReason) + trackReason: dictionary.trackReason ?? false) } var dictionaryValue: [String: Any] { @@ -62,10 +62,10 @@ struct FeatureFlag { dictionaryValue[CodingKeys.variation.rawValue] = variation ?? NSNull() dictionaryValue[CodingKeys.version.rawValue] = version ?? NSNull() dictionaryValue[CodingKeys.flagVersion.rawValue] = flagVersion ?? NSNull() - dictionaryValue[CodingKeys.trackEvents.rawValue] = trackEvents ?? NSNull() + dictionaryValue[CodingKeys.trackEvents.rawValue] = trackEvents ? true : NSNull() dictionaryValue[CodingKeys.debugEventsUntilDate.rawValue] = debugEventsUntilDate?.millisSince1970 ?? NSNull() dictionaryValue[CodingKeys.reason.rawValue] = reason ?? NSNull() - dictionaryValue[CodingKeys.trackReason.rawValue] = trackReason ?? NSNull() + dictionaryValue[CodingKeys.trackReason.rawValue] = trackReason ? true : NSNull() return dictionaryValue } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift index dd45518e..f81ceeb6 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift @@ -65,7 +65,7 @@ final class DeprecatedCacheModelV5: DeprecatedCache { variation: featureFlagDictionary.variation, version: featureFlagDictionary.version, flagVersion: featureFlagDictionary.flagVersion, - trackEvents: featureFlagDictionary.trackEvents, + trackEvents: featureFlagDictionary.trackEvents ?? false, debugEventsUntilDate: Date(millisSince1970: featureFlagDictionary.debugEventsUntilDate))) }) return (featureFlags, cachedUserDictionary.lastUpdated) diff --git a/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift b/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift deleted file mode 100644 index ddd5aa6c..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift +++ /dev/null @@ -1,283 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class AnyComparerSpec: QuickSpec { - struct Constants { - } - - struct Values { - static let bool = true - static let int = 1027 - static let double = 1.6180339887 - static let string = "an interesting string" - static let array = [1, 2, 3, 5, 7, 11] - static let dictionary: [String: Any] = ["bool-key": true, - "int-key": -72, - "double-key": 1.414, - "string-key": "a not so interesting string", - "any-array-key": [true, 2, "hello-kitty"], - "int-array-key": [1, 2, 3], - "dictionary-key": ["keyA": true, "keyB": -1, "keyC": "howdy"]] - static let date = Date() - static let null = NSNull() - - static let all: [Any] = [bool, int, double, string, array, dictionary, date, null] - static let allThatCanBeInequal: [Any] = [bool, int, double, string, array, dictionary, date] - } - - struct AltValues { - static let bool = false - static let int = 1028 - static let double = 1.6180339887 * 2 - static let string = "an interesting string-" - static let array = [1, 2, 3, 5, 7] - static let dictionary: [String: Any] = ["bool-key": false, - "int-key": -72, - "double-key": 1.414, - "string-key": "a not so interesting string", - "any-array-key": [true, 2, "hello-kitty"], - "int-array-key": [1, 2, 3], - "dictionary-key": ["keyA": true, "keyB": -1, "keyC": "howdy"]] - static let date = Date().addingTimeInterval(-1.0) - static let null = NSNull() - - static let all: [Any] = [bool, int, double, string, array, dictionary, date, null] - static let allThatCanBeInequal: [Any] = [bool, int, double, string, array, dictionary, date] - } - - override func spec() { - nonOptionalSpec() - semiOptionalSpec() - optionalSpec() - } - - func nonOptionalSpec() { - var other: Any! - - describe("isEqual(to:)") { - context("when values match") { - context("and are the same type") { - it("returns true") { - Values.all.forEach { value in - other = value - - expect(AnyComparer.isEqual(value, to: other)).to(beTrue()) - } - expect(AnyComparer.isEqual(Int64(Values.int), to: Int64(Values.int))).to(beTrue()) - } - } - context("and are different types") { - it("returns true") { - expect(AnyComparer.isEqual(Values.int, to: Double(Values.int))).to(beTrue()) - expect(AnyComparer.isEqual(Double(Values.int), to: Values.int)).to(beTrue()) - expect(AnyComparer.isEqual(Int64(Values.int), to: Double(Values.int))).to(beTrue()) - expect(AnyComparer.isEqual(Double(Values.int), to: Int64(Values.int))).to(beTrue()) - } - } - } - context("when values dont match") { - context("and are the same type") { - it("returns false") { - zip(Values.allThatCanBeInequal, AltValues.allThatCanBeInequal).forEach { (value, altValue) in - other = altValue - expect(AnyComparer.isEqual(value, to: other)).to(beFalse()) - } - } - expect(AnyComparer.isEqual(Int64(Values.int), to: Int64(AltValues.int))).to(beFalse()) - } - context("and are different types") { - it("returns false") { - expect(AnyComparer.isEqual(Values.int, to: Values.double)).to(beFalse()) - expect(AnyComparer.isEqual(Values.double, to: Values.int)).to(beFalse()) - expect(AnyComparer.isEqual(Int64(Values.int), to: Values.double)).to(beFalse()) - expect(AnyComparer.isEqual(Values.double, to: Int64(Values.int))).to(beFalse()) - } - } - } - context("with matching feature flags") { - var featureFlags: [LDFlagKey: FeatureFlag]! - var otherFlag: FeatureFlag! - context("with elements") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags() - } - it("returns true") { - featureFlags.forEach { _, featureFlag in - otherFlag = FeatureFlag(copying: featureFlag) - - expect(AnyComparer.isEqual(featureFlag, to: otherFlag)).to(beTrue()) - } - } - } - context("without elements") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false) - } - it("returns true") { - featureFlags.forEach { flagKey, featureFlag in - otherFlag = FeatureFlag(flagKey: flagKey, value: featureFlag.value, trackReason: false) - - expect(AnyComparer.isEqual(featureFlag, to: otherFlag)).to(beTrue()) - } - } - } - } - context("with different feature flags") { - var featureFlags: [LDFlagKey: FeatureFlag]! - var otherFlag: FeatureFlag! - context("with elements") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags() - } - context("with differing variation") { - it("returns false") { - featureFlags.forEach { _, featureFlag in - otherFlag = FeatureFlag(copying: featureFlag, variation: featureFlag.variation! + 1) - - expect(AnyComparer.isEqual(featureFlag, to: otherFlag)).to(beFalse()) - } - } - } - context("with differing version") { - it("returns false") { - featureFlags.forEach { _, featureFlag in - otherFlag = FeatureFlag(copying: featureFlag, version: featureFlag.version! + 1) - - expect(AnyComparer.isEqual(featureFlag, to: otherFlag)).to(beFalse()) - } - } - } - context("with differing flagVersion") { - it("returns true") { - featureFlags.forEach { _, featureFlag in - otherFlag = FeatureFlag(copying: featureFlag, flagVersion: featureFlag.flagVersion! + 1) - - expect(AnyComparer.isEqual(featureFlag, to: otherFlag)).to(beTrue()) - } - } - } - context("with differing trackEvents") { - it("returns true") { - featureFlags.forEach { _, featureFlag in - otherFlag = FeatureFlag(copying: featureFlag, trackEvents: false) - - expect(AnyComparer.isEqual(featureFlag, to: otherFlag)).to(beTrue()) - } - } - } - context("with differing debugEventsUntilDate") { - it("returns true") { - featureFlags.forEach { _, featureFlag in - otherFlag = FeatureFlag(copying: featureFlag, debugEventsUntilDate: Date()) - - expect(AnyComparer.isEqual(featureFlag, to: otherFlag)).to(beTrue()) - } - } - } - } - context("without elements") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false) - } - context("with differing value") { - it("returns true") { // Yeah, this is weird. Since the variation is missing the comparison succeeds - featureFlags.forEach { flagKey, featureFlag in - otherFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey, - includeVariation: false, - includeVersion: false, - includeFlagVersion: false, - useAlternateValue: true) - - expect(AnyComparer.isEqual(featureFlag, to: otherFlag)).to(beTrue()) - } - } - } - } - } - } - } - - func semiOptionalSpec() { - var other: Any? - - describe("isEqual(to:)") { - context("when values match") { - it("returns true") { - Values.all.forEach { value in - other = value - - expect(AnyComparer.isEqual(value, to: other)).to(beTrue()) - expect(AnyComparer.isEqual(other, to: value)).to(beTrue()) - } - } - } - context("when values dont match") { - it("returns false") { - zip(Values.all, AltValues.all).forEach { value, altValue in - other = altValue - - if !(value is NSNull) { - expect(AnyComparer.isEqual(value, to: other)).to(beFalse()) - expect(AnyComparer.isEqual(other, to: value)).to(beFalse()) - } - } - } - } - context("when one value is nil") { - it("returns false") { - Values.all.forEach { value in - expect(AnyComparer.isEqual(value, to: nil)).to(beFalse()) - expect(AnyComparer.isEqual(nil, to: value)).to(beFalse()) - } - } - } - } - } - - func optionalSpec() { - var optionalValue: Any? - var other: Any? - - describe("isEqual(to:)") { - context("when values match") { - it("returns true") { - Values.all.forEach { value in - optionalValue = value - other = value - - expect(AnyComparer.isEqual(optionalValue, to: other)).to(beTrue()) - } - } - } - context("when values dont match") { - it("returns false") { - zip(Values.all, AltValues.all).forEach { value, altValue in - optionalValue = value - other = altValue - - if !(value is NSNull) { - expect(AnyComparer.isEqual(optionalValue, to: other)).to(beFalse()) - } - } - } - } - context("when one value is nil") { - it("returns false") { - Values.all.forEach { value in - optionalValue = value - - expect(AnyComparer.isEqual(optionalValue, to: nil)).to(beFalse()) - expect(AnyComparer.isEqual(nil, to: optionalValue)).to(beFalse()) - } - } - } - context("when both values are nil") { - it("returns true") { - expect(AnyComparer.isEqual(nil, to: nil)).to(beTrue()) - } - } - } - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift b/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift index f8d53f42..c53ed123 100644 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift @@ -7,7 +7,6 @@ final class DictionarySpec: QuickSpec { public override func spec() { symmetricDifferenceSpec() withNullValuesRemovedSpec() - dictionarySpec() } private func symmetricDifferenceSpec() { @@ -24,103 +23,81 @@ final class DictionarySpec: QuickSpec { } } context("when other is empty") { - beforeEach { - otherDictionary = [:] - } it("returns all keys in subject") { + otherDictionary = [:] expect(dictionary.symmetricDifference(otherDictionary)) == dictionary.keys.sorted() } } context("when subject is empty") { - beforeEach { - dictionary = [:] - } it("returns all keys in other") { + dictionary = [:] expect(dictionary.symmetricDifference(otherDictionary)) == otherDictionary.keys.sorted() } } context("when subject has an added key") { - let addedKey = "addedKey" - beforeEach { - dictionary[addedKey] = true - } it("returns the different key") { + let addedKey = "addedKey" + dictionary[addedKey] = true expect(dictionary.symmetricDifference(otherDictionary)) == [addedKey] } } context("when other has an added key") { - let addedKey = "addedKey" - beforeEach { - otherDictionary[addedKey] = true - } it("returns the different key") { + let addedKey = "addedKey" + otherDictionary[addedKey] = true expect(dictionary.symmetricDifference(otherDictionary)) == [addedKey] } } context("when other has a different key") { - let addedKeyA = "addedKeyA" - let addedKeyB = "addedKeyB" - beforeEach { + it("returns the different keys") { + let addedKeyA = "addedKeyA" + let addedKeyB = "addedKeyB" otherDictionary[addedKeyA] = true dictionary[addedKeyB] = true - } - it("returns the different keys") { expect(dictionary.symmetricDifference(otherDictionary)) == [addedKeyA, addedKeyB] } } context("when other has a different bool value") { - let differingKey = DarklyServiceMock.FlagKeys.bool - beforeEach { - otherDictionary[differingKey] = !DarklyServiceMock.FlagValues.bool - } it("returns the different key") { + let differingKey = DarklyServiceMock.FlagKeys.bool + otherDictionary[differingKey] = !DarklyServiceMock.FlagValues.bool expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] } } context("when other has a different int value") { - let differingKey = DarklyServiceMock.FlagKeys.int - beforeEach { - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.int + 1 - } it("returns the different key") { + let differingKey = DarklyServiceMock.FlagKeys.int + otherDictionary[differingKey] = DarklyServiceMock.FlagValues.int + 1 expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] } } context("when other has a different double value") { - let differingKey = DarklyServiceMock.FlagKeys.double - beforeEach { - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.double - 1.0 - } it("returns the different key") { + let differingKey = DarklyServiceMock.FlagKeys.double + otherDictionary[differingKey] = DarklyServiceMock.FlagValues.double - 1.0 expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] } } context("when other has a different string value") { - let differingKey = DarklyServiceMock.FlagKeys.string - beforeEach { - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.string + " some new text" - } it("returns the different key") { + let differingKey = DarklyServiceMock.FlagKeys.string + otherDictionary[differingKey] = DarklyServiceMock.FlagValues.string + " some new text" expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] } } context("when other has a different array value") { - let differingKey = DarklyServiceMock.FlagKeys.array - beforeEach { - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.array + [4] - } it("returns the different key") { + let differingKey = DarklyServiceMock.FlagKeys.array + otherDictionary[differingKey] = DarklyServiceMock.FlagValues.array + [4] expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] } } context("when other has a different dictionary value") { - let differingKey = DarklyServiceMock.FlagKeys.dictionary - beforeEach { + it("returns the different key") { + let differingKey = DarklyServiceMock.FlagKeys.dictionary var differingDictionary = DarklyServiceMock.FlagValues.dictionary differingDictionary["sub-flag-a"] = !(differingDictionary["sub-flag-a"] as! Bool) otherDictionary[differingKey] = differingDictionary - } - it("returns the different key") { expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] } } @@ -129,52 +106,25 @@ final class DictionarySpec: QuickSpec { private func withNullValuesRemovedSpec() { describe("withNullValuesRemoved") { - var dictionary: [String: Any]! - var resultingDictionary: [String: Any]! - context("when no null values exist") { - beforeEach { - dictionary = Dictionary.stub() - - resultingDictionary = dictionary.withNullValuesRemoved - } - it("returns the same dictionary") { - expect(dictionary == resultingDictionary).to(beTrue()) - } + it("when no null values exist") { + let dictionary = Dictionary.stub() + let resultingDictionary = dictionary.withNullValuesRemoved + expect(dictionary.keys) == resultingDictionary.keys } context("when null values exist") { - context("in the top level") { - beforeEach { - dictionary = Dictionary.stub().withNullValueAppended - - resultingDictionary = dictionary.withNullValuesRemoved - } - it("returns the dictionary without the null value") { - expect(resultingDictionary == Dictionary.stub()).to(beTrue()) - } + it("in the top level") { + var dictionary = Dictionary.stub() + dictionary["null-key"] = NSNull() + let resultingDictionary = dictionary.withNullValuesRemoved + expect(resultingDictionary.keys) == Dictionary.stub().keys } - context("in the second level") { - beforeEach { - dictionary = Dictionary.stub() - dictionary[Dictionary.Keys.dictionary] = Dictionary.Values.dictionary.withNullValueAppended - - resultingDictionary = dictionary.withNullValuesRemoved - } - it("returns a dictionary without the null value") { - expect(resultingDictionary == Dictionary.stub()).to(beTrue()) - } - } - } - } - } - - private func dictionarySpec() { - describe("Optional extension") { - context("when both are null") { - let dict1: [String: Any]? = nil - let dict2: [String: Any]? = nil - - it("does not stack overflow") { - expect(dict1 == dict2).to(beTrue()) + it("in the second level") { + var dictionary = Dictionary.stub() + var subDict = Dictionary.Values.dictionary + subDict["null-key"] = NSNull() + dictionary[Dictionary.Keys.dictionary] = subDict + let resultingDictionary = dictionary.withNullValuesRemoved + expect((resultingDictionary[Dictionary.Keys.dictionary] as! [String: Any]).keys) == Dictionary.Values.dictionary.keys } } } @@ -212,22 +162,10 @@ fileprivate extension Dictionary where Key == String, Value == Any { } } -extension Optional where Wrapped == [String: Any] { - public static func == (lhs: [String: Any]?, rhs: [String: Any]?) -> Bool { - AnyComparer.isEqual(lhs, to: rhs) - } -} - extension Dictionary where Key == String, Value == Any { func appendNull() -> [String: Any] { var dictWithNull = self dictWithNull[Keys.null] = Values.null return dictWithNull } - - var withNullValueAppended: [String: Any] { - var modifiedDictionary = self - modifiedDictionary[Keys.null] = Values.null - return modifiedDictionary - } } diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 0610cf02..5c41b400 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -166,42 +166,29 @@ final class LDClientSpec: QuickSpec { let anonUser = LDUser(key: "unknown", isAnonymous: true) let knownUser = LDUser(key: "known", isAnonymous: false) describe("aliasing") { - var ctx: TestContext! - beforeEach { - ctx = TestContext(autoAliasingOptOut: false) - } - context("automatic aliasing from anonymous to user") { - beforeEach { - ctx.withUser(anonUser).start() - ctx.subject.internalIdentify(newUser: knownUser) - } - it("records an alias and identify event") { - // init, identify, and alias event - expect(ctx.eventReporterMock.recordCallCount) == 3 - expect(ctx.recordedEvent?.kind) == .alias - } - } - context("automatic aliasing from user to user") { - beforeEach { - ctx.withUser(knownUser).start() - ctx.subject.internalIdentify(newUser: knownUser) - } - it("doesnt record an alias event") { - // init and identify event - expect(ctx.eventReporterMock.recordCallCount) == 2 - expect(ctx.recordedEvent?.kind) == .identify - } - } - context("automatic aliasing from anonymous to anonymous") { - beforeEach { - ctx.withUser(anonUser).start() - ctx.subject.internalIdentify(newUser: anonUser) - } - it("doesnt record an alias event") { - // init and identify event - expect(ctx.eventReporterMock.recordCallCount) == 2 - expect(ctx.recordedEvent?.kind) == .identify - } + it("automatic aliasing from anonymous to user") { + let ctx = TestContext(autoAliasingOptOut: false) + ctx.withUser(anonUser).start() + ctx.subject.internalIdentify(newUser: knownUser) + // init, identify, and alias event + expect(ctx.eventReporterMock.recordCallCount) == 3 + expect(ctx.recordedEvent?.kind) == .alias + } + it("no automatic aliasing from user to user") { + let ctx = TestContext(autoAliasingOptOut: false) + ctx.withUser(knownUser).start() + ctx.subject.internalIdentify(newUser: knownUser) + // init and identify event + expect(ctx.eventReporterMock.recordCallCount) == 2 + expect(ctx.recordedEvent?.kind) == .identify + } + it("no automatic aliasing from anonymous to anonymous") { + let ctx = TestContext(autoAliasingOptOut: false) + ctx.withUser(anonUser).start() + ctx.subject.internalIdentify(newUser: anonUser) + // init and identify event + expect(ctx.eventReporterMock.recordCallCount) == 2 + expect(ctx.recordedEvent?.kind) == .identify } } } @@ -384,43 +371,33 @@ final class LDClientSpec: QuickSpec { } } } - context("when called with cached flags for the user and environment") { - beforeEach { - testContext = TestContext().withCached(flags: FlagMaintainingMock.stubFlags()) - withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() - } - it("checks the flag cache for the user and environment") { - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - } - it("restores user flags from cache") { - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags.flagCollection) == FlagMaintainingMock.stubFlags() - } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } + it("when called with cached flags for the user and environment") { + let testContext = TestContext().withCached(flags: FlagMaintainingMock.stubFlags()) + withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() + + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + + expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags.flagCollection) == FlagMaintainingMock.stubFlags() + + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } - context("when called without cached flags for the user") { - beforeEach { - testContext = TestContext() - withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() - } - it("checks the flag cache for the user and environment") { - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - } - it("does not restore user flags from cache") { - expect(testContext.flagStoreMock.replaceStoreCallCount) == 0 - } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } + it("when called without cached flags for the user") { + let testContext = TestContext() + withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() + + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + + expect(testContext.flagStoreMock.replaceStoreCallCount) == 0 + + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } } @@ -449,14 +426,13 @@ final class LDClientSpec: QuickSpec { } context("when configured to start offline") { - beforeEach { - testContext = TestContext() - } it("completes immediately without timeout") { + testContext = TestContext() testContext.start(completion: startCompletion) expect(completed) == true } it("completes immediately with timeout") { + testContext = TestContext() testContext.start(timeOut: 5.0, timeOutCompletion: startTimeoutCompletion()) expect(completed) == true expect(didTimeOut) == true @@ -619,293 +595,189 @@ final class LDClientSpec: QuickSpec { } private func identifySpec() { - var testContext: TestContext! - describe("identify") { - var newUser: LDUser! - context("when the client is online") { - beforeEach { - testContext = TestContext(startOnline: true) - testContext.start() - testContext.featureFlagCachingMock.reset() - testContext.cacheConvertingMock.reset() + it("when the client is online") { + let testContext = TestContext(startOnline: true) + testContext.start() + testContext.featureFlagCachingMock.reset() + testContext.cacheConvertingMock.reset() - newUser = LDUser.stub() - testContext.subject.internalIdentify(newUser: newUser) - } - it("changes to the new user") { - expect(testContext.subject.user) == newUser - expect(testContext.subject.service.user) == newUser - expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 - expect(testContext.makeFlagSynchronizerService?.user) == newUser - } - it("leaves the client online") { - expect(testContext.subject.isOnline) == true - expect(testContext.subject.eventReporter.isOnline) == true - expect(testContext.subject.flagSynchronizer.isOnline) == true - } - it("uncaches the new users flags") { - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - } - it("records identify and summary events") { - expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) - } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } + let newUser = LDUser.stub() + testContext.subject.internalIdentify(newUser: newUser) + + expect(testContext.subject.user) == newUser + expect(testContext.subject.service.user) == newUser + expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 + expect(testContext.makeFlagSynchronizerService?.user) == newUser + + expect(testContext.subject.isOnline) == true + expect(testContext.subject.eventReporter.isOnline) == true + expect(testContext.subject.flagSynchronizer.isOnline) == true + + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + + expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) + + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } - context("when the client is offline") { - beforeEach { - testContext = TestContext() - testContext.start() - testContext.featureFlagCachingMock.reset() - testContext.cacheConvertingMock.reset() + it("when the client is offline") { + let testContext = TestContext() + testContext.start() + testContext.featureFlagCachingMock.reset() + testContext.cacheConvertingMock.reset() - newUser = LDUser.stub() - testContext.subject.internalIdentify(newUser: newUser) - } - it("changes to the new user") { - expect(testContext.subject.user) == newUser - expect(testContext.subject.service.user) == newUser - expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 - expect(testContext.makeFlagSynchronizerService?.user) == newUser - } - it("leaves the client offline") { - expect(testContext.subject.isOnline) == false - expect(testContext.subject.eventReporter.isOnline) == false - expect(testContext.subject.flagSynchronizer.isOnline) == false - } - it("uncaches the new users flags") { - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - } - it("records identify and summary events") { - expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) - } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } + let newUser = LDUser.stub() + testContext.subject.internalIdentify(newUser: newUser) + + expect(testContext.subject.user) == newUser + expect(testContext.subject.service.user) == newUser + expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 + expect(testContext.makeFlagSynchronizerService?.user) == newUser + + expect(testContext.subject.isOnline) == false + expect(testContext.subject.eventReporter.isOnline) == false + expect(testContext.subject.flagSynchronizer.isOnline) == false + + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + + expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) + + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } - context("when the new user has cached feature flags") { + it("when the new user has cached feature flags") { let stubFlags = FlagMaintainingMock.stubFlags() - beforeEach { - newUser = LDUser.stub() - testContext = TestContext().withCached(userKey: newUser.key, flags: stubFlags) - testContext.start() - testContext.featureFlagCachingMock.reset() - testContext.cacheConvertingMock.reset() + let newUser = LDUser.stub() + let testContext = TestContext().withCached(userKey: newUser.key, flags: stubFlags) + testContext.start() + testContext.featureFlagCachingMock.reset() + testContext.cacheConvertingMock.reset() - testContext.subject.internalIdentify(newUser: newUser) - } - it("restores the cached users feature flags") { - expect(testContext.subject.user) == newUser - expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags.flagCollection) == stubFlags - } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } + testContext.subject.internalIdentify(newUser: newUser) + + expect(testContext.subject.user) == newUser + expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 + expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags.flagCollection) == stubFlags + + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } } } private func setOnlineSpec() { describe("setOnline") { - var testContext: TestContext! - - context("when the client is offline") { - context("setting online") { - beforeEach { - waitUntil { done in - testContext = TestContext() - testContext.start { - testContext.subject.setOnline(true) - done() - } - } - } - it("sets the client and service objects online") { - expect(testContext.throttlerMock?.runThrottledCallCount) == 1 - expect(testContext.subject.isOnline) == true - expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline - expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline + it("set online when the client is offline") { + let testContext = TestContext() + waitUntil { done in + testContext.start { + testContext.subject.setOnline(true) + done() } } + + expect(testContext.throttlerMock?.runThrottledCallCount) == 1 + expect(testContext.subject.isOnline) == true + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline } - context("when the client is online") { - context("setting offline") { - beforeEach { - testContext = TestContext(startOnline: true) - testContext.start() + it("set offline when the client is online") { + let testContext = TestContext(startOnline: true) + testContext.start() + testContext.throttlerMock?.runThrottledCallCount = 0 + testContext.subject.setOnline(false) - testContext.throttlerMock?.runThrottledCallCount = 0 - testContext.subject.setOnline(false) - } - it("takes the client and service objects offline") { - expect(testContext.throttlerMock?.runThrottledCallCount) == 0 - expect(testContext.subject.isOnline) == false - expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline - expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline - } - } + expect(testContext.throttlerMock?.runThrottledCallCount) == 0 + expect(testContext.subject.isOnline) == false + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline } - context("when the client runs in the background") { + context("set online when the client runs in the background") { OperatingSystem.allOperatingSystems.forEach { os in context("on \(os)") { - context("while configured to enable background updates") { - context("and setting online") { - var targetRunThrottledCalls: Int! - beforeEach { - waitUntil { done in - testContext = TestContext(operatingSystem: os) - testContext.start(runMode: .background, completion: done) - } - targetRunThrottledCalls = os.isBackgroundEnabled ? 1 : 0 - testContext.subject.setOnline(true) - } - it("takes the client and service objects online") { - expect(testContext.throttlerMock?.runThrottledCallCount) == targetRunThrottledCalls - expect(testContext.subject.isOnline) == os.isBackgroundEnabled - expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline - expect(testContext.makeFlagSynchronizerStreamingMode) == os.backgroundStreamingMode - expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) - expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline - } - } + it("while configured to enable background updates") { + let testContext = TestContext(operatingSystem: os) + waitUntil { testContext.start(runMode: .background, completion: $0) } + testContext.subject.setOnline(true) + + expect(testContext.throttlerMock?.runThrottledCallCount) == (os.isBackgroundEnabled ? 1 : 0) + expect(testContext.subject.isOnline) == os.isBackgroundEnabled + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.makeFlagSynchronizerStreamingMode) == os.backgroundStreamingMode + expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline } - context("while configured to disable background updates") { - beforeEach { - waitUntil { done in - testContext = TestContext(enableBackgroundUpdates: false, operatingSystem: os) - testContext.start(runMode: .background, completion: done) - } - } - context("and setting online") { - beforeEach { - testContext.subject.setOnline(true) - } - it("leaves the client and service objects offline") { - expect(testContext.throttlerMock?.runThrottledCallCount) == 0 - expect(testContext.subject.isOnline) == false - expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline - expect(testContext.makeFlagSynchronizerStreamingMode) == LDStreamingMode.polling - expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) - expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline - } - } + it("while configured to disable background updates") { + let testContext = TestContext(enableBackgroundUpdates: false, operatingSystem: os) + waitUntil { testContext.start(runMode: .background, completion: $0) } + testContext.subject.setOnline(true) + + expect(testContext.throttlerMock?.runThrottledCallCount) == 0 + expect(testContext.subject.isOnline) == false + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.makeFlagSynchronizerStreamingMode) == LDStreamingMode.polling + expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline } } } } - context("when the mobile key is empty") { - beforeEach { - waitUntil { done in - testContext = TestContext(newConfig: LDConfig(mobileKey: "")) - testContext.start(completion: done) - } - testContext.throttlerMock?.runThrottledCallCount = 0 + it("set online when the mobile key is empty") { + let testContext = TestContext(newConfig: LDConfig(mobileKey: "")) + waitUntil { testContext.start(completion: $0) } + testContext.subject.setOnline(true) - testContext.subject.setOnline(true) - } - it("leaves the client and service objects offline") { - expect(testContext.throttlerMock?.runThrottledCallCount) == 0 - expect(testContext.subject.isOnline) == false - expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline - expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline - } + expect(testContext.throttlerMock?.runThrottledCallCount) == 0 + expect(testContext.subject.isOnline) == false + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline } } } private func closeSpec() { - var testContext: TestContext! - describe("stop") { - var priorRecordedEvents: Int! - context("when started") { - beforeEach { - priorRecordedEvents = 0 - } - context("and online") { - beforeEach { - testContext = TestContext(startOnline: true) - testContext.start() - priorRecordedEvents = testContext.eventReporterMock.recordCallCount + it("when started and online") { + let testContext = TestContext(startOnline: true) + testContext.start() + testContext.subject.close() - testContext.subject.close() - } - it("takes the client offline") { - expect(testContext.subject.isOnline) == false - } - it("stops recording events") { - testContext.subject.track(key: "abc") - expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents - } - it("flushes the event reporter") { - expect(testContext.eventReporterMock.flushCallCount) == 1 - } - } - context("and offline") { - beforeEach { - testContext = TestContext() - testContext.start() - priorRecordedEvents = testContext.eventReporterMock.recordCallCount + expect(testContext.subject.isOnline) == false + expect(testContext.eventReporterMock.flushCallCount) == 1 + } + it("when started and offline") { + let testContext = TestContext() + testContext.start() + testContext.subject.close() - testContext.subject.close() - } - it("leaves the client offline") { - expect(testContext.subject.isOnline) == false - } - it("stops recording events") { - testContext.subject.track(key: "abc") - expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents - } - it("flushes the event reporter") { - expect(testContext.eventReporterMock.flushCallCount) == 1 - } - } + expect(testContext.subject.isOnline) == false + expect(testContext.eventReporterMock.flushCallCount) == 1 } - context("when already stopped") { - beforeEach { - testContext = TestContext() - testContext.start() - testContext.subject.close() - priorRecordedEvents = testContext.eventReporterMock.recordCallCount + it("when already stopped") { + let testContext = TestContext() + testContext.start() + testContext.subject.close() + testContext.subject.close() - testContext.subject.close() - } - it("leaves the client offline") { - expect(testContext.subject.isOnline) == false - } - it("stops recording events") { - testContext.subject.track(key: "abc") - expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents - } - it("flushes the event reporter") { - expect(testContext.eventReporterMock.flushCallCount) == 1 - } + expect(testContext.subject.isOnline) == false + expect(testContext.eventReporterMock.flushCallCount) == 1 } } } private func trackEventSpec() { - var testContext: TestContext! - describe("track event") { - beforeEach { - testContext = TestContext() + it("records a custom event") { + let testContext = TestContext() testContext.start() - } - it("records a custom event when client was started") { testContext.subject.track(key: "customEvent", data: "abc", metricValue: 5.0) let receivedEvent = testContext.eventReporterMock.recordReceivedEvent as? CustomEvent expect(receivedEvent?.key) == "customEvent" @@ -913,17 +785,13 @@ final class LDClientSpec: QuickSpec { expect(receivedEvent?.data) == "abc" expect(receivedEvent?.metricValue) == 5.0 } - context("when client was stopped") { - var priorRecordedEvents: Int! - beforeEach { - testContext.subject.close() - priorRecordedEvents = testContext.eventReporterMock.recordCallCount - - testContext.subject.track(key: "abc") - } - it("does not record any more events") { - expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents - } + context("does not record when client was stopped") { + let testContext = TestContext() + testContext.start() + testContext.subject.close() + let priorRecordedEvents = testContext.eventReporterMock.recordCallCount + testContext.subject.track(key: "abc") + expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents } } } @@ -950,8 +818,8 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DarklyServiceMock.FlagValues.double expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DarklyServiceMock.FlagValues.string expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array) == DarklyServiceMock.FlagValues.array).to(beTrue()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary) - == DarklyServiceMock.FlagValues.dictionary).to(beTrue()) + expect(AnyComparer.isEqual(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary), + to: DarklyServiceMock.FlagValues.dictionary)).to(beTrue()) } it("records a flag evaluation event") { _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) @@ -972,7 +840,8 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DefaultFlagValues.double expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DefaultFlagValues.string expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array) == DefaultFlagValues.array).to(beTrue()) - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary) == DefaultFlagValues.dictionary).to(beTrue()) + expect(AnyComparer.isEqual(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary), + to: DefaultFlagValues.dictionary)).to(beTrue()) } it("records a flag evaluation event") { _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) @@ -1061,161 +930,129 @@ final class LDClientSpec: QuickSpec { } private func onSyncCompleteSuccessSpec() { - context("polling") { - onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .polling) + it("polling") { + self.onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .polling) } - context("streaming ping") { - onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .streaming, eventType: .ping) + it("streaming ping") { + self.onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .streaming, eventType: .ping) } - context("streaming put") { - onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .streaming, eventType: .put) + it("streaming put") { + self.onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .streaming, eventType: .put) } - context("streaming patch") { - onSyncCompleteStreamingPatchSpec() + it("streaming patch") { + self.onSyncCompleteStreamingPatchSpec() } - context("streaming delete") { - onSyncCompleteDeleteFlagSpec() + it("streaming delete") { + self.onSyncCompleteDeleteFlagSpec() } } private func onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: LDStreamingMode, eventType: FlagUpdateType? = nil) { - var testContext: TestContext! - var newFlags: [LDFlagKey: FeatureFlag]! + let testContext = TestContext(startOnline: true) + testContext.start() + testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() + + var newFlags = FlagMaintainingMock.stubFlags() + newFlags[Constants.newFlagKey] = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.string, useAlternateValue: true) + var updateDate: Date! + waitUntil { done in + testContext.changeNotifierMock.notifyObserversCallback = done + updateDate = Date() + testContext.onSyncComplete?(.success(newFlags, eventType)) + } - beforeEach { - testContext = TestContext(startOnline: true) - testContext.start() - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() + expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 + expect(AnyComparer.isEqual(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags, to: newFlags)).to(beTrue()) - newFlags = FlagMaintainingMock.stubFlags() - newFlags[Constants.newFlagKey] = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.string, useAlternateValue: true) + expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(newFlags, eventType)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags == newFlags).to(beTrue()) - } - it("caches the new flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the changed flags") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == testContext.cachedFlags).to(beTrue()) - } + expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags + expect(AnyComparer.isEqual(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags, to: testContext.cachedFlags)).to(beTrue()) } func onSyncCompleteStreamingPatchSpec() { - var testContext: TestContext! - var flagUpdateDictionary: [String: Any]! - var updateDate: Date! let stubFlags = FlagMaintainingMock.stubFlags() - beforeEach { - testContext = TestContext(startOnline: true).withCached(flags: stubFlags) - testContext.start() - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - flagUpdateDictionary = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, + let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) + testContext.start() + testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() + let flagUpdateDictionary = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, value: DarklyServiceMock.FlagValues.int + 1, variation: DarklyServiceMock.Constants.variation + 1, version: DarklyServiceMock.Constants.version + 1) - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(flagUpdateDictionary, .patch)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.updateStoreCallCount) == 1 - expect(testContext.flagStoreMock.updateStoreReceivedArguments?.updateDictionary == flagUpdateDictionary).to(beTrue()) - } - it("caches the updated flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the changed flag") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) + var updateDate: Date! + waitUntil { done in + testContext.changeNotifierMock.notifyObserversCallback = done + updateDate = Date() + testContext.onSyncComplete?(.success(flagUpdateDictionary, .patch)) } + + expect(testContext.flagStoreMock.updateStoreCallCount) == 1 + expect(AnyComparer.isEqual(testContext.flagStoreMock.updateStoreReceivedArguments?.updateDictionary, to: flagUpdateDictionary)).to(beTrue()) + + expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async + + expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) } func onSyncCompleteDeleteFlagSpec() { - var testContext: TestContext! - var flagUpdateDictionary: [String: Any]! - var updateDate: Date! let stubFlags = FlagMaintainingMock.stubFlags() - beforeEach { - testContext = TestContext(startOnline: true).withCached(flags: stubFlags) - testContext.start() - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - flagUpdateDictionary = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) + let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) + testContext.start() + testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() + let flagUpdateDictionary = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(flagUpdateDictionary, .delete)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.deleteFlagCallCount) == 1 - expect(testContext.flagStoreMock.deleteFlagReceivedArguments?.deleteDictionary == flagUpdateDictionary).to(beTrue()) - } - it("caches the updated flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the changed flag") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) + var updateDate: Date! + waitUntil { done in + testContext.changeNotifierMock.notifyObserversCallback = done + updateDate = Date() + testContext.onSyncComplete?(.success(flagUpdateDictionary, .delete)) } + + expect(testContext.flagStoreMock.deleteFlagCallCount) == 1 + expect(AnyComparer.isEqual(testContext.flagStoreMock.deleteFlagReceivedArguments?.deleteDictionary, to: flagUpdateDictionary)).to(beTrue()) + + expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async + + expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) } func onSyncCompleteErrorSpec() { func runTest(_ ctx: String, _ err: SynchronizingError, testError: @escaping ((ConnectionInformation.LastConnectionFailureReason) -> Void)) { - var testContext: TestContext! - context(ctx) { - beforeEach { - testContext = TestContext(startOnline: true) - testContext.start() - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - testContext.onSyncComplete?(.error(err)) - } - it("takes the client offline when unauthed") { - expect(testContext.subject.isOnline) == !err.isClientUnauthorized - } - it("does not cache the users flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 - } - it("does not call the flag change notifier") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 0 - } - it("Updates the connection information") { - expect(testContext.subject.getConnectionInformation().lastFailedConnection).to(beCloseTo(Date(), within: 5.0)) - testError(testContext.subject.getConnectionInformation().lastConnectionFailureReason) - } + it(ctx) { + let testContext = TestContext(startOnline: true) + testContext.start() + testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() + testContext.onSyncComplete?(.error(err)) + + expect(testContext.subject.isOnline) == !err.isClientUnauthorized + expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 + expect(testContext.changeNotifierMock.notifyObserversCallCount) == 0 + expect(testContext.subject.getConnectionInformation().lastFailedConnection).to(beCloseTo(Date(), within: 5.0)) + testError(testContext.subject.getConnectionInformation().lastConnectionFailureReason) } } @@ -1246,60 +1083,49 @@ final class LDClientSpec: QuickSpec { } private func runModeSpec() { - var testContext: TestContext! - describe("didEnterBackground notification") { context("after starting client") { context("when online") { OperatingSystem.allOperatingSystems.forEach { os in context("on \(os)") { - context("background updates disabled") { - beforeEach { - testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, operatingSystem: os) - testContext.start() - NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) - expect(testContext.subject.runMode).toEventually(equal(LDClientRunMode.background)) - } - it("takes the sdk offline") { - expect(testContext.subject.isOnline) == true - expect(testContext.subject.runMode) == LDClientRunMode.background - expect(testContext.eventReporterMock.isOnline) == true - expect(testContext.flagSynchronizerMock.isOnline) == false - } + it("background updates disabled") { + let testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, operatingSystem: os) + testContext.start() + NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) + expect(testContext.subject.runMode).toEventually(equal(LDClientRunMode.background)) + + expect(testContext.subject.isOnline) == true + expect(testContext.subject.runMode) == LDClientRunMode.background + expect(testContext.eventReporterMock.isOnline) == true + expect(testContext.flagSynchronizerMock.isOnline) == false } - context("background updates enabled") { - beforeEach { - testContext = TestContext(startOnline: true, operatingSystem: os) - testContext.start() - - waitUntil { done in - NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) - DispatchQueue(label: "BackgroundUpdatesEnabled").asyncAfter(deadline: .now() + 0.2, execute: done) - } - } - it("leaves the sdk online") { - expect(testContext.subject.isOnline) == true - expect(testContext.subject.runMode) == LDClientRunMode.background - expect(testContext.eventReporterMock.isOnline) == true - expect(testContext.flagSynchronizerMock.isOnline) == os.isBackgroundEnabled - expect(testContext.flagSynchronizerMock.streamingMode) == os.backgroundStreamingMode + it("background updates enabled") { + let testContext = TestContext(startOnline: true, operatingSystem: os) + testContext.start() + + waitUntil { done in + NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) + DispatchQueue(label: "BackgroundUpdatesEnabled").asyncAfter(deadline: .now() + 0.2, execute: done) } + + expect(testContext.subject.isOnline) == true + expect(testContext.subject.runMode) == LDClientRunMode.background + expect(testContext.eventReporterMock.isOnline) == true + expect(testContext.flagSynchronizerMock.isOnline) == os.isBackgroundEnabled + expect(testContext.flagSynchronizerMock.streamingMode) == os.backgroundStreamingMode } } } } - context("when offline") { - beforeEach { - testContext = TestContext() - testContext.start() - NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) - } - it("leaves the sdk offline") { - expect(testContext.subject.isOnline) == false - expect(testContext.subject.runMode) == LDClientRunMode.background - expect(testContext.eventReporterMock.isOnline) == false - expect(testContext.flagSynchronizerMock.isOnline) == false - } + it("when offline") { + let testContext = TestContext() + testContext.start() + NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) + + expect(testContext.subject.isOnline) == false + expect(testContext.subject.runMode) == LDClientRunMode.background + expect(testContext.eventReporterMock.isOnline) == false + expect(testContext.flagSynchronizerMock.isOnline) == false } } } @@ -1308,32 +1134,25 @@ final class LDClientSpec: QuickSpec { context("after starting client") { OperatingSystem.allOperatingSystems.forEach { os in context("on \(os)") { - context("when online at foreground notification") { - beforeEach { - testContext = TestContext(startOnline: true, operatingSystem: os) - testContext.start(runMode: .background) - NotificationCenter.default.post(name: testContext.environmentReporterMock.foregroundNotification!, object: self) - } - it("takes the sdk online") { - expect(testContext.subject.isOnline) == true - expect(testContext.subject.runMode) == LDClientRunMode.foreground - expect(testContext.eventReporterMock.isOnline) == true - expect(testContext.flagSynchronizerMock.isOnline) == true - } + it("when online at foreground notification") { + let testContext = TestContext(startOnline: true, operatingSystem: os) + testContext.start(runMode: .background) + NotificationCenter.default.post(name: testContext.environmentReporterMock.foregroundNotification!, object: self) + + expect(testContext.subject.isOnline) == true + expect(testContext.subject.runMode) == LDClientRunMode.foreground + expect(testContext.eventReporterMock.isOnline) == true + expect(testContext.flagSynchronizerMock.isOnline) == true } - context("when offline at foreground notification") { - beforeEach { - testContext = TestContext(operatingSystem: os) - testContext.start(runMode: .background) + it("when offline at foreground notification") { + let testContext = TestContext(operatingSystem: os) + testContext.start(runMode: .background) + NotificationCenter.default.post(name: testContext.environmentReporterMock.foregroundNotification!, object: self) - NotificationCenter.default.post(name: testContext.environmentReporterMock.foregroundNotification!, object: self) - } - it("leaves the sdk offline") { - expect(testContext.subject.isOnline) == false - expect(testContext.subject.runMode) == LDClientRunMode.foreground - expect(testContext.eventReporterMock.isOnline) == false - expect(testContext.flagSynchronizerMock.isOnline) == false - } + expect(testContext.subject.isOnline) == false + expect(testContext.subject.runMode) == LDClientRunMode.foreground + expect(testContext.eventReporterMock.isOnline) == false + expect(testContext.flagSynchronizerMock.isOnline) == false } } } @@ -1345,124 +1164,87 @@ final class LDClientSpec: QuickSpec { context("and running in the foreground") { context("set background") { context("with background updates enabled") { - context("streaming mode") { - beforeEach { - testContext = TestContext(startOnline: true, operatingSystem: .macOS) - testContext.start() - testContext.subject.setRunMode(.background) - } - it("leaves the event reporter online") { - expect(testContext.eventReporterMock.isOnline) == true - } - it("sets the flag synchronizer for background streaming online") { - expect(testContext.flagSynchronizerMock.isOnline) == true - expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming - } - } - context("polling mode") { - beforeEach { - testContext = TestContext(startOnline: true, streamingMode: .polling, operatingSystem: .macOS) - testContext.start() - testContext.subject.setRunMode(.background) - } - it("leaves the event reporter online") { - expect(testContext.eventReporterMock.isOnline) == true - } - it("sets the flag synchronizer for background polling online") { - expect(testContext.flagSynchronizerMock.isOnline) == true - expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.polling - expect(testContext.flagSynchronizerMock.pollingInterval) == testContext.config.flagPollingInterval(runMode: .background) - } - } - } - context("with background updates disabled") { - beforeEach { - testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, operatingSystem: .macOS) + it("streaming mode") { + let testContext = TestContext(startOnline: true, operatingSystem: .macOS) testContext.start() testContext.subject.setRunMode(.background) - } - it("leaves the event reporter online") { + expect(testContext.eventReporterMock.isOnline) == true + expect(testContext.flagSynchronizerMock.isOnline) == true + expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming } - it("sets the flag synchronizer for background polling offline") { - expect(testContext.flagSynchronizerMock.isOnline) == false + it("polling mode") { + let testContext = TestContext(startOnline: true, streamingMode: .polling, operatingSystem: .macOS) + testContext.start() + testContext.subject.setRunMode(.background) + + expect(testContext.eventReporterMock.isOnline) == true + expect(testContext.flagSynchronizerMock.isOnline) == true expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.polling expect(testContext.flagSynchronizerMock.pollingInterval) == testContext.config.flagPollingInterval(runMode: .background) } } - } - context("set foreground") { - var eventReporterIsOnlineSetCount: Int! - var flagSynchronizerIsOnlineSetCount: Int! - var makeFlagSynchronizerCallCount: Int! - beforeEach { - testContext = TestContext(startOnline: true, operatingSystem: .macOS) + it("with background updates disabled") { + let testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, operatingSystem: .macOS) testContext.start() - eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount - flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount - makeFlagSynchronizerCallCount = testContext.serviceFactoryMock.makeFlagSynchronizerCallCount - testContext.subject.setRunMode(.foreground) - } - it("makes no changes") { + testContext.subject.setRunMode(.background) + expect(testContext.eventReporterMock.isOnline) == true - expect(testContext.eventReporterMock.isOnlineSetCount) == eventReporterIsOnlineSetCount - expect(testContext.flagSynchronizerMock.isOnline) == true - expect(testContext.flagSynchronizerMock.isOnlineSetCount) == flagSynchronizerIsOnlineSetCount - expect(testContext.serviceFactoryMock.makeFlagSynchronizerCallCount) == makeFlagSynchronizerCallCount + expect(testContext.flagSynchronizerMock.isOnline) == false + expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.polling + expect(testContext.flagSynchronizerMock.pollingInterval) == testContext.config.flagPollingInterval(runMode: .background) } } + it("set foreground") { + let testContext = TestContext(startOnline: true, operatingSystem: .macOS) + testContext.start() + let eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount + let flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount + let makeFlagSynchronizerCallCount = testContext.serviceFactoryMock.makeFlagSynchronizerCallCount + testContext.subject.setRunMode(.foreground) + + expect(testContext.eventReporterMock.isOnline) == true + expect(testContext.eventReporterMock.isOnlineSetCount) == eventReporterIsOnlineSetCount + expect(testContext.flagSynchronizerMock.isOnline) == true + expect(testContext.flagSynchronizerMock.isOnlineSetCount) == flagSynchronizerIsOnlineSetCount + expect(testContext.serviceFactoryMock.makeFlagSynchronizerCallCount) == makeFlagSynchronizerCallCount + } } context("and running in the background") { - context("set background") { - var eventReporterIsOnlineSetCount: Int! - var flagSynchronizerIsOnlineSetCount: Int! - var makeFlagSynchronizerCallCount: Int! - beforeEach { - testContext = TestContext(startOnline: true, operatingSystem: .macOS) - testContext.start() - testContext.subject.setRunMode(.background) - eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount - flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount - makeFlagSynchronizerCallCount = testContext.serviceFactoryMock.makeFlagSynchronizerCallCount - testContext.subject.setRunMode(.background) - } - it("makes no changes") { - expect(testContext.eventReporterMock.isOnline) == true - expect(testContext.eventReporterMock.isOnlineSetCount) == eventReporterIsOnlineSetCount - expect(testContext.flagSynchronizerMock.isOnline) == true - expect(testContext.flagSynchronizerMock.isOnlineSetCount) == flagSynchronizerIsOnlineSetCount - expect(testContext.serviceFactoryMock.makeFlagSynchronizerCallCount) == makeFlagSynchronizerCallCount - } + it("set background") { + let testContext = TestContext(startOnline: true, operatingSystem: .macOS) + testContext.start() + testContext.subject.setRunMode(.background) + let eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount + let flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount + let makeFlagSynchronizerCallCount = testContext.serviceFactoryMock.makeFlagSynchronizerCallCount + testContext.subject.setRunMode(.background) + + expect(testContext.eventReporterMock.isOnline) == true + expect(testContext.eventReporterMock.isOnlineSetCount) == eventReporterIsOnlineSetCount + expect(testContext.flagSynchronizerMock.isOnline) == true + expect(testContext.flagSynchronizerMock.isOnlineSetCount) == flagSynchronizerIsOnlineSetCount + expect(testContext.serviceFactoryMock.makeFlagSynchronizerCallCount) == makeFlagSynchronizerCallCount } context("set foreground") { - context("streaming mode") { - beforeEach { - testContext = TestContext(startOnline: true, operatingSystem: .macOS) - testContext.start(runMode: .background) - testContext.subject.setRunMode(.foreground) - } - it("takes the event reporter online") { - expect(testContext.eventReporterMock.isOnline) == true - } - it("sets the flag synchronizer for foreground streaming online") { - expect(testContext.flagSynchronizerMock.isOnline) == true - expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming - } + it("streaming mode") { + let testContext = TestContext(startOnline: true, operatingSystem: .macOS) + testContext.start(runMode: .background) + testContext.subject.setRunMode(.foreground) + + expect(testContext.eventReporterMock.isOnline) == true + expect(testContext.flagSynchronizerMock.isOnline) == true + expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming } - context("polling mode") { - beforeEach { - testContext = TestContext(startOnline: true, streamingMode: .polling, operatingSystem: .macOS) - testContext.start(runMode: .background) - testContext.subject.setRunMode(.foreground) - } - it("takes the event reporter online") { - expect(testContext.eventReporterMock.isOnline) == true - } - it("sets the flag synchronizer for foreground polling online") { - expect(testContext.flagSynchronizerMock.isOnline) == true - expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.polling - expect(testContext.flagSynchronizerMock.pollingInterval) == testContext.config.flagPollingInterval(runMode: .foreground) - } + it("polling mode") { + let testContext = TestContext(startOnline: true, streamingMode: .polling, operatingSystem: .macOS) + testContext.start(runMode: .background) + testContext.subject.setRunMode(.foreground) + + expect(testContext.eventReporterMock.isOnline) == true + expect(testContext.flagSynchronizerMock.isOnline) == true + expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.polling + expect(testContext.flagSynchronizerMock.pollingInterval) == testContext.config.flagPollingInterval(runMode: .foreground) } } } @@ -1470,121 +1252,81 @@ final class LDClientSpec: QuickSpec { context("while offline") { context("and running in the foreground") { context("set background") { - context("with background updates enabled") { - beforeEach { - waitUntil { done in - testContext = TestContext(operatingSystem: .macOS) - testContext.start(completion: done) - } - testContext.subject.setRunMode(.background) - } - it("leaves the event reporter offline") { - expect(testContext.eventReporterMock.isOnline) == false - } - it("configures the flag synchronizer for background streaming offline") { - expect(testContext.flagSynchronizerMock.isOnline) == false - expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming - } - } - context("with background updates disabled") { - beforeEach { - waitUntil { done in - testContext = TestContext(enableBackgroundUpdates: false, operatingSystem: .macOS) - testContext.start(completion: done) - } - testContext.subject.setRunMode(.background) - } - it("leaves the event reporter offline") { - expect(testContext.eventReporterMock.isOnline) == false - } - it("configures the flag synchronizer for background polling offline") { - expect(testContext.flagSynchronizerMock.isOnline) == false - expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.polling - expect(testContext.flagSynchronizerMock.pollingInterval) == testContext.config.flagPollingInterval(runMode: .background) - } - } - } - context("set foreground") { - var eventReporterIsOnlineSetCount: Int! - var flagSynchronizerIsOnlineSetCount: Int! - var makeFlagSynchronizerCallCount: Int! - beforeEach { - waitUntil { done in - testContext = TestContext(operatingSystem: .macOS) - testContext.start(completion: done) - } - - eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount - flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount - makeFlagSynchronizerCallCount = testContext.serviceFactoryMock.makeFlagSynchronizerCallCount - testContext.subject.setRunMode(.foreground) - } - it("makes no changes") { + it("with background updates enabled") { + let testContext = TestContext(operatingSystem: .macOS) + waitUntil { testContext.start(completion: $0) } + + testContext.subject.setRunMode(.background) + expect(testContext.eventReporterMock.isOnline) == false - expect(testContext.eventReporterMock.isOnlineSetCount) == eventReporterIsOnlineSetCount expect(testContext.flagSynchronizerMock.isOnline) == false - expect(testContext.flagSynchronizerMock.isOnlineSetCount) == flagSynchronizerIsOnlineSetCount - expect(testContext.serviceFactoryMock.makeFlagSynchronizerCallCount) == makeFlagSynchronizerCallCount + expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming } - } - } - context("and running in the background") { - context("set background") { - var eventReporterIsOnlineSetCount: Int! - var flagSynchronizerIsOnlineSetCount: Int! - var makeFlagSynchronizerCallCount: Int! - beforeEach { - waitUntil { done in - testContext = TestContext(operatingSystem: .macOS) - testContext.start(runMode: .background, completion: done) - } - - eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount - flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount - makeFlagSynchronizerCallCount = testContext.serviceFactoryMock.makeFlagSynchronizerCallCount + it("with background updates disabled") { + let testContext = TestContext(enableBackgroundUpdates: false, operatingSystem: .macOS) + waitUntil { testContext.start(completion: $0) } + testContext.subject.setRunMode(.background) - } - it("makes no changes") { + expect(testContext.eventReporterMock.isOnline) == false - expect(testContext.eventReporterMock.isOnlineSetCount) == eventReporterIsOnlineSetCount expect(testContext.flagSynchronizerMock.isOnline) == false - expect(testContext.flagSynchronizerMock.isOnlineSetCount) == flagSynchronizerIsOnlineSetCount - expect(testContext.serviceFactoryMock.makeFlagSynchronizerCallCount) == makeFlagSynchronizerCallCount + expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.polling + expect(testContext.flagSynchronizerMock.pollingInterval) == testContext.config.flagPollingInterval(runMode: .background) } } + it("set foreground") { + let testContext = TestContext(operatingSystem: .macOS) + waitUntil { testContext.start(completion: $0) } + + let eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount + let flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount + let makeFlagSynchronizerCallCount = testContext.serviceFactoryMock.makeFlagSynchronizerCallCount + testContext.subject.setRunMode(.foreground) + + expect(testContext.eventReporterMock.isOnline) == false + expect(testContext.eventReporterMock.isOnlineSetCount) == eventReporterIsOnlineSetCount + expect(testContext.flagSynchronizerMock.isOnline) == false + expect(testContext.flagSynchronizerMock.isOnlineSetCount) == flagSynchronizerIsOnlineSetCount + expect(testContext.serviceFactoryMock.makeFlagSynchronizerCallCount) == makeFlagSynchronizerCallCount + } + } + context("and running in the background") { + it("set background") { + let testContext = TestContext(operatingSystem: .macOS) + waitUntil { testContext.start(runMode: .background, completion: $0) } + + let eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount + let flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount + let makeFlagSynchronizerCallCount = testContext.serviceFactoryMock.makeFlagSynchronizerCallCount + testContext.subject.setRunMode(.background) + + expect(testContext.eventReporterMock.isOnline) == false + expect(testContext.eventReporterMock.isOnlineSetCount) == eventReporterIsOnlineSetCount + expect(testContext.flagSynchronizerMock.isOnline) == false + expect(testContext.flagSynchronizerMock.isOnlineSetCount) == flagSynchronizerIsOnlineSetCount + expect(testContext.serviceFactoryMock.makeFlagSynchronizerCallCount) == makeFlagSynchronizerCallCount + } context("set foreground") { - context("streaming mode") { - beforeEach { - waitUntil { done in - testContext = TestContext(operatingSystem: .macOS) - testContext.start(runMode: .background, completion: done) - } - testContext.subject.setRunMode(.foreground) - } - it("leaves the event reporter offline") { - expect(testContext.eventReporterMock.isOnline) == false - } - it("configures the flag synchronizer for foreground streaming offline") { - expect(testContext.flagSynchronizerMock.isOnline) == false - expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming - } + it("streaming mode") { + let testContext = TestContext(operatingSystem: .macOS) + waitUntil { testContext.start(runMode: .background, completion: $0) } + + testContext.subject.setRunMode(.foreground) + + expect(testContext.eventReporterMock.isOnline) == false + expect(testContext.flagSynchronizerMock.isOnline) == false + expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming } - context("polling mode") { - beforeEach { - waitUntil { done in - testContext = TestContext(streamingMode: .polling, operatingSystem: .macOS) - testContext.start(runMode: .background, completion: done) - } - testContext.subject.setRunMode(.foreground) - } - it("leaves the event reporter offline") { - expect(testContext.eventReporterMock.isOnline) == false - } - it("configures the flag synchronizer for foreground polling offline") { - expect(testContext.flagSynchronizerMock.isOnline) == false - expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.polling - expect(testContext.flagSynchronizerMock.pollingInterval) == testContext.config.flagPollingInterval(runMode: .foreground) - } + it("polling mode") { + let testContext = TestContext(streamingMode: .polling, operatingSystem: .macOS) + waitUntil { testContext.start(runMode: .background, completion: $0) } + + testContext.subject.setRunMode(.foreground) + + expect(testContext.eventReporterMock.isOnline) == false + expect(testContext.flagSynchronizerMock.isOnline) == false + expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.polling + expect(testContext.flagSynchronizerMock.pollingInterval) == testContext.config.flagPollingInterval(runMode: .foreground) } } } @@ -1619,16 +1361,15 @@ final class LDClientSpec: QuickSpec { private func allFlagsSpec() { let stubFlags = FlagMaintainingMock.stubFlags() - var testContext: TestContext! describe("allFlags") { - beforeEach { - testContext = TestContext().withCached(flags: stubFlags) - testContext.start() - } it("returns all non-null flag values from store") { + let testContext = TestContext().withCached(flags: stubFlags) + testContext.start() expect(AnyComparer.isEqual(testContext.subject.allFlags, to: stubFlags.compactMapValues { $0.value })).to(beTrue()) } it("returns nil when client is closed") { + let testContext = TestContext().withCached(flags: stubFlags) + testContext.start() testContext.subject.close() expect(testContext.subject.allFlags).to(beNil()) } @@ -1636,111 +1377,72 @@ final class LDClientSpec: QuickSpec { } private func connectionInformationSpec() { - var testContext: TestContext! - describe("ConnectionInformation") { - context("when client was started in foreground") { - beforeEach { - testContext = TestContext(startOnline: true) - testContext.start() - } - it("returns a ConnectionInformation object with currentConnectionMode.establishingStreamingConnection") { - expect(testContext.subject.isOnline) == true - expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.establishingStreamingConnection)) - } - it("returns a String from toString") { - expect(testContext.subject.connectionInformation.description).to(beAKindOf(String.self)) - } + it("when client was started in foreground") { + let testContext = TestContext(startOnline: true) + testContext.start() + expect(testContext.subject.isOnline) == true + expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.establishingStreamingConnection)) } - context("when client was started in background") { - beforeEach { - testContext = TestContext(startOnline: true, enableBackgroundUpdates: false) - testContext.start() - testContext.subject.setRunMode(.background) - } - it("returns a ConnectionInformation object with currentConnectionMode.offline") { - expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.offline)) - } - it("returns a String from toString") { - expect(testContext.subject.connectionInformation.description).to(beAKindOf(String.self)) - } + it("when client was started in background") { + let testContext = TestContext(startOnline: true, enableBackgroundUpdates: false) + testContext.start() + testContext.subject.setRunMode(.background) + expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.offline)) } - context("when offline and client started") { - beforeEach { - testContext = TestContext() - testContext.start() - } - it("leaves the sdk offline") { - expect(testContext.subject.isOnline) == false - expect(testContext.eventReporterMock.isOnline) == false - expect(testContext.flagSynchronizerMock.isOnline) == false - expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.offline)) - } + it("client started offline") { + let testContext = TestContext() + testContext.start() + expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.offline)) } } } private func variationDetailSpec() { describe("variationDetail") { - context("when client was started and flag key doesn't exist") { - it("returns FLAG_NOT_FOUND") { - let testContext = TestContext() - testContext.start() - let detail = testContext.subject.variationDetail(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool).reason - if let errorKind = detail?["errorKind"] as? String { - expect(errorKind) == "FLAG_NOT_FOUND" - } + it("when flag doesn't exist") { + let testContext = TestContext() + testContext.start() + let detail = testContext.subject.variationDetail(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool).reason + if let errorKind = detail?["errorKind"] as? String { + expect(errorKind) == "FLAG_NOT_FOUND" } } } } private func isInitializedSpec() { - var testContext: TestContext! - describe("isInitialized") { - context("when client was started but no flag update") { - beforeEach { - testContext = TestContext(startOnline: true) - testContext.start() - } - it("returns false") { - expect(testContext.subject.isInitialized) == false - } - it("and then stopped returns false") { - testContext.subject.close() - expect(testContext.subject.isInitialized) == false - } + it("when client was started but no flag update") { + let testContext = TestContext(startOnline: true) + testContext.start() + + expect(testContext.subject.isInitialized) == false + + testContext.subject.close() + expect(testContext.subject.isInitialized) == false } - context("when client was started offline") { - beforeEach { - testContext = TestContext() + it("when client was started offline") { + let testContext = TestContext() + testContext.start() + + expect(testContext.subject.isInitialized) == true + + testContext.subject.close() + expect(testContext.subject.isInitialized) == false + } + for eventType in [nil, FlagUpdateType.ping, FlagUpdateType.put] { + it("when client was started and after receiving flags as " + (eventType?.rawValue ?? "poll")) { + let testContext = TestContext(startOnline: true) testContext.start() - } - it("returns true") { - expect(testContext.subject.isInitialized) == true - } - it("and then stopped returns false") { + testContext.onSyncComplete?(.success([:], eventType)) + + expect(testContext.subject.isInitialized).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + testContext.subject.close() expect(testContext.subject.isInitialized) == false } } - for eventType in [nil, FlagUpdateType.ping, FlagUpdateType.put] { - context("when client was started and after receiving flags as " + (eventType?.rawValue ?? "poll")) { - beforeEach { - testContext = TestContext(startOnline: true) - testContext.start() - testContext.onSyncComplete?(.success([:], eventType)) - } - it("returns true") { - expect(testContext.subject.isInitialized).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) - } - it("and then stopped returns false") { - testContext.subject.close() - expect(testContext.subject.isInitialized) == false - } - } - } } } } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 698470e3..38d5ebb0 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -99,7 +99,7 @@ final class DarklyServiceMock: DarklyServiceProvider { alternateVariationNumber: Bool = true, bumpFlagVersions: Bool = false, alternateValuesForKeys alternateValueKeys: [LDFlagKey] = [], - trackEvents: Bool? = true, + trackEvents: Bool = true, debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0)) -> [LDFlagKey: FeatureFlag] { let flagKeys = includeNullValue ? FlagKeys.knownFlags : FlagKeys.flagsWithAnAlternateValue @@ -162,7 +162,7 @@ final class DarklyServiceMock: DarklyServiceProvider { useAlternateVersion: Bool = false, useAlternateFlagVersion: Bool = false, useAlternateVariationNumber: Bool = true, - trackEvents: Bool? = true, + trackEvents: Bool = true, debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0), includeEvaluationReason: Bool = false, includeTrackReason: Bool = false) -> FeatureFlag { diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift index 1befd42f..6df3061a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift @@ -58,10 +58,10 @@ final class FeatureFlagSpec: QuickSpec { expect(featureFlag.value).to(beNil()) expect(featureFlag.variation).to(beNil()) expect(featureFlag.version).to(beNil()) - expect(featureFlag.trackEvents).to(beNil()) + expect(featureFlag.trackEvents) == false expect(featureFlag.debugEventsUntilDate).to(beNil()) expect(featureFlag.reason).to(beNil()) - expect(featureFlag.trackReason).to(beNil()) + expect(featureFlag.trackReason) == false } } } @@ -127,7 +127,7 @@ final class FeatureFlagSpec: QuickSpec { expect(featureFlag?.variation).to(beNil()) expect(featureFlag?.version).to(beNil()) expect(featureFlag?.flagVersion).to(beNil()) - expect(featureFlag?.trackEvents).to(beNil()) + expect(featureFlag?.trackEvents) == false } } } @@ -148,7 +148,7 @@ final class FeatureFlagSpec: QuickSpec { expect(featureFlag?.variation) == DarklyServiceMock.Constants.variation expect(featureFlag?.version).to(beNil()) expect(featureFlag?.flagVersion).to(beNil()) - expect(featureFlag?.trackEvents).to(beNil()) + expect(featureFlag?.trackEvents) == false } } context("when dictionary only contains the key and version") { @@ -167,7 +167,7 @@ final class FeatureFlagSpec: QuickSpec { expect(featureFlag?.variation).to(beNil()) expect(featureFlag?.version) == DarklyServiceMock.Constants.version expect(featureFlag?.flagVersion).to(beNil()) - expect(featureFlag?.trackEvents).to(beNil()) + expect(featureFlag?.trackEvents) == false } } context("when dictionary only contains the key and flagVersion") { @@ -187,7 +187,7 @@ final class FeatureFlagSpec: QuickSpec { expect(featureFlag?.variation).to(beNil()) expect(featureFlag?.version).to(beNil()) expect(featureFlag?.flagVersion) == DarklyServiceMock.Constants.flagVersion - expect(featureFlag?.trackEvents).to(beNil()) + expect(featureFlag?.trackEvents) == false } } context("when dictionary only contains the key and trackEvents") { @@ -261,7 +261,7 @@ final class FeatureFlagSpec: QuickSpec { } context("without elements") { beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false, trackEvents: nil, debugEventsUntilDate: nil) + featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false, trackEvents: false, debugEventsUntilDate: nil) } it("creates a dictionary with the value including nil value and version representations") { featureFlags.forEach { flagKey, featureFlag in @@ -462,82 +462,44 @@ final class FeatureFlagSpec: QuickSpec { private func shouldCreateDebugEventsSpec() { describe("shouldCreateDebugEventsSpec") { - var lastEventResponseDate: Date! - var shouldCreateDebugEvents: Bool! - var flag: FeatureFlag! - beforeEach { - flag = FeatureFlag(flagKey: "test-key") - } context("lastEventResponseDate exists") { - context("debugEventsUntilDate hasn't passed lastEventResponseDate") { - beforeEach { - lastEventResponseDate = Date().addingTimeInterval(-1.0) - flag = FeatureFlag(copying: flag, trackEvents: true, debugEventsUntilDate: Date()) - shouldCreateDebugEvents = flag.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate) - } - it("returns true") { - expect(shouldCreateDebugEvents) == true - } + it("debugEventsUntilDate hasn't passed lastEventResponseDate") { + let lastEventResponseDate = Date().addingTimeInterval(-1.0) + let flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: Date()) + expect(flag.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate)) == true } - context("debugEventsUntilDate is lastEventResponseDate") { - beforeEach { - lastEventResponseDate = Date() - flag = FeatureFlag(copying: flag, trackEvents: true, debugEventsUntilDate: lastEventResponseDate) - shouldCreateDebugEvents = flag.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate) - } - it("returns true") { - expect(shouldCreateDebugEvents) == true - } + it("debugEventsUntilDate is lastEventResponseDate") { + let lastEventResponseDate = Date() + let flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: lastEventResponseDate) + expect(flag.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate)) == true } - context("debugEventsUntilDate has passed lastEventResponseDate") { - beforeEach { - lastEventResponseDate = Date().addingTimeInterval(1.0) - flag = FeatureFlag(copying: flag, trackEvents: true, debugEventsUntilDate: Date()) - shouldCreateDebugEvents = flag.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate) - } - it("returns false") { - expect(shouldCreateDebugEvents) == false - } + it("debugEventsUntilDate has passed lastEventResponseDate") { + let lastEventResponseDate = Date().addingTimeInterval(1.0) + let flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: Date()) + expect(flag.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate)) == false } } context("lastEventResponseDate does not exist") { - context("debugEventsUntilDate hasn't passed system date") { - beforeEach { - flag = FeatureFlag(copying: flag, trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(1.0)) - shouldCreateDebugEvents = flag.shouldCreateDebugEvents(lastEventReportResponseTime: nil) - } - it("returns true") { - expect(shouldCreateDebugEvents) == true - } - } - context("debugEventsUntilDate is system date") { - beforeEach { - // Without creating a SystemDateServiceMock and corresponding service protocol, this is really difficult to test, but the level of accuracy is not crucial. Since the debugEventsUntilDate comes in millisSince1970, setting the debugEventsUntilDate to 1 millisecond beyond the date seems like it will get "close enough" to the current date - flag = FeatureFlag(copying: flag, trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(0.001)) - shouldCreateDebugEvents = flag.shouldCreateDebugEvents(lastEventReportResponseTime: nil) - } - it("returns true") { - expect(shouldCreateDebugEvents) == true - } - } - context("debugEventsUntilDate has passed system date") { - beforeEach { - flag = FeatureFlag(copying: flag, trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(-1.0)) - shouldCreateDebugEvents = flag.shouldCreateDebugEvents(lastEventReportResponseTime: nil) - } - it("returns false") { - expect(shouldCreateDebugEvents) == false - } + it("debugEventsUntilDate hasn't passed system date") { + let flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(1.0)) + expect(flag.shouldCreateDebugEvents(lastEventReportResponseTime: nil)) == true + } + it("debugEventsUntilDate is system date") { + // Without creating a SystemDateServiceMock and corresponding service protocol, this is really + // difficult to test, but the level of accuracy is not crucial. Since the debugEventsUntilDate comes + // in millisSince1970, setting the debugEventsUntilDate to 1 millisecond beyond the date seems like + // it will get "close enough" to the current date + let flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(0.001)) + expect(flag.shouldCreateDebugEvents(lastEventReportResponseTime: nil)) == true + } + it("debugEventsUntilDate has passed system date") { + let flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(-1.0)) + expect(flag.shouldCreateDebugEvents(lastEventReportResponseTime: nil)) == false } } - context("debugEventsUntilDate doesn't exist") { - beforeEach { - flag = FeatureFlag(copying: flag, trackEvents: true, debugEventsUntilDate: nil) - shouldCreateDebugEvents = flag.shouldCreateDebugEvents(lastEventReportResponseTime: Date()) - } - it("returns false") { - expect(shouldCreateDebugEvents) == false - } + it("debugEventsUntilDate doesn't exist") { + let flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: nil) + expect(flag.shouldCreateDebugEvents(lastEventReportResponseTime: Date())) == false } } } @@ -685,7 +647,7 @@ final class FeatureFlagSpec: QuickSpec { featureFlags = flagDictionaries.flagCollection } it("returns the existing FeatureFlag dictionary") { - expect(featureFlags == flagDictionaries).to(beTrue()) + expect(AnyComparer.isEqual(featureFlags, to: flagDictionaries)).to(beTrue()) } } context("dictionary does not convert into FeatureFlags") { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift index 1df77c31..91cc6a08 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift @@ -87,24 +87,12 @@ final class DiagnosticCacheSpec: QuickSpec { let diagnosticCache = DiagnosticCache(sdkKey: "this_is_a_fake_key") let diagnosticId = diagnosticCache.getDiagnosticId() - let requestQueue = DispatchQueue(label: "com.launchdarkly.test.diagnosticCacheSpec.incrementDroppedEventCount.concurrent", - qos: .userInitiated, - attributes: .concurrent) - var incrementCallCount = 0 - waitUntil { done in - let fireTime = DispatchTime.now() + 0.2 - for _ in 0..<10 { - requestQueue.asyncAfter(deadline: fireTime) { - diagnosticCache.incrementDroppedEventCount() - DispatchQueue.main.async { - incrementCallCount += 1 - if incrementCallCount == 10 { - done() - } - } - } - } + let counter = DispatchSemaphore(value: 0) + DispatchQueue.concurrentPerform(iterations: 10) { _ in + diagnosticCache.incrementDroppedEventCount() + counter.signal() } + (0..<10).forEach { _ in counter.wait() } let diagnosticStats = diagnosticCache.getCurrentStatsAndReset() expect(UUID(uuidString: diagnosticId.diagnosticId)).toNot(beNil()) @@ -253,10 +241,9 @@ final class DiagnosticCacheSpec: QuickSpec { private func backingStoreSpec() { context("backing store") { - beforeEach { - self.clearStoredCaches() - } it("stores to expected key") { + self.clearStoredCaches() + let expectedDataKey = "com.launchdarkly.DiagnosticCache.diagnosticData.this_is_a_fake_key" let defaults = UserDefaults.standard let beforeData = defaults.data(forKey: expectedDataKey) diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift index 549aff21..981a616d 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift @@ -55,7 +55,7 @@ final class DiagnosticReporterSpec: XCTestCase { } func expectNoEvent() { - XCTAssertEqual(awaiter.wait(timeout: DispatchTime.now() + 1.0), .timedOut) + XCTAssertEqual(awaiter.wait(timeout: DispatchTime.now() + 0.1), .timedOut) XCTAssertTrue(receivedEvents.isEmpty) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index c2529480..caa3f72a 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -591,21 +591,13 @@ final class EventReporterSpec: QuickSpec { let reporter = EventReporter(service: serviceMock, onSyncComplete: nil) reporter.setLastEventResponseDate(Date()) let flag = FeatureFlag(flagKey: "unused", trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(3.0)) - waitUntil { done in - var recordFlagEvaluationCompletionCallCount = 0 - let recordFlagEvaluationCompletion = { - DispatchQueue.main.async { - recordFlagEvaluationCompletionCallCount += 1 - if recordFlagEvaluationCompletionCallCount == 10 { - done() - } - } - } - DispatchQueue.concurrentPerform(iterations: 10) { _ in - reporter.recordFlagEvaluationEvents(flagKey: "flag-key", value: "a", defaultValue: "b", featureFlag: flag, user: user, includeReason: false) - recordFlagEvaluationCompletion() - } + + let counter = DispatchSemaphore(value: 0) + DispatchQueue.concurrentPerform(iterations: 10) { _ in + reporter.recordFlagEvaluationEvents(flagKey: "flag-key", value: "a", defaultValue: "b", featureFlag: flag, user: user, includeReason: false) + counter.signal() } + (0..<10).forEach { _ in counter.wait() } expect(reporter.eventStore.count) == 20 expect(reporter.eventStore.filter { $0.kind == .feature }.count) == 10 @@ -643,17 +635,14 @@ final class EventReporterSpec: QuickSpec { } } } - context("without events") { - beforeEach { - testContext = TestContext(eventFlushInterval: Constants.eventFlushIntervalHalfSecond) - testContext.eventReporter.isOnline = true - } - it("doesn't report events") { - waitUntil { done in - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Constants.eventFlushIntervalHalfSecond) { - expect(testContext.serviceMock.publishEventDataCallCount) == 0 - done() - } + it("without events") { + testContext = TestContext(eventFlushInterval: Constants.eventFlushIntervalHalfSecond) + testContext.eventReporter.isOnline = true + + waitUntil { done in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Constants.eventFlushIntervalHalfSecond) { + expect(testContext.serviceMock.publishEventDataCallCount) == 0 + done() } } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift index 31125317..2b781380 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift @@ -527,10 +527,11 @@ final class FlagSynchronizerSpec: QuickSpec { streamCreated: true, streamOpened: true, streamClosed: false) }).to(match()) - expect(flagDictionary == FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, - value: DarklyServiceMock.FlagValues.int + 1, - variation: DarklyServiceMock.Constants.variation + 1, - version: DarklyServiceMock.Constants.version + 1)).to(beTrue()) + let stubPatch = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, + value: DarklyServiceMock.FlagValues.int + 1, + variation: DarklyServiceMock.Constants.variation + 1, + version: DarklyServiceMock.Constants.version + 1) + expect(AnyComparer.isEqual(flagDictionary, to: stubPatch)).to(beTrue()) expect(streamingEvent) == .patch } } @@ -597,7 +598,8 @@ final class FlagSynchronizerSpec: QuickSpec { streamCreated: true, streamOpened: true, streamClosed: false) }).to(match()) - expect(flagDictionary == FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1)).to(beTrue()) + let stubDelete = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) + expect(AnyComparer.isEqual(flagDictionary, to: stubDelete)).to(beTrue()) expect(streamingEvent) == .delete } } From c6201c58b41ea08b1e3e0e7466ad329b8525e514 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 25 Mar 2022 12:04:32 -0500 Subject: [PATCH 43/50] (V6) Add Codable instance for FeatureFlag. (#192) --- .../Models/FeatureFlag/FeatureFlag.swift | 63 ++++++++- .../Models/FeatureFlag/FeatureFlagSpec.swift | 120 ++++++++++++++++++ 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift index b8c1b41d..2e7abae4 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift @@ -1,6 +1,6 @@ import Foundation -struct FeatureFlag { +struct FeatureFlag: Codable { enum CodingKeys: String, CodingKey, CaseIterable { case flagKey = "key", value, variation, version, flagVersion, trackEvents, debugEventsUntilDate, reason, trackReason @@ -55,6 +55,45 @@ struct FeatureFlag { trackReason: dictionary.trackReason ?? false) } + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let flagKey = try container.decode(LDFlagKey.self, forKey: .flagKey) + try self.init(flagKey: flagKey, container: container) + } + + fileprivate init(flagKey: LDFlagKey, container: KeyedDecodingContainer) throws { + let containedFlagKey = try container.decodeIfPresent(LDFlagKey.self, forKey: .flagKey) + if let contained = containedFlagKey, contained != flagKey { + let description = "key in flag model \"\(contained)\" does not match contextual flag key \"\(flagKey)\"" + throw DecodingError.dataCorruptedError(forKey: .flagKey, in: container, debugDescription: description) + } + self.flagKey = flagKey + self.value = (try container.decodeIfPresent(LDValue.self, forKey: .value))?.toAny() + self.variation = try container.decodeIfPresent(Int.self, forKey: .variation) + self.version = try container.decodeIfPresent(Int.self, forKey: .version) + self.flagVersion = try container.decodeIfPresent(Int.self, forKey: .flagVersion) + self.trackEvents = (try container.decodeIfPresent(Bool.self, forKey: .trackEvents)) ?? false + self.debugEventsUntilDate = Date(millisSince1970: try container.decodeIfPresent(Int64.self, forKey: .debugEventsUntilDate)) + self.reason = (try container.decodeIfPresent(LDValue.self, forKey: .reason))?.toAny() as? [String: Any] + self.trackReason = (try container.decodeIfPresent(Bool.self, forKey: .trackReason)) ?? false + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(flagKey, forKey: .flagKey) + let val = LDValue.fromAny(value) + if val != .null { try container.encode(val, forKey: .value) } + try container.encodeIfPresent(variation, forKey: .variation) + try container.encodeIfPresent(version, forKey: .version) + try container.encodeIfPresent(flagVersion, forKey: .flagVersion) + if trackEvents { try container.encode(true, forKey: .trackEvents) } + if let debugEventsUntilDate = debugEventsUntilDate { + try container.encode(debugEventsUntilDate.millisSince1970, forKey: .debugEventsUntilDate) + } + if reason != nil { try container.encode(LDValue.fromAny(reason), forKey: .reason) } + if trackReason { try container.encode(true, forKey: .trackReason) } + } + var dictionaryValue: [String: Any] { var dictionaryValue = [String: Any]() dictionaryValue[CodingKeys.flagKey.rawValue] = flagKey @@ -74,6 +113,28 @@ struct FeatureFlag { } } +struct FeatureFlagCollection: Codable { + let flags: [LDFlagKey: FeatureFlag] + + init(_ flags: [FeatureFlag]) { + self.flags = Dictionary(uniqueKeysWithValues: flags.map { ($0.flagKey, $0) }) + } + + init(from decoder: Decoder) throws { + var allFlags: [LDFlagKey: FeatureFlag] = [:] + let container = try decoder.container(keyedBy: DynamicKey.self) + try container.allKeys.forEach { key in + let flagContainer = try container.nestedContainer(keyedBy: FeatureFlag.CodingKeys.self, forKey: key) + allFlags[key.stringValue] = try FeatureFlag(flagKey: key.stringValue, container: flagContainer) + } + self.flags = allFlags + } + + func encode(to encoder: Encoder) throws { + try flags.encode(to: encoder) + } +} + extension FeatureFlag: Equatable { static func == (lhs: FeatureFlag, rhs: FeatureFlag) -> Bool { lhs.flagKey == rhs.flagKey && diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift index 6df3061a..61668b22 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift @@ -13,11 +13,131 @@ final class FeatureFlagSpec: QuickSpec { override func spec() { initSpec() dictionaryValueSpec() + codableSpec() + flagCollectionSpec() equalsSpec() shouldCreateDebugEventsSpec() collectionSpec() } + func codableSpec() { + describe("codable") { + it("decode minimal") { + let minimal: LDValue = ["key": "flag-key"] + let flag = try JSONDecoder().decode(FeatureFlag.self, from: try JSONEncoder().encode(minimal)) + expect(flag.flagKey) == "flag-key" + expect(flag.value).to(beNil()) + expect(flag.variation).to(beNil()) + expect(flag.version).to(beNil()) + expect(flag.flagVersion).to(beNil()) + expect(flag.trackEvents) == false + expect(flag.debugEventsUntilDate).to(beNil()) + expect(flag.reason).to(beNil()) + expect(flag.trackReason) == false + } + it("decode full") { + let now = Date().millisSince1970 + let value: LDValue = ["key": "flag-key", "value": [1, 2, 3], "variation": 2, "version": 3, + "flagVersion": 4, "trackEvents": false, "debugEventsUntilDate": .number(Double(now)), + "reason": ["kind": "OFF"], "trackReason": true] + let flag = try JSONDecoder().decode(FeatureFlag.self, from: try JSONEncoder().encode(value)) + expect(flag.flagKey) == "flag-key" + expect(LDValue.fromAny(flag.value)) == [1, 2, 3] + expect(flag.variation) == 2 + expect(flag.version) == 3 + expect(flag.flagVersion) == 4 + expect(flag.trackEvents) == false + expect(flag.debugEventsUntilDate?.millisSince1970) == now + expect(LDValue.fromAny(flag.reason)) == ["kind": "OFF"] + expect(flag.trackReason) == true + } + it("decode with extra fields") { + let extra: LDValue = ["key": "flag-key", "unused": "foo"] + let flag = try JSONDecoder().decode(FeatureFlag.self, from: try JSONEncoder().encode(extra)) + expect(flag.flagKey) == "flag-key" + } + it("decode missing key") { + let testData = try JSONEncoder().encode([:] as LDValue) + expect(try JSONDecoder().decode(FeatureFlag.self, from: testData)).to(throwError(errorType: DecodingError.self) { err in + guard case .keyNotFound = err + else { return fail("Expected key not found error") } + }) + } + it("decode mismatched type") { + let encoder = JSONEncoder() + let invalidValues: [LDValue] = [[], ["key": 5], ["key": "a", "variation": "1"], + ["key": "a", "version": "1"], ["key": "a", "flagVersion": "1"], + ["key": "a", "trackEvents": "1"], ["key": "a", "trackReason": "1"], + ["key": "a", "debugEventsUntilDate": "1"]] + try invalidValues.map { try encoder.encode($0) }.forEach { + expect(try JSONDecoder().decode(FeatureFlag.self, from: $0)).to(throwError(errorType: DecodingError.self) { err in + guard case .typeMismatch = err + else { return fail("Expected type mismatch error") } + }) + } + } + it("encode minimal") { + let flag = FeatureFlag(flagKey: "flag-key") + encodesToObject(flag) { value in + expect(value.count) == 1 + expect(value["key"]) == "flag-key" + } + } + it("encode full") { + let now = Date() + let flag = FeatureFlag(flagKey: "flag-key", value: [1, 2, 3], variation: 2, version: 3, flagVersion: 4, + trackEvents: true, debugEventsUntilDate: now, reason: ["kind": "OFF"], trackReason: true) + encodesToObject(flag) { value in + expect(value.count) == 9 + expect(value["key"]) == "flag-key" + expect(value["value"]) == [1, 2, 3] + expect(value["variation"]) == 2 + expect(value["version"]) == 3 + expect(value["flagVersion"]) == 4 + expect(value["trackEvents"]) == true + expect(value["debugEventsUntilDate"]) == .number(Double(now.millisSince1970)) + expect(value["reason"]) == ["kind": "OFF"] + expect(value["trackReason"]) == true + } + } + it("encode omits defaults") { + let flag = FeatureFlag(flagKey: "flag-key", trackEvents: false, trackReason: false) + encodesToObject(flag) { value in + expect(value.count) == 1 + expect(value["key"]) == "flag-key" + } + } + } + } + + func flagCollectionSpec() { + describe("flag collection coding") { + it("decoding non-conflicting keys") { + let testData: LDValue = ["key1": [:], "key2": ["key": "key2"]] + let flagCollection = try JSONDecoder().decode(FeatureFlagCollection.self, from: JSONEncoder().encode(testData)) + expect(flagCollection.flags.count) == 2 + expect(flagCollection.flags["key1"]?.flagKey) == "key1" + expect(flagCollection.flags["key2"]?.flagKey) == "key2" + } + it("decoding conflicting keys throws") { + let testData = try JSONEncoder().encode(["flag-key": ["key": "flag-key2"]] as LDValue) + expect(try JSONDecoder().decode(FeatureFlagCollection.self, from: testData)).to(throwError(errorType: DecodingError.self) { err in + guard case .dataCorrupted = err + else { return fail("Expected type mismatch error") } + }) + } + it("encoding keys") { + encodesToObject(FeatureFlagCollection([FeatureFlag(flagKey: "flag-key")])) { values in + expect(values.count) == 1 + print(values) + valueIsObject(values["flag-key"]) { flagValue in + expect(flagValue["key"]) == "flag-key" + } + } + } + } + } + func initSpec() { describe("init") { var featureFlag: FeatureFlag! From 18c3c9b1d16e91c3ed94265a9b9da2e22a8c473e Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 21 Apr 2022 09:24:17 -0400 Subject: [PATCH 44/50] (V6) LDValue for flags (#193) --- LaunchDarkly.xcodeproj/project.pbxproj | 180 +--- .../GeneratedCode/mocks.generated.swift | 224 ++-- .../LaunchDarkly/Extensions/AnyComparer.swift | 103 -- .../LaunchDarkly/Extensions/Data.swift | 2 +- .../LaunchDarkly/Extensions/Dictionary.swift | 60 -- .../Extensions/JSONSerialization.swift | 11 - LaunchDarkly/LaunchDarkly/LDClient.swift | 61 +- .../LaunchDarkly/LDClientVariation.swift | 247 +++-- LaunchDarkly/LaunchDarkly/LDCommon.swift | 12 +- .../Cache/CacheableEnvironmentFlags.swift | 30 - .../Cache/CacheableUserEnvironmentFlags.swift | 93 -- LaunchDarkly/LaunchDarkly/Models/Event.swift | 2 +- .../Models/FeatureFlag/FeatureFlag.swift | 123 +-- .../FeatureFlag/FlagValue/LDFlagValue.swift | 128 --- .../FlagValue/LDFlagValueConvertible.swift | 118 --- .../FeatureFlag/LDEvaluationDetail.swift | 4 +- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 9 - .../ObjectiveC/ObjcLDChangedFlag.swift | 12 +- .../ObjectiveC/ObjcLDClient.swift | 54 +- .../ServiceObjects/Cache/CacheConverter.swift | 164 ++- .../Cache/DeprecatedCache.swift | 44 - .../Cache/DeprecatedCacheModelV5.swift | 81 -- .../Cache/FeatureFlagCache.swift | 56 + .../Cache/KeyedValueCache.swift | 15 +- .../Cache/UserEnvironmentFlagCache.swift | 100 -- .../ServiceObjects/ClientServiceFactory.swift | 25 +- .../ServiceObjects/FlagChangeNotifier.swift | 15 +- .../ServiceObjects/FlagStore.swift | 93 +- .../ServiceObjects/FlagSynchronizer.swift | 128 +-- LaunchDarkly/LaunchDarkly/Util.swift | 13 + .../Extensions/DictionarySpec.swift | 171 ---- .../LaunchDarklyTests/LDClientSpec.swift | 261 ++--- .../Mocks/ClientServiceMockFactory.swift | 36 +- .../Mocks/DarklyServiceMock.swift | 126 +-- .../Mocks/DeprecatedCacheMock.swift | 71 -- .../Mocks/FlagMaintainingMock.swift | 68 +- .../Mocks/LDEventSourceMock.swift | 30 +- .../Cache/CacheableEnvironmentFlagsSpec.swift | 107 -- .../CacheableUserEnvironmentFlagsSpec.swift | 177 ---- .../LaunchDarklyTests/Models/EventSpec.swift | 4 +- .../Models/FeatureFlag/FeatureFlagSpec.swift | 961 +++--------------- .../FlagRequestTracking/FlagCounterSpec.swift | 2 +- .../Networking/DarklyServiceSpec.swift | 4 +- .../Cache/CacheConverterSpec.swift | 126 +-- .../Cache/DeprecatedCacheModelSpec.swift | 157 --- .../Cache/DeprecatedCacheModelV5Spec.swift | 81 -- .../Cache/FeatureFlagCacheSpec.swift | 137 +++ .../Cache/KeyedValueCacheSpec.swift | 29 - .../Cache/UserEnvironmentFlagCacheSpec.swift | 270 ----- .../ServiceObjects/EventReporterSpec.swift | 1 - .../ServiceObjects/FlagStoreSpec.swift | 268 ++--- .../ServiceObjects/FlagSynchronizerSpec.swift | 930 +++++++---------- SourceryTemplates/mocks.stencil | 8 +- 53 files changed, 1583 insertions(+), 4649 deletions(-) delete mode 100644 LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift delete mode 100644 LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift delete mode 100644 LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift delete mode 100644 LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift delete mode 100644 LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift delete mode 100644 LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift delete mode 100644 LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift delete mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift delete mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift delete mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift create mode 100644 LaunchDarkly/LaunchDarkly/Util.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index afb3c46e..6adc8c12 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -11,6 +11,10 @@ 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; 29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; 29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; + 29FE1298280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; + 29FE1299280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; + 29FE129A280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; + 29FE129B280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */; }; 830DB3AE2239B54900D65D25 /* URLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AD2239B54900D65D25 /* URLResponse.swift */; }; @@ -21,8 +25,6 @@ 831188442113ADC200D77CB5 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; 831188452113ADC500D77CB5 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; 831188462113ADCA00D77CB5 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; - 831188472113ADCD00D77CB5 /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; - 831188482113ADD100D77CB5 /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 8311884B2113ADDA00D77CB5 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -41,11 +43,8 @@ 8311885D2113AE2500D77CB5 /* DarklyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D8B711F71D3E700ED65E8 /* DarklyService.swift */; }; 8311885E2113AE2900D77CB5 /* HTTPURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C2461FE4071F0082B8A9 /* HTTPURLResponse.swift */; }; 8311885F2113AE2D00D77CB5 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; - 831188602113AE3400D77CB5 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67891F97CFEC00403126 /* Dictionary.swift */; }; 831188622113AE3A00D77CB5 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEF51FA24A7E00E428B6 /* Data.swift */; }; - 831188642113AE4200D77CB5 /* JSONSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */; }; 831188652113AE4600D77CB5 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFD1FA24F9600E428B6 /* Date.swift */; }; - 831188662113AE4A00D77CB5 /* AnyComparer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5611FB4D66600550A95 /* AnyComparer.swift */; }; 831188672113AE4D00D77CB5 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D2AAE2061AAA000B4AC3C /* Thread.swift */; }; 831188682113AE5600D77CB5 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 831188692113AE5900D77CB5 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; @@ -70,8 +69,6 @@ 831EF34420655E730001C643 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; 831EF34520655E730001C643 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; 831EF34620655E730001C643 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; - 831EF34720655E730001C643 /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; - 831EF34820655E730001C643 /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -88,11 +85,8 @@ 831EF35B20655E730001C643 /* DarklyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D8B711F71D3E700ED65E8 /* DarklyService.swift */; }; 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C2461FE4071F0082B8A9 /* HTTPURLResponse.swift */; }; 831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; - 831EF35E20655E730001C643 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67891F97CFEC00403126 /* Dictionary.swift */; }; 831EF36020655E730001C643 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEF51FA24A7E00E428B6 /* Data.swift */; }; - 831EF36220655E730001C643 /* JSONSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */; }; 831EF36320655E730001C643 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFD1FA24F9600E428B6 /* Date.swift */; }; - 831EF36420655E730001C643 /* AnyComparer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5611FB4D66600550A95 /* AnyComparer.swift */; }; 831EF36520655E730001C643 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D2AAE2061AAA000B4AC3C /* Thread.swift */; }; 831EF36620655E730001C643 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 831EF36720655E730001C643 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; @@ -101,10 +95,6 @@ 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832307A51F7D8D720029815A /* URLRequestSpec.swift */; }; 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */; }; 832307AA1F7ECA630029815A /* LDConfigStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832307A91F7ECA630029815A /* LDConfigStub.swift */; }; - 832D689D224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */; }; - 832D689E224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */; }; - 832D689F224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */; }; - 832D68A0224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */; }; 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; @@ -118,21 +108,11 @@ 8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8347BB0B21F147E100E56BCD /* LDTimer.swift */; }; 8347BB0E21F147E100E56BCD /* LDTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8347BB0B21F147E100E56BCD /* LDTimer.swift */; }; 8347BB0F21F147E100E56BCD /* LDTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8347BB0B21F147E100E56BCD /* LDTimer.swift */; }; - 8354AC612241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */; }; - 8354AC622241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */; }; - 8354AC632241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */; }; - 8354AC642241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */; }; - 8354AC662241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC652241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift */; }; - 8354AC6922418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */; }; - 8354AC6A22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */; }; - 8354AC6B22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */; }; - 8354AC6C22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */; }; - 8354AC6E22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6D22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift */; }; - 8354AC702243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */; }; - 8354AC712243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */; }; - 8354AC722243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */; }; - 8354AC732243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */; }; - 8354AC77224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC76224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift */; }; + 8354AC702243166900CDE602 /* FeatureFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */; }; + 8354AC712243166900CDE602 /* FeatureFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */; }; + 8354AC722243166900CDE602 /* FeatureFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */; }; + 8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */; }; + 8354AC77224316F800CDE602 /* FeatureFlagCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC76224316F800CDE602 /* FeatureFlagCacheSpec.swift */; }; 8354EFCC1F22491C00C05156 /* LaunchDarkly.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8354EFC21F22491C00C05156 /* LaunchDarkly.framework */; }; 8354EFE01F26380700C05156 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; 8354EFE11F26380700C05156 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; @@ -147,18 +127,12 @@ 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */; }; 835E4C54206BDF8D004C6E6C /* EnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831425B0206B030100F2EF36 /* EnvironmentReporter.swift */; }; 835E4C57206BF7E3004C6E6C /* LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8354EFC51F22491C00C05156 /* LaunchDarkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 8370DF6C225E40B800F84810 /* DeprecatedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */; }; - 8370DF6D225E40B800F84810 /* DeprecatedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */; }; - 8370DF6E225E40B800F84810 /* DeprecatedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */; }; - 8370DF6F225E40B800F84810 /* DeprecatedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */; }; 8372668C20D4439600BD1088 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8372668B20D4439600BD1088 /* DateFormatter.swift */; }; 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8372668B20D4439600BD1088 /* DateFormatter.swift */; }; 8372668E20D4439600BD1088 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8372668B20D4439600BD1088 /* DateFormatter.swift */; }; 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837406D321F760640087B22B /* LDTimerSpec.swift */; }; 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */; }; 837EF3742059C237009D628A /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837EF3732059C237009D628A /* Log.swift */; }; - 838838411F5EFADF0023D11B /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; - 838838451F5EFBAF0023D11B /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; 838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D8B761F72A4B400ED65E8 /* FlagSynchronizerSpec.swift */; }; 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838F96731FB9F024009CFC45 /* LDClientSpec.swift */; }; 838F96781FBA504A009CFC45 /* ClientServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838F96771FBA504A009CFC45 /* ClientServiceFactory.swift */; }; @@ -177,16 +151,11 @@ 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */; }; 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */; }; 83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */; }; - 83D1523B22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */; }; - 83D17EAA1FCDA18C00B2823C /* DictionarySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */; }; 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */; }; - 83D5597E1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D5597D1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift */; }; 83D9EC752062DEAB004D7FA6 /* LDCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6C4B51F4DE7630055351C /* LDCommon.swift */; }; 83D9EC762062DEAB004D7FA6 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; 83D9EC772062DEAB004D7FA6 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; 83D9EC782062DEAB004D7FA6 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; - 83D9EC792062DEAB004D7FA6 /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; - 83D9EC7A2062DEAB004D7FA6 /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 83D9EC7E2062DEAB004D7FA6 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -203,18 +172,14 @@ 83D9EC8D2062DEAB004D7FA6 /* DarklyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D8B711F71D3E700ED65E8 /* DarklyService.swift */; }; 83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C2461FE4071F0082B8A9 /* HTTPURLResponse.swift */; }; 83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; - 83D9EC902062DEAB004D7FA6 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67891F97CFEC00403126 /* Dictionary.swift */; }; 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEF51FA24A7E00E428B6 /* Data.swift */; }; - 83D9EC942062DEAB004D7FA6 /* JSONSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */; }; 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFD1FA24F9600E428B6 /* Date.swift */; }; - 83D9EC962062DEAB004D7FA6 /* AnyComparer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5611FB4D66600550A95 /* AnyComparer.swift */; }; 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D2AAE2061AAA000B4AC3C /* Thread.swift */; }; 83D9EC982062DEAB004D7FA6 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 83D9EC992062DEAB004D7FA6 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; 83D9EC9A2062DEAB004D7FA6 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */; }; 83D9EC9C2062DEAB004D7FA6 /* ObjcLDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */; }; 83DDBEF61FA24A7E00E428B6 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEF51FA24A7E00E428B6 /* Data.swift */; }; - 83DDBEFC1FA24B2700E428B6 /* JSONSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */; }; 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFD1FA24F9600E428B6 /* Date.swift */; }; 83DDBF001FA2589900E428B6 /* FlagStoreSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */; }; 83E2E2061F9E7AC7007514E9 /* LDUserSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83E2E2051F9E7AC7007514E9 /* LDUserSpec.swift */; }; @@ -223,16 +188,13 @@ 83EBCBB420DABE1B003A7142 /* FlagRequestTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */; }; 83EBCBB520DABE1B003A7142 /* FlagRequestTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */; }; 83EBCBB720DABE93003A7142 /* FlagRequestTrackerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB620DABE93003A7142 /* FlagRequestTrackerSpec.swift */; }; - 83EF678A1F97CFEC00403126 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67891F97CFEC00403126 /* Dictionary.swift */; }; 83EF67931F9945E800403126 /* EventSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67921F9945E800403126 /* EventSpec.swift */; }; 83EF67951F994BAD00403126 /* LDUserStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67941F994BAD00403126 /* LDUserStub.swift */; }; - 83F0A5621FB4D66600550A95 /* AnyComparer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5611FB4D66600550A95 /* AnyComparer.swift */; }; 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */; }; 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FEF8DC1F266742001CF12C /* FlagSynchronizer.swift */; }; 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */; }; B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; }; B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.swift */; }; - B43D5AD025FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43D5ACF25FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift */; }; B467791324D8AEEC00897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791224D8AEEC00897F00 /* LDSwiftEventSourceStatic */; }; B467791524D8AEF300897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791424D8AEF300897F00 /* LDSwiftEventSourceStatic */; }; B467791724D8AEF800897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791624D8AEF800897F00 /* LDSwiftEventSourceStatic */; }; @@ -282,7 +244,6 @@ C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; - C48ED691242D27E200464F5F /* DeprecatedCacheMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48ED690242D27E200464F5F /* DeprecatedCacheMock.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -350,6 +311,7 @@ /* Begin PBXFileReference section */ 29A4C47427DA6266005B8D34 /* UserAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAttribute.swift; sourceTree = ""; }; + 29FE1297280413D4008CC918 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLRequest.swift; sourceTree = ""; }; 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersSpec.swift; sourceTree = ""; }; 830DB3AD2239B54900D65D25 /* URLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponse.swift; sourceTree = ""; }; @@ -368,7 +330,6 @@ 832307A51F7D8D720029815A /* URLRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestSpec.swift; sourceTree = ""; }; 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDEventSourceMock.swift; sourceTree = ""; }; 832307A91F7ECA630029815A /* LDConfigStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDConfigStub.swift; sourceTree = ""; }; - 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV5.swift; sourceTree = ""; }; 832D68A1224A38FC005F052A /* CacheConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverter.swift; sourceTree = ""; }; 832D68AB224B3321005F052A /* CacheConverterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverterSpec.swift; sourceTree = ""; }; 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagMaintainingMock.swift; sourceTree = ""; }; @@ -376,12 +337,8 @@ 83396BC81F7C3711000E256E /* DarklyServiceSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarklyServiceSpec.swift; sourceTree = ""; }; 83411A5D1FABDA8700E5CF39 /* mocks.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = mocks.generated.swift; path = LaunchDarkly/GeneratedCode/mocks.generated.swift; sourceTree = SOURCE_ROOT; }; 8347BB0B21F147E100E56BCD /* LDTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDTimer.swift; sourceTree = ""; }; - 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheableEnvironmentFlags.swift; sourceTree = ""; }; - 8354AC652241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheableEnvironmentFlagsSpec.swift; sourceTree = ""; }; - 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheableUserEnvironmentFlags.swift; sourceTree = ""; }; - 8354AC6D22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheableUserEnvironmentFlagsSpec.swift; sourceTree = ""; }; - 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEnvironmentFlagCache.swift; sourceTree = ""; }; - 8354AC76224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEnvironmentFlagCacheSpec.swift; sourceTree = ""; }; + 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagCache.swift; sourceTree = ""; }; + 8354AC76224316F800CDE602 /* FeatureFlagCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagCacheSpec.swift; sourceTree = ""; }; 8354EFC21F22491C00C05156 /* LaunchDarkly.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LaunchDarkly.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8354EFC51F22491C00C05156 /* LaunchDarkly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LaunchDarkly.h; sourceTree = ""; }; 8354EFCB1F22491C00C05156 /* LaunchDarklyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LaunchDarklyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -397,13 +354,10 @@ 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDConfig.swift; sourceTree = ""; }; 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDUser.swift; sourceTree = ""; }; 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDChangedFlag.swift; sourceTree = ""; }; - 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCache.swift; sourceTree = ""; }; 8372668B20D4439600BD1088 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 837406D321F760640087B22B /* LDTimerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDTimerSpec.swift; sourceTree = ""; }; 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentReporterSpec.swift; sourceTree = ""; }; 837EF3732059C237009D628A /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; - 838838401F5EFADF0023D11B /* LDFlagValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDFlagValue.swift; sourceTree = ""; }; - 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDFlagValueConvertible.swift; sourceTree = ""; }; 838F96731FB9F024009CFC45 /* LDClientSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDClientSpec.swift; sourceTree = ""; }; 838F96771FBA504A009CFC45 /* ClientServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientServiceFactory.swift; sourceTree = ""; }; 838F96791FBA551A009CFC45 /* ClientServiceMockFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientServiceMockFactory.swift; sourceTree = ""; }; @@ -420,29 +374,22 @@ 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagsUnchangedObserver.swift; sourceTree = ""; }; 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventReporterSpec.swift; sourceTree = ""; }; 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarklyServiceMock.swift; sourceTree = ""; }; - 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV5Spec.swift; sourceTree = ""; }; - 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionarySpec.swift; sourceTree = ""; }; 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedValueCache.swift; sourceTree = ""; }; - 83D5597D1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedValueCacheSpec.swift; sourceTree = ""; }; 83D9EC6B2062DBB7004D7FA6 /* LaunchDarkly_watchOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LaunchDarkly_watchOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 83DDBEF51FA24A7E00E428B6 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; - 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONSerialization.swift; sourceTree = ""; }; 83DDBEFD1FA24F9600E428B6 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagStoreSpec.swift; sourceTree = ""; }; 83E2E2051F9E7AC7007514E9 /* LDUserSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDUserSpec.swift; sourceTree = ""; }; 83EBCBB020D9C7B5003A7142 /* FlagCounterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagCounterSpec.swift; sourceTree = ""; }; 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagRequestTracker.swift; sourceTree = ""; }; 83EBCBB620DABE93003A7142 /* FlagRequestTrackerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagRequestTrackerSpec.swift; sourceTree = ""; }; - 83EF67891F97CFEC00403126 /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; 83EF67921F9945E800403126 /* EventSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSpec.swift; sourceTree = ""; }; 83EF67941F994BAD00403126 /* LDUserStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDUserStub.swift; sourceTree = ""; }; - 83F0A5611FB4D66600550A95 /* AnyComparer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyComparer.swift; sourceTree = ""; }; 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDConfigSpec.swift; sourceTree = ""; }; 83FEF8DC1F266742001CF12C /* FlagSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlagSynchronizer.swift; sourceTree = ""; }; 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventReporter.swift; sourceTree = ""; }; B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCacheSpec.swift; sourceTree = ""; }; B4265EB024E7390C001CFD2C /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = ""; }; - B43D5ACF25FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelSpec.swift; sourceTree = ""; }; B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = ""; }; B46F344025E6DB7D0078D45F /* DiagnosticReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticReporterSpec.swift; sourceTree = ""; }; B495A8A12787762C0051977C /* LDClientVariation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDClientVariation.swift; sourceTree = ""; }; @@ -455,7 +402,6 @@ C43C37E0236BA050003C1624 /* LDEvaluationDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDEvaluationDetail.swift; sourceTree = ""; }; C443A4092315AA4D00145710 /* NetworkReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkReporter.swift; sourceTree = ""; }; C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionModeChangeObserver.swift; sourceTree = ""; }; - C48ED690242D27E200464F5F /* DeprecatedCacheMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheMock.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -562,33 +508,13 @@ path = LaunchDarkly/GeneratedCode; sourceTree = ""; }; - 8354AC5F224150C300CDE602 /* Cache */ = { - isa = PBXGroup; - children = ( - 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */, - 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */, - ); - path = Cache; - sourceTree = ""; - }; - 8354AC672241586D00CDE602 /* Cache */ = { - isa = PBXGroup; - children = ( - 8354AC652241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift */, - 8354AC6D22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift */, - ); - path = Cache; - sourceTree = ""; - }; 8354AC742243168800CDE602 /* Cache */ = { isa = PBXGroup; children = ( C408884623033B3600420721 /* ConnectionInformationStore.swift */, 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */, - 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */, + 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */, 832D68A1224A38FC005F052A /* CacheConverter.swift */, - 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */, - 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */, B4C9D4322489C8FD004A9B03 /* DiagnosticCache.swift */, ); path = Cache; @@ -597,11 +523,8 @@ 8354AC75224316C700CDE602 /* Cache */ = { isa = PBXGroup; children = ( - 83D5597D1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift */, - 8354AC76224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift */, + 8354AC76224316F800CDE602 /* FeatureFlagCacheSpec.swift */, 832D68AB224B3321005F052A /* CacheConverterSpec.swift */, - B43D5ACF25FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift */, - 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */, B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */, ); path = Cache; @@ -636,6 +559,7 @@ 83B6C4B51F4DE7630055351C /* LDCommon.swift */, 8354EFDC1F26380700C05156 /* LDClient.swift */, B495A8A12787762C0051977C /* LDClientVariation.swift */, + 29FE1297280413D4008CC918 /* Util.swift */, 8354EFE61F263E4200C05156 /* Models */, 83FEF8D91F2666BF001CF12C /* ServiceObjects */, 831D8B701F71D3A600ED65E8 /* Networking */, @@ -667,7 +591,6 @@ 8354EFE61F263E4200C05156 /* Models */ = { isa = PBXGroup; children = ( - 8354AC5F224150C300CDE602 /* Cache */, C408884823033B7500420721 /* ConnectionInformation.swift */, B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */, 8354EFDE1F26380700C05156 /* Event.swift */, @@ -710,7 +633,6 @@ 838F96791FBA551A009CFC45 /* ClientServiceMockFactory.swift */, 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */, 831425AE206ABB5300F2EF36 /* EnvironmentReportingMock.swift */, - C48ED690242D27E200464F5F /* DeprecatedCacheMock.swift */, ); path = Mocks; sourceTree = ""; @@ -718,7 +640,6 @@ 83D17EA81FCDA16300B2823C /* Extensions */ = { isa = PBXGroup; children = ( - 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */, 83B6E3F0222EFA3800FF2A6A /* ThreadSpec.swift */, ); path = Extensions; @@ -727,11 +648,8 @@ 83E2E2071F9FF9A0007514E9 /* Extensions */ = { isa = PBXGroup; children = ( - 83EF67891F97CFEC00403126 /* Dictionary.swift */, 83DDBEF51FA24A7E00E428B6 /* Data.swift */, - 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */, 83DDBEFD1FA24F9600E428B6 /* Date.swift */, - 83F0A5611FB4D66600550A95 /* AnyComparer.swift */, 831D2AAE2061AAA000B4AC3C /* Thread.swift */, 8372668B20D4439600BD1088 /* DateFormatter.swift */, ); @@ -743,7 +661,6 @@ children = ( 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */, 83EBCB9F20D9A143003A7142 /* FlagChange */, - 83EBCBA020D9A168003A7142 /* FlagValue */, C43C37E0236BA050003C1624 /* LDEvaluationDetail.swift */, 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */, ); @@ -761,15 +678,6 @@ path = FlagChange; sourceTree = ""; }; - 83EBCBA020D9A168003A7142 /* FlagValue */ = { - isa = PBXGroup; - children = ( - 838838401F5EFADF0023D11B /* LDFlagValue.swift */, - 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */, - ); - path = FlagValue; - sourceTree = ""; - }; 83EBCBA620D9A23E003A7142 /* User */ = { isa = PBXGroup; children = ( @@ -812,7 +720,6 @@ 83EBCBA620D9A23E003A7142 /* User */, 83EBCBA720D9A251003A7142 /* FeatureFlag */, 83EF67921F9945E800403126 /* EventSpec.swift */, - 8354AC672241586D00CDE602 /* Cache */, B4F689132497B2FC004D3CE0 /* DiagnosticEventSpec.swift */, ); path = Models; @@ -1186,7 +1093,6 @@ files = ( 83906A7B21190B7700D7D3C5 /* DateFormatter.swift in Sources */, 8311886A2113AE5D00D77CB5 /* ObjcLDUser.swift in Sources */, - 8370DF6F225E40B800F84810 /* DeprecatedCache.swift in Sources */, 831188502113ADEF00D77CB5 /* EnvironmentReporter.swift in Sources */, 831188682113AE5600D77CB5 /* ObjcLDClient.swift in Sources */, 831188572113AE0B00D77CB5 /* FlagChangeNotifier.swift in Sources */, @@ -1196,23 +1102,18 @@ 831188452113ADC500D77CB5 /* LDClient.swift in Sources */, 831188522113ADF700D77CB5 /* KeyedValueCache.swift in Sources */, 831188582113AE0F00D77CB5 /* EventReporter.swift in Sources */, - 831188642113AE4200D77CB5 /* JSONSerialization.swift in Sources */, 8311885D2113AE2500D77CB5 /* DarklyService.swift in Sources */, - 8354AC6C22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, 831188692113AE5900D77CB5 /* ObjcLDConfig.swift in Sources */, - 831188602113AE3400D77CB5 /* Dictionary.swift in Sources */, 8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */, C43C37E8238DF22D003C1624 /* LDEvaluationDetail.swift in Sources */, 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */, C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831188592113AE1200D77CB5 /* FlagStore.swift in Sources */, C443A40D2315AA4D00145710 /* NetworkReporter.swift in Sources */, + 29FE129B280413D4008CC918 /* Util.swift in Sources */, 831188652113AE4600D77CB5 /* Date.swift in Sources */, 831188672113AE4D00D77CB5 /* Thread.swift in Sources */, - 832D68A0224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, - 8354AC642241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, C443A40823145FEE00145710 /* ConnectionInformationStore.swift in Sources */, - 831188662113AE4A00D77CB5 /* AnyComparer.swift in Sources */, 8311885C2113AE2200D77CB5 /* HTTPHeaders.swift in Sources */, 831188562113AE0800D77CB5 /* FlagSynchronizer.swift in Sources */, 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */, @@ -1224,17 +1125,15 @@ B4C9D43B2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */, B468E71324B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, - 8354AC732243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, + 8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */, 8311885B2113AE1D00D77CB5 /* Throttler.swift in Sources */, 8311884E2113ADE500D77CB5 /* Event.swift in Sources */, 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */, - 831188482113ADD100D77CB5 /* LDFlagValueConvertible.swift in Sources */, 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */, B4C9D4312489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 831188462113ADCA00D77CB5 /* LDUser.swift in Sources */, 830DB3B12239B54900D65D25 /* URLResponse.swift in Sources */, 831188512113ADF400D77CB5 /* ClientServiceFactory.swift in Sources */, - 831188472113ADCD00D77CB5 /* LDFlagValue.swift in Sources */, 831188442113ADC200D77CB5 /* LDConfig.swift in Sources */, 83906A7721190B1900D7D3C5 /* FlagRequestTracker.swift in Sources */, B495A8A52787762C0051977C /* LDClientVariation.swift in Sources */, @@ -1246,21 +1145,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8354AC632241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, B468E71224B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, 831EF34320655E730001C643 /* LDCommon.swift in Sources */, 831EF34420655E730001C643 /* LDConfig.swift in Sources */, 831EF34520655E730001C643 /* LDClient.swift in Sources */, - 832D689F224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 831EF34620655E730001C643 /* LDUser.swift in Sources */, - 831EF34720655E730001C643 /* LDFlagValue.swift in Sources */, 830DB3B02239B54900D65D25 /* URLResponse.swift in Sources */, - 831EF34820655E730001C643 /* LDFlagValueConvertible.swift in Sources */, B4C9D4352489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */, C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */, - 8354AC722243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, + 8354AC722243166900CDE602 /* FeatureFlagCache.swift in Sources */, C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */, 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */, 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */, @@ -1280,21 +1175,17 @@ 831EF35B20655E730001C643 /* DarklyService.swift in Sources */, 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */, 29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */, - 8354AC6B22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, C443A40723145FEE00145710 /* ConnectionInformationStore.swift in Sources */, + 29FE129A280413D4008CC918 /* Util.swift in Sources */, 831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */, - 831EF35E20655E730001C643 /* Dictionary.swift in Sources */, 835E4C54206BDF8D004C6E6C /* EnvironmentReporter.swift in Sources */, 8372668E20D4439600BD1088 /* DateFormatter.swift in Sources */, - 8370DF6E225E40B800F84810 /* DeprecatedCache.swift in Sources */, C43C37E7238DF22C003C1624 /* LDEvaluationDetail.swift in Sources */, 831EF36020655E730001C643 /* Data.swift in Sources */, 83EBCBB520DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, - 831EF36220655E730001C643 /* JSONSerialization.swift in Sources */, 8347BB0E21F147E100E56BCD /* LDTimer.swift in Sources */, B495A8A42787762C0051977C /* LDClientVariation.swift in Sources */, 831EF36320655E730001C643 /* Date.swift in Sources */, - 831EF36420655E730001C643 /* AnyComparer.swift in Sources */, 831EF36520655E730001C643 /* Thread.swift in Sources */, 83B1D7C92073F354006D1B1C /* CwlSysctl.swift in Sources */, 831EF36620655E730001C643 /* ObjcLDClient.swift in Sources */, @@ -1311,7 +1202,6 @@ files = ( 831D8B6F1F71532300ED65E8 /* HTTPHeaders.swift in Sources */, 835E1D3F1F63450A00184DB4 /* ObjcLDClient.swift in Sources */, - 8370DF6C225E40B800F84810 /* DeprecatedCache.swift in Sources */, 83EBCBB320DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, 837EF3742059C237009D628A /* Log.swift in Sources */, 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */, @@ -1325,21 +1215,16 @@ 831D8B721F71D3E700ED65E8 /* DarklyService.swift in Sources */, 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */, 8358F25E1F474E5900ECE1AF /* LDChangedFlag.swift in Sources */, - 8354AC6922418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */, C43C37E1236BA050003C1624 /* LDEvaluationDetail.swift in Sources */, 831AAE2C20A9E4F600B46DBA /* Throttler.swift in Sources */, 8354EFE11F26380700C05156 /* LDConfig.swift in Sources */, + 29FE1298280413D4008CC918 /* Util.swift in Sources */, C443A40F23186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, - 83F0A5621FB4D66600550A95 /* AnyComparer.swift in Sources */, C443A40A2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831D8B741F72994600ED65E8 /* FlagStore.swift in Sources */, 8358F2601F476AD800ECE1AF /* FlagChangeNotifier.swift in Sources */, - 838838451F5EFBAF0023D11B /* LDFlagValueConvertible.swift in Sources */, - 832D689D224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, - 838838411F5EFADF0023D11B /* LDFlagValue.swift in Sources */, 835E1D411F63450A00184DB4 /* ObjcLDUser.swift in Sources */, - 8354AC612241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */, 831D2AAF2061AAA000B4AC3C /* Thread.swift in Sources */, 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */, @@ -1348,13 +1233,11 @@ 831425B1206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, C408884723033B3600420721 /* ConnectionInformationStore.swift in Sources */, 83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */, - 83EF678A1F97CFEC00403126 /* Dictionary.swift in Sources */, B4C9D4382489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, 8347BB0C21F147E100E56BCD /* LDTimer.swift in Sources */, B468E71024B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, - 8354AC702243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, + 8354AC702243166900CDE602 /* FeatureFlagCache.swift in Sources */, 8358F2621F47747F00ECE1AF /* FlagChangeObserver.swift in Sources */, - 83DDBEFC1FA24B2700E428B6 /* JSONSerialization.swift in Sources */, 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */, 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */, 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */, @@ -1374,7 +1257,6 @@ 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */, 8392FFA32033565700320914 /* HTTPURLResponse.swift in Sources */, 83411A5F1FABDA8700E5CF39 /* mocks.generated.swift in Sources */, - 83D5597E1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift in Sources */, 831CE0661F853A1700A13A3A /* Match.swift in Sources */, 83DDBF001FA2589900E428B6 /* FlagStoreSpec.swift in Sources */, B4F689142497B2FC004D3CE0 /* DiagnosticEventSpec.swift in Sources */, @@ -1383,29 +1265,23 @@ 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */, 83B6E3F1222EFA3800FF2A6A /* ThreadSpec.swift in Sources */, 831AAE3020A9E75D00B46DBA /* ThrottlerSpec.swift in Sources */, - C48ED691242D27E200464F5F /* DeprecatedCacheMock.swift in Sources */, 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */, 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */, 83E2E2061F9E7AC7007514E9 /* LDUserSpec.swift in Sources */, - 83D17EAA1FCDA18C00B2823C /* DictionarySpec.swift in Sources */, 83A0E6B1203B557F00224298 /* FeatureFlagSpec.swift in Sources */, 83EBCBB720DABE93003A7142 /* FlagRequestTrackerSpec.swift in Sources */, - 8354AC662241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift in Sources */, B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */, B46F344125E6DB7D0078D45F /* DiagnosticReporterSpec.swift in Sources */, - 83D1523B22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift in Sources */, 83EF67951F994BAD00403126 /* LDUserStub.swift in Sources */, B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */, 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */, 83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */, - B43D5AD025FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift in Sources */, 832307AA1F7ECA630029815A /* LDConfigStub.swift in Sources */, - 8354AC77224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift in Sources */, + 8354AC77224316F800CDE602 /* FeatureFlagCacheSpec.swift in Sources */, 83EBCBB120D9C7B5003A7142 /* FlagCounterSpec.swift in Sources */, 83B8C2451FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift in Sources */, 8335299E1FC37727001166F8 /* FlagMaintainingMock.swift in Sources */, 83383A5120460DD30024D975 /* SynchronizingErrorSpec.swift in Sources */, - 8354AC6E22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift in Sources */, 83B9A080204F56F4000C3F17 /* FlagChangeObserverSpec.swift in Sources */, 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, 831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */, @@ -1423,12 +1299,9 @@ files = ( 83D9EC752062DEAB004D7FA6 /* LDCommon.swift in Sources */, 83D9EC762062DEAB004D7FA6 /* LDConfig.swift in Sources */, - 8370DF6D225E40B800F84810 /* DeprecatedCache.swift in Sources */, 83EBCBB420DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, 83D9EC772062DEAB004D7FA6 /* LDClient.swift in Sources */, 83D9EC782062DEAB004D7FA6 /* LDUser.swift in Sources */, - 83D9EC792062DEAB004D7FA6 /* LDFlagValue.swift in Sources */, - 83D9EC7A2062DEAB004D7FA6 /* LDFlagValueConvertible.swift in Sources */, B4C9D4342489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */, @@ -1437,36 +1310,31 @@ 83D9EC7F2062DEAB004D7FA6 /* FlagsUnchangedObserver.swift in Sources */, 83D9EC802062DEAB004D7FA6 /* Event.swift in Sources */, 83D9EC822062DEAB004D7FA6 /* ClientServiceFactory.swift in Sources */, - 8354AC6A22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, 83D9EC832062DEAB004D7FA6 /* KeyedValueCache.swift in Sources */, 831AAE2D20A9E4F600B46DBA /* Throttler.swift in Sources */, C43C37E6238DF22B003C1624 /* LDEvaluationDetail.swift in Sources */, 83D9EC872062DEAB004D7FA6 /* FlagSynchronizer.swift in Sources */, C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, + 29FE1299280413D4008CC918 /* Util.swift in Sources */, 83D9EC882062DEAB004D7FA6 /* FlagChangeNotifier.swift in Sources */, C443A40B2315AA4D00145710 /* NetworkReporter.swift in Sources */, 83D9EC892062DEAB004D7FA6 /* EventReporter.swift in Sources */, 83D9EC8A2062DEAB004D7FA6 /* FlagStore.swift in Sources */, 83D9EC8B2062DEAB004D7FA6 /* Log.swift in Sources */, - 832D689E224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, - 8354AC622241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83D9EC8C2062DEAB004D7FA6 /* HTTPHeaders.swift in Sources */, C443A40623145FED00145710 /* ConnectionInformationStore.swift in Sources */, 83D9EC8D2062DEAB004D7FA6 /* DarklyService.swift in Sources */, 83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */, 83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */, - 83D9EC902062DEAB004D7FA6 /* Dictionary.swift in Sources */, 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B2206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */, 8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */, - 8354AC712243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, + 8354AC712243166900CDE602 /* FeatureFlagCache.swift in Sources */, C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */, B4C9D4392489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, - 83D9EC942062DEAB004D7FA6 /* JSONSerialization.swift in Sources */, 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */, - 83D9EC962062DEAB004D7FA6 /* AnyComparer.swift in Sources */, 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */, 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */, 83D9EC982062DEAB004D7FA6 /* ObjcLDClient.swift in Sources */, diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index dbfdd106..b8ad98cc 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -11,12 +11,12 @@ import LDSwiftEventSource final class CacheConvertingMock: CacheConverting { var convertCacheDataCallCount = 0 - var convertCacheDataCallback: (() -> Void)? - var convertCacheDataReceivedArguments: (user: LDUser, config: LDConfig)? - func convertCacheData(for user: LDUser, and config: LDConfig) { + var convertCacheDataCallback: (() throws -> Void)? + var convertCacheDataReceivedArguments: (serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int)? + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) { convertCacheDataCallCount += 1 - convertCacheDataReceivedArguments = (user: user, config: config) - convertCacheDataCallback?() + convertCacheDataReceivedArguments = (serviceFactory: serviceFactory, keysToConvert: keysToConvert, maxCachedUsers: maxCachedUsers) + try! convertCacheDataCallback?() } } @@ -24,17 +24,17 @@ final class CacheConvertingMock: CacheConverting { final class DarklyStreamingProviderMock: DarklyStreamingProvider { var startCallCount = 0 - var startCallback: (() -> Void)? + var startCallback: (() throws -> Void)? func start() { startCallCount += 1 - startCallback?() + try! startCallback?() } var stopCallCount = 0 - var stopCallback: (() -> Void)? + var stopCallback: (() throws -> Void)? func stop() { stopCallCount += 1 - stopCallback?() + try! stopCallback?() } } @@ -42,55 +42,55 @@ final class DarklyStreamingProviderMock: DarklyStreamingProvider { final class DiagnosticCachingMock: DiagnosticCaching { var lastStatsSetCount = 0 - var setLastStatsCallback: (() -> Void)? + var setLastStatsCallback: (() throws -> Void)? var lastStats: DiagnosticStats? = nil { didSet { lastStatsSetCount += 1 - setLastStatsCallback?() + try! setLastStatsCallback?() } } var getDiagnosticIdCallCount = 0 - var getDiagnosticIdCallback: (() -> Void)? + var getDiagnosticIdCallback: (() throws -> Void)? var getDiagnosticIdReturnValue: DiagnosticId! func getDiagnosticId() -> DiagnosticId { getDiagnosticIdCallCount += 1 - getDiagnosticIdCallback?() + try! getDiagnosticIdCallback?() return getDiagnosticIdReturnValue } var getCurrentStatsAndResetCallCount = 0 - var getCurrentStatsAndResetCallback: (() -> Void)? + var getCurrentStatsAndResetCallback: (() throws -> Void)? var getCurrentStatsAndResetReturnValue: DiagnosticStats! func getCurrentStatsAndReset() -> DiagnosticStats { getCurrentStatsAndResetCallCount += 1 - getCurrentStatsAndResetCallback?() + try! getCurrentStatsAndResetCallback?() return getCurrentStatsAndResetReturnValue } var incrementDroppedEventCountCallCount = 0 - var incrementDroppedEventCountCallback: (() -> Void)? + var incrementDroppedEventCountCallback: (() throws -> Void)? func incrementDroppedEventCount() { incrementDroppedEventCountCallCount += 1 - incrementDroppedEventCountCallback?() + try! incrementDroppedEventCountCallback?() } var recordEventsInLastBatchCallCount = 0 - var recordEventsInLastBatchCallback: (() -> Void)? + var recordEventsInLastBatchCallback: (() throws -> Void)? var recordEventsInLastBatchReceivedEventsInLastBatch: Int? func recordEventsInLastBatch(eventsInLastBatch: Int) { recordEventsInLastBatchCallCount += 1 recordEventsInLastBatchReceivedEventsInLastBatch = eventsInLastBatch - recordEventsInLastBatchCallback?() + try! recordEventsInLastBatchCallback?() } var addStreamInitCallCount = 0 - var addStreamInitCallback: (() -> Void)? + var addStreamInitCallback: (() throws -> Void)? var addStreamInitReceivedStreamInit: DiagnosticStreamInit? func addStreamInit(streamInit: DiagnosticStreamInit) { addStreamInitCallCount += 1 addStreamInitReceivedStreamInit = streamInit - addStreamInitCallback?() + try! addStreamInitCallback?() } } @@ -98,12 +98,12 @@ final class DiagnosticCachingMock: DiagnosticCaching { final class DiagnosticReportingMock: DiagnosticReporting { var setModeCallCount = 0 - var setModeCallback: (() -> Void)? + var setModeCallback: (() throws -> Void)? var setModeReceivedArguments: (runMode: LDClientRunMode, online: Bool)? func setMode(_ runMode: LDClientRunMode, online: Bool) { setModeCallCount += 1 setModeReceivedArguments = (runMode: runMode, online: online) - setModeCallback?() + try! setModeCallback?() } } @@ -111,101 +111,101 @@ final class DiagnosticReportingMock: DiagnosticReporting { final class EnvironmentReportingMock: EnvironmentReporting { var isDebugBuildSetCount = 0 - var setIsDebugBuildCallback: (() -> Void)? + var setIsDebugBuildCallback: (() throws -> Void)? var isDebugBuild: Bool = true { didSet { isDebugBuildSetCount += 1 - setIsDebugBuildCallback?() + try! setIsDebugBuildCallback?() } } var deviceTypeSetCount = 0 - var setDeviceTypeCallback: (() -> Void)? + var setDeviceTypeCallback: (() throws -> Void)? var deviceType: String = Constants.deviceType { didSet { deviceTypeSetCount += 1 - setDeviceTypeCallback?() + try! setDeviceTypeCallback?() } } var deviceModelSetCount = 0 - var setDeviceModelCallback: (() -> Void)? + var setDeviceModelCallback: (() throws -> Void)? var deviceModel: String = Constants.deviceModel { didSet { deviceModelSetCount += 1 - setDeviceModelCallback?() + try! setDeviceModelCallback?() } } var systemVersionSetCount = 0 - var setSystemVersionCallback: (() -> Void)? + var setSystemVersionCallback: (() throws -> Void)? var systemVersion: String = Constants.systemVersion { didSet { systemVersionSetCount += 1 - setSystemVersionCallback?() + try! setSystemVersionCallback?() } } var systemNameSetCount = 0 - var setSystemNameCallback: (() -> Void)? + var setSystemNameCallback: (() throws -> Void)? var systemName: String = Constants.systemName { didSet { systemNameSetCount += 1 - setSystemNameCallback?() + try! setSystemNameCallback?() } } var operatingSystemSetCount = 0 - var setOperatingSystemCallback: (() -> Void)? + var setOperatingSystemCallback: (() throws -> Void)? var operatingSystem: OperatingSystem = .iOS { didSet { operatingSystemSetCount += 1 - setOperatingSystemCallback?() + try! setOperatingSystemCallback?() } } var backgroundNotificationSetCount = 0 - var setBackgroundNotificationCallback: (() -> Void)? + var setBackgroundNotificationCallback: (() throws -> Void)? var backgroundNotification: Notification.Name? = EnvironmentReporter().backgroundNotification { didSet { backgroundNotificationSetCount += 1 - setBackgroundNotificationCallback?() + try! setBackgroundNotificationCallback?() } } var foregroundNotificationSetCount = 0 - var setForegroundNotificationCallback: (() -> Void)? + var setForegroundNotificationCallback: (() throws -> Void)? var foregroundNotification: Notification.Name? = EnvironmentReporter().foregroundNotification { didSet { foregroundNotificationSetCount += 1 - setForegroundNotificationCallback?() + try! setForegroundNotificationCallback?() } } var vendorUUIDSetCount = 0 - var setVendorUUIDCallback: (() -> Void)? + var setVendorUUIDCallback: (() throws -> Void)? var vendorUUID: String? = Constants.vendorUUID { didSet { vendorUUIDSetCount += 1 - setVendorUUIDCallback?() + try! setVendorUUIDCallback?() } } var sdkVersionSetCount = 0 - var setSdkVersionCallback: (() -> Void)? + var setSdkVersionCallback: (() throws -> Void)? var sdkVersion: String = Constants.sdkVersion { didSet { sdkVersionSetCount += 1 - setSdkVersionCallback?() + try! setSdkVersionCallback?() } } var shouldThrottleOnlineCallsSetCount = 0 - var setShouldThrottleOnlineCallsCallback: (() -> Void)? + var setShouldThrottleOnlineCallsCallback: (() throws -> Void)? var shouldThrottleOnlineCalls: Bool = true { didSet { shouldThrottleOnlineCallsSetCount += 1 - setShouldThrottleOnlineCallsCallback?() + try! setShouldThrottleOnlineCallsCallback?() } } } @@ -214,81 +214,81 @@ final class EnvironmentReportingMock: EnvironmentReporting { final class EventReportingMock: EventReporting { var isOnlineSetCount = 0 - var setIsOnlineCallback: (() -> Void)? + var setIsOnlineCallback: (() throws -> Void)? var isOnline: Bool = false { didSet { isOnlineSetCount += 1 - setIsOnlineCallback?() + try! setIsOnlineCallback?() } } var lastEventResponseDateSetCount = 0 - var setLastEventResponseDateCallback: (() -> Void)? + var setLastEventResponseDateCallback: (() throws -> Void)? var lastEventResponseDate: Date? = nil { didSet { lastEventResponseDateSetCount += 1 - setLastEventResponseDateCallback?() + try! setLastEventResponseDateCallback?() } } var recordCallCount = 0 - var recordCallback: (() -> Void)? + var recordCallback: (() throws -> Void)? var recordReceivedEvent: Event? func record(_ event: Event) { recordCallCount += 1 recordReceivedEvent = event - recordCallback?() + try! recordCallback?() } var recordFlagEvaluationEventsCallCount = 0 - var recordFlagEvaluationEventsCallback: (() -> Void)? + var recordFlagEvaluationEventsCallback: (() throws -> Void)? var recordFlagEvaluationEventsReceivedArguments: (flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool)? func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) { recordFlagEvaluationEventsCallCount += 1 recordFlagEvaluationEventsReceivedArguments = (flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason) - recordFlagEvaluationEventsCallback?() + try! recordFlagEvaluationEventsCallback?() } var flushCallCount = 0 - var flushCallback: (() -> Void)? + var flushCallback: (() throws -> Void)? var flushReceivedCompletion: CompletionClosure? func flush(completion: CompletionClosure?) { flushCallCount += 1 flushReceivedCompletion = completion - flushCallback?() + try! flushCallback?() } } // MARK: - FeatureFlagCachingMock final class FeatureFlagCachingMock: FeatureFlagCaching { - var maxCachedUsersSetCount = 0 - var setMaxCachedUsersCallback: (() -> Void)? - var maxCachedUsers: Int = 5 { + var keyedValueCacheSetCount = 0 + var setKeyedValueCacheCallback: (() throws -> Void)? + var keyedValueCache: KeyedValueCaching = KeyedValueCachingMock() { didSet { - maxCachedUsersSetCount += 1 - setMaxCachedUsersCallback?() + keyedValueCacheSetCount += 1 + try! setKeyedValueCacheCallback?() } } var retrieveFeatureFlagsCallCount = 0 - var retrieveFeatureFlagsCallback: (() -> Void)? - var retrieveFeatureFlagsReceivedArguments: (userKey: String, mobileKey: String)? + var retrieveFeatureFlagsCallback: (() throws -> Void)? + var retrieveFeatureFlagsReceivedUserKey: String? var retrieveFeatureFlagsReturnValue: [LDFlagKey: FeatureFlag]? - func retrieveFeatureFlags(forUserWithKey userKey: String, andMobileKey mobileKey: String) -> [LDFlagKey: FeatureFlag]? { + func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? { retrieveFeatureFlagsCallCount += 1 - retrieveFeatureFlagsReceivedArguments = (userKey: userKey, mobileKey: mobileKey) - retrieveFeatureFlagsCallback?() + retrieveFeatureFlagsReceivedUserKey = userKey + try! retrieveFeatureFlagsCallback?() return retrieveFeatureFlagsReturnValue } var storeFeatureFlagsCallCount = 0 - var storeFeatureFlagsCallback: (() -> Void)? - var storeFeatureFlagsReceivedArguments: (featureFlags: [LDFlagKey: FeatureFlag], userKey: String, mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode)? - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode) { + var storeFeatureFlagsCallback: (() throws -> Void)? + var storeFeatureFlagsReceivedArguments: (featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date)? + func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) { storeFeatureFlagsCallCount += 1 - storeFeatureFlagsReceivedArguments = (featureFlags: featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated, storeMode: storeMode) - storeFeatureFlagsCallback?() + storeFeatureFlagsReceivedArguments = (featureFlags: featureFlags, userKey: userKey, lastUpdated: lastUpdated) + try! storeFeatureFlagsCallback?() } } @@ -296,64 +296,64 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { final class FlagChangeNotifyingMock: FlagChangeNotifying { var addFlagChangeObserverCallCount = 0 - var addFlagChangeObserverCallback: (() -> Void)? + var addFlagChangeObserverCallback: (() throws -> Void)? var addFlagChangeObserverReceivedObserver: FlagChangeObserver? func addFlagChangeObserver(_ observer: FlagChangeObserver) { addFlagChangeObserverCallCount += 1 addFlagChangeObserverReceivedObserver = observer - addFlagChangeObserverCallback?() + try! addFlagChangeObserverCallback?() } var addFlagsUnchangedObserverCallCount = 0 - var addFlagsUnchangedObserverCallback: (() -> Void)? + var addFlagsUnchangedObserverCallback: (() throws -> Void)? var addFlagsUnchangedObserverReceivedObserver: FlagsUnchangedObserver? func addFlagsUnchangedObserver(_ observer: FlagsUnchangedObserver) { addFlagsUnchangedObserverCallCount += 1 addFlagsUnchangedObserverReceivedObserver = observer - addFlagsUnchangedObserverCallback?() + try! addFlagsUnchangedObserverCallback?() } var addConnectionModeChangedObserverCallCount = 0 - var addConnectionModeChangedObserverCallback: (() -> Void)? + var addConnectionModeChangedObserverCallback: (() throws -> Void)? var addConnectionModeChangedObserverReceivedObserver: ConnectionModeChangedObserver? func addConnectionModeChangedObserver(_ observer: ConnectionModeChangedObserver) { addConnectionModeChangedObserverCallCount += 1 addConnectionModeChangedObserverReceivedObserver = observer - addConnectionModeChangedObserverCallback?() + try! addConnectionModeChangedObserverCallback?() } var removeObserverCallCount = 0 - var removeObserverCallback: (() -> Void)? + var removeObserverCallback: (() throws -> Void)? var removeObserverReceivedOwner: LDObserverOwner? func removeObserver(owner: LDObserverOwner) { removeObserverCallCount += 1 removeObserverReceivedOwner = owner - removeObserverCallback?() + try! removeObserverCallback?() } var notifyConnectionModeChangedObserversCallCount = 0 - var notifyConnectionModeChangedObserversCallback: (() -> Void)? + var notifyConnectionModeChangedObserversCallback: (() throws -> Void)? var notifyConnectionModeChangedObserversReceivedConnectionMode: ConnectionInformation.ConnectionMode? func notifyConnectionModeChangedObservers(connectionMode: ConnectionInformation.ConnectionMode) { notifyConnectionModeChangedObserversCallCount += 1 notifyConnectionModeChangedObserversReceivedConnectionMode = connectionMode - notifyConnectionModeChangedObserversCallback?() + try! notifyConnectionModeChangedObserversCallback?() } var notifyUnchangedCallCount = 0 - var notifyUnchangedCallback: (() -> Void)? + var notifyUnchangedCallback: (() throws -> Void)? func notifyUnchanged() { notifyUnchangedCallCount += 1 - notifyUnchangedCallback?() + try! notifyUnchangedCallback?() } var notifyObserversCallCount = 0 - var notifyObserversCallback: (() -> Void)? + var notifyObserversCallback: (() throws -> Void)? var notifyObserversReceivedArguments: (oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag])? func notifyObservers(oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) { notifyObserversCallCount += 1 notifyObserversReceivedArguments = (oldFlags: oldFlags, newFlags: newFlags) - notifyObserversCallback?() + try! notifyObserversCallback?() } } @@ -361,32 +361,50 @@ final class FlagChangeNotifyingMock: FlagChangeNotifying { final class KeyedValueCachingMock: KeyedValueCaching { var setCallCount = 0 - var setCallback: (() -> Void)? - var setReceivedArguments: (value: Any?, forKey: String)? - func set(_ value: Any?, forKey: String) { + var setCallback: (() throws -> Void)? + var setReceivedArguments: (value: Data, forKey: String)? + func set(_ value: Data, forKey: String) { setCallCount += 1 setReceivedArguments = (value: value, forKey: forKey) - setCallback?() + try! setCallback?() + } + + var dataCallCount = 0 + var dataCallback: (() throws -> Void)? + var dataReceivedForKey: String? + var dataReturnValue: Data? + func data(forKey: String) -> Data? { + dataCallCount += 1 + dataReceivedForKey = forKey + try! dataCallback?() + return dataReturnValue } var dictionaryCallCount = 0 - var dictionaryCallback: (() -> Void)? + var dictionaryCallback: (() throws -> Void)? var dictionaryReceivedForKey: String? - var dictionaryReturnValue: [String: Any]? = nil + var dictionaryReturnValue: [String: Any]? func dictionary(forKey: String) -> [String: Any]? { dictionaryCallCount += 1 dictionaryReceivedForKey = forKey - dictionaryCallback?() + try! dictionaryCallback?() return dictionaryReturnValue } var removeObjectCallCount = 0 - var removeObjectCallback: (() -> Void)? + var removeObjectCallback: (() throws -> Void)? var removeObjectReceivedForKey: String? func removeObject(forKey: String) { removeObjectCallCount += 1 removeObjectReceivedForKey = forKey - removeObjectCallback?() + try! removeObjectCallback?() + } + + var removeAllCallCount = 0 + var removeAllCallback: (() throws -> Void)? + func removeAll() { + removeAllCallCount += 1 + try! removeAllCallback?() } } @@ -394,29 +412,29 @@ final class KeyedValueCachingMock: KeyedValueCaching { final class LDFlagSynchronizingMock: LDFlagSynchronizing { var isOnlineSetCount = 0 - var setIsOnlineCallback: (() -> Void)? + var setIsOnlineCallback: (() throws -> Void)? var isOnline: Bool = false { didSet { isOnlineSetCount += 1 - setIsOnlineCallback?() + try! setIsOnlineCallback?() } } var streamingModeSetCount = 0 - var setStreamingModeCallback: (() -> Void)? + var setStreamingModeCallback: (() throws -> Void)? var streamingMode: LDStreamingMode = .streaming { didSet { streamingModeSetCount += 1 - setStreamingModeCallback?() + try! setStreamingModeCallback?() } } var pollingIntervalSetCount = 0 - var setPollingIntervalCallback: (() -> Void)? + var setPollingIntervalCallback: (() throws -> Void)? var pollingInterval: TimeInterval = 60_000 { didSet { pollingIntervalSetCount += 1 - setPollingIntervalCallback?() + try! setPollingIntervalCallback?() } } } @@ -425,18 +443,18 @@ final class LDFlagSynchronizingMock: LDFlagSynchronizing { final class ThrottlingMock: Throttling { var runThrottledCallCount = 0 - var runThrottledCallback: (() -> Void)? + var runThrottledCallback: (() throws -> Void)? var runThrottledReceivedRunClosure: RunClosure? func runThrottled(_ runClosure: @escaping RunClosure) { runThrottledCallCount += 1 runThrottledReceivedRunClosure = runClosure - runThrottledCallback?() + try! runThrottledCallback?() } var cancelThrottledRunCallCount = 0 - var cancelThrottledRunCallback: (() -> Void)? + var cancelThrottledRunCallback: (() throws -> Void)? func cancelThrottledRun() { cancelThrottledRunCallCount += 1 - cancelThrottledRunCallback?() + try! cancelThrottledRunCallback?() } } diff --git a/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift b/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift deleted file mode 100644 index bca2a855..00000000 --- a/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation - -struct AnyComparer { - private init() { } - - // If editing this method to add classes here, update AnySpec with tests that verify the comparison for that class - // swiftlint:disable:next cyclomatic_complexity - static func isEqual(_ value: Any, to other: Any) -> Bool { - switch (value, other) { - case let (value, other) as (Bool, Bool): - if value != other { - return false - } - case let (value, other) as (Int, Int): - if value != other { - return false - } - case let (value, other) as (Int, Double): - if Double(value) != other { - return false - } - case let (value, other) as (Double, Int): - if value != Double(other) { - return false - } - case let (value, other) as (Int64, Int64): - if value != other { - return false - } - case let (value, other) as (Int64, Double): - if Double(value) != other { - return false - } - case let (value, other) as (Double, Int64): - if value != Double(other) { - return false - } - case let (value, other) as (Double, Double): - if value != other { - return false - } - case let (value, other) as (String, String): - if value != other { - return false - } - case let (value, other) as ([Any], [Any]): - if value.count != other.count { - return false - } - for index in 0.. Bool { - guard let nonNilValue = value, let nonNilOther = other - else { - return value == nil && other == nil - } - return isEqual(nonNilValue, to: nonNilOther) - } - - static func isEqual(_ value: Any, to other: Any?) -> Bool { - guard let other = other - else { - return false - } - return isEqual(value, to: other) - } - - static func isEqual(_ value: Any?, to other: Any) -> Bool { - guard let value = value - else { - return false - } - return isEqual(value, to: other) - } -} diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Data.swift b/LaunchDarkly/LaunchDarkly/Extensions/Data.swift index fe3e8a32..9ca0b4c7 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/Data.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/Data.swift @@ -6,6 +6,6 @@ extension Data { } var jsonDictionary: [String: Any]? { - try? JSONSerialization.jsonDictionary(with: self, options: [.allowFragments]) + try? JSONSerialization.jsonObject(with: self, options: [.allowFragments]) as? [String: Any] } } diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift b/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift deleted file mode 100644 index e70fce44..00000000 --- a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Foundation - -extension Dictionary where Key == String { - var jsonString: String? { - guard let encodedDictionary = jsonData - else { return nil } - return String(data: encodedDictionary, encoding: .utf8) - } - - var jsonData: Data? { - guard JSONSerialization.isValidJSONObject(self) - else { return nil } - return try? JSONSerialization.data(withJSONObject: self, options: []) - } - - func symmetricDifference(_ other: [String: Any]) -> [String] { - let leftKeys: Set = Set(self.keys) - let rightKeys: Set = Set(other.keys) - let differingKeys = leftKeys.symmetricDifference(rightKeys) - let matchingKeys = leftKeys.intersection(rightKeys) - let matchingKeysWithDifferentValues = matchingKeys.filter { key -> Bool in - !AnyComparer.isEqual(self[key], to: other[key]) - } - return differingKeys.union(matchingKeysWithDifferentValues).sorted() - } -} - -extension Dictionary where Key == String, Value == Any { - var withNullValuesRemoved: [String: Any] { - (self as [String: Any?]).compactMapValues { value in - if value is NSNull { - return nil - } - if let dictionary = value as? [String: Any] { - return dictionary.withNullValuesRemoved - } - if let arr = value as? [Any] { - return arr.withNullValuesRemoved - } - return value - } - } -} - -private extension Array where Element == Any { - var withNullValuesRemoved: [Any] { - (self as [Any?]).compactMap { value in - if value is NSNull { - return nil - } - if let arr = value as? [Any] { - return arr.withNullValuesRemoved - } - if let dict = value as? [String: Any] { - return dict.withNullValuesRemoved - } - return value - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift b/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift deleted file mode 100644 index aad475c1..00000000 --- a/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -extension JSONSerialization { - static func jsonDictionary(with data: Data, options: JSONSerialization.ReadingOptions = []) throws -> [String: Any] { - guard let decodedDictionary = try JSONSerialization.jsonObject(with: data, options: options) as? [String: Any] - else { - throw LDInvalidArgumentError("JSON is not an object") - } - return decodedDictionary - } -} diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 4dcab792..a0ab4ff3 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -116,10 +116,10 @@ public class LDClient { private func internalSetOnline(_ goOnline: Bool, completion: (() -> Void)? = nil) { internalSetOnlineQueue.sync { guard goOnline, self.canGoOnline - else { - // go offline, which is not throttled - self.go(online: false, reasonOnlineUnavailable: self.reasonOnlineUnavailable(goOnline: goOnline), completion: completion) - return + else { + // go offline, which is not throttled + self.go(online: false, reasonOnlineUnavailable: self.reasonOnlineUnavailable(goOnline: goOnline), completion: completion) + return } self.throttler.runThrottled { @@ -294,9 +294,8 @@ public class LDClient { let wasOnline = self.isOnline self.internalSetOnline(false) - cacheConverter.convertCacheData(for: user, and: config) - let cachedUserFlags = self.flagCache.retrieveFeatureFlags(forUserWithKey: self.user.key, andMobileKey: self.config.mobileKey) ?? [:] - flagStore.replaceStore(newFlags: cachedUserFlags, completion: nil) + let cachedUserFlags = self.flagCache.retrieveFeatureFlags(userKey: self.user.key) ?? [:] + flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedUserFlags)) self.service.user = self.user self.service.clearFlagResponseCache() flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), @@ -326,7 +325,7 @@ public class LDClient { LDClient will not provide any source or change information, only flag keys and flag values. The client app should convert the feature flag value into the desired type. */ - public var allFlags: [LDFlagKey: Any]? { + public var allFlags: [LDFlagKey: LDValue]? { guard hasStarted else { return nil } return flagStore.featureFlags.compactMapValues { $0.value } @@ -487,23 +486,21 @@ public class LDClient { private func onFlagSyncComplete(result: FlagSyncResult) { Log.debug(typeName(and: #function) + "result: \(result)") switch result { - case let .success(flagDictionary, streamingEvent): + case let .flagCollection(flagCollection): let oldFlags = flagStore.featureFlags connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) - switch streamingEvent { - case nil, .ping?, .put?: - flagStore.replaceStore(newFlags: flagDictionary) { - self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) - } - case .patch?: - flagStore.updateStore(updateDictionary: flagDictionary) { - self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) - } - case .delete?: - flagStore.deleteFlag(deleteDictionary: flagDictionary) { - self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) - } - } + flagStore.replaceStore(newFlags: flagCollection) + self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) + case let .patch(featureFlag): + let oldFlags = flagStore.featureFlags + connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) + flagStore.updateStore(updatedFlag: featureFlag) + self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) + case let .delete(deleteResponse): + let oldFlags = flagStore.featureFlags + connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) + flagStore.deleteFlag(deleteResponse: deleteResponse) + self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) case .upToDate: connectionInformation.lastKnownFlagValidity = Date() flagChangeNotifier.notifyUnchanged() @@ -522,7 +519,7 @@ public class LDClient { private func updateCacheAndReportChanges(user: LDUser, oldFlags: [LDFlagKey: FeatureFlag]) { - flagCache.storeFeatureFlags(flagStore.featureFlags, userKey: user.key, mobileKey: config.mobileKey, lastUpdated: Date(), storeMode: .async) + flagCache.storeFeatureFlags(flagStore.featureFlags, userKey: user.key, lastUpdated: Date()) flagChangeNotifier.notifyObservers(oldFlags: oldFlags, newFlags: flagStore.featureFlags) } @@ -635,7 +632,10 @@ public class LDClient { return } - let internalUser = user + let serviceFactory = serviceFactory ?? ClientServiceFactory() + var keys = [config.mobileKey] + keys.append(contentsOf: config.getSecondaryMobileKeys().values) + serviceFactory.makeCacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: keys, maxCachedUsers: config.maxCachedUsers) LDClient.instances = [:] var mobileKeys = config.getSecondaryMobileKeys() @@ -651,7 +651,7 @@ public class LDClient { for (name, mobileKey) in mobileKeys { var internalConfig = config internalConfig.mobileKey = mobileKey - let instance = LDClient(serviceFactory: serviceFactory ?? ClientServiceFactory(), configuration: internalConfig, startUser: internalUser, completion: completionCheck) + let instance = LDClient(serviceFactory: serviceFactory, configuration: internalConfig, startUser: user, completion: completionCheck) LDClient.instances?[name] = instance } completionCheck() @@ -714,7 +714,6 @@ public class LDClient { let serviceFactory: ClientServiceCreating private(set) var flagCache: FeatureFlagCaching - private(set) var cacheConverter: CacheConverting private(set) var flagSynchronizer: LDFlagSynchronizing var flagChangeNotifier: FlagChangeNotifying private(set) var eventReporter: EventReporting @@ -739,9 +738,8 @@ public class LDClient { private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startUser: LDUser?, completion: (() -> Void)? = nil) { self.serviceFactory = serviceFactory environmentReporter = self.serviceFactory.makeEnvironmentReporter() - flagCache = self.serviceFactory.makeFeatureFlagCache(maxCachedUsers: configuration.maxCachedUsers) + flagCache = self.serviceFactory.makeFeatureFlagCache(mobileKey: configuration.mobileKey, maxCachedUsers: configuration.maxCachedUsers) flagStore = self.serviceFactory.makeFlagStore() - cacheConverter = self.serviceFactory.makeCacheConverter(maxCachedUsers: configuration.maxCachedUsers) flagChangeNotifier = self.serviceFactory.makeFlagChangeNotifier() throttler = self.serviceFactory.makeThrottler(environmentReporter: environmentReporter) @@ -774,9 +772,8 @@ public class LDClient { onSyncComplete: onFlagSyncComplete) Log.level = environmentReporter.isDebugBuild && config.isDebugMode ? .debug : .noLogging - cacheConverter.convertCacheData(for: user, and: config) - if let cachedFlags = flagCache.retrieveFeatureFlags(forUserWithKey: user.key, andMobileKey: config.mobileKey), !cachedFlags.isEmpty { - flagStore.replaceStore(newFlags: cachedFlags, completion: nil) + if let cachedFlags = flagCache.retrieveFeatureFlags(userKey: user.key), !cachedFlags.isEmpty { + flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedFlags)) } eventReporter.record(IdentifyEvent(user: user)) diff --git a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift index 41e0bfe1..b93bc158 100644 --- a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -1,129 +1,170 @@ import Foundation extension LDClient { - // MARK: Retrieving Flag Values /** - Returns the variation for the given feature flag. If the flag does not exist, cannot be cast to the correct return - type, or the LDClient is not started, returns the default value. - - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *true* and *false*. - You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the - available types. - - When online, the LDClient has two modes for maintaining feature flag values: *streaming* and *polling*. The client - app requests the mode by setting the `config.streamingMode`, see `LDConfig` for details. - - A call to `variation` records events reported later. Recorded events allow clients to analyze usage and assist in - debugging issues. - - ### Usage - ```` - let boolFeatureFlagValue = LDClient.get()!.variation(forKey: "bool-flag-key", defaultValue: false) //boolFeatureFlagValue is a Bool - ```` - **Important** The default value tells the SDK the type of the feature flag. In several cases, the feature flag type - cannot be determined by the values sent from the server. It is possible to provide a default value with a type that - does not match the feature flag value's type. The SDK will attempt to convert the feature flag's value into the - type of the default value in the variation request. If that cast fails, the SDK will not be able to determine the - correct return type, and will always return the default value. - - Pay close attention to the type of the default value for collections. If the default value collection type is more - restrictive than the feature flag, the sdk will return the default value even though the feature flag is present - because it cannot convert the feature flag into the type requested via the default value. For example, if the - feature flag has the type `[String: Any]`, but the default value has the type `[String: Int]`, the sdk will not be - able to convert the flags into the requested type, and will return the default value. - - To avoid this, make sure the default value type matches the expected feature flag type. Either specify the default - value type to be the feature flag type, or cast the default value to the feature flag type prior to making the - variation request. In the above example, either specify that the default value's type is `[String: Any]`: - ```` - let defaultValue: [String: Any] = ["a": 1, "b": 2] //dictionary type would be [String: Int] without the type specifier - ```` - or cast the default value into the feature flag type prior to calling variation: - ```` - let dictionaryFlagValue = LDClient.get()!.variation(forKey: "dictionary-key", defaultValue: ["a": 1, "b": 2] as [String: Any]) - ```` - - - parameter forKey: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. - - - returns: The requested feature flag value, or the default value if the flag is missing or cannot be cast to the default value type, or the client is not started - */ - /// - Tag: variation - public func variation(forKey flagKey: LDFlagKey, defaultValue: T) -> T { - variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: false) + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func boolVariation(forKey flagKey: LDFlagKey, defaultValue: Bool) -> Bool { + variationDetailInternal(flagKey, defaultValue, needsReason: false).value } /** - Returns the LDEvaluationDetail for the given feature flag. LDEvaluationDetail gives you more insight into why your - variation contains the specified value. If the flag does not exist, cannot be cast to the correct return type, or - the LDClient is not started, returns an LDEvaluationDetail with the default value. - See [variation](x-source-tag://variation) + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func boolVariationDetail(forKey flagKey: LDFlagKey, defaultValue: Bool) -> LDEvaluationDetail { + variationDetailInternal(flagKey, defaultValue, needsReason: true) + } + + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func intVariation(forKey flagKey: LDFlagKey, defaultValue: Int) -> Int { + variationDetailInternal(flagKey, defaultValue, needsReason: false).value + } - - parameter forKey: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value value to return if the feature flag key does not exist. + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func intVariationDetail(forKey flagKey: LDFlagKey, defaultValue: Int) -> LDEvaluationDetail { + variationDetailInternal(flagKey, defaultValue, needsReason: true) + } - - returns: LDEvaluationDetail which wraps the requested feature flag value, or the default value, which variation was served, and the evaluation reason. + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. */ - public func variationDetail(forKey flagKey: LDFlagKey, defaultValue: T) -> LDEvaluationDetail { - let featureFlag = flagStore.featureFlag(for: flagKey) - let reason = checkErrorKinds(featureFlag: featureFlag) ?? featureFlag?.reason - let value = variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: true) - return LDEvaluationDetail(value: value, variationIndex: featureFlag?.variation, reason: reason) + public func doubleVariation(forKey flagKey: LDFlagKey, defaultValue: Double) -> Double { + variationDetailInternal(flagKey, defaultValue, needsReason: false).value + } + + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func doubleVariationDetail(forKey flagKey: LDFlagKey, defaultValue: Double) -> LDEvaluationDetail { + variationDetailInternal(flagKey, defaultValue, needsReason: true) + } + + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func stringVariation(forKey flagKey: LDFlagKey, defaultValue: String) -> String { + variationDetailInternal(flagKey, defaultValue, needsReason: false).value + } + + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func stringVariationDetail(forKey flagKey: LDFlagKey, defaultValue: String) -> LDEvaluationDetail { + variationDetailInternal(flagKey, defaultValue, needsReason: true) + } + + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func jsonVariation(forKey flagKey: LDFlagKey, defaultValue: LDValue) -> LDValue { + variationDetailInternal(flagKey, defaultValue, needsReason: false).value } - private func checkErrorKinds(featureFlag: FeatureFlag?) -> [String: Any]? { - if !hasStarted { - return ["kind": "ERROR", "errorKind": "CLIENT_NOT_READY"] - } else if featureFlag == nil { - return ["kind": "ERROR", "errorKind": "FLAG_NOT_FOUND"] + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func jsonVariationDetail(forKey flagKey: LDFlagKey, defaultValue: LDValue) -> LDEvaluationDetail { + variationDetailInternal(flagKey, defaultValue, needsReason: true) + } + + private func variationDetailInternal(_ flagKey: LDFlagKey, _ defaultValue: T, needsReason: Bool) -> LDEvaluationDetail { + var result: LDEvaluationDetail + let featureFlag = flagStore.featureFlag(for: flagKey) + if let featureFlag = featureFlag { + if featureFlag.value == .null { + result = LDEvaluationDetail(value: defaultValue, variationIndex: featureFlag.variation, reason: featureFlag.reason) + } else if let convertedValue = T(fromLDValue: featureFlag.value) { + result = LDEvaluationDetail(value: convertedValue, variationIndex: featureFlag.variation, reason: featureFlag.reason) + } else { + result = LDEvaluationDetail(value: defaultValue, variationIndex: nil, reason: ["kind": "ERROR", "errorKind": "WRONG_TYPE"]) + } } else { - return nil + Log.debug(typeName(and: #function) + " Unknown feature flag \(flagKey); returning default value") + result = LDEvaluationDetail(value: defaultValue, variationIndex: nil, reason: ["kind": "ERROR", "errorKind": "FLAG_NOT_FOUND"]) } + eventReporter.recordFlagEvaluationEvents(flagKey: flagKey, + value: result.value.toLDValue(), + defaultValue: defaultValue.toLDValue(), + featureFlag: featureFlag, + user: user, + includeReason: needsReason) + return result } +} - private func variationInternal(forKey flagKey: LDFlagKey, defaultValue: T, includeReason: Bool) -> T { - guard hasStarted - else { - Log.debug(typeName(and: #function) + "returning defaultValue: \(defaultValue)." + " LDClient not started.") - return defaultValue - } - let featureFlag = flagStore.featureFlag(for: flagKey) - let value = (featureFlag?.value as? T) ?? defaultValue - let failedConversionMessage = self.failedConversionMessage(featureFlag: featureFlag, defaultValue: defaultValue) - Log.debug(typeName(and: #function) + "flagKey: \(flagKey), value: \(value), defaultValue: \(defaultValue), " + - "featureFlag: \(String(describing: featureFlag)), reason: \(featureFlag?.reason?.description ?? "nil"). \(failedConversionMessage)") - eventReporter.recordFlagEvaluationEvents(flagKey: flagKey, value: LDValue.fromAny(value), defaultValue: LDValue.fromAny(defaultValue), featureFlag: featureFlag, user: user, includeReason: includeReason) - return value +private protocol LDValueConvertible { + init?(fromLDValue: LDValue) + func toLDValue() -> LDValue +} + +extension Bool: LDValueConvertible { + init?(fromLDValue value: LDValue) { + guard case .bool(let value) = value + else { return nil } + self = value } - private func failedConversionMessage(featureFlag: FeatureFlag?, defaultValue: T) -> String { - if featureFlag == nil { - return " Feature flag not found." - } - if featureFlag?.value is T { - return "" - } - return " LDClient was unable to convert the feature flag to the requested type (\(T.self))." - + (isCollection(defaultValue) ? " The defaultValue type is a collection. Make sure the element of the defaultValue's type is not too restrictive for the actual feature flag type." : "") + func toLDValue() -> LDValue { + return .bool(self) + } +} + +extension Int: LDValueConvertible { + init?(fromLDValue value: LDValue) { + guard case .number(let value) = value, let intValue = Int(exactly: value.rounded()) + else { return nil } + self = intValue } - private func isCollection(_ object: T) -> Bool { - let collectionsTypes = ["Set", "Array", "Dictionary"] - let typeString = String(describing: type(of: object)) + func toLDValue() -> LDValue { + return .number(Double(self)) + } +} - for type in collectionsTypes { - if typeString.contains(type) { return true } - } - return false +extension Double: LDValueConvertible { + init?(fromLDValue value: LDValue) { + guard case .number(let value) = value + else { return nil } + self = value + } + + func toLDValue() -> LDValue { + return .number(self) } } -private extension Optional { - var stringValue: String { - guard let value = self - else { - return "" - } - return "\(value)" +extension String: LDValueConvertible { + init?(fromLDValue value: LDValue) { + guard case .string(let value) = value + else { return nil } + self = value + } + + func toLDValue() -> LDValue { + return .string(self) + } +} + +extension LDValue: LDValueConvertible { + init?(fromLDValue value: LDValue) { + self = value + } + + func toLDValue() -> LDValue { + return self } } diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index afd2b173..ff51353a 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -151,9 +151,7 @@ public enum LDValue: Codable, } func booleanValue() -> Bool { - if case .bool(let val) = self { - return val - } + if case .bool(let val) = self { return val } return false } @@ -166,16 +164,12 @@ public enum LDValue: Codable, } func doubleValue() -> Double { - if case .number(let val) = self { - return val - } + if case .number(let val) = self { return val } return 0 } func stringValue() -> String { - if case .string(let val) = self { - return val - } + if case .string(let val) = self { return val } return "" } diff --git a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift deleted file mode 100644 index 3aaa3923..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation - -// Data structure used to cache feature flags for a specific user from a specific environment -struct CacheableEnvironmentFlags { - enum CodingKeys: String, CodingKey, CaseIterable { - case userKey, mobileKey, featureFlags - } - - let userKey: String - let mobileKey: String - let featureFlags: [LDFlagKey: FeatureFlag] - - init(userKey: String, mobileKey: String, featureFlags: [LDFlagKey: FeatureFlag]) { - (self.userKey, self.mobileKey, self.featureFlags) = (userKey, mobileKey, featureFlags) - } - - var dictionaryValue: [String: Any] { - [CodingKeys.userKey.rawValue: userKey, - CodingKeys.mobileKey.rawValue: mobileKey, - CodingKeys.featureFlags.rawValue: featureFlags.dictionaryValue.withNullValuesRemoved] - } - - init?(dictionary: [String: Any]) { - guard let userKey = dictionary[CodingKeys.userKey.rawValue] as? String, - let mobileKey = dictionary[CodingKeys.mobileKey.rawValue] as? String, - let featureFlags = (dictionary[CodingKeys.featureFlags.rawValue] as? [String: Any])?.flagCollection - else { return nil } - self.init(userKey: userKey, mobileKey: mobileKey, featureFlags: featureFlags) - } -} diff --git a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift deleted file mode 100644 index d6a3d0d7..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift +++ /dev/null @@ -1,93 +0,0 @@ -import Foundation - -// Data structure used to cache feature flags for a specific user for multiple environments -// Cache model in use from 4.0.0 -/* -[: [ - “userKey”: , //CacheableUserEnvironmentFlags dictionary - “environmentFlags”: [ - : [ - “userKey”: , //CacheableEnvironmentFlags dictionary - “mobileKey”: , - “featureFlags”: [ - : [ - “key”: , //FeatureFlag dictionary - “version”: , - “flagVersion”: , - “variation”: , - “value”: , - “trackEvents”: , - “debugEventsUntilDate”: , - "reason: , - "trackReason": - ] - ] - ] - ], - “lastUpdated”: - ] -] -*/ -struct CacheableUserEnvironmentFlags { - enum CodingKeys: String, CodingKey, CaseIterable { - case userKey, environmentFlags, lastUpdated - } - - let userKey: String - let environmentFlags: [MobileKey: CacheableEnvironmentFlags] - let lastUpdated: Date - - init(userKey: String, environmentFlags: [MobileKey: CacheableEnvironmentFlags], lastUpdated: Date) { - self.userKey = userKey - self.environmentFlags = environmentFlags - self.lastUpdated = lastUpdated - } - - init?(dictionary: [String: Any]) { - guard let userKey = dictionary[CodingKeys.userKey.rawValue] as? String, - let environmentFlagsDictionary = dictionary[CodingKeys.environmentFlags.rawValue] as? [MobileKey: [LDFlagKey: Any]], - let lastUpdated = (dictionary[CodingKeys.lastUpdated.rawValue] as? String)?.dateValue - else { return nil } - let environmentFlags = environmentFlagsDictionary.compactMapValues { cacheableEnvironmentFlagsDictionary in - CacheableEnvironmentFlags(dictionary: cacheableEnvironmentFlagsDictionary) - } - self.init(userKey: userKey, environmentFlags: environmentFlags, lastUpdated: lastUpdated) - } - - init?(object: Any) { - guard let dictionary = object as? [String: Any] - else { return nil } - self.init(dictionary: dictionary) - } - - var dictionaryValue: [String: Any] { - [CodingKeys.userKey.rawValue: userKey, - CodingKeys.lastUpdated.rawValue: lastUpdated.stringValue, - CodingKeys.environmentFlags.rawValue: environmentFlags.compactMapValues { $0.dictionaryValue } ] - } -} - -extension DateFormatter { - /// Date formatter configured to format dates to/from the format 2018-08-13T19:06:38.123Z - class var ldDateFormatter: DateFormatter { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - formatter.timeZone = TimeZone(identifier: "UTC") - return formatter - } -} - -extension Date { - /// Date string using the format 2018-08-13T19:06:38.123Z - var stringValue: String { DateFormatter.ldDateFormatter.string(from: self) } - - // When a date is converted to JSON, the resulting string is not as precise as the original date (only to the nearest .001s) - // By converting the date to json, then back into a date, the result can be compared with any date re-inflated from json - /// Date truncated to the nearest millisecond, which is the precision for string formatted dates - var stringEquivalentDate: Date { stringValue.dateValue } -} - -extension String { - /// Date converted from a string using the format 2018-08-13T19:06:38.123Z - var dateValue: Date { DateFormatter.ldDateFormatter.date(from: self) ?? Date() } -} diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index e33b9832..a0a76f87 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -138,7 +138,7 @@ class FeatureEvent: Event, SubEvent { try container.encode(value, forKey: .value) try container.encode(defaultValue, forKey: .defaultValue) if let reason = includeReason || featureFlag?.trackReason ?? false ? featureFlag?.reason : nil { - try container.encode(LDValue.fromAny(reason), forKey: .reason) + try container.encode(reason, forKey: .reason) } try container.encode(creationDate, forKey: .creationDate) } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift index 2e7abae4..b2357de7 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift @@ -7,7 +7,7 @@ struct FeatureFlag: Codable { } let flagKey: LDFlagKey - let value: Any? + let value: LDValue let variation: Int? /// The "environment" version. It changes whenever any feature flag in the environment changes. Used for version comparisons for streaming patch and delete. let version: Int? @@ -15,22 +15,22 @@ struct FeatureFlag: Codable { let flagVersion: Int? let trackEvents: Bool let debugEventsUntilDate: Date? - let reason: [String: Any]? + let reason: [String: LDValue]? let trackReason: Bool var versionForEvents: Int? { flagVersion ?? version } init(flagKey: LDFlagKey, - value: Any? = nil, + value: LDValue = .null, variation: Int? = nil, version: Int? = nil, flagVersion: Int? = nil, trackEvents: Bool = false, debugEventsUntilDate: Date? = nil, - reason: [String: Any]? = nil, + reason: [String: LDValue]? = nil, trackReason: Bool = false) { self.flagKey = flagKey - self.value = value is NSNull ? nil : value + self.value = value self.variation = variation self.version = version self.flagVersion = flagVersion @@ -40,21 +40,6 @@ struct FeatureFlag: Codable { self.trackReason = trackReason } - init?(dictionary: [String: Any]?) { - guard let dictionary = dictionary, - let flagKey = dictionary.flagKey - else { return nil } - self.init(flagKey: flagKey, - value: dictionary.value, - variation: dictionary.variation, - version: dictionary.version, - flagVersion: dictionary.flagVersion, - trackEvents: dictionary.trackEvents ?? false, - debugEventsUntilDate: Date(millisSince1970: dictionary.debugEventsUntilDate), - reason: dictionary.reason, - trackReason: dictionary.trackReason ?? false) - } - init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let flagKey = try container.decode(LDFlagKey.self, forKey: .flagKey) @@ -68,21 +53,20 @@ struct FeatureFlag: Codable { throw DecodingError.dataCorruptedError(forKey: .flagKey, in: container, debugDescription: description) } self.flagKey = flagKey - self.value = (try container.decodeIfPresent(LDValue.self, forKey: .value))?.toAny() + self.value = (try container.decodeIfPresent(LDValue.self, forKey: .value)) ?? .null self.variation = try container.decodeIfPresent(Int.self, forKey: .variation) self.version = try container.decodeIfPresent(Int.self, forKey: .version) self.flagVersion = try container.decodeIfPresent(Int.self, forKey: .flagVersion) self.trackEvents = (try container.decodeIfPresent(Bool.self, forKey: .trackEvents)) ?? false self.debugEventsUntilDate = Date(millisSince1970: try container.decodeIfPresent(Int64.self, forKey: .debugEventsUntilDate)) - self.reason = (try container.decodeIfPresent(LDValue.self, forKey: .reason))?.toAny() as? [String: Any] + self.reason = try container.decodeIfPresent([String: LDValue].self, forKey: .reason) self.trackReason = (try container.decodeIfPresent(Bool.self, forKey: .trackReason)) ?? false } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(flagKey, forKey: .flagKey) - let val = LDValue.fromAny(value) - if val != .null { try container.encode(val, forKey: .value) } + if value != .null { try container.encode(value, forKey: .value) } try container.encodeIfPresent(variation, forKey: .variation) try container.encodeIfPresent(version, forKey: .version) try container.encodeIfPresent(flagVersion, forKey: .flagVersion) @@ -90,24 +74,10 @@ struct FeatureFlag: Codable { if let debugEventsUntilDate = debugEventsUntilDate { try container.encode(debugEventsUntilDate.millisSince1970, forKey: .debugEventsUntilDate) } - if reason != nil { try container.encode(LDValue.fromAny(reason), forKey: .reason) } + if reason != nil { try container.encode(reason, forKey: .reason) } if trackReason { try container.encode(true, forKey: .trackReason) } } - var dictionaryValue: [String: Any] { - var dictionaryValue = [String: Any]() - dictionaryValue[CodingKeys.flagKey.rawValue] = flagKey - dictionaryValue[CodingKeys.value.rawValue] = value ?? NSNull() - dictionaryValue[CodingKeys.variation.rawValue] = variation ?? NSNull() - dictionaryValue[CodingKeys.version.rawValue] = version ?? NSNull() - dictionaryValue[CodingKeys.flagVersion.rawValue] = flagVersion ?? NSNull() - dictionaryValue[CodingKeys.trackEvents.rawValue] = trackEvents ? true : NSNull() - dictionaryValue[CodingKeys.debugEventsUntilDate.rawValue] = debugEventsUntilDate?.millisSince1970 ?? NSNull() - dictionaryValue[CodingKeys.reason.rawValue] = reason ?? NSNull() - dictionaryValue[CodingKeys.trackReason.rawValue] = trackReason ? true : NSNull() - return dictionaryValue - } - func shouldCreateDebugEvents(lastEventReportResponseTime: Date?) -> Bool { (lastEventReportResponseTime ?? Date()) <= (debugEventsUntilDate ?? Date.distantPast) } @@ -116,8 +86,8 @@ struct FeatureFlag: Codable { struct FeatureFlagCollection: Codable { let flags: [LDFlagKey: FeatureFlag] - init(_ flags: [FeatureFlag]) { - self.flags = Dictionary(uniqueKeysWithValues: flags.map { ($0.flagKey, $0) }) + init(_ flags: [LDFlagKey: FeatureFlag]) { + self.flags = flags } init(from decoder: Decoder) throws { @@ -134,74 +104,3 @@ struct FeatureFlagCollection: Codable { try flags.encode(to: encoder) } } - -extension FeatureFlag: Equatable { - static func == (lhs: FeatureFlag, rhs: FeatureFlag) -> Bool { - lhs.flagKey == rhs.flagKey && - lhs.variation == rhs.variation && - lhs.version == rhs.version && - AnyComparer.isEqual(lhs.reason, to: rhs.reason) && - lhs.trackReason == rhs.trackReason - } -} - -extension Dictionary where Key == LDFlagKey, Value == FeatureFlag { - var dictionaryValue: [String: Any] { self.compactMapValues { $0.dictionaryValue } } -} - -extension Dictionary where Key == String, Value == Any { - var flagKey: String? { - self[FeatureFlag.CodingKeys.flagKey.rawValue] as? String - } - - var value: Any? { - self[FeatureFlag.CodingKeys.value.rawValue] - } - - var variation: Int? { - self[FeatureFlag.CodingKeys.variation.rawValue] as? Int - } - - var version: Int? { - self[FeatureFlag.CodingKeys.version.rawValue] as? Int - } - - var flagVersion: Int? { - self[FeatureFlag.CodingKeys.flagVersion.rawValue] as? Int - } - - var trackEvents: Bool? { - self[FeatureFlag.CodingKeys.trackEvents.rawValue] as? Bool - } - - var debugEventsUntilDate: Int64? { - self[FeatureFlag.CodingKeys.debugEventsUntilDate.rawValue] as? Int64 - } - - var reason: [String: Any]? { - self[FeatureFlag.CodingKeys.reason.rawValue] as? [String: Any] - } - - var trackReason: Bool? { - self[FeatureFlag.CodingKeys.trackReason.rawValue] as? Bool - } - - var flagCollection: [LDFlagKey: FeatureFlag]? { - guard !(self is [LDFlagKey: FeatureFlag]) - else { - return self as? [LDFlagKey: FeatureFlag] - } - let flagCollection = [LDFlagKey: FeatureFlag](uniqueKeysWithValues: compactMap { flagKey, value -> (LDFlagKey, FeatureFlag)? in - var elementDictionary = value as? [String: Any] - if elementDictionary?[FeatureFlag.CodingKeys.flagKey.rawValue] == nil { - elementDictionary?[FeatureFlag.CodingKeys.flagKey.rawValue] = flagKey - } - guard let featureFlag = FeatureFlag(dictionary: elementDictionary) - else { return nil } - return (flagKey, featureFlag) - }) - guard flagCollection.count == self.count - else { return nil } - return flagCollection - } -} diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift deleted file mode 100644 index 6c888ba7..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift +++ /dev/null @@ -1,128 +0,0 @@ -import Foundation - -/// Defines the types and values of a feature flag. The SDK limits feature flags to these types by use of the `LDFlagValueConvertible` protocol, which uses this type. Client app developers should not construct an LDFlagValue. -enum LDFlagValue: Equatable { - /// Bool flag value - case bool(Bool) - /// Int flag value - case int(Int) - /// Double flag value - case double(Double) - /// String flag value - case string(String) - /// Array flag value - case array([LDFlagValue]) - /// Dictionary flag value - case dictionary([LDFlagKey: LDFlagValue]) - /// Null flag value - case null -} - -// The commented out code in this file is intended to support automated typing from the json, which is not implemented in the 4.0.0 release. When that capability can be supported with later Swift versions, uncomment this code to support it. - -// MARK: - Bool - -// extension LDFlagValue: ExpressibleByBooleanLiteral { -// init(_ value: Bool) { -// self = .bool(value) -// } -// -// public init(booleanLiteral value: Bool) { -// self.init(value) -// } -// } - -// MARK: - Int - -// extension LDFlagValue: ExpressibleByIntegerLiteral { -// public init(_ value: Int) { -// self = .int(value) -// } -// -// public init(integerLiteral value: Int) { -// self.init(value) -// } -// } - -// MARK: - Double - -// extension LDFlagValue: ExpressibleByFloatLiteral { -// public init(_ value: FloatLiteralType) { -// self = .double(value) -// } -// -// public init(floatLiteral value: FloatLiteralType) { -// self.init(value) -// } -// } - -// MARK: - String - -// extension LDFlagValue: ExpressibleByStringLiteral { -// public init(_ value: StringLiteralType) { -// self = .string(value) -// } -// -// public init(unicodeScalarLiteral value: StringLiteralType) { -// self.init(value) -// } -// -// public init(extendedGraphemeClusterLiteral value: StringLiteralType) { -// self.init(value) -// } -// -// public init(stringLiteral value: StringLiteralType) { -// self.init(value) -// } -// } - -// MARK: - Array - -// extension LDFlagValue: ExpressibleByArrayLiteral { -// public init(_ collection: Collection) where Collection.Iterator.Element == LDFlagValue { -// self = .array(Array(collection)) -// } -// -// public init(arrayLiteral elements: LDFlagValue...) { -// self.init(elements) -// } -// } - -extension LDFlagValue { - var flagValueArray: [LDFlagValue]? { - guard case let .array(array) = self - else { return nil } - return array - } -} - -// MARK: - Dictionary - -// extension LDFlagValue: ExpressibleByDictionaryLiteral { -// public typealias Key = LDFlagKey -// public typealias Value = LDFlagValue -// -// public init(_ keyValuePairs: Dictionary) where Dictionary.Iterator.Element == (Key, Value) { -// var dictionary = [Key: Value]() -// for (key, value) in keyValuePairs { -// dictionary[key] = value -// } -// self.init(dictionary) -// } -// -// public init(dictionaryLiteral elements: (Key, Value)...) { -// self.init(elements) -// } -// -// public init(_ dictionary: Dictionary) { -// self = .dictionary(dictionary) -// } -// } - -extension LDFlagValue { - var flagValueDictionary: [LDFlagKey: LDFlagValue]? { - guard case let .dictionary(value) = self - else { return nil } - return value - } -} diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift deleted file mode 100644 index 2f3cf1cb..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift +++ /dev/null @@ -1,118 +0,0 @@ -import Foundation - -/// Protocol used by the SDK to limit feature flag types to those representable on LaunchDarkly servers. Client app developers should not need to use this protocol. The protocol is public because `LDClient.variation(forKey:defaultValue:)` and `LDClient.variationDetail(forKey:defaultValue:)` return a type that conforms to this protocol. See `LDFlagValue` for types that LaunchDarkly feature flags can take. -public protocol LDFlagValueConvertible { -// This commented out code here and in each extension will be used to support automatic typing. Version `4.0.0` does not support that capability. When that capability is added, uncomment this code. -// func toLDFlagValue() -> LDFlagValue -} - -/// :nodoc: -extension Bool: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// return .bool(self) -// } -} - -/// :nodoc: -extension Int: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// return .int(self) -// } -} - -/// :nodoc: -extension Double: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// return .double(self) -// } -} - -/// :nodoc: -extension String: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// return .string(self) -// } -} - -/// :nodoc: -extension Array where Element: LDFlagValueConvertible { -// func toLDFlagValue() -> LDFlagValue { -// let flagValues = self.map { (element) in -// element.toLDFlagValue() -// } -// return .array(flagValues) -// } -} - -/// :nodoc: -extension Array: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// guard let flags = self as? [LDFlagValueConvertible] -// else { -// return .null -// } -// let flagValues = flags.map { (element) in -// element.toLDFlagValue() -// } -// return .array(flagValues) -// } -} - -/// :nodoc: -extension Dictionary where Value: LDFlagValueConvertible { -// func toLDFlagValue() -> LDFlagValue { -// var flagValues = [LDFlagKey: LDFlagValue]() -// for (key, value) in self { -// flagValues[String(describing: key)] = value.toLDFlagValue() -// } -// return .dictionary(flagValues) -// } -} - -/// :nodoc: -extension Dictionary: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// if let flagValueDictionary = self as? [LDFlagKey: LDFlagValue] { -// return .dictionary(flagValueDictionary) -// } -// guard let flagValues = Dictionary.convertToFlagValues(self as? [LDFlagKey: LDFlagValueConvertible]) -// else { -// return .null -// } -// return .dictionary(flagValues) -// } -// -// static func convertToFlagValues(_ dictionary: [LDFlagKey: LDFlagValueConvertible]?) -> [LDFlagKey: LDFlagValue]? { -// guard let dictionary = dictionary -// else { -// return nil -// } -// var flagValues = [LDFlagKey: LDFlagValue]() -// for (key, value) in dictionary { -// flagValues[String(describing: key)] = value.toLDFlagValue() -// } -// return flagValues -// } -} - -/// :nodoc: -extension NSNull: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// return .null -// } -} - -// extension LDFlagValueConvertible { -// func isEqual(to other: LDFlagValueConvertible) -> Bool { -// switch (self.toLDFlagValue(), other.toLDFlagValue()) { -// case (.bool(let value), .bool(let otherValue)): return value == otherValue -// case (.int(let value), .int(let otherValue)): return value == otherValue -// case (.double(let value), .double(let otherValue)): return value == otherValue -// case (.string(let value), .string(let otherValue)): return value == otherValue -// case (.array(let value), .array(let otherValue)): return value == otherValue -// case (.dictionary(let value), .dictionary(let otherValue)): return value == otherValue -// case (.null, .null): return true -// default: return false -// } -// } -// } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift index ceb12c10..5b467085 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift @@ -10,9 +10,9 @@ public final class LDEvaluationDetail { /// The index of the returned value within the flag's list of variations, or `nil` if the default was returned. public internal(set) var variationIndex: Int? /// A structure representing the main factor that influenced the resultant flag evaluation value. - public internal(set) var reason: [String: Any]? + public internal(set) var reason: [String: LDValue]? - internal init(value: T, variationIndex: Int?, reason: [String: Any]?) { + internal init(value: T, variationIndex: Int?, reason: [String: LDValue]?) { self.value = value self.variationIndex = variationIndex self.reason = reason diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 10257a42..91ec678b 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -50,9 +50,6 @@ public struct LDUser: Encodable { */ public var privateAttributes: [UserAttribute] - /// An NSObject wrapper for the Swift LDUser struct. Intended for use in mixed apps when Swift code needs to pass a user into an Objective-C method. - public var objcLdUser: ObjcLDUser { ObjcLDUser(self) } - var contextKind: String { isAnonymous ? "anonymousUser" : "user" } /** @@ -199,10 +196,4 @@ extension LDUser: Equatable { } } -extension LDUserWrapper { - struct Keys { - fileprivate static let featureFlags = "featuresJsonDictionary" - } -} - extension LDUser: TypeIdentifying { } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift index 73eba2a7..a02a0cad 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift @@ -29,11 +29,11 @@ public class ObjcLDChangedFlag: NSObject { public final class ObjcLDBoolChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: Bool { - (changedFlag.oldValue.toAny() as? Bool) ?? false + changedFlag.oldValue.booleanValue() } /// The changed flag's value after it changed @objc public var newValue: Bool { - (changedFlag.newValue.toAny() as? Bool) ?? false + changedFlag.newValue.booleanValue() } override init(_ changedFlag: LDChangedFlag) { @@ -75,11 +75,11 @@ public final class ObjcLDIntegerChangedFlag: ObjcLDChangedFlag { public final class ObjcLDDoubleChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: Double { - (changedFlag.oldValue.toAny() as? Double) ?? 0.0 + changedFlag.oldValue.doubleValue() } /// The changed flag's value after it changed @objc public var newValue: Double { - (changedFlag.newValue.toAny() as? Double) ?? 0.0 + changedFlag.newValue.doubleValue() } override init(_ changedFlag: LDChangedFlag) { @@ -98,11 +98,11 @@ public final class ObjcLDDoubleChangedFlag: ObjcLDChangedFlag { public final class ObjcLDStringChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: String? { - (changedFlag.oldValue.toAny() as? String) + changedFlag.oldValue.stringValue() } /// The changed flag's value after it changed @objc public var newValue: String? { - (changedFlag.newValue.toAny() as? String) + changedFlag.newValue.stringValue() } override init(_ changedFlag: LDChangedFlag) { diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index cc0fb65d..3d345679 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -188,7 +188,7 @@ public final class ObjcLDClient: NSObject { */ /// - Tag: boolVariation @objc public func boolVariation(forKey key: LDFlagKey, defaultValue: Bool) -> Bool { - ldClient.variation(forKey: key, defaultValue: defaultValue) + ldClient.boolVariation(forKey: key, defaultValue: defaultValue) } /** @@ -200,8 +200,8 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDBoolEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func boolVariationDetail(forKey key: LDFlagKey, defaultValue: Bool) -> ObjcLDBoolEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDBoolEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) + let evaluationDetail = ldClient.boolVariationDetail(forKey: key, defaultValue: defaultValue) + return ObjcLDBoolEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) } /** @@ -229,7 +229,7 @@ public final class ObjcLDClient: NSObject { */ /// - Tag: integerVariation @objc public func integerVariation(forKey key: LDFlagKey, defaultValue: Int) -> Int { - ldClient.variation(forKey: key, defaultValue: defaultValue) + ldClient.intVariation(forKey: key, defaultValue: defaultValue) } /** @@ -241,8 +241,8 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDIntegerEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func integerVariationDetail(forKey key: LDFlagKey, defaultValue: Int) -> ObjcLDIntegerEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDIntegerEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) + let evaluationDetail = ldClient.intVariationDetail(forKey: key, defaultValue: defaultValue) + return ObjcLDIntegerEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) } /** @@ -270,7 +270,7 @@ public final class ObjcLDClient: NSObject { */ /// - Tag: doubleVariation @objc public func doubleVariation(forKey key: LDFlagKey, defaultValue: Double) -> Double { - ldClient.variation(forKey: key, defaultValue: defaultValue) + ldClient.doubleVariation(forKey: key, defaultValue: defaultValue) } /** @@ -282,8 +282,8 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDDoubleEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func doubleVariationDetail(forKey key: LDFlagKey, defaultValue: Double) -> ObjcLDDoubleEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDDoubleEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) + let evaluationDetail = ldClient.doubleVariationDetail(forKey: key, defaultValue: defaultValue) + return ObjcLDDoubleEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) } /** @@ -311,7 +311,7 @@ public final class ObjcLDClient: NSObject { */ /// - Tag: stringVariation @objc public func stringVariation(forKey key: LDFlagKey, defaultValue: String) -> String { - ldClient.variation(forKey: key, defaultValue: defaultValue) + ldClient.stringVariation(forKey: key, defaultValue: defaultValue) } /** @@ -323,8 +323,8 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDStringEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func stringVariationDetail(forKey key: LDFlagKey, defaultValue: String) -> ObjcLDStringEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDStringEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) + let evaluationDetail = ldClient.stringVariationDetail(forKey: key, defaultValue: defaultValue) + return ObjcLDStringEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) } /** @@ -351,9 +351,9 @@ public final class ObjcLDClient: NSObject { - returns: The requested NSArray feature flag value, or the default value if the flag is missing or cannot be cast to a NSArray, or the client is not started */ /// - Tag: arrayVariation - @objc public func arrayVariation(forKey key: LDFlagKey, defaultValue: [Any]) -> [Any] { - ldClient.variation(forKey: key, defaultValue: defaultValue) - } +// @objc public func arrayVariation(forKey key: LDFlagKey, defaultValue: [Any]) -> [Any] { +// ldClient.variation(forKey: key, defaultValue: defaultValue) +// } /** See [arrayVariation](x-source-tag://arrayVariation) for more information on variation methods. @@ -363,10 +363,10 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDArrayEvaluationDetail containing your value as well as useful information on why that value was returned. */ - @objc public func arrayVariationDetail(forKey key: LDFlagKey, defaultValue: [Any]) -> ObjcLDArrayEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDArrayEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) - } +// @objc public func arrayVariationDetail(forKey key: LDFlagKey, defaultValue: [Any]) -> ObjcLDArrayEvaluationDetail { +// let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) +// return ObjcLDArrayEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() }) +// } /** Returns the NSDictionary variation for the given feature flag. If the flag does not exist, cannot be cast to a NSDictionary, or the LDClient is not started, returns the default value. @@ -392,9 +392,9 @@ public final class ObjcLDClient: NSObject { - returns: The requested NSDictionary feature flag value, or the default value if the flag is missing or cannot be cast to a NSDictionary, or the client is not started */ /// - Tag: dictionaryVariation - @objc public func dictionaryVariation(forKey key: LDFlagKey, defaultValue: [String: Any]) -> [String: Any] { - ldClient.variation(forKey: key, defaultValue: defaultValue) - } +// @objc public func dictionaryVariation(forKey key: LDFlagKey, defaultValue: [String: Any]) -> [String: Any] { +// ldClient.variation(forKey: key, defaultValue: defaultValue) +// } /** See [dictionaryVariation](x-source-tag://dictionaryVariation) for more information on variation methods. @@ -404,10 +404,10 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDDictionaryEvaluationDetail containing your value as well as useful information on why that value was returned. */ - @objc public func dictionaryVariationDetail(forKey key: LDFlagKey, defaultValue: [String: Any]) -> ObjcLDDictionaryEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDDictionaryEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) - } +// @objc public func dictionaryVariationDetail(forKey key: LDFlagKey, defaultValue: [String: Any]) -> ObjcLDDictionaryEvaluationDetail { +// let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) +// return ObjcLDDictionaryEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() }) +// } /** Returns a dictionary with the flag keys and their values. If the LDClient is not started, returns nil. @@ -416,7 +416,7 @@ public final class ObjcLDClient: NSObject { LDClient will not provide any source or change information, only flag keys and flag values. The client app should convert the feature flag value into the desired type. */ - @objc public var allFlags: [LDFlagKey: Any]? { ldClient.allFlags } + @objc public var allFlags: [LDFlagKey: Any]? { ldClient.allFlags?.mapValues { $0.toAny() ?? NSNull() } } // MARK: - Feature Flag Updates diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index 070f5a66..084cf592 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -2,53 +2,124 @@ import Foundation // sourcery: autoMockable protocol CacheConverting { - func convertCacheData(for user: LDUser, and config: LDConfig) + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) } -// CacheConverter is not thread-safe; run it from a single thread and don't allow other threads to call convertCacheData or data corruption could occur +// Cache model in SDK versions >=4.0.0 <6.0.0. Migration is not supported for earlier versions. +// +// [: [ +// “userKey”: , +// “environmentFlags”: [ +// : [ +// “userKey”: , +// “mobileKey”: , +// “featureFlags”: [ +// : [ +// “key”: , +// “version”: , +// “flagVersion”: , +// “variation”: , +// “value”: , +// “trackEvents”: , +// “debugEventsUntilDate”: , +// "reason: , +// "trackReason": +// ] +// ] +// ] +// ], +// “lastUpdated”: +// ] +// ] + final class CacheConverter: CacheConverting { - struct Constants { - static let maxAge: TimeInterval = -90.0 * 24 * 60 * 60 // 90 days - } + init() { } - struct CacheKeys { - static let ldUserModelDictionary = "ldUserModelDictionary" - static let cachedDataKeyStub = "com.launchdarkly.test.deprecatedCache.cachedDataKey" - } + private func convertV6Data(v6cache: KeyedValueCaching, flagCaches: [MobileKey: FeatureFlagCaching]) { + guard let cachedV6Data = v6cache.dictionary(forKey: "com.launchDarkly.cachedUserEnvironmentFlags") + else { return } - let currentCache: FeatureFlagCaching - private(set) var deprecatedCaches = [DeprecatedCacheModel: DeprecatedCache]() + var cachedEnvData: [MobileKey: [String: (updated: Date, flags: [LDFlagKey: FeatureFlag])]] = [:] + cachedV6Data.forEach { userKey, userDict in + guard let userDict = userDict as? [String: Any], + let userDictUserKey = userDict["userKey"] as? String, + let lastUpdated = (userDict["lastUpdated"] as? String)?.dateValue, + let envsDict = userDict["environmentFlags"] as? [String: Any], + userKey == userDictUserKey + else { return } + envsDict.forEach { mobileKey, envDict in + guard flagCaches.keys.contains(mobileKey), + let envDict = envDict as? [String: Any], + let envUserKey = envDict["userKey"] as? String, + let envMobileKey = envDict["mobileKey"] as? String, + let envFlags = envDict["featureFlags"] as? [String: Any], + envUserKey == userKey && envMobileKey == mobileKey + else { return } - init(serviceFactory: ClientServiceCreating, maxCachedUsers: Int) { - currentCache = serviceFactory.makeFeatureFlagCache(maxCachedUsers: maxCachedUsers) - DeprecatedCacheModel.allCases.forEach { version in - deprecatedCaches[version] = serviceFactory.makeDeprecatedCacheModel(version) + var userEnvFlags: [LDFlagKey: FeatureFlag] = [:] + envFlags.forEach { flagKey, flagDict in + guard let flagDict = flagDict as? [String: Any] + else { return } + let flag = FeatureFlag(flagKey: flagKey, + value: LDValue.fromAny(flagDict["value"]), + variation: flagDict["variation"] as? Int, + version: flagDict["version"] as? Int, + flagVersion: flagDict["flagVersion"] as? Int, + trackEvents: flagDict["trackEvents"] as? Bool ?? false, + debugEventsUntilDate: Date(millisSince1970: flagDict["debugEventsUntilDate"] as? Int64), + reason: (flagDict["reason"] as? [String: Any])?.mapValues { LDValue.fromAny($0) }, + trackReason: flagDict["trackReason"] as? Bool ?? false) + userEnvFlags[flagKey] = flag + } + var otherEnvData = cachedEnvData[mobileKey] ?? [:] + otherEnvData[userKey] = (lastUpdated, userEnvFlags) + cachedEnvData[mobileKey] = otherEnvData + } } - } - func convertCacheData(for user: LDUser, and config: LDConfig) { - convertCacheData(for: user, mobileKey: config.mobileKey) - removeData() + cachedEnvData.forEach { mobileKey, users in + users.forEach { userKey, data in + flagCaches[mobileKey]?.storeFeatureFlags(data.flags, userKey: userKey, lastUpdated: data.updated) + } + } + + v6cache.removeObject(forKey: "com.launchDarkly.cachedUserEnvironmentFlags") } - private func convertCacheData(for user: LDUser, mobileKey: String) { - guard currentCache.retrieveFeatureFlags(forUserWithKey: user.key, andMobileKey: mobileKey) == nil - else { return } - for deprecatedCacheModel in DeprecatedCacheModel.allCases { - let deprecatedCache = deprecatedCaches[deprecatedCacheModel] - guard let cachedData = deprecatedCache?.retrieveFlags(for: user.key, and: mobileKey), - let cachedFlags = cachedData.featureFlags - else { continue } - currentCache.storeFeatureFlags(cachedFlags, userKey: user.key, mobileKey: mobileKey, lastUpdated: cachedData.lastUpdated ?? Date(), storeMode: .sync) - return // If we hit on a cached user, bailout since we converted the flags for that userKey-mobileKey combination; This prefers newer caches over older + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) { + var flagCaches: [String: FeatureFlagCaching] = [:] + keysToConvert.forEach { mobileKey in + let flagCache = serviceFactory.makeFeatureFlagCache(mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) + flagCaches[mobileKey] = flagCache + // Get current cache version and return if up to date + guard let cacheVersionData = flagCache.keyedValueCache.data(forKey: "ld-cache-metadata") + else { return } // Convert those that do not have a version + guard let cacheVersion = (try? JSONDecoder().decode([String: Int].self, from: cacheVersionData))?["version"], + cacheVersion == 7 + else { + // Metadata is invalid, remove existing data and attempt migration + flagCache.keyedValueCache.removeAll() + return + } + // Already up to date + flagCaches.removeValue(forKey: mobileKey) } - } - private func removeData() { - let maxAge = Date().addingTimeInterval(Constants.maxAge) - deprecatedCaches.values.forEach { deprecatedCache in - deprecatedCache.removeData(olderThan: maxAge) + // Skip migration if all environments are V7 + if flagCaches.isEmpty { return } + + // Remove V5 cache data (migration not supported) + let standardDefaults = serviceFactory.makeKeyedValueCache(cacheKey: nil) + standardDefaults.removeObject(forKey: "com.launchdarkly.dataManager.userEnvironments") + + convertV6Data(v6cache: standardDefaults, flagCaches: flagCaches) + + // Set cache version to skip this logic in the future + if let versionMetadata = try? JSONEncoder().encode(["version": 7]) { + flagCaches.forEach { + $0.value.keyedValueCache.set(versionMetadata, forKey: "ld-cache-metadata") + } } } } @@ -58,3 +129,28 @@ extension Date { self.stringEquivalentDate < expirationDate.stringEquivalentDate } } + +extension DateFormatter { + /// Date formatter configured to format dates to/from the format 2018-08-13T19:06:38.123Z + class var ldDateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + formatter.timeZone = TimeZone(identifier: "UTC") + return formatter + } +} + +extension Date { + /// Date string using the format 2018-08-13T19:06:38.123Z + var stringValue: String { DateFormatter.ldDateFormatter.string(from: self) } + + // When a date is converted to JSON, the resulting string is not as precise as the original date (only to the nearest .001s) + // By converting the date to json, then back into a date, the result can be compared with any date re-inflated from json + /// Date truncated to the nearest millisecond, which is the precision for string formatted dates + var stringEquivalentDate: Date { stringValue.dateValue } +} + +extension String { + /// Date converted from a string using the format 2018-08-13T19:06:38.123Z + var dateValue: Date { DateFormatter.ldDateFormatter.date(from: self) ?? Date() } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift deleted file mode 100644 index e952edfb..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation - -protocol DeprecatedCache { - var cachedDataKey: String { get } - var keyedValueCache: KeyedValueCaching { get } - - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan: Date) -> [UserKey] - func removeData(olderThan expirationDate: Date) // provided for testing, to allow the mock to override the protocol extension -} - -extension DeprecatedCache { - func removeData(olderThan expirationDate: Date) { - guard let cachedUserData = keyedValueCache.dictionary(forKey: cachedDataKey) as? [UserKey: [String: Any]], !cachedUserData.isEmpty - else { return } // no cached data - let expiredUserKeys = userKeys(from: cachedUserData, olderThan: expirationDate) - guard !expiredUserKeys.isEmpty - else { return } // no expired user cached data, leave the cache alone - guard expiredUserKeys.count != cachedUserData.count - else { - keyedValueCache.removeObject(forKey: cachedDataKey) // all user cached data is expired, remove the cache key & values - return - } - let unexpiredUserData: [UserKey: [String: Any]] = cachedUserData.filter { userKey, _ in - !expiredUserKeys.contains(userKey) - } - keyedValueCache.set(unexpiredUserData, forKey: cachedDataKey) - } -} - -enum DeprecatedCacheModel: String, CaseIterable { - case version5 // earlier versions are not supported -} - -// updatedAt in cached data was used as the LDUser.lastUpdated, which is deprecated in the Swift SDK -private extension LDUser.CodingKeys { - static let lastUpdated = "updatedAt" // Can't use the CodingKey protocol here, this keeps the usage similar -} - -extension Dictionary where Key == String, Value == Any { - var lastUpdated: Date? { - (self[LDUser.CodingKeys.lastUpdated] as? String)?.dateValue - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift deleted file mode 100644 index f81ceeb6..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Foundation - -// Cache model in use from 2.14.0 up to 4.0.0 -/* Cache model v5 schema -[: [ - “userKey”: , //LDUserEnvironment dictionary - “environments”: [ - : [ - “key: , //LDUserModel dictionary - “ip”: , - “country”: , - “email”: , - “name”: , - “firstName”: , - “lastName”: , - “avatar”: , - “custom”: [ - “device”: , - “os”: , - ...], - “anonymous”: , - “updatedAt: , - ”config”: [ - : [ //LDFlagConfigModel dictionary - “version”: , //LDFlagConfigValue dictionary - “flagVersion”: , - “variation”: , - “value”: , - “trackEvents”: , - “debugEventsUntilDate”: - ] - ], - “privateAttrs”: - ] - ] - ] -] -*/ -final class DeprecatedCacheModelV5: DeprecatedCache { - - struct CacheKeys { - static let userEnvironments = "com.launchdarkly.dataManager.userEnvironments" - static let environments = "environments" - } - - let keyedValueCache: KeyedValueCaching - let cachedDataKey = CacheKeys.userEnvironments - - init(keyedValueCache: KeyedValueCaching) { - self.keyedValueCache = keyedValueCache - } - - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) { - guard let cachedUserEnvironmentsCollection = keyedValueCache.dictionary(forKey: cachedDataKey), !cachedUserEnvironmentsCollection.isEmpty, - let cachedUserEnvironments = cachedUserEnvironmentsCollection[userKey] as? [String: Any], !cachedUserEnvironments.isEmpty, - let cachedEnvironments = cachedUserEnvironments[CacheKeys.environments] as? [MobileKey: [String: Any]], !cachedEnvironments.isEmpty, - let cachedUserDictionary = cachedEnvironments[mobileKey], !cachedUserDictionary.isEmpty, - let featureFlagDictionaries = cachedUserDictionary[LDUser.CodingKeys.config.rawValue] as? [LDFlagKey: [String: Any]] - else { - return (nil, nil) - } - let featureFlags = Dictionary(uniqueKeysWithValues: featureFlagDictionaries.compactMap { flagKey, featureFlagDictionary in - return (flagKey, FeatureFlag(flagKey: flagKey, - value: featureFlagDictionary.value, - variation: featureFlagDictionary.variation, - version: featureFlagDictionary.version, - flagVersion: featureFlagDictionary.flagVersion, - trackEvents: featureFlagDictionary.trackEvents ?? false, - debugEventsUntilDate: Date(millisSince1970: featureFlagDictionary.debugEventsUntilDate))) - }) - return (featureFlags, cachedUserDictionary.lastUpdated) - } - - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan expirationDate: Date) -> [UserKey] { - cachedUserData.compactMap { userKey, userDictionary in - let envsDictionary = userDictionary[CacheKeys.environments] as? [MobileKey: [String: Any]] - let lastUpdated = envsDictionary?.compactMap { $1.lastUpdated }.max() ?? Date.distantFuture - return lastUpdated.isExpired(expirationDate: expirationDate) ? userKey : nil - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift new file mode 100644 index 00000000..4f120db5 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -0,0 +1,56 @@ +import Foundation + +// sourcery: autoMockable +protocol FeatureFlagCaching { + // sourcery: defaultMockValue = KeyedValueCachingMock() + var keyedValueCache: KeyedValueCaching { get } + func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? + func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) +} + +final class FeatureFlagCache: FeatureFlagCaching { + let keyedValueCache: KeyedValueCaching + let maxCachedUsers: Int + + init(serviceFactory: ClientServiceCreating, mobileKey: MobileKey, maxCachedUsers: Int) { + let cacheKey: String + if let bundleId = Bundle.main.bundleIdentifier { + cacheKey = "\(Util.sha256base64(bundleId)).\(Util.sha256base64(mobileKey))" + } else { + cacheKey = Util.sha256base64(mobileKey) + } + self.keyedValueCache = serviceFactory.makeKeyedValueCache(cacheKey: "com.launchdarkly.client.\(cacheKey)") + self.maxCachedUsers = maxCachedUsers + } + + func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? { + guard let cachedData = keyedValueCache.data(forKey: "flags-\(Util.sha256base64(userKey))"), + let cachedFlags = try? JSONDecoder().decode(FeatureFlagCollection.self, from: cachedData) + else { return nil } + return cachedFlags.flags + } + + func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) { + guard self.maxCachedUsers != 0, let encoded = try? JSONEncoder().encode(featureFlags) + else { return } + + let userSha = Util.sha256base64(userKey) + self.keyedValueCache.set(encoded, forKey: "flags-\(userSha)") + + var cachedUsers: [String: Int64] = [:] + if let cacheMetadata = self.keyedValueCache.data(forKey: "cached-users") { + cachedUsers = (try? JSONDecoder().decode([String: Int64].self, from: cacheMetadata)) ?? [:] + } + cachedUsers[userSha] = lastUpdated.millisSince1970 + if cachedUsers.count > self.maxCachedUsers && self.maxCachedUsers > 0 { + let sorted = cachedUsers.sorted { $0.value < $1.value } + sorted.prefix(cachedUsers.count - self.maxCachedUsers).forEach { sha, _ in + cachedUsers.removeValue(forKey: sha) + self.keyedValueCache.removeObject(forKey: "flags-\(sha)") + } + } + if let encoded = try? JSONEncoder().encode(cachedUsers) { + self.keyedValueCache.set(encoded, forKey: "cached-users") + } + } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift index 5598594f..bb6a1de5 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift @@ -2,10 +2,19 @@ import Foundation // sourcery: autoMockable protocol KeyedValueCaching { - func set(_ value: Any?, forKey: String) - // sourcery: DefaultReturnValue = nil + func set(_ value: Data, forKey: String) + func data(forKey: String) -> Data? func dictionary(forKey: String) -> [String: Any]? func removeObject(forKey: String) + func removeAll() } -extension UserDefaults: KeyedValueCaching { } +extension UserDefaults: KeyedValueCaching { + func set(_ value: Data, forKey: String) { + set(value as Any?, forKey: forKey) + } + + func removeAll() { + dictionaryRepresentation().keys.forEach { removeObject(forKey: $0) } + } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift deleted file mode 100644 index 81db8dde..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift +++ /dev/null @@ -1,100 +0,0 @@ -import Foundation - -enum FlagCachingStoreMode: CaseIterable { - case async, sync -} - -// sourcery: autoMockable -protocol FeatureFlagCaching { - // sourcery: defaultMockValue = 5 - var maxCachedUsers: Int { get set } - - func retrieveFeatureFlags(forUserWithKey userKey: String, andMobileKey mobileKey: String) -> [LDFlagKey: FeatureFlag]? - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode) -} - -final class UserEnvironmentFlagCache: FeatureFlagCaching { - - struct Constants { - static let cacheStoreOperationQueueLabel = "com.launchDarkly.FeatureFlagCaching.cacheStoreOperationQueue" - } - - struct CacheKeys { - static let cachedUserEnvironmentFlags = "com.launchDarkly.cachedUserEnvironmentFlags" - } - - private(set) var keyedValueCache: KeyedValueCaching - var maxCachedUsers: Int - - private static let cacheStoreOperationQueue = DispatchQueue(label: Constants.cacheStoreOperationQueueLabel, qos: .background) - - init(withKeyedValueCache keyedValueCache: KeyedValueCaching, maxCachedUsers: Int) { - self.keyedValueCache = keyedValueCache - self.maxCachedUsers = maxCachedUsers - } - - func retrieveFeatureFlags(forUserWithKey userKey: String, andMobileKey mobileKey: String) -> [LDFlagKey: FeatureFlag]? { - let cacheableUserEnvironmentsCollection = retrieveCacheableUserEnvironmentsCollection() - return cacheableUserEnvironmentsCollection[userKey]?.environmentFlags[mobileKey]?.featureFlags - } - - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode) { - storeFeatureFlags(featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated, storeMode: storeMode, completion: nil) - } - - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], - userKey: String, - mobileKey: String, - lastUpdated: Date, - storeMode: FlagCachingStoreMode = .async, - completion: (() -> Void)?) { - if storeMode == .async { - UserEnvironmentFlagCache.cacheStoreOperationQueue.async { - self.storeFlags(featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated) - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - } - } else { - UserEnvironmentFlagCache.cacheStoreOperationQueue.sync { - self.storeFlags(featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated) - } - } - } - - private func storeFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, mobileKey: String, lastUpdated: Date) { - var cacheableUserEnvironmentsCollection = self.retrieveCacheableUserEnvironmentsCollection() - let selectedCacheableUserEnvironments = cacheableUserEnvironmentsCollection[userKey] ?? CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: [:], lastUpdated: Date()) - var environmentFlags = selectedCacheableUserEnvironments.environmentFlags - environmentFlags[mobileKey] = CacheableEnvironmentFlags(userKey: userKey, mobileKey: mobileKey, featureFlags: featureFlags) - cacheableUserEnvironmentsCollection[userKey] = CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: environmentFlags, lastUpdated: lastUpdated) - self.store(cacheableUserEnvironmentsCollection: cacheableUserEnvironmentsCollection) - } - - // MARK: - CacheableUserEnvironmentsCollection - private func store(cacheableUserEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags]) { - let userEnvironmentsCollection = removeOldestUsersIfNeeded(from: cacheableUserEnvironmentsCollection) - keyedValueCache.set(userEnvironmentsCollection.compactMapValues { $0.dictionaryValue }, forKey: CacheKeys.cachedUserEnvironmentFlags) - } - - private func removeOldestUsersIfNeeded(from cacheableUserEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags]) -> [UserKey: CacheableUserEnvironmentFlags] { - guard cacheableUserEnvironmentsCollection.count > maxCachedUsers && maxCachedUsers >= 0 - else { - return cacheableUserEnvironmentsCollection - } - // sort collection into key-value pairs in descending order...youngest to oldest - var userEnvironmentsCollection = cacheableUserEnvironmentsCollection.sorted { - $1.value.lastUpdated < $0.value.lastUpdated - } - while userEnvironmentsCollection.count > maxCachedUsers && maxCachedUsers >= 0 { - userEnvironmentsCollection.removeLast() - } - return [UserKey: CacheableUserEnvironmentFlags](userEnvironmentsCollection, uniquingKeysWith: { value1, _ in - value1 - }) - } - - private func retrieveCacheableUserEnvironmentsCollection() -> [UserKey: CacheableUserEnvironmentFlags] { - keyedValueCache.dictionary(forKey: CacheKeys.cachedUserEnvironmentFlags)?.compactMapValues { CacheableUserEnvironmentFlags(object: $0) } ?? [:] - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index 51112d0e..aa804f8a 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -2,10 +2,9 @@ import Foundation import LDSwiftEventSource protocol ClientServiceCreating { - func makeKeyedValueCache() -> KeyedValueCaching - func makeFeatureFlagCache(maxCachedUsers: Int) -> FeatureFlagCaching - func makeCacheConverter(maxCachedUsers: Int) -> CacheConverting - func makeDeprecatedCacheModel(_ model: DeprecatedCacheModel) -> DeprecatedCache + func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching + func makeFeatureFlagCache(mobileKey: String, maxCachedUsers: Int) -> FeatureFlagCaching + func makeCacheConverter() -> CacheConverting func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, useReport: Bool, service: DarklyServiceProvider) -> LDFlagSynchronizing func makeFlagSynchronizer(streamingMode: LDStreamingMode, @@ -26,22 +25,16 @@ protocol ClientServiceCreating { } final class ClientServiceFactory: ClientServiceCreating { - func makeKeyedValueCache() -> KeyedValueCaching { - UserDefaults.standard + func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching { + UserDefaults(suiteName: cacheKey)! } - func makeFeatureFlagCache(maxCachedUsers: Int) -> FeatureFlagCaching { - UserEnvironmentFlagCache(withKeyedValueCache: makeKeyedValueCache(), maxCachedUsers: maxCachedUsers) + func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedUsers: Int) -> FeatureFlagCaching { + FeatureFlagCache(serviceFactory: self, mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) } - func makeCacheConverter(maxCachedUsers: Int) -> CacheConverting { - CacheConverter(serviceFactory: self, maxCachedUsers: maxCachedUsers) - } - - func makeDeprecatedCacheModel(_ model: DeprecatedCacheModel) -> DeprecatedCache { - switch model { - case .version5: return DeprecatedCacheModelV5(keyedValueCache: makeKeyedValueCache()) - } + func makeCacheConverter() -> CacheConverting { + CacheConverter() } func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift index 296c5a00..9938eb90 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift @@ -90,7 +90,7 @@ final class FlagChangeNotifier: FlagChangeNotifying { } let changedFlags = [LDFlagKey: LDChangedFlag](uniqueKeysWithValues: changedFlagKeys.map { - ($0, LDChangedFlag(key: $0, oldValue: LDValue.fromAny(oldFlags[$0]?.value), newValue: LDValue.fromAny(newFlags[$0]?.value))) + ($0, LDChangedFlag(key: $0, oldValue: oldFlags[$0]?.value ?? .null, newValue: newFlags[$0]?.value ?? .null)) }) Log.debug(typeName(and: #function) + "notifying observers for changes to flags: \(changedFlags.keys.joined(separator: ", ")).") selectedObservers.forEach { observer in @@ -113,12 +113,15 @@ final class FlagChangeNotifier: FlagChangeNotifying { } private func findChangedFlagKeys(oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey] { - oldFlags.symmetricDifference(newFlags) // symmetricDifference tests for equality, which includes version. Exclude version here. - .filter { - guard let old = oldFlags[$0], let new = newFlags[$0] - else { return true } - return !(old.variation == new.variation && AnyComparer.isEqual(old.value, to: new.value)) + let oldKeys = Set(oldFlags.keys) + let newKeys = Set(newFlags.keys) + let newOrDeletedKeys = oldKeys.symmetricDifference(newKeys) + let updatedKeys = oldKeys.intersection(newKeys).filter { possibleUpdatedKey in + guard let old = oldFlags[possibleUpdatedKey], let new = newFlags[possibleUpdatedKey] + else { return true } + return old.variation != new.variation || old.value != new.value } + return newOrDeletedKeys.union(updatedKeys).sorted() } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift index ac9ed9e2..f613ba97 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -3,9 +3,9 @@ import Foundation protocol FlagMaintaining { var featureFlags: [LDFlagKey: FeatureFlag] { get } - func replaceStore(newFlags: [LDFlagKey: Any], completion: CompletionClosure?) - func updateStore(updateDictionary: [LDFlagKey: Any], completion: CompletionClosure?) - func deleteFlag(deleteDictionary: [LDFlagKey: Any], completion: CompletionClosure?) + func replaceStore(newFlags: FeatureFlagCollection) + func updateStore(updatedFlag: FeatureFlag) + func deleteFlag(deleteResponse: DeleteResponse) func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? } @@ -14,10 +14,6 @@ final class FlagStore: FlagMaintaining { fileprivate static let flagQueueLabel = "com.launchdarkly.flagStore.flagQueue" } - struct Keys { - static let flagKey = "key" - } - var featureFlags: [LDFlagKey: FeatureFlag] { flagQueue.sync { _featureFlags } } private var _featureFlags: [LDFlagKey: FeatureFlag] = [:] @@ -26,87 +22,44 @@ final class FlagStore: FlagMaintaining { init() { } - init(featureFlags: [LDFlagKey: FeatureFlag]?) { + init(featureFlags: [LDFlagKey: FeatureFlag]) { Log.debug(typeName(and: #function) + "featureFlags: \(String(describing: featureFlags))") - self._featureFlags = featureFlags ?? [:] - } - - convenience init(featureFlagDictionary: [LDFlagKey: Any]?) { - self.init(featureFlags: featureFlagDictionary?.flagCollection) + self._featureFlags = featureFlags } - /// Replaces all feature flags with new flags. Pass nil to reset to an empty flag store - func replaceStore(newFlags: [LDFlagKey: Any], completion: CompletionClosure?) { + func replaceStore(newFlags: FeatureFlagCollection) { Log.debug(typeName(and: #function) + "newFlags: \(String(describing: newFlags))") - flagQueue.async(flags: .barrier) { - self._featureFlags = newFlags.flagCollection ?? [:] - if let completion = completion { - DispatchQueue.main.async { - completion() - } - } + flagQueue.sync(flags: .barrier) { + self._featureFlags = newFlags.flags } } - // An update dictionary is the same as a flag dictionary. The version will be validated and if it's newer than the - // stored flag, the store will replace the flag with the updated flag. - func updateStore(updateDictionary: [LDFlagKey: Any], completion: CompletionClosure?) { - flagQueue.async(flags: .barrier) { - defer { - if let completion = completion { - DispatchQueue.main.async { - completion() - } - } - } - guard let flagKey = updateDictionary[Keys.flagKey] as? String, - let newFlag = FeatureFlag(dictionary: updateDictionary) - else { - Log.debug(self.typeName(and: #function) + "aborted. Malformed update dictionary. updateDictionary: \(String(describing: updateDictionary))") - return - } - guard self.isValidVersion(for: flagKey, newVersion: newFlag.version) + func updateStore(updatedFlag: FeatureFlag) { + flagQueue.sync(flags: .barrier) { + guard self.isValidVersion(for: updatedFlag.flagKey, newVersion: updatedFlag.version) else { - Log.debug(self.typeName(and: #function) + "aborted. Invalid version. updateDictionary: \(String(describing: updateDictionary)) " - + "existing flag: \(String(describing: self._featureFlags[flagKey]))") + Log.debug(self.typeName(and: #function) + "aborted. Invalid version. updateDictionary: \(updatedFlag) " + + "existing flag: \(String(describing: self._featureFlags[updatedFlag.flagKey]))") return } - Log.debug(self.typeName(and: #function) + "succeeded. new flag: \(newFlag), " + "prior flag: \(String(describing: self._featureFlags[flagKey]))") - self._featureFlags.updateValue(newFlag, forKey: flagKey) + Log.debug(self.typeName(and: #function) + "succeeded. new flag: \(updatedFlag), " + + "prior flag: \(String(describing: self._featureFlags[updatedFlag.flagKey]))") + self._featureFlags.updateValue(updatedFlag, forKey: updatedFlag.flagKey) } } - /* deleteDictionary should have the form: - { - "key": , - "version": - } - */ - func deleteFlag(deleteDictionary: [LDFlagKey: Any], completion: CompletionClosure?) { - flagQueue.async(flags: .barrier) { - defer { - if let completion = completion { - DispatchQueue.main.async { - completion() - } - } - } - guard let flagKey = deleteDictionary[Keys.flagKey] as? String, - let newVersion = deleteDictionary[FeatureFlag.CodingKeys.version.rawValue] as? Int - else { - Log.debug(self.typeName(and: #function) + "aborted. Malformed delete dictionary. deleteDictionary: \(String(describing: deleteDictionary))") - return - } - guard self.isValidVersion(for: flagKey, newVersion: newVersion) + func deleteFlag(deleteResponse: DeleteResponse) { + flagQueue.sync(flags: .barrier) { + guard self.isValidVersion(for: deleteResponse.key, newVersion: deleteResponse.version) else { - Log.debug(self.typeName(and: #function) + "aborted. Invalid version. deleteDictionary: \(String(describing: deleteDictionary)) " - + "existing flag: \(String(describing: self._featureFlags[flagKey]))") + Log.debug(self.typeName(and: #function) + "aborted. Invalid version. deleteResponse: \(deleteResponse) " + + "existing flag: \(String(describing: self._featureFlags[deleteResponse.key]))") return } - Log.debug(self.typeName(and: #function) + "deleted flag with key: " + flagKey) - self._featureFlags.removeValue(forKey: flagKey) + Log.debug(self.typeName(and: #function) + "deleted flag with key: " + deleteResponse.key) + self._featureFlags.removeValue(forKey: deleteResponse.key) } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift index 741358c2..6789c7c6 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift @@ -35,18 +35,21 @@ enum SynchronizingError: Error { } enum FlagSyncResult { - case success([String: Any], FlagUpdateType?) + case flagCollection(FeatureFlagCollection) + case patch(FeatureFlag) + case delete(DeleteResponse) case upToDate case error(SynchronizingError) } +struct DeleteResponse: Decodable { + let key: String + let version: Int? +} + typealias CompletionClosure = (() -> Void) typealias FlagSyncCompleteClosure = ((FlagSyncResult) -> Void) -enum FlagUpdateType: String { - case ping, put, patch, delete -} - class FlagSynchronizer: LDFlagSynchronizing, EventHandler { struct Constants { fileprivate static let queueName = "LaunchDarkly.FlagSynchronizer.syncQueue" @@ -76,8 +79,6 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { let pollingInterval: TimeInterval let useReport: Bool - var streamingActive: Bool { eventSource != nil } - var pollingActive: Bool { flagRequestTimer != nil } private var syncQueue = DispatchQueue(label: Constants.queueName, qos: .utility) private var eventSourceStarted: Date? @@ -99,10 +100,10 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { switch streamingMode { case .streaming: stopPolling() - startEventSource(isOnline: isOnline) + startEventSource() case .polling: stopEventSource() - startPolling(isOnline: isOnline) + startPolling() } } else { stopEventSource() @@ -112,24 +113,9 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { // MARK: Streaming - private func startEventSource(isOnline: Bool) { - guard isOnline, - streamingMode == .streaming, - !streamingActive - else { - var reason = "" - if !isOnline { - reason = "Flag Synchronizer is offline." - } - if reason.isEmpty && streamingMode != .streaming { - reason = "Flag synchronizer is not set for streaming." - } - if reason.isEmpty && streamingActive { - reason = "Clientstream already connected." - } - Log.debug(typeName(and: #function) + "aborted. " + reason) - return - } + private func startEventSource() { + guard eventSource == nil + else { return Log.debug(typeName(and: #function) + "aborted. Clientstream already connected.") } Log.debug(typeName(and: #function)) eventSourceStarted = Date() @@ -141,10 +127,9 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { } private func stopEventSource() { - guard streamingActive else { - Log.debug(typeName(and: #function) + "aborted. Clientstream is not connected.") - return - } + guard eventSource != nil + else { return Log.debug(typeName(and: #function) + "aborted. Clientstream is not connected.") } + Log.debug(typeName(and: #function)) eventSource?.stop() eventSource = nil @@ -152,32 +137,19 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { // MARK: Polling - private func startPolling(isOnline: Bool) { - guard isOnline, - streamingMode == .polling, - !pollingActive - else { - var reason = "" - if !isOnline { reason = "Flag Synchronizer is offline." } - if reason.isEmpty && streamingMode != .polling { - reason = "Flag synchronizer is not set for polling." - } - if reason.isEmpty && pollingActive { - reason = "Polling already active." - } - Log.debug(typeName(and: #function) + "aborted. " + reason) - return - } + private func startPolling() { + guard flagRequestTimer == nil + else { return Log.debug(typeName(and: #function) + "aborted. Polling already active.") } + Log.debug(typeName(and: #function)) flagRequestTimer = LDTimer(withTimeInterval: pollingInterval, fireQueue: syncQueue, execute: processTimer) - makeFlagRequest(isOnline: isOnline) + makeFlagRequest(isOnline: true) } private func stopPolling() { - guard pollingActive else { - Log.debug(typeName(and: #function) + "aborted. Polling already inactive.") - return - } + guard flagRequestTimer != nil + else { return Log.debug(typeName(and: #function) + "aborted. Polling already inactive.") } + Log.debug(typeName(and: #function)) flagRequestTimer?.cancel() flagRequestTimer = nil @@ -234,17 +206,12 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { return } guard let data = serviceResponse.data, - let flags = try? JSONSerialization.jsonDictionary(with: data, options: .allowFragments) + let flagCollection = try? JSONDecoder().decode(FeatureFlagCollection.self, from: data) else { reportDataError(serviceResponse.data) return } - reportSuccess(flagDictionary: flags, eventType: streamingActive ? .ping : nil) - } - - private func reportSuccess(flagDictionary: [String: Any], eventType: FlagUpdateType?) { - Log.debug(typeName(and: #function) + "flagDictionary: \(flagDictionary)" + (eventType == nil ? "" : ", eventType: \(String(describing: eventType))")) - reportSyncComplete(.success(flagDictionary, streamingActive ? eventType : nil)) + reportSyncComplete(.flagCollection(flagCollection)) } private func reportDataError(_ data: Data?) { @@ -301,7 +268,7 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { reportSyncComplete(.error(.streamEventWhilePolling)) return true } - if !streamingActive { + if eventSource == nil { // Since eventSource.close() is async, this prevents responding to events after .close() is called, but before it's actually closed Log.debug(typeName(and: #function) + "aborted. " + "Clientstream is not active.") reportSyncComplete(.error(.isOffline)) @@ -329,18 +296,33 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { guard !shouldAbortStreamUpdate() else { return } - let updateType: FlagUpdateType? = FlagUpdateType(rawValue: eventType) - switch updateType { - case .ping: makeFlagRequest(isOnline: isOnline) - case .put, .patch, .delete: + switch eventType { + case "ping": makeFlagRequest(isOnline: isOnline) + case "put": + guard let data = messageEvent.data.data(using: .utf8), + let flagCollection = try? JSONDecoder().decode(FeatureFlagCollection.self, from: data) + else { + reportDataError(messageEvent.data.data(using: .utf8)) + return + } + reportSyncComplete(.flagCollection(flagCollection)) + case "patch": + guard let data = messageEvent.data.data(using: .utf8), + let flag = try? JSONDecoder().decode(FeatureFlag.self, from: data) + else { + reportDataError(messageEvent.data.data(using: .utf8)) + return + } + reportSyncComplete(.patch(flag)) + case "delete": guard let data = messageEvent.data.data(using: .utf8), - let flagDictionary = try? JSONSerialization.jsonDictionary(with: data) + let deleteResponse = try? JSONDecoder().decode(DeleteResponse.self, from: data) else { reportDataError(messageEvent.data.data(using: .utf8)) return } - reportSuccess(flagDictionary: flagDictionary, eventType: updateType) - case nil: + reportSyncComplete(.delete(deleteResponse)) + default: Log.debug(typeName(and: #function) + "aborted. Unknown event type.") reportSyncComplete(.error(.unknownEventType(eventType))) return @@ -372,21 +354,9 @@ extension FlagSynchronizer { makeFlagRequest(isOnline: isOnline) } - func testStreamOnOpened() { - onOpened() - } - - func testStreamOnClosed() { - onClosed() - } - func testStreamOnMessage(event: String, messageEvent: MessageEvent) { onMessage(eventType: event, messageEvent: messageEvent) } - - func testStreamOnError(error: Error) { - onError(error: error) - } } #endif diff --git a/LaunchDarkly/LaunchDarkly/Util.swift b/LaunchDarkly/LaunchDarkly/Util.swift new file mode 100644 index 00000000..7ecf2a2b --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Util.swift @@ -0,0 +1,13 @@ +import CommonCrypto +import Foundation + +class Util { + class func sha256base64(_ str: String) -> String { + let data = Data(str.utf8) + var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &digest) + } + return Data(digest).base64EncodedString() + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift b/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift deleted file mode 100644 index c53ed123..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift +++ /dev/null @@ -1,171 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DictionarySpec: QuickSpec { - public override func spec() { - symmetricDifferenceSpec() - withNullValuesRemovedSpec() - } - - private func symmetricDifferenceSpec() { - describe("symmetric difference") { - var dictionary: [String: Any]! - var otherDictionary: [String: Any]! - beforeEach { - dictionary = [String: Any].stub() - otherDictionary = [String: Any].stub() - } - context("when dictionaries are equal") { - it("returns an empty array") { - expect(dictionary.symmetricDifference(otherDictionary)) == [] - } - } - context("when other is empty") { - it("returns all keys in subject") { - otherDictionary = [:] - expect(dictionary.symmetricDifference(otherDictionary)) == dictionary.keys.sorted() - } - } - context("when subject is empty") { - it("returns all keys in other") { - dictionary = [:] - expect(dictionary.symmetricDifference(otherDictionary)) == otherDictionary.keys.sorted() - } - } - context("when subject has an added key") { - it("returns the different key") { - let addedKey = "addedKey" - dictionary[addedKey] = true - expect(dictionary.symmetricDifference(otherDictionary)) == [addedKey] - } - } - context("when other has an added key") { - it("returns the different key") { - let addedKey = "addedKey" - otherDictionary[addedKey] = true - expect(dictionary.symmetricDifference(otherDictionary)) == [addedKey] - } - } - context("when other has a different key") { - it("returns the different keys") { - let addedKeyA = "addedKeyA" - let addedKeyB = "addedKeyB" - otherDictionary[addedKeyA] = true - dictionary[addedKeyB] = true - expect(dictionary.symmetricDifference(otherDictionary)) == [addedKeyA, addedKeyB] - } - } - context("when other has a different bool value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.bool - otherDictionary[differingKey] = !DarklyServiceMock.FlagValues.bool - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different int value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.int - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.int + 1 - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different double value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.double - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.double - 1.0 - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different string value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.string - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.string + " some new text" - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different array value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.array - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.array + [4] - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different dictionary value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.dictionary - var differingDictionary = DarklyServiceMock.FlagValues.dictionary - differingDictionary["sub-flag-a"] = !(differingDictionary["sub-flag-a"] as! Bool) - otherDictionary[differingKey] = differingDictionary - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - } - } - - private func withNullValuesRemovedSpec() { - describe("withNullValuesRemoved") { - it("when no null values exist") { - let dictionary = Dictionary.stub() - let resultingDictionary = dictionary.withNullValuesRemoved - expect(dictionary.keys) == resultingDictionary.keys - } - context("when null values exist") { - it("in the top level") { - var dictionary = Dictionary.stub() - dictionary["null-key"] = NSNull() - let resultingDictionary = dictionary.withNullValuesRemoved - expect(resultingDictionary.keys) == Dictionary.stub().keys - } - it("in the second level") { - var dictionary = Dictionary.stub() - var subDict = Dictionary.Values.dictionary - subDict["null-key"] = NSNull() - dictionary[Dictionary.Keys.dictionary] = subDict - let resultingDictionary = dictionary.withNullValuesRemoved - expect((resultingDictionary[Dictionary.Keys.dictionary] as! [String: Any]).keys) == Dictionary.Values.dictionary.keys - } - } - } - } -} - -fileprivate extension Dictionary where Key == String, Value == Any { - struct Keys { - static let bool: String = "bool-key" - static let int: String = "int-key" - static let double: String = "double-key" - static let string: String = "string-key" - static let array: String = "array-key" - static let dictionary: String = "dictionary-key" - static let null: String = "null-key" - } - - struct Values { - static let bool: Bool = true - static let int: Int = 7 - static let double: Double = 3.14159 - static let string: String = "string value" - static let array: [Int] = [1, 2, 3] - static let dictionary: [String: Any] = ["sub-flag-a": false, "sub-flag-b": 3, "sub-flag-c": 2.71828] - static let null: NSNull = NSNull() - } - - static func stub() -> [String: Any] { - [Keys.bool: Values.bool, - Keys.int: Values.int, - Keys.double: Values.double, - Keys.string: Values.string, - Keys.array: Values.array, - Keys.dictionary: Values.dictionary] - } -} - -extension Dictionary where Key == String, Value == Any { - func appendNull() -> [String: Any] { - var dictWithNull = self - dictWithNull[Keys.null] = Values.null - return dictWithNull - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 5c41b400..5cdcf111 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -9,9 +9,6 @@ final class LDClientSpec: QuickSpec { fileprivate static let alternateMockUrl = URL(string: "https://dummy.alternate.com")! fileprivate static let alternateMockMobileKey = "alternateMockMobileKey" - fileprivate static let newFlagKey = "LDClientSpec.newFlagKey" - fileprivate static let newFlagValue = "LDClientSpec.newFlagValue" - fileprivate static let updateThreshold: TimeInterval = 0.05 } @@ -20,8 +17,8 @@ final class LDClientSpec: QuickSpec { static let int = 5 static let double = 2.71828 static let string = "default string value" - static let array = [-1, -2] - static let dictionary: [String: Any] = ["sub-flag-x": true, "sub-flag-y": 1, "sub-flag-z": 42.42] + static let array: LDValue = [-1, -2] + static let dictionary: LDValue = ["sub-flag-x": true, "sub-flag-y": 1, "sub-flag-z": 42.42] } class TestContext { @@ -36,9 +33,6 @@ final class LDClientSpec: QuickSpec { var featureFlagCachingMock: FeatureFlagCachingMock! { subject.flagCache as? FeatureFlagCachingMock } - var cacheConvertingMock: CacheConvertingMock! { - subject.cacheConverter as? CacheConvertingMock - } var flagStoreMock: FlagMaintainingMock! { subject.flagStore as? FlagMaintainingMock } @@ -87,10 +81,13 @@ final class LDClientSpec: QuickSpec { } serviceFactoryMock.makeFlagChangeNotifierReturnValue = FlagChangeNotifier() - let flagCache = serviceFactoryMock.makeFeatureFlagCacheReturnValue - flagCache.retrieveFeatureFlagsCallback = { - let received = flagCache.retrieveFeatureFlagsReceivedArguments! - flagCache.retrieveFeatureFlagsReturnValue = self.cachedFlags[received.mobileKey]?[received.userKey] + serviceFactoryMock.makeFeatureFlagCacheCallback = { + let mobileKey = self.serviceFactoryMock.makeFeatureFlagCacheReceivedParameters!.mobileKey + let mockCache = FeatureFlagCachingMock() + mockCache.retrieveFeatureFlagsCallback = { + mockCache.retrieveFeatureFlagsReturnValue = self.cachedFlags[mobileKey]?[mockCache.retrieveFeatureFlagsReceivedUserKey!] + } + self.serviceFactoryMock.makeFeatureFlagCacheReturnValue = mockCache } config = newConfig ?? LDConfig.stub(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: serviceFactoryMock.makeEnvironmentReporterReturnValue) @@ -236,17 +233,16 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } it("starts in foreground") { expect(testContext.subject.runMode) == .foreground @@ -280,17 +276,16 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } it("starts in foreground") { expect(testContext.subject.runMode) == .foreground @@ -323,17 +318,16 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 // called on init and subsequent identify - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 2 // both start and internalIdentify expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 // Both start and internalIdentify - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } context("without setting user") { @@ -357,47 +351,45 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.subject.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.subject.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.subject.user } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.subject.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } } it("when called with cached flags for the user and environment") { - let testContext = TestContext().withCached(flags: FlagMaintainingMock.stubFlags()) + let cachedFlags = ["test-flag": FeatureFlag(flagKey: "test-flag")] + let testContext = TestContext().withCached(flags: cachedFlags) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags.flagCollection) == FlagMaintainingMock.stubFlags() + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == cachedFlags - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } it("when called without cached flags for the user") { let testContext = TestContext() withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key expect(testContext.flagStoreMock.replaceStoreCallCount) == 0 - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } @@ -467,30 +459,26 @@ final class LDClientSpec: QuickSpec { // Test that already timed out completion is not called when sync completes completed = false - testContext.onSyncComplete?(.success([:], nil)) - testContext.onSyncComplete?(.success([:], .ping)) - testContext.onSyncComplete?(.success([:], .put)) + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection([:]))) Thread.sleep(forTimeInterval: 1.0) expect(completed) == false } } } } - for eventType in [nil, FlagUpdateType.ping, FlagUpdateType.put] { - context("after receiving flags as " + (eventType?.rawValue ?? "poll")) { - it("does complete without timeout") { - testContext.start(completion: startCompletion) - testContext.onSyncComplete?(.success([:], eventType)) - expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) - } - it("does complete with timeout") { - waitUntil(timeout: .seconds(3)) { done in - testContext.start(timeOut: 5.0, timeOutCompletion: startTimeoutCompletion(done)) - testContext.onSyncComplete?(.success([:], eventType)) - } - expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) - expect(didTimeOut) == false + context("after receiving flags") { + it("does complete without timeout") { + testContext.start(completion: startCompletion) + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection([:]))) + expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + } + it("does complete with timeout") { + waitUntil(timeout: .seconds(3)) { done in + testContext.start(timeOut: 5.0, timeOutCompletion: startTimeoutCompletion(done)) + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection([:]))) } + expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + expect(didTimeOut) == false } } } @@ -513,7 +501,6 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline } it("saves the config") { - expect(testContext.subject.config) == testContext.config expect(testContext.subject.service.config) == testContext.config expect(testContext.makeFlagSynchronizerStreamingMode) == os.backgroundStreamingMode expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) @@ -530,18 +517,12 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } } } } @@ -559,7 +540,6 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.eventReporter.isOnline) == true } it("saves the config") { - expect(testContext.subject.config) == testContext.config expect(testContext.subject.service.config) == testContext.config expect(testContext.makeFlagSynchronizerStreamingMode) == LDStreamingMode.polling expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) @@ -576,18 +556,12 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } } } } @@ -600,8 +574,7 @@ final class LDClientSpec: QuickSpec { let testContext = TestContext(startOnline: true) testContext.start() testContext.featureFlagCachingMock.reset() - testContext.cacheConvertingMock.reset() - + let newUser = LDUser.stub() testContext.subject.internalIdentify(newUser: newUser) @@ -615,20 +588,14 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.flagSynchronizer.isOnline) == true expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == newUser.key expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) - - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } it("when the client is offline") { let testContext = TestContext() testContext.start() testContext.featureFlagCachingMock.reset() - testContext.cacheConvertingMock.reset() let newUser = LDUser.stub() testContext.subject.internalIdentify(newUser: newUser) @@ -643,14 +610,9 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.flagSynchronizer.isOnline) == false expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == newUser.key expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) - - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } it("when the new user has cached feature flags") { let stubFlags = FlagMaintainingMock.stubFlags() @@ -658,17 +620,12 @@ final class LDClientSpec: QuickSpec { let testContext = TestContext().withCached(userKey: newUser.key, flags: stubFlags) testContext.start() testContext.featureFlagCachingMock.reset() - testContext.cacheConvertingMock.reset() testContext.subject.internalIdentify(newUser: newUser) expect(testContext.subject.user) == newUser expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags.flagCollection) == stubFlags - - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == stubFlags } } } @@ -807,22 +764,19 @@ final class LDClientSpec: QuickSpec { } context("flag store contains the requested value") { beforeEach { - waitUntil { done in - testContext.flagStoreMock.replaceStore(newFlags: FlagMaintainingMock.stubFlags(), completion: done) - } + testContext.flagStoreMock.replaceStore(newFlags: FeatureFlagCollection(FlagMaintainingMock.stubFlags())) } context("non-Optional default value") { it("returns the flag value") { - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DarklyServiceMock.FlagValues.bool - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DarklyServiceMock.FlagValues.int - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DarklyServiceMock.FlagValues.double - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DarklyServiceMock.FlagValues.string - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array) == DarklyServiceMock.FlagValues.array).to(beTrue()) - expect(AnyComparer.isEqual(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary), - to: DarklyServiceMock.FlagValues.dictionary)).to(beTrue()) + expect(testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DarklyServiceMock.FlagValues.bool + expect(testContext.subject.intVariation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DarklyServiceMock.FlagValues.int + expect(testContext.subject.doubleVariation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DarklyServiceMock.FlagValues.double + expect(testContext.subject.stringVariation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DarklyServiceMock.FlagValues.string + expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array)) == LDValue.fromAny(DarklyServiceMock.FlagValues.array) + expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary)) == LDValue.fromAny(DarklyServiceMock.FlagValues.dictionary) } it("records a flag evaluation event") { - _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) + _ = testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == LDValue.fromAny(DarklyServiceMock.FlagValues.bool) @@ -835,16 +789,15 @@ final class LDClientSpec: QuickSpec { context("flag store does not contain the requested value") { context("non-Optional default value") { it("returns the default value") { - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DefaultFlagValues.bool - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DefaultFlagValues.int - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DefaultFlagValues.double - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DefaultFlagValues.string - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array) == DefaultFlagValues.array).to(beTrue()) - expect(AnyComparer.isEqual(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary), - to: DefaultFlagValues.dictionary)).to(beTrue()) + expect(testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DefaultFlagValues.bool + expect(testContext.subject.intVariation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DefaultFlagValues.int + expect(testContext.subject.doubleVariation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DefaultFlagValues.double + expect(testContext.subject.stringVariation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DefaultFlagValues.string + expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array)) == DefaultFlagValues.array + expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary)) == DefaultFlagValues.dictionary } it("records a flag evaluation event") { - _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) + _ = testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == LDValue.fromAny(DefaultFlagValues.bool) @@ -930,14 +883,8 @@ final class LDClientSpec: QuickSpec { } private func onSyncCompleteSuccessSpec() { - it("polling") { - self.onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .polling) - } - it("streaming ping") { - self.onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .streaming, eventType: .ping) - } - it("streaming put") { - self.onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .streaming, eventType: .put) + it("flag collection") { + self.onSyncCompleteSuccessReplacingFlagsSpec() } it("streaming patch") { self.onSyncCompleteStreamingPatchSpec() @@ -947,34 +894,30 @@ final class LDClientSpec: QuickSpec { } } - private func onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: LDStreamingMode, eventType: FlagUpdateType? = nil) { + private func onSyncCompleteSuccessReplacingFlagsSpec() { let testContext = TestContext(startOnline: true) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - var newFlags = FlagMaintainingMock.stubFlags() - newFlags[Constants.newFlagKey] = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.string, useAlternateValue: true) - + let newFlags = ["flag1": FeatureFlag(flagKey: "flag1")] var updateDate: Date! waitUntil { done in testContext.changeNotifierMock.notifyObserversCallback = done updateDate = Date() - testContext.onSyncComplete?(.success(newFlags, eventType)) + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection(newFlags))) } expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(AnyComparer.isEqual(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags, to: newFlags)).to(beTrue()) + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == newFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags - expect(AnyComparer.isEqual(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags, to: testContext.cachedFlags)).to(beTrue()) + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags) == [:] } func onSyncCompleteStreamingPatchSpec() { @@ -982,27 +925,22 @@ final class LDClientSpec: QuickSpec { let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - let flagUpdateDictionary = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, - value: DarklyServiceMock.FlagValues.int + 1, - variation: DarklyServiceMock.Constants.variation + 1, - version: DarklyServiceMock.Constants.version + 1) + let updateFlag = FeatureFlag(flagKey: "abc") var updateDate: Date! waitUntil { done in testContext.changeNotifierMock.notifyObserversCallback = done updateDate = Date() - testContext.onSyncComplete?(.success(flagUpdateDictionary, .patch)) + testContext.onSyncComplete?(.patch(updateFlag)) } expect(testContext.flagStoreMock.updateStoreCallCount) == 1 - expect(AnyComparer.isEqual(testContext.flagStoreMock.updateStoreReceivedArguments?.updateDictionary, to: flagUpdateDictionary)).to(beTrue()) + expect(testContext.flagStoreMock.updateStoreReceivedUpdatedFlag) == updateFlag expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags @@ -1014,24 +952,22 @@ final class LDClientSpec: QuickSpec { let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - let flagUpdateDictionary = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) + let deleteResponse = DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) var updateDate: Date! waitUntil { done in testContext.changeNotifierMock.notifyObserversCallback = done updateDate = Date() - testContext.onSyncComplete?(.success(flagUpdateDictionary, .delete)) + testContext.onSyncComplete?(.delete(deleteResponse)) } expect(testContext.flagStoreMock.deleteFlagCallCount) == 1 - expect(AnyComparer.isEqual(testContext.flagStoreMock.deleteFlagReceivedArguments?.deleteDictionary, to: flagUpdateDictionary)).to(beTrue()) + expect(testContext.flagStoreMock.deleteFlagReceivedDeleteResponse) == deleteResponse expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags @@ -1365,7 +1301,7 @@ final class LDClientSpec: QuickSpec { it("returns all non-null flag values from store") { let testContext = TestContext().withCached(flags: stubFlags) testContext.start() - expect(AnyComparer.isEqual(testContext.subject.allFlags, to: stubFlags.compactMapValues { $0.value })).to(beTrue()) + expect(testContext.subject.allFlags) == stubFlags.compactMapValues { $0.value } } it("returns nil when client is closed") { let testContext = TestContext().withCached(flags: stubFlags) @@ -1403,8 +1339,8 @@ final class LDClientSpec: QuickSpec { it("when flag doesn't exist") { let testContext = TestContext() testContext.start() - let detail = testContext.subject.variationDetail(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool).reason - if let errorKind = detail?["errorKind"] as? String { + let detail = testContext.subject.boolVariationDetail(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool).reason + if let errorKind = detail?["errorKind"] { expect(errorKind) == "FLAG_NOT_FOUND" } } @@ -1431,17 +1367,15 @@ final class LDClientSpec: QuickSpec { testContext.subject.close() expect(testContext.subject.isInitialized) == false } - for eventType in [nil, FlagUpdateType.ping, FlagUpdateType.put] { - it("when client was started and after receiving flags as " + (eventType?.rawValue ?? "poll")) { - let testContext = TestContext(startOnline: true) - testContext.start() - testContext.onSyncComplete?(.success([:], eventType)) + it("when client was started and after receiving flags") { + let testContext = TestContext(startOnline: true) + testContext.start() + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection([:]))) - expect(testContext.subject.isInitialized).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + expect(testContext.subject.isInitialized).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) - testContext.subject.close() - expect(testContext.subject.isInitialized) == false - } + testContext.subject.close() + expect(testContext.subject.isInitialized) == false } } } @@ -1450,7 +1384,7 @@ final class LDClientSpec: QuickSpec { extension FeatureFlagCachingMock { func reset() { retrieveFeatureFlagsCallCount = 0 - retrieveFeatureFlagsReceivedArguments = nil + retrieveFeatureFlagsReceivedUserKey = nil retrieveFeatureFlagsReturnValue = nil storeFeatureFlagsCallCount = 0 storeFeatureFlagsReceivedArguments = nil @@ -1464,10 +1398,3 @@ extension OperatingSystem { } private class ErrorMock: Error { } - -extension CacheConvertingMock { - func reset() { - convertCacheDataCallCount = 0 - convertCacheDataReceivedArguments = nil - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index 405efe0d..e41a117a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -3,35 +3,29 @@ import LDSwiftEventSource @testable import LaunchDarkly final class ClientServiceMockFactory: ClientServiceCreating { - func makeKeyedValueCache() -> KeyedValueCaching { - KeyedValueCachingMock() + var makeKeyedValueCacheReturnValue = KeyedValueCachingMock() + var makeKeyedValueCacheCallCount = 0 + var makeKeyedValueCacheReceivedCacheKey: String? = nil + func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching { + makeKeyedValueCacheCallCount += 1 + makeKeyedValueCacheReceivedCacheKey = cacheKey + return makeKeyedValueCacheReturnValue } var makeFeatureFlagCacheReturnValue = FeatureFlagCachingMock() + var makeFeatureFlagCacheCallback: (() -> Void)? var makeFeatureFlagCacheCallCount = 0 - func makeFeatureFlagCache(maxCachedUsers: Int = 5) -> FeatureFlagCaching { + var makeFeatureFlagCacheReceivedParameters: (mobileKey: MobileKey, maxCachedUsers: Int)? = nil + func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedUsers: Int = 5) -> FeatureFlagCaching { makeFeatureFlagCacheCallCount += 1 + makeFeatureFlagCacheReceivedParameters = (mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) + makeFeatureFlagCacheCallback?() return makeFeatureFlagCacheReturnValue } - func makeCacheConverter(maxCachedUsers: Int = 5) -> CacheConverting { - CacheConvertingMock() - } - - var makeDeprecatedCacheModelReturnValue: DeprecatedCacheMock? - var makeDeprecatedCacheModelReturnedValues = [DeprecatedCacheModel: DeprecatedCacheMock]() - var makeDeprecatedCacheModelCallCount = 0 - var makeDeprecatedCacheModelReceivedModels = [DeprecatedCacheModel]() - func makeDeprecatedCacheModel(_ model: DeprecatedCacheModel) -> DeprecatedCache { - makeDeprecatedCacheModelCallCount += 1 - makeDeprecatedCacheModelReceivedModels.append(model) - var returnedCacheMock = makeDeprecatedCacheModelReturnValue - if returnedCacheMock == nil { - returnedCacheMock = DeprecatedCacheMock() - returnedCacheMock?.model = model - } - makeDeprecatedCacheModelReturnedValues[model] = returnedCacheMock! - return returnedCacheMock! + var makeCacheConverterReturnValue = CacheConvertingMock() + func makeCacheConverter() -> CacheConverting { + return makeCacheConverterReturnValue } func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider { diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 38d5ebb0..1c320d72 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -17,12 +17,9 @@ final class DarklyServiceMock: DarklyServiceProvider { static let null = "null-flag" static let unknown = "unknown-flag" - static var knownFlags: [LDFlagKey] { // known means the SDK has the feature flag value + static var knownFlags: [LDFlagKey] { [bool, int, double, string, array, dictionary, null] } - static var flagsWithAnAlternateValue: [LDFlagKey] { - [bool, int, double, string, array, dictionary] - } } struct FlagValues { @@ -46,26 +43,6 @@ final class DarklyServiceMock: DarklyServiceProvider { default: return nil } } - - static func alternateValue(from flagKey: LDFlagKey) -> Any? { - alternate(value(from: flagKey)) - } - - static func alternate(_ value: T) -> T { - switch value { - case let value as Bool: return !value as! T - case let value as Int: return value + 1 as! T - case let value as Double: return value + 1.0 as! T - case let value as String: return value + "-alternate" as! T - case var value as [Any]: - value.append(4) - return value as! T // Not sure why, but this crashes if you combine append the value into the return - case var value as [String: Any]: - value["new-flag"] = "new-value" - return value as! T - default: return value - } - } } struct Constants { @@ -80,101 +57,39 @@ final class DarklyServiceMock: DarklyServiceProvider { static let mockEventsUrl = URL(string: "https://dummy.events.com")! static let mockStreamUrl = URL(string: "https://dummy.stream.com")! - static let stubNameFlag = "Flag Request Stub" - static let stubNameStream = "Stream Connect Stub" - static let stubNameEvent = "Event Report Stub" - static let stubNameDiagnostic = "Diagnostic Report Stub" - static let variation = 2 static let version = 4 static let flagVersion = 3 static let trackEvents = true static let debugEventsUntilDate = Date().addingTimeInterval(30.0) - static let reason = Optional(["kind": "OFF"]) + static let reason: [String: LDValue] = ["kind": "OFF"] - static func stubFeatureFlags(includeNullValue: Bool = true, - includeVariations: Bool = true, - includeVersions: Bool = true, - includeFlagVersions: Bool = true, - alternateVariationNumber: Bool = true, - bumpFlagVersions: Bool = false, - alternateValuesForKeys alternateValueKeys: [LDFlagKey] = [], - trackEvents: Bool = true, - debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0)) -> [LDFlagKey: FeatureFlag] { - - let flagKeys = includeNullValue ? FlagKeys.knownFlags : FlagKeys.flagsWithAnAlternateValue + static func stubFeatureFlags(debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0)) -> [LDFlagKey: FeatureFlag] { + let flagKeys = FlagKeys.knownFlags let featureFlagTuples = flagKeys.map { flagKey in - (flagKey, stubFeatureFlag(for: flagKey, - includeVariation: includeVariations, - includeVersion: includeVersions, - includeFlagVersion: includeFlagVersions, - useAlternateValue: useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), - useAlternateVersion: bumpFlagVersions && useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), - useAlternateFlagVersion: bumpFlagVersions && useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), - useAlternateVariationNumber: alternateVariationNumber, - trackEvents: trackEvents, - debugEventsUntilDate: debugEventsUntilDate)) + (flagKey, stubFeatureFlag(for: flagKey, debugEventsUntilDate: debugEventsUntilDate)) } return Dictionary(uniqueKeysWithValues: featureFlagTuples) } - private static func useAlternateValue(for flagKey: LDFlagKey, alternateValueKeys: [LDFlagKey]) -> Bool { - alternateValueKeys.contains(flagKey) - } - - private static func value(for flagKey: LDFlagKey, useAlternateValue: Bool) -> Any? { - useAlternateValue ? FlagValues.alternateValue(from: flagKey) : FlagValues.value(from: flagKey) - } - - private static func variation(for flagKey: LDFlagKey, includeVariation: Bool, useAlternateValue: Bool) -> Int? { - guard includeVariation - else { return nil } - return useAlternateValue ? variation + 1 : variation - } - - private static func variation(for flagKey: LDFlagKey, includeVariation: Bool) -> Int? { - guard includeVariation - else { return nil } - return variation - } - - private static func version(for flagKey: LDFlagKey, includeVersion: Bool, useAlternateVersion: Bool) -> Int? { - guard includeVersion - else { return nil } + private static func version(for flagKey: LDFlagKey, useAlternateVersion: Bool) -> Int? { return useAlternateVersion ? version + 1 : version } - private static func flagVersion(for flagKey: LDFlagKey, includeFlagVersion: Bool, useAlternateFlagVersion: Bool) -> Int? { - guard includeFlagVersion - else { return nil } - return useAlternateFlagVersion ? flagVersion + 1 : flagVersion - } - private static func reason(includeEvaluationReason: Bool) -> [String: Any]? { - includeEvaluationReason ? reason : nil - } - static func stubFeatureFlag(for flagKey: LDFlagKey, - includeVariation: Bool = true, - includeVersion: Bool = true, - includeFlagVersion: Bool = true, - useAlternateValue: Bool = false, useAlternateVersion: Bool = false, - useAlternateFlagVersion: Bool = false, - useAlternateVariationNumber: Bool = true, trackEvents: Bool = true, - debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0), - includeEvaluationReason: Bool = false, - includeTrackReason: Bool = false) -> FeatureFlag { + debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0)) -> FeatureFlag { FeatureFlag(flagKey: flagKey, - value: value(for: flagKey, useAlternateValue: useAlternateValue), - variation: useAlternateVariationNumber ? variation(for: flagKey, includeVariation: includeVariation, useAlternateValue: useAlternateValue) : variation(for: flagKey, includeVariation: includeVariation), - version: version(for: flagKey, includeVersion: includeVersion, useAlternateVersion: useAlternateValue || useAlternateVersion), - flagVersion: flagVersion(for: flagKey, includeFlagVersion: includeFlagVersion, useAlternateFlagVersion: useAlternateValue || useAlternateFlagVersion), - trackEvents: trackEvents, - debugEventsUntilDate: debugEventsUntilDate, - reason: reason(includeEvaluationReason: includeEvaluationReason), - trackReason: includeTrackReason) + value: LDValue.fromAny(FlagValues.value(from: flagKey)), + variation: variation, + version: version(for: flagKey, useAlternateVersion: useAlternateVersion), + flagVersion: flagVersion, + trackEvents: trackEvents, + debugEventsUntilDate: debugEventsUntilDate, + reason: nil, + trackReason: false) } } @@ -263,7 +178,7 @@ extension DarklyServiceMock { flagResponseEtag: String? = nil, onActivation activate: ((URLRequest) -> Void)? = nil) { let stubbedFeatureFlags = featureFlags ?? Constants.stubFeatureFlags() - let responseData = statusCode == HTTPURLResponse.StatusCodes.ok ? stubbedFeatureFlags.dictionaryValue.jsonData! : Data() + let responseData = statusCode == HTTPURLResponse.StatusCodes.ok ? try! JSONEncoder().encode(stubbedFeatureFlags) : Data() let stubResponse: HTTPStubsResponseBlock = { _ in var headers: [String: String] = [:] if let flagResponseEtag = flagResponseEtag { @@ -283,8 +198,7 @@ extension DarklyServiceMock { func stubFlagResponse(statusCode: Int, badData: Bool = false, responseOnly: Bool = false, errorOnly: Bool = false, responseDate: Date? = nil) { let response = HTTPURLResponse(url: config.baseUrl, statusCode: statusCode, httpVersion: Constants.httpVersion, headerFields: HTTPURLResponse.dateHeader(from: responseDate)) if statusCode == HTTPURLResponse.StatusCodes.ok { - let flagData = try? JSONSerialization.data(withJSONObject: Constants.stubFeatureFlags(includeNullValue: false).dictionaryValue, - options: []) + let flagData = try? JSONEncoder().encode(Constants.stubFeatureFlags()) stubbedFlagResponse = (flagData, response, nil) if badData { stubbedFlagResponse = (Constants.errorData, response, nil) @@ -302,7 +216,7 @@ extension DarklyServiceMock { } func flagStubName(statusCode: Int, useReport: Bool) -> String { - "\(Constants.stubNameFlag) using method \(useReport ? URLRequest.HTTPMethods.report : URLRequest.HTTPMethods.get) with response status code \(statusCode)" + "Flag request stub using method \(useReport ? URLRequest.HTTPMethods.report : URLRequest.HTTPMethods.get) with response status code \(statusCode)" } // MARK: Publish Event @@ -321,7 +235,7 @@ extension DarklyServiceMock { } : { _ in HTTPStubsResponse(error: Constants.error) } - stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: Constants.stubNameEvent) { request, _, _ in + stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: "Event report stub") { request, _, _ in activate?(request) } } @@ -358,7 +272,7 @@ extension DarklyServiceMock { } : { _ in HTTPStubsResponse(error: Constants.error) } - stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: Constants.stubNameDiagnostic, onActivation: activate) + stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: "Diagnostic report stub", onActivation: activate) } // MARK: Stub diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift deleted file mode 100644 index 80934705..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift +++ /dev/null @@ -1,71 +0,0 @@ -import Foundation -@testable import LaunchDarkly - -// MARK: - DeprecatedCacheMock -final class DeprecatedCacheMock: DeprecatedCache { - - // MARK: model - var modelSetCount = 0 - var setModelCallback: (() -> Void)? - // This may need to be updated when new cache versions are introduced - var model: DeprecatedCacheModel = .version5 { - didSet { - modelSetCount += 1 - setModelCallback?() - } - } - - // MARK: cachedDataKey - var cachedDataKeySetCount = 0 - var setCachedDataKeyCallback: (() -> Void)? - var cachedDataKey: String = CacheConverter.CacheKeys.cachedDataKeyStub { - didSet { - cachedDataKeySetCount += 1 - setCachedDataKeyCallback?() - } - } - - // MARK: keyedValueCache - var keyedValueCacheSetCount = 0 - var setKeyedValueCacheCallback: (() -> Void)? - var keyedValueCache: KeyedValueCaching = KeyedValueCachingMock() { - didSet { - keyedValueCacheSetCount += 1 - setKeyedValueCacheCallback?() - } - } - - // MARK: retrieveFlags - var retrieveFlagsCallCount = 0 - var retrieveFlagsCallback: (() -> Void)? - var retrieveFlagsReceivedArguments: (userKey: UserKey, mobileKey: MobileKey)? - var retrieveFlagsReturnValue: (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?)! - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) { - retrieveFlagsCallCount += 1 - retrieveFlagsReceivedArguments = (userKey: userKey, mobileKey: mobileKey) - retrieveFlagsCallback?() - return retrieveFlagsReturnValue - } - - // MARK: userKeys - var userKeysCallCount = 0 - var userKeysCallback: (() -> Void)? - var userKeysReceivedArguments: (cachedUserData: [UserKey: [String: Any]], olderThan: Date)? - var userKeysReturnValue: [UserKey]! - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan: Date) -> [UserKey] { - userKeysCallCount += 1 - userKeysReceivedArguments = (cachedUserData: cachedUserData, olderThan: olderThan) - userKeysCallback?() - return userKeysReturnValue - } - - // MARK: removeData - var removeDataCallCount = 0 - var removeDataCallback: (() -> Void)? - var removeDataReceivedExpirationDate: Date? - func removeData(olderThan expirationDate: Date) { - removeDataCallCount += 1 - removeDataReceivedExpirationDate = expirationDate - removeDataCallback?() - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift index d9b40800..9db37b2b 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift @@ -2,11 +2,6 @@ import Foundation @testable import LaunchDarkly final class FlagMaintainingMock: FlagMaintaining { - struct Constants { - static let updateDictionaryExtraKey = "FlagMaintainingMock.UpdateDictionary.extraKey" - static let updateDictionaryExtraValue = "FlagMaintainingMock.UpdateDictionary.extraValue" - } - let innerStore: FlagStore init() { @@ -22,70 +17,39 @@ final class FlagMaintainingMock: FlagMaintaining { } var replaceStoreCallCount = 0 - var replaceStoreReceivedArguments: (newFlags: [LDFlagKey: Any], completion: CompletionClosure?)? - func replaceStore(newFlags: [LDFlagKey: Any], completion: CompletionClosure?) { + var replaceStoreReceivedNewFlags: FeatureFlagCollection? + func replaceStore(newFlags: FeatureFlagCollection) { replaceStoreCallCount += 1 - replaceStoreReceivedArguments = (newFlags: newFlags, completion: completion) - innerStore.replaceStore(newFlags: newFlags, completion: completion) + replaceStoreReceivedNewFlags = newFlags + innerStore.replaceStore(newFlags: newFlags) } var updateStoreCallCount = 0 - var updateStoreReceivedArguments: (updateDictionary: [String: Any], completion: CompletionClosure?)? - func updateStore(updateDictionary: [String: Any], completion: CompletionClosure?) { + var updateStoreReceivedUpdatedFlag: FeatureFlag? + func updateStore(updatedFlag: FeatureFlag) { updateStoreCallCount += 1 - updateStoreReceivedArguments = (updateDictionary: updateDictionary, completion: completion) - innerStore.updateStore(updateDictionary: updateDictionary, completion: completion) + updateStoreReceivedUpdatedFlag = updatedFlag + innerStore.updateStore(updatedFlag: updatedFlag) } var deleteFlagCallCount = 0 - var deleteFlagReceivedArguments: (deleteDictionary: [String: Any], completion: CompletionClosure?)? - func deleteFlag(deleteDictionary: [String: Any], completion: CompletionClosure?) { + var deleteFlagReceivedDeleteResponse: DeleteResponse? + func deleteFlag(deleteResponse: DeleteResponse) { deleteFlagCallCount += 1 - deleteFlagReceivedArguments = (deleteDictionary: deleteDictionary, completion: completion) - innerStore.deleteFlag(deleteDictionary: deleteDictionary, completion: completion) + deleteFlagReceivedDeleteResponse = deleteResponse + innerStore.deleteFlag(deleteResponse: deleteResponse) } func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? { innerStore.featureFlag(for: flagKey) } - static func stubPatchDictionary(key: LDFlagKey?, value: Any?, variation: Int?, version: Int?, includeExtraKey: Bool = false) -> [String: Any] { - var updateDictionary = [String: Any]() - if let key = key { - updateDictionary[FlagStore.Keys.flagKey] = key - } - if let value = value { - updateDictionary[FeatureFlag.CodingKeys.value.rawValue] = value - } - if let variation = variation { - updateDictionary[FeatureFlag.CodingKeys.variation.rawValue] = variation - } - if let version = version { - updateDictionary[FeatureFlag.CodingKeys.version.rawValue] = version - } - if includeExtraKey { - updateDictionary[Constants.updateDictionaryExtraKey] = Constants.updateDictionaryExtraValue - } - return updateDictionary - } - - static func stubDeleteDictionary(key: LDFlagKey?, version: Int?) -> [String: Any] { - var deleteDictionary = [String: Any]() - if let key = key { - deleteDictionary[FlagStore.Keys.flagKey] = key - } - if let version = version { - deleteDictionary[FeatureFlag.CodingKeys.version.rawValue] = version - } - return deleteDictionary - } - - static func stubFlags(includeNullValue: Bool = true, includeVersions: Bool = true) -> [String: FeatureFlag] { - var flags = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: includeNullValue, includeVersions: includeVersions) + static func stubFlags() -> [LDFlagKey: FeatureFlag] { + var flags = DarklyServiceMock.Constants.stubFeatureFlags() flags["userKey"] = FeatureFlag(flagKey: "userKey", - value: UUID().uuidString, + value: .string(UUID().uuidString), variation: DarklyServiceMock.Constants.variation, - version: includeVersions ? DarklyServiceMock.Constants.version : nil, + version: DarklyServiceMock.Constants.version, flagVersion: DarklyServiceMock.Constants.flagVersion, trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(30.0), diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift index 8cd5a488..0f08112a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift @@ -3,36 +3,12 @@ import LDSwiftEventSource @testable import LaunchDarkly extension EventHandler { - func send(event: FlagUpdateType, dict: [String: Any]) { - send(event: event, string: dict.jsonString!) - } - - func send(event: FlagUpdateType, string: String) { - onMessage(eventType: event.rawValue, messageEvent: MessageEvent(data: string)) + func send(event: String, string: String) { + onMessage(eventType: event, messageEvent: MessageEvent(data: string)) } func sendPing() { - onMessage(eventType: FlagUpdateType.ping.rawValue, messageEvent: MessageEvent(data: "")) - } - - func sendPut() { - let data = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, includeVariations: true, includeVersions: true) - .dictionaryValue - send(event: .put, dict: data) - } - - func sendPatch() { - let data = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, - value: DarklyServiceMock.FlagValues.int + 1, - variation: DarklyServiceMock.Constants.variation + 1, - version: DarklyServiceMock.Constants.version + 1) - send(event: .patch, dict: data) - } - - func sendDelete() { - let data = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, - version: DarklyServiceMock.Constants.version + 1) - send(event: .delete, dict: data) + onMessage(eventType: "ping", messageEvent: MessageEvent(data: "")) } func sendUnauthorizedError() { diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift deleted file mode 100644 index 134b6464..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class CacheableEnvironmentFlagsSpec: QuickSpec { - - private struct TestValues { - static let userKey = UUID().uuidString - static let mobKey = UUID().uuidString - static let flags = FlagMaintainingMock.stubFlags() - - static func defaultEnvironment(withFlags: [String: FeatureFlag] = flags) -> CacheableEnvironmentFlags { - CacheableEnvironmentFlags(userKey: userKey, mobileKey: mobKey, featureFlags: withFlags) - } - } - - override func spec() { - initWithElementsSpec() - initWithDictionarySpec() - dictionaryValueSpec() - } - - private func initWithElementsSpec() { - describe("initWithElements") { - it("creates a CacheableEnvironmentFlags with the elements") { - let environmentFlags = TestValues.defaultEnvironment() - expect(environmentFlags.userKey) == TestValues.userKey - expect(environmentFlags.mobileKey) == TestValues.mobKey - expect(environmentFlags.featureFlags) == TestValues.flags - } - } - } - - private func initWithDictionarySpec() { - let defaultDictionary = TestValues.defaultEnvironment().dictionaryValue - describe("initWithDictionary") { - context("creates a new CacheableEnvironmentFlags") { - it("with all elements") { - let other = CacheableEnvironmentFlags(dictionary: defaultDictionary) - expect(other?.userKey) == TestValues.userKey - expect(other?.mobileKey) == TestValues.mobKey - expect(other?.featureFlags) == TestValues.flags - } - it("with extra elements") { - var testDictionary = defaultDictionary - testDictionary["extraKey"] = "abc" - let other = CacheableEnvironmentFlags(dictionary: testDictionary) - expect(other?.userKey) == TestValues.userKey - expect(other?.mobileKey) == TestValues.mobKey - expect(other?.featureFlags) == TestValues.flags - } - } - for key in CacheableEnvironmentFlags.CodingKeys.allCases { - it("returns nil when \(key.rawValue) missing or invalid") { - var testDictionary = defaultDictionary - testDictionary[key.rawValue] = 3 // Invalid value for all fields - expect(CacheableEnvironmentFlags(dictionary: testDictionary)).to(beNil()) - testDictionary.removeValue(forKey: key.rawValue) - expect(CacheableEnvironmentFlags(dictionary: testDictionary)).to(beNil()) - } - } - } - } - - private func dictionaryValueSpec() { - describe("dictionaryValue") { - context("creates a dictionary with the elements") { - it("with null feature flag value") { - let cacheDictionary = TestValues.defaultEnvironment().dictionaryValue - expect(cacheDictionary["userKey"] as? String) == TestValues.userKey - expect(cacheDictionary["mobileKey"] as? String) == TestValues.mobKey - expect((cacheDictionary["featureFlags"] as? [LDFlagKey: Any])?.flagCollection) == TestValues.flags - } - it("without feature flags") { - let cacheDictionary = TestValues.defaultEnvironment(withFlags: [:]).dictionaryValue - expect(cacheDictionary["userKey"] as? String) == TestValues.userKey - expect(cacheDictionary["mobileKey"] as? String) == TestValues.mobKey - expect(AnyComparer.isEqual(cacheDictionary["featureFlags"], to: [:])) == true - } - // Ultimately, this is not desired behavior, but currently we are unable to store internal nil/null values - // inside of the `KeyedValueCache`. When we update our cache format, we can encode all data to get around this. - it("removes internal nulls") { - let flags = ["flag1": FeatureFlag(flagKey: "flag1", value: ["abc": [1, nil, 3]]), - "flag2": FeatureFlag(flagKey: "flag2", value: [1, ["abc": nil], 3])] - let cacheable = CacheableEnvironmentFlags(userKey: "user", mobileKey: "mobile", featureFlags: flags) - let dictionaryFlags = cacheable.dictionaryValue["featureFlags"] as! [String: [String: Any]] - let flag1 = FeatureFlag(dictionary: dictionaryFlags["flag1"]) - let flag2 = FeatureFlag(dictionary: dictionaryFlags["flag2"]) - // Manually comparing fields, `==` on `FeatureFlag` does not compare values. - expect(flag1?.flagKey) == "flag1" - expect(AnyComparer.isEqual(flag1?.value, to: ["abc": [1, 3]])).to(beTrue()) - expect(flag2?.flagKey) == "flag2" - expect(AnyComparer.isEqual(flag2?.value, to: [1, [:], 3])).to(beTrue()) - } - } - } - } -} - -extension CacheableEnvironmentFlags: Equatable { - public static func == (lhs: CacheableEnvironmentFlags, rhs: CacheableEnvironmentFlags) -> Bool { - lhs.userKey == rhs.userKey - && lhs.mobileKey == rhs.mobileKey - && lhs.featureFlags == rhs.featureFlags - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift deleted file mode 100644 index 066035b8..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift +++ /dev/null @@ -1,177 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class CacheableUserEnvironmentFlagsSpec: QuickSpec { - - private struct TestValues { - static let userKey = UUID().uuidString - static let environments = CacheableEnvironmentFlags.stubCollection(userKey: TestValues.userKey, environmentCount: 3) - static let updated = Date().stringEquivalentDate - - static func defaultEnvironment(withEnvironments: [String: CacheableEnvironmentFlags] = environments) -> CacheableUserEnvironmentFlags { - CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: withEnvironments, lastUpdated: updated) - } - } - - override func spec() { - initWithElementsSpec() - initWithDictionarySpec() - initWithObjectSpec() - dictionaryValueSpec() - } - - private func initWithElementsSpec() { - describe("init") { - it("with no environments") { - let userEnvironmentFlags = TestValues.defaultEnvironment(withEnvironments: [:]) - expect(userEnvironmentFlags.userKey) == TestValues.userKey - expect(userEnvironmentFlags.environmentFlags) == [:] - expect(userEnvironmentFlags.lastUpdated) == TestValues.updated - } - it("with environments") { - let userEnvironmentFlags = TestValues.defaultEnvironment() - expect(userEnvironmentFlags.userKey) == TestValues.userKey - expect(userEnvironmentFlags.environmentFlags) == TestValues.environments - expect(userEnvironmentFlags.lastUpdated) == TestValues.updated - } - } - } - - private func initWithDictionarySpec() { - let defaultDictionary = TestValues.defaultEnvironment().dictionaryValue - describe("initWithDictionary") { - context("creates a matching cacheableUserEnvironments") { - it("with all elements") { - let userEnv = CacheableUserEnvironmentFlags(dictionary: defaultDictionary) - expect(userEnv?.userKey) == TestValues.userKey - expect(userEnv?.environmentFlags) == TestValues.environments - expect(userEnv?.lastUpdated) == TestValues.updated - } - it("with extra dictionary items") { - var testDictionary = defaultDictionary - testDictionary["extraKey"] = "abc" - let userEnv = CacheableUserEnvironmentFlags(dictionary: testDictionary) - expect(userEnv?.userKey) == TestValues.userKey - expect(userEnv?.environmentFlags) == TestValues.environments - expect(userEnv?.lastUpdated) == TestValues.updated - } - } - for key in CacheableUserEnvironmentFlags.CodingKeys.allCases { - it("returns nil when \(key.rawValue) missing or invalid") { - var testDictionary = defaultDictionary - testDictionary[key.rawValue] = 3 // Invalid value for all fields - expect(CacheableUserEnvironmentFlags(dictionary: testDictionary)).to(beNil()) - testDictionary.removeValue(forKey: key.rawValue) - expect(CacheableUserEnvironmentFlags(dictionary: testDictionary)).to(beNil()) - } - } - } - } - - private func initWithObjectSpec() { - describe("initWithObject") { - it("inits when object is a valid dictionary") { - let userEnv = CacheableUserEnvironmentFlags(object: TestValues.defaultEnvironment().dictionaryValue) - expect(userEnv?.userKey) == TestValues.userKey - expect(userEnv?.environmentFlags) == TestValues.environments - expect(userEnv?.lastUpdated) == TestValues.updated - } - it("return nil when object is not a valid dictionary") { - expect(CacheableUserEnvironmentFlags(object: 12 as Any)).to(beNil()) - } - } - } - - private func dictionaryValueSpec() { - describe("dictionaryValue") { - it("creates a dictionary with matching elements") { - let dict = TestValues.defaultEnvironment().dictionaryValue - expect(dict["userKey"] as? String) == TestValues.userKey - let dictEnvs = dict["environmentFlags"] as? [String: [String: Any]] - expect(dictEnvs?.compactMapValues { CacheableEnvironmentFlags(dictionary: $0)}) == TestValues.environments - expect(dict["lastUpdated"] as? String) == TestValues.updated.stringValue - } - it("creates a dictionary without environments") { - let dict = TestValues.defaultEnvironment(withEnvironments: [:]).dictionaryValue - expect(dict["userKey"] as? String) == TestValues.userKey - expect((dict["environmentFlags"] as? [String: Any])?.isEmpty) == true - expect(dict["lastUpdated"] as? String) == TestValues.updated.stringValue - } - } - } -} - -extension FeatureFlag { - struct StubConstants { - static let mobileKey = "mobileKey" - } - - static func stubFlagCollection(userKey: String, mobileKey: String) -> [LDFlagKey: FeatureFlag] { - var flagCollection = DarklyServiceMock.Constants.stubFeatureFlags() - flagCollection[LDUser.StubConstants.userKey] = FeatureFlag(flagKey: LDUser.StubConstants.userKey, - value: userKey, - variation: DarklyServiceMock.Constants.variation, - version: DarklyServiceMock.Constants.version, - flagVersion: DarklyServiceMock.Constants.flagVersion, - trackEvents: true, - debugEventsUntilDate: Date().addingTimeInterval(30.0), - reason: DarklyServiceMock.Constants.reason, - trackReason: false) - flagCollection[StubConstants.mobileKey] = FeatureFlag(flagKey: StubConstants.mobileKey, - value: mobileKey, - variation: DarklyServiceMock.Constants.variation, - version: DarklyServiceMock.Constants.version, - flagVersion: DarklyServiceMock.Constants.flagVersion, - trackEvents: true, - debugEventsUntilDate: Date().addingTimeInterval(30.0), - reason: DarklyServiceMock.Constants.reason, - trackReason: false) - return flagCollection - } -} - -extension Date { - static let stubString = "2018-02-21T18:10:40.823Z" - static let stubDate = stubString.dateValue -} - -extension CacheableEnvironmentFlags { - static func stubCollection(userKey: String, environmentCount: Int) -> [MobileKey: CacheableEnvironmentFlags] { - (0.. (users: [LDUser], collection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) { - var pastSeconds = 0.0 - - let users = (0.. Bool { - if value == nil && other is NSNull { - return considerNilAndNullEqual - } - if value is NSNull && other == nil { - return considerNilAndNullEqual - } - return isEqual(value, to: other) + func testVersionForEvents() { + XCTAssertNil(FeatureFlag(flagKey: "t").versionForEvents) + XCTAssertEqual(FeatureFlag(flagKey: "t", version: 4).versionForEvents, 4) + XCTAssertEqual(FeatureFlag(flagKey: "t", flagVersion: 3).versionForEvents, 3) + XCTAssertEqual(FeatureFlag(flagKey: "t", version: 2, flagVersion: 3).versionForEvents, 3) } } -extension FeatureFlag { - func allPropertiesMatch(_ otherFlag: FeatureFlag) -> Bool { - AnyComparer.isEqual(self.value, to: otherFlag.value, considerNilAndNullEqual: true) - && variation == otherFlag.variation - && version == otherFlag.version - && flagVersion == otherFlag.flagVersion - } - - init(copying featureFlag: FeatureFlag, value: Any? = nil, variation: Int? = nil, version: Int? = nil, flagVersion: Int? = nil, trackEvents: Bool? = nil, debugEventsUntilDate: Date? = nil, reason: [String: Any]? = nil, trackReason: Bool? = nil) { - self.init(flagKey: featureFlag.flagKey, - value: value ?? featureFlag.value, - variation: variation ?? featureFlag.variation, - version: version ?? featureFlag.version, - flagVersion: flagVersion ?? featureFlag.flagVersion, - trackEvents: trackEvents ?? featureFlag.trackEvents, - debugEventsUntilDate: debugEventsUntilDate ?? featureFlag.debugEventsUntilDate, - reason: reason ?? featureFlag.reason, - trackReason: trackReason ?? featureFlag.trackReason) +extension FeatureFlag: Equatable { + public static func == (lhs: FeatureFlag, rhs: FeatureFlag) -> Bool { + lhs.flagKey == rhs.flagKey && + lhs.value == rhs.value && + lhs.variation == rhs.variation && + lhs.version == rhs.version && + lhs.flagVersion == rhs.flagVersion && + lhs.trackEvents == rhs.trackEvents && +// lhs.debugEventsUntilDate == rhs.debugEventsUntilDate && + lhs.reason == rhs.reason && + lhs.trackReason == rhs.trackReason } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift index f37372c7..cb09469e 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift @@ -184,7 +184,7 @@ extension FlagCounter { let flagCounter = FlagCounter() var featureFlag: FeatureFlag? = nil if flagKey.isKnown { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey, includeVersion: true, includeFlagVersion: true) + featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey) for _ in 0.. DeprecatedCacheMock { - cacheConverter.deprecatedCaches[version] as! DeprecatedCacheMock - } + override class func setUp() { + upToDateData = try! JSONEncoder().encode(["version": 7]) } - override func spec() { - initSpec() - convertCacheDataSpec() + override func setUp() { + serviceFactory = ClientServiceMockFactory() } - private func initSpec() { - var testContext: TestContext! - describe("init") { - it("creates a cache converter") { - testContext = TestContext() - expect(testContext.clientServiceFactoryMock.makeFeatureFlagCacheCallCount) == 1 - expect(testContext.cacheConverter.currentCache) === testContext.clientServiceFactoryMock.makeFeatureFlagCacheReturnValue - DeprecatedCacheModel.allCases.forEach { deprecatedCacheModel in - expect(testContext.cacheConverter.deprecatedCaches[deprecatedCacheModel]).toNot(beNil()) - expect(testContext.clientServiceFactoryMock.makeDeprecatedCacheModelReceivedModels.contains(deprecatedCacheModel)) == true - } - } - } + func testNoKeysGiven() { + CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: [], maxCachedUsers: 0) + XCTAssertEqual(serviceFactory.makeKeyedValueCacheCallCount, 0) + XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 0) } - private func convertCacheDataSpec() { - let cacheCases: [DeprecatedCacheModel?] = [.version5, nil] // Nil for no deprecated cache - var testContext: TestContext! - describe("convertCacheData") { - afterEach { - // The CacheConverter should always remove all expired data - DeprecatedCacheModel.allCases.forEach { model in - expect(testContext.deprecatedCacheMock(for: model).removeDataCallCount) == 1 - expect(testContext.deprecatedCacheMock(for: model).removeDataReceivedExpirationDate) - .to(beCloseTo(testContext.expiredCacheThreshold, within: 0.5)) - } - } - for deprecatedData in cacheCases { - context("current cache and \(deprecatedData?.rawValue ?? "no") deprecated cache data exists") { - it("does not load from deprecated caches") { - testContext = TestContext(createCacheData: true, deprecatedCacheData: deprecatedData) - testContext.cacheConverter.convertCacheData(for: testContext.user, and: testContext.config) - DeprecatedCacheModel.allCases.forEach { - expect(testContext.deprecatedCacheMock(for: $0).retrieveFlagsCallCount) == 0 - } - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 - } - } - context("no current cache data and \(deprecatedData?.rawValue ?? "no") deprecated cache data exists") { - beforeEach { - testContext = TestContext(createCacheData: false, deprecatedCacheData: deprecatedData) - testContext.cacheConverter.convertCacheData(for: testContext.user, and: testContext.config) - } - it("looks in the deprecated caches for data") { - let searchUpTo = cacheCases.firstIndex(of: deprecatedData)! - DeprecatedCacheModel.allCases.forEach { - expect(testContext.deprecatedCacheMock(for: $0).retrieveFlagsCallCount) == (cacheCases.firstIndex(of: $0)! <= searchUpTo ? 1 : 0) - } - } - if let deprecatedData = deprecatedData { - it("creates current cache data from the deprecated cache data") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == - testContext.deprecatedCacheMock(for: deprecatedData).retrieveFlagsReturnValue?.featureFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated) == - testContext.deprecatedCacheMock(for: deprecatedData).retrieveFlagsReturnValue?.lastUpdated - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .sync - } - } else { - it("leaves the current cache data unchanged") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 - } - } - } - } - } + func testUpToDate() { + let v7valueCacheMock = KeyedValueCachingMock() + serviceFactory.makeFeatureFlagCacheReturnValue.keyedValueCache = v7valueCacheMock + v7valueCacheMock.dataReturnValue = CacheConverterSpec.upToDateData + CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: ["key1", "key2"], maxCachedUsers: 0) + XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 2) + XCTAssertEqual(v7valueCacheMock.dataCallCount, 2) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift deleted file mode 100644 index 9e6df446..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift +++ /dev/null @@ -1,157 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -protocol CacheModelTestInterface { - var cacheKey: String { get } - func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache - func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? - func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] -} - -class DeprecatedCacheModelSpec { - - let cacheModelInterface: CacheModelTestInterface - - struct Constants { - static let offsetInterval: TimeInterval = 0.1 - } - - struct TestContext { - let cacheModel: CacheModelTestInterface - var keyedValueCacheMock = KeyedValueCachingMock() - var deprecatedCache: DeprecatedCache - var users: [LDUser] - var userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags] - var mobileKeys: [MobileKey] - var sortedLastUpdatedDates: [(userKey: UserKey, lastUpdated: Date)] { - userEnvironmentsCollection.map { ($0, $1.lastUpdated) }.sorted { - $0.lastUpdated < $1.lastUpdated - } - } - var userKeys: [UserKey] { users.map { $0.key } } - - init(_ cacheModel: CacheModelTestInterface, userCount: Int = 0) { - self.cacheModel = cacheModel - deprecatedCache = cacheModel.createDeprecatedCache(keyedValueCache: keyedValueCacheMock) - (users, userEnvironmentsCollection, mobileKeys) = CacheableUserEnvironmentFlags.stubCollection(userCount: userCount) - keyedValueCacheMock.dictionaryReturnValue = cacheModel.modelDictionary(for: users, and: userEnvironmentsCollection, mobileKeys: mobileKeys) - } - - func featureFlags(for userKey: UserKey, and mobileKey: MobileKey) -> [LDFlagKey: FeatureFlag]? { - guard let originalFlags = userEnvironmentsCollection[userKey]?.environmentFlags[mobileKey]?.featureFlags - else { return nil } - return cacheModel.expectedFeatureFlags(originalFlags: originalFlags) - } - - func expiredUserKeys(for expirationDate: Date) -> [UserKey] { - sortedLastUpdatedDates.compactMap { - $0.lastUpdated < expirationDate ? $0.userKey : nil - } - } - } - - init(cacheModelInterface: CacheModelTestInterface) { - self.cacheModelInterface = cacheModelInterface - } - - func spec() { - initSpec() - retrieveFlagsSpec() - removeDataSpec() - } - - private func initSpec() { - var testContext: TestContext! - describe("init") { - it("creates cache with the keyed value cache") { - testContext = TestContext(self.cacheModelInterface) - expect(testContext.deprecatedCache.keyedValueCache) === testContext.keyedValueCacheMock - } - } - } - - private func retrieveFlagsSpec() { - var testContext: TestContext! - var cachedData: (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?)! - describe("retrieveFlags") { - it("returns nil when no cached data exists") { - testContext = TestContext(self.cacheModelInterface) - cachedData = testContext.deprecatedCache.retrieveFlags(for: UUID().uuidString, and: UUID().uuidString) - expect(cachedData.featureFlags).to(beNil()) - expect(cachedData.lastUpdated).to(beNil()) - } - context("when cached data exists") { - it("retrieves cached user") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - testContext.users.forEach { user in - let expectedLastUpdated = testContext.userEnvironmentsCollection[user.key]?.lastUpdated.stringEquivalentDate - testContext.mobileKeys.forEach { mobileKey in - let expectedFlags = testContext.featureFlags(for: user.key, and: mobileKey) - cachedData = testContext.deprecatedCache.retrieveFlags(for: user.key, and: mobileKey) - expect(cachedData.featureFlags) == expectedFlags - expect(cachedData.lastUpdated) == expectedLastUpdated - } - } - } - it("returns nil for uncached environment") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - cachedData = testContext.deprecatedCache.retrieveFlags(for: testContext.users.first!.key, and: UUID().uuidString) - expect(cachedData.featureFlags).to(beNil()) - expect(cachedData.lastUpdated).to(beNil()) - } - it("returns nil for uncached user") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - cachedData = testContext.deprecatedCache.retrieveFlags(for: UUID().uuidString, and: testContext.mobileKeys.first!) - expect(cachedData.featureFlags).to(beNil()) - expect(cachedData.lastUpdated).to(beNil()) - } - } - } - } - - private func removeDataSpec() { - var testContext: TestContext! - var expirationDate: Date! - describe("removeData") { - it("no cached data expired") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - let oldestLastUpdatedDate = testContext.sortedLastUpdatedDates.first! - expirationDate = oldestLastUpdatedDate.lastUpdated.addingTimeInterval(-Constants.offsetInterval) - - testContext.deprecatedCache.removeData(olderThan: expirationDate) - expect(testContext.keyedValueCacheMock.setCallCount) == 0 - } - it("some cached data expired") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - let selectedLastUpdatedDate = testContext.sortedLastUpdatedDates[testContext.users.count / 2] - expirationDate = selectedLastUpdatedDate.lastUpdated.addingTimeInterval(-Constants.offsetInterval) - - testContext.deprecatedCache.removeData(olderThan: expirationDate) - expect(testContext.keyedValueCacheMock.setCallCount) == 1 - expect(testContext.keyedValueCacheMock.setReceivedArguments?.forKey) == self.cacheModelInterface.cacheKey - let recachedData = testContext.keyedValueCacheMock.setReceivedArguments?.value as? [String: Any] - let expiredUserKeys = testContext.expiredUserKeys(for: expirationDate) - testContext.userKeys.forEach { userKey in - expect(recachedData?.keys.contains(userKey)) == !expiredUserKeys.contains(userKey) - } - } - it("all cached data expired") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - let newestLastUpdatedDate = testContext.sortedLastUpdatedDates.last! - expirationDate = newestLastUpdatedDate.lastUpdated.addingTimeInterval(Constants.offsetInterval) - - testContext.deprecatedCache.removeData(olderThan: expirationDate) - expect(testContext.keyedValueCacheMock.removeObjectCallCount) == 1 - expect(testContext.keyedValueCacheMock.removeObjectReceivedForKey) == self.cacheModelInterface.cacheKey - } - it("no cached data") { - let testContext = TestContext(self.cacheModelInterface) - testContext.keyedValueCacheMock.dictionaryReturnValue = nil - testContext.deprecatedCache.removeData(olderThan: Date()) - expect(testContext.keyedValueCacheMock.setCallCount) == 0 - } - } - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift deleted file mode 100644 index 9a75b969..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DeprecatedCacheModelV5Spec: QuickSpec, CacheModelTestInterface { - let cacheKey = DeprecatedCacheModelV5.CacheKeys.userEnvironments - - func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache { - DeprecatedCacheModelV5(keyedValueCache: keyedValueCache) - } - - func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? { - guard !users.isEmpty - else { return nil } - - var cacheDictionary = [UserKey: [String: Any]]() - users.forEach { user in - guard let userEnvironment = userEnvironmentsCollection[user.key] - else { return } - var environmentsDictionary = [MobileKey: Any]() - let lastUpdated = userEnvironmentsCollection[user.key]?.lastUpdated - mobileKeys.forEach { mobileKey in - guard let featureFlags = userEnvironment.environmentFlags[mobileKey]?.featureFlags - else { return } - environmentsDictionary[mobileKey] = user.modelV5DictionaryValue(including: featureFlags, using: lastUpdated) - } - cacheDictionary[user.key] = [CacheableEnvironmentFlags.CodingKeys.userKey.rawValue: user.key, - DeprecatedCacheModelV5.CacheKeys.environments: environmentsDictionary] - } - return cacheDictionary - } - - func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] { - originalFlags.filter { $0.value.value != nil }.compactMapValues { orig in - FeatureFlag(flagKey: orig.flagKey, - value: orig.value, - variation: orig.variation, - version: orig.version, - flagVersion: orig.flagVersion, - trackEvents: orig.trackEvents, - debugEventsUntilDate: orig.debugEventsUntilDate) - } - } - - override func spec() { - DeprecatedCacheModelSpec(cacheModelInterface: self).spec() - } -} - -// MARK: Dictionary value to cache - -extension LDUser { - func modelV5DictionaryValue(including featureFlags: [LDFlagKey: FeatureFlag], using lastUpdated: Date?) -> [String: Any] { - var userDictionary = encodeToLDValue(self, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true])?.toAny() as! [String: Any] - userDictionary[CodingKeys.privateAttributes.rawValue] = privateAttributes - userDictionary["updatedAt"] = lastUpdated?.stringValue - userDictionary[LDUser.CodingKeys.config.rawValue] = featureFlags.compactMapValues { $0.modelV5dictionaryValue } - return userDictionary - } -} - -extension FeatureFlag { -/* - [“version”: , - “flagVersion”: , - “variation”: , - “value”: , - “trackEvents”: , - “debugEventsUntilDate”: ] -*/ - var modelV5dictionaryValue: [String: Any]? { - guard value != nil - else { return nil } - var flagDictionary = dictionaryValue - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.flagKey.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.reason.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.trackReason.rawValue) - return flagDictionary - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift new file mode 100644 index 00000000..3989e155 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift @@ -0,0 +1,137 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class FeatureFlagCacheSpec: XCTestCase { + + let testFlagCollection = FeatureFlagCollection(["flag1": FeatureFlag(flagKey: "flag1", variation: 1, flagVersion: 2)]) + + private var serviceFactory: ClientServiceMockFactory! + private var mockValueCache: KeyedValueCachingMock { serviceFactory.makeKeyedValueCacheReturnValue } + + override func setUp() { + serviceFactory = ClientServiceMockFactory() + } + + func testInit() { + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) + XCTAssertEqual(flagCache.maxCachedUsers, 2) + XCTAssertEqual(serviceFactory.makeKeyedValueCacheCallCount, 1) + let bundleHashed = Util.sha256base64(Bundle.main.bundleIdentifier!) + let keyHashed = Util.sha256base64("abc") + let expectedCacheKey = "com.launchdarkly.client.\(bundleHashed).\(keyHashed)" + XCTAssertEqual(serviceFactory.makeKeyedValueCacheReceivedCacheKey, expectedCacheKey) + XCTAssertTrue(flagCache.keyedValueCache as? KeyedValueCachingMock === mockValueCache) + } + + func testRetrieveNoData() { + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) + XCTAssertNil(flagCache.retrieveFeatureFlags(userKey: "user1")) + XCTAssertEqual(mockValueCache.dataCallCount, 1) + XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-\(Util.sha256base64("user1"))") + } + + func testRetrieveInvalidData() { + mockValueCache.dataReturnValue = Data("invalid".utf8) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) + XCTAssertNil(flagCache.retrieveFeatureFlags(userKey: "user1")) + } + + func testRetrieveEmptyData() throws { + mockValueCache.dataReturnValue = try JSONEncoder().encode(FeatureFlagCollection([:])) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) + XCTAssertEqual(flagCache.retrieveFeatureFlags(userKey: "user1")?.count, 0) + } + + func testRetrieveValidData() throws { + mockValueCache.dataReturnValue = try JSONEncoder().encode(testFlagCollection) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) + let retrieved = flagCache.retrieveFeatureFlags(userKey: "user1") + XCTAssertEqual(retrieved, testFlagCollection.flags) + XCTAssertEqual(mockValueCache.dataCallCount, 1) + XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-\(Util.sha256base64("user1"))") + } + + func testStoreCacheDisabled() { + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) + flagCache.storeFeatureFlags([:], userKey: "user1", lastUpdated: Date()) + XCTAssertEqual(mockValueCache.setCallCount, 0) + XCTAssertEqual(mockValueCache.dataCallCount, 0) + XCTAssertEqual(mockValueCache.removeObjectCallCount, 0) + } + + func testStoreEmptyData() throws { + let now = Date() + let hashedUserKey = Util.sha256base64("user1") + var count = 0 + mockValueCache.setCallback = { + if self.mockValueCache.setReceivedArguments?.forKey == "cached-users" { + let setData = self.mockValueCache.setReceivedArguments!.value + XCTAssertEqual(setData, try JSONEncoder().encode([hashedUserKey: now.millisSince1970])) + count += 1 + } else if let received = self.mockValueCache.setReceivedArguments { + XCTAssertEqual(received.forKey, "flags-\(hashedUserKey)") + XCTAssertEqual(received.value, try JSONEncoder().encode(FeatureFlagCollection([:]))) + count += 2 + } + } + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: -1) + flagCache.storeFeatureFlags([:], userKey: "user1", lastUpdated: now) + XCTAssertEqual(count, 3) + } + + func testStoreValidData() throws { + mockValueCache.setCallback = { + if let received = self.mockValueCache.setReceivedArguments, received.forKey.starts(with: "flags-") { + XCTAssertEqual(received.value, try JSONEncoder().encode(self.testFlagCollection)) + } + } + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: Date()) + XCTAssertEqual(mockValueCache.setCallCount, 2) + } + + func testStoreMaxCachedUsersStored() throws { + let hashedUserKey = Util.sha256base64("user1") + let now = Date() + let earlier = now.addingTimeInterval(-30.0) + mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": earlier.millisSince1970]) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: now) + XCTAssertEqual(mockValueCache.removeObjectCallCount, 1) + XCTAssertEqual(mockValueCache.removeObjectReceivedForKey, "flags-key1") + let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) + XCTAssertEqual(setMetadata, [hashedUserKey: now.millisSince1970]) + } + + func testStoreAboveMaxCachedUsersStored() throws { + let hashedUserKey = Util.sha256base64("user1") + let now = Date() + let earlier = now.addingTimeInterval(-30.0) + let later = now.addingTimeInterval(30.0) + mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": now.millisSince1970, + "key2": earlier.millisSince1970, + "key3": later.millisSince1970]) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) + var removedObjects: [String] = [] + mockValueCache.removeObjectCallback = { removedObjects.append(self.mockValueCache.removeObjectReceivedForKey!) } + flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: later) + XCTAssertEqual(mockValueCache.removeObjectCallCount, 2) + XCTAssertTrue(removedObjects.contains("flags-key1")) + XCTAssertTrue(removedObjects.contains("flags-key2")) + let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) + XCTAssertEqual(setMetadata, [hashedUserKey: later.millisSince1970, "key3": later.millisSince1970]) + } + + func testStoreInvalidMetadataStored() throws { + let hashedUserKey = Util.sha256base64("user1") + let now = Date() + mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": "123"]) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: now) + XCTAssertEqual(mockValueCache.removeObjectCallCount, 0) + let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) + XCTAssertEqual(setMetadata, [hashedUserKey: now.millisSince1970]) + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift deleted file mode 100644 index 3b59c37d..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import XCTest - -@testable import LaunchDarkly - -final class KeyedValueCacheSpec: XCTestCase { - private let cacheKey = UserEnvironmentFlagCache.CacheKeys.cachedUserEnvironmentFlags - - override func setUp() { - UserDefaults.standard.removeObject(forKey: cacheKey) - } - - func testKeyValueCache() { - let testDictionary = CacheableUserEnvironmentFlags.stubCollection().collection - let cache: KeyedValueCaching = UserDefaults.standard - // Returns nil when nothing stored - XCTAssertNil(cache.dictionary(forKey: cacheKey)) - // Can store flags collection - cache.set(testDictionary.compactMapValues { $0.dictionaryValue }, forKey: cacheKey) - XCTAssertEqual(cache.dictionary(forKey: cacheKey)?.compactMapValues { CacheableUserEnvironmentFlags(object: $0) }, testDictionary) - // Set nil should remove value - cache.set(nil, forKey: cacheKey) - XCTAssertNil(cache.dictionary(forKey: cacheKey)) - // Remove should also remove value - cache.set(testDictionary.compactMapValues { $0.dictionaryValue }, forKey: cacheKey) - cache.removeObject(forKey: cacheKey) - XCTAssertNil(cache.dictionary(forKey: cacheKey)) - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift deleted file mode 100644 index 10145d3e..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift +++ /dev/null @@ -1,270 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class UserEnvironmentFlagCacheSpec: QuickSpec { - - private struct TestValues { - static let replacementFlags = ["newFlagKey": FeatureFlag.stub(flagKey: "newFlagKey", flagValue: "newFlagValue")] - static let newUserEnv = CacheableEnvironmentFlags(userKey: UUID().uuidString, - mobileKey: UUID().uuidString, - featureFlags: TestValues.replacementFlags) - static let lastUpdated = Date().addingTimeInterval(60.0).stringEquivalentDate - } - - struct TestContext { - var keyedValueCacheMock = KeyedValueCachingMock() - let storeMode: FlagCachingStoreMode - var subject: UserEnvironmentFlagCache - var userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags]! - var selectedUser: String { - userEnvironmentsCollection.randomElement()!.key - } - var selectedMobileKey: String { - userEnvironmentsCollection[selectedUser]!.environmentFlags.randomElement()!.key - } - var oldestUser: String { - userEnvironmentsCollection.compactMapValues { $0.lastUpdated } - .max { $1.value < $0.value }! - .key - } - var setUserEnvironments: [UserKey: CacheableUserEnvironmentFlags]? { - (keyedValueCacheMock.setReceivedArguments?.value as? [UserKey: Any])?.compactMapValues { CacheableUserEnvironmentFlags(object: $0) } - } - - init(maxUsers: Int = 5, storeMode: FlagCachingStoreMode = .async) { - self.storeMode = storeMode - subject = UserEnvironmentFlagCache(withKeyedValueCache: keyedValueCacheMock, maxCachedUsers: maxUsers) - } - - mutating func withCached(userCount: Int = 1) { - userEnvironmentsCollection = CacheableUserEnvironmentFlags.stubCollection(userCount: userCount).collection - keyedValueCacheMock.dictionaryReturnValue = userEnvironmentsCollection.compactMapValues { $0.dictionaryValue } - } - - func storeNewUser() -> CacheableUserEnvironmentFlags { - let env = storeNewUserEnv(userKey: UUID().uuidString) - return CacheableUserEnvironmentFlags(userKey: env.userKey, - environmentFlags: [env.mobileKey: env], - lastUpdated: TestValues.lastUpdated) - } - - func storeNewUserEnv(userKey: String) -> CacheableEnvironmentFlags { - storeUserEnvUpdate(userKey: userKey, mobileKey: UUID().uuidString) - } - - func storeUserEnvUpdate(userKey: String, mobileKey: String) -> CacheableEnvironmentFlags { - storeFlags(TestValues.replacementFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: TestValues.lastUpdated) - return CacheableEnvironmentFlags(userKey: userKey, mobileKey: mobileKey, featureFlags: TestValues.replacementFlags) - } - - func storeFlags(_ featureFlags: [LDFlagKey: FeatureFlag], - userKey: String, - mobileKey: String, - lastUpdated: Date) { - waitUntil { done in - self.subject.storeFeatureFlags(featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated, storeMode: self.storeMode, completion: done) - if self.storeMode == .sync { done() } - } - expect(self.keyedValueCacheMock.setReceivedArguments?.forKey) == UserEnvironmentFlagCache.CacheKeys.cachedUserEnvironmentFlags - } - } - - override func spec() { - initSpec() - retrieveFeatureFlagsSpec() - storeFeatureFlagsSpec(maxUsers: LDConfig.Defaults.maxCachedUsers) - storeFeatureFlagsSpec(maxUsers: 3) - storeUnlimitedUsersSpec() - } - - private func initSpec() { - describe("init") { - it("creates a UserEnvironmentFlagCache") { - let testContext = TestContext(maxUsers: 5) - expect(testContext.subject.keyedValueCache) === testContext.keyedValueCacheMock - expect(testContext.subject.maxCachedUsers) == 5 - } - } - } - - private func retrieveFeatureFlagsSpec() { - var testContext: TestContext! - describe("retrieveFeatureFlags") { - beforeEach { - testContext = TestContext() - } - context("returns nil") { - it("when no flags are stored") { - expect(testContext.subject.retrieveFeatureFlags(forUserWithKey: "unknown", andMobileKey: "unknown")).to(beNil()) - } - it("when no flags are stored for user") { - testContext.withCached(userCount: LDConfig.Defaults.maxCachedUsers) - expect(testContext.subject.retrieveFeatureFlags(forUserWithKey: "unknown", andMobileKey: testContext.selectedMobileKey)).to(beNil()) - } - it("when no flags are stored for environment") { - testContext.withCached(userCount: LDConfig.Defaults.maxCachedUsers) - expect(testContext.subject.retrieveFeatureFlags(forUserWithKey: testContext.selectedUser, andMobileKey: "unknown")).to(beNil()) - } - } - it("returns the flags for user and environment") { - testContext.withCached(userCount: LDConfig.Defaults.maxCachedUsers) - let toRetrieve = testContext.userEnvironmentsCollection.randomElement()!.value.environmentFlags.randomElement()!.value - expect(testContext.subject.retrieveFeatureFlags(forUserWithKey: toRetrieve.userKey, andMobileKey: toRetrieve.mobileKey)) == toRetrieve.featureFlags - } - } - } - - private func storeUnlimitedUsersSpec() { - describe("storeFeatureFlags with no cached limit") { - FlagCachingStoreMode.allCases.forEach { storeMode in - it("and a new users flags are stored") { - var testContext = TestContext(maxUsers: -1, storeMode: storeMode) - testContext.withCached(userCount: LDConfig.Defaults.maxCachedUsers) - let expectedEnv = testContext.storeNewUser() - - expect(testContext.setUserEnvironments?.count) == LDConfig.Defaults.maxCachedUsers + 1 - expect(testContext.setUserEnvironments?[expectedEnv.userKey]) == expectedEnv - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - expect(testContext.setUserEnvironments?[userKey]) == userEnv - } - } - } - } - } - - private func storeFeatureFlagsSpec(maxUsers: Int) { - FlagCachingStoreMode.allCases.forEach { storeMode in - storeFeatureFlagsSpec(maxUsers: maxUsers, storeMode: storeMode) - } - } - - private func storeFeatureFlagsSpec(maxUsers: Int, storeMode: FlagCachingStoreMode) { - var testContext: TestContext! - describe(storeMode == .async ? "storeFeatureFlagsAsync" : "storeFeatureFlagsSync") { - beforeEach { - testContext = TestContext(maxUsers: maxUsers, storeMode: storeMode) - } - it("when store is empty") { - let expectedEnv = testContext.storeNewUser() - - expect(testContext.setUserEnvironments?.count) == 1 - expect(testContext.setUserEnvironments?[expectedEnv.userKey]) == expectedEnv - } - context("when less than the max number of users flags are stored") { - it("and an existing users flags are changed") { - testContext.withCached(userCount: maxUsers - 1) - let expectedEnv = testContext.storeUserEnvUpdate(userKey: testContext.selectedUser, mobileKey: testContext.selectedMobileKey) - - expect(testContext.setUserEnvironments?.count) == maxUsers - 1 - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - if userKey != expectedEnv.userKey { - expect(testContext.setUserEnvironments?[userKey]) == userEnv - return - } - - var userFlags = userEnv.environmentFlags - userFlags[expectedEnv.mobileKey] = expectedEnv - expect(testContext.setUserEnvironments?[userKey]) == CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: userFlags, lastUpdated: TestValues.lastUpdated) - } - } - it("and an existing user adds a new environment") { - testContext.withCached(userCount: maxUsers - 1) - let expectedEnv = testContext.storeNewUserEnv(userKey: testContext.selectedUser) - - expect(testContext.setUserEnvironments?.count) == maxUsers - 1 - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - if userKey != expectedEnv.userKey { - expect(testContext.setUserEnvironments?[userKey]) == userEnv - return - } - - var userFlags = userEnv.environmentFlags - userFlags[expectedEnv.mobileKey] = expectedEnv - expect(testContext.setUserEnvironments?[userKey]) == CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: userFlags, lastUpdated: TestValues.lastUpdated) - } - } - it("and a new users flags are stored") { - testContext.withCached(userCount: maxUsers - 1) - let expectedEnv = testContext.storeNewUser() - - expect(testContext.setUserEnvironments?.count) == maxUsers - expect(testContext.setUserEnvironments?[expectedEnv.userKey]) == expectedEnv - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - expect(testContext.setUserEnvironments?[userKey]) == userEnv - } - } - } - context("when max number of users flags are stored") { - it("and an existing users flags are changed") { - testContext.withCached(userCount: maxUsers) - let expectedEnv = testContext.storeUserEnvUpdate(userKey: testContext.selectedUser, mobileKey: testContext.selectedMobileKey) - - expect(testContext.setUserEnvironments?.count) == maxUsers - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - if userKey != expectedEnv.userKey { - expect(testContext.setUserEnvironments?[userKey]) == userEnv - return - } - - var userFlags = userEnv.environmentFlags - userFlags[expectedEnv.mobileKey] = expectedEnv - expect(testContext.setUserEnvironments?[userKey]) == CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: userFlags, lastUpdated: TestValues.lastUpdated) - } - } - it("and an existing user adds a new environment") { - testContext.withCached(userCount: maxUsers) - let expectedEnv = testContext.storeNewUserEnv(userKey: testContext.selectedUser) - - expect(testContext.setUserEnvironments?.count) == maxUsers - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - if userKey != expectedEnv.userKey { - expect(testContext.setUserEnvironments?[userKey]) == userEnv - return - } - - var userFlags = userEnv.environmentFlags - userFlags[expectedEnv.mobileKey] = expectedEnv - expect(testContext.setUserEnvironments?[userKey]) == CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: userFlags, lastUpdated: TestValues.lastUpdated) - } - } - it("and a new users flags are stored overwrites oldest user") { - testContext.withCached(userCount: maxUsers) - let expectedEnv = testContext.storeNewUser() - - expect(testContext.setUserEnvironments?.count) == maxUsers - expect(testContext.setUserEnvironments?.keys.contains(testContext.oldestUser)) == false - expect(testContext.setUserEnvironments?[expectedEnv.userKey]) == expectedEnv - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - guard userKey != testContext.oldestUser - else { return } - expect(testContext.setUserEnvironments?[userKey]) == userEnv - } - } - } - } - } -} - -extension CacheableUserEnvironmentFlags: Equatable { - public static func == (lhs: CacheableUserEnvironmentFlags, rhs: CacheableUserEnvironmentFlags) -> Bool { - lhs.userKey == rhs.userKey && - lhs.lastUpdated == rhs.lastUpdated && - lhs.environmentFlags == rhs.environmentFlags - } -} - -private extension FeatureFlag { - static func stub(flagKey: LDFlagKey, flagValue: Any?) -> FeatureFlag { - FeatureFlag(flagKey: flagKey, - value: flagValue, - variation: DarklyServiceMock.Constants.variation, - version: DarklyServiceMock.Constants.version, - flagVersion: DarklyServiceMock.Constants.flagVersion, - trackEvents: true, - debugEventsUntilDate: Date().addingTimeInterval(30.0), - reason: DarklyServiceMock.Constants.reason, - trackReason: false) - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index caa3f72a..1e0d177e 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -1,7 +1,6 @@ import Foundation import Quick import Nimble -import XCTest @testable import LaunchDarkly final class EventReporterSpec: QuickSpec { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift index 12cf793d..591f7cad 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift @@ -1,227 +1,85 @@ import Foundation -import Quick -import Nimble -@testable import LaunchDarkly +import XCTest -final class FlagStoreSpec: QuickSpec { - struct DefaultValues { - static let bool = false - static let int = 3 - static let double = 2.71828 - static let string = "defaultValue string" - static let array = [0] - static let dictionary: [String: Any] = [DarklyServiceMock.FlagKeys.string: DarklyServiceMock.FlagValues.string] - } +@testable import LaunchDarkly +final class FlagStoreSpec: XCTestCase { let stubFlags = DarklyServiceMock.Constants.stubFeatureFlags() - override func spec() { - initSpec() - replaceStoreSpec() - updateStoreSpec() - deleteFlagSpec() - featureFlagSpec() + func testInit() { + XCTAssertEqual(FlagStore().featureFlags, [:]) + XCTAssertEqual(FlagStore(featureFlags: self.stubFlags).featureFlags, self.stubFlags) } - func initSpec() { - describe("init") { - it("without an initial flag store is empty") { - expect(FlagStore().featureFlags.isEmpty) == true - } - it("with an initial flag store") { - expect(FlagStore(featureFlags: self.stubFlags).featureFlags) == self.stubFlags - } - it("with an initial flag store without elements") { - let featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false) - expect(FlagStore(featureFlags: featureFlags).featureFlags) == featureFlags - } - it("with an initial flag dictionary") { - expect(FlagStore(featureFlagDictionary: self.stubFlags.dictionaryValue).featureFlags) == self.stubFlags - } - } + func testReplaceStore() { + let featureFlags: [LDFlagKey: FeatureFlag] = DarklyServiceMock.Constants.stubFeatureFlags() + let flagStore = FlagStore() + flagStore.replaceStore(newFlags: FeatureFlagCollection(featureFlags)) + XCTAssertEqual(flagStore.featureFlags, featureFlags) } - func replaceStoreSpec() { - let featureFlags: [LDFlagKey: FeatureFlag] = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false) - describe("replaceStore") { - it("with new flag values replaces flag values") { - let flagStore = FlagStore() - waitUntil { done in - flagStore.replaceStore(newFlags: featureFlags, completion: done) - } - expect(flagStore.featureFlags) == featureFlags - } - it("with flags dictionary replaces flag values") { - let flagStore = FlagStore() - waitUntil { done in - flagStore.replaceStore(newFlags: featureFlags.dictionaryValue, completion: done) - } - expect(flagStore.featureFlags) == featureFlags - } - it("with invalid dictionary empties the flag values") { - let flagStore = FlagStore(featureFlags: featureFlags) - waitUntil { done in - flagStore.replaceStore(newFlags: ["fakeKey": "Not a flag dict"], completion: done) - } - expect(flagStore.featureFlags.isEmpty).to(beTrue()) - } - } + func testUpdateStoreNewFlag() { + let flagStore = FlagStore(featureFlags: stubFlags) + let flagUpdate = FeatureFlag(flagKey: "new-int-flag", value: "abc", version: 0) + flagStore.updateStore(updatedFlag: flagUpdate) + XCTAssertEqual(flagStore.featureFlags.count, stubFlags.count + 1) + XCTAssertEqual(flagStore.featureFlags["new-int-flag"], flagUpdate) } - func updateStoreSpec() { - var subject: FlagStore! - var updateDictionary: [String: Any]! + func testUpdateStoreNewerVersion() { + let flagStore = FlagStore(featureFlags: stubFlags) + let flagUpdate = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.int, useAlternateVersion: true) + flagStore.updateStore(updatedFlag: flagUpdate) + XCTAssertEqual(flagStore.featureFlags.count, stubFlags.count) + XCTAssertEqual(flagStore.featureFlags[DarklyServiceMock.FlagKeys.int], flagUpdate) + } - func updateFlag(key: String? = DarklyServiceMock.FlagKeys.int, - value: Any? = DarklyServiceMock.FlagValues.alternate(DarklyServiceMock.FlagValues.int), - variation: Int? = DarklyServiceMock.Constants.variation + 1, - version: Int? = DarklyServiceMock.Constants.version + 1, - includeExtraKey: Bool = false) { - waitUntil { done in - updateDictionary = FlagMaintainingMock.stubPatchDictionary(key: key, value: value, variation: variation, version: version, includeExtraKey: includeExtraKey) - subject.updateStore(updateDictionary: updateDictionary, completion: done) - } - } + func testUpdateStoreNoVersion() { + let flagStore = FlagStore(featureFlags: stubFlags) + let flagUpdate = FeatureFlag(flagKey: DarklyServiceMock.FlagKeys.int, value: "abc", version: nil) + flagStore.updateStore(updatedFlag: flagUpdate) + XCTAssertEqual(flagStore.featureFlags.count, stubFlags.count) + XCTAssertEqual(flagStore.featureFlags[DarklyServiceMock.FlagKeys.int], flagUpdate) + } - describe("updateStore") { - beforeEach { - subject = FlagStore(featureFlags: DarklyServiceMock.Constants.stubFeatureFlags()) - } - context("makes no changes") { - it("when the update version == existing version") { - updateFlag(variation: DarklyServiceMock.Constants.variation, version: DarklyServiceMock.Constants.version) - expect(subject.featureFlags) == self.stubFlags - } - it("when the update version < existing version") { - updateFlag(variation: DarklyServiceMock.Constants.variation - 1, version: DarklyServiceMock.Constants.version - 1) - expect(subject.featureFlags) == self.stubFlags - } - it("when the update dictionary is missing the flagKey") { - updateFlag(key: nil) - expect(subject.featureFlags) == self.stubFlags - } - } - context("updates the feature flag") { - it("when the update version > existing version") { - updateFlag() - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(AnyComparer.isEqual(featureFlag?.value, to: updateDictionary?.value)).to(beTrue()) - expect(featureFlag?.variation) == updateDictionary?.variation - expect(featureFlag?.version) == updateDictionary?.version - } - it("when the new value is null") { - updateFlag(value: NSNull()) - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(featureFlag?.value).to(beNil()) - expect(featureFlag?.variation) == updateDictionary.variation - expect(featureFlag?.version) == updateDictionary.version - } - it("when the update dictionary is missing the value") { - updateFlag(value: nil) - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(featureFlag?.value).to(beNil()) - expect(featureFlag?.variation) == updateDictionary.variation - expect(featureFlag?.version) == updateDictionary.version - } - it("when the update dictionary is missing the variation") { - updateFlag(variation: nil) - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(AnyComparer.isEqual(featureFlag?.value, to: updateDictionary.value)).to(beTrue()) - expect(featureFlag?.variation).to(beNil()) - expect(featureFlag?.version) == updateDictionary.version - } - it("when the update dictionary is missing the version") { - updateFlag(version: nil) - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(AnyComparer.isEqual(featureFlag?.value, to: updateDictionary.value)).to(beTrue()) - expect(featureFlag?.variation) == updateDictionary.variation - expect(featureFlag?.version).to(beNil()) - } - it("when the update dictionary has more keys than needed") { - updateFlag(includeExtraKey: true) - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(AnyComparer.isEqual(featureFlag?.value, to: updateDictionary.value)).to(beTrue()) - expect(featureFlag?.variation) == updateDictionary.variation - expect(featureFlag?.version) == updateDictionary.version - } - } - it("adds new feature flag to the store") { - updateFlag(key: "new-int-flag") - let featureFlag = subject.featureFlags["new-int-flag"] - expect(AnyComparer.isEqual(featureFlag?.value, to: updateDictionary?.value)).to(beTrue()) - expect(featureFlag?.variation) == updateDictionary?.variation - expect(featureFlag?.version) == updateDictionary?.version - } - } + func testUpdateStoreEarlierOrSameVersion() { + let testFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.int) + let initialVersion = testFlag.version! + let flagStore = FlagStore(featureFlags: stubFlags) + let flagUpdateSameVersion = FeatureFlag(flagKey: DarklyServiceMock.FlagKeys.int, value: "abc", version: initialVersion) + let flagUpdateOlderVersion = FeatureFlag(flagKey: DarklyServiceMock.FlagKeys.int, value: "abc", version: initialVersion - 1) + flagStore.updateStore(updatedFlag: flagUpdateSameVersion) + flagStore.updateStore(updatedFlag: flagUpdateOlderVersion) + XCTAssertEqual(flagStore.featureFlags, self.stubFlags) } - func deleteFlagSpec() { - var subject: FlagStore! + func testDeleteFlagNewerVersion() { + let flagStore = FlagStore(featureFlags: stubFlags) + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1)) + XCTAssertEqual(flagStore.featureFlags.count, self.stubFlags.count - 1) + XCTAssertNil(flagStore.featureFlags[DarklyServiceMock.FlagKeys.int]) + } - func deleteFlag(_ deleteDictionary: [String: Any]) { - waitUntil { done in - subject.deleteFlag(deleteDictionary: deleteDictionary, completion: done) - } - } + func testDeleteFlagMissingVersion() { + let flagStore = FlagStore(featureFlags: stubFlags) + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: nil)) + XCTAssertEqual(flagStore.featureFlags.count, self.stubFlags.count - 1) + XCTAssertNil(flagStore.featureFlags[DarklyServiceMock.FlagKeys.int]) + } - describe("deleteFlag") { - beforeEach { - subject = FlagStore(featureFlags: DarklyServiceMock.Constants.stubFeatureFlags()) - } - context("removes flag") { - it("with exact dictionary") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1)) - expect(subject.featureFlags.count) == self.stubFlags.count - 1 - expect(subject.featureFlags[DarklyServiceMock.FlagKeys.int]).to(beNil()) - } - it("with extra fields on dictionary") { - var deleteDictionary = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) - deleteDictionary["new-field"] = 10 - deleteFlag(deleteDictionary) - expect(subject.featureFlags.count) == self.stubFlags.count - 1 - expect(subject.featureFlags[DarklyServiceMock.FlagKeys.int]).to(beNil()) - } - } - context("makes no changes to the flag store") { - it("when the version is the same") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version)) - expect(subject.featureFlags) == self.stubFlags - } - it("when the new version < existing version") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version - 1)) - expect(subject.featureFlags) == self.stubFlags - } - it("when the flag doesn't exist") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: "new-int-flag", version: DarklyServiceMock.Constants.version + 1)) - expect(subject.featureFlags) == self.stubFlags - } - it("when delete dictionary is missing the key") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: nil, version: DarklyServiceMock.Constants.version + 1)) - expect(subject.featureFlags) == self.stubFlags - } - it("when delete dictionary is missing the version") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: nil)) - expect(subject.featureFlags) == self.stubFlags - } - } - } + func testDeleteOlderOrNonExistent() { + let flagStore = FlagStore(featureFlags: stubFlags) + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version)) + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version - 1)) + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: "new-int-flag", version: DarklyServiceMock.Constants.version + 1)) + XCTAssertEqual(flagStore.featureFlags, self.stubFlags) } - func featureFlagSpec() { - var flagStore: FlagStore! - describe("featureFlag") { - beforeEach { - flagStore = FlagStore(featureFlags: DarklyServiceMock.Constants.stubFeatureFlags()) - } - it("returns existing feature flag") { - flagStore.featureFlags.forEach { flagKey, featureFlag in - expect(flagStore.featureFlag(for: flagKey)?.allPropertiesMatch(featureFlag)).to(beTrue()) - } - } - it("returns nil when flag doesn't exist") { - let featureFlag = flagStore.featureFlag(for: DarklyServiceMock.FlagKeys.unknown) - expect(featureFlag).to(beNil()) - } + func testFeatureFlag() { + let flagStore = FlagStore(featureFlags: stubFlags) + flagStore.featureFlags.forEach { flagKey, featureFlag in + XCTAssertEqual(flagStore.featureFlag(for: flagKey), featureFlag) } + XCTAssertNil(flagStore.featureFlag(for: DarklyServiceMock.FlagKeys.unknown)) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift index 2b781380..722916cd 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift @@ -7,16 +7,10 @@ import LDSwiftEventSource final class FlagSynchronizerSpec: QuickSpec { struct Constants { fileprivate static let pollingInterval: TimeInterval = 1 - fileprivate static let waitMillis: Int = 500 } struct TestContext { - var config: LDConfig! - var user: LDUser! var serviceMock: DarklyServiceMock! - var eventSourceMock: DarklyStreamingProviderMock? { - serviceMock.createdEventSource - } var providedEventHandler: EventHandler? { serviceMock.createEventSourceReceivedHandler } @@ -28,8 +22,6 @@ final class FlagSynchronizerSpec: QuickSpec { var diagnosticCacheMock: DiagnosticCachingMock init(streamingMode: LDStreamingMode, useReport: Bool, onSyncComplete: FlagSyncCompleteClosure? = nil) { - config = LDConfig.stub - user = LDUser.stub() serviceMock = DarklyServiceMock() diagnosticCacheMock = DiagnosticCachingMock() serviceMock.diagnosticCache = diagnosticCacheMock @@ -39,69 +31,6 @@ final class FlagSynchronizerSpec: QuickSpec { service: serviceMock, onSyncComplete: onSyncComplete) } - - private func isStreamingActive(online: Bool, streamingMode: LDStreamingMode) -> Bool { - online && (streamingMode == .streaming) - } - private func isPollingActive(online: Bool, streamingMode: LDStreamingMode) -> Bool { - online && (streamingMode == .polling) - } - - fileprivate func synchronizerState(synchronizerOnline isOnline: Bool, - streamingMode: LDStreamingMode, - flagRequests: Int, - streamCreated: Bool, - streamOpened: Bool? = nil, - streamClosed: Bool? = nil) -> ToMatchResult { - var messages = [String]() - - // synchronizer state - if flagSynchronizer.isOnline != isOnline { - messages.append("isOnline equals \(flagSynchronizer.isOnline)") - } - if flagSynchronizer.streamingMode != streamingMode { - messages.append("streamingMode equals \(flagSynchronizer.streamingMode)") - } - if flagSynchronizer.streamingActive != isStreamingActive(online: isOnline, streamingMode: streamingMode) { - messages.append("streamingActive equals \(flagSynchronizer.streamingActive)") - } - if flagSynchronizer.pollingActive != isPollingActive(online: isOnline, streamingMode: streamingMode) { - messages.append("pollingActive equals \(flagSynchronizer.pollingActive)") - } - - // flag requests - if serviceMock.getFeatureFlagsCallCount != flagRequests { - messages.append("flag requests equals \(serviceMock.getFeatureFlagsCallCount)") - } - - messages.append(contentsOf: eventSourceStateVerificationMessages(streamCreated: streamCreated, streamOpened: streamOpened, streamClosed: streamClosed)) - - return messages.isEmpty ? .matched : .failed(reason: messages.joined(separator: ", ")) - } - - private func eventSourceStateVerificationMessages(streamCreated: Bool, streamOpened: Bool? = nil, streamClosed: Bool? = nil) -> [String] { - var messages = [String]() - - let expectedStreamCreate = streamCreated ? 1 : 0 - if serviceMock.createEventSourceCallCount != expectedStreamCreate { - messages.append("stream create call count equals \(serviceMock.createEventSourceCallCount), expected \(expectedStreamCreate)") - } - - if let streamOpened = streamOpened { - let expectedStreamOpened = streamOpened ? 1 : 0 - if eventSourceMock?.startCallCount != expectedStreamOpened { - messages.append("stream start call count equals \(String(describing: eventSourceMock?.startCallCount)), expected \(expectedStreamOpened)") - } - } - - if let streamClosed = streamClosed { - if eventSourceMock?.stopCallCount != (streamClosed ? 1 : 0) { - messages.append("stream closed call count equals \(eventSourceMock?.stopCallCount ?? 0), expected \(streamClosed ? 1 : 0)") - } - } - - return messages - } } override func spec() { @@ -114,54 +43,49 @@ final class FlagSynchronizerSpec: QuickSpec { func initSpec() { describe("init") { - var testContext: TestContext! + it("starts up streaming offline using get flag requests") { + let testContext = TestContext(streamingMode: .streaming, useReport: false) - beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: false) + expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval + expect(testContext.flagSynchronizer.useReport) == false + + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } - context("streaming mode") { - context("get flag requests") { - it("starts up streaming offline using get flag requests") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .streaming, flagRequests: 0, streamCreated: false) }).to(match()) - expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval - expect(testContext.flagSynchronizer.useReport) == false - } - } - context("report flag requests") { - beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: true) - } - it("starts up streaming offline using report flag requests") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .streaming, flagRequests: 0, streamCreated: false) }).to(match()) - expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval - expect(testContext.flagSynchronizer.useReport) == true - } - } + it("starts up streaming offline using report flag requests") { + let testContext = TestContext(streamingMode: .streaming, useReport: true) + + expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval + expect(testContext.flagSynchronizer.useReport) == true + + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } - context("polling mode") { - afterEach { - testContext.flagSynchronizer.isOnline = false - } - context("get flag requests") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: false) - } - it("starts up polling offline using get flag requests") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .polling, flagRequests: 0, streamCreated: false) }).to(match()) - expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval - expect(testContext.flagSynchronizer.useReport) == false - } - } - context("report flag requests") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: true) - } - it("starts up polling offline using report flag requests") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .polling, flagRequests: 0, streamCreated: false) }).to(match()) - expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval - expect(testContext.flagSynchronizer.useReport) == true - } - } + it("starts up polling offline using get flag requests") { + let testContext = TestContext(streamingMode: .polling, useReport: false) + + expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval + expect(testContext.flagSynchronizer.useReport) == false + + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 + } + it("starts up polling offline using report flag requests") { + let testContext = TestContext(streamingMode: .polling, useReport: true) + + expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval + expect(testContext.flagSynchronizer.useReport) == true + + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } } } @@ -174,117 +98,95 @@ final class FlagSynchronizerSpec: QuickSpec { testContext = TestContext(streamingMode: .streaming, useReport: false) } context("online to offline") { - context("streaming") { - beforeEach { - testContext.flagSynchronizer.isOnline = true + it("stops streaming") { + testContext.flagSynchronizer.isOnline = true + testContext.flagSynchronizer.isOnline = false - testContext.flagSynchronizer.isOnline = false - } - it("stops streaming") { - expect({ testContext.synchronizerState(synchronizerOnline: false, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: true) }).to(match()) - } + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 1 } - context("polling") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: false) - testContext.flagSynchronizer.isOnline = true + it("stops polling") { + testContext = TestContext(streamingMode: .polling, useReport: false) + testContext.flagSynchronizer.isOnline = true + testContext.flagSynchronizer.isOnline = false - testContext.flagSynchronizer.isOnline = false - } - it("stops polling") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .polling, flagRequests: 1, streamCreated: false) }).to(match()) - } + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } } context("offline to online") { - context("streaming") { - beforeEach { - testContext.flagSynchronizer.isOnline = true - } - it("starts streaming") { - // streaming expects a ping on successful connection that triggers a flag request. No ping means no flag requests - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - expect(testContext.serviceMock.createEventSourceCallCount) == 1 - expect(testContext.eventSourceMock!.startCallCount) == 1 - } + it("starts streaming") { + testContext.flagSynchronizer.isOnline = true + + // streaming expects a ping on successful connection that triggers a flag request. No ping means no flag requests + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 } - context("polling") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: false) + it("starts polling") { + testContext = TestContext(streamingMode: .polling, useReport: false) + testContext.flagSynchronizer.isOnline = true - testContext.flagSynchronizer.isOnline = true - } - afterEach { - testContext.flagSynchronizer.isOnline = false - } - it("starts polling") { - // polling starts by requesting flags - expect({ testContext.synchronizerState(synchronizerOnline: true, streamingMode: .polling, flagRequests: 1, streamCreated: false) }).to(match()) - } + // polling starts by requesting flags + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 + + testContext.flagSynchronizer.isOnline = false } } context("online to online") { - context("streaming") { - beforeEach { - testContext.flagSynchronizer.isOnline = true + it("does not stop streaming") { + testContext.flagSynchronizer.isOnline = true + testContext.flagSynchronizer.isOnline = true - testContext.flagSynchronizer.isOnline = true - } - it("does not stop streaming") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - expect(testContext.serviceMock.createEventSourceCallCount) == 1 - expect(testContext.eventSourceMock!.startCallCount) == 1 - } + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 } - context("polling") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: false) - testContext.flagSynchronizer.isOnline = true + it("does not stop polling") { + testContext = TestContext(streamingMode: .polling, useReport: false) + testContext.flagSynchronizer.isOnline = true + testContext.flagSynchronizer.isOnline = true - testContext.flagSynchronizer.isOnline = true - } - afterEach { - testContext.flagSynchronizer.isOnline = false - } - it("does not stop polling") { - // setting the same value shouldn't make another flag request - expect({ testContext.synchronizerState(synchronizerOnline: true, streamingMode: .polling, flagRequests: 1, streamCreated: false) }).to(match()) - } + // setting the same value shouldn't make another flag request + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 + + testContext.flagSynchronizer.isOnline = false } } context("offline to offline") { - context("streaming") { - beforeEach { - testContext.flagSynchronizer.isOnline = false - } - it("does not start streaming") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .streaming, flagRequests: 0, streamCreated: false) }).to(match()) - } + it("does not start streaming") { + testContext.flagSynchronizer.isOnline = false + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } - context("polling") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: false) + it("does not start polling") { + testContext = TestContext(streamingMode: .polling, useReport: false) + testContext.flagSynchronizer.isOnline = false - testContext.flagSynchronizer.isOnline = false - } - it("does not start polling") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .polling, flagRequests: 0, streamCreated: false) }).to(match()) - } + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } } } @@ -303,92 +205,84 @@ final class FlagSynchronizerSpec: QuickSpec { func streamingPingEventSpec() { var testContext: TestContext! - - beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: false) - } - context("ping") { context("success") { - var newFlags: [String: FeatureFlag]? - var streamingEvent: FlagUpdateType? - beforeEach { + var syncResult: FlagSyncResult? + it("requests flags and calls onSyncComplete with the new flags and streaming event") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in - if case .success(let flags, let streamEvent) = result { - (newFlags, streamingEvent) = (flags.flagCollection, streamEvent) - } + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in + syncResult = result done() - }) + } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags and calls onSyncComplete with the new flags and streaming event") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 1, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - expect(newFlags) == DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false) - expect(streamingEvent) == .ping + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + + guard case let .flagCollection(flagCollection) = syncResult + else { return fail("Expected flag collection sync result") } + expect(flagCollection.flags) == DarklyServiceMock.Constants.stubFeatureFlags() } } context("bad data") { var synchronizingError: SynchronizingError? - beforeEach { + it("requests flags and calls onSyncComplete with a data error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case let .error(syncError) = result { synchronizingError = syncError } done() - }) + } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok, badData: true) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags and calls onSyncComplete with a data error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 1, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + guard case .data(DarklyServiceMock.Constants.errorData) = synchronizingError else { - fail("Unexpected error for bad data: \(String(describing: synchronizingError))") - return + return fail("Unexpected error for bad data: \(String(describing: synchronizingError))") } } } context("failure response") { var urlResponse: URLResponse? - beforeEach { + it("requests flags and calls onSyncComplete with a response error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case let .error(syncError) = result, case .response(let syncErrorResponse) = syncError { urlResponse = syncErrorResponse } done() - }) + } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.internalServerError, responseOnly: true) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags and calls onSyncComplete with a response error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 1, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(urlResponse).toNot(beNil()) if let urlResponse = urlResponse { expect(urlResponse.httpStatusCode) == HTTPURLResponse.StatusCodes.internalServerError @@ -397,32 +291,31 @@ final class FlagSynchronizerSpec: QuickSpec { } context("failure error") { var synchronizingError: SynchronizingError? - beforeEach { + it("requests flags and calls onSyncComplete with a request error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case let .error(syncError) = result { synchronizingError = syncError } done() - }) + } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.internalServerError, errorOnly: true) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags and calls onSyncComplete with a request error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 1, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + guard case let .request(error) = synchronizingError, DarklyServiceMock.Constants.error == error as NSError else { - fail("Unexpected error for failure: \(String(describing: synchronizingError))") - return + return fail("Unexpected error for failure: \(String(describing: synchronizingError))") } } } @@ -431,65 +324,54 @@ final class FlagSynchronizerSpec: QuickSpec { func streamingPutEventSpec() { var testContext: TestContext! - - beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: false) - } - + var syncResult: FlagSyncResult? context("put") { context("success") { - var newFlags: [String: FeatureFlag]? - var streamingEvent: FlagUpdateType? - beforeEach { + it("does not request flags and calls onSyncComplete with new flags and put event type") { + let putData = "{\"flagKey\": {\"value\": 123}}" waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in - if case .success(let flags, let streamEvent) = result { - (newFlags, streamingEvent) = (flags.flagCollection, streamEvent) - } + testContext = TestContext(streamingMode: .streaming, useReport: false) { + syncResult = $0 done() - }) + } testContext.flagSynchronizer.isOnline = true - - testContext.providedEventHandler!.sendPut() + testContext.providedEventHandler!.send(event: "put", string: putData) } - } - it("does not request flags and calls onSyncComplete with new flags and put event type") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - expect(newFlags) == DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false) - expect(streamingEvent) == .put + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + + guard case let .flagCollection(flagCollection) = syncResult + else { return fail("Expected flag collection sync result") } + expect(flagCollection.flags.count) == 1 + expect(flagCollection.flags["flagKey"]) == FeatureFlag(flagKey: "flagKey", value: 123) } } context("bad data") { - var syncError: SynchronizingError? - beforeEach { + it("does not request flags and calls onSyncComplete with a data error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in - if case .error(let error) = result { - syncError = error - } + testContext = TestContext(streamingMode: .streaming, useReport: false) { + syncResult = $0 done() - }) + } testContext.flagSynchronizer.isOnline = true - - testContext.providedEventHandler!.send(event: .put, string: DarklyServiceMock.Constants.jsonErrorString) + testContext.providedEventHandler!.send(event: "put", string: DarklyServiceMock.Constants.jsonErrorString) } - } - it("does not request flags and calls onSyncComplete with a data error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - guard case .data(DarklyServiceMock.Constants.errorData) = syncError + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + + guard case .error(.data(DarklyServiceMock.Constants.errorData)) = syncResult else { - fail("Unexpected error for bad data: \(String(describing: syncError))") - return + return fail("Unexpected error for bad data: \(String(describing: syncResult))") } } } @@ -498,69 +380,56 @@ final class FlagSynchronizerSpec: QuickSpec { func streamingPatchEventSpec() { var testContext: TestContext! - - beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: false) - } - + var syncResult: FlagSyncResult? context("patch") { context("success") { - var flagDictionary: [String: Any]? - var streamingEvent: FlagUpdateType? - beforeEach { + it("does not request flags and calls onSyncComplete with new flags and put event type") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in - if case .success(let patch, let streamEvent) = result { - (flagDictionary, streamingEvent) = (patch, streamEvent) - } + testContext = TestContext(streamingMode: .streaming, useReport: false) { + syncResult = $0 done() - }) + } testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.sendPatch() + testContext.providedEventHandler!.send(event: "patch", string: "{\"key\": \"abc\"}") } - } - it("does not request flags and calls onSyncComplete with new flags and put event type") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - let stubPatch = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, - value: DarklyServiceMock.FlagValues.int + 1, - variation: DarklyServiceMock.Constants.variation + 1, - version: DarklyServiceMock.Constants.version + 1) - expect(AnyComparer.isEqual(flagDictionary, to: stubPatch)).to(beTrue()) - expect(streamingEvent) == .patch + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + + guard case let .patch(flag) = syncResult + else { return fail("Expected patch sync result") } + expect(flag.flagKey) == "abc" } } context("bad data") { var syncError: SynchronizingError? - beforeEach { + it("does not request flags and calls onSyncComplete with a data error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case .error(let error) = result { syncError = error } done() - }) + } testContext.flagSynchronizer.isOnline = true - - testContext.providedEventHandler!.send(event: .patch, string: DarklyServiceMock.Constants.jsonErrorString) + testContext.providedEventHandler!.send(event: "patch", string: DarklyServiceMock.Constants.jsonErrorString) } - } - it("does not request flags and calls onSyncComplete with a data error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + guard case .data(DarklyServiceMock.Constants.errorData) = syncError else { - fail("Unexpected error for bad data: \(String(describing: syncError))") - return + return fail("Unexpected error for bad data: \(String(describing: syncError))") } } } @@ -576,59 +445,55 @@ final class FlagSynchronizerSpec: QuickSpec { context("delete") { context("success") { - var flagDictionary: [String: Any]? - var streamingEvent: FlagUpdateType? - beforeEach { + var syncResult: FlagSyncResult? + it("does not request flags and calls onSyncComplete with new flags and put event type") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in - if case .success(let delete, let streamEvent) = result { - (flagDictionary, streamingEvent) = (delete, streamEvent) - } + testContext = TestContext(streamingMode: .streaming, useReport: false) { + syncResult = $0 done() - }) + } testContext.flagSynchronizer.isOnline = true - - testContext.providedEventHandler!.sendDelete() + let deleteData = "{\"key\": \"\(DarklyServiceMock.FlagKeys.int)\", \"version\": \(DarklyServiceMock.Constants.version + 1)}" + testContext.providedEventHandler!.send(event: "delete", string: deleteData) } - } - it("does not request flags and calls onSyncComplete with new flags and put event type") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - let stubDelete = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) - expect(AnyComparer.isEqual(flagDictionary, to: stubDelete)).to(beTrue()) - expect(streamingEvent) == .delete + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + + guard case let .delete(deleteResponse) = syncResult + else { return fail("expected delete dictionary sync result") } + expect(deleteResponse.key) == DarklyServiceMock.FlagKeys.int + expect(deleteResponse.version) == DarklyServiceMock.Constants.version + 1 } } context("bad data") { var syncError: SynchronizingError? - beforeEach { + it("does not request flags and calls onSyncComplete with a data error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case .error(let error) = result { syncError = error } done() - }) + } testContext.flagSynchronizer.isOnline = true - - testContext.providedEventHandler!.send(event: .delete, string: DarklyServiceMock.Constants.jsonErrorString) + testContext.providedEventHandler!.send(event: "delete", string: DarklyServiceMock.Constants.jsonErrorString) } - } - it("does not request flags and calls onSyncComplete with a data error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + guard case .data(DarklyServiceMock.Constants.errorData) = syncError else { - fail("Unexpected error for bad data: \(String(describing: syncError))") - return + return fail("Unexpected error for bad data: \(String(describing: syncError))") } } } @@ -642,37 +507,36 @@ final class FlagSynchronizerSpec: QuickSpec { var testContext: TestContext! beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { _ in + testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in testContext.onSyncCompleteCallCount += 1 - }) + } } context("error event") { beforeEach { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { syncResult in + testContext = TestContext(streamingMode: .streaming, useReport: false) { syncResult in if case .error(let errorResult) = syncResult { syncError = errorResult } done() - }) + } testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.sendServerError() } } it("does not request flags & reports the error via onSyncComplete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(syncError).toNot(beNil()) expect(syncError?.isClientUnauthorized).to(beFalse()) guard case .streamError = syncError else { - fail("Expected stream error") - return + return fail("Expected stream error") } } it("does not record stream init diagnostic") { @@ -684,29 +548,29 @@ final class FlagSynchronizerSpec: QuickSpec { var returnedAction: ConnectionErrorAction! beforeEach { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { syncResult in + testContext = TestContext(streamingMode: .streaming, useReport: false) { syncResult in if case .error(let errorResult) = syncResult { syncError = errorResult } done() - }) + } testContext.flagSynchronizer.isOnline = true returnedAction = testContext.providedErrorHandler!(UnsuccessfulResponseError(responseCode: 418)) } } it("does not request flags & reports the error via onSyncComplete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(syncError).toNot(beNil()) expect(syncError?.isClientUnauthorized).to(beFalse()) guard case .streamError = syncError else { - fail("Expected stream error") - return + return fail("Expected stream error") } } it("does not record stream init diagnostic") { @@ -726,30 +590,29 @@ final class FlagSynchronizerSpec: QuickSpec { context("unauthorized error event") { beforeEach { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { syncResult in + testContext = TestContext(streamingMode: .streaming, useReport: false) { syncResult in if case .error(let errorResult) = syncResult { syncError = errorResult } done() - }) + } testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.sendUnauthorizedError() } } it("does not request flags & reports the error via onSyncComplete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(syncError).toNot(beNil()) expect(syncError?.isClientUnauthorized).to(beTrue()) guard case .streamError = syncError else { - fail("Expected stream error") - return + return fail("Expected stream error") } } it("does not record stream init diagnostic") { @@ -760,16 +623,16 @@ final class FlagSynchronizerSpec: QuickSpec { context("heartbeat") { beforeEach { testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.onComment(comment: "") } it("does not request flags or report sync complete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(testContext.onSyncCompleteCallCount) == 0 } it("does not record stream init diagnostic") { @@ -780,16 +643,16 @@ final class FlagSynchronizerSpec: QuickSpec { context("comment") { beforeEach { testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.onComment(comment: "foo") } it("does not request flags or report sync complete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(testContext.onSyncCompleteCallCount) == 0 } it("does not record stream init diagnostic") { @@ -800,16 +663,16 @@ final class FlagSynchronizerSpec: QuickSpec { context("open event") { beforeEach { testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.onOpened() } it("does not request flags or report sync complete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(testContext.onSyncCompleteCallCount) == 0 } it("records stream init diagnostic") { @@ -828,16 +691,16 @@ final class FlagSynchronizerSpec: QuickSpec { context("closed event") { beforeEach { testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.onClosed() } it("does not request flags") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(testContext.onSyncCompleteCallCount) == 0 } it("does not record stream init diagnostic") { @@ -849,30 +712,29 @@ final class FlagSynchronizerSpec: QuickSpec { var syncErrorEvent: SynchronizingError? beforeEach { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { syncResult in + testContext = TestContext(streamingMode: .streaming, useReport: false) { syncResult in if case .error(let errorResult) = syncResult { syncErrorEvent = errorResult } done() - }) + } testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.sendNonResponseError() } } it("does not request flags & reports the error via onSyncComplete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(syncErrorEvent).toNot(beNil()) expect(syncErrorEvent?.isClientUnauthorized).to(beFalse()) guard case .streamError = syncErrorEvent else { - fail("Expected stream error") - return + return fail("Expected stream error") } } it("does not record stream init diagnostic") { @@ -887,44 +749,30 @@ final class FlagSynchronizerSpec: QuickSpec { var testContext: TestContext! var syncError: SynchronizingError? + let data = "{\"flag1\": {}}" + context("event reported while offline") { - beforeEach { - let data = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, - includeVariations: true, - includeVersions: true) - .dictionaryValue - .jsonString! + it("reports offline") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case .error(let errorResult) = result { syncError = errorResult } done() - }) - - testContext.flagSynchronizer.testStreamOnMessage(event: FlagUpdateType.put.rawValue, - messageEvent: MessageEvent(data: data)) + } + testContext.flagSynchronizer.testStreamOnMessage(event: "put", messageEvent: MessageEvent(data: data)) } - } - it("reports offline") { + guard case .isOffline = syncError else { - fail("Expected syncError to be .isOffline, was: \(String(describing: syncError))") - return + return fail("Expected syncError to be .isOffline, was: \(String(describing: syncError))") } } } context("event reported while polling") { - beforeEach { - let data = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, - includeVariations: true, - includeVersions: true) - .dictionaryValue - .jsonString! + it("reports an event error") { waitUntil { done in - testContext = TestContext(streamingMode: .polling, useReport: false, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .polling, useReport: false) { _ in done() } testContext.flagSynchronizer.isOnline = true } waitUntil { done in @@ -935,57 +783,44 @@ final class FlagSynchronizerSpec: QuickSpec { done() } - testContext.flagSynchronizer.testStreamOnMessage(event: FlagUpdateType.put.rawValue, - messageEvent: MessageEvent(data: data)) + testContext.flagSynchronizer.testStreamOnMessage(event: "put", messageEvent: MessageEvent(data: data)) } - } - afterEach { - testContext.flagSynchronizer.isOnline = false - } - it("reports an event error") { + guard case .streamEventWhilePolling = syncError else { - fail("Expected syncError to be .streamEventWhilePolling, was: \(String(describing: syncError))") - return + return fail("Expected syncError to be .streamEventWhilePolling, was: \(String(describing: syncError))") } + + testContext.flagSynchronizer.isOnline = false } } context("event reported while streaming inactive") { - beforeEach { - let data = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, - includeVariations: true, - includeVersions: true) - .dictionaryValue - .jsonString! + it("reports offline") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case .error(let errorResult) = result { syncError = errorResult } done() - }) + } testContext.flagSynchronizer.isOnline = true testContext.flagSynchronizer.testEventSource = nil - testContext.flagSynchronizer.testStreamOnMessage(event: FlagUpdateType.put.rawValue, - messageEvent: MessageEvent(data: data)) + testContext.flagSynchronizer.testStreamOnMessage(event: "put", messageEvent: MessageEvent(data: data)) } - } - it("reports offline") { + guard case .isOffline = syncError else { - fail("Expected syncError to be .isOffline, was: \(String(describing: syncError))") - return + return fail("Expected syncError to be .isOffline, was: \(String(describing: syncError))") } } } } func pollingTimerFiresSpec() { + var syncResult: FlagSyncResult? describe("polling timer fires") { context("one second interval") { var testContext: TestContext! - var newFlags: [String: FeatureFlag]? - var streamingEvent: FlagUpdateType? beforeEach { testContext = TestContext(streamingMode: .polling, useReport: false) testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) @@ -994,9 +829,7 @@ final class FlagSynchronizerSpec: QuickSpec { waitUntil(timeout: .seconds(2)) { done in // In polling mode, the flagSynchronizer makes a flag request when set online right away. To verify the timer this test waits the polling interval (1s) for a second flag request testContext.flagSynchronizer.onSyncComplete = { result in - if case .success(let flags, let streamEvent) = result { - (newFlags, streamingEvent) = (flags.flagCollection, streamEvent) - } + syncResult = result done() } } @@ -1006,8 +839,9 @@ final class FlagSynchronizerSpec: QuickSpec { } it("makes a flag request and calls onSyncComplete with no streaming event") { expect(testContext.serviceMock.getFeatureFlagsCallCount) == 2 - expect(newFlags == DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, includeVariations: true, includeVersions: true)).to(beTrue()) - expect(streamingEvent).to(beNil()) + guard case let .flagCollection(flagCollection) = syncResult + else { return fail("Expected flag collection sync result") } + expect(flagCollection.flags) == DarklyServiceMock.Constants.stubFeatureFlags() } // This particular test causes a retain cycle between the FlagSynchronizer and something else. By removing onSyncComplete, the closure is no longer called after the test is complete. afterEach { @@ -1022,18 +856,15 @@ final class FlagSynchronizerSpec: QuickSpec { var testContext: TestContext! context("using get method") { context("success") { - beforeEach { + it("requests flags using a get request exactly one time") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags using a get request exactly one time") { + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 expect(testContext.serviceMock.getFeatureFlagsUseReportCalledValue.first) == false } @@ -1043,9 +874,7 @@ final class FlagSynchronizerSpec: QuickSpec { it("requests flags using a get request exactly one time") { for statusCode in HTTPURLResponse.StatusCodes.nonRetry { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: statusCode) testContext.flagSynchronizer.isOnline = true @@ -1061,9 +890,7 @@ final class FlagSynchronizerSpec: QuickSpec { it("requests flags using a get request exactly one time") { for statusCode in HTTPURLResponse.StatusCodes.retry { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: statusCode) testContext.flagSynchronizer.isOnline = true @@ -1079,18 +906,15 @@ final class FlagSynchronizerSpec: QuickSpec { } context("using report method") { context("success") { - beforeEach { + it("requests flags using a get request exactly one time") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: true, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: true) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags using a get request exactly one time") { + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 expect(testContext.serviceMock.getFeatureFlagsUseReportCalledValue.first) == true } @@ -1100,9 +924,7 @@ final class FlagSynchronizerSpec: QuickSpec { it("requests flags using a get request exactly one time") { for statusCode in HTTPURLResponse.StatusCodes.nonRetry { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: true, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: true) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: statusCode) testContext.flagSynchronizer.isOnline = true @@ -1118,9 +940,7 @@ final class FlagSynchronizerSpec: QuickSpec { it("requests flags using a report request exactly one time, followed by a get request exactly one time") { for statusCode in HTTPURLResponse.StatusCodes.retry { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: true, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: true) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: statusCode) testContext.flagSynchronizer.isOnline = true @@ -1141,28 +961,36 @@ final class FlagSynchronizerSpec: QuickSpec { // This test completes the test suite on makeFlagRequest by validating the method bails out if it's called and the synchronizer is offline. While that shouldn't happen, there are 2 code paths that don't directly verify the SDK is online before calling the method, so it seems a wise precaution to validate that the method does bailout. Other tests exercise the rest of the method. context("offline") { var synchronizingError: SynchronizingError? - beforeEach { + it("does not request flags and calls onSyncComplete with an isOffline error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case .error(let syncError) = result { synchronizingError = syncError } done() - }) + } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) testContext.flagSynchronizer.testMakeFlagRequest() } - } - it("does not request flags and calls onSyncComplete with an isOffline error") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .streaming, flagRequests: 0, streamCreated: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 + guard case .isOffline = synchronizingError else { - fail("Expected syncError to be .isOffline, was: \(String(describing: synchronizingError))") - return + return fail("Expected syncError to be .isOffline, was: \(String(describing: synchronizingError))") } } } } } } + +extension DeleteResponse: Equatable { + public static func == (lhs: DeleteResponse, rhs: DeleteResponse) -> Bool { + lhs.key == rhs.key && lhs.version == rhs.version + } +} diff --git a/SourceryTemplates/mocks.stencil b/SourceryTemplates/mocks.stencil index 4ba5443f..b03faefb 100644 --- a/SourceryTemplates/mocks.stencil +++ b/SourceryTemplates/mocks.stencil @@ -12,18 +12,18 @@ final class {{ type.name }}Mock: {{ type.name }} { {% for variable in type.allVariables|!annotated:"noMock" %} var {{ variable.name }}SetCount = 0 - var set{{ variable.name|upperFirstLetter }}Callback: (() -> Void)? + var set{{ variable.name|upperFirstLetter }}Callback: (() throws -> Void)? var {{ variable.name }}: {{ variable.typeName }}{% if variable|annotated:"defaultMockValue" %} = {{ variable.annotations.defaultMockValue }}{% elif variable.isOptional %} = nil{% elif variable.isArray %} = []{% elif variable.isDictionary %} = [:]{% else %} // You must annotate mocked variables that are not optional, arrays, or dictionaries, using a comment: //sourcery: defaultMockValue = {% endif %} { didSet { {{ variable.name }}SetCount += 1 - set{{ variable.name|upperFirstLetter }}Callback?() + try! set{{ variable.name|upperFirstLetter }}Callback?() } } {% endfor %} {% for method in type.allMethods|!annotated:"noMock" %} var {{ method.callName }}CallCount = 0 - var {{ method.callName }}Callback: (() -> Void)? + var {{ method.callName }}Callback: (() throws -> Void)? {% if method.throws %} var {{ method.callName }}ShouldThrow: Error?{% endif %} {% if method.parameters.count == 1 %} var {{ method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {{ param.typeName.unwrappedTypeName }}?{% endfor %} {% else %}{% if not method.parameters.count == 0 %} var {{ method.callName }}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% if param.typeAttributes.escaping %}{{ param.unwrappedTypeName }}{% else %}{{ param.typeName }}{% endif %}{% if not forloop.last %}, {% endif %}{% endfor %})?{% endif %} @@ -33,7 +33,7 @@ final class {{ type.name }}Mock: {{ type.name }} { {{ method.callName }}CallCount += 1 {%if method.parameters.count == 1 %} {{ method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %}{% else %}{% if not method.parameters.count == 0 %} {{ method.callName }}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}){% endif %}{% if not method.returnTypeName.isVoid %}{% endif %}{% endif %} {% if method.throws %} if let {{ method.callName }}ShouldThrow = {{ method.callName }}ShouldThrow { throw {{ method.callName }}ShouldThrow }{% endif %} - {{ method.callName }}Callback?() + try! {{ method.callName }}Callback?() {% if not method.returnTypeName.isVoid %} return {{ method.callName }}ReturnValue{% endif %} } From 979bf7f5b40e563e5c1d393d831de5640caa29d1 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Fri, 22 Apr 2022 14:12:34 -0400 Subject: [PATCH 45/50] Update LDUser to use default equatable instance rather than custom one that only compares keys. (#194) --- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 91ec678b..683935d1 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -7,7 +7,7 @@ typealias UserKey = String // use for identifying semantics for strings, partic The SDK caches last known feature flags for use on app startup to provide continuity with the last app run. Provided the LDClient is online and can establish a connection with LaunchDarkly servers, cached information will only be used a very short time. Once the latest feature flags arrive at the SDK, the SDK no longer uses cached feature flags. The SDK retains feature flags on the last 5 client defined users. The SDK will retain feature flags until they are overwritten by a different user's feature flags, or until the user removes the app from the device. The SDK does not cache user information collected, except for the user key. The user key is used to identify the cached feature flags for that user. Client app developers should use caution not to use sensitive user information as the user-key. */ -public struct LDUser: Encodable { +public struct LDUser: Encodable, Equatable { /// String keys associated with LDUser properties. public enum CodingKeys: String, CodingKey { @@ -179,13 +179,6 @@ public struct LDUser: Encodable { } } -extension LDUser: Equatable { - /// Compares users by comparing their user keys only, to allow the client app to collect user information over time - public static func == (lhs: LDUser, rhs: LDUser) -> Bool { - lhs.key == rhs.key - } -} - /// Class providing ObjC interoperability with the LDUser struct @objc final class LDUserWrapper: NSObject { let wrapped: LDUser From ade94992f84391e6e0452215df0f1d8061ce0110 Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Mon, 2 May 2022 14:42:34 -0400 Subject: [PATCH 46/50] (V6) Objective-C bridging (#196) --- .jazzy.yaml | 17 +- LaunchDarkly.xcodeproj/project.pbxproj | 10 + LaunchDarkly/LaunchDarkly/LDClient.swift | 75 ++-- .../LaunchDarkly/LDClientVariation.swift | 35 ++ LaunchDarkly/LaunchDarkly/LDCommon.swift | 65 +-- .../FlagChange/LDChangedFlag.swift | 3 +- .../FeatureFlag/LDEvaluationDetail.swift | 6 +- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 38 +- .../LaunchDarkly/Models/UserAttribute.swift | 35 +- .../ObjectiveC/ObjcLDChangedFlag.swift | 175 +-------- .../ObjectiveC/ObjcLDClient.swift | 370 ++++-------------- .../ObjectiveC/ObjcLDConfig.swift | 2 +- .../ObjectiveC/ObjcLDEvaluationDetail.swift | 59 +-- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 47 +-- .../LaunchDarkly/ObjectiveC/ObjcLDValue.swift | 132 +++++++ .../ServiceObjects/Cache/CacheConverter.swift | 15 +- .../Cache/FeatureFlagCache.swift | 1 + .../ServiceObjects/EnvironmentReporter.swift | 4 - .../ServiceObjects/EventReporter.swift | 29 +- .../LaunchDarklyTests/LDClientSpec.swift | 20 +- .../Mocks/DarklyServiceMock.swift | 20 +- .../LaunchDarklyTests/Mocks/LDUserStub.swift | 4 +- .../LaunchDarklyTests/Models/EventSpec.swift | 22 +- .../FlagRequestTracking/FlagCounterSpec.swift | 2 +- .../Models/User/LDUserSpec.swift | 8 +- .../EnvironmentReporterSpec.swift | 22 +- .../ServiceObjects/ThrottlerSpec.swift | 4 +- 27 files changed, 470 insertions(+), 750 deletions(-) create mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift diff --git a/.jazzy.yaml b/.jazzy.yaml index 4b73ab27..1c74e034 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -19,6 +19,7 @@ custom_categories: - LDConfig - LDUser - LDEvaluationDetail + - LDValue - name: Flag Change Observers children: @@ -36,10 +37,8 @@ custom_categories: - name: Other Types children: - LDStreamingMode - - LDFlagValueConvertible - LDFlagKey - LDInvalidArgumentError - - LDErrorHandler - name: Objective-C Core Interfaces children: @@ -47,6 +46,8 @@ custom_categories: - ObjcLDConfig - ObjcLDUser - ObjcLDChangedFlag + - ObjcLDValue + - ObjcLDValueType - name: Objective-C EvaluationDetail Wrappers children: @@ -54,14 +55,4 @@ custom_categories: - ObjcLDIntegerEvaluationDetail - ObjcLDDoubleEvaluationDetail - ObjcLDStringEvaluationDetail - - ObjcLDArrayEvaluationDetail - - ObjcLDDictionaryEvaluationDetail - - - name: Objective-C ChangedFlag Wrappers - children: - - ObjcLDBoolChangedFlag - - ObjcLDIntegerChangedFlag - - ObjcLDDoubleChangedFlag - - ObjcLDStringChangedFlag - - ObjcLDArrayChangedFlag - - ObjcLDDictionaryChangedFlag + - ObjcLDJSONEvaluationDetail diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 6adc8c12..b77d3c3a 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -11,6 +11,10 @@ 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; 29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; 29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; + 29F9D19E2812E005008D12C0 /* ObjcLDValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */; }; + 29F9D19F2812E005008D12C0 /* ObjcLDValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */; }; + 29F9D1A02812E005008D12C0 /* ObjcLDValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */; }; + 29F9D1A12812E005008D12C0 /* ObjcLDValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */; }; 29FE1298280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; 29FE1299280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; 29FE129A280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; @@ -311,6 +315,7 @@ /* Begin PBXFileReference section */ 29A4C47427DA6266005B8D34 /* UserAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAttribute.swift; sourceTree = ""; }; + 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDValue.swift; sourceTree = ""; }; 29FE1297280413D4008CC918 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLRequest.swift; sourceTree = ""; }; 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersSpec.swift; sourceTree = ""; }; @@ -610,6 +615,7 @@ 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */, 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */, B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */, + 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */, ); path = ObjectiveC; sourceTree = ""; @@ -1114,6 +1120,7 @@ 831188652113AE4600D77CB5 /* Date.swift in Sources */, 831188672113AE4D00D77CB5 /* Thread.swift in Sources */, C443A40823145FEE00145710 /* ConnectionInformationStore.swift in Sources */, + 29F9D1A12812E005008D12C0 /* ObjcLDValue.swift in Sources */, 8311885C2113AE2200D77CB5 /* HTTPHeaders.swift in Sources */, 831188562113AE0800D77CB5 /* FlagSynchronizer.swift in Sources */, 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */, @@ -1179,6 +1186,7 @@ 29FE129A280413D4008CC918 /* Util.swift in Sources */, 831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */, 835E4C54206BDF8D004C6E6C /* EnvironmentReporter.swift in Sources */, + 29F9D1A02812E005008D12C0 /* ObjcLDValue.swift in Sources */, 8372668E20D4439600BD1088 /* DateFormatter.swift in Sources */, C43C37E7238DF22C003C1624 /* LDEvaluationDetail.swift in Sources */, 831EF36020655E730001C643 /* Data.swift in Sources */, @@ -1223,6 +1231,7 @@ C443A40F23186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, C443A40A2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831D8B741F72994600ED65E8 /* FlagStore.swift in Sources */, + 29F9D19E2812E005008D12C0 /* ObjcLDValue.swift in Sources */, 8358F2601F476AD800ECE1AF /* FlagChangeNotifier.swift in Sources */, 835E1D411F63450A00184DB4 /* ObjcLDUser.swift in Sources */, 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */, @@ -1320,6 +1329,7 @@ C443A40B2315AA4D00145710 /* NetworkReporter.swift in Sources */, 83D9EC892062DEAB004D7FA6 /* EventReporter.swift in Sources */, 83D9EC8A2062DEAB004D7FA6 /* FlagStore.swift in Sources */, + 29F9D19F2812E005008D12C0 /* ObjcLDValue.swift in Sources */, 83D9EC8B2062DEAB004D7FA6 /* Log.swift in Sources */, 83D9EC8C2062DEAB004D7FA6 /* HTTPHeaders.swift in Sources */, C443A40623145FED00145710 /* ConnectionInformationStore.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index a0ab4ff3..dab69a55 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -17,22 +17,22 @@ enum LDClientRunMode { ### Getting Feature Flags Once the LDClient has started, it makes your feature flags available using the `variation` and `variationDetail` methods. A `variation` is a specific flag value. For example a boolean feature flag has 2 variations, `true` and `false`. You can create feature flags with more than 2 variations using other feature flag types. - ```` - let boolFlag = LDClient.get()?.variation(forKey: "my-bool-flag", defaultValue: false) - ```` + ``` + let boolFlag = LDClient.get()?.boolVariation(forKey: "my-bool-flag", defaultValue: false) + ``` If you need to know more information about why a given value is returned, use `variationDetail`. - See `variation(forKey: defaultValue:)` or `variationDetail(forKey: defaultValue:)` for details + See `boolVariation(forKey: defaultValue:)` or `boolVariationDetail(forKey: defaultValue:)` for details ### Observing Feature Flags You might need to know when a feature flag value changes. This is not required, you can check the flag's value when you need it. If you want to know when a feature flag value changes, you can check the flag's value. You can also use one of several `observe` methods to have the LDClient notify you when a change occurs. There are several options--you can set up notificiations based on when a specific flag changes, when any flag in a collection changes, or when a flag doesn't change. - ```` + ``` LDClient.get()?.observe("flag-key", owner: self, observer: { [weak self] (changedFlag) in self?.updateFlag(key: "flag-key", changedFlag: changedFlag) } - ```` + ``` The `changedFlag` passed in to the closure contains the old and new value of the flag. */ public class LDClient { @@ -268,9 +268,9 @@ public class LDClient { Normally, the client app should create and set the LDUser and pass that into `start(config: user: completion:)`. - The client app can change the active `user` by calling identify with a new or updated LDUser. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. If the client app does not create a LDUser, LDClient creates an anonymous default user, which can affect the feature flags delivered to the LDClient. + The client app can change the active `user` by calling identify with a new or updated LDUser. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. - When a new user is set, the LDClient goes offline and sets the new user. If the client was online when the new user was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). To change both the `config` and `user`, set the LDClient offline, set both properties, then set the LDClient online. A completion may be passed to the identify method to allow a client app to know when fresh flag values for the new user are ready. + When a new user is set, the LDClient goes offline and sets the new user. If the client was online when the new user was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). A completion may be passed to the identify method to allow a client app to know when fresh flag values for the new user are ready. - parameter user: The LDUser set with the desired user. - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. (Optional) @@ -318,13 +318,7 @@ public class LDClient { private let internalIdentifyQueue: DispatchQueue = DispatchQueue(label: "InternalIdentifyQueue") - /** - Returns a dictionary with the flag keys and their values. If the LDClient is not started, returns nil. - - The dictionary will not contain feature flags from the server with null values. - - LDClient will not provide any source or change information, only flag keys and flag values. The client app should convert the feature flag value into the desired type. - */ + /// Returns a dictionary with the flag keys and their values. If the `LDClient` is not started, returns `nil`. public var allFlags: [LDFlagKey: LDValue]? { guard hasStarted else { return nil } @@ -340,17 +334,15 @@ public class LDClient { The SDK executes handlers on the main thread. - LDChangedFlag does not know the type of oldValue or newValue. The client app should cast the value into the type needed. See `variation(forKey: defaultValue:)` for details about the SDK and feature flag types. - SeeAlso: `LDChangedFlag` and `stopObserving(owner:)` ### Usage - ```` + ``` LDClient.get()?.observe("flag-key", owner: self) { [weak self] (changedFlag) in - if let newValue = changedFlag.newValue as? Bool { - //do something with the newValue + if let .bool(newValue) = changedFlag.newValue { + // do something with the newValue } - ```` + ``` - parameter key: The LDFlagKey for the flag to observe. - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. @@ -368,19 +360,17 @@ public class LDClient { The SDK executes handlers on the main thread. - LDChangedFlag does not know the type of oldValue or newValue. The client app should cast the value into the type needed. See `variation(forKey: defaultValue:)` for details about the SDK and feature flag types. - SeeAlso: `LDChangedFlag` and `stopObserving(owner:)` ### Usage - ```` + ``` LDClient.get()?.observe(flagKeys, owner: self) { [weak self] (changedFlags) in // changedFlags is a [LDFlagKey: LDChangedFlag] //There will be an LDChangedFlag entry in changedFlags for each changed flag. The closure will only be called once regardless of how many flags changed. if let someChangedFlag = changedFlags["some-flag-key"] { // someChangedFlag is a LDChangedFlag //do something with someChangedFlag } } - ```` + ``` - parameter keys: An array of LDFlagKeys for the flags to observe. - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. @@ -398,19 +388,17 @@ public class LDClient { The SDK executes handlers on the main thread. - LDChangedFlag does not know the type of oldValue or newValue. The client app should cast the value into the type needed. See `variation(forKey: defaultValue:)` for details about the SDK and feature flag types. - SeeAlso: `LDChangedFlag` and `stopObserving(owner:)` ### Usage - ```` + ``` LDClient.get()?.observeAll(owner: self) { [weak self] (changedFlags) in // changedFlags is a [LDFlagKey: LDChangedFlag] //There will be an LDChangedFlag entry in changedFlags for each changed flag. The closure will only be called once regardless of how many flags changed. if let someChangedFlag = changedFlags["some-flag-key"] { // someChangedFlag is a LDChangedFlag //do something with someChangedFlag } } - ```` + ``` - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. - parameter handler: The LDFlagCollectionChangeHandler the SDK will execute 1 time when any of the observed feature flags change. @@ -432,12 +420,12 @@ public class LDClient { SeeAlso: `stopObserving(owner:)` ### Usage - ```` + ``` LDClient.get()?.observeFlagsUnchanged(owner: self) { [weak self] in // Do something after an update was received that did not update any flag values. //The closure will be called once on the main thread after the update. } - ```` + ``` - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. - parameter handler: The LDFlagsUnchangedHandler the SDK will execute 1 time when a flag request completes with no flags changed. @@ -457,11 +445,11 @@ public class LDClient { SeeAlso: `stopObserving(owner:)` ### Usage - ```` + ``` LDClient.get()?.observeCurrentConnectionMode(owner: self) { [weak self] in //do something after ConnectionMode was updated. } - ```` + ``` - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. - parameter handler: The LDConnectionModeChangedHandler the SDK will execute 1 time when ConnectionInformation.currentConnectionMode is changed. @@ -528,18 +516,18 @@ public class LDClient { /** Adds a custom event to the LDClient event store. A client app can set a tracking event to allow client customized data analysis. Once an app has called `track`, the app cannot remove the event from the event store. - LDClient periodically transmits events to LaunchDarkly based on the frequency set in LDConfig.eventFlushInterval. The LDClient must be started and online. Ths SDK stores events tracked while the LDClient is offline, but started. + LDClient periodically transmits events to LaunchDarkly based on the frequency set in `LDConfig.eventFlushInterval`. The LDClient must be started and online. Ths SDK stores events tracked while the LDClient is offline, but started. Once the SDK's event store is full, the SDK discards events until they can be reported to LaunchDarkly. Configure the size of the event store using `eventCapacity` on the `config`. See `LDConfig` for details. ### Usage - ```` - let appEventData = ["some-custom-key: "some-custom-value", "another-custom-key": 7] + ``` + let appEventData: LDValue = ["some-custom-key: "some-custom-value", "another-custom-key": 7] LDClient.get()?.track(key: "app-event-key", data: appEventData) - ```` + ``` - - parameter key: The key for the event. The SDK does nothing with the key, which can be any string the client app sends - - parameter data: The data for the event. The SDK does nothing with the data, which can be any valid JSON item the client app sends. (Optional) + - parameter key: The key for the event. + - parameter data: The data for the event. (Optional) - parameter metricValue: A numeric value used by the LaunchDarkly experimentation feature in numeric custom metrics. Can be omitted if this event is used by only non-numeric metrics. This field will also be returned as part of the custom event for Data Export. (Optional) */ public func track(key: String, data: LDValue? = nil, metricValue: Double? = nil) { @@ -685,11 +673,9 @@ public class LDClient { } } } - DispatchQueue.global().asyncAfter(deadline: .now() + startWaitSeconds) { - internalCompletedQueue.async { - if completed { - completion?(completed) - } + internalCompletedQueue.asyncAfter(deadline: .now() + startWaitSeconds) { + if completed { + completion?(completed) } } } @@ -699,7 +685,6 @@ public class LDClient { Returns the LDClient instance for a given environment. - parameter environment: The name of an environment provided in LDConfig.secondaryMobileKeys, defaults to `LDConfig.Constants.primaryEnvironmentName` which is always associated with the `LDConfig.mobileKey` environment. - - returns: The requested LDClient instance. */ public static func get(environment: String = LDConfig.Constants.primaryEnvironmentName) -> LDClient? { diff --git a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift index b93bc158..d11f3c25 100644 --- a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -2,80 +2,115 @@ import Foundation extension LDClient { /** + Returns the boolean value of a feature flag for a given flag key. + - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: the variation for the selected user, or `defaultValue` if the flag is not available. */ public func boolVariation(forKey flagKey: LDFlagKey, defaultValue: Bool) -> Bool { variationDetailInternal(flagKey, defaultValue, needsReason: false).value } /** + Returns the boolean value of a feature flag for a given flag key, in an object that also describes the way the + value was determined. + - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: an `LDEvaluationDetail` object */ public func boolVariationDetail(forKey flagKey: LDFlagKey, defaultValue: Bool) -> LDEvaluationDetail { variationDetailInternal(flagKey, defaultValue, needsReason: true) } /** + Returns the integer value of a feature flag for a given flag key. + - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: the variation for the selected user, or `defaultValue` if the flag is not available. */ public func intVariation(forKey flagKey: LDFlagKey, defaultValue: Int) -> Int { variationDetailInternal(flagKey, defaultValue, needsReason: false).value } /** + Returns the integer value of a feature flag for a given flag key, in an object that also describes the way the + value was determined. + - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: an `LDEvaluationDetail` object */ public func intVariationDetail(forKey flagKey: LDFlagKey, defaultValue: Int) -> LDEvaluationDetail { variationDetailInternal(flagKey, defaultValue, needsReason: true) } /** + Returns the double-precision floating-point value of a feature flag for a given flag key. + - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: the variation for the selected user, or `defaultValue` if the flag is not available. */ public func doubleVariation(forKey flagKey: LDFlagKey, defaultValue: Double) -> Double { variationDetailInternal(flagKey, defaultValue, needsReason: false).value } /** + Returns the double-precision floating-point value of a feature flag for a given flag key, in an object that also + describes the way the value was determined. + - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: an `LDEvaluationDetail` object */ public func doubleVariationDetail(forKey flagKey: LDFlagKey, defaultValue: Double) -> LDEvaluationDetail { variationDetailInternal(flagKey, defaultValue, needsReason: true) } /** + Returns the string value of a feature flag for a given flag key. + - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: the variation for the selected user, or `defaultValue` if the flag is not available. */ public func stringVariation(forKey flagKey: LDFlagKey, defaultValue: String) -> String { variationDetailInternal(flagKey, defaultValue, needsReason: false).value } /** + Returns the string value of a feature flag for a given flag key, in an object that also describes the way the + value was determined. + - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: an `LDEvaluationDetail` object */ public func stringVariationDetail(forKey flagKey: LDFlagKey, defaultValue: String) -> LDEvaluationDetail { variationDetailInternal(flagKey, defaultValue, needsReason: true) } /** + Returns the JSON value of a feature flag for a given flag key. + - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: the variation for the selected user, or `defaultValue` if the flag is not available. */ public func jsonVariation(forKey flagKey: LDFlagKey, defaultValue: LDValue) -> LDValue { variationDetailInternal(flagKey, defaultValue, needsReason: false).value } /** + Returns the JSON value of a feature flag for a given flag key, in an object that also describes the way the + value was determined. + - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: an `LDEvaluationDetail` object */ public func jsonVariationDetail(forKey flagKey: LDFlagKey, defaultValue: LDValue) -> LDEvaluationDetail { variationDetailInternal(flagKey, defaultValue, needsReason: true) diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index ff51353a..1cfcefb1 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -13,15 +13,15 @@ public typealias LDFlagCollectionChangeHandler = ([LDFlagKey: LDChangedFlag]) -> public typealias LDFlagsUnchangedHandler = () -> Void /// A closure used to notify an observer owner that the current connection mode has changed. public typealias LDConnectionModeChangedHandler = (ConnectionInformation.ConnectionMode) -> Void -/// A closure used to notify an observer owner that an error occurred during feature flag processing. -public typealias LDErrorHandler = (Error) -> Void extension LDFlagKey { private static var anyKeyIdentifier: LDFlagKey { "Darkly.FlagKeyList.Any" } static var anyKey: [LDFlagKey] { [anyKeyIdentifier] } } +/// An error thrown from APIs when an invalid argument is provided. @objc public class LDInvalidArgumentError: NSObject, Error { + /// A description of the error. public let localizedDescription: String init(_ description: String) { @@ -42,6 +42,16 @@ struct DynamicKey: CodingKey { } } +/** + An immutable instance of any data type that is allowed in JSON. + + An `LDValue` can be a null (that is, an instance that represents a JSON null value), a boolean, a number (always + encoded internally as double-precision floating-point), a string, an ordered list of `LDValue` values (a JSON array), + or a map of strings to `LDValue` values (a JSON object). + + This can be used to represent complex data in a user custom attribute, or to get a feature flag value that uses a + complex type or does not always use the same type. + */ public enum LDValue: Codable, Equatable, ExpressibleByNilLiteral, @@ -62,11 +72,17 @@ public enum LDValue: Codable, public typealias IntegerLiteralType = Double public typealias FloatLiteralType = Double + /// Represents a JSON null value. case null + /// Represents a JSON boolean value. case bool(Bool) + /// Represents a JSON number value. case number(Double) + /// Represents a JSON string value. case string(String) + /// Represents an array of JSON values. case array([LDValue]) + /// Represents a JSON object. case object([String: LDValue]) public init(nilLiteral: ()) { @@ -149,49 +165,4 @@ public enum LDValue: Codable, try dictValue.forEach { try keyedEncoder.encode($1, forKey: DynamicKey(stringValue: $0)!) } } } - - func booleanValue() -> Bool { - if case .bool(let val) = self { return val } - return false - } - - func intValue() -> Int { - if case .number(let val) = self { - // TODO check - return Int.init(val) - } - return 0 - } - - func doubleValue() -> Double { - if case .number(let val) = self { return val } - return 0 - } - - func stringValue() -> String { - if case .string(let val) = self { return val } - return "" - } - - func toAny() -> Any? { - switch self { - case .null: return nil - case .bool(let boolValue): return boolValue - case .number(let doubleValue): return doubleValue - case .string(let stringValue): return stringValue - case .array(let arrayValue): return arrayValue.map { $0.toAny() } - case .object(let dictValue): return dictValue.mapValues { $0.toAny() } - } - } - - static func fromAny(_ value: Any?) -> LDValue { - guard let value = value, !(value is NSNull) - else { return .null } - if let boolValue = value as? Bool { return .bool(boolValue) } - if let numValue = value as? NSNumber { return .number(Double(truncating: numValue)) } - if let stringValue = value as? String { return .string(stringValue) } - if let arrayValue = value as? [Any?] { return .array(arrayValue.map { LDValue.fromAny($0) }) } - if let dictValue = value as? [String: Any?] { return .object(dictValue.mapValues { LDValue.fromAny($0) }) } - return .null - } } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift index cd75b60d..b029bb1e 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift @@ -1,7 +1,8 @@ import Foundation /** - Collects the elements of a feature flag that changed as a result of a `clientstream` update or feature flag request. + Collects the elements of a feature flag that changed as a result of the SDK receiving an update. + The SDK will pass a LDChangedFlag or a collection of LDChangedFlags into feature flag observer closures. See `LDClient.observe(key:owner:handler:)`, `LDClient.observe(keys:owner:handler:)`, and `LDClient.observeAll(owner:handler:)` for more details. diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift index 5b467085..4b5b46a9 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift @@ -6,11 +6,11 @@ import Foundation */ public final class LDEvaluationDetail { /// The value of the flag for the current user. - public internal(set) var value: T + public let value: T /// The index of the returned value within the flag's list of variations, or `nil` if the default was returned. - public internal(set) var variationIndex: Int? + public let variationIndex: Int? /// A structure representing the main factor that influenced the resultant flag evaluation value. - public internal(set) var reason: [String: LDValue]? + public let reason: [String: LDValue]? internal init(value: T, variationIndex: Int?, reason: [String: LDValue]?) { self.value = value diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 683935d1..71621baf 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -1,20 +1,21 @@ import Foundation -typealias UserKey = String // use for identifying semantics for strings, particularly in dictionaries - /** - LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. For example, the client app may launch with the SDK defined anonymous user. As the user works with the client app, information may be collected as needed and sent to LaunchDarkly. The client app controls the information collected, which LaunchDarkly does not use except as the client directs to refine feature flags. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. - The SDK caches last known feature flags for use on app startup to provide continuity with the last app run. Provided the LDClient is online and can establish a connection with LaunchDarkly servers, cached information will only be used a very short time. Once the latest feature flags arrive at the SDK, the SDK no longer uses cached feature flags. The SDK retains feature flags on the last 5 client defined users. The SDK will retain feature flags until they are overwritten by a different user's feature flags, or until the user removes the app from the device. - The SDK does not cache user information collected, except for the user key. The user key is used to identify the cached feature flags for that user. Client app developers should use caution not to use sensitive user information as the user-key. + LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. + + For example, the client app may launch with the SDK defined anonymous user. As the user works with the client app, + information may be collected as needed and sent to LaunchDarkly. The client app controls the information collected. + Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. + + The SDK caches last known feature flags for use on app startup to provide continuity with the last app run. Provided + the `LDClient` is online and can establish a connection with LaunchDarkly servers, cached information will only be used + a very short time. Once the latest feature flags arrive at the SDK, the SDK no longer uses cached feature flags. The + SDK retains feature flags on the last 5 client defined users. The SDK will retain feature flags until they are + overwritten by a different user's feature flags, or until the user removes the app from the device. The SDK does not + cache user information collected. */ public struct LDUser: Encodable, Equatable { - /// String keys associated with LDUser properties. - public enum CodingKeys: String, CodingKey { - /// Key names match the corresponding LDUser property - case key, name, firstName, lastName, country, ipAddress = "ip", email, avatar, custom, isAnonymous = "anonymous", device, operatingSystem = "os", config, privateAttributes = "privateAttrs", secondary - } - static let optionalAttributes = UserAttribute.BuiltIn.allBuiltIns.filter { $0.name != "key" && $0.name != "anonymous"} static let storedIdKey: String = "ldDeviceIdentifier" @@ -37,7 +38,7 @@ public struct LDUser: Encodable, Equatable { public var email: String? /// Client app defined avatar for the user. (Default: nil) public var avatar: String? - /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) + /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private, see `privateAttributes` for details. (Default: [:]) public var custom: [String: LDValue] /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: true) public var isAnonymous: Bool @@ -92,8 +93,8 @@ public struct LDUser: Encodable, Equatable { self.avatar = avatar self.isAnonymous = isAnonymous ?? (selectedKey == LDUser.defaultKey(environmentReporter: environmentReporter)) self.custom = custom ?? [:] - self.custom.merge([CodingKeys.device.rawValue: .string(environmentReporter.deviceModel), - CodingKeys.operatingSystem.rawValue: .string(environmentReporter.systemVersion)]) { lhs, _ in lhs } + self.custom.merge(["device": .string(environmentReporter.deviceModel), + "os": .string(environmentReporter.systemVersion)]) { lhs, _ in lhs } self.privateAttributes = privateAttributes ?? [] Log.debug(typeName(and: #function) + "user: \(self)") } @@ -103,8 +104,8 @@ public struct LDUser: Encodable, Equatable { */ init(environmentReporter: EnvironmentReporting) { self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), - custom: [CodingKeys.device.rawValue: .string(environmentReporter.deviceModel), - CodingKeys.operatingSystem.rawValue: .string(environmentReporter.systemVersion)], + custom: ["device": .string(environmentReporter.deviceModel), + "os": .string(environmentReporter.systemVersion)], isAnonymous: true) } @@ -133,7 +134,10 @@ public struct LDUser: Encodable, Equatable { var container = encoder.container(keyedBy: DynamicKey.self) try container.encode(key, forKey: DynamicKey(stringValue: "key")!) - try container.encode(isAnonymous, forKey: DynamicKey(stringValue: "anonymous")!) + + if isAnonymous { + try container.encode(isAnonymous, forKey: DynamicKey(stringValue: "anonymous")!) + } try LDUser.optionalAttributes.forEach { attribute in if let value = self.value(for: attribute) as? String { diff --git a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift index cf08e89f..069b45bc 100644 --- a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift +++ b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift @@ -1,18 +1,39 @@ import Foundation +/** + Represents a built-in or custom attribute name supported by `LDUser`. + + This abstraction helps to distinguish attribute names from other `String` values. + + For a more complete description of user attributes and how they can be referenced in feature flag rules, see the + reference guides [Setting user attributes](https://docs.launchdarkly.com/home/users/attributes) and + [Targeting users](https://docs.launchdarkly.com/home/flags/targeting-users). + */ public class UserAttribute: Equatable, Hashable { + /** + Instances for built in attributes. + */ public struct BuiltIn { + /// Represents the user key attribute. public static let key = UserAttribute("key") { $0.key } + /// Represents the secondary key attribute. public static let secondaryKey = UserAttribute("secondary") { $0.secondary } - // swiftlint:disable:next identifier_name - public static let ip = UserAttribute("ip") { $0.ipAddress } + /// Represents the IP address attribute. + public static let ip = UserAttribute("ip") { $0.ipAddress } // swiftlint:disable:this identifier_name + /// Represents the email address attribute. public static let email = UserAttribute("email") { $0.email } + /// Represents the full name attribute. public static let name = UserAttribute("name") { $0.name } + /// Represents the avatar attribute. public static let avatar = UserAttribute("avatar") { $0.avatar } + /// Represents the first name attribute. public static let firstName = UserAttribute("firstName") { $0.firstName } + /// Represents the last name attribute. public static let lastName = UserAttribute("lastName") { $0.lastName } + /// Represents the country attribute. public static let country = UserAttribute("country") { $0.country } + /// Represents the anonymous attribute. public static let anonymous = UserAttribute("anonymous") { $0.isAnonymous } static let allBuiltIns = [key, secondaryKey, ip, email, name, avatar, firstName, lastName, country, anonymous] @@ -20,6 +41,15 @@ public class UserAttribute: Equatable, Hashable { static var builtInMap = { return BuiltIn.allBuiltIns.reduce(into: [:]) { $0[$1.name] = $1 } }() + /** + Returns a `UserAttribute` instance for the specified atttribute name. + + For built-in attributes, the same instances are always reused and `isBuiltIn` will be `true`. For custom + attributes, a new instance is created and `isBuiltIn` will be `false`. + + - parameter name: the attribute name + - returns: a `UserAttribute` + */ public static func forName(_ name: String) -> UserAttribute { if let builtIn = builtInMap[name] { return builtIn @@ -35,6 +65,7 @@ public class UserAttribute: Equatable, Hashable { self.builtInGetter = builtInGetter } + /// Whether the attribute is built-in rather than custom. public var isBuiltIn: Bool { builtInGetter != nil } public static func == (lhs: UserAttribute, rhs: UserAttribute) -> Bool { diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift index a02a0cad..1af8618b 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift @@ -7,171 +7,16 @@ import Foundation */ @objc(LDChangedFlag) public class ObjcLDChangedFlag: NSObject { - fileprivate let changedFlag: LDChangedFlag - fileprivate var sourceValue: Any? { - changedFlag.oldValue.toAny() ?? changedFlag.newValue.toAny() - } - /// The changed feature flag's key - @objc public var key: String { - changedFlag.key - } - - fileprivate init(_ changedFlag: LDChangedFlag) { - self.changedFlag = changedFlag - } -} - -/// Wraps the changed feature flag's BOOL values. -/// -/// If the flag is not actually a BOOL the SDK sets the old and new value to false, and `typeMismatch` will be `YES`. -@objc(LDBoolChangedFlag) -public final class ObjcLDBoolChangedFlag: ObjcLDChangedFlag { - /// The changed flag's value before it changed - @objc public var oldValue: Bool { - changedFlag.oldValue.booleanValue() - } - /// The changed flag's value after it changed - @objc public var newValue: Bool { - changedFlag.newValue.booleanValue() - } - - override init(_ changedFlag: LDChangedFlag) { - super.init(changedFlag) - } - - @objc public var typeMismatch: Bool { - !(sourceValue is Bool) - } -} - -/// Wraps the changed feature flag's NSInteger values. -/// -/// If the flag is not actually an NSInteger the SDK sets the old and new value to 0, and `typeMismatch` will be `YES`. -@objc(LDIntegerChangedFlag) -public final class ObjcLDIntegerChangedFlag: ObjcLDChangedFlag { - /// The changed flag's value before it changed - @objc public var oldValue: Int { - (changedFlag.oldValue.toAny() as? Int) ?? 0 - } - /// The changed flag's value after it changed - @objc public var newValue: Int { - (changedFlag.newValue.toAny() as? Int) ?? 0 - } - - override init(_ changedFlag: LDChangedFlag) { - super.init(changedFlag) - } - - @objc public var typeMismatch: Bool { - !(sourceValue is Int) - } -} - -/// Wraps the changed feature flag's double values. -/// -/// If the flag is not actually a double the SDK sets the old and new value to 0.0, and `typeMismatch` will be `YES`. -@objc(LDDoubleChangedFlag) -public final class ObjcLDDoubleChangedFlag: ObjcLDChangedFlag { - /// The changed flag's value before it changed - @objc public var oldValue: Double { - changedFlag.oldValue.doubleValue() - } - /// The changed flag's value after it changed - @objc public var newValue: Double { - changedFlag.newValue.doubleValue() - } - - override init(_ changedFlag: LDChangedFlag) { - super.init(changedFlag) - } - - @objc public var typeMismatch: Bool { - !(sourceValue is Double) - } -} - -/// Wraps the changed feature flag's NSString values. -/// -/// If the flag is not actually an NSString the SDK sets the old and new value to nil, and `typeMismatch` will be `YES`. -@objc(LDStringChangedFlag) -public final class ObjcLDStringChangedFlag: ObjcLDChangedFlag { - /// The changed flag's value before it changed - @objc public var oldValue: String? { - changedFlag.oldValue.stringValue() - } - /// The changed flag's value after it changed - @objc public var newValue: String? { - changedFlag.newValue.stringValue() - } - - override init(_ changedFlag: LDChangedFlag) { - super.init(changedFlag) - } - - @objc public var typeMismatch: Bool { - !(sourceValue is String) - } -} - -/// Wraps the changed feature flag's NSArray values. -/// -/// If the flag is not actually a NSArray the SDK sets the old and new value to nil, and `typeMismatch` will be `YES`. -@objc(LDArrayChangedFlag) -public final class ObjcLDArrayChangedFlag: ObjcLDChangedFlag { - /// The changed flag's value before it changed - @objc public var oldValue: [Any]? { - changedFlag.oldValue.toAny() as? [Any] - } - /// The changed flag's value after it changed - @objc public var newValue: [Any]? { - changedFlag.newValue.toAny() as? [Any] - } - - override init(_ changedFlag: LDChangedFlag) { - super.init(changedFlag) - } - - @objc public var typeMismatch: Bool { - !(sourceValue is [Any]) - } -} - -/// Wraps the changed feature flag's NSDictionary values. -/// -/// If the flag is not actually an NSDictionary the SDK sets the old and new value to nil, and `typeMismatch` will be `YES`. -@objc(LDDictionaryChangedFlag) -public final class ObjcLDDictionaryChangedFlag: ObjcLDChangedFlag { - /// The changed flag's value before it changed - @objc public var oldValue: [String: Any]? { - changedFlag.oldValue.toAny() as? [String: Any] - } - /// The changed flag's value after it changed - @objc public var newValue: [String: Any]? { - changedFlag.newValue.toAny() as? [String: Any] - } - - override init(_ changedFlag: LDChangedFlag) { - super.init(changedFlag) - } - - @objc public var typeMismatch: Bool { - !(sourceValue is [String: Any]) - } -} - -public extension LDChangedFlag { - /// An NSObject wrapper for the Swift LDChangeFlag enum. Intended for use in mixed apps when Swift code needs to pass a LDChangeFlag into an Objective-C method. - var objcChangedFlag: ObjcLDChangedFlag { - let extantValue = oldValue.toAny() ?? newValue.toAny() - switch extantValue { - case _ as Bool: return ObjcLDBoolChangedFlag(self) - case _ as Int: return ObjcLDIntegerChangedFlag(self) - case _ as Double: return ObjcLDDoubleChangedFlag(self) - case _ as String: return ObjcLDStringChangedFlag(self) - case _ as [Any]: return ObjcLDArrayChangedFlag(self) - case _ as [String: Any]: return ObjcLDDictionaryChangedFlag(self) - default: return ObjcLDChangedFlag(self) - } + @objc public let key: String + /// The value from before the flag change occurred. + @objc public let oldValue: ObjcLDValue + /// The value after the flag change occurred. + @objc public let newValue: ObjcLDValue + + init(_ changedFlag: LDChangedFlag) { + self.key = changedFlag.key + self.oldValue = ObjcLDValue(wrappedValue: changedFlag.oldValue) + self.newValue = ObjcLDValue(wrappedValue: changedFlag.newValue) } } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index 3d345679..b9dfcfc9 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -7,7 +7,7 @@ import Foundation The SDK creates an Objective-C native style API by wrapping Swift specific classes, properties, and methods into Objective-C wrapper classes prefixed by `Objc`. By defining Objective-C specific names, client apps written in Objective-C can use a native coding style, including using familiar LaunchDarkly SDK names like `LDClient`, `LDConfig`, and `LDUser`. Objective-C developers should refer to the Objc documentation by following the Objc specific links following type, property, and method names. ## Usage ### Startup - 1. To customize, configure a LDConfig (`ObjcLDConfig`) and LDUser (`ObjcLDUser`). The `config` is required, the `user` is optional. Both give you additional control over the feature flags delivered to the LDClient. See `ObjcLDConfig` & `ObjcLDUser` for more details. + 1. To customize, configure a LDConfig (`ObjcLDConfig`) and LDUser (`ObjcLDUser`). Both give you additional control over the feature flags delivered to the LDClient. See `ObjcLDConfig` & `ObjcLDUser` for more details. - The mobileKey set into the `LDConfig` comes from your LaunchDarkly Account settings (on the left, at the bottom). If you have multiple projects be sure to choose the correct Mobile key. 2. Call `[ObjcLDClient startWithConfig: user: completion:]` (`ObjcLDClient.startWithConfig(_:config:user:completion:)`) - If you do not pass in a LDUser, LDCLient will create a default for you. @@ -15,29 +15,29 @@ import Foundation 3. Because the LDClient is a singleton, you do not have to keep a reference to it in your code. ### Getting Feature Flags - Once the LDClient has started, it makes your feature flags available using the `variation` and `variationDetail` methods. A `variation` is a specific flag value. For example, a boolean feature flag has 2 variations, `YES` and `NO`. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. - ```` + Once the LDClient has started, it makes your feature flags available using the `variation` and `variationDetail` methods. A `variation` is a specific flag value. For example, a boolean feature flag has 2 variations, `YES` and `NO`. You can create feature flags with more than 2 variations using other feature flag types. See `LDValue` for the available types. + ``` BOOL boolFlag = [ldClientInstance boolVariationForKey:@"my-bool-flag" defaultValue:NO]; - ```` + ``` If you need to know more information about why a given value is returned, the typed `variationDetail` methods return an `LDEvaluationDetail` with an detail about the evaluation. - ```` + ``` LDBoolEvaluationDetail *boolVariationDetail = [ldClientInstance boolVariationDetail:@"my-bool-flag" defaultValue:NO]; BOOL boolFlagValue = boolVariationDetail.value; NSInteger boolFlagVariation = boolVariationDetail.variationIndex NSDictionary boolFlagReason = boolVariationValue.reason; - ```` + ``` See the typed `-[LDCLient variationForKey: defaultValue:]` or `-[LDClient variationDetailForKey: defaultValue:]` methods in the section **Feature Flag values** for details. ### Observing Feature Flags If you want to know when a feature flag value changes, you can check the flag's value. You can also use one of several `observe` methods to have the LDClient notify you when a change occurs. There are several options-- you can setup notifications based on when a specific flag changes, when any flag in a collection changes, or when a flag doesn't change. - ```` + ``` __weak typeof(self) weakSelf = self; [ldClientInstance observeBool:@"my-bool-flag" owner:self handler:^(LDBoolChangedFlag *changedFlag) { __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf updateFlagWithKey:@"my-bool-flag" changedFlag:changedFlag]; }]; - ```` + ``` The `changedFlag` passed in to the block contains the old and new value. See the typed `LDChangedFlag` classes in the **Obj-C Changed Flags**. */ @objc(LDClient) @@ -166,20 +166,14 @@ public final class ObjcLDClient: NSObject { /** Returns the BOOL variation for the given feature flag. If the flag does not exist, cannot be cast to a BOOL, or the LDClient is not started, returns the default value. - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. - - The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. - - See `LDStreamingMode` for details about the modes the LDClient uses to update feature flags. - - When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. + A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDValue` for the available types. A call to `boolVariation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. ### Usage - ```` + ``` BOOL boolFeatureFlagValue = [ldClientInstance boolVariationForKey:@"my-bool-flag" defaultValue:NO]; - ```` + ``` - parameter key: The LDFlagKey for the requested feature flag. - parameter defaultValue: The default value to return if the feature flag key does not exist. @@ -201,19 +195,15 @@ public final class ObjcLDClient: NSObject { */ @objc public func boolVariationDetail(forKey key: LDFlagKey, defaultValue: Bool) -> ObjcLDBoolEvaluationDetail { let evaluationDetail = ldClient.boolVariationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDBoolEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) + return ObjcLDBoolEvaluationDetail(value: evaluationDetail.value, + variationIndex: evaluationDetail.variationIndex, + reason: evaluationDetail.reason?.mapValues { ObjcLDValue(wrappedValue: $0) }) } /** Returns the NSInteger variation for the given feature flag. If the flag does not exist, cannot be cast to a NSInteger, or the LDClient is not started, returns the default value. - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. - - The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. - - See `LDStreamingMode` for details about the modes the LDClient uses to update feature flags. - - When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. + A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDValue` for the available types. A call to `integerVariation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. @@ -242,26 +232,22 @@ public final class ObjcLDClient: NSObject { */ @objc public func integerVariationDetail(forKey key: LDFlagKey, defaultValue: Int) -> ObjcLDIntegerEvaluationDetail { let evaluationDetail = ldClient.intVariationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDIntegerEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) + return ObjcLDIntegerEvaluationDetail(value: evaluationDetail.value, + variationIndex: evaluationDetail.variationIndex, + reason: evaluationDetail.reason?.mapValues { ObjcLDValue(wrappedValue: $0) }) } /** Returns the double variation for the given feature flag. If the flag does not exist, cannot be cast to a double, or the LDClient is not started, returns the default value. - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. - - The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. - - See `LDStreamingMode` for details about the modes the LDClient uses to update feature flags. - - When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. + A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDValue` for the available types. A call to `doubleVariation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. ### Usage - ```` + ``` double doubleFeatureFlagValue = [ldClientInstance doubleVariationForKey:@"my-double-flag" defaultValue:2.71828]; - ```` + ``` - parameter key: The LDFlagKey for the requested feature flag. - parameter defaultValue: The default value to return if the feature flag key does not exist. @@ -283,26 +269,22 @@ public final class ObjcLDClient: NSObject { */ @objc public func doubleVariationDetail(forKey key: LDFlagKey, defaultValue: Double) -> ObjcLDDoubleEvaluationDetail { let evaluationDetail = ldClient.doubleVariationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDDoubleEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) + return ObjcLDDoubleEvaluationDetail(value: evaluationDetail.value, + variationIndex: evaluationDetail.variationIndex, + reason: evaluationDetail.reason?.mapValues { ObjcLDValue(wrappedValue: $0) }) } /** Returns the NSString variation for the given feature flag. If the flag does not exist, cannot be cast to a NSString, or the LDClient is not started, returns the default value. - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. - - The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. - - See `LDStreamingMode` for details about the modes the LDClient uses to update feature flags. - - When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. + A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDValue` for the available types. A call to `stringVariation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. ### Usage - ```` + ``` NSString *stringFeatureFlagValue = [ldClientInstance stringVariationForKey:@"my-string-flag" defaultValue:@""]; - ```` + ``` - parameter key: The LDFlagKey for the requested feature flag. - parameter defaultValue: The default value to return if the feature flag key does not exist. @@ -324,36 +306,30 @@ public final class ObjcLDClient: NSObject { */ @objc public func stringVariationDetail(forKey key: LDFlagKey, defaultValue: String) -> ObjcLDStringEvaluationDetail { let evaluationDetail = ldClient.stringVariationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDStringEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) + return ObjcLDStringEvaluationDetail(value: evaluationDetail.value, + variationIndex: evaluationDetail.variationIndex, + reason: evaluationDetail.reason?.mapValues { ObjcLDValue(wrappedValue: $0) }) } /** - Returns the NSArray variation for the given feature flag. If the flag does not exist, cannot be cast to a NSArray, or the LDClient is not started, returns the default value. + Returns the JSON variation for the given feature flag. If the flag does not exist, or the LDClient is not started, returns the default value. - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. - - The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. - - See `LDStreamingMode` for details about the modes the LDClient uses to update feature flags. - - When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. - - A call to `arrayVariation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. + A call to `jsonVariation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. ### Usage - ```` - NSArray *arrayFeatureFlagValue = [ldClientInstance arrayVariationForKey:@"my-array-flag" defaultValue:@[@1,@2,@3]]; - ```` + ``` + ObjcLDValue *featureFlagValue = [ldClientInstance jsonVariationForKey:@"my-flag" defaultValue:[LDValue ofBool:NO]]; + ``` - parameter key: The LDFlagKey for the requested feature flag. - parameter defaultValue: The default value to return if the feature flag key does not exist. - - returns: The requested NSArray feature flag value, or the default value if the flag is missing or cannot be cast to a NSArray, or the client is not started + - returns: The requested feature flag value, or the default value if the flag is missing or the client is not started */ /// - Tag: arrayVariation -// @objc public func arrayVariation(forKey key: LDFlagKey, defaultValue: [Any]) -> [Any] { -// ldClient.variation(forKey: key, defaultValue: defaultValue) -// } + @objc public func jsonVariation(forKey key: LDFlagKey, defaultValue: ObjcLDValue) -> ObjcLDValue { + ObjcLDValue(wrappedValue: ldClient.jsonVariation(forKey: key, defaultValue: defaultValue.wrappedValue)) + } /** See [arrayVariation](x-source-tag://arrayVariation) for more information on variation methods. @@ -361,54 +337,15 @@ public final class ObjcLDClient: NSObject { - parameter key: The LDFlagKey for the requested feature flag. - parameter defaultValue: The default value to return if the feature flag key does not exist. - - returns: ObjcLDArrayEvaluationDetail containing your value as well as useful information on why that value was returned. + - returns: ObjcLDJSONEvaluationDetail containing your value as well as useful information on why that value was returned. */ -// @objc public func arrayVariationDetail(forKey key: LDFlagKey, defaultValue: [Any]) -> ObjcLDArrayEvaluationDetail { -// let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) -// return ObjcLDArrayEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() }) -// } - - /** - Returns the NSDictionary variation for the given feature flag. If the flag does not exist, cannot be cast to a NSDictionary, or the LDClient is not started, returns the default value. - - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *YES* and *NO*. You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the available types. - - The LDClient must be started in order to return feature flag values. If the LDClient is not started, it will always return the default value. The LDClient must be online to keep the feature flag values up-to-date. - - See `LDStreamingMode` for details about the modes the LDClient uses to update feature flags. - - When offline, LDClient closes the clientstream connection and no longer requests feature flags. The LDClient will return feature flag values (assuming the LDClient was started), which may not match the values set on the LaunchDarkly server. - - A call to `dictionaryVariation` records events reported later. Recorded events allow clients to analyze usage and assist in debugging issues. - - ### Usage - ```` - NSDictionary *dictionaryFeatureFlagValue = [ldClientInstance dictionaryVariationForKey:@"my-dictionary-flag" defaultValue:@{@"dictionary":@"defaultValue"}]; - ```` - - - parameter key: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. + @objc public func jsonVariationDetail(forKey key: LDFlagKey, defaultValue: ObjcLDValue) -> ObjcLDJSONEvaluationDetail { + let evaluationDetail = ldClient.jsonVariationDetail(forKey: key, defaultValue: defaultValue.wrappedValue) + return ObjcLDJSONEvaluationDetail(value: ObjcLDValue(wrappedValue: evaluationDetail.value), + variationIndex: evaluationDetail.variationIndex, + reason: evaluationDetail.reason?.mapValues { ObjcLDValue(wrappedValue: $0) }) + } - - returns: The requested NSDictionary feature flag value, or the default value if the flag is missing or cannot be cast to a NSDictionary, or the client is not started - */ - /// - Tag: dictionaryVariation -// @objc public func dictionaryVariation(forKey key: LDFlagKey, defaultValue: [String: Any]) -> [String: Any] { -// ldClient.variation(forKey: key, defaultValue: defaultValue) -// } - - /** - See [dictionaryVariation](x-source-tag://dictionaryVariation) for more information on variation methods. - - - parameter key: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. - - - returns: ObjcLDDictionaryEvaluationDetail containing your value as well as useful information on why that value was returned. - */ -// @objc public func dictionaryVariationDetail(forKey key: LDFlagKey, defaultValue: [String: Any]) -> ObjcLDDictionaryEvaluationDetail { -// let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) -// return ObjcLDDictionaryEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() }) -// } - /** Returns a dictionary with the flag keys and their values. If the LDClient is not started, returns nil. @@ -416,7 +353,7 @@ public final class ObjcLDClient: NSObject { LDClient will not provide any source or change information, only flag keys and flag values. The client app should convert the feature flag value into the desired type. */ - @objc public var allFlags: [LDFlagKey: Any]? { ldClient.allFlags?.mapValues { $0.toAny() ?? NSNull() } } + @objc public var allFlags: [LDFlagKey: ObjcLDValue]? { ldClient.allFlags?.mapValues { ObjcLDValue(wrappedValue: $0) } } // MARK: - Feature Flag Updates @@ -430,152 +367,22 @@ public final class ObjcLDClient: NSObject { SeeAlso: `ObjcLDBoolChangedFlag` and `stopObserving(owner:)` ### Usage - ```` + ``` __weak typeof(self) weakSelf = self; [ldClientInstance observeBool:"my-bool-flag" owner:self handler:^(LDBoolChangedFlag *changedFlag){ __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf showBoolChangedFlag:changedFlag]; }]; - ```` - - - parameter key: The LDFlagKey for the flag to observe. - - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. - - parameter handler: The block the SDK will execute when the feature flag changes. - */ - @objc public func observeBool(_ key: LDFlagKey, owner: LDObserverOwner, handler: @escaping ObjcLDBoolChangedFlagHandler) { - ldClient.observe(key: key, owner: owner) { changedFlag in handler(ObjcLDBoolChangedFlag(changedFlag)) } - } - - /** - Sets a handler for the specified NSInteger flag key executed on the specified owner. If the flag's value changes, executes the handler, passing in the `changedFlag` containing the old and new flag values. See `ObjcLDIntegerChangedFlag` for details. - - The SDK retains only weak references to the owner, which allows the client app to freely destroy owners without issues. Client apps should capture a strong self reference from a weak reference immediately inside the handler to avoid retain cycles causing a memory leak. - - The SDK executes handlers on the main thread. - - SeeAlso: `ObjcLDIntegerChangedFlag` and `stopObserving(owner:)` - - ### Usage - ```` - __weak typeof(self) weakSelf = self; - [ldClientInstance observeInteger:"my-integer-flag" owner:self handler:^(LDIntegerChangedFlag *changedFlag){ - __strong typeof(weakSelf) strongSelf = weakSelf; - [strongSelf showIntegerChangedFlag:changedFlag]; - }]; - ```` - - - parameter key: The LDFlagKey for the flag to observe. - - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. - - parameter handler: The block the SDK will execute when the feature flag changes. - */ - @objc public func observeInteger(_ key: LDFlagKey, owner: LDObserverOwner, handler: @escaping ObjcLDIntegerChangedFlagHandler) { - ldClient.observe(key: key, owner: owner) { changedFlag in handler(ObjcLDIntegerChangedFlag(changedFlag)) } - } - - /** - Sets a handler for the specified double flag key executed on the specified owner. If the flag's value changes, executes the handler, passing in the `changedFlag` containing the old and new flag values. See `ObjcLDDoubleChangedFlag` for details. - - The SDK retains only weak references to the owner, which allows the client app to freely destroy owners without issues. Client apps should capture a strong self reference from a weak reference immediately inside the handler to avoid retain cycles causing a memory leak. - - The SDK executes handlers on the main thread. - - SeeAlso: `ObjcLDDoubleChangedFlag` and `stopObserving(owner:)` - - ### Usage - ```` - __weak typeof(self) weakSelf = self; - [ldClientInstance observeDouble:"my-double-flag" owner:self handler:^(LDDoubleChangedFlag *changedFlag){ - __strong typeof(weakSelf) strongSelf = weakSelf; - [strongSelf showDoubleChangedFlag:changedFlag]; - }]; - ```` + ``` - parameter key: The LDFlagKey for the flag to observe. - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. - parameter handler: The block the SDK will execute when the feature flag changes. */ - @objc public func observeDouble(_ key: LDFlagKey, owner: LDObserverOwner, handler: @escaping ObjcLDDoubleChangedFlagHandler) { - ldClient.observe(key: key, owner: owner) { changedFlag in handler(ObjcLDDoubleChangedFlag(changedFlag)) } + @objc public func observe(_ key: LDFlagKey, owner: LDObserverOwner, handler: @escaping ObjcLDChangedFlagHandler) { + ldClient.observe(key: key, owner: owner) { changedFlag in handler(ObjcLDChangedFlag(changedFlag)) } } - /** - Sets a handler for the specified NSString flag key executed on the specified owner. If the flag's value changes, executes the handler, passing in the `changedFlag` containing the old and new flag values. See `ObjcLDStringChangedFlag` for details. - - The SDK retains only weak references to the owner, which allows the client app to freely destroy owners without issues. Client apps should capture a strong self reference from a weak reference immediately inside the handler to avoid retain cycles causing a memory leak. - - The SDK executes handlers on the main thread. - - SeeAlso: `ObjcLDStringChangedFlag` and `stopObserving(owner:)` - - ### Usage - ```` - __weak typeof(self) weakSelf = self; - [ldClientInstance observeString:"my-string-flag" owner:self handler:^(LDStringChangedFlag *changedFlag){ - __strong typeof(weakSelf) strongSelf = weakSelf; - [strongSelf showStringChangedFlag:changedFlag]; - }]; - ```` - - - parameter key: The LDFlagKey for the flag to observe. - - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. - - parameter handler: The block the SDK will execute when the feature flag changes. - */ - @objc public func observeString(_ key: LDFlagKey, owner: LDObserverOwner, handler: @escaping ObjcLDStringChangedFlagHandler) { - ldClient.observe(key: key, owner: owner) { changedFlag in handler(ObjcLDStringChangedFlag(changedFlag)) } - } - - /** - Sets a handler for the specified NSArray flag key executed on the specified owner. If the flag's value changes, executes the handler, passing in the `changedFlag` containing the old and new flag values. See `ObjcLDArrayChangedFlag` for details. - - The SDK retains only weak references to the owner, which allows the client app to freely destroy owners without issues. Client apps should capture a strong self reference from a weak reference immediately inside the handler to avoid retain cycles causing a memory leak. - - The SDK executes handlers on the main thread. - - SeeAlso: `ObjcLDArrayChangedFlag` and `stopObserving(owner:)` - - ### Usage - ```` - __weak typeof(self) weakSelf = self; - [ldClientInstance observeArray:"my-array-flag" owner:self handler:^(LDArrayChangedFlag *changedFlag){ - __strong typeof(weakSelf) strongSelf = weakSelf; - [strongSelf showArrayChangedFlag:changedFlag]; - }]; - ```` - - - parameter key: The LDFlagKey for the flag to observe. - - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. - - parameter handler: The block the SDK will execute when the feature flag changes. - */ - @objc public func observeArray(_ key: LDFlagKey, owner: LDObserverOwner, handler: @escaping ObjcLDArrayChangedFlagHandler) { - ldClient.observe(key: key, owner: owner) { changedFlag in handler(ObjcLDArrayChangedFlag(changedFlag)) } - } - - /** - Sets a handler for the specified NSDictionary flag key executed on the specified owner. If the flag's value changes, executes the handler, passing in the `changedFlag` containing the old and new flag values. See `ObjcLDDictionaryChangedFlag` for details. - - The SDK retains only weak references to the owner, which allows the client app to freely destroy owners without issues. Client apps should capture a strong self reference from a weak reference immediately inside the handler to avoid retain cycles causing a memory leak. - - The SDK executes handlers on the main thread. - - SeeAlso: `ObjcLDDictionaryChangedFlag` and `stopObserving(owner:)` - - ### Usage - ```` - __weak typeof(self) weakSelf = self; - [ldClientInstance observeDictionary:"my-dictionary-flag" owner:self handler:^(LDDictionaryChangedFlag *changedFlag){ - __strong typeof(weakSelf) strongSelf = weakSelf; - [strongSelf showDictionaryChangedFlag:changedFlag]; - }]; - ```` - - - parameter key: The LDFlagKey for the flag to observe. - - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. - - parameter handler: The block the SDK will execute when the feature flag changes. - */ - @objc public func observeDictionary(_ key: LDFlagKey, owner: LDObserverOwner, handler: @escaping ObjcLDDictionaryChangedFlagHandler) { - ldClient.observe(key: key, owner: owner) { changedFlag in handler(ObjcLDDictionaryChangedFlag(changedFlag)) } - } - /** Sets a handler for the specified flag keys executed on the specified owner. If any observed flag's value changes, executes the handler 1 time, passing in a dictionary of containing the old and new flag values. See LDChangedFlag (`ObjcLDChangedFlag`) for details. @@ -586,14 +393,14 @@ public final class ObjcLDClient: NSObject { SeeAlso: `ObjcLDChangedFlag` and `stopObserving(owner:)` ### Usage - ```` + ``` __weak typeof(self) weakSelf = self; [ldClientInstance observeKeys:@[@"my-bool-flag",@"my-string-flag", @"my-dictionary-flag"] owner:self handler:^(NSDictionary * _Nonnull changedFlags) { __strong typeof(weakSelf) strongSelf = weakSelf; //There will be a typed LDChangedFlag entry in changedFlags for each changed flag. The block will only be called once regardless of how many flags changed. [strongSelf showChangedFlags: changedFlags]; }]; - ```` + ``` - parameter keys: An array of NSString* flag keys for the flags to observe. - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. @@ -601,10 +408,7 @@ public final class ObjcLDClient: NSObject { */ @objc public func observeKeys(_ keys: [LDFlagKey], owner: LDObserverOwner, handler: @escaping ObjcLDChangedFlagCollectionHandler) { ldClient.observe(keys: keys, owner: owner) { changedFlags in - let objcChangedFlags = changedFlags.mapValues { changedFlag -> ObjcLDChangedFlag in - changedFlag.objcChangedFlag - } - handler(objcChangedFlags) + handler(changedFlags.mapValues { ObjcLDChangedFlag($0) }) } } @@ -618,24 +422,21 @@ public final class ObjcLDClient: NSObject { SeeAlso: `ObjcLDChangedFlag` and `stopObserving(owner:)` ### Usage - ```` + ``` __weak typeof(self) weakSelf = self; [ldClientInstance observeAllKeysWithOwner:self handler:^(NSDictionary * _Nonnull changedFlags) { __strong typeof(weakSelf) strongSelf = weakSelf; //There will be a typed LDChangedFlag entry in changedFlags for each changed flag. The block will only be called once regardless of how many flags changed. [strongSelf showChangedFlags:changedFlags]; }]; - ```` + ``` - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. - parameter handler: The LDFlagCollectionChangeHandler the SDK will execute 1 time when any of the observed feature flags change. */ @objc public func observeAllKeys(owner: LDObserverOwner, handler: @escaping ObjcLDChangedFlagCollectionHandler) { ldClient.observeAll(owner: owner) { changedFlags in - let objcChangedFlags = changedFlags.mapValues { changedFlag -> ObjcLDChangedFlag in - changedFlag.objcChangedFlag - } - handler(objcChangedFlags) + handler(changedFlags.mapValues { ObjcLDChangedFlag($0) }) } } @@ -651,14 +452,14 @@ public final class ObjcLDClient: NSObject { SeeAlso: `stopObserving(owner:)` ### Usage - ```` + ``` __weak typeof(self) weakSelf = self; [[LDClient sharedInstance] observeFlagsUnchangedWithOwner:self handler:^{ __strong typeof(weakSelf) strongSelf = weakSelf; //do something after the flags were not updated. The block will be called once on the main thread if the client is polling and the poll did not change any flag values. [self checkFeatureValues]; }]; - ```` + ``` - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. - parameter handler: The LDFlagsUnchangedHandler the SDK will execute 1 time when a flag request completes with no flags changed. @@ -679,46 +480,11 @@ public final class ObjcLDClient: NSObject { } /** - Handler passed to the client app when a BOOL feature flag value changes + Handler passed to the client app when a feature flag value changes - - parameter changedFlag: The LDBoolChangedFlag passed into the handler containing the old & new flag value + - parameter changedFlag: The LDChangedFlag passed into the handler containing the old & new flag value */ - public typealias ObjcLDBoolChangedFlagHandler = (_ changedFlag: ObjcLDBoolChangedFlag) -> Void - - /** - Handler passed to the client app when a NSInteger feature flag value changes - - - parameter changedFlag: The LDIntegerChangedFlag passed into the handler containing the old & new flag value - */ - public typealias ObjcLDIntegerChangedFlagHandler = (_ changedFlag: ObjcLDIntegerChangedFlag) -> Void - - /** - Handler passed to the client app when a double feature flag value changes - - - parameter changedFlag: The LDDoubleChangedFlag passed into the handler containing the old & new flag value - */ - public typealias ObjcLDDoubleChangedFlagHandler = (_ changedFlag: ObjcLDDoubleChangedFlag) -> Void - - /** - Handler passed to the client app when a NSString feature flag value changes - - - parameter changedFlag: The LDStringChangedFlag passed into the handler containing the old & new flag value - */ - public typealias ObjcLDStringChangedFlagHandler = (_ changedFlag: ObjcLDStringChangedFlag) -> Void - - /** - Handler passed to the client app when a NSArray feature flag value changes - - - parameter changedFlag: The LDArrayChangedFlag passed into the handler containing the old & new flag value - */ - public typealias ObjcLDArrayChangedFlagHandler = (_ changedFlag: ObjcLDArrayChangedFlag) -> Void - - /** - Handler passed to the client app when a NSArray feature flag value changes - - - parameter changedFlag: The LDDictionaryChangedFlag passed into the handler containing the old & new flag value - */ - public typealias ObjcLDDictionaryChangedFlagHandler = (_ changedFlag: ObjcLDDictionaryChangedFlag) -> Void + public typealias ObjcLDChangedFlagHandler = (_ changedFlag: ObjcLDChangedFlag) -> Void /** Handler passed to the client app when a NSArray feature flag value changes @@ -737,17 +503,17 @@ public final class ObjcLDClient: NSObject { Once the SDK's event store is full, the SDK discards events until they can be reported to LaunchDarkly. Configure the size of the event store using `eventCapacity` on the `config`. See `LDConfig` (`ObjcLDConfig`) for details. ### Usage - ```` + ``` [ldClientInstance trackWithKey:@"event-key" data:@{@"event-data-key":7}]; - ```` + ``` - parameter key: The key for the event. The SDK does nothing with the key, which can be any string the client app sends - parameter data: The data for the event. The SDK does nothing with the data, which can be any valid JSON item the client app sends. (Optional) - parameter error: NSError object to hold the invalidJsonObject error if the data is not a valid JSON item. (Optional) */ /// - Tag: track - @objc public func track(key: String, data: Any? = nil) { - ldClient.track(key: key, data: LDValue.fromAny(data), metricValue: nil) + @objc public func track(key: String, data: ObjcLDValue? = nil) { + ldClient.track(key: key, data: data?.wrappedValue, metricValue: nil) } /** @@ -758,8 +524,8 @@ public final class ObjcLDClient: NSObject { - parameter metricValue: A numeric value used by the LaunchDarkly experimentation feature in numeric custom metrics. Can be omitted if this event is used by only non-numeric metrics. This field will also be returned as part of the custom event for Data Export. - parameter error: NSError object to hold the invalidJsonObject error if the data is not a valid JSON item. (Optional) */ - @objc public func track(key: String, data: Any? = nil, metricValue: Double) { - ldClient.track(key: key, data: LDValue.fromAny(data), metricValue: metricValue) + @objc public func track(key: String, data: ObjcLDValue? = nil, metricValue: Double) { + ldClient.track(key: key, data: data?.wrappedValue, metricValue: metricValue) } /** diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index b1e7ec64..3a6bc5df 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -101,7 +101,7 @@ public final class ObjcLDConfig: NSObject { The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - See `LDUser.privatizableAttributes` (`ObjcLDUser.privatizableAttributes`) for the attribute names that can be declared private. To set private user attributes for a specific user, see `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). (Default: nil) + See `LDUser.privatizableAttributes` (`ObjcLDUser.privatizableAttributes`) for the attribute names that can be declared private. To set private user attributes for a specific user, see `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). (Default: `[]`) See Also: `allUserAttributesPrivate`, `LDUser.privatizableAttributes` (`ObjcLDUser.privatizableAttributes`), and `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). */ diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift index a13ce6ba..3088f09a 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift @@ -1,77 +1,84 @@ import Foundation +/// Structure that contains the evaluation result and additional information when evaluating a flag as a boolean. @objc(LDBoolEvaluationDetail) public final class ObjcLDBoolEvaluationDetail: NSObject { + /// The value of the flag for the current user. @objc public let value: Bool + /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. @objc public let variationIndex: Int - @objc public let reason: [String: Any]? + /// A structure representing the main factor that influenced the resultant flag evaluation value. + @objc public let reason: [String: ObjcLDValue]? - internal init(value: Bool, variationIndex: Int?, reason: [String: Any]?) { + internal init(value: Bool, variationIndex: Int?, reason: [String: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 self.reason = reason } } +/// Structure that contains the evaluation result and additional information when evaluating a flag as a double. @objc(LDDoubleEvaluationDetail) public final class ObjcLDDoubleEvaluationDetail: NSObject { + /// The value of the flag for the current user. @objc public let value: Double + /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. @objc public let variationIndex: Int - @objc public let reason: [String: Any]? + /// A structure representing the main factor that influenced the resultant flag evaluation value. + @objc public let reason: [String: ObjcLDValue]? - internal init(value: Double, variationIndex: Int?, reason: [String: Any]?) { + internal init(value: Double, variationIndex: Int?, reason: [String: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 self.reason = reason } } +/// Structure that contains the evaluation result and additional information when evaluating a flag as an integer. @objc(LDIntegerEvaluationDetail) public final class ObjcLDIntegerEvaluationDetail: NSObject { + /// The value of the flag for the current user. @objc public let value: Int + /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. @objc public let variationIndex: Int - @objc public let reason: [String: Any]? + /// A structure representing the main factor that influenced the resultant flag evaluation value. + @objc public let reason: [String: ObjcLDValue]? - internal init(value: Int, variationIndex: Int?, reason: [String: Any]?) { + internal init(value: Int, variationIndex: Int?, reason: [String: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 self.reason = reason } } +/// Structure that contains the evaluation result and additional information when evaluating a flag as a string. @objc(LDStringEvaluationDetail) public final class ObjcLDStringEvaluationDetail: NSObject { + /// The value of the flag for the current user. @objc public let value: String? + /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. @objc public let variationIndex: Int - @objc public let reason: [String: Any]? + /// A structure representing the main factor that influenced the resultant flag evaluation value. + @objc public let reason: [String: ObjcLDValue]? - internal init(value: String?, variationIndex: Int?, reason: [String: Any]?) { + internal init(value: String?, variationIndex: Int?, reason: [String: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 self.reason = reason } } -@objc(ArrayEvaluationDetail) -public final class ObjcLDArrayEvaluationDetail: NSObject { - @objc public let value: [Any]? +/// Structure that contains the evaluation result and additional information when evaluating a flag as a JSON value. +@objc(LDJSONEvaluationDetail) +public final class ObjcLDJSONEvaluationDetail: NSObject { + /// The value of the flag for the current user. + @objc public let value: ObjcLDValue + /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. @objc public let variationIndex: Int - @objc public let reason: [String: Any]? - - internal init(value: [Any]?, variationIndex: Int?, reason: [String: Any]?) { - self.value = value - self.variationIndex = variationIndex ?? -1 - self.reason = reason - } -} + /// A structure representing the main factor that influenced the resultant flag evaluation value. + @objc public let reason: [String: ObjcLDValue]? -@objc(DictionaryEvaluationDetail) -public final class ObjcLDDictionaryEvaluationDetail: NSObject { - @objc public let value: [String: Any]? - @objc public let variationIndex: Int - @objc public let reason: [String: Any]? - - internal init(value: [String: Any]?, variationIndex: Int?, reason: [String: Any]?) { + internal init(value: ObjcLDValue, variationIndex: Int?, reason: [String: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 self.reason = reason diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index e417b1e3..0961e3e5 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -11,43 +11,6 @@ import Foundation public final class ObjcLDUser: NSObject { var user: LDUser - /// LDUser secondary attribute used to make `secondary` private - @objc public class var attributeSecondary: String { - LDUser.CodingKeys.secondary.rawValue - } - /// LDUser name attribute used to make `name` private - @objc public class var attributeName: String { - LDUser.CodingKeys.name.rawValue - } - /// LDUser firstName attribute used to make `firstName` private - @objc public class var attributeFirstName: String { - LDUser.CodingKeys.firstName.rawValue - } - /// LDUser lastName attribute used to make `lastName` private - @objc public class var attributeLastName: String { - LDUser.CodingKeys.lastName.rawValue - } - /// LDUser country attribute used to make `country` private - @objc public class var attributeCountry: String { - LDUser.CodingKeys.country.rawValue - } - /// LDUser ipAddress attribute used to make `ipAddress` private - @objc public class var attributeIPAddress: String { - LDUser.CodingKeys.ipAddress.rawValue - } - /// LDUser email attribute used to make `email` private - @objc public class var attributeEmail: String { - LDUser.CodingKeys.email.rawValue - } - /// LDUser avatar attribute used to make `avatar` private - @objc public class var attributeAvatar: String { - LDUser.CodingKeys.avatar.rawValue - } - /// LDUser custom attribute used to make `custom` private - @objc public class var attributeCustom: String { - LDUser.CodingKeys.custom.rawValue - } - /// Client app defined string that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. The key cannot be made private. @objc public var key: String { return user.key @@ -92,10 +55,10 @@ public final class ObjcLDUser: NSObject { get { user.avatar } set { user.avatar = newValue } } - /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) - @objc public var custom: [String: Any] { - get { user.custom.mapValues { $0.toAny() as Any } } - set { user.custom = newValue.mapValues { LDValue.fromAny($0) } } + /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. See `privateAttributes` for details. + @objc public var custom: [String: ObjcLDValue] { + get { user.custom.mapValues { ObjcLDValue(wrappedValue: $0) } } + set { user.custom = newValue.mapValues { $0.wrappedValue } } } /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: YES) @objc public var isAnonymous: Bool { @@ -108,7 +71,7 @@ public final class ObjcLDUser: NSObject { The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - This attribute is ignored if `ObjcLDConfig.allUserAttributesPrivate` is YES. Combined with `ObjcLDConfig.privateUserAttributes`. The SDK considers attributes appearing in either list as private. Client apps may define attributes found in `privatizableAttributes` and top level `custom` dictionary keys here. (Default: nil) + This attribute is ignored if `ObjcLDConfig.allUserAttributesPrivate` is YES. Combined with `ObjcLDConfig.privateUserAttributes`. The SDK considers attributes appearing in either list as private. Client apps may define attributes found in `privatizableAttributes` and top level `custom` dictionary keys here. (Default: `[]`]) See Also: `ObjcLDConfig.allUserAttributesPrivate` and `ObjcLDConfig.privateUserAttributes`. diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift new file mode 100644 index 00000000..6f048624 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift @@ -0,0 +1,132 @@ +import Foundation + +/** + Used to represent the type of an `LDValue`. + */ +@objc(LDValueType) +public enum ObjcLDValueType: Int { + /// The value returned by `LDValue.getType()` when the represented value is a null. + case null + /// The value returned by `LDValue.getType()` when the represented value is a boolean. + case bool + /// The value returned by `LDValue.getType()` when the represented value is a number. + case number + /// The value returned by `LDValue.getType()` when the represented value is a string. + case string + /// The value returned by `LDValue.getType()` when the represented value is an array. + case array + /// The value returned by `LDValue.getType()` when the represented value is an object. + case object +} + +/** + Bridged `LDValue` type for Objective-C. + + Can create instances from Objective-C with the provided `of` static functions, for example `[LDValue ofBool:YES]`. + */ +@objc(LDValue) +public final class ObjcLDValue: NSObject { + /// The Swift `LDValue` enum the instance is wrapping. + public let wrappedValue: LDValue + + /** + Create a instance of the bridging object for the given value. + + - parameter wrappedValue: The value to wrap. + */ + public init(wrappedValue: LDValue) { + self.wrappedValue = wrappedValue + } + + /// Create a new `LDValue` from a boolean value. + @objc public static func of(bool: Bool) -> ObjcLDValue { + return ObjcLDValue(wrappedValue: .bool(bool)) + } + + /// Create a new `LDValue` from a numeric value. + @objc public static func of(number: NSNumber) -> ObjcLDValue { + return ObjcLDValue(wrappedValue: .number(number.doubleValue)) + } + + /// Create a new `LDValue` from a string value. + @objc public static func of(string: String) -> ObjcLDValue { + return ObjcLDValue(wrappedValue: .string(string)) + } + + /// Create a new `LDValue` from an array of values. + @objc public static func of(array: [ObjcLDValue]) -> ObjcLDValue { + return ObjcLDValue(wrappedValue: .array(array.map { $0.wrappedValue })) + } + + /// Create a new `LDValue` object from dictionary of values. + @objc public static func of(dict: [String: ObjcLDValue]) -> ObjcLDValue { + return ObjcLDValue(wrappedValue: .object(dict.mapValues { $0.wrappedValue })) + } + + /// Get the type of the value. + @objc public func getType() -> ObjcLDValueType { + switch wrappedValue { + case .null: return .null + case .bool: return .bool + case .number: return .number + case .string: return .string + case .array: return .array + case .object: return .object + } + } + + /** + Get the value as a `Bool`. + + - returns: The contained boolean value or `NO` if the value is not a boolean. + */ + @objc public func boolValue() -> Bool { + guard case let .bool(value) = wrappedValue + else { return false } + return value + } + + /** + Get the value as a `Double`. + + - returns: The contained double value or `0.0` if the value is not a number. + */ + @objc public func doubleValue() -> Double { + guard case let .number(value) = wrappedValue + else { return 0.0 } + return value + } + + /** + Get the value as a `String`. + + - returns: The contained string value or the empty string if the value is not a string. + */ + @objc public func stringValue() -> String { + guard case let .string(value) = wrappedValue + else { return "" } + return value + } + + /** + Get the value as an array. + + - returns: An array of the contained values, or the empty array if the value is not an array. + */ + @objc public func arrayValue() -> [ObjcLDValue] { + guard case let .array(values) = wrappedValue + else { return [] } + return values.map { ObjcLDValue(wrappedValue: $0) } + } + + /** + Get the value as a dictionary representing the JSON object + + - returns: A dictionary representing the JSON object, or the empty dictionary if the value is not a dictionary. + */ + @objc public func dictValue() -> [String: ObjcLDValue] { + guard case let .object(values) = wrappedValue + else { return [:] } + return values.mapValues { ObjcLDValue(wrappedValue: $0) } + } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index 084cf592..43067d3e 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -36,6 +36,17 @@ final class CacheConverter: CacheConverting { init() { } + private func convertValue(_ value: Any?) -> LDValue { + guard let value = value, !(value is NSNull) + else { return .null } + if let boolValue = value as? Bool { return .bool(boolValue) } + if let numValue = value as? NSNumber { return .number(Double(truncating: numValue)) } + if let stringValue = value as? String { return .string(stringValue) } + if let arrayValue = value as? [Any?] { return .array(arrayValue.map { convertValue($0) }) } + if let dictValue = value as? [String: Any?] { return .object(dictValue.mapValues { convertValue($0) }) } + return .null + } + private func convertV6Data(v6cache: KeyedValueCaching, flagCaches: [MobileKey: FeatureFlagCaching]) { guard let cachedV6Data = v6cache.dictionary(forKey: "com.launchDarkly.cachedUserEnvironmentFlags") else { return } @@ -62,13 +73,13 @@ final class CacheConverter: CacheConverting { guard let flagDict = flagDict as? [String: Any] else { return } let flag = FeatureFlag(flagKey: flagKey, - value: LDValue.fromAny(flagDict["value"]), + value: convertValue(flagDict["value"]), variation: flagDict["variation"] as? Int, version: flagDict["version"] as? Int, flagVersion: flagDict["flagVersion"] as? Int, trackEvents: flagDict["trackEvents"] as? Bool ?? false, debugEventsUntilDate: Date(millisSince1970: flagDict["debugEventsUntilDate"] as? Int64), - reason: (flagDict["reason"] as? [String: Any])?.mapValues { LDValue.fromAny($0) }, + reason: (flagDict["reason"] as? [String: Any])?.mapValues { convertValue($0) }, trackReason: flagDict["trackReason"] as? Bool ?? false) userEnvFlags[flagKey] = flag } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift index 4f120db5..c4720153 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -4,6 +4,7 @@ import Foundation protocol FeatureFlagCaching { // sourcery: defaultMockValue = KeyedValueCachingMock() var keyedValueCache: KeyedValueCaching { get } + func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift index 4e331111..6aee68d8 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -122,11 +122,7 @@ struct EnvironmentReporter: EnvironmentReporting { var vendorUUID: String? { UIDevice.current.identifierForVendor?.uuidString } #endif - #if INTEGRATION_HARNESS var shouldThrottleOnlineCalls: Bool { !isDebugBuild } - #else - var shouldThrottleOnlineCalls: Bool { true } - #endif let sdkVersion = "5.4.5" // Unfortunately, the following does not function in certain configurations, such as when included through SPM diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index e2e5ed73..8d746de1 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -14,32 +14,21 @@ protocol EventReporting { } class EventReporter: EventReporting { - fileprivate struct Constants { - static let eventQueueLabel = "com.launchdarkly.eventSyncQueue" - } - - private let eventQueue = DispatchQueue(label: Constants.eventQueueLabel, qos: .userInitiated) var isOnline: Bool { - get { isOnlineQueue.sync { _isOnline } } - set { - isOnlineQueue.sync { - _isOnline = newValue - Log.debug(typeName(and: #function, appending: ": ") + "\(_isOnline)") - _isOnline ? startReporting(isOnline: _isOnline) : stopReporting() - } - } + get { timerQueue.sync { eventReportTimer != nil } } + set { timerQueue.sync { newValue ? startReporting() : stopReporting() } } } - private var _isOnline = false - private var isOnlineQueue = DispatchQueue(label: "com.launchdarkly.EventReporter.isOnlineQueue") private (set) var lastEventResponseDate: Date? let service: DarklyServiceProvider + private let eventQueue = DispatchQueue(label: "com.launchdarkly.eventSyncQueue", qos: .userInitiated) // These fields should only be used synchronized on the eventQueue private(set) var eventStore: [Event] = [] private(set) var flagRequestTracker = FlagRequestTracker() + private var timerQueue = DispatchQueue(label: "com.launchdarkly.EventReporter.timerQueue") private var eventReportTimer: TimeResponding? var isReportingActive: Bool { eventReportTimer != nil } @@ -82,15 +71,13 @@ class EventReporter: EventReporting { } } - private func startReporting(isOnline: Bool) { - guard isOnline && !isReportingActive + private func startReporting() { + guard eventReportTimer == nil else { return } eventReportTimer = LDTimer(withTimeInterval: service.config.eventFlushInterval, fireQueue: eventQueue, execute: reportEvents) } private func stopReporting() { - guard isReportingActive - else { return } eventReportTimer?.cancel() eventReportTimer = nil } @@ -134,7 +121,7 @@ class EventReporter: EventReporting { service.diagnosticCache?.recordEventsInLastBatch(eventsInLastBatch: toPublish.count) - DispatchQueue.main.async { + DispatchQueue.global().async { self.publish(toPublish, UUID().uuidString, completion) } } @@ -160,7 +147,7 @@ class EventReporter: EventReporting { let shouldRetry = self.processEventResponse(sentEvents: events.count, response: urlResponse as? HTTPURLResponse, error: error, isRetry: false) if shouldRetry { Log.debug("Retrying event post after delay.") - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) { + DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 1.0) { self.service.publishEventData(eventData, payloadId) { _, urlResponse, error in _ = self.processEventResponse(sentEvents: events.count, response: urlResponse as? HTTPURLResponse, error: error, isRetry: true) completion?() diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 5cdcf111..a7678449 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -768,19 +768,19 @@ final class LDClientSpec: QuickSpec { } context("non-Optional default value") { it("returns the flag value") { - expect(testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DarklyServiceMock.FlagValues.bool - expect(testContext.subject.intVariation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DarklyServiceMock.FlagValues.int - expect(testContext.subject.doubleVariation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DarklyServiceMock.FlagValues.double - expect(testContext.subject.stringVariation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DarklyServiceMock.FlagValues.string - expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array)) == LDValue.fromAny(DarklyServiceMock.FlagValues.array) - expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary)) == LDValue.fromAny(DarklyServiceMock.FlagValues.dictionary) + expect(.bool(testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool))) == DarklyServiceMock.FlagValues.bool + expect(.number(Double(testContext.subject.intVariation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)))) == DarklyServiceMock.FlagValues.int + expect(.number(testContext.subject.doubleVariation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double))) == DarklyServiceMock.FlagValues.double + expect(.string(testContext.subject.stringVariation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string))) == DarklyServiceMock.FlagValues.string + expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array)) == DarklyServiceMock.FlagValues.array + expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary)) == DarklyServiceMock.FlagValues.dictionary } it("records a flag evaluation event") { _ = testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == LDValue.fromAny(DarklyServiceMock.FlagValues.bool) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue) == LDValue.fromAny(DefaultFlagValues.bool) + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == DarklyServiceMock.FlagValues.bool + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue) == .bool(DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag) == testContext.flagStoreMock.featureFlags[DarklyServiceMock.FlagKeys.bool] expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user } @@ -800,8 +800,8 @@ final class LDClientSpec: QuickSpec { _ = testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == LDValue.fromAny(DefaultFlagValues.bool) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue) == LDValue.fromAny(DefaultFlagValues.bool) + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == .bool(DefaultFlagValues.bool) + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue) == .bool(DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag).to(beNil()) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 1c320d72..3e212454 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -23,15 +23,15 @@ final class DarklyServiceMock: DarklyServiceProvider { } struct FlagValues { - static let bool = true - static let int = 7 - static let double = 3.14159 - static let string = "string value" - static let array = [1, 2, 3] - static let dictionary: [String: Any] = ["sub-flag-a": false, "sub-flag-b": 3, "sub-flag-c": 2.71828] - static let null = NSNull() - - static func value(from flagKey: LDFlagKey) -> Any? { + static let bool: LDValue = true + static let int: LDValue = 7 + static let double: LDValue = 3.14159 + static let string: LDValue = "string value" + static let array: LDValue = [1, 2, 3] + static let dictionary: LDValue = ["sub-flag-a": false, "sub-flag-b": 3, "sub-flag-c": 2.71828] + static let null: LDValue = nil + + static func value(from flagKey: LDFlagKey) -> LDValue { switch flagKey { case FlagKeys.bool: return FlagValues.bool case FlagKeys.int: return FlagValues.int @@ -82,7 +82,7 @@ final class DarklyServiceMock: DarklyServiceProvider { trackEvents: Bool = true, debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0)) -> FeatureFlag { FeatureFlag(flagKey: flagKey, - value: LDValue.fromAny(FlagValues.value(from: flagKey)), + value: FlagValues.value(from: flagKey), variation: variation, version: version(for: flagKey, useAlternateVersion: useAlternateVersion), flagVersion: flagVersion, diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift index 5a83b3e1..943be88c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift @@ -26,8 +26,8 @@ extension LDUser { static func custom(includeSystemValues: Bool) -> [String: LDValue] { var custom = StubConstants.custom if includeSystemValues { - custom[CodingKeys.device.rawValue] = StubConstants.device - custom[CodingKeys.operatingSystem.rawValue] = StubConstants.operatingSystem + custom["device"] = StubConstants.device + custom["os"] = StubConstants.operatingSystem } return custom } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 6653761a..4bf2efd9 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -85,7 +85,7 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["previousKey"], "def") XCTAssertEqual(dict["contextKind"], "user") XCTAssertEqual(dict["previousContextKind"], "anonymousUser") - XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) + XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } @@ -99,7 +99,7 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["data"], ["abc", 12]) XCTAssertEqual(dict["metricValue"], 0.5) XCTAssertEqual(dict["userKey"], .string(user.key)) - XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) + XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } @@ -113,7 +113,7 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["data"], ["key": "val"]) XCTAssertEqual(dict["userKey"], .string(anonUser.key)) XCTAssertEqual(dict["contextKind"], "anonymousUser") - XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) + XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } @@ -126,7 +126,7 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["key"], "event-key") XCTAssertEqual(dict["metricValue"], 2.5) XCTAssertEqual(dict["user"], encodeToLDValue(user)) - XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) + XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } @@ -148,7 +148,7 @@ final class EventSpec: XCTestCase { } else { XCTAssertEqual(dict["userKey"], .string(user.key)) } - XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) + XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } } @@ -172,7 +172,7 @@ final class EventSpec: XCTestCase { } else { XCTAssertEqual(dict["userKey"], .string(user.key)) } - XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) + XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } } @@ -194,7 +194,7 @@ final class EventSpec: XCTestCase { } else { XCTAssertEqual(dict["userKey"], .string(user.key)) } - XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) + XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } } @@ -215,7 +215,7 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["userKey"], .string(user.key)) XCTAssertEqual(dict["contextKind"], "anonymousUser") } - XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) + XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } } @@ -246,7 +246,7 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["kind"], "identify") XCTAssertEqual(dict["key"], .string(user.key)) XCTAssertEqual(dict["user"], encodeToLDValue(user)) - XCTAssertEqual(dict["creationDate"], LDValue.fromAny(event.creationDate.millisSince1970)) + XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } } @@ -260,8 +260,8 @@ final class EventSpec: XCTestCase { encodesToObject(event) { dict in XCTAssertEqual(dict.count, 4) XCTAssertEqual(dict["kind"], "summary") - XCTAssertEqual(dict["startDate"], LDValue.fromAny(flagRequestTracker.startDate.millisSince1970)) - XCTAssertEqual(dict["endDate"], LDValue.fromAny(event.endDate.millisSince1970)) + XCTAssertEqual(dict["startDate"], .number(Double(flagRequestTracker.startDate.millisSince1970))) + XCTAssertEqual(dict["endDate"], .number(Double(event.endDate.millisSince1970))) valueIsObject(dict["features"]) { features in XCTAssertEqual(features.count, 1) let counter = FlagCounter() diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift index cb09469e..3186dc39 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift @@ -186,7 +186,7 @@ extension FlagCounter { if flagKey.isKnown { featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey) for _ in 0.. Date: Wed, 4 May 2022 12:38:08 -0400 Subject: [PATCH 47/50] (V6) Bring back some ObjcLDUser attribute* constants (#198) --- .jazzy.yaml | 2 ++ .../LaunchDarkly/LDClientVariation.swift | 2 ++ .../LaunchDarkly/Models/LDConfig.swift | 4 ++-- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 2 +- .../ObjectiveC/ObjcLDConfig.swift | 4 ++-- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 19 ++++++++++++++++++- .../LaunchDarkly/ObjectiveC/ObjcLDValue.swift | 5 +++++ 7 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.jazzy.yaml b/.jazzy.yaml index 1c74e034..d9d3f5b1 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -36,9 +36,11 @@ custom_categories: - name: Other Types children: + - UserAttribute - LDStreamingMode - LDFlagKey - LDInvalidArgumentError + - RequestHeaderTransform - name: Objective-C Core Interfaces children: diff --git a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift index d11f3c25..42c55aff 100644 --- a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -1,6 +1,8 @@ import Foundation extension LDClient { + // MARK: Flag variation methods + /** Returns the boolean value of a feature flag for a given flag key. diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index fe98ba7a..1b47e295 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -209,9 +209,9 @@ public struct LDConfig { The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - See `LDUser.privatizableAttributes` for the attribute names that can be declared private. To set private user attributes for a specific user, see `LDUser.privateAttributes`. (Default: nil) + To set private user attributes for a specific user, see `LDUser.privateAttributes`. (Default: nil) - See Also: `allUserAttributesPrivate`, `LDUser.privatizableAttributes`, and `LDUser.privateAttributes`. + See Also: `allUserAttributesPrivate` and `LDUser.privateAttributes`. */ public var privateUserAttributes: [UserAttribute] = Defaults.privateUserAttributes diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 71621baf..681df574 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -46,7 +46,7 @@ public struct LDUser: Encodable, Equatable { /** Client app defined privateAttributes for the user. The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - This attribute is ignored if `LDConfig.allUserAttributesPrivate` is true. Combined with `LDConfig.privateUserAttributes`. The SDK considers attributes appearing in either list as private. Client apps may define attributes found in `privatizableAttributes` and top level `custom` dictionary keys here. (Default: nil) + This attribute is ignored if `LDConfig.allUserAttributesPrivate` is true. Combined with `LDConfig.privateUserAttributes`. The SDK considers attributes appearing in either list as private. Client apps may define most built-in attributes and all top level `custom` dictionary keys here. (Default: []]) See Also: `LDConfig.allUserAttributesPrivate` and `LDConfig.privateUserAttributes`. */ public var privateAttributes: [UserAttribute] diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index 3a6bc5df..db8038b0 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -101,9 +101,9 @@ public final class ObjcLDConfig: NSObject { The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - See `LDUser.privatizableAttributes` (`ObjcLDUser.privatizableAttributes`) for the attribute names that can be declared private. To set private user attributes for a specific user, see `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). (Default: `[]`) + To set private user attributes for a specific user, see `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). (Default: `[]`) - See Also: `allUserAttributesPrivate`, `LDUser.privatizableAttributes` (`ObjcLDUser.privatizableAttributes`), and `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). + See Also: `allUserAttributesPrivate` and `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). */ @objc public var privateUserAttributes: [String] { get { config.privateUserAttributes.map { $0.name } } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index 0961e3e5..d6192219 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -11,6 +11,23 @@ import Foundation public final class ObjcLDUser: NSObject { var user: LDUser + /// LDUser secondary attribute used to make `secondary` private + @objc public class var attributeSecondary: String { "secondary" } + /// LDUser name attribute used to make `name` private + @objc public class var attributeName: String { "name" } + /// LDUser firstName attribute used to make `firstName` private + @objc public class var attributeFirstName: String { "firstName" } + /// LDUser lastName attribute used to make `lastName` private + @objc public class var attributeLastName: String { "lastName" } + /// LDUser country attribute used to make `country` private + @objc public class var attributeCountry: String { "country" } + /// LDUser ipAddress attribute used to make `ipAddress` private + @objc public class var attributeIPAddress: String { "ip" } + /// LDUser email attribute used to make `email` private + @objc public class var attributeEmail: String { "email" } + /// LDUser avatar attribute used to make `avatar` private + @objc public class var attributeAvatar: String { "avatar" } + /// Client app defined string that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. The key cannot be made private. @objc public var key: String { return user.key @@ -71,7 +88,7 @@ public final class ObjcLDUser: NSObject { The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - This attribute is ignored if `ObjcLDConfig.allUserAttributesPrivate` is YES. Combined with `ObjcLDConfig.privateUserAttributes`. The SDK considers attributes appearing in either list as private. Client apps may define attributes found in `privatizableAttributes` and top level `custom` dictionary keys here. (Default: `[]`]) + This attribute is ignored if `ObjcLDConfig.allUserAttributesPrivate` is YES. Combined with `ObjcLDConfig.privateUserAttributes`. The SDK considers attributes appearing in either list as private. Client apps may define most built-in attributes and all top level `custom` dictionary keys here. (Default: `[]`]) See Also: `ObjcLDConfig.allUserAttributesPrivate` and `ObjcLDConfig.privateUserAttributes`. diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift index 6f048624..9ac75a8b 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift @@ -38,6 +38,11 @@ public final class ObjcLDValue: NSObject { self.wrappedValue = wrappedValue } + /// Create a new `LDValue` that represents a JSON null. + @objc public static func ofNull() -> ObjcLDValue { + return ObjcLDValue(wrappedValue: .null) + } + /// Create a new `LDValue` from a boolean value. @objc public static func of(bool: Bool) -> ObjcLDValue { return ObjcLDValue(wrappedValue: .bool(bool)) From ccc5702ee9a4ff0d12718eab1568dd06a0763568 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Wed, 4 May 2022 14:16:27 -0400 Subject: [PATCH 48/50] master -> main --- .circleci/config.yml | 2 +- .circleci/run-build-locally.sh | 2 +- .github/pull_request_template.md | 2 +- README.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index da14498c..64fb5d8e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -110,7 +110,7 @@ jobs: - run: name: CocoaPods spec lint command: | - if [ "$CIRCLE_BRANCH" = 'master' ]; then + if [ "$CIRCLE_BRANCH" = 'main' ]; then pod spec lint else pod lib lint diff --git a/.circleci/run-build-locally.sh b/.circleci/run-build-locally.sh index 1e26ce23..6d75fee4 100644 --- a/.circleci/run-build-locally.sh +++ b/.circleci/run-build-locally.sh @@ -10,4 +10,4 @@ curl --user ${CIRCLE_TOKEN}: \ --request POST \ --form config=@.circleci/config.yml \ --form notify=false \ - https://circleci.com/api/v1.1/project/github/launchdarkly/ios-swift-client-sdk-private/tree/master + https://circleci.com/api/v1.1/project/github/launchdarkly/ios-swift-client-sdk-private/tree/main diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 19806760..e7723490 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,7 @@ **Requirements** - [ ] I have added test coverage for new or changed functionality -- [ ] I have followed the repository's [pull request submission guidelines](../blob/master/CONTRIBUTING.md#submitting-pull-requests) +- [ ] I have followed the repository's [pull request submission guidelines](../blob/v6/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions **Related issues** diff --git a/README.md b/README.md index 54afd70f..b4c358f7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ LaunchDarkly SDK for iOS ======================== -[![CircleCI](https://circleci.com/gh/launchdarkly/ios-client-sdk/tree/master.svg?style=shield)](https://circleci.com/gh/launchdarkly/ios-client-sdk) +[![CircleCI](https://circleci.com/gh/launchdarkly/ios-client-sdk/tree/v6.svg?style=shield)](https://circleci.com/gh/launchdarkly/ios-client-sdk) [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-4BC51D.svg?style=flat)](https://swift.org/package-manager/) [![CocoaPods compatible](https://img.shields.io/cocoapods/v/LaunchDarkly.svg)](https://cocoapods.org/pods/LaunchDarkly) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) From 7fd2c0cb42b6f54d98eb6395d174ae7eb48e7ba6 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Wed, 4 May 2022 14:21:48 -0400 Subject: [PATCH 49/50] Update changelog for 6.0 release --- CHANGELOG.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea33631..f46316c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,58 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [6.0.0] - 2022-05-04 + +Note that Objective-C bridging types are prefixed by `Objc`, but that prefix is not exposed to code written in Objective-C. For example, changes listed to `ObjcLDClient` are changes to the class referred to as `LDClient` from within Objective-C. + +### Added +- Added the `LDValue` class to represent any data type that is allowed in JSON. This new type is used to provide more type safety when representing complex or non-statically determined data types. The SDK also provides the bridge types `ObjcLDValue` and `ObjcLDValueType` for Objective-C interoperability. +- Added the `UserAttribute` class which provides a less error-prone way to refer to user attribute names in configuration. +- Added the type specific variation functions, `boolVariation`, `intVariation`, `doubleVariation`, `stringVariation`, and `jsonVariation`, to `LDClient`. +- Added the type specific detailed variation functions, `boolVariationDetail`, `intVariationDetail`, `doubleVariationDetail`, `stringVariationDetail`, and `jsonVariationDetail`, to `LDClient`. +- Added `jsonVariation` and `jsonVariationDetail` to `ObjcLDClient`. These functions allow evaluating feature flags where the provided `defaultValue` and the resulting variation can be any valid JSON data type. +- Added `ObjcLDJSONEvaluationDetail` for retrieving the detailed evaluation information of arbitrary type flag variations. +- Added `ObjcLDChangedFlagHandler` type alias for new non-type specific Objective-C flag observers. + +### Changed (API) +- `LDClient.track(key: data: metricValue:)` no longer `throws`. +- The type of the `data` parameter of `LDClient.track(key: data: metricValue:)` has changed from `Any?` to `LDValue?`. +- `ObjcLDClient.track(key: data:)` and `ObjcLDClient.track(key: data: metricValue:)` no longer `throws`. In Objective-C this change means that the `track` functions no longer accept a `error:` parameter. +- The type of the `data` parameter of `ObjcLDClient.track(key: data:)` and `ObjcLDClient.track(key: data: metricValue)` has changed from `Any?` to `ObjLDValue?`. In Objective-C this would be a change from `id _Nullable` to `LDValue * _Nullable`. +- `LDClient.allFlags` now has the type `[LDFlagKey: LDValue]?` rather than `[LDFlagKey: Any]?`. +- `ObjcLDClient.allFlags` now has the type `[String: ObjcLDValue]?` rather than `[String: Any]?`. In Objective-C this would be a change from `NSDictionary * _Nullable` to `NSDictionary * _Nullable`. +- The type of the `LDUser.custom` dictionary, and the corresponding `LDUser.init` parameter has been changed from `[String: Any]?` to `[String: LDValue]`. +- The type of the `ObjcLDUser.custom` property has been changed from `[String: Any]?` to `[String: ObjcLDValue]`. In Objective-C this would be a change from `NSDictionary * _Nullable` to `NSDictionary * _Nonnull`. +- The type of the `LDUser.privateAttributes` property, and the corresponding `LDUser.init` parameter, have been changed from `[String]?` to `[UserAttribute]`. +- The type of the `ObjcLDUser.privateAttributes` property has been changed from `[String]?` to `[String]`. In Objective-C this would be a change from `NSArray * _Nullable` to `NSArray * _Nonnull`. +- The types of the properties `LDChangedFlag.oldValue` and `LDChangedFlag.newValue` have been changed from `Any?` to `LDValue`. +- The type of the `LDConfig.privateUserAttributes` property has been changed from `[String]?` to `[UserAttribute]`. +- `ObjcLDConfig.privateUserAttributes` now has the non-optional type `[String]` rather than `[String]?`. In Objective-C this would be a change from `NSArray * _Nullable` to `NSArray * _Nonnull`. +- The type of the `LDEvaluationDetail.reason` property has been changed from `[String: Any]` to `[String: LDValue]`. +- The type of the `reason` property of `ObjcLDBoolEvaluationDetail`, `ObjcLDIntegerEvaluationDetail`, `ObjcLDDoubleEvaluationDetail`, and `ObjcLDStringEvaluationDetail` has been changed from `[String: Any]?` to `[String: ObjcLDValue]?`. In Objective-C this would be a change from `NSDictionary * _Nullable` to `NSDictionary * _Nullable`. + +### Changed (behavioral) +- The `Equatable` instance for `LDUser` has been changed to compare all user properties, rather than just the `key` property. +- Using `"custom"` as a private attribute name in `LDConfig.privateUserAttributes` or `LDUser.privateAttributes` will no longer set all `LDUser` custom attributes private. +- The automatically set `device` and `operatingSystem` custom attributes can now be set private. +- SDK versions from 4.0.0 and less than 6.0.0 supported migration of cached flag data from any SDK version of at least 2.3.3. The 6.0.0 release only supports migration of cached flag data from SDK versions of at least 4.0.0. + +### Removed +- Removed `LDClient.variation(forKey: defaultValue:)` and `LDClient.variationDetail(forKey: defaultValue:)` functions. Please use the new type-specific variation functions instead (e.g. `LDClient.boolVariation(forKey: defaultValue:)`). +- Removed the `LDFlagValueConvertible` protocol which was previously used to constrain the parameters and return types of the variation functions. +- `LDErrorHandler` and `LDClient.observeError(owner: handler:)` have been removed. Please use `ConnectionInformation` instead. +- Removed the `LDUser.init(userDictionary: )` and `ObjcLDUser.init(userDictionary: )` initializers, please use the explicit initializers instead. +- Removed `LDUser.CodingKeys`. To refer to user attributes, please use `UserAttribute` instead. +- Removed `LDUser.privatizableAttributes` and `ObjcLDUser.privatizableAttributes`. +- Removed `ObjcLDUser.attributeCustom`. +- The `LDUser.device` and `LDUser.operatingSystem` properties, and the corresponding `LDUser.init` parameters have been removed. These fields will be included in the `LDUser.custom` dictionary. +- The `ObjcLDUser.device` and `ObjcLDUser.operatingSystem` properties have been removed. These fields will be included in the `ObjcLDUser.custom` dictionary. +- The `ObjcLDClient` functions, `arrayVariation`, `arrayVariationDetail`, `dictionaryVariation`, and `dictionaryVariationDetail`, have been removed. Please use `ObjcLDClient.jsonVariation` and `ObjcLDClient.jsonVariationDetail` instead. +- The per-type instances of `ObjcLDChangedFlag` have been removed. Please use the base class `ObjcLDChangedFlag`, which now provides `oldValue` and `newValue` `ObjcLDValue` properties. The removed classes are `ObjcLDBoolChangedFlag`, `ObjcLDIntegerChangedFlag`, `ObjcLDDoubleChangedFlag`, `ObjcLDStringChangedFlag`, `ObjcLDArrayChangedFlag`, and `ObjcLDDictionaryChangedFlag`. +- The classes `ObjcLDArrayEvaluationDetail` and `ObjcLDDictionaryEvaluationDetail` have been removed. Please use `ObjcLDJSONEvaluationDetail` instead. +- The type aliases, `ObjcLDBoolChangedFlagHandler`, `ObjcLDIntegerChangedFlagHandler`, `ObjcLDDoubleChangedFlagHandler`, `ObjcLDStringChangedFlagHandler`, `ObjcLDArrayChangedFlagHandler`, and `ObjcLDDictionaryChangedFlagHandler`, have been removed. Please use `ObjcLDChangedFlagHandler` instead. +- The `ObjcLDClient` functions, `observeBool`, `observeInteger`, `observeDouble`, `observeString`, `observeArray`, and `observeDictionary`, have been removed. Please use the non-type specific `ObjcLDClient.observe(key: owner: handler:)` instead. + ## [5.4.5] - 2022-03-11 ### Fixed - Fixed race condition in `LDSwiftEventSource` that could cause a crash if the stream is explicitly stopped (such as when `identify` is called) while the stream is waiting to reconnect. From 040a40ab8593bc2da3969b69a5946a33b94b5c20 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Wed, 4 May 2022 14:29:56 -0400 Subject: [PATCH 50/50] Bump versions --- LaunchDarkly.podspec | 2 +- LaunchDarkly.xcodeproj/project.pbxproj | 28 +++++++++---------- .../ServiceObjects/EnvironmentReporter.swift | 2 +- README.md | 6 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 001a59fd..7b7bc322 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |ld| ld.name = "LaunchDarkly" - ld.version = "5.4.5" + ld.version = "6.0.0" ld.summary = "iOS SDK for LaunchDarkly" ld.description = <<-DESC diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index b77d3c3a..0d448bc2 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -1397,7 +1397,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1420,7 +1420,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1443,7 +1443,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1464,7 +1464,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1507,11 +1507,11 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DYLIB_COMPATIBILITY_VERSION = 5.4.0; - DYLIB_CURRENT_VERSION = 5.4.5; + DYLIB_COMPATIBILITY_VERSION = 6.0.0; + DYLIB_CURRENT_VERSION = 6.0.0; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - FRAMEWORK_VERSION = B; + FRAMEWORK_VERSION = C; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1578,11 +1578,11 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DYLIB_COMPATIBILITY_VERSION = 5.4.0; - DYLIB_CURRENT_VERSION = 5.4.5; + DYLIB_COMPATIBILITY_VERSION = 6.0.0; + DYLIB_CURRENT_VERSION = 6.0.0; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - FRAMEWORK_VERSION = B; + FRAMEWORK_VERSION = C; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -1618,7 +1618,7 @@ INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1638,7 +1638,7 @@ INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1680,7 +1680,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; @@ -1702,7 +1702,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift index 6aee68d8..6f17bb9f 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -124,7 +124,7 @@ struct EnvironmentReporter: EnvironmentReporting { var shouldThrottleOnlineCalls: Bool { !isDebugBuild } - let sdkVersion = "5.4.5" + let sdkVersion = "6.0.0" // Unfortunately, the following does not function in certain configurations, such as when included through SPM // var sdkVersion: String { // Bundle(for: LDClient.self).infoDictionary?["CFBundleShortVersionString"] as? String ?? "5.x" diff --git a/README.md b/README.md index b4c358f7..3f872bc5 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ To include LaunchDarkly in a Swift package, simply add it to the dependencies se ```swift dependencies: [ - .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor(from: "5.4.5")) + .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor(from: "6.0.0")) ] ``` @@ -60,7 +60,7 @@ To use the [CocoaPods](https://cocoapods.org) dependency manager to integrate La ```ruby use_frameworks! target 'YourTargetName' do - pod 'LaunchDarkly', '~> 5.4' + pod 'LaunchDarkly', '~> 6.0' end ``` @@ -71,7 +71,7 @@ To use the [Carthage](https://github.com/Carthage/Carthage) dependency manager t To integrate LaunchDarkly into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "launchdarkly/ios-client-sdk" ~> 5.4 +github "launchdarkly/ios-client-sdk" ~> 6.0 ``` ### Manual installation