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/90] 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/90] 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/90] [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/90] 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/90] 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/90] 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/90] [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/90] [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/90] 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/90] 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/90] 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.aaakk.us.kg.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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] (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/90] 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/90] 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/90] (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/90] (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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] 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/90] (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/90] (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/90] 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/90] (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/90] (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/90] 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 25d315bd696e37264c7169f4fa67f923d6eabffb Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Thu, 5 May 2022 13:24:38 -0400 Subject: [PATCH 49/90] Remove very old migration guide for SDK 4.0. (#200) --- MigrationGuide.md | 255 ---------------------------------------------- 1 file changed, 255 deletions(-) delete mode 100644 MigrationGuide.md diff --git a/MigrationGuide.md b/MigrationGuide.md deleted file mode 100644 index 92188b91..00000000 --- a/MigrationGuide.md +++ /dev/null @@ -1,255 +0,0 @@ -LaunchDarkly Migration Guide for the iOS Swift SDK -================================================== - -# Getting Started -**This documentation is for migrating from the v2.x or v3.x Objective-C implementations of the SDK to the previous major version of the SDK. For information on upgrading from 4.x to the 5.0 major release, see the [Migration Guide](https://docs.launchdarkly.com/sdk/client-side/ios/migration-4-to-5) on the documentation site.** - -Follow the steps below to migrate your app from v2.x or v3.x to v4.x. Most of these steps will be required, your IDE will give you build errors if you don't take them. We recommend reading through the guide before starting to get a feel for what you will need to do to migrate your app to v4.x. - -### Multiple Environments -Version 4.1 does not support multiple environments. If you use version 2.14.0 or later and set LDConfig's `secondaryMobileKeys` you will not be able to migrate to version 4.1. Multiple Environments will be added in a future release to the Swift SDK. - -### Swift Version -Version 4.x is built on Swift 5 using Xcode 10. The SDK will not build using previous Swift versions, and therefore you must use Xcode 10 or later to build. - -[API Differences from v2.x or v3.x](#api-differences-from-v2x-or-v3x) lists all of the changes to the API. - -## Migration Steps -1. Integrate the v4.x SDK into your app. (e.g. CocoaPods podfile or Carthage cartfile change to point to v4.1.1). See [Integrate the Swift SDK into your app using either CocoaPods or Carthage](#integrate-the-swift-sdk-into-your-app-using-either-cocoapods-or-carthage) for details. -2. Clean, and delete Derived Data. -3. Update imports to `LaunchDarkly`. See [Update imports to `LaunchDarkly`](#update-imports-to-launchdarkly) for details. -4. In Swift code, replace instances of `LDClient.sharedInstance()` with `LDClient.shared`. Do not change Objective-C `[LDClient sharedInstance]` occurrences. -5. Update `LDUser` and properties. See [Update `LDUser` and properties](#update-lduser-and-properties) for details. -6. Update `LDConfig` and properties. See [Update `LDConfig` and properties](#update-ldconfig-and-properties) for details. -7. Update `LDClient` Controls. See [Update `LDClient` Controls](#update-ldclient-controls) for details. -8. Update `LDClient` Feature Flag access. See [Update `LDClient` Feature Flag access](#update-ldclient-feature-flag-access) for details. -9. Install `LDClient` Feature Flag Observers. See [Install `LDClient` Feature Flag Observers](#install-ldclient-feature-flag-observers) for details. -10. Remove `LDClientDelegate` methods if they were not re-used. See [Remove `LDClientDelegate` methods](#remove-ldclientdelegate-methods) for details. -11. Install exception handling for `LDClient` `trackEvent` calls. See [Install `LDClient.trackEvent` exception handling](#install-ldclienttrackevent-exception-handling) for details. - -### Integrate the Swift SDK into your app using either CocoaPods or Carthage -#### CocoaPods -- Add `use_frameworks!` to either the root or to any targets that include `LaunchDarkly` - -#### Carthage -- Replace the `Darkly.framework` for each target with `LaunchDarkly.framework`. Non-iOS platform frameworks include the platform in the framework name, e.g. `LaunchDarkly_tvOS.framework`. The `DarklyEventSource.framework` should also be present, just as with v2.x. -- For Objective-C apps using Carthage, turn on `Always Embed Swift Standard Libraries` in Build Settings -- If you use the copy frameworks carthage script, replace `Darkly.framework` with `LaunchDarkly.framework` (or the framework with the platform name) in both the Input and Output files entries. - -### Update imports to `LaunchDarkly` -The module was renamed from `Darkly` to `LaunchDarkly`. -- Objective-C apps replace any `#import` that has a LD header with a single `@import LaunchDarkly;` -- Swift apps replace `import Darkly` with `import LaunchDarkly`. -- CocoaPods users import `LaunchDarkly` for all platforms, similar to prior releases. -- For non-iOS platforms built outside of CocoaPods, append the platform name to LaunchDarkly, e.g. `@import LaunchDarkly_macOS;` or `import LaunchDarkly_watchOS`. - -### Update `LDUser` and properties -- Replace any references to `LDUserBuilder` or `LDUserModel` with `LDUser` -- Replace constructor calls to use the `withKey` constructor -- Replace references to `.ip` with `.ipAddress` -- Replace references to `.custom` with `.custom`, and convert the object set into a `[String: Any]` - -See [Custom users with `LDUser`](#custom-users-with-lduser) for details. - -### Update `LDConfig` and properties -- Change lines that set `baseUrl`, `eventsUrl`, and `streamUrl` with strings to instead set these properties with `URL` objects. -- Change lines that set `capacity` to set `eventCapacity` -- Change lines that set time-based properties (`connectionTimeout`, `flushInterval`, `pollingInterval`, and `backgroundFetchInterval`) to their `TimeInterval` property (`connectionTimeout`, `eventFlushInterval`, `flagPollingInterval`, and `backgroundFlagPollingInterval`). -- Change lines that set `streaming` to set `streamingMode`. For Swift apps, use the enum `.streaming` or `.polling`. For Objective-C apps, set to `YES` for streaming, and `NO` for polling as with the v2.x or v3.x SDK. Note that if you do not have this property set, LDConfig sets it to streaming mode for you. -- Change lines that set `debugEnabled` to set `debugMode` instead. - -See [Configuration with LDConfig](#configuration-with-ldconfig) for details. - -### Update `LDClient` Controls -- Replace references to `LDClient.sharedInstance()` in Swift code to `LDClient.shared`. Do not change references to `sharedInstance` in Objective-C code. -- Replace references to `ldUser` to `user`. -- Replace references to `ldConfig` to `config`. -- Remove any references to `delegate`. -- Change calls to `start` to use the parameter name `config`. -- Change calls to `start` to use the parameter name `user`. -- Change calls to `start` to not expect a return value. If desired, capture the SDK's online status using `isOnline`. -- Change calls to `stopClient` to `stop` - -See [`LDClient` Controls](#ldclient-controls) for details. - -### Update `LDClient` Feature Flag access -- For Objective-C apps, add `ForKey` to each `variation` method call. e.g. `[[LDClient sharedInstance] boolVariationForKey:some-key fallback:fallback-value]` -- For Swift apps remove the type in the variation call, and add `forKey:` to the first parameter. e.g. `LDClient.shared.variation(forKey: some-key, fallback: fallback-value)`. -- Replace calls to `numberVariation` with `integerVariation` or `doubleVariation` as appropriate. Replace the type of the `integerVariation` call with `Int` (Swift) or `NSInteger` (Objective-C). For Objective-C apps, the client can wrap the result into an NSNumber if needed. - -See [Getting Feature Flag Values](#getting-feature-flag-values) for details. - -### Install `LDClient` Feature Flag Observers -For any object (Swift enum and struct cannot be feature flag observers) that conformed to `LDClientDelegate`, set observers on the `LDClient` that correspond to the implemented delegate methods. Use the steps below as a guide. -- `featureFlagDidUpdate` was called whenever any feature flag was updated. Assess the feature flags the former delegate was interested in. - 1. If the object watches a single feature flag, use `observe(key:, owner:, handler:)`. You can call the original delegate method from the `handler`, or copy the code in the delegate method into the handler. The `LDChangedFlag` passed into the handler has the `key` for the changed flag. - 2. If the object watches a set of feature flags, but not all of them in the environment, use `observe(keys:, owner:, handler:)`. As with 1 above, you can call the original delegate method by looping through the keys in the `[LDFlagKey, LDChangedFlag]` passed into the handler. Or you may copy the delegate method code into the handler. Each changed feature flag has an entry in `changedFlags` that contains a `LDChangedFlag` you can use to update the client app. - 3. If the object watches all of the feature flags in an environment (available with a specific `mobileKey`) use `observeAll(owner: , handler:)`. Follow the guidance in #2 above to unpack changed feature flag details. -- `userDidUpdate` was called whenever any feature flag changed. Follow the guidance under `featureFlagDidUpdate` to decide which observer method to call on the client. -- `userUnchanged` was called in `.polling` mode when the response to a flag request did not change any feature flag value. If your app uses `.streaming` mode (whether you set it explicitly or accept the default value in `LDConfig`) you can ignore this method. If using `.polling` mode, call `observeFlagsUnchanged` and either call the delegate method from within the handler, or copy the delegate method code into the handler. - 4. `serverConnectionUnavailable` was called when the SDK could not connect to LaunchDarkly servers. Call `observeError(owner:, handler:)` with a handler to execute when an error occurs. - -See [Monitoring Feature Flags for changes](#monitoring-feature-flags-for-changes) for details. - -### Remove `LDClientDelegate` methods. -If they were not re-used when implementing observers, you can delete the former `LDClientDelegate` methods. - -### Install `LDClient.trackEvent` exception handling -See [Event Controls](#event-controls) for more details. -#### Swift Client Apps -Wrap calls to `trackEvent` into do-catch statements. If desired, catch `JSONSerialization.JSONError.invalidJsonObject` errors. Alternatively, add `throws` to any method calls `trackEvent` to allow the calling method to handle the error. -#### Objective-C Client Apps -Calls to `trackEvent` include a 3rd parameter `error`, which the SDK sets when a call receives invalid JSON data. To verify the `error` object set by `trackEvents` threw a `JSONSerialization.JSONError.invalidJsonObject` error, compare the `domain` to `LaunchDarklyJSONErrorDomain` and the `code` to `LaunchDarklyJSONErrorInvalidJsonObject`. - ---- -## API Differences from v2.x or v3.x -This section details the changes between the v2.x or v3.x and v4.x APIs. - -### Configuration with `LDConfig` -LDConfig has changed to a `struct`, and therefore uses value semantics. - -#### Changed `LDConfig` Properties -##### URL properties (`baseUrl`, `eventsUrl`, and `streamUrl`) -These properties have changed to `URL` objects. Set these properties by converting URL strings to a URL using: -```swift - ldconfig.baseUrl = URL(string: "https://custom.url.string")! -``` -##### `capacity` -This property has changed to `eventCapacity`. -##### Time based properties (`connectionTimeout`, `flushInterval`, `pollingInterval`, and `backgroundFetchInterval`) -These properties have changed to `TimeInterval` properties. -- `flushInterval` has changed to `eventFlushInterval`. -- `pollingInterval` has changed to `flagPollingInterval`. -- `backgroundFetchInterval` has changed to `backgroundFlagPollingInterval` - -##### `streaming` -This property has changed to `streamingMode` and to an enum type `LDStreamingMode`. Swift apps use the new API. The default remains `.streaming`. To set polling mode, set this property to `.polling`. Objective-C apps continue to use `streaming` set to `YES` or `NO` -##### `debugEnabled` -This property has changed to `debugMode`. - -#### New `LDConfig` Properties and Methods -##### `enableBackgroundUpdates` -Set this property to `true` to allow the SDK to poll while running in the background. -**NOTE**: Background polling requires additional client app support. -##### `startOnline` -Set this property to `false` if you want the SDK to remain offline after you call `start()` -##### `Minima` -We created the Minima struct and defined polling and background polling minima there. This allows the client app to ensure the values set into the corresponding properties meet the requirements for those properties. Access these via the `minima` property on your LDConfig struct. -##### `==` -LDConfig conforms to `Equatable`. - -#### `LDConfig` Objective-C Compatibility -Since Objective-C does not represent `struct` items, the SDK wraps the LDConfig into a `NSObject` based wrapper with all the same properties as the Swift `struct`. The class `ObjcLDConfig` encapsulates the wrapper class. Objective-C client apps should refer to `LDConfig` and allow the Swift runtime to handle the conversion. Mixed apps can use the `LDConfig` var `objcLdConfig` to vend the Objective-C wrapper if needed to pass the `LDConfig` to an Objective-C method. -The type changes mentioned above all apply to the Objective-C LDConfig. `Int` types become `NSInteger` types in Objective-C, replacing `NSNumber` objects from v2.x or v3.x. -An Objective-C `isEqual` method provides LDConfig object comparison capability. - -### Custom users with `LDUser` -`LDUser` replaces `LDUserBuilder` and `LDUserModel` from v2.x or v3.x. `LDUser` is a Swift `struct`, and therefore uses value semantics. -#### Changed `LDUser` Properties -Since the only required property is `key`, all other properties are Optional. While this is not really a change from v2.x or v3.x, it is more explicit in Swift and may require some Optional handling that was not required in v2.x or v3.x. -##### `ip` -This property has changed to `ipAddress`. -##### `custom` -This property has changed to `custom` and its type has also changed to [String: Any]?. - -#### New `LDUser` Properties and Methods -##### `CodingKeys` -We added coding keys for all of the user properties. If you add your own custom attributes, you might want to extend `CodingKeys` to include your custom attribute keys. -##### `privatizableAttributes` -This new static property contains a `[String]` with the attributes that can be made private. This list is used if the LDConfig has the flag `allUserAttributesPrivate` set. -##### `device` -The SDK sets this property with the system provided device string. -##### `operatingSystem` -The SDK sets this property with the system provided operating system string. -##### `init(object:)` and `init(userDictionary:)` -These methods allows you to pass in a `[String: Any]` to create a user. Any other object passed in returns a `nil`. Use the `CodingKeys` to set user properties in the dictionary. -##### `==` -LDUser conforms to `Equatable`. - -#### `LDUser` Objective-C Compatibility -Since Objective-C does not represent `struct` items, the SDK wraps the LDUser into a `NSObject` based wrapper with all the same properties as the Swift `struct`. The class `ObjcLDUser` encapsulates the wrapper class. Objective-C client apps should refer to `LDUser` and allow the Swift runtime to handle the conversion. Mixed apps can use the `LDUser` var `objcLdUser` to vend the Objective-C wrapper if needed to pass the `LDUser` to an Objective-C method. -An Objective-C `isEqual` method provides LDConfig object comparison capability. -##### `CodingKeys` -Since `CodingKeys` is not accessible to Objective-C, we defined class vars for attribute names, allowing you to define a user dictionary that you can pass into constructors. -##### Constructors -The new constructors added to Swift were translated to Objective-C also. Use `[[LDUser alloc] initWithObject:]` and `[[LDUser alloc] initWithUserDictionary:]` to access them. -##### `isEqual` -An Objective-C `isEqual` method provides `LDUser` object comparison capability. - -### `LDClient` Controls -#### Changed `LDClient` Properties & Methods -##### `sharedInstance` -This property has changed to `shared`. -##### `ldUser` -This property has changed to `user` and its type has changed to `LDUser`. Client apps can set the `user` directly. -##### `ldConfig` -This property has changed to `config`. Client apps can set the `config` directly. -##### `delegate` -This property was removed. See [Replacing LDClient delegate methods](#replacing-ldclient-delegate-methods) -##### `start` -- `inputConfig` has changed to `config`. -- `withUserBuilder` has changed to `user` and its type changed to `LDUser` -- `completion` was added to get a callback when the SDK has completed starting -- The return value has been removed. Use `isOnline` to determine the SDK's online status. - -##### `stopClient` -This method was renamed to `stop()` -##### `updateUser` -This method was removed. Set the `user` property instead. - -#### Objective-C `LDClient` Compatibility -`LDClient` does not inherit from NSObject, and is therefore not directly available to Objective-C. Instead, the class `ObjcLDClient` wraps the LDClient. Since the wrapper inherits from NSObject, Objective-C apps can access the LDClient. We have defined the Objective-C name for `ObjcLDClient` to `LDClient`, so you access the client through `LDClient` just as before. - -`shared` isn't used with Objective-C, continue to use `sharedInstance`. - -### Getting Feature Flag Values -#### `variation()` -Swift Generics allowed us to combine the `variation` methods that were used in the v2.x or v3.x SDK. v4.x has one `variation` method that returns a non-Optional type that matches the non-Optional type the client app provides in the `fallback` parameter. A second `variation` method allows the client app to use an Optional as the `fallback`. This method requires the client to specify the Optional type when passing `nil` as the `fallback`. For this second method, the fallback parameter is defaulted to `nil`. When using this second method, set the type on the item holding the return value, e.g. -```swift - let boolFlagValue: Bool? = LDClient.shared.variation(forKey: "bool-flag-key") -``` -#### `variationAndSource()` -A new `variationAndSource()` method returns a tuple `(value, source)` that allows the client app to see what the source of the value was. `source` is an `LDFlagValueSource` enum with `.server`, `.cache`, and `.fallback`. As with the `variation` methods, there are two `variationAndSource` methods. The first returns a non-Optional type as the tuple's `value`, while the second returns an Optional type as the tuple's `value`. This second method requires the client to specify the Optional type when passing `nil` as the `fallback`. For this second method, the fallback parameter is defaulted to `nil`. When using this second method, set the type on the item holding the return value, e.g. -```swift - let (boolFlagValue, boolFlagSource): (Bool?, LDFlagValueSource) = LDClient.shared.variationAndSource(forKey: "bool-flag-key") -``` -#### `allFlagValues` -A new computed property `allFlagValues` returns a `[LDFlagKey: Any]` that has all feature flag keys and their values. This dictionary is a snapshot taken when `allFlagValues` was requested. The SDK does not try to keep these values up-to-date, and does not record any events when accessing the dictionary. -#### Objective-C Feature Flag Value Compatibility -Swift generic functions cannot operate in Objective-C. The `ObjcLDClient` wrapper retains the type-based variation methods used in v2.x or v3.x, except for `numberVariation`. A new `integerVariation` method reports NSInteger feature flags. NSNumbers that were decimal numbers should use `doubleVariation`. - -The wrapper also includes new type-based `variationAndSource` methods that return a type-based `VariationValue` object (e.g. `LDBoolVariationValue`) that encapsulates the `value` and `source`. `source` is an `ObjcLDFlagValueSource` Objective-C int backed enum (accessed in Objective-C via `LDFlagValueSource`). In addition to `server`, `cache`, and `fallback`, other possible values could be `nilSource` and `typeMismatch`. Feature Flag types that are object types have value types that are nullable in the wrapper. Take care to verify a value exists before using it. - -### Monitoring Feature Flags for changes -v4.x removes the `LDClientDelegate`, which included `featureFlagDidUpdate` and `userDidUpdate` that the SDK called to notify client apps of changes in the set of feature flags for a given mobile key (called the environment), and the `userUnchanged` that the SDK called in `.polling` mode when the response to a feature flag request did not change any feature flag value. In order to have the SDK notify the client app when feature flags change, we have provided a closure based observer API. -#### Single-key `observe()` -To monitor a single feature flag, set a callback handler using `observe(key:, owner:, handler:)`. The SDK will keep a weak reference to the `owner`. When an observed feature flag changes, the SDK executes the closure, passing into it an `LDChangedFlag` that provides the `key`, `oldValue`, `oldValueSource`, `newValue`, and `newValueSource`. The client app can use this to update itself with the new value, or use the closure as a trigger to make another `variation` request from the LDClient. -#### Multiple-key `observe()` -To monitor a set of feature flags, set a callback handler using `observe(keys: owner: handler:)`. This functions similar to the single feature flag observer. When any of the observed feature flags change, the SDK will call the closure one time. The closure takes a `[LDFlagKey: LDChangedFlag]` which the client app can use to update itself with the new values. -#### All-Flags `observeAll()` -To monitor all feature flags in an environment, set a callback handler using `observeAll()`. This functions similar to the multiple-key feature flag observer. When any feature flag in the environment changes, the SDK will call the closure one time. -#### `observeFlagsUnchanged()` -To monitor when a polling request completes with no changes to the environment, set a callback handler using `observeFlagsUnchanged()`. If the SDK is in `.polling` mode, and a flag request did not change any flag values, the SDK will call this closure. (NOTE: In `.streaming` mode, there is no event that signals flags are unchanged. Therefore this callback will be ignored in `.streaming` mode). This method effectively replaces the LDClientDelegate method `userUnchanged`. -#### `stopObserving()` -To discontinue monitoring all feature flags for a given object, call this method. Note that this is not required, the SDK will only keep a weak reference to observers. When the observer goes out of scope, the SDK reference will be nil'd out, and the SDK will no longer call that handler. -#### Objective-C Observer Support -The LDClient wrapper provides type-based single-key observer methods that function as described above. The only difference is that the object passed into the observer block will contain type-based Objective-C wrappers for `LDChangedFlag`. `observeKeys` provides multiple-key observing, and `observeAllKeys` provides all-key observing. These function as described above, except that the dictionary passed into the handler will contain Objective-C type-based wrappers that encapsulate the LDChangedFlag properties. - -### Event Controls -#### Changed Event Controls -##### `flush` -This method has changed to `reportEvents()`. -##### `track` -This method has changed to `trackEvent`. `trackEvent` can now throw if the `data` parameter does not contain a valid JSON Object, or `nil`. This check is made at run-time. For Objective-C client apps, the method has an additional `error` parameter, which the SDK populates when the data is not a valid JSON object. -#### New Event Controls -##### `JSONSerialization` -A new enum `JSONError` has cases the SDK uses when a JSON object does not meet expectations. `notADictionary` and `invalidJsonObject` cases were added. The SDK does not throw `notADictionary` to client apps. Client apps should handle `invalidJsonObject` errors thrown from `trackEvent` -A new string constant `LaunchDarklyJSONErrorDomain` provides the ability for Objective-C apps to verify the `NSError` `domain`. The SDK sets this domain for `JSONError`s reported via `trackEvent` in Objective-C only. - -## Replacing LDClient delegate methods -### `featureFlagDidUpdate` and `userDidUpdate` -The `observe` methods provide the ability to monitor feature flags individually, as a collection, or the whole environment. The SDK will release these observers when they go out of scope, so you can set and forget them. Of course if you need to stop observing you can do that also. -### `userUnchanged` -The `observeFlagsUnchanged` method sets an observer called in `.polling` mode when a flag request leaves the flags unchanged, effectively replacing `userUnchanged`. -### `serverConnectionUnavailable` -The `observeError(owner:, handler:)` method sets an observer called when an error occurs during flag processing. Replace the delegate method with a call to this method, or call the former delegate method from within the handler set with this method. From 0fcfece82869a185bb7a4a5a3b8f8b3cf7688c2f Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Tue, 17 May 2022 09:49:52 -0600 Subject: [PATCH 50/90] Track optionality of isAnonymous property. (#201) --- LaunchDarkly/LaunchDarkly/LDClient.swift | 2 +- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 23 +++++++++++++++---- .../Models/User/LDUserSpec.swift | 9 ++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index dab69a55..d7b5fb16 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -537,7 +537,7 @@ public class LDClient { return } 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))") + Log.debug(typeName(and: #function) + "key: \(key), data: \(String(describing: data)), metricValue: \(String(describing: metricValue))") eventReporter.record(event) } diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 681df574..3f4552bd 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -41,7 +41,19 @@ public struct LDUser: Encodable, Equatable { /// 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 + public var isAnonymous: Bool { + get { isAnonymousNullable == true } + set { isAnonymousNullable = newValue } + } + + /** + Whether or not the user is anonymous, if that has been specified (or set due to the lack of a `key` property). + + Although the `isAnonymous` property defaults to `false` in terms of LaunchDarkly's indexing behavior, for historical + reasons flag evaluation may behave differently if the value is explicitly set to `false` verses being omitted. This + field allows treating the property as optional for consisent evaluation with other LaunchDarkly SDKs. + */ + public var isAnonymousNullable: Bool? /** Client app defined privateAttributes for the user. @@ -91,7 +103,10 @@ public struct LDUser: Encodable, Equatable { self.ipAddress = ipAddress self.email = email self.avatar = avatar - self.isAnonymous = isAnonymous ?? (selectedKey == LDUser.defaultKey(environmentReporter: environmentReporter)) + self.isAnonymousNullable = isAnonymous + if isAnonymous == nil && selectedKey == LDUser.defaultKey(environmentReporter: environmentReporter) { + self.isAnonymousNullable = true + } self.custom = custom ?? [:] self.custom.merge(["device": .string(environmentReporter.deviceModel), "os": .string(environmentReporter.systemVersion)]) { lhs, _ in lhs } @@ -135,8 +150,8 @@ public struct LDUser: Encodable, Equatable { var container = encoder.container(keyedBy: DynamicKey.self) try container.encode(key, forKey: DynamicKey(stringValue: "key")!) - if isAnonymous { - try container.encode(isAnonymous, forKey: DynamicKey(stringValue: "anonymous")!) + if let anonymous = isAnonymousNullable { + try container.encode(anonymous, forKey: DynamicKey(stringValue: "anonymous")!) } try LDUser.optionalAttributes.forEach { attribute in diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 4cc99f1e..98a641f7 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -36,6 +36,7 @@ final class LDUserSpec: QuickSpec { expect(user.firstName) == LDUser.StubConstants.firstName expect(user.lastName) == LDUser.StubConstants.lastName expect(user.isAnonymous) == LDUser.StubConstants.isAnonymous + expect(user.isAnonymousNullable) == LDUser.StubConstants.isAnonymous expect(user.country) == LDUser.StubConstants.country expect(user.ipAddress) == LDUser.StubConstants.ipAddress expect(user.email) == LDUser.StubConstants.email @@ -43,6 +44,11 @@ final class LDUserSpec: QuickSpec { expect(user.custom == LDUser.StubConstants.custom(includeSystemValues: true)).to(beTrue()) expect(user.privateAttributes) == LDUser.optionalAttributes } + it("without setting anonymous") { + user = LDUser(key: "abc") + expect(user.isAnonymous) == false + expect(user.isAnonymousNullable).to(beNil()) + } context("called without optional elements") { var environmentReporter: EnvironmentReporter! beforeEach { @@ -52,6 +58,7 @@ final class LDUserSpec: QuickSpec { it("creates a LDUser without optional elements") { expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) expect(user.isAnonymous) == true + expect(user.isAnonymousNullable) == true expect(user.name).to(beNil()) expect(user.firstName).to(beNil()) @@ -79,6 +86,7 @@ final class LDUserSpec: QuickSpec { users.forEach { user in expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) expect(user.isAnonymous) == true + expect(user.isAnonymousNullable) == true } } } @@ -96,6 +104,7 @@ final class LDUserSpec: QuickSpec { it("creates a user with system values matching the environment reporter") { expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) expect(user.isAnonymous) == true + expect(user.isAnonymousNullable) == true expect(user.secondary).to(beNil()) expect(user.name).to(beNil()) From a212925a6f42ca5f7312d795b2db9fbb210fce89 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 18 May 2022 15:35:01 -0400 Subject: [PATCH 51/90] Add SDK contract tests (#199) --- .circleci/config.yml | 38 +++ .gitignore | 8 +- .swiftlint.yml | 2 +- ContractTests/.swiftlint.yml | 57 +++++ ContractTests/Package.swift | 31 +++ .../Source/Controllers/SdkController.swift | 225 ++++++++++++++++++ ContractTests/Source/Models/client.swift | 49 ++++ ContractTests/Source/Models/command.swift | 71 ++++++ ContractTests/Source/Models/status.swift | 6 + ContractTests/Source/Models/user.swift | 30 +++ ContractTests/Source/app.swift | 14 ++ ContractTests/Source/boot.swift | 6 + ContractTests/Source/configure.swift | 14 ++ ContractTests/Source/main.swift | 15 ++ ContractTests/Source/routes.swift | 12 + ContractTests/testharness-suppressions.txt | 75 ++++++ .../ServiceObjects/EnvironmentReporter.swift | 3 +- Makefile | 19 ++ 18 files changed, 668 insertions(+), 7 deletions(-) create mode 100644 ContractTests/.swiftlint.yml create mode 100644 ContractTests/Package.swift create mode 100644 ContractTests/Source/Controllers/SdkController.swift create mode 100644 ContractTests/Source/Models/client.swift create mode 100644 ContractTests/Source/Models/command.swift create mode 100644 ContractTests/Source/Models/status.swift create mode 100644 ContractTests/Source/Models/user.swift create mode 100644 ContractTests/Source/app.swift create mode 100644 ContractTests/Source/boot.swift create mode 100644 ContractTests/Source/configure.swift create mode 100644 ContractTests/Source/main.swift create mode 100644 ContractTests/Source/routes.swift create mode 100644 ContractTests/testharness-suppressions.txt create mode 100644 Makefile diff --git a/.circleci/config.yml b/.circleci/config.yml index 64fb5d8e..9ab3bb79 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,42 @@ version: 2.1 + jobs: + contract-tests: + macos: + xcode: '13.1.0' + + steps: + - checkout + + - run: + name: Install swift lint + command: brew install swiftlint + - run: + name: Run swiftlint + command: | + cd ./ContractTests + swiftlint lint --reporter junit | tee /tmp/contract-test-swiftlint-results.xml + - store_artifacts: + path: /tmp/contract-test-swiftlint-results.xml + - store_test_results: + path: /tmp/contract-test-swiftlint-results.xml + + - run: + name: Install required ssl libraries + command: brew install libressl + - run: + name: make test output directory + command: mkdir /tmp/test-results + - run: make build-contract-tests + - run: + command: make start-contract-test-service + background: true + - run: + name: run contract tests + command: TEST_HARNESS_PARAMS="-junit /tmp/test-results/contract-tests-junit.xml" make run-contract-tests + - store_test_results: + path: /tmp/test-results/ + build: parameters: xcode-version: @@ -146,3 +183,4 @@ workflows: name: Xcode 11.7 - Swift 5.2 xcode-version: '11.7.0' ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=12.4' + - contract-tests diff --git a/.gitignore b/.gitignore index ff647b61..9e9bbc74 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,10 @@ xcuserdata *.playground /default.profraw /build -/.build +**/.build /docs /Carthage/Checkouts -/.swiftpm -/Package.resolved +**/.swiftpm +**/Package.resolved /LaunchDarkly.xcworkspace/xcshareddata/swiftpm/Package.resolved -/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved \ No newline at end of file +/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/.swiftlint.yml b/.swiftlint.yml index 8f35130b..71ad606f 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -57,4 +57,4 @@ identifier_name: - lhs - rhs -reporter: "xcode" \ No newline at end of file +reporter: "xcode" diff --git a/ContractTests/.swiftlint.yml b/ContractTests/.swiftlint.yml new file mode 100644 index 00000000..61290d2c --- /dev/null +++ b/ContractTests/.swiftlint.yml @@ -0,0 +1,57 @@ +disabled_rules: + - line_length + - trailing_whitespace + +opt_in_rules: + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - empty_count + - first_where + - flatmap_over_map_reduce + - implicitly_unwrapped_optional + - let_var_whitespace + - missing_docs + - redundant_nil_coalescing + - sorted_first_last + - trailing_closure + - unused_declaration + - unused_import + - vertical_whitespace_closing_braces + +included: + - Source + +excluded: + +function_body_length: + warning: 50 + error: 70 + +type_body_length: + warning: 300 + error: 500 + +file_length: + warning: 1000 + error: 1500 + +identifier_name: + min_length: # only min_length + warning: 3 # only warning + max_length: + warning: 50 + error: 60 + excluded: + - id + - URL + - url + - obj + - key + - all + - tag + - lhs + - rhs + +reporter: "xcode" diff --git a/ContractTests/Package.swift b/ContractTests/Package.swift new file mode 100644 index 00000000..2bf68a23 --- /dev/null +++ b/ContractTests/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version:5.2 + +import PackageDescription + +let package = Package( + name: "ContractTests", + platforms: [ + .iOS(.v10), + .macOS(.v10_12), + .watchOS(.v3), + .tvOS(.v10) + ], + products: [ + .executable( + name: "ContractTests", + targets: ["ContractTests"]) + ], + dependencies: [ + Package.Dependency.package(name: "LaunchDarkly", path: ".."), + .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0") + ], + targets: [ + .target( + name: "ContractTests", + dependencies: [ + .product(name: "LaunchDarkly", package: "LaunchDarkly"), + .product(name: "Vapor", package: "vapor") + ], + path: "Source"), + ], + swiftLanguageVersions: [.v5]) diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift new file mode 100644 index 00000000..71a4c14f --- /dev/null +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -0,0 +1,225 @@ +import Vapor +import LaunchDarkly + +final class SdkController { + private var clients: [Int : LDClient] = [:] + private var clientCounter = 0 + + func status(_ req: Request) -> StatusResponse { + let capabilities = [ + "client-side", + "mobile", + "service-endpoints", + "tags" + ] + + return StatusResponse( + name: "ios-swift-client-sdk", + capabilities: capabilities) + } + + func createClient(_ req: Request) throws -> Future { + return try req.content.decode(CreateInstance.self).map { createInstance in + var config = LDConfig(mobileKey: createInstance.configuration.credential) + config.enableBackgroundUpdates = true + config.isDebugMode = true + + if let streaming = createInstance.configuration.streaming { + if let baseUri = streaming.baseUri { + config.streamUrl = URL(string: baseUri)! + } + + // TODO(mmk) Need to hook up initialRetryDelayMs + } + + if let polling = createInstance.configuration.polling { + if let baseUri = polling.baseUri { + config.baseUrl = URL(string: baseUri)! + } + } + + if let events = createInstance.configuration.events { + if let baseUri = events.baseUri { + config.eventsUrl = URL(string: baseUri)! + } + + if let capacity = events.capacity { + config.eventCapacity = capacity + } + + if let enable = events.enableDiagnostics { + config.diagnosticOptOut = !enable + } + + if let allPrivate = events.allAttributesPrivate { + config.allUserAttributesPrivate = allPrivate + } + + if let globalPrivate = events.globalPrivateAttributes { + config.privateUserAttributes = globalPrivate.map({ UserAttribute.forName($0) }) + } + + if let flushIntervalMs = events.flushIntervalMs { + config.eventFlushInterval = flushIntervalMs + } + + if let inlineUsers = events.inlineUsers { + config.inlineUserInEvents = inlineUsers + } + } + + // TODO(mmk) Handle tag parameters + + let clientSide = createInstance.configuration.clientSide + + if let autoAliasingOptOut = clientSide.autoAliasingOptOut { + config.autoAliasingOptOut = autoAliasingOptOut + } + + if let evaluationReasons = clientSide.evaluationReasons { + config.evaluationReasons = evaluationReasons + } + + if let useReport = clientSide.useReport { + config.useReport = useReport + } + + let dispatchSemaphore = DispatchSemaphore(value: 0) + let startWaitSeconds = (createInstance.configuration.startWaitTimeMs ?? 5_000) / 1_000 + + LDClient.start(config:config, user: clientSide.initialUser, startWaitSeconds: startWaitSeconds) { timedOut in + dispatchSemaphore.signal() + } + + dispatchSemaphore.wait() + + let client = LDClient.get()! + + self.clientCounter += 1 + self.clients.updateValue(client, forKey: self.clientCounter) + + var headers = HTTPHeaders() + headers.add(name: "Location", value: "/clients/\(self.clientCounter)") + + var response = HTTPResponse() + response.status = .ok + response.headers = headers + + return response + } + } + + func shutdownClient(_ req: Request) throws -> HTTPStatus { + let id = try req.parameters.next(Int.self) + guard let client = self.clients[id] else { + return HTTPStatus.badRequest + } + + client.close() + clients.removeValue(forKey: id) + + return HTTPStatus.accepted + } + + func executeCommand(_ req: Request) throws -> Future { + return try req.content.decode(CommandParameters.self).map { commandParameters in + guard let client = self.clients[self.clientCounter] else { + throw Abort(.badRequest) + } + + switch commandParameters.command { + case "evaluate": + let result: EvaluateFlagResponse = try self.evaluate(client, commandParameters.evaluate!) + return CommandResponse.evaluateFlag(result) + case "evaluateAll": + let result: EvaluateAllFlagsResponse = try self.evaluateAll(client, commandParameters.evaluateAll!) + return CommandResponse.evaluateAll(result) + case "identifyEvent": + let semaphore = DispatchSemaphore(value: 0) + client.identify(user: commandParameters.identifyEvent!.user) { + semaphore.signal() + } + semaphore.wait() + case "aliasEvent": + client.alias(context: commandParameters.aliasEvent!.user, previousContext: commandParameters.aliasEvent!.previousUser) + case "customEvent": + let event = commandParameters.customEvent! + client.track(key: event.eventKey, data: event.data, metricValue: event.metricValue) + case "flushEvents": + client.flush() + default: + throw Abort(.badRequest) + } + + return CommandResponse.ok + } + } + + func evaluate(_ client: LDClient, _ params: EvaluateFlagParameters) throws -> EvaluateFlagResponse { + switch params.valueType { + case "bool": + if case let LDValue.bool(defaultValue) = params.defaultValue { + if params.detail { + let result = client.boolVariationDetail(forKey: params.flagKey, defaultValue: defaultValue) + return EvaluateFlagResponse(value: LDValue.bool(result.value), variationIndex: result.variationIndex, reason: result.reason) + } + + let result = client.boolVariation(forKey: params.flagKey, defaultValue: defaultValue) + return EvaluateFlagResponse(value: LDValue.bool(result)) + } + throw "Failed to convert \(params.valueType) to bool" + case "int": + if case let LDValue.number(defaultValue) = params.defaultValue { + if params.detail { + let result = client.intVariationDetail(forKey: params.flagKey, defaultValue: Int(defaultValue)) + return EvaluateFlagResponse(value: LDValue.number(Double(result.value)), variationIndex: result.variationIndex, reason: result.reason) + } + + let result = client.intVariation(forKey: params.flagKey, defaultValue: Int(defaultValue)) + return EvaluateFlagResponse(value: LDValue.number(Double(result))) + } + throw "Failed to convert \(params.valueType) to int" + case "double": + if case let LDValue.number(defaultValue) = params.defaultValue { + if params.detail { + let result = client.doubleVariationDetail(forKey: params.flagKey, defaultValue: defaultValue) + return EvaluateFlagResponse(value: LDValue.number(result.value), variationIndex: result.variationIndex, reason: result.reason) + } + + let result = client.doubleVariation(forKey: params.flagKey, defaultValue: defaultValue) + return EvaluateFlagResponse(value: LDValue.number(result), variationIndex: nil, reason: nil) + } + throw "Failed to convert \(params.valueType) to bool" + case "string": + if case let LDValue.string(defaultValue) = params.defaultValue { + if params.detail { + let result = client.stringVariationDetail(forKey: params.flagKey, defaultValue: defaultValue) + return EvaluateFlagResponse(value: LDValue.string(result.value), variationIndex: result.variationIndex, reason: result.reason) + } + + let result = client.stringVariation(forKey: params.flagKey, defaultValue: defaultValue) + return EvaluateFlagResponse(value: LDValue.string(result), variationIndex: nil, reason: nil) + } + throw "Failed to convert \(params.valueType) to string" + default: + if params.detail { + let result = client.jsonVariationDetail(forKey: params.flagKey, defaultValue: params.defaultValue) + return EvaluateFlagResponse(value: result.value, variationIndex: result.variationIndex, reason: result.reason) + } + + let result = client.jsonVariation(forKey: params.flagKey, defaultValue: params.defaultValue) + return EvaluateFlagResponse(value: result, variationIndex: nil, reason: nil) + } + } + + func evaluateAll(_ client: LDClient, _ params: EvaluateAllFlagsParameters) throws -> EvaluateAllFlagsResponse { + let result = client.allFlags + + return EvaluateAllFlagsResponse(state: result) + } + + func shutdown(_ req: Request) -> HTTPStatus { + exit(0) + return HTTPStatus.accepted + } +} diff --git a/ContractTests/Source/Models/client.swift b/ContractTests/Source/Models/client.swift new file mode 100644 index 00000000..f7d42854 --- /dev/null +++ b/ContractTests/Source/Models/client.swift @@ -0,0 +1,49 @@ +import Vapor +import LaunchDarkly + +struct CreateInstance: Content { + var tag: String? + var configuration: Configuration +} + +struct Configuration: Content { + var credential: String + var startWaitTimeMs: Double? + var initCanFail: Bool? + var streaming: StreamingParameters? + var polling: PollingParameters? + var events: EventParameters? + var tags: TagParameters? + var clientSide: ClientSideParameters +} + +struct StreamingParameters: Content { + var baseUri: String? + var initialRetryDelayMs: Int? +} + +struct PollingParameters: Content { + var baseUri: String? +} + +struct EventParameters: Content { + var baseUri: String? + var capacity: Int? + var enableDiagnostics: Bool? + var allAttributesPrivate: Bool? + var globalPrivateAttributes: [String]? + var flushIntervalMs: Double? + var inlineUsers: Bool? +} + +struct TagParameters: Content { + var applicationId: String? + var applicationVersion: String? +} + +struct ClientSideParameters: Content { + var initialUser: LDUser + var autoAliasingOptOut: Bool? + var evaluationReasons: Bool? + var useReport: Bool? +} diff --git a/ContractTests/Source/Models/command.swift b/ContractTests/Source/Models/command.swift new file mode 100644 index 00000000..415b2aaa --- /dev/null +++ b/ContractTests/Source/Models/command.swift @@ -0,0 +1,71 @@ +import Vapor +import LaunchDarkly + +enum CommandResponse: Content, Encodable { + case evaluateFlag(EvaluateFlagResponse) + case evaluateAll(EvaluateAllFlagsResponse) + case ok + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + if case let CommandResponse.evaluateFlag(response) = self { + try container.encode(response) + return + } + + if case let CommandResponse.evaluateAll(response) = self { + try container.encode(response) + return + } + + try container.encode(true) + + return + } +} + +struct CommandParameters: Content { + var command: String + var evaluate: EvaluateFlagParameters? + var evaluateAll: EvaluateAllFlagsParameters? + var customEvent: CustomEventParameters? + var identifyEvent: IdentifyEventParameters? + var aliasEvent: AliasEventParameters? +} + +struct EvaluateFlagParameters: Content { + var flagKey: String + var valueType: String + var defaultValue: LDValue + var detail: Bool +} + +struct EvaluateFlagResponse: Content { + var value: LDValue + var variationIndex: Int? + var reason: [String: LDValue]? +} + +struct EvaluateAllFlagsParameters: Content { +} + +struct EvaluateAllFlagsResponse: Content { + var state: [LDFlagKey: LDValue]? +} + +struct CustomEventParameters: Content { + var eventKey: String + var data: LDValue? + var omitNullData: Bool + var metricValue: Double? +} + +struct IdentifyEventParameters: Content, Decodable { + var user: LDUser +} + +struct AliasEventParameters: Content { + var user: LDUser + var previousUser: LDUser +} diff --git a/ContractTests/Source/Models/status.swift b/ContractTests/Source/Models/status.swift new file mode 100644 index 00000000..8bdc2fce --- /dev/null +++ b/ContractTests/Source/Models/status.swift @@ -0,0 +1,6 @@ +import Vapor + +struct StatusResponse: Content { + var name: String + var capabilities: [String] +} diff --git a/ContractTests/Source/Models/user.swift b/ContractTests/Source/Models/user.swift new file mode 100644 index 00000000..5bdf696c --- /dev/null +++ b/ContractTests/Source/Models/user.swift @@ -0,0 +1,30 @@ +import Foundation +import LaunchDarkly + +extension LDUser: Decodable { + + /// 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 = "privateAttributeNames", secondary + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.init() + + key = try values.decodeIfPresent(String.self, forKey: .key) ?? "" + name = try values.decodeIfPresent(String.self, forKey: .name) + firstName = try values.decodeIfPresent(String.self, forKey: .firstName) + lastName = try values.decodeIfPresent(String.self, forKey: .lastName) + country = try values.decodeIfPresent(String.self, forKey: .country) + ipAddress = try values.decodeIfPresent(String.self, forKey: .ipAddress) + email = try values.decodeIfPresent(String.self, forKey: .email) + avatar = try values.decodeIfPresent(String.self, forKey: .avatar) + custom = try values.decodeIfPresent([String: LDValue].self, forKey: .custom) ?? [:] + isAnonymous = try values.decodeIfPresent(Bool.self, forKey: .isAnonymous) ?? false + let _ = try values.decodeIfPresent([String].self, forKey: .privateAttributes) + privateAttributes = (try values.decodeIfPresent([String].self, forKey: .privateAttributes) ?? []).map({ UserAttribute.forName($0) }) + secondary = try values.decodeIfPresent(String.self, forKey: .secondary) + } +} diff --git a/ContractTests/Source/app.swift b/ContractTests/Source/app.swift new file mode 100644 index 00000000..3d24e8a3 --- /dev/null +++ b/ContractTests/Source/app.swift @@ -0,0 +1,14 @@ +import Vapor + +extension String: Error {} + +public func app(_ env: Environment) throws -> Application { + var config = Config.default() + var env = env + var services = Services.default() + try configure(&config, &env, &services) + let app = try Application(config: config, environment: env, services: services) + try boot(app) + + return app +} diff --git a/ContractTests/Source/boot.swift b/ContractTests/Source/boot.swift new file mode 100644 index 00000000..9313115b --- /dev/null +++ b/ContractTests/Source/boot.swift @@ -0,0 +1,6 @@ +import Vapor + +/// Called after your application has initialized. +public func boot(_ app: Application) throws { + // Your code here +} diff --git a/ContractTests/Source/configure.swift b/ContractTests/Source/configure.swift new file mode 100644 index 00000000..3c3aca84 --- /dev/null +++ b/ContractTests/Source/configure.swift @@ -0,0 +1,14 @@ +import Vapor + +/// Called before your application initializes. +public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { + // Register routes to the router + let router = EngineRouter.default() + try routes(router) + services.register(router, as: Router.self) + + // Register middleware + var middlewares = MiddlewareConfig() // Create _empty_ middleware config + middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response + services.register(middlewares) +} diff --git a/ContractTests/Source/main.swift b/ContractTests/Source/main.swift new file mode 100644 index 00000000..aa0396e7 --- /dev/null +++ b/ContractTests/Source/main.swift @@ -0,0 +1,15 @@ +import Foundation + +let semaphore = DispatchSemaphore(value: 0) +DispatchQueue.global(qos: .userInitiated).async { + do { + try app(.detect()).run() + } catch { + } + semaphore.signal() +} + +let runLoop = RunLoop.current +while (semaphore.wait(timeout: .now()) == .timedOut) { + runLoop.run(mode: .default, before: .distantFuture) +} diff --git a/ContractTests/Source/routes.swift b/ContractTests/Source/routes.swift new file mode 100644 index 00000000..33a58076 --- /dev/null +++ b/ContractTests/Source/routes.swift @@ -0,0 +1,12 @@ +import Vapor + +public func routes(_ router: Router) throws { + let sdkController = SdkController() + router.get("/", use: sdkController.status) + router.post("/", use: sdkController.createClient) + router.delete("/", use: sdkController.shutdown) + + let clientRoutes = router.grouped("clients") + clientRoutes.post(Int.parameter, use: sdkController.executeCommand) + clientRoutes.delete(Int.parameter, use: sdkController.shutdownClient) +} diff --git a/ContractTests/testharness-suppressions.txt b/ContractTests/testharness-suppressions.txt new file mode 100644 index 00000000..8a25994b --- /dev/null +++ b/ContractTests/testharness-suppressions.txt @@ -0,0 +1,75 @@ +events/requests/method and headers +tags/stream requests/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} +tags/stream requests/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} +tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":null} +tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":""} +tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} +tags/poll requests/{"applicationId":null,"applicationVersion":null} +tags/poll requests/{"applicationId":null,"applicationVersion":""} +tags/poll requests/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} +tags/poll requests/{"applicationId":"","applicationVersion":null} +tags/poll requests/{"applicationId":"","applicationVersion":""} +tags/poll requests/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} +tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":null} +tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":""} +tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} +tags/event posts/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} +tags/event posts/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} +tags/event posts/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":null} +tags/event posts/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":""} +tags/event posts/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} +tags/disallowed characters +evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate flag with detail +evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate all flags +evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag without detail +evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag with detail +evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate all flags +streaming/requests/user properties/GET +streaming/requests/user properties/REPORT +streaming/updates/flag delete for previously nonexistent flag is applied +polling/requests/method and headers/GET +polling/requests/method and headers/REPORT +polling/requests/URL path is computed correctly/base URI has no trailing slash/GET +polling/requests/URL path is computed correctly/base URI has no trailing slash/REPORT +polling/requests/URL path is computed correctly/base URI has a trailing slash/GET +polling/requests/URL path is computed correctly/base URI has a trailing slash/REPORT +polling/requests/query parameters/evaluationReasons set to [none]/GET +polling/requests/query parameters/evaluationReasons set to [none]/REPORT +polling/requests/query parameters/evaluationReasons set to false/GET +polling/requests/query parameters/evaluationReasons set to false/REPORT +polling/requests/query parameters/evaluationReasons set to true/GET +polling/requests/query parameters/evaluationReasons set to true/REPORT +polling/requests/user properties/GET +polling/requests/user properties/REPORT +events/user properties/inlineUsers=false/user-private=none/identify event +events/user properties/inlineUsers=false/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=false, globally-private=[firstName]/user-private=none/identify event +events/user properties/inlineUsers=false, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=false, allAttributesPrivate=true/user-private=none/identify event +events/user properties/inlineUsers=false, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=false, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/identify event +events/user properties/inlineUsers=false, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=true/user-private=none/identify event +events/user properties/inlineUsers=true/user-private=none/feature event +events/user properties/inlineUsers=true/user-private=none/custom event +events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/feature event +events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/custom event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/identify event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/feature event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/custom event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/identify event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/feature event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/custom event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/feature event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/custom event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/identify event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/feature event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/custom event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift index 06f5ba86..2f88c5ee 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -16,7 +16,7 @@ enum OperatingSystem: String { static var allOperatingSystems: [OperatingSystem] { [.iOS, .watchOS, .macOS, .tvOS] } - + var isBackgroundEnabled: Bool { OperatingSystem.backgroundEnabledOperatingSystems.contains(self) } @@ -123,7 +123,6 @@ struct EnvironmentReporter: EnvironmentReporting { #endif var shouldThrottleOnlineCalls: Bool { !isDebugBuild } - let sdkVersion = "6.1.0" // Unfortunately, the following does not function in certain configurations, such as when included through SPM // var sdkVersion: String { diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..85dd13df --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +TEMP_TEST_OUTPUT=/tmp/contract-test-service.log + +build-contract-tests: + cd ./ContractTests && swift build + +start-contract-test-service: build-contract-tests + cd ./ContractTests && swift run + +start-contract-test-service-bg: + @echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)" + @make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 & + +run-contract-tests: + @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/master/downloader/run.sh \ + | VERSION=v1 PARAMS="-url http://localhost:8080 -debug -stop-service-at-end -skip-from ./ContractTests/testharness-suppressions.txt $(TEST_HARNESS_PARAMS)" sh + +contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests + +.PHONY: build-contract-tests start-contract-test-service run-contract-tests contract-tests From 5bd1b2ffe2add7384b388d7727be7e3c5e017431 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Thu, 19 May 2022 10:43:07 -0400 Subject: [PATCH 52/90] Add support for application tags (#197) --- .../Source/Controllers/SdkController.swift | 18 ++++- ContractTests/testharness-suppressions.txt | 20 ----- .../LaunchDarkly/Models/LDConfig.swift | 73 +++++++++++++++++++ .../LaunchDarkly/Networking/HTTPHeaders.swift | 7 ++ .../Models/LDConfigSpec.swift | 23 ++++++ 5 files changed, 117 insertions(+), 24 deletions(-) diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index 71a4c14f..59c1b278 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -30,9 +30,8 @@ final class SdkController { } // TODO(mmk) Need to hook up initialRetryDelayMs - } - - if let polling = createInstance.configuration.polling { + } else if let polling = createInstance.configuration.polling { + config.streamingMode = .polling if let baseUri = polling.baseUri { config.baseUrl = URL(string: baseUri)! } @@ -68,7 +67,18 @@ final class SdkController { } } - // TODO(mmk) Handle tag parameters + if let tags = createInstance.configuration.tags { + var applicationInfo = ApplicationInfo() + if let id = tags.applicationId { + applicationInfo.applicationIdentifier(id) + } + + if let verision = tags.applicationVersion { + applicationInfo.applicationVersion(verision) + } + + config.applicationInfo = applicationInfo + } let clientSide = createInstance.configuration.clientSide diff --git a/ContractTests/testharness-suppressions.txt b/ContractTests/testharness-suppressions.txt index 8a25994b..1b99e0d2 100644 --- a/ContractTests/testharness-suppressions.txt +++ b/ContractTests/testharness-suppressions.txt @@ -1,24 +1,4 @@ events/requests/method and headers -tags/stream requests/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} -tags/stream requests/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} -tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":null} -tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":""} -tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} -tags/poll requests/{"applicationId":null,"applicationVersion":null} -tags/poll requests/{"applicationId":null,"applicationVersion":""} -tags/poll requests/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} -tags/poll requests/{"applicationId":"","applicationVersion":null} -tags/poll requests/{"applicationId":"","applicationVersion":""} -tags/poll requests/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} -tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":null} -tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":""} -tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} -tags/event posts/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} -tags/event posts/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} -tags/event posts/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":null} -tags/event posts/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":""} -tags/event posts/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"} -tags/disallowed characters evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate flag with detail evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate all flags evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag without detail diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 1b47e295..163f1b8d 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -32,6 +32,76 @@ typealias MobileKey = String */ public typealias RequestHeaderTransform = (_ url: URL, _ headers: [String: String]) -> [String: String] +/// Defines application metadata. +/// +/// These properties are optional and informational. They may be used in LaunchDarkly +/// analytics or other product features, but they do not affect feature flag evaluations. +public struct ApplicationInfo: Equatable { + private var applicationId: String + private var applicationVersion: String + + public init() { + applicationId = "" + applicationVersion = "" + } + + /// A unique identifier representing the application where the LaunchDarkly SDK is running. + /// + /// This can be specified as any string value as long as it only uses the following characters: + /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other + /// characters will be ignored. + public mutating func applicationIdentifier(_ applicationId: String) { + if let error = validate(applicationId) { + Log.debug("applicationIdentifier \(error)") + return + } + + self.applicationId = applicationId + } + + /// A unique identifier representing the version of the application where the LaunchDarkly SDK + /// is running. + /// + /// This can be specified as any string value as long as it only uses the following characters: + /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other + /// characters will be ignored. + public mutating func applicationVersion(_ applicationVersion: String) { + if let error = validate(applicationVersion) { + Log.debug("applicationVersion \(error)") + return + } + + self.applicationVersion = applicationVersion + } + + func buildTag() -> String { + var tags: [String] = [] + + if !applicationId.isEmpty { + tags.append("application-id/\(applicationId)") + } + + if !applicationVersion.isEmpty { + tags.append("application-version/\(applicationVersion)") + } + + return tags.lazy.joined(separator: " ") + } + + private func validate(_ value: String) -> String? { + let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") + if value.rangeOfCharacter(from: allowed.inverted) != nil { + return "contained invalid characters" + } + + if value.count > 64 { + return "longer than 64 characters and was discarded" + } + + return nil + } +} + /** Use LDConfig to configure the LDClient. When initialized, a LDConfig contains the default values which can be changed as needed. */ @@ -169,6 +239,8 @@ public struct LDConfig { public var flagPollingInterval: TimeInterval = Defaults.flagPollingInterval /// 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 + /// The configuration for application metadata. + public var applicationInfo: ApplicationInfo? = nil /** Controls the method the SDK uses to keep feature flags updated. (Default: `.streaming`) @@ -364,6 +436,7 @@ extension LDConfig: Equatable { && lhs.eventFlushInterval == rhs.eventFlushInterval && lhs.flagPollingInterval == rhs.flagPollingInterval && lhs.backgroundFlagPollingInterval == rhs.backgroundFlagPollingInterval + && lhs.applicationInfo == rhs.applicationInfo && lhs.streamingMode == rhs.streamingMode && lhs.enableBackgroundUpdates == rhs.enableBackgroundUpdates && lhs.startOnline == rhs.startOnline diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift index d37fadee..a8bdd949 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift @@ -11,6 +11,7 @@ struct HTTPHeaders { static let ifNoneMatch = "If-None-Match" static let eventPayloadIDHeader = "X-LaunchDarkly-Payload-ID" static let sdkWrapper = "X-LaunchDarkly-Wrapper" + static let tags = "X-LaunchDarkly-Tags" } struct HeaderValue { @@ -24,12 +25,14 @@ struct HTTPHeaders { private let authKey: String private let userAgent: String private let wrapperHeaderVal: String? + private let applicationTag: String init(config: LDConfig, environmentReporter: EnvironmentReporting) { self.mobileKey = config.mobileKey self.additionalHeaders = config.additionalHeaders self.userAgent = "\(environmentReporter.systemName)/\(environmentReporter.sdkVersion)" self.authKey = "\(HeaderValue.apiKey) \(config.mobileKey)" + self.applicationTag = config.applicationInfo?.buildTag() ?? "" if let wrapperName = config.wrapperName { if let wrapperVersion = config.wrapperVersion { @@ -50,6 +53,10 @@ struct HTTPHeaders { headers[HeaderKey.sdkWrapper] = wrapperHeader } + if !self.applicationTag.isEmpty { + headers[HeaderKey.tags] = self.applicationTag + } + return headers } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index baca06dc..698ea35b 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -231,4 +231,27 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.enableBackgroundUpdates, operatingSystem.isBackgroundEnabled) } } + + func testApplicationInfoGeneratesTagCorrectly() { + var applicationInfo = ApplicationInfo() + XCTAssertEqual("", applicationInfo.buildTag()) + + applicationInfo.applicationVersion("example-version") + XCTAssertEqual("application-version/example-version", applicationInfo.buildTag()) + + applicationInfo.applicationIdentifier("example-id") + XCTAssertEqual("application-id/example-id application-version/example-version", applicationInfo.buildTag()) + } + + func testApplicationInfoRejectsInvalidConfigurations() { + let values = ["", " ", "/", ":", "🐦", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890._-"] + var info = ApplicationInfo() + + for value in values { + info.applicationIdentifier(value) + info.applicationVersion(value) + + XCTAssertEqual("", info.buildTag()) + } + } } From e2abc455ca6535629b0f687a462c76d0db0a5c33 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Thu, 19 May 2022 12:02:16 -0400 Subject: [PATCH 53/90] Remove unsupported alias functionality (#202) --- .circleci/config.yml | 31 ++--- .../Source/Controllers/SdkController.swift | 12 +- ContractTests/Source/Models/client.swift | 1 - ContractTests/Source/Models/command.swift | 8 +- ContractTests/Source/Models/user.swift | 4 +- ContractTests/Source/main.swift | 4 +- ContractTests/testharness-suppressions.txt | 127 +++++++++++++++++- LaunchDarkly/LaunchDarkly/LDClient.swift | 28 ---- .../LaunchDarkly/Models/DiagnosticEvent.swift | 2 - LaunchDarkly/LaunchDarkly/Models/Event.swift | 31 +---- .../LaunchDarkly/Models/LDConfig.swift | 7 - .../ObjectiveC/ObjcLDClient.swift | 17 --- .../LaunchDarklyTests/LDClientSpec.swift | 36 +---- .../Models/DiagnosticEventSpec.swift | 3 +- .../LaunchDarklyTests/Models/EventSpec.swift | 25 ---- .../Models/LDConfigSpec.swift | 6 +- Makefile | 2 +- 17 files changed, 152 insertions(+), 192 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9ab3bb79..064ad4c3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,21 +21,22 @@ jobs: - store_test_results: path: /tmp/contract-test-swiftlint-results.xml - - run: - name: Install required ssl libraries - command: brew install libressl - - run: - name: make test output directory - command: mkdir /tmp/test-results - - run: make build-contract-tests - - run: - command: make start-contract-test-service - background: true - - run: - name: run contract tests - command: TEST_HARNESS_PARAMS="-junit /tmp/test-results/contract-tests-junit.xml" make run-contract-tests - - store_test_results: - path: /tmp/test-results/ + # Disabled temporarily until v2 of the SDK test harness supports client side SDKs + # - run: + # name: Install required ssl libraries + # command: brew install libressl + # - run: + # name: make test output directory + # command: mkdir /tmp/test-results + # - run: make build-contract-tests + # - run: + # command: make start-contract-test-service + # background: true + # - run: + # name: run contract tests + # command: TEST_HARNESS_PARAMS="-junit /tmp/test-results/contract-tests-junit.xml" make run-contract-tests + # - store_test_results: + # path: /tmp/test-results/ build: parameters: diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index 59c1b278..8ae43444 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -2,7 +2,7 @@ import Vapor import LaunchDarkly final class SdkController { - private var clients: [Int : LDClient] = [:] + private var clients: [Int: LDClient] = [:] private var clientCounter = 0 func status(_ req: Request) -> StatusResponse { @@ -55,7 +55,7 @@ final class SdkController { } if let globalPrivate = events.globalPrivateAttributes { - config.privateUserAttributes = globalPrivate.map({ UserAttribute.forName($0) }) + config.privateUserAttributes = globalPrivate.map { UserAttribute.forName($0) } } if let flushIntervalMs = events.flushIntervalMs { @@ -82,10 +82,6 @@ final class SdkController { let clientSide = createInstance.configuration.clientSide - if let autoAliasingOptOut = clientSide.autoAliasingOptOut { - config.autoAliasingOptOut = autoAliasingOptOut - } - if let evaluationReasons = clientSide.evaluationReasons { config.evaluationReasons = evaluationReasons } @@ -97,7 +93,7 @@ final class SdkController { let dispatchSemaphore = DispatchSemaphore(value: 0) let startWaitSeconds = (createInstance.configuration.startWaitTimeMs ?? 5_000) / 1_000 - LDClient.start(config:config, user: clientSide.initialUser, startWaitSeconds: startWaitSeconds) { timedOut in + LDClient.start(config: config, user: clientSide.initialUser, startWaitSeconds: startWaitSeconds) { _ in dispatchSemaphore.signal() } @@ -150,8 +146,6 @@ final class SdkController { semaphore.signal() } semaphore.wait() - case "aliasEvent": - client.alias(context: commandParameters.aliasEvent!.user, previousContext: commandParameters.aliasEvent!.previousUser) case "customEvent": let event = commandParameters.customEvent! client.track(key: event.eventKey, data: event.data, metricValue: event.metricValue) diff --git a/ContractTests/Source/Models/client.swift b/ContractTests/Source/Models/client.swift index f7d42854..88a0a3f4 100644 --- a/ContractTests/Source/Models/client.swift +++ b/ContractTests/Source/Models/client.swift @@ -43,7 +43,6 @@ struct TagParameters: Content { struct ClientSideParameters: Content { var initialUser: LDUser - var autoAliasingOptOut: Bool? var evaluationReasons: Bool? var useReport: Bool? } diff --git a/ContractTests/Source/Models/command.swift b/ContractTests/Source/Models/command.swift index 415b2aaa..1981a2ac 100644 --- a/ContractTests/Source/Models/command.swift +++ b/ContractTests/Source/Models/command.swift @@ -31,7 +31,6 @@ struct CommandParameters: Content { var evaluateAll: EvaluateAllFlagsParameters? var customEvent: CustomEventParameters? var identifyEvent: IdentifyEventParameters? - var aliasEvent: AliasEventParameters? } struct EvaluateFlagParameters: Content { @@ -61,11 +60,6 @@ struct CustomEventParameters: Content { var metricValue: Double? } -struct IdentifyEventParameters: Content, Decodable { +struct IdentifyEventParameters: Content, Decodable { var user: LDUser } - -struct AliasEventParameters: Content { - var user: LDUser - var previousUser: LDUser -} diff --git a/ContractTests/Source/Models/user.swift b/ContractTests/Source/Models/user.swift index 5bdf696c..7cbba775 100644 --- a/ContractTests/Source/Models/user.swift +++ b/ContractTests/Source/Models/user.swift @@ -23,8 +23,8 @@ extension LDUser: Decodable { avatar = try values.decodeIfPresent(String.self, forKey: .avatar) custom = try values.decodeIfPresent([String: LDValue].self, forKey: .custom) ?? [:] isAnonymous = try values.decodeIfPresent(Bool.self, forKey: .isAnonymous) ?? false - let _ = try values.decodeIfPresent([String].self, forKey: .privateAttributes) - privateAttributes = (try values.decodeIfPresent([String].self, forKey: .privateAttributes) ?? []).map({ UserAttribute.forName($0) }) + _ = try values.decodeIfPresent([String].self, forKey: .privateAttributes) + privateAttributes = (try values.decodeIfPresent([String].self, forKey: .privateAttributes) ?? []).map { UserAttribute.forName($0) } secondary = try values.decodeIfPresent(String.self, forKey: .secondary) } } diff --git a/ContractTests/Source/main.swift b/ContractTests/Source/main.swift index aa0396e7..0cf94baa 100644 --- a/ContractTests/Source/main.swift +++ b/ContractTests/Source/main.swift @@ -1,6 +1,7 @@ import Foundation let semaphore = DispatchSemaphore(value: 0) + DispatchQueue.global(qos: .userInitiated).async { do { try app(.detect()).run() @@ -10,6 +11,7 @@ DispatchQueue.global(qos: .userInitiated).async { } let runLoop = RunLoop.current -while (semaphore.wait(timeout: .now()) == .timedOut) { + +while semaphore.wait(timeout: .now()) == .timedOut { runLoop.run(mode: .default, before: .distantFuture) } diff --git a/ContractTests/testharness-suppressions.txt b/ContractTests/testharness-suppressions.txt index 1b99e0d2..cc2f1626 100644 --- a/ContractTests/testharness-suppressions.txt +++ b/ContractTests/testharness-suppressions.txt @@ -7,12 +7,6 @@ evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/ streaming/requests/user properties/GET streaming/requests/user properties/REPORT streaming/updates/flag delete for previously nonexistent flag is applied -polling/requests/method and headers/GET -polling/requests/method and headers/REPORT -polling/requests/URL path is computed correctly/base URI has no trailing slash/GET -polling/requests/URL path is computed correctly/base URI has no trailing slash/REPORT -polling/requests/URL path is computed correctly/base URI has a trailing slash/GET -polling/requests/URL path is computed correctly/base URI has a trailing slash/REPORT polling/requests/query parameters/evaluationReasons set to [none]/GET polling/requests/query parameters/evaluationReasons set to [none]/REPORT polling/requests/query parameters/evaluationReasons set to false/GET @@ -53,3 +47,124 @@ events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-pri events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event +evaluation/parameterized/evaluationReasons=true/evaluation reasons - error MALFORMED_FLAG/flag-key/evaluate flag with detail +events/requests/URL path is computed correctly/base URI has no trailing slash +events/requests/URL path is computed correctly/base URI has a trailing slash +events/requests/new payload ID for each post +events/experimentation/FALLTHROUGH +events/experimentation/RULE_MATCH +events/identify events/basic properties/single kind default +events/identify events/basic properties/single kind non-default +events/identify events/basic properties/multi-kind +events/custom events/data and metricValue parameters/data=null +events/custom events/data and metricValue parameters/data=false +events/custom events/data and metricValue parameters/data=true +events/custom events/data and metricValue parameters/data=0 +events/custom events/data and metricValue parameters/data=1000 +events/custom events/data and metricValue parameters/data=1000.5 +events/custom events/data and metricValue parameters/data="" +events/custom events/data and metricValue parameters/data="abc" +events/custom events/data and metricValue parameters/data=[1,2] +events/custom events/data and metricValue parameters/data={"property":true} +events/custom events/data and metricValue parameters/data=null, omitNullData +events/custom events/data and metricValue parameters/data=null, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=false, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=true, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=0, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=1000, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=1000.5, metricValue=0.000000 +events/custom events/data and metricValue parameters/data="", metricValue=0.000000 +events/custom events/data and metricValue parameters/data="abc", metricValue=0.000000 +events/custom events/data and metricValue parameters/data=[1,2], metricValue=0.000000 +events/custom events/data and metricValue parameters/data={"property":true}, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=null, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=false, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=true, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=0, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=1000, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=1000.5, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data="", metricValue=-1.500000 +events/custom events/data and metricValue parameters/data="abc", metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=[1,2], metricValue=-1.500000 +events/custom events/data and metricValue parameters/data={"property":true}, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=null, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=false, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=true, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=0, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=1000, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=1000.5, metricValue=1.500000 +events/custom events/data and metricValue parameters/data="", metricValue=1.500000 +events/custom events/data and metricValue parameters/data="abc", metricValue=1.500000 +events/custom events/data and metricValue parameters/data=[1,2], metricValue=1.500000 +events/custom events/data and metricValue parameters/data={"property":true}, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=1.500000 +events/custom events/basic properties/single kind default +events/custom events/basic properties/single kind non-default +events/custom events/basic properties/multi-kind +events/context properties/single-kind minimal +events/context properties/multi-kind minimal +events/context properties/single-kind with attributes, nothing private +events/context properties/single-kind, allAttributesPrivate +events/context properties/single-kind, specific private attributes +events/context properties/single-kind, private attribute nested property +events/context properties/custom attribute with value false +events/context properties/custom attribute with value true +events/context properties/custom attribute with value -1000 +events/context properties/custom attribute with value 0 +events/context properties/custom attribute with value 1000 +events/context properties/custom attribute with value -1000.5 +events/context properties/custom attribute with value 1000.5 +events/context properties/custom attribute with value "" +events/context properties/custom attribute with value "abc" +events/context properties/custom attribute with value "has \"escaped\" characters" +events/context properties/custom attribute with value [] +events/context properties/custom attribute with value ["a","b"] +events/context properties/custom attribute with value {} +events/context properties/custom attribute with value {"a":1} +events/event capacity/capacity is enforced +events/event capacity/buffer is reset after flush +events/event capacity/summary event is still included even if buffer was full +events/disabling/identify event +streaming/requests/query parameters/evaluationReasons set to [none]/GET +streaming/requests/query parameters/evaluationReasons set to [none]/REPORT +streaming/requests/query parameters/evaluationReasons set to false/GET +streaming/requests/query parameters/evaluationReasons set to false/REPORT +streaming/requests/query parameters/evaluationReasons set to true/GET +streaming/requests/query parameters/evaluationReasons set to true/REPORT +streaming/requests/context properties/single kind minimal/GET +streaming/requests/context properties/single kind minimal/REPORT +streaming/requests/context properties/single kind with all attributes/GET +streaming/requests/context properties/single kind with all attributes/REPORT +streaming/requests/context properties/multi-kind/GET +streaming/requests/context properties/multi-kind/REPORT +streaming/updates/flag delete for previously nonexistent flag is applied +polling/requests/query parameters/evaluationReasons set to [none]/GET +polling/requests/query parameters/evaluationReasons set to [none]/REPORT +polling/requests/query parameters/evaluationReasons set to false/GET +polling/requests/query parameters/evaluationReasons set to false/REPORT +polling/requests/query parameters/evaluationReasons set to true/GET +polling/requests/query parameters/evaluationReasons set to true/REPORT +polling/requests/context properties/single kind minimal/GET +polling/requests/context properties/single kind minimal/REPORT +polling/requests/context properties/single kind with all attributes/GET +polling/requests/context properties/single kind with all attributes/REPORT +polling/requests/context properties/multi-kind/GET +polling/requests/context properties/multi-kind/REPORT +tags/event posts/{"applicationId":null,"applicationVersion":null} +tags/event posts/{"applicationId":null,"applicationVersion":""} +tags/event posts/{"applicationId":null,"applicationVersion":""} +tags/event posts/{"applicationId":null,"applicationVersion":""} +tags/event posts/{"applicationId":"","applicationVersion":null} +tags/event posts/{"applicationId":"","applicationVersion":""} +tags/event posts/{"applicationId":"","applicationVersion":""} +tags/event posts/{"applicationId":"","applicationVersion":""} +tags/event posts/{"applicationId":"","applicationVersion":null} +tags/event posts/{"applicationId":"","applicationVersion":""} +tags/event posts/{"applicationId":"","applicationVersion":""} +tags/event posts/{"applicationId":"","applicationVersion":""} +tags/event posts/{"applicationId":"","applicationVersion":null} +tags/event posts/{"applicationId":"","applicationVersion":""} +tags/event posts/{"applicationId":"","applicationVersion":""} +tags/event posts/{"applicationId":"","applicationVersion":""} diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index d7b5fb16..2c010201 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -288,7 +288,6 @@ public class LDClient { func internalIdentify(newUser: LDUser, completion: (() -> Void)? = nil) { internalIdentifyQueue.sync { - let previousUser = self.user self.user = newUser Log.debug(self.typeName(and: #function) + "new user set with key: " + self.user.key ) let wasOnline = self.isOnline @@ -309,10 +308,6 @@ public class LDClient { } self.internalSetOnline(wasOnline, completion: completion) - - if !config.autoAliasingOptOut && previousUser.isAnonymous && !newUser.isAnonymous { - self.alias(context: newUser, previousContext: previousUser) - } } } @@ -541,29 +536,6 @@ public class LDClient { eventReporter.record(event) } - /** - Tells the SDK to generate an alias event. - - Associates two users for analytics purposes. - - This can be helpful in the situation where a person is represented by multiple - LaunchDarkly users. This may happen, for example, when a person initially logs into - an application-- the person might be represented by an anonymous user prior to logging - in and a different user after logging in, as denoted by a different user key. - - - parameter context: the user that will be aliased to - - parameter previousContext: the user that will be bound to the new context - */ - public func alias(context new: LDUser, previousContext old: LDUser) { - guard hasStarted - else { - Log.debug(typeName(and: #function) + "aborted. LDClient not started") - return - } - - self.eventReporter.record(AliasEvent(key: new.key, previousKey: old.key, contextKind: new.contextKind, previousContextKind: old.contextKind)) - } - /** Tells the SDK to immediately send any currently queued events to LaunchDarkly. diff --git a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift index 3df691e7..88d670ba 100644 --- a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift +++ b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift @@ -91,7 +91,6 @@ struct DiagnosticSdk: Encodable { } struct DiagnosticConfig: Codable { - let autoAliasingOptOut: Bool let customBaseURI: Bool let customEventsURI: Bool let customStreamURI: Bool @@ -112,7 +111,6 @@ struct DiagnosticConfig: Codable { let customHeaders: Bool init(config: LDConfig) { - autoAliasingOptOut = config.autoAliasingOptOut customBaseURI = config.baseUrl != LDConfig.Defaults.baseUrl customEventsURI = config.eventsUrl != LDConfig.Defaults.eventsUrl customStreamURI = config.streamUrl != LDConfig.Defaults.streamUrl diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index a0a76f87..9b82eea3 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -11,10 +11,10 @@ class Event: Encodable { } enum Kind: String { - case feature, debug, identify, custom, summary, alias + case feature, debug, identify, custom, summary static var allKinds: [Kind] { - [feature, debug, identify, custom, summary, alias] + [feature, debug, identify, custom, summary] } } @@ -32,7 +32,6 @@ class Event: Encodable { 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) @@ -41,32 +40,6 @@ class Event: Encodable { } } -class AliasEvent: Event, SubEvent { - let key: String - let previousKey: String - let contextKind: String - let previousContextKind: String - let creationDate: Date - - 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) - } - - 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) - } -} - class CustomEvent: Event, SubEvent { let key: String let user: LDUser diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 163f1b8d..33fdeaa3 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -175,9 +175,6 @@ public struct LDConfig { /// a closure to allow dynamic changes of headers on connect & reconnect static let headerDelegate: RequestHeaderTransform? = nil - - /// should anonymous users automatically be aliased when identifying - static let autoAliasingOptOut: Bool = false } /// Constants relevant to setting up an `LDConfig` @@ -347,9 +344,6 @@ public struct LDConfig { let environmentReporter: EnvironmentReporting - /// should anonymous users automatically be aliased when identifying - public var autoAliasingOptOut: Bool = Defaults.autoAliasingOptOut - /// A Dictionary of identifying names to unique mobile keys for all environments private var mobileKeys: [String: String] { var internalMobileKeys = getSecondaryMobileKeys() @@ -452,7 +446,6 @@ extension LDConfig: Equatable { && lhs.wrapperName == rhs.wrapperName && lhs.wrapperVersion == rhs.wrapperVersion && lhs.additionalHeaders == rhs.additionalHeaders - && lhs.autoAliasingOptOut == rhs.autoAliasingOptOut } } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index b9dfcfc9..76ab999b 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -539,23 +539,6 @@ public final class ObjcLDClient: NSObject { ldClient.flush() } - /** - Tells the SDK to generate an alias event. - - Associates two users for analytics purposes. - - This can be helpful in the situation where a person is represented by multiple - LaunchDarkly users. This may happen, for example, when a person initially logs into - an application-- the person might be represented by an anonymous user prior to logging - in and a different user after logging in, as denoted by a different user key. - - - parameter context: the user that will be aliased to - - parameter previousContext: the user that will be bound to the new context - */ - @objc public func alias(context: ObjcLDUser, previousContext: ObjcLDUser) { - ldClient.alias(context: context.user, previousContext: previousContext.user) - } - /** Starts the LDClient using the passed in `config` & `user`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. Starting the LDClient means setting the `config` & `user`, setting the client online if `config.startOnline` is true (the default setting), and starting event recording. The client app must start the LDClient before it will report feature flag values. If a client does not call `start`, no methods will work. diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index a7678449..b4bd49ea 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -73,8 +73,7 @@ final class LDClientSpec: QuickSpec { startOnline: Bool = false, streamingMode: LDStreamingMode = .streaming, enableBackgroundUpdates: Bool = true, - operatingSystem: OperatingSystem? = nil, - autoAliasingOptOut: Bool = true) { + operatingSystem: OperatingSystem? = nil) { if let operatingSystem = operatingSystem { serviceFactoryMock.makeEnvironmentReporterReturnValue.operatingSystem = operatingSystem @@ -95,7 +94,6 @@ final class LDClientSpec: QuickSpec { config.streamingMode = streamingMode config.enableBackgroundUpdates = enableBackgroundUpdates config.eventFlushInterval = 300.0 // 5 min...don't want this to trigger - config.autoAliasingOptOut = autoAliasingOptOut user = LDUser.stub() } @@ -155,41 +153,9 @@ final class LDClientSpec: QuickSpec { allFlagsSpec() connectionInformationSpec() variationDetailSpec() - aliasingSpec() isInitializedSpec() } - private func aliasingSpec() { - let anonUser = LDUser(key: "unknown", isAnonymous: true) - let knownUser = LDUser(key: "known", isAnonymous: false) - describe("aliasing") { - 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 - } - } - } - private func startSpec() { describe("start") { startSpec(withTimeout: false) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index 005e882a..6c9c947c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -270,8 +270,7 @@ final class DiagnosticEventSpec: QuickSpec { } it("encodes correct values to keys") { encodesToObject(diagnosticConfig) { decoded in - expect(decoded.count) == 19 - expect(decoded["autoAliasingOptOut"]) == .bool(diagnosticConfig.autoAliasingOptOut) + expect(decoded.count) == 18 expect(decoded["customBaseURI"]) == .bool(diagnosticConfig.customBaseURI) expect(decoded["customEventsURI"]) == .bool(diagnosticConfig.customEventsURI) expect(decoded["customStreamURI"]) == .bool(diagnosticConfig.customStreamURI) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 4bf2efd9..380c2e6a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -4,17 +4,6 @@ import XCTest @testable import LaunchDarkly 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) - } - func testFeatureEventInit() { let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) let user = LDUser.stub() @@ -76,19 +65,6 @@ final class EventSpec: XCTestCase { XCTAssertEqual(event.flagRequestTracker.flagCounters, flagRequestTracker.flagCounters) } - 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"], .number(Double(event.creationDate.millisSince1970))) - } - } - func testCustomEventEncodingDataAndMetric() { let user = LDUser.stub() let event = CustomEvent(key: "event-key", user: user, data: ["abc", 12], metricValue: 0.5) @@ -292,7 +268,6 @@ extension Event { 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/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index 698ea35b..9a9d8155 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -31,7 +31,6 @@ final class LDConfigSpec: XCTestCase { fileprivate static let wrapperName = "ReactNative" fileprivate static let wrapperVersion = "0.1.0" fileprivate static let additionalHeaders = ["Proxy-Authorization": "creds"] - fileprivate static let autoAliasingOptOut = true } let testFields: [(String, Any, (inout LDConfig, Any?) -> Void)] = @@ -58,8 +57,7 @@ final class LDConfigSpec: XCTestCase { ("diagnostic recording interval", Constants.diagnosticRecordingInterval, { c, v in c.diagnosticRecordingInterval = v as! TimeInterval }), ("wrapper name", Constants.wrapperName, { c, v in c.wrapperName = v as! String? }), ("wrapper version", Constants.wrapperVersion, { c, v in c.wrapperVersion = v as! String? }), - ("additional headers", Constants.additionalHeaders, { c, v in c.additionalHeaders = v as! [String: String]}), - ("auto aliasing opt out", Constants.autoAliasingOptOut, { c, v in c.autoAliasingOptOut = v as! Bool })] + ("additional headers", Constants.additionalHeaders, { c, v in c.additionalHeaders = v as! [String: String]})] func testInitDefault() { let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey) @@ -87,7 +85,6 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.wrapperName, LDConfig.Defaults.wrapperName) XCTAssertEqual(config.wrapperVersion, LDConfig.Defaults.wrapperVersion) XCTAssertEqual(config.additionalHeaders, LDConfig.Defaults.additionalHeaders) - XCTAssertEqual(config.autoAliasingOptOut, LDConfig.Defaults.autoAliasingOptOut) } func testInitUpdate() { @@ -122,7 +119,6 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.wrapperName, Constants.wrapperName, "\(os)") XCTAssertEqual(config.wrapperVersion, Constants.wrapperVersion, "\(os)") XCTAssertEqual(config.additionalHeaders, Constants.additionalHeaders, "\(os)") - XCTAssertEqual(config.autoAliasingOptOut, Constants.autoAliasingOptOut, "\(os)") } } diff --git a/Makefile b/Makefile index 85dd13df..e8523241 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ start-contract-test-service-bg: run-contract-tests: @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/master/downloader/run.sh \ - | VERSION=v1 PARAMS="-url http://localhost:8080 -debug -stop-service-at-end -skip-from ./ContractTests/testharness-suppressions.txt $(TEST_HARNESS_PARAMS)" sh + | VERSION=v2 PARAMS="-url http://localhost:8080 -debug -stop-service-at-end -skip-from ./ContractTests/testharness-suppressions.txt $(TEST_HARNESS_PARAMS)" sh contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests From a3a0e8ae84c5d693283b63ccb045933e93e002d5 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 25 May 2022 11:46:38 -0400 Subject: [PATCH 54/90] Add initial structure for context and reference (#203) --- ContractTests/Package.swift | 2 +- ContractTests/Source/app.swift | 2 +- ContractTests/Source/boot.swift | 2 +- ContractTests/Source/configure.swift | 2 +- ContractTests/Source/routes.swift | 2 +- LaunchDarkly.xcodeproj/project.pbxproj | 58 ++++ .../LaunchDarkly/Models/Context/Kind.swift | 79 +++++ .../Models/Context/LDContext.swift | 290 ++++++++++++++++++ .../Models/Context/Reference.swift | 147 +++++++++ .../LaunchDarkly/Models/LDConfig.swift | 2 +- LaunchDarkly/LaunchDarkly/Util.swift | 13 + .../Models/Context/KindSpec.swift | 46 +++ .../Models/Context/LDContextSpec.swift | 243 +++++++++++++++ .../Models/Context/ReferenceSpec.swift | 84 +++++ 14 files changed, 966 insertions(+), 6 deletions(-) create mode 100644 LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift create mode 100644 LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift create mode 100644 LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift diff --git a/ContractTests/Package.swift b/ContractTests/Package.swift index 2bf68a23..25418fae 100644 --- a/ContractTests/Package.swift +++ b/ContractTests/Package.swift @@ -26,6 +26,6 @@ let package = Package( .product(name: "LaunchDarkly", package: "LaunchDarkly"), .product(name: "Vapor", package: "vapor") ], - path: "Source"), + path: "Source") ], swiftLanguageVersions: [.v5]) diff --git a/ContractTests/Source/app.swift b/ContractTests/Source/app.swift index 3d24e8a3..0a0094e7 100644 --- a/ContractTests/Source/app.swift +++ b/ContractTests/Source/app.swift @@ -2,7 +2,7 @@ import Vapor extension String: Error {} -public func app(_ env: Environment) throws -> Application { +func app(_ env: Environment) throws -> Application { var config = Config.default() var env = env var services = Services.default() diff --git a/ContractTests/Source/boot.swift b/ContractTests/Source/boot.swift index 9313115b..603be99f 100644 --- a/ContractTests/Source/boot.swift +++ b/ContractTests/Source/boot.swift @@ -1,6 +1,6 @@ import Vapor /// Called after your application has initialized. -public func boot(_ app: Application) throws { +func boot(_ app: Application) throws { // Your code here } diff --git a/ContractTests/Source/configure.swift b/ContractTests/Source/configure.swift index 3c3aca84..8b1a9ee0 100644 --- a/ContractTests/Source/configure.swift +++ b/ContractTests/Source/configure.swift @@ -1,7 +1,7 @@ import Vapor /// Called before your application initializes. -public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { +func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { // Register routes to the router let router = EngineRouter.default() try routes(router) diff --git a/ContractTests/Source/routes.swift b/ContractTests/Source/routes.swift index 33a58076..f2c84590 100644 --- a/ContractTests/Source/routes.swift +++ b/ContractTests/Source/routes.swift @@ -1,6 +1,6 @@ import Vapor -public func routes(_ router: Router) throws { +func routes(_ router: Router) throws { let sdkController = SdkController() router.get("/", use: sdkController.status) router.post("/", use: sdkController.createClient) diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 23e2408d..ef6db553 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -197,6 +197,21 @@ 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 */; }; + A31088172837DC0400184942 /* Reference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088142837DC0400184942 /* Reference.swift */; }; + A31088182837DC0400184942 /* Reference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088142837DC0400184942 /* Reference.swift */; }; + A31088192837DC0400184942 /* Reference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088142837DC0400184942 /* Reference.swift */; }; + A310881A2837DC0400184942 /* Reference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088142837DC0400184942 /* Reference.swift */; }; + A310881B2837DC0400184942 /* Kind.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088152837DC0400184942 /* Kind.swift */; }; + A310881C2837DC0400184942 /* Kind.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088152837DC0400184942 /* Kind.swift */; }; + A310881D2837DC0400184942 /* Kind.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088152837DC0400184942 /* Kind.swift */; }; + A310881E2837DC0400184942 /* Kind.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088152837DC0400184942 /* Kind.swift */; }; + A310881F2837DC0400184942 /* LDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088162837DC0400184942 /* LDContext.swift */; }; + A31088202837DC0400184942 /* LDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088162837DC0400184942 /* LDContext.swift */; }; + A31088212837DC0400184942 /* LDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088162837DC0400184942 /* LDContext.swift */; }; + A31088222837DC0400184942 /* LDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088162837DC0400184942 /* LDContext.swift */; }; + A31088272837DCA900184942 /* LDContextSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088242837DCA900184942 /* LDContextSpec.swift */; }; + A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088252837DCA900184942 /* ReferenceSpec.swift */; }; + A31088292837DCA900184942 /* KindSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088262837DCA900184942 /* KindSpec.swift */; }; B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; }; B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.swift */; }; B467791324D8AEEC00897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791224D8AEEC00897F00 /* LDSwiftEventSourceStatic */; }; @@ -393,6 +408,12 @@ 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 = ""; }; + A31088142837DC0400184942 /* Reference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reference.swift; sourceTree = ""; }; + A31088152837DC0400184942 /* Kind.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Kind.swift; sourceTree = ""; }; + A31088162837DC0400184942 /* LDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDContext.swift; sourceTree = ""; }; + A31088242837DCA900184942 /* LDContextSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDContextSpec.swift; sourceTree = ""; }; + A31088252837DCA900184942 /* ReferenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceSpec.swift; sourceTree = ""; }; + A31088262837DCA900184942 /* KindSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KindSpec.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 = ""; }; B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = ""; }; @@ -596,6 +617,7 @@ 8354EFE61F263E4200C05156 /* Models */ = { isa = PBXGroup; children = ( + A31088132837DC0400184942 /* Context */, C408884823033B7500420721 /* ConnectionInformation.swift */, B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */, 8354EFDE1F26380700C05156 /* Event.swift */, @@ -722,6 +744,7 @@ 83EF67911F9945CE00403126 /* Models */ = { isa = PBXGroup; children = ( + A31088232837DCA900184942 /* Context */, 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */, 83EBCBA620D9A23E003A7142 /* User */, 83EBCBA720D9A251003A7142 /* FeatureFlag */, @@ -751,6 +774,26 @@ path = ServiceObjects; sourceTree = ""; }; + A31088132837DC0400184942 /* Context */ = { + isa = PBXGroup; + children = ( + A31088142837DC0400184942 /* Reference.swift */, + A31088152837DC0400184942 /* Kind.swift */, + A31088162837DC0400184942 /* LDContext.swift */, + ); + path = Context; + sourceTree = ""; + }; + A31088232837DCA900184942 /* Context */ = { + isa = PBXGroup; + children = ( + A31088242837DCA900184942 /* LDContextSpec.swift */, + A31088252837DCA900184942 /* ReferenceSpec.swift */, + A31088262837DCA900184942 /* KindSpec.swift */, + ); + path = Context; + sourceTree = ""; + }; B467790E24D8AECA00897F00 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1106,10 +1149,13 @@ 8311885F2113AE2D00D77CB5 /* HTTPURLRequest.swift in Sources */, B4C9D4362489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 831188452113ADC500D77CB5 /* LDClient.swift in Sources */, + A310881E2837DC0400184942 /* Kind.swift in Sources */, + A310881A2837DC0400184942 /* Reference.swift in Sources */, 831188522113ADF700D77CB5 /* KeyedValueCache.swift in Sources */, 831188582113AE0F00D77CB5 /* EventReporter.swift in Sources */, 8311885D2113AE2500D77CB5 /* DarklyService.swift in Sources */, 831188692113AE5900D77CB5 /* ObjcLDConfig.swift in Sources */, + A31088222837DC0400184942 /* LDContext.swift in Sources */, 8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */, C43C37E8238DF22D003C1624 /* LDEvaluationDetail.swift in Sources */, 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */, @@ -1155,6 +1201,7 @@ B468E71224B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, 831EF34320655E730001C643 /* LDCommon.swift in Sources */, 831EF34420655E730001C643 /* LDConfig.swift in Sources */, + A31088212837DC0400184942 /* LDContext.swift in Sources */, 831EF34520655E730001C643 /* LDClient.swift in Sources */, 831EF34620655E730001C643 /* LDUser.swift in Sources */, 830DB3B02239B54900D65D25 /* URLResponse.swift in Sources */, @@ -1163,10 +1210,12 @@ C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */, 8354AC722243166900CDE602 /* FeatureFlagCache.swift in Sources */, + A310881D2837DC0400184942 /* Kind.swift in Sources */, C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */, 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */, 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */, 831EF34D20655E730001C643 /* FlagsUnchangedObserver.swift in Sources */, + A31088192837DC0400184942 /* Reference.swift in Sources */, 831EF34E20655E730001C643 /* Event.swift in Sources */, C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831EF35020655E730001C643 /* ClientServiceFactory.swift in Sources */, @@ -1217,10 +1266,13 @@ B4C9D4332489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 8354EFE51F263DAC00C05156 /* FeatureFlag.swift in Sources */, 8372668C20D4439600BD1088 /* DateFormatter.swift in Sources */, + A310881B2837DC0400184942 /* Kind.swift in Sources */, + A31088172837DC0400184942 /* Reference.swift in Sources */, 83A2D6241F51CD7A00EA3BD4 /* LDUser.swift in Sources */, 8354EFE21F26380700C05156 /* Event.swift in Sources */, C408884923033B7500420721 /* ConnectionInformation.swift in Sources */, 831D8B721F71D3E700ED65E8 /* DarklyService.swift in Sources */, + A310881F2837DC0400184942 /* LDContext.swift in Sources */, 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */, 8358F25E1F474E5900ECE1AF /* LDChangedFlag.swift in Sources */, 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */, @@ -1263,6 +1315,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A31088272837DCA900184942 /* LDContextSpec.swift in Sources */, 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */, 8392FFA32033565700320914 /* HTTPURLResponse.swift in Sources */, 83411A5F1FABDA8700E5CF39 /* mocks.generated.swift in Sources */, @@ -1278,6 +1331,7 @@ 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */, 83E2E2061F9E7AC7007514E9 /* LDUserSpec.swift in Sources */, 83A0E6B1203B557F00224298 /* FeatureFlagSpec.swift in Sources */, + A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */, 83EBCBB720DABE93003A7142 /* FlagRequestTrackerSpec.swift in Sources */, B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */, B46F344125E6DB7D0078D45F /* DiagnosticReporterSpec.swift in Sources */, @@ -1299,6 +1353,7 @@ 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */, 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */, 838F967A1FBA551A009CFC45 /* ClientServiceMockFactory.swift in Sources */, + A31088292837DCA900184942 /* KindSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1315,10 +1370,13 @@ 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */, 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */, + A310881C2837DC0400184942 /* Kind.swift in Sources */, + A31088182837DC0400184942 /* Reference.swift in Sources */, 83D9EC7E2062DEAB004D7FA6 /* FlagChangeObserver.swift in Sources */, 83D9EC7F2062DEAB004D7FA6 /* FlagsUnchangedObserver.swift in Sources */, 83D9EC802062DEAB004D7FA6 /* Event.swift in Sources */, 83D9EC822062DEAB004D7FA6 /* ClientServiceFactory.swift in Sources */, + A31088202837DC0400184942 /* LDContext.swift in Sources */, 83D9EC832062DEAB004D7FA6 /* KeyedValueCache.swift in Sources */, 831AAE2D20A9E4F600B46DBA /* Throttler.swift in Sources */, C43C37E6238DF22B003C1624 /* LDEvaluationDetail.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift new file mode 100644 index 00000000..edb76b9b --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift @@ -0,0 +1,79 @@ +import Foundation + +public enum Kind: Codable, Equatable, Hashable { + case user + case multi + case custom(String) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + switch try container.decode(String.self) { + case "user": + self = .user + case "multi": + self = .multi + case let custom: + self = .custom(custom) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.description) + } + + public func isMulti() -> Bool { + self == .multi || self == .custom("multi") + } + + public func isUser() -> Bool { + self == .user || self == .custom("user") || self == .custom("") + } + + private static func isValid(_ description: String) -> Bool { + description.onlyContainsCharset(Util.validKindCharacterSet) + } +} + +extension Kind: Comparable { + public static func < (lhs: Kind, rhs: Kind) -> Bool { + lhs.description < rhs.description + } + + public static func == (lhs: Kind, rhs: Kind) -> Bool { + lhs.description == rhs.description + } +} + +extension Kind: LosslessStringConvertible { + public init?(_ description: String) { + switch description { + case "kind": + return nil + case "multi": + self = .multi + case "", "user": + self = .user + default: + if !Kind.isValid(description) { + return nil + } + + self = .custom(description) + } + } +} + +extension Kind: CustomStringConvertible { + public var description: String { + switch self { + case .user: + return "user" + case .multi: + return "multi" + case let .custom(val): + return val + } + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift new file mode 100644 index 00000000..eadf4b83 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -0,0 +1,290 @@ +import Foundation + +public enum ContextBuilderError: Error { + case invalidKind + case requiresMultiBuilder + case emptyKey + case emptyMultiKind + case nestedMultiKind + case duplicateKinds +} + +/// TKTK +public struct LDContext { + internal var kind: Kind = .user + fileprivate var contexts: [LDContext] = [] + + // Meta attributes + fileprivate var name: String? + fileprivate var transient: Bool = false + fileprivate var secondary: String? + internal var privateAttributes: [Reference] = [] + + fileprivate var key: String? + fileprivate var canonicalizedKey: String + internal var attributes: [String: LDValue] = [:] + + fileprivate init(canonicalizedKey: String) { + self.canonicalizedKey = canonicalizedKey + } + + /// TKTK + public func fullyQualifiedKey() -> String { + return canonicalizedKey + } + + /// TKTK + public func isMulti() -> Bool { + return self.kind.isMulti() + } + + /// TKTK + public func getValue(_ reference: Reference) -> LDValue? { + if !reference.isValid() { + return nil + } + + guard let (component, _) = reference.component(0) else { + return nil + } + + if isMulti() { + if reference.depth() == 1 && component == "kind" { + return .string(String(kind)) + } + + Log.debug(typeName(and: #function) + ": Cannot get non-kind attribute from multi-kind context") + return nil + } + + guard var attribute: LDValue = self.getTopLevelAddressableAttributeSingleKind(component) else { + return nil + } + + for depth in 1.. LDValue? { + switch name { + case "kind": + return .string(String(self.kind)) + case "key": + return self.key.map { .string($0) } + case "name": + return self.name.map { .string($0) } + case "transient": + return .bool(self.transient) + default: + return self.attributes[name] + } + } +} + +extension LDContext: TypeIdentifying {} + +/// TKTK +public struct LDContextBuilder { + private var kind: String = Kind.user.description + + // Meta attributes + private var name: String? + private var transient: Bool = false + private var secondary: String? + private var privateAttributes: [Reference] = [] + + private var key: String? + private var attributes: [String: LDValue] = [:] + + /// TKTK + public init(key: String) { + self.key = key + } + + /// TKTK + public mutating func kind(_ kind: String) { + self.kind = kind + } + + /// TKTK + public mutating func key(_ key: String) { + self.key = key + } + + /// TKTK + public mutating func name(_ name: String) { + self.name = name + } + + /// TKTK + @discardableResult + public mutating func trySetValue(_ name: String, _ value: LDValue) -> Bool { + switch (name, value) { + case ("", _): + Log.debug(typeName(and: #function) + ": Provided attribute is empty. Ignoring.") + return false + case ("kind", .string(kind)): + self.kind(kind) + case ("kind", _): + return false + case ("key", .string(let val)): + self.key(val) + case ("key", _): + return false + case ("name", .string(let val)): + self.name(val) + case ("name", _): + return false + case ("transient", .bool(let val)): + self.transient(val) + case ("transient", _): + return false + case ("secondary", .string(let val)): + self.secondary(val) + case ("secondary", _): + return false + case ("privateAttributeNames", _): + Log.debug(typeName(and: #function) + ": The privateAttributeNames property has been replaced with privateAttributes. Refusing to set a property named privateAttributeNames.") + return false + case ("anonymous", _): + Log.debug(typeName(and: #function) + ": The anonymous property has been replaced with transient. Refusing to set a property named anonymous.") + return false + case (_, .null): + self.attributes.removeValue(forKey: name) + return false + case (_, _): + self.attributes.updateValue(value, forKey: name) + return false + } + + return true + } + + mutating func secondary(_ secondary: String) { + self.secondary = secondary + } + + mutating func transient(_ transient: Bool) { + self.transient = transient + } + + mutating func addPrivateAttribute(_ reference: Reference) { + self.privateAttributes.append(reference) + } + + mutating func removePrivateAttribute(_ reference: Reference) { + self.privateAttributes.removeAll { $0 == reference } + } + + /// TKTK + public func build() -> Result { + guard let kind = Kind(self.kind) else { + return Result.failure(.invalidKind) + } + + if kind.isMulti() { + return Result.failure(.requiresMultiBuilder) + } + + // TODO(mmk) If we are converting legacy users to newer user contexts, + // then the key is allowed to be empty. Otherwise, it cannot be. So we + // need to hook up that condition still. + if self.key?.isEmpty ?? true { + return Result.failure(.emptyKey) + } + + var context = LDContext(canonicalizedKey: canonicalizeKeyForKind(kind: kind, key: self.key!, omitUserKind: true)) + context.kind = kind + context.contexts = [] + context.name = self.name + context.transient = self.transient + context.privateAttributes = self.privateAttributes + context.key = self.key + context.attributes = self.attributes + + return Result.success(context) + } +} + +extension LDContextBuilder: TypeIdentifying { } + +/// TKTK +public struct LDMultiContextBuilder { + private var contexts: [LDContext] = [] + + /// TKTK + public mutating func addContext(_ context: LDContext) { + contexts.append(context) + } + + /// TKTK + public func build() -> Result { + if contexts.isEmpty { + return Result.failure(.emptyMultiKind) + } + + if contexts.contains(where: { $0.isMulti() }) { + return Result.failure(.nestedMultiKind) + } + + if contexts.count == 1 { + return Result.success(contexts[0]) + } + + let uniqueKinds = Set(contexts.map { context in context.kind }) + if uniqueKinds.count != contexts.count { + return Result.failure(.duplicateKinds) + } + + let sortedContexts = contexts.sorted { $0.kind < $1.kind } + let canonicalizedKey = sortedContexts.map { context in + return canonicalizeKeyForKind(kind: context.kind, key: context.key ?? "", omitUserKind: false) + }.joined(separator: ":") + + var context = LDContext(canonicalizedKey: canonicalizedKey) + context.kind = .multi + context.contexts = sortedContexts + + return Result.success(context) + } +} + +func canonicalizeKeyForKind(kind: Kind, key: String, omitUserKind: Bool) -> String { + if omitUserKind && kind.isUser() { + return key + } + + let encoding = key.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" + + return "\(kind):\(encoding)" +} diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift new file mode 100644 index 00000000..c83df0ac --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift @@ -0,0 +1,147 @@ +import Foundation + +enum ReferenceError: Codable, Equatable, Error { + case empty + case doubleSlash + case invalidEscapeSequence + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + switch try container.decode(String.self) { + case "empy": + self = .empty + case "doubleSlash": + self = .doubleSlash + default: + self = .invalidEscapeSequence + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.description) + } +} + +extension ReferenceError: CustomStringConvertible { + var description: String { + switch self { + case .empty: return "empty" + case .doubleSlash: return "doubleSlash" + case .invalidEscapeSequence: return "invalidEscapeSequence" + } + } +} + +public struct Reference: Codable, Equatable { + private var error: ReferenceError? + private var rawPath: String + private var components: [Component] = [] + + static func unescapePath(_ part: String) -> Result { + if !part.contains("~") { + return Result.success(part) + } + + var output = "" + var index = part.startIndex + + while index < part.endIndex { + if part[index] != "~" { + output.append(part[index]) + index = part.index(after: index) + continue + } + + index = part.index(after: index) + if index == part.endIndex { + return Result.failure(.invalidEscapeSequence) + } + + switch part[index] { + case "0": + output.append("~") + case "1": + output.append("/") + default: + return Result.failure(.invalidEscapeSequence) + } + + index = part.index(after: index) + } + + return Result.success(output) + } + + init(_ value: String) { + rawPath = value + + if value.isEmpty || value == "/" { + error = .empty + return + } + + if value.prefix(1) != "/" { + components = [Component(name: value, value: nil)] + return + } + + var referenceComponents: [Component] = [] + let parts = value.components(separatedBy: "/") + for (index, part) in parts.enumerated() { + if index == 0 { + // We can ignore the first match since we know we had a leading slash. + continue + } + + // We must have had a double slash + if part.isEmpty { + error = .doubleSlash + return + } + + let result = Reference.unescapePath(part) + switch result { + case .success(let unescapedPath): + referenceComponents.append(Component(name: unescapedPath, value: Int(part))) + case .failure(let err): + error = err + return + } + } + + components = referenceComponents + } + + public func isValid() -> Bool { + return error == nil + } + + internal func getError() -> ReferenceError? { + return error + } + + public func depth() -> Int { + return components.count + } + + public func component(_ index: Int) -> (String, Int?)? { + if index >= self.depth() { + return nil + } + + let component = self.components[index] + return (component.name, component.value) + } +} + +private struct Component: Codable, Equatable { + fileprivate let name: String + fileprivate let value: Int? + + init(name: String, value: Int?) { + self.name = name + self.value = value + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 33fdeaa3..0a3e1094 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -237,7 +237,7 @@ 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 /// The configuration for application metadata. - public var applicationInfo: ApplicationInfo? = nil + public var applicationInfo: ApplicationInfo? /** Controls the method the SDK uses to keep feature flags updated. (Default: `.streaming`) diff --git a/LaunchDarkly/LaunchDarkly/Util.swift b/LaunchDarkly/LaunchDarkly/Util.swift index 7ecf2a2b..fd7eea02 100644 --- a/LaunchDarkly/LaunchDarkly/Util.swift +++ b/LaunchDarkly/LaunchDarkly/Util.swift @@ -2,6 +2,9 @@ import CommonCrypto import Foundation class Util { + internal static let validKindCharacterSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") + internal static let validTagCharacterSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") + class func sha256base64(_ str: String) -> String { let data = Data(str.utf8) var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) @@ -11,3 +14,13 @@ class Util { return Data(digest).base64EncodedString() } } + +extension String { + func onlyContainsCharset(_ set: CharacterSet) -> Bool { + if description.rangeOfCharacter(from: set.inverted) != nil { + return false + } + + return true + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift new file mode 100644 index 00000000..ca6e3f71 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift @@ -0,0 +1,46 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class KindSpec: XCTestCase { + func testKindCorrectlyIdentifiesAsMulti() { + let options: [(Kind, Bool)] = [ + (.user, false), + (.multi, true), + (.custom("multi"), true), + (.custom("org"), false) + ] + + for (kind, isMulti) in options { + XCTAssertEqual(kind.isMulti(), isMulti) + } + } + + func testKindCorrectlyIdentifiesAsUser() { + let options: [(Kind, Bool)] = [ + (.user, true), + (.multi, false), + (.custom(""), true), + (.custom("user"), true), + (.custom("org"), false) + ] + + for (kind, isUser) in options { + XCTAssertEqual(kind.isUser(), isUser) + } + } + + func testKindBuildsFromStringCorrectly() { + XCTAssertNil(Kind("kind")) + XCTAssertNil(Kind("no spaces allowed")) + XCTAssertNil(Kind("#invalidcharactersarefun")) + + XCTAssertEqual(Kind(""), .user) + XCTAssertEqual(Kind("user"), .user) + XCTAssertEqual(Kind("User"), .custom("User")) + + XCTAssertEqual(Kind("multi"), .multi) + XCTAssertEqual(Kind("org"), .custom("org")) + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift new file mode 100644 index 00000000..ed3bb97a --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift @@ -0,0 +1,243 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class LDContextSpec: XCTestCase { + // TOOD(mmk) Make sure we cannot make a context with a kind of kind + + func testBuildCanCreateSimpleContext() throws { + var builder = LDContextBuilder(key: "context-key") + builder.name("Name") + builder.secondary("Secondary") + + let context = try builder.build().get() + XCTAssertFalse(context.isMulti()) + } + + func testBuilderCanHandleMissingKind() throws { + var builder = LDContextBuilder(key: "key") + + var context = try builder.build().get() + XCTAssertTrue(context.kind.isUser()) + + builder.kind("") + context = try builder.build().get() + XCTAssertTrue(context.kind.isUser()) + } + + func testSingleContextHasCorrectCanonicalKey() throws { + let tests: [(String, String, String)] = [ + ("key", "user", "key"), + ("key", "org", "org:key"), + ("hi:there", "user", "hi:there"), + ("hi:there", "org", "org:hi%3Athere") + ] + + for (key, kind, expectedKey) in tests { + var builder = LDContextBuilder(key: key) + builder.kind(kind) + + let context = try builder.build().get() + XCTAssertEqual(context.fullyQualifiedKey(), expectedKey) + } + } + + func testMultiContextHasCorrectCanonicalKey() throws { + let tests: [([(String, String)], String)] = [ + ([("key", "user")], "key"), + ([("userKey", "user"), ("orgKey", "org")], "org:orgKey:user:userKey"), + ([("some user", "user"), ("org:key", "org")], "org:org%3Akey:user:some%20user") + ] + + for (contextOptions, qualifiedKey) in tests { + var multibuilder = LDMultiContextBuilder() + + for (key, kind) in contextOptions { + var builder = LDContextBuilder(key: key) + builder.kind(kind) + + switch builder.build() { + case .success(let context): + multibuilder.addContext(context) + case .failure(let error): + XCTFail(error.localizedDescription) + } + } + + let context = try multibuilder.build().get() + XCTAssertEqual(context.fullyQualifiedKey(), qualifiedKey) + } + } + + func testMultikindBuilderRequiresContext() throws { + let multiBuilder = LDMultiContextBuilder() + switch multiBuilder.build() { + case .success(_): + XCTFail("Multibuilder should have failed to build.") + case .failure(let error): + XCTAssertEqual(error, .emptyMultiKind) + } + } + + func testMultikindCannotContainAnotherMultiKind() throws { + var multiBuilder = LDMultiContextBuilder() + + var builder = LDContextBuilder(key: "key") + multiBuilder.addContext(try builder.build().get()) + + builder.key("orgKey") + builder.kind("org") + multiBuilder.addContext(try builder.build().get()) + + let multiContext = try multiBuilder.build().get() + + multiBuilder.addContext(multiContext) + + switch multiBuilder.build() { + case .success(_): + XCTFail("Multibuilder should have failed to build with a multi-context.") + case .failure(let error): + XCTAssertEqual(error, .nestedMultiKind) + } + } + + func testMultikindBuilderFailsWithDuplicateContexts() throws { + var multiBuilder = LDMultiContextBuilder() + + multiBuilder.addContext(try LDContextBuilder(key: "key").build().get()) + multiBuilder.addContext(try LDContextBuilder(key: "second").build().get()) + + switch multiBuilder.build() { + case .success(_): + XCTFail("Multibuilder should have failed to build.") + case .failure(let error): + XCTAssertEqual(error, .duplicateKinds) + } + } + + func testCanSetCustomPropertiesByType() throws { + var builder = LDContextBuilder(key: "key") + builder.kind("user") + builder.trySetValue("loves-swift", true) + builder.trySetValue("pi", 3.1459) + builder.trySetValue("answer-to-life", 42) + builder.trySetValue("company", "LaunchDarkly") + + let context = try builder.build().get() + XCTAssertEqual(.bool(true), context.attributes["loves-swift"]) + XCTAssertEqual(.number(3.1459), context.attributes["pi"]) + XCTAssertEqual(.number(42), context.attributes["answer-to-life"]) + XCTAssertEqual(.string("LaunchDarkly"), context.attributes["company"]) + } + + func testCanSetAndRemovePrivateAttributes() throws { + var builder = LDContextBuilder(key: "key") + + XCTAssertTrue(try builder.build().get().privateAttributes.isEmpty) + + builder.addPrivateAttribute(Reference("name")) + XCTAssertTrue(try builder.build().get().privateAttributes.first == Reference("name")) + + builder.removePrivateAttribute(Reference("name")) + XCTAssertTrue(try builder.build().get().privateAttributes.isEmpty) + + // Removing one should remove them all + builder.addPrivateAttribute(Reference("name")) + builder.addPrivateAttribute(Reference("name")) + builder.removePrivateAttribute(Reference("name")) + XCTAssertTrue(try builder.build().get().privateAttributes.isEmpty) + } + + func testTrySetValueHandlesInvalidValues() { + let tests: [(String, LDValue, Bool)] = [ + ("", .bool(true), false), + ("kind", .bool(true), false), + ("kind", .string("user"), true) + ] + + for (attribute, value, expected) in tests { + var builder = LDContextBuilder(key: "key") + let result = builder.trySetValue(attribute, value) + XCTAssertEqual(result, expected) + } + } + + func testContextCanGetValue() throws { + let tests: [(String, LDValue?)] = [ + // Basic simple attribute retrievals + ("kind", .string("org")), + ("key", .string("my-key")), + ("name", .string("my-name")), + ("transient", .bool(true)), + ("attr", .string("my-attr")), + ("/starts-with-slash", .string("love that prefix")), + ("/crazy~0name", .string("still works")), + ("/other", nil), + // Invalid reference retrieval + ("/", nil), + ("", nil), + ("/a//b", nil), + // Hidden meta attributes + ("privateAttributes", nil), + ("secondary", nil), + // Can index arrays and objects + ("/my-map/array", .array([.string("first"), .string("second")])), + ("/my-map/array/1", .string("second")), + ("/my-map/array/2", nil), + ("my-map/missing", nil), + ("/starts-with-slash/1", nil) + ] + + let array: [LDValue] = [.string("first"), .string("second")] + let map: [String: LDValue] = ["array": .array(array)] + + for (input, expectedValue) in tests { + var builder = LDContextBuilder(key: "my-key") + builder.kind("org") + builder.name("my-name") + builder.transient(true) + builder.secondary("my-secondary") + builder.trySetValue("attr", .string("my-attr")) + builder.trySetValue("starts-with-slash", .string("love that prefix")) + builder.trySetValue("crazy~name", .string("still works")) + builder.trySetValue("my-map", .object(map)) + + let context = try builder.build().get() + + let reference = Reference(input) + + XCTAssertEqual(expectedValue, context.getValue(reference)) + } + } + + func testMultiContextCanGetValue() throws { + var multibuilder = LDMultiContextBuilder() + var builder = LDContextBuilder(key: "user") + + multibuilder.addContext(try builder.build().get()) + + builder.key("org") + builder.kind("org") + builder.name("my-name") + builder.transient(true) + builder.trySetValue("attr", .string("my-attr")) + + multibuilder.addContext(try builder.build().get()) + + let context = try multibuilder.build().get() + + let tests: [(String, LDValue?)] = [ + ("kind", LDValue.string("multi")), + ("key", nil), + ("name", nil), + ("transient", nil), + ("attr", nil) + ] + + for (input, expectedValue) in tests { + let reference = Reference(input) + XCTAssertEqual(context.getValue(reference), expectedValue) + } + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift new file mode 100644 index 00000000..d5263a14 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift @@ -0,0 +1,84 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class ReferenceSpec: XCTestCase { + func testFailsWithCorrectError() { + let tests: [(String, ReferenceError)] = [ + ("", .empty), + ("/", .empty), + ("//", .doubleSlash), + ("/a//b", .doubleSlash), + ("/a/b/", .doubleSlash), + ("/~3", .invalidEscapeSequence), + ("/testing~something", .invalidEscapeSequence), + ("/m~~0", .invalidEscapeSequence), + ("/a~", .invalidEscapeSequence) + ] + + for (path, error) in tests { + let reference = Reference(path) + XCTAssertTrue(!reference.isValid()) + XCTAssertEqual(reference.getError(), error) + } + } + + func testWithoutLeadingSlashes() { + let tests = ["key", "kind", "name", "name/with/slashes", "name~0~1with-what-looks-like-escape-sequences"] + + for test in tests { + let ref = Reference(test) + XCTAssertTrue(ref.isValid()) + XCTAssertEqual(1, ref.depth()) + XCTAssertEqual(test, ref.component(0)?.0) + } + } + + func testWithLeadingSlashes() { + let tests = [ + ("/key", "key"), + ("/kind", "kind"), + ("/name", "name"), + ("/custom", "custom") + ] + + for (ref, expected) in tests { + let ref = Reference(ref) + XCTAssertTrue(ref.isValid()) + XCTAssertEqual(1, ref.depth()) + XCTAssertEqual(expected, ref.component(0)?.0) + } + } + + func testHandlesSubcomponents() { + let tests: [(String, Int, Int, String, Int?)] = [ + ("/a/b", 2, 0, "a", nil), + ("/a/b", 2, 1, "b", nil), + ("/a~1b/c", 2, 0, "a/b", nil), + ("/a~1b/c", 2, 1, "c", nil), + ("/a/10/20/30x", 4, 1, "10", 10), + ("/a/10/20/30x", 4, 2, "20", 20), + ("/a/10/20/30x", 4, 3, "30x", nil) + ] + + for (input, expectedLength, index, expectedName, expectedValue) in tests { + let reference = Reference(input) + + XCTAssertEqual(expectedLength, reference.depth()) + XCTAssertEqual(expectedName, reference.component(index)?.0) + XCTAssertEqual(expectedValue, reference.component(index)?.1) + } + } + + func testCanHandleInvalidIndexRequests() { + let reference = Reference("/a/b/c") + + XCTAssertTrue(reference.isValid()) + XCTAssertNotNil(reference.component(0)) + XCTAssertNotNil(reference.component(1)) + XCTAssertNotNil(reference.component(2)) + + XCTAssertNil(reference.component(3)) + } +} From c66879639428da835b434384811620578234bcbc Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Tue, 31 May 2022 13:36:17 -0400 Subject: [PATCH 55/90] Support context JSON conversions (#204) --- .swiftlint.yml | 5 + ContractTests/.swiftlint.yml | 1 - .../Source/Controllers/SdkController.swift | 81 ++++- ContractTests/Source/Models/client.swift | 5 +- ContractTests/Source/Models/command.swift | 55 ++- ContractTests/testharness-suppressions.txt | 333 ++++++++++-------- LaunchDarkly.xcodeproj/project.pbxproj | 12 + .../contents.xcworkspacedata | 3 + .../xcschemes/ContractTests.xcscheme | 87 +++++ .../Models/Context/LDContext.swift | 233 +++++++++++- .../Models/Context/Reference.swift | 13 +- 11 files changed, 658 insertions(+), 170 deletions(-) create mode 100644 LaunchDarkly.xcworkspace/xcshareddata/xcschemes/ContractTests.xcscheme diff --git a/.swiftlint.yml b/.swiftlint.yml index 71ad606f..59f5d076 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -57,4 +57,9 @@ identifier_name: - lhs - rhs +missing_docs: + error: + - open + - public + reporter: "xcode" diff --git a/ContractTests/.swiftlint.yml b/ContractTests/.swiftlint.yml index 61290d2c..888582b3 100644 --- a/ContractTests/.swiftlint.yml +++ b/ContractTests/.swiftlint.yml @@ -12,7 +12,6 @@ opt_in_rules: - flatmap_over_map_reduce - implicitly_unwrapped_optional - let_var_whitespace - - missing_docs - redundant_nil_coalescing - sorted_first_last - trailing_closure diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index 8ae43444..6530978c 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -10,6 +10,7 @@ final class SdkController { "client-side", "mobile", "service-endpoints", + "strongly-typed", "tags" ] @@ -61,10 +62,6 @@ final class SdkController { if let flushIntervalMs = events.flushIntervalMs { config.eventFlushInterval = flushIntervalMs } - - if let inlineUsers = events.inlineUsers { - config.inlineUserInEvents = inlineUsers - } } if let tags = createInstance.configuration.tags { @@ -151,6 +148,52 @@ final class SdkController { client.track(key: event.eventKey, data: event.data, metricValue: event.metricValue) case "flushEvents": client.flush() + case "contextBuild": + let contextBuild = commandParameters.contextBuild! + + do { + if let singleParams = contextBuild.single { + let context = try SdkController.buildSingleContextFromParams(singleParams) + + let encoder = JSONEncoder() + let output = try encoder.encode(context) + + let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) + return CommandResponse.contextBuild(response) + } + + if let multiParams = contextBuild.multi { + var multiContextBuilder = LDMultiContextBuilder() + try multiParams.forEach { + multiContextBuilder.addContext(try SdkController.buildSingleContextFromParams($0)) + } + + let context = try multiContextBuilder.build().get() + let encoder = JSONEncoder() + let output = try encoder.encode(context) + + let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) + return CommandResponse.contextBuild(response) + } + } catch { + let response = ContextBuildResponse(output: nil, error: error.localizedDescription) + return CommandResponse.contextBuild(response) + } + case "contextConvert": + let convertRequest = commandParameters.contextConvert! + do { + let decoder = JSONDecoder() + let context: LDContext = try decoder.decode(LDContext.self, from: convertRequest.input) + + let encoder = JSONEncoder() + let output = try encoder.encode(context) + + let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) + return CommandResponse.contextBuild(response) + } catch { + let response = ContextBuildResponse(output: nil, error: error.localizedDescription) + return CommandResponse.contextBuild(response) + } default: throw Abort(.badRequest) } @@ -159,6 +202,36 @@ final class SdkController { } } + static func buildSingleContextFromParams(_ params: SingleContextParameters) throws -> LDContext { + var contextBuilder = LDContextBuilder(key: params.key) + + if let kind = params.kind { + contextBuilder.kind(kind) + } + + if let name = params.name { + contextBuilder.name(name) + } + + if let transient = params.transient { + contextBuilder.transient(transient) + } + + if let secondary = params.secondary { + contextBuilder.secondary(secondary) + } + + if let privateAttributes = params.privateAttribute { + privateAttributes.forEach { contextBuilder.addPrivateAttribute(Reference($0)) } + } + + if let custom = params.custom { + custom.forEach { contextBuilder.trySetValue($0.key, $0.value) } + } + + return try contextBuilder.build().get() + } + func evaluate(_ client: LDClient, _ params: EvaluateFlagParameters) throws -> EvaluateFlagResponse { switch params.valueType { case "bool": diff --git a/ContractTests/Source/Models/client.swift b/ContractTests/Source/Models/client.swift index 88a0a3f4..e45170e4 100644 --- a/ContractTests/Source/Models/client.swift +++ b/ContractTests/Source/Models/client.swift @@ -10,6 +10,7 @@ struct Configuration: Content { var credential: String var startWaitTimeMs: Double? var initCanFail: Bool? + // TODO(mmk) Add serviceEndpoints var streaming: StreamingParameters? var polling: PollingParameters? var events: EventParameters? @@ -24,6 +25,7 @@ struct StreamingParameters: Content { struct PollingParameters: Content { var baseUri: String? + // TODO(mmk) Add pollIntervalMs } struct EventParameters: Content { @@ -33,7 +35,6 @@ struct EventParameters: Content { var allAttributesPrivate: Bool? var globalPrivateAttributes: [String]? var flushIntervalMs: Double? - var inlineUsers: Bool? } struct TagParameters: Content { @@ -42,7 +43,9 @@ struct TagParameters: Content { } struct ClientSideParameters: Content { + // TODO(mmk) Remove this user when you have converted everything var initialUser: LDUser + var initialContext: LDContext? var evaluationReasons: Bool? var useReport: Bool? } diff --git a/ContractTests/Source/Models/command.swift b/ContractTests/Source/Models/command.swift index 1981a2ac..298c9967 100644 --- a/ContractTests/Source/Models/command.swift +++ b/ContractTests/Source/Models/command.swift @@ -4,24 +4,30 @@ import LaunchDarkly enum CommandResponse: Content, Encodable { case evaluateFlag(EvaluateFlagResponse) case evaluateAll(EvaluateAllFlagsResponse) + case contextBuild(ContextBuildResponse) + case contextConvert(ContextBuildResponse) case ok func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - if case let CommandResponse.evaluateFlag(response) = self { + switch self { + case .evaluateFlag(let response): try container.encode(response) return - } - - if case let CommandResponse.evaluateAll(response) = self { + case .evaluateAll(let response): + try container.encode(response) + return + case .contextBuild(let response): + try container.encode(response) + return + case .contextConvert(let response): try container.encode(response) return + case .ok: + try container.encode(true) + return } - - try container.encode(true) - - return } } @@ -31,6 +37,8 @@ struct CommandParameters: Content { var evaluateAll: EvaluateAllFlagsParameters? var customEvent: CustomEventParameters? var identifyEvent: IdentifyEventParameters? + var contextBuild: ContextBuildParameters? + var contextConvert: ContextConvertParameters? } struct EvaluateFlagParameters: Content { @@ -47,6 +55,7 @@ struct EvaluateFlagResponse: Content { } struct EvaluateAllFlagsParameters: Content { + // TODO(mmk) Add support for withReasons, clientSideOnly, and detailsOnlyForTrackedFlags } struct EvaluateAllFlagsResponse: Content { @@ -61,5 +70,35 @@ struct CustomEventParameters: Content { } struct IdentifyEventParameters: Content, Decodable { + // TODO(mmk) Remove this user when you have converted everything var user: LDUser + var context: LDContext +} + +struct ContextBuildParameters: Content, Decodable { + var single: SingleContextParameters? + var multi: [SingleContextParameters]? +} + +struct SingleContextParameters: Content, Decodable { + var kind: String? + var key: String + var name: String? + var transient: Bool? + var secondary: String? + var privateAttribute: [String]? + var custom: [String:LDValue]? + + private enum CodingKeys: String, CodingKey { + case kind, key, name, transient, secondary, privateAttribute = "private", custom + } +} + +struct ContextBuildResponse: Content, Encodable { + var output: String? + var error: String? +} + +struct ContextConvertParameters: Content, Decodable { + var input: String } diff --git a/ContractTests/testharness-suppressions.txt b/ContractTests/testharness-suppressions.txt index cc2f1626..35119476 100644 --- a/ContractTests/testharness-suppressions.txt +++ b/ContractTests/testharness-suppressions.txt @@ -1,170 +1,211 @@ -events/requests/method and headers -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate flag with detail +evaluation/parameterized/evaluationReasons=false/basic values - any +evaluation/parameterized/evaluationReasons=false/basic values - bool evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate all flags -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag without detail -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag with detail +evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate flag with detail evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate all flags -streaming/requests/user properties/GET -streaming/requests/user properties/REPORT -streaming/updates/flag delete for previously nonexistent flag is applied -polling/requests/query parameters/evaluationReasons set to [none]/GET -polling/requests/query parameters/evaluationReasons set to [none]/REPORT -polling/requests/query parameters/evaluationReasons set to false/GET -polling/requests/query parameters/evaluationReasons set to false/REPORT -polling/requests/query parameters/evaluationReasons set to true/GET -polling/requests/query parameters/evaluationReasons set to true/REPORT -polling/requests/user properties/GET -polling/requests/user properties/REPORT -events/user properties/inlineUsers=false/user-private=none/identify event -events/user properties/inlineUsers=false/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=false, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=false, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true/user-private=none/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true/user-private=none/identify event -events/user properties/inlineUsers=true/user-private=none/feature event -events/user properties/inlineUsers=true/user-private=none/custom event -events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/custom event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/feature event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/custom event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event +evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag with detail +evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag without detail +evaluation/parameterized/evaluationReasons=false/basic values - double +evaluation/parameterized/evaluationReasons=false/basic values - int +evaluation/parameterized/evaluationReasons=false/basic values - string +evaluation/parameterized/evaluationReasons=false/errors - any +evaluation/parameterized/evaluationReasons=false/errors - bool +evaluation/parameterized/evaluationReasons=false/errors - double +evaluation/parameterized/evaluationReasons=false/errors - int +evaluation/parameterized/evaluationReasons=false/errors - string +evaluation/parameterized/evaluationReasons=false/evaluation reasons - error MALFORMED_FLAG +evaluation/parameterized/evaluationReasons=false/evaluation reasons - fallthrough +evaluation/parameterized/evaluationReasons=false/evaluation reasons - fallthrough experiment +evaluation/parameterized/evaluationReasons=false/evaluation reasons - off +evaluation/parameterized/evaluationReasons=false/evaluation reasons - prerequisite failed +evaluation/parameterized/evaluationReasons=false/evaluation reasons - rule match +evaluation/parameterized/evaluationReasons=false/evaluation reasons - rule match experiment +evaluation/parameterized/evaluationReasons=false/evaluation reasons - target match +evaluation/parameterized/evaluationReasons=false/wrong type errors +evaluation/parameterized/evaluationReasons=true/basic values - any +evaluation/parameterized/evaluationReasons=true/basic values - bool +evaluation/parameterized/evaluationReasons=true/basic values - double +evaluation/parameterized/evaluationReasons=true/basic values - int +evaluation/parameterized/evaluationReasons=true/basic values - string +evaluation/parameterized/evaluationReasons=true/errors - any +evaluation/parameterized/evaluationReasons=true/errors - bool +evaluation/parameterized/evaluationReasons=true/errors - double +evaluation/parameterized/evaluationReasons=true/errors - int +evaluation/parameterized/evaluationReasons=true/errors - string +evaluation/parameterized/evaluationReasons=true/evaluation reasons - error MALFORMED_FLAG evaluation/parameterized/evaluationReasons=true/evaluation reasons - error MALFORMED_FLAG/flag-key/evaluate flag with detail -events/requests/URL path is computed correctly/base URI has no trailing slash -events/requests/URL path is computed correctly/base URI has a trailing slash -events/requests/new payload ID for each post -events/experimentation/FALLTHROUGH -events/experimentation/RULE_MATCH -events/identify events/basic properties/single kind default -events/identify events/basic properties/single kind non-default -events/identify events/basic properties/multi-kind -events/custom events/data and metricValue parameters/data=null -events/custom events/data and metricValue parameters/data=false -events/custom events/data and metricValue parameters/data=true -events/custom events/data and metricValue parameters/data=0 -events/custom events/data and metricValue parameters/data=1000 -events/custom events/data and metricValue parameters/data=1000.5 +evaluation/parameterized/evaluationReasons=true/evaluation reasons - fallthrough +evaluation/parameterized/evaluationReasons=true/evaluation reasons - fallthrough experiment +evaluation/parameterized/evaluationReasons=true/evaluation reasons - off +evaluation/parameterized/evaluationReasons=true/evaluation reasons - prerequisite failed +evaluation/parameterized/evaluationReasons=true/evaluation reasons - rule match +evaluation/parameterized/evaluationReasons=true/evaluation reasons - rule match experiment +evaluation/parameterized/evaluationReasons=true/evaluation reasons - target match +evaluation/parameterized/evaluationReasons=true/wrong type errors +events/context properties/custom attribute with value "" +events/context properties/custom attribute with value "abc" +events/context properties/custom attribute with value "has \"escaped\" characters" +events/context properties/custom attribute with value -1000 +events/context properties/custom attribute with value -1000.5 +events/context properties/custom attribute with value 0 +events/context properties/custom attribute with value 1000 +events/context properties/custom attribute with value 1000.5 +events/context properties/custom attribute with value ["a","b"] +events/context properties/custom attribute with value [] +events/context properties/custom attribute with value false +events/context properties/custom attribute with value true +events/context properties/custom attribute with value {"a":1} +events/context properties/custom attribute with value {} +events/context properties/multi-kind minimal +events/context properties/single-kind minimal +events/context properties/single-kind with attributes, nothing private +events/context properties/single-kind, allAttributesPrivate +events/context properties/single-kind, private attribute nested property +events/context properties/single-kind, specific private attributes +events/custom events/basic properties/multi-kind +events/custom events/basic properties/single kind default +events/custom events/basic properties/single kind non-default +events/custom events/data and metricValue parameters events/custom events/data and metricValue parameters/data="" -events/custom events/data and metricValue parameters/data="abc" -events/custom events/data and metricValue parameters/data=[1,2] -events/custom events/data and metricValue parameters/data={"property":true} -events/custom events/data and metricValue parameters/data=null, omitNullData -events/custom events/data and metricValue parameters/data=null, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=false, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=true, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=0, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=1000, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=1000.5, metricValue=0.000000 +events/custom events/data and metricValue parameters/data="", metricValue=-1.500000 events/custom events/data and metricValue parameters/data="", metricValue=0.000000 +events/custom events/data and metricValue parameters/data="", metricValue=1.500000 +events/custom events/data and metricValue parameters/data="abc" +events/custom events/data and metricValue parameters/data="abc", metricValue=-1.500000 events/custom events/data and metricValue parameters/data="abc", metricValue=0.000000 -events/custom events/data and metricValue parameters/data=[1,2], metricValue=0.000000 -events/custom events/data and metricValue parameters/data={"property":true}, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=null, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=false, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=true, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data="abc", metricValue=1.500000 +events/custom events/data and metricValue parameters/data=0 events/custom events/data and metricValue parameters/data=0, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=0, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=0, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=1000 events/custom events/data and metricValue parameters/data=1000, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=1000, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=1000, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=1000.5 events/custom events/data and metricValue parameters/data=1000.5, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data="", metricValue=-1.500000 -events/custom events/data and metricValue parameters/data="abc", metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=1000.5, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=1000.5, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=[1,2] events/custom events/data and metricValue parameters/data=[1,2], metricValue=-1.500000 -events/custom events/data and metricValue parameters/data={"property":true}, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=null, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=[1,2], metricValue=0.000000 +events/custom events/data and metricValue parameters/data=[1,2], metricValue=1.500000 +events/custom events/data and metricValue parameters/data=false +events/custom events/data and metricValue parameters/data=false, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=false, metricValue=0.000000 events/custom events/data and metricValue parameters/data=false, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=null +events/custom events/data and metricValue parameters/data=null, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=null, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=null, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=null, omitNullData +events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=0.000000 +events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=1.500000 +events/custom events/data and metricValue parameters/data=true +events/custom events/data and metricValue parameters/data=true, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data=true, metricValue=0.000000 events/custom events/data and metricValue parameters/data=true, metricValue=1.500000 -events/custom events/data and metricValue parameters/data=0, metricValue=1.500000 -events/custom events/data and metricValue parameters/data=1000, metricValue=1.500000 -events/custom events/data and metricValue parameters/data=1000.5, metricValue=1.500000 -events/custom events/data and metricValue parameters/data="", metricValue=1.500000 -events/custom events/data and metricValue parameters/data="abc", metricValue=1.500000 -events/custom events/data and metricValue parameters/data=[1,2], metricValue=1.500000 +events/custom events/data and metricValue parameters/data={"property":true} +events/custom events/data and metricValue parameters/data={"property":true}, metricValue=-1.500000 +events/custom events/data and metricValue parameters/data={"property":true}, metricValue=0.000000 events/custom events/data and metricValue parameters/data={"property":true}, metricValue=1.500000 -events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=1.500000 -events/custom events/basic properties/single kind default -events/custom events/basic properties/single kind non-default -events/custom events/basic properties/multi-kind -events/context properties/single-kind minimal -events/context properties/multi-kind minimal -events/context properties/single-kind with attributes, nothing private -events/context properties/single-kind, allAttributesPrivate -events/context properties/single-kind, specific private attributes -events/context properties/single-kind, private attribute nested property -events/context properties/custom attribute with value false -events/context properties/custom attribute with value true -events/context properties/custom attribute with value -1000 -events/context properties/custom attribute with value 0 -events/context properties/custom attribute with value 1000 -events/context properties/custom attribute with value -1000.5 -events/context properties/custom attribute with value 1000.5 -events/context properties/custom attribute with value "" -events/context properties/custom attribute with value "abc" -events/context properties/custom attribute with value "has \"escaped\" characters" -events/context properties/custom attribute with value [] -events/context properties/custom attribute with value ["a","b"] -events/context properties/custom attribute with value {} -events/context properties/custom attribute with value {"a":1} -events/event capacity/capacity is enforced +events/disabling/custom event +events/disabling/evaluation +events/disabling/identify event events/event capacity/buffer is reset after flush +events/event capacity/capacity is enforced events/event capacity/summary event is still included even if buffer was full -events/disabling/identify event +events/experimentation/FALLTHROUGH +events/experimentation/RULE_MATCH +events/identify events/basic properties/multi-kind +events/identify events/basic properties/single kind default +events/identify events/basic properties/single kind non-default +events/requests/URL path is computed correctly/base URI has a trailing slash +events/requests/URL path is computed correctly/base URI has no trailing slash +events/requests/method and headers +events/requests/new payload ID for each post +events/user properties/inlineUsers=false, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=false, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/identify event +events/user properties/inlineUsers=false, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=false, allAttributesPrivate=true/user-private=none/identify event +events/user properties/inlineUsers=false, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=false, globally-private=[firstName]/user-private=none/identify event +events/user properties/inlineUsers=false/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=false/user-private=none/identify event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/custom event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/feature event +events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/identify event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/custom event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/feature event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/custom event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/feature event +events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/identify event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/custom event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/feature event +events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/identify event +events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/custom event +events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/feature event +events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/identify event +events/user properties/inlineUsers=true/user-private=none/custom event +events/user properties/inlineUsers=true/user-private=none/feature event +events/user properties/inlineUsers=true/user-private=none/identify event +polling/requests/URL path is computed correctly/base URI has a trailing slash/GET +polling/requests/URL path is computed correctly/base URI has a trailing slash/REPORT +polling/requests/URL path is computed correctly/base URI has no trailing slash/GET +polling/requests/URL path is computed correctly/base URI has no trailing slash/REPORT +polling/requests/context properties/multi-kind/GET +polling/requests/context properties/multi-kind/REPORT +polling/requests/context properties/single kind minimal/GET +polling/requests/context properties/single kind minimal/REPORT +polling/requests/context properties/single kind with all attributes/GET +polling/requests/context properties/single kind with all attributes/REPORT +polling/requests/method and headers/GET +polling/requests/method and headers/REPORT +polling/requests/query parameters/evaluationReasons set to [none]/GET +polling/requests/query parameters/evaluationReasons set to [none]/REPORT +polling/requests/query parameters/evaluationReasons set to false/GET +polling/requests/query parameters/evaluationReasons set to false/REPORT +polling/requests/query parameters/evaluationReasons set to true/GET +polling/requests/query parameters/evaluationReasons set to true/REPORT +polling/requests/user properties/GET +polling/requests/user properties/REPORT +streaming/requests/URL path is computed correctly/base URI has a trailing slash/GET +streaming/requests/URL path is computed correctly/base URI has a trailing slash/REPORT +streaming/requests/URL path is computed correctly/base URI has no trailing slash/GET +streaming/requests/URL path is computed correctly/base URI has no trailing slash/REPORT +streaming/requests/context properties/multi-kind/GET +streaming/requests/context properties/multi-kind/REPORT +streaming/requests/context properties/single kind minimal/GET +streaming/requests/context properties/single kind minimal/REPORT +streaming/requests/context properties/single kind with all attributes/GET +streaming/requests/context properties/single kind with all attributes/REPORT +streaming/requests/method and headers/GET +streaming/requests/method and headers/REPORT streaming/requests/query parameters/evaluationReasons set to [none]/GET streaming/requests/query parameters/evaluationReasons set to [none]/REPORT streaming/requests/query parameters/evaluationReasons set to false/GET streaming/requests/query parameters/evaluationReasons set to false/REPORT streaming/requests/query parameters/evaluationReasons set to true/GET streaming/requests/query parameters/evaluationReasons set to true/REPORT -streaming/requests/context properties/single kind minimal/GET -streaming/requests/context properties/single kind minimal/REPORT -streaming/requests/context properties/single kind with all attributes/GET -streaming/requests/context properties/single kind with all attributes/REPORT -streaming/requests/context properties/multi-kind/GET -streaming/requests/context properties/multi-kind/REPORT +streaming/requests/user properties/GET +streaming/requests/user properties/REPORT streaming/updates/flag delete for previously nonexistent flag is applied -polling/requests/query parameters/evaluationReasons set to [none]/GET -polling/requests/query parameters/evaluationReasons set to [none]/REPORT -polling/requests/query parameters/evaluationReasons set to false/GET -polling/requests/query parameters/evaluationReasons set to false/REPORT -polling/requests/query parameters/evaluationReasons set to true/GET -polling/requests/query parameters/evaluationReasons set to true/REPORT -polling/requests/context properties/single kind minimal/GET -polling/requests/context properties/single kind minimal/REPORT -polling/requests/context properties/single kind with all attributes/GET -polling/requests/context properties/single kind with all attributes/REPORT -polling/requests/context properties/multi-kind/GET -polling/requests/context properties/multi-kind/REPORT -tags/event posts/{"applicationId":null,"applicationVersion":null} -tags/event posts/{"applicationId":null,"applicationVersion":""} -tags/event posts/{"applicationId":null,"applicationVersion":""} -tags/event posts/{"applicationId":null,"applicationVersion":""} -tags/event posts/{"applicationId":"","applicationVersion":null} -tags/event posts/{"applicationId":"","applicationVersion":""} -tags/event posts/{"applicationId":"","applicationVersion":""} +streaming/updates/flag delete with higher version is applied +streaming/updates/flag delete with lower version is not applied +streaming/updates/flag delete with same version is not applied +streaming/updates/flag patch for previously nonexistent flag is applied +streaming/updates/flag patch with higher version is applied +streaming/updates/flag patch with lower version is not applied +streaming/updates/flag patch with same version is not applied tags/event posts/{"applicationId":"","applicationVersion":""} tags/event posts/{"applicationId":"","applicationVersion":null} -tags/event posts/{"applicationId":"","applicationVersion":""} -tags/event posts/{"applicationId":"","applicationVersion":""} -tags/event posts/{"applicationId":"","applicationVersion":""} -tags/event posts/{"applicationId":"","applicationVersion":null} -tags/event posts/{"applicationId":"","applicationVersion":""} -tags/event posts/{"applicationId":"","applicationVersion":""} -tags/event posts/{"applicationId":"","applicationVersion":""} +tags/event posts/{"applicationId":null,"applicationVersion":""} +tags/event posts/{"applicationId":null,"applicationVersion":null} diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index ef6db553..dbda7735 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -1152,12 +1152,14 @@ A310881E2837DC0400184942 /* Kind.swift in Sources */, A310881A2837DC0400184942 /* Reference.swift in Sources */, 831188522113ADF700D77CB5 /* KeyedValueCache.swift in Sources */, + A3422BB9283591D30047396B /* ReferenceSpec.swift in Sources */, 831188582113AE0F00D77CB5 /* EventReporter.swift in Sources */, 8311885D2113AE2500D77CB5 /* DarklyService.swift in Sources */, 831188692113AE5900D77CB5 /* ObjcLDConfig.swift in Sources */, A31088222837DC0400184942 /* LDContext.swift in Sources */, 8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */, C43C37E8238DF22D003C1624 /* LDEvaluationDetail.swift in Sources */, + A3422BB5283591D30047396B /* LDContextSpec.swift in Sources */, 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */, C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831188592113AE1200D77CB5 /* FlagStore.swift in Sources */, @@ -1176,6 +1178,7 @@ 29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */, 8347BB0F21F147E100E56BCD /* LDTimer.swift in Sources */, B4C9D43B2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, + A3422BBD283591D30047396B /* KindSpec.swift in Sources */, C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */, B468E71324B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, 8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */, @@ -1209,9 +1212,11 @@ 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */, C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */, + A3422BBC283591D30047396B /* KindSpec.swift in Sources */, 8354AC722243166900CDE602 /* FeatureFlagCache.swift in Sources */, A310881D2837DC0400184942 /* Kind.swift in Sources */, C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */, + A3422BB8283591D30047396B /* ReferenceSpec.swift in Sources */, 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */, 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */, 831EF34D20655E730001C643 /* FlagsUnchangedObserver.swift in Sources */, @@ -1246,6 +1251,7 @@ 831EF36520655E730001C643 /* Thread.swift in Sources */, 83B1D7C92073F354006D1B1C /* CwlSysctl.swift in Sources */, 831EF36620655E730001C643 /* ObjcLDClient.swift in Sources */, + A3422BB4283591D30047396B /* LDContextSpec.swift in Sources */, 831EF36720655E730001C643 /* ObjcLDConfig.swift in Sources */, 831EF36820655E730001C643 /* ObjcLDUser.swift in Sources */, B4C9D43A2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, @@ -1269,12 +1275,14 @@ A310881B2837DC0400184942 /* Kind.swift in Sources */, A31088172837DC0400184942 /* Reference.swift in Sources */, 83A2D6241F51CD7A00EA3BD4 /* LDUser.swift in Sources */, + A3422BB6283591D30047396B /* ReferenceSpec.swift in Sources */, 8354EFE21F26380700C05156 /* Event.swift in Sources */, C408884923033B7500420721 /* ConnectionInformation.swift in Sources */, 831D8B721F71D3E700ED65E8 /* DarklyService.swift in Sources */, A310881F2837DC0400184942 /* LDContext.swift in Sources */, 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */, 8358F25E1F474E5900ECE1AF /* LDChangedFlag.swift in Sources */, + A3422BB2283591D30047396B /* LDContextSpec.swift in Sources */, 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */, C43C37E1236BA050003C1624 /* LDEvaluationDetail.swift in Sources */, 831AAE2C20A9E4F600B46DBA /* Throttler.swift in Sources */, @@ -1293,6 +1301,7 @@ 29A4C47527DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B1206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, C408884723033B3600420721 /* ConnectionInformationStore.swift in Sources */, + A3422BBA283591D30047396B /* KindSpec.swift in Sources */, 83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */, B4C9D4382489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, 8347BB0C21F147E100E56BCD /* LDTimer.swift in Sources */, @@ -1373,12 +1382,14 @@ A310881C2837DC0400184942 /* Kind.swift in Sources */, A31088182837DC0400184942 /* Reference.swift in Sources */, 83D9EC7E2062DEAB004D7FA6 /* FlagChangeObserver.swift in Sources */, + A3422BB7283591D30047396B /* ReferenceSpec.swift in Sources */, 83D9EC7F2062DEAB004D7FA6 /* FlagsUnchangedObserver.swift in Sources */, 83D9EC802062DEAB004D7FA6 /* Event.swift in Sources */, 83D9EC822062DEAB004D7FA6 /* ClientServiceFactory.swift in Sources */, A31088202837DC0400184942 /* LDContext.swift in Sources */, 83D9EC832062DEAB004D7FA6 /* KeyedValueCache.swift in Sources */, 831AAE2D20A9E4F600B46DBA /* Throttler.swift in Sources */, + A3422BB3283591D30047396B /* LDContextSpec.swift in Sources */, C43C37E6238DF22B003C1624 /* LDEvaluationDetail.swift in Sources */, 83D9EC872062DEAB004D7FA6 /* FlagSynchronizer.swift in Sources */, C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, @@ -1397,6 +1408,7 @@ 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B2206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */, + A3422BBB283591D30047396B /* KindSpec.swift in Sources */, 8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */, 8354AC712243166900CDE602 /* FeatureFlagCache.swift in Sources */, C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */, diff --git a/LaunchDarkly.xcworkspace/contents.xcworkspacedata b/LaunchDarkly.xcworkspace/contents.xcworkspacedata index ad05ac86..8c28d75a 100644 --- a/LaunchDarkly.xcworkspace/contents.xcworkspacedata +++ b/LaunchDarkly.xcworkspace/contents.xcworkspacedata @@ -1,6 +1,9 @@ + + diff --git a/LaunchDarkly.xcworkspace/xcshareddata/xcschemes/ContractTests.xcscheme b/LaunchDarkly.xcworkspace/xcshareddata/xcschemes/ContractTests.xcscheme new file mode 100644 index 00000000..388c31da --- /dev/null +++ b/LaunchDarkly.xcworkspace/xcshareddata/xcschemes/ContractTests.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index eadf4b83..61ad5280 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -10,7 +10,7 @@ public enum ContextBuilderError: Error { } /// TKTK -public struct LDContext { +public struct LDContext: Encodable { internal var kind: Kind = .user fileprivate var contexts: [LDContext] = [] @@ -28,6 +28,68 @@ public struct LDContext { self.canonicalizedKey = canonicalizedKey } + public struct Meta: Codable { + public var secondary: String? + public var privateAttributes: [Reference]? + + enum CodingKeys: CodingKey { + case secondary, privateAttributes + } + + public var isEmpty: Bool { + secondary == nil && (privateAttributes?.isEmpty ?? true) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(secondary, forKey: .secondary) + + if let privateAttributes = privateAttributes, !privateAttributes.isEmpty { + try container.encodeIfPresent(privateAttributes, forKey: .privateAttributes) + } + } + } + + static private func encodeSingleContext(container: inout KeyedEncodingContainer, context: LDContext, discardKind: Bool) throws { + if !discardKind { + try container.encodeIfPresent(context.kind.description, forKey: DynamicCodingKeys(string: "kind")) + } + + try container.encodeIfPresent(context.key, forKey: DynamicCodingKeys(string: "key")) + try container.encodeIfPresent(context.name, forKey: DynamicCodingKeys(string: "name")) + + let meta = Meta(secondary: context.secondary, privateAttributes: context.privateAttributes) + + if !meta.isEmpty { + try container.encodeIfPresent(meta, forKey: DynamicCodingKeys(string: "_meta")) + } + + if !context.attributes.isEmpty { + try context.attributes.forEach { + try container.encodeIfPresent($0.value, forKey: DynamicCodingKeys(string: $0.key)) + } + } + + if context.transient { + try container.encodeIfPresent(context.transient, forKey: DynamicCodingKeys(string: "transient")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: DynamicCodingKeys.self) + + if isMulti() { + try container.encodeIfPresent(kind.description, forKey: DynamicCodingKeys(string: "kind")) + + for context in contexts { + var contextContainer = container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: DynamicCodingKeys(string: context.kind.description)) + try LDContext.encodeSingleContext(container: &contextContainer, context: context, discardKind: true) + } + } else { + try LDContext.encodeSingleContext(container: &container, context: self, discardKind: false) + } + } + /// TKTK public func fullyQualifiedKey() -> String { return canonicalizedKey @@ -111,6 +173,149 @@ public struct LDContext { } } +extension LDContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + switch try container.decodeIfPresent(String.self, forKey: DynamicCodingKeys(string: "kind")) { + case .none: + if container.contains(DynamicCodingKeys(string: "kind")) { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [DynamicCodingKeys(string: "kind")], + debugDescription: "Kind cannot be null" + ) + ) + } + + let values = try decoder.container(keyedBy: UserCodingKeys.self) + + let key = try values.decode(String.self, forKey: .key) + var contextBuilder = LDContextBuilder(key: key) + contextBuilder.allowEmptyKey = true + + if let name = try values.decodeIfPresent(String.self, forKey: .name) { + contextBuilder.name(name) + } + if let firstName = try values.decodeIfPresent(String.self, forKey: .firstName) { + contextBuilder.trySetValue("firstName", .string(firstName)) + } + if let lastName = try values.decodeIfPresent(String.self, forKey: .lastName) { + contextBuilder.trySetValue("lastName", .string(lastName)) + } + if let country = try values.decodeIfPresent(String.self, forKey: .country) { + contextBuilder.trySetValue("country", .string(country)) + } + if let ip = try values.decodeIfPresent(String.self, forKey: .ip) { + contextBuilder.trySetValue("ip", .string(ip)) + } + if let email = try values.decodeIfPresent(String.self, forKey: .email) { + contextBuilder.trySetValue("email", .string(email)) + } + if let avatar = try values.decodeIfPresent(String.self, forKey: .avatar) { + contextBuilder.trySetValue("avatar", .string(avatar)) + } + + let custom = try values.decodeIfPresent([String: LDValue].self, forKey: .custom) ?? [:] + custom.forEach { contextBuilder.trySetValue($0.key, $0.value) } + + let isAnonymous = try values.decodeIfPresent(Bool.self, forKey: .isAnonymous) ?? false + contextBuilder.transient(isAnonymous) + + let privateAttributeNames = try values.decodeIfPresent([String].self, forKey: .privateAttributeNames) ?? [] + privateAttributeNames.forEach { contextBuilder.addPrivateAttribute(Reference($0)) } + + if let secondary = try values.decodeIfPresent(String.self, forKey: .secondary) { + contextBuilder.secondary(secondary) + } + + self = try contextBuilder.build().get() + case .some("multi"): + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + var multiContextBuilder = LDMultiContextBuilder() + + for key in container.allKeys { + if key.stringValue == "kind" { + continue + } + + let contextContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: DynamicCodingKeys(string: key.stringValue)) + multiContextBuilder.addContext(try LDContext.decodeSingleContext(container: contextContainer, kind: key.stringValue)) + } + + self = try multiContextBuilder.build().get() + case .some(""): + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [DynamicCodingKeys(string: "kind")], + debugDescription: "Kind cannot be empty" + ) + ) + case .some(let kind): + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + self = try LDContext.decodeSingleContext(container: container, kind: kind) + } + } + + static private func decodeSingleContext(container: KeyedDecodingContainer, kind: String) throws -> LDContext { + let key = try container.decode(String.self, forKey: DynamicCodingKeys(string: "key")) + + var contextBuilder = LDContextBuilder(key: key) + contextBuilder.kind(kind) + + for key in container.allKeys { + switch key.stringValue { + case "key": + continue + case "_meta": + if let meta = try container.decodeIfPresent(LDContext.Meta.self, forKey: DynamicCodingKeys(string: "_meta")) { + if let secondary = meta.secondary { + contextBuilder.secondary(secondary) + } + + if let privateAttributes = meta.privateAttributes { + privateAttributes.forEach { contextBuilder.addPrivateAttribute($0) } + } + } + + default: + if let value = try container.decodeIfPresent(LDValue.self, forKey: DynamicCodingKeys(string: key.stringValue)) { + contextBuilder.trySetValue(key.stringValue, value) + } + } + } + + return try contextBuilder.build().get() + } + + // This CodingKey implementation allows us to dynamically access fields in + // any JSON payload without having to pre-define the possible keys. + private struct DynamicCodingKeys: CodingKey { + // Protocol required implementations + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + return nil + } + + // Convenience method since we don't want to unwrap everywhere + init(string: String) { + self.stringValue = string + } + } + + enum UserCodingKeys: String, CodingKey { + case key, name, firstName, lastName, country, ip, email, avatar, custom, isAnonymous = "anonymous", device, operatingSystem = "os", config, privateAttributeNames, secondary + } +} + extension LDContext: TypeIdentifying {} /// TKTK @@ -126,6 +331,11 @@ public struct LDContextBuilder { private var key: String? private var attributes: [String: LDValue] = [:] + // Contexts that were deserialized from implicit user formats + // are allowed to have empty string keys. Otherwise, key is + // never allowed to be empty. + fileprivate var allowEmptyKey: Bool = false + /// TKTK public init(key: String) { self.key = key @@ -190,19 +400,23 @@ public struct LDContextBuilder { return true } - mutating func secondary(_ secondary: String) { + /// TKTK + public mutating func secondary(_ secondary: String) { self.secondary = secondary } - mutating func transient(_ transient: Bool) { + /// TKTK + public mutating func transient(_ transient: Bool) { self.transient = transient } - mutating func addPrivateAttribute(_ reference: Reference) { + /// TKTK + public mutating func addPrivateAttribute(_ reference: Reference) { self.privateAttributes.append(reference) } - mutating func removePrivateAttribute(_ reference: Reference) { + /// TKTK + public mutating func removePrivateAttribute(_ reference: Reference) { self.privateAttributes.removeAll { $0 == reference } } @@ -216,10 +430,7 @@ public struct LDContextBuilder { return Result.failure(.requiresMultiBuilder) } - // TODO(mmk) If we are converting legacy users to newer user contexts, - // then the key is allowed to be empty. Otherwise, it cannot be. So we - // need to hook up that condition still. - if self.key?.isEmpty ?? true { + if !allowEmptyKey && self.key?.isEmpty ?? true { return Result.failure(.emptyKey) } @@ -228,6 +439,7 @@ public struct LDContextBuilder { context.contexts = [] context.name = self.name context.transient = self.transient + context.secondary = self.secondary context.privateAttributes = self.privateAttributes context.key = self.key context.attributes = self.attributes @@ -242,6 +454,9 @@ extension LDContextBuilder: TypeIdentifying { } public struct LDMultiContextBuilder { private var contexts: [LDContext] = [] + /// TKTK + public init() {} + /// TKTK public mutating func addContext(_ context: LDContext) { contexts.append(context) diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift index c83df0ac..b2f634a8 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift @@ -74,7 +74,7 @@ public struct Reference: Codable, Equatable { return Result.success(output) } - init(_ value: String) { + public init(_ value: String) { rawPath = value if value.isEmpty || value == "/" { @@ -114,6 +114,17 @@ public struct Reference: Codable, Equatable { components = referenceComponents } + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let reference = try container.decode(String.self) + self = Reference(reference) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawPath) + } + public func isValid() -> Bool { return error == nil } From b1a443b9a36a6fb4778a716f7bebb2636fb4a6c2 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 1 Jun 2022 12:49:16 -0400 Subject: [PATCH 56/90] Update events from users to contexts (#206) --- .../Source/Controllers/SdkController.swift | 8 +- ContractTests/Source/Models/client.swift | 2 +- ContractTests/Source/Models/command.swift | 2 - ContractTests/testharness-suppressions.txt | 214 +------------- LaunchDarkly.xcodeproj/project.pbxproj | 16 +- .../GeneratedCode/mocks.generated.swift | 18 +- LaunchDarkly/LaunchDarkly/LDClient.swift | 138 ++++----- .../LaunchDarkly/LDClientVariation.swift | 12 +- LaunchDarkly/LaunchDarkly/LDCommon.swift | 2 +- .../Models/Context/LDContext.swift | 275 +++++++++++++++++- .../Models/Context/Reference.swift | 8 +- .../LaunchDarkly/Models/DiagnosticEvent.swift | 4 +- LaunchDarkly/LaunchDarkly/Models/Event.swift | 39 ++- .../LaunchDarkly/Models/LDConfig.swift | 44 +-- .../Networking/DarklyService.swift | 16 +- .../LaunchDarkly/Networking/HTTPHeaders.swift | 4 +- .../ObjectiveC/ObjcLDClient.swift | 54 ++-- .../ObjectiveC/ObjcLDConfig.swift | 24 +- .../ServiceObjects/Cache/CacheConverter.swift | 6 +- .../Cache/FeatureFlagCache.swift | 16 +- .../ServiceObjects/ClientServiceFactory.swift | 6 +- .../ServiceObjects/EventReporter.swift | 18 +- .../LaunchDarklyTests/LDClientSpec.swift | 149 +++++----- .../Mocks/ClientServiceMockFactory.swift | 4 +- .../Mocks/DarklyServiceMock.swift | 6 +- .../Mocks/LDContextStub.swift | 58 ++++ .../Models/DiagnosticEventSpec.swift | 2 +- .../LaunchDarklyTests/Models/EventSpec.swift | 111 ++++--- .../Models/LDConfigSpec.swift | 16 +- .../Networking/DarklyServiceSpec.swift | 32 +- .../Networking/HTTPHeadersSpec.swift | 2 +- .../Cache/FeatureFlagCacheSpec.swift | 20 +- .../ServiceObjects/EventReporterSpec.swift | 36 +-- 33 files changed, 732 insertions(+), 630 deletions(-) create mode 100644 LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index 6530978c..c2a997fe 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -52,11 +52,11 @@ final class SdkController { } if let allPrivate = events.allAttributesPrivate { - config.allUserAttributesPrivate = allPrivate + config.allContextAttributesPrivate = allPrivate } if let globalPrivate = events.globalPrivateAttributes { - config.privateUserAttributes = globalPrivate.map { UserAttribute.forName($0) } + config.privateContextAttributes = globalPrivate.map { Reference($0) } } if let flushIntervalMs = events.flushIntervalMs { @@ -90,7 +90,7 @@ final class SdkController { let dispatchSemaphore = DispatchSemaphore(value: 0) let startWaitSeconds = (createInstance.configuration.startWaitTimeMs ?? 5_000) / 1_000 - LDClient.start(config: config, user: clientSide.initialUser, startWaitSeconds: startWaitSeconds) { _ in + LDClient.start(config: config, context: clientSide.initialContext, startWaitSeconds: startWaitSeconds) { _ in dispatchSemaphore.signal() } @@ -139,7 +139,7 @@ final class SdkController { return CommandResponse.evaluateAll(result) case "identifyEvent": let semaphore = DispatchSemaphore(value: 0) - client.identify(user: commandParameters.identifyEvent!.user) { + client.identify(context: commandParameters.identifyEvent!.context) { semaphore.signal() } semaphore.wait() diff --git a/ContractTests/Source/Models/client.swift b/ContractTests/Source/Models/client.swift index e45170e4..c8e4eccc 100644 --- a/ContractTests/Source/Models/client.swift +++ b/ContractTests/Source/Models/client.swift @@ -44,7 +44,7 @@ struct TagParameters: Content { struct ClientSideParameters: Content { // TODO(mmk) Remove this user when you have converted everything - var initialUser: LDUser + var initialUser: LDUser? var initialContext: LDContext? var evaluationReasons: Bool? var useReport: Bool? diff --git a/ContractTests/Source/Models/command.swift b/ContractTests/Source/Models/command.swift index 298c9967..c2167226 100644 --- a/ContractTests/Source/Models/command.swift +++ b/ContractTests/Source/Models/command.swift @@ -70,8 +70,6 @@ struct CustomEventParameters: Content { } struct IdentifyEventParameters: Content, Decodable { - // TODO(mmk) Remove this user when you have converted everything - var user: LDUser var context: LDContext } diff --git a/ContractTests/testharness-suppressions.txt b/ContractTests/testharness-suppressions.txt index 35119476..78c48e6c 100644 --- a/ContractTests/testharness-suppressions.txt +++ b/ContractTests/testharness-suppressions.txt @@ -1,211 +1,9 @@ -evaluation/parameterized/evaluationReasons=false/basic values - any -evaluation/parameterized/evaluationReasons=false/basic values - bool -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate all flags -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate flag with detail -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate all flags -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag with detail -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag without detail -evaluation/parameterized/evaluationReasons=false/basic values - double -evaluation/parameterized/evaluationReasons=false/basic values - int -evaluation/parameterized/evaluationReasons=false/basic values - string -evaluation/parameterized/evaluationReasons=false/errors - any -evaluation/parameterized/evaluationReasons=false/errors - bool -evaluation/parameterized/evaluationReasons=false/errors - double -evaluation/parameterized/evaluationReasons=false/errors - int -evaluation/parameterized/evaluationReasons=false/errors - string -evaluation/parameterized/evaluationReasons=false/evaluation reasons - error MALFORMED_FLAG -evaluation/parameterized/evaluationReasons=false/evaluation reasons - fallthrough -evaluation/parameterized/evaluationReasons=false/evaluation reasons - fallthrough experiment -evaluation/parameterized/evaluationReasons=false/evaluation reasons - off -evaluation/parameterized/evaluationReasons=false/evaluation reasons - prerequisite failed -evaluation/parameterized/evaluationReasons=false/evaluation reasons - rule match -evaluation/parameterized/evaluationReasons=false/evaluation reasons - rule match experiment -evaluation/parameterized/evaluationReasons=false/evaluation reasons - target match -evaluation/parameterized/evaluationReasons=false/wrong type errors -evaluation/parameterized/evaluationReasons=true/basic values - any -evaluation/parameterized/evaluationReasons=true/basic values - bool -evaluation/parameterized/evaluationReasons=true/basic values - double -evaluation/parameterized/evaluationReasons=true/basic values - int -evaluation/parameterized/evaluationReasons=true/basic values - string -evaluation/parameterized/evaluationReasons=true/errors - any -evaluation/parameterized/evaluationReasons=true/errors - bool -evaluation/parameterized/evaluationReasons=true/errors - double -evaluation/parameterized/evaluationReasons=true/errors - int -evaluation/parameterized/evaluationReasons=true/errors - string -evaluation/parameterized/evaluationReasons=true/evaluation reasons - error MALFORMED_FLAG -evaluation/parameterized/evaluationReasons=true/evaluation reasons - error MALFORMED_FLAG/flag-key/evaluate flag with detail -evaluation/parameterized/evaluationReasons=true/evaluation reasons - fallthrough -evaluation/parameterized/evaluationReasons=true/evaluation reasons - fallthrough experiment -evaluation/parameterized/evaluationReasons=true/evaluation reasons - off -evaluation/parameterized/evaluationReasons=true/evaluation reasons - prerequisite failed -evaluation/parameterized/evaluationReasons=true/evaluation reasons - rule match -evaluation/parameterized/evaluationReasons=true/evaluation reasons - rule match experiment -evaluation/parameterized/evaluationReasons=true/evaluation reasons - target match -evaluation/parameterized/evaluationReasons=true/wrong type errors -events/context properties/custom attribute with value "" -events/context properties/custom attribute with value "abc" -events/context properties/custom attribute with value "has \"escaped\" characters" -events/context properties/custom attribute with value -1000 -events/context properties/custom attribute with value -1000.5 -events/context properties/custom attribute with value 0 -events/context properties/custom attribute with value 1000 -events/context properties/custom attribute with value 1000.5 -events/context properties/custom attribute with value ["a","b"] -events/context properties/custom attribute with value [] -events/context properties/custom attribute with value false -events/context properties/custom attribute with value true -events/context properties/custom attribute with value {"a":1} -events/context properties/custom attribute with value {} -events/context properties/multi-kind minimal -events/context properties/single-kind minimal -events/context properties/single-kind with attributes, nothing private -events/context properties/single-kind, allAttributesPrivate -events/context properties/single-kind, private attribute nested property -events/context properties/single-kind, specific private attributes -events/custom events/basic properties/multi-kind -events/custom events/basic properties/single kind default -events/custom events/basic properties/single kind non-default -events/custom events/data and metricValue parameters -events/custom events/data and metricValue parameters/data="" -events/custom events/data and metricValue parameters/data="", metricValue=-1.500000 -events/custom events/data and metricValue parameters/data="", metricValue=0.000000 -events/custom events/data and metricValue parameters/data="", metricValue=1.500000 -events/custom events/data and metricValue parameters/data="abc" -events/custom events/data and metricValue parameters/data="abc", metricValue=-1.500000 -events/custom events/data and metricValue parameters/data="abc", metricValue=0.000000 -events/custom events/data and metricValue parameters/data="abc", metricValue=1.500000 -events/custom events/data and metricValue parameters/data=0 -events/custom events/data and metricValue parameters/data=0, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=0, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=0, metricValue=1.500000 -events/custom events/data and metricValue parameters/data=1000 -events/custom events/data and metricValue parameters/data=1000, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=1000, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=1000, metricValue=1.500000 -events/custom events/data and metricValue parameters/data=1000.5 -events/custom events/data and metricValue parameters/data=1000.5, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=1000.5, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=1000.5, metricValue=1.500000 -events/custom events/data and metricValue parameters/data=[1,2] -events/custom events/data and metricValue parameters/data=[1,2], metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=[1,2], metricValue=0.000000 -events/custom events/data and metricValue parameters/data=[1,2], metricValue=1.500000 -events/custom events/data and metricValue parameters/data=false -events/custom events/data and metricValue parameters/data=false, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=false, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=false, metricValue=1.500000 -events/custom events/data and metricValue parameters/data=null -events/custom events/data and metricValue parameters/data=null, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=null, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=null, metricValue=1.500000 -events/custom events/data and metricValue parameters/data=null, omitNullData -events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=null, omitNullData, metricValue=1.500000 -events/custom events/data and metricValue parameters/data=true -events/custom events/data and metricValue parameters/data=true, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data=true, metricValue=0.000000 -events/custom events/data and metricValue parameters/data=true, metricValue=1.500000 -events/custom events/data and metricValue parameters/data={"property":true} -events/custom events/data and metricValue parameters/data={"property":true}, metricValue=-1.500000 -events/custom events/data and metricValue parameters/data={"property":true}, metricValue=0.000000 -events/custom events/data and metricValue parameters/data={"property":true}, metricValue=1.500000 -events/disabling/custom event -events/disabling/evaluation -events/disabling/identify event -events/event capacity/buffer is reset after flush -events/event capacity/capacity is enforced -events/event capacity/summary event is still included even if buffer was full -events/experimentation/FALLTHROUGH -events/experimentation/RULE_MATCH -events/identify events/basic properties/multi-kind -events/identify events/basic properties/single kind default -events/identify events/basic properties/single kind non-default -events/requests/URL path is computed correctly/base URI has a trailing slash -events/requests/URL path is computed correctly/base URI has no trailing slash -events/requests/method and headers -events/requests/new payload ID for each post -events/user properties/inlineUsers=false, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true/user-private=none/identify event -events/user properties/inlineUsers=false, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=false, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=false/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=false/user-private=none/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/identify event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/custom event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/feature event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/custom event -events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true/user-private=none/custom event -events/user properties/inlineUsers=true/user-private=none/feature event -events/user properties/inlineUsers=true/user-private=none/identify event -polling/requests/URL path is computed correctly/base URI has a trailing slash/GET -polling/requests/URL path is computed correctly/base URI has a trailing slash/REPORT -polling/requests/URL path is computed correctly/base URI has no trailing slash/GET -polling/requests/URL path is computed correctly/base URI has no trailing slash/REPORT -polling/requests/context properties/multi-kind/GET -polling/requests/context properties/multi-kind/REPORT -polling/requests/context properties/single kind minimal/GET -polling/requests/context properties/single kind minimal/REPORT -polling/requests/context properties/single kind with all attributes/GET -polling/requests/context properties/single kind with all attributes/REPORT -polling/requests/method and headers/GET -polling/requests/method and headers/REPORT -polling/requests/query parameters/evaluationReasons set to [none]/GET -polling/requests/query parameters/evaluationReasons set to [none]/REPORT -polling/requests/query parameters/evaluationReasons set to false/GET -polling/requests/query parameters/evaluationReasons set to false/REPORT -polling/requests/query parameters/evaluationReasons set to true/GET -polling/requests/query parameters/evaluationReasons set to true/REPORT -polling/requests/user properties/GET -polling/requests/user properties/REPORT -streaming/requests/URL path is computed correctly/base URI has a trailing slash/GET -streaming/requests/URL path is computed correctly/base URI has a trailing slash/REPORT -streaming/requests/URL path is computed correctly/base URI has no trailing slash/GET -streaming/requests/URL path is computed correctly/base URI has no trailing slash/REPORT -streaming/requests/context properties/multi-kind/GET -streaming/requests/context properties/multi-kind/REPORT -streaming/requests/context properties/single kind minimal/GET -streaming/requests/context properties/single kind minimal/REPORT streaming/requests/context properties/single kind with all attributes/GET streaming/requests/context properties/single kind with all attributes/REPORT -streaming/requests/method and headers/GET -streaming/requests/method and headers/REPORT -streaming/requests/query parameters/evaluationReasons set to [none]/GET -streaming/requests/query parameters/evaluationReasons set to [none]/REPORT -streaming/requests/query parameters/evaluationReasons set to false/GET -streaming/requests/query parameters/evaluationReasons set to false/REPORT -streaming/requests/query parameters/evaluationReasons set to true/GET -streaming/requests/query parameters/evaluationReasons set to true/REPORT -streaming/requests/user properties/GET -streaming/requests/user properties/REPORT +streaming/requests/context properties/multi-kind/GET +streaming/requests/context properties/multi-kind/REPORT streaming/updates/flag delete for previously nonexistent flag is applied -streaming/updates/flag delete with higher version is applied -streaming/updates/flag delete with lower version is not applied -streaming/updates/flag delete with same version is not applied -streaming/updates/flag patch for previously nonexistent flag is applied -streaming/updates/flag patch with higher version is applied -streaming/updates/flag patch with lower version is not applied -streaming/updates/flag patch with same version is not applied -tags/event posts/{"applicationId":"","applicationVersion":""} -tags/event posts/{"applicationId":"","applicationVersion":null} -tags/event posts/{"applicationId":null,"applicationVersion":""} -tags/event posts/{"applicationId":null,"applicationVersion":null} +polling/requests/context properties/single kind with all attributes/GET +polling/requests/context properties/single kind with all attributes/REPORT +polling/requests/context properties/multi-kind/GET +polling/requests/context properties/multi-kind/REPORT diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index dbda7735..36033147 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -212,6 +212,7 @@ A31088272837DCA900184942 /* LDContextSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088242837DCA900184942 /* LDContextSpec.swift */; }; A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088252837DCA900184942 /* ReferenceSpec.swift */; }; A31088292837DCA900184942 /* KindSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088262837DCA900184942 /* KindSpec.swift */; }; + A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33A5F7928466D04000C29C7 /* LDContextStub.swift */; }; B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; }; B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.swift */; }; B467791324D8AEEC00897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791224D8AEEC00897F00 /* LDSwiftEventSourceStatic */; }; @@ -414,6 +415,7 @@ A31088242837DCA900184942 /* LDContextSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDContextSpec.swift; sourceTree = ""; }; A31088252837DCA900184942 /* ReferenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceSpec.swift; sourceTree = ""; }; A31088262837DCA900184942 /* KindSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KindSpec.swift; sourceTree = ""; }; + A33A5F7928466D04000C29C7 /* LDContextStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextStub.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 = ""; }; B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = ""; }; @@ -658,6 +660,7 @@ 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */, 832307A91F7ECA630029815A /* LDConfigStub.swift */, 83EF67941F994BAD00403126 /* LDUserStub.swift */, + A33A5F7928466D04000C29C7 /* LDContextStub.swift */, 838F96791FBA551A009CFC45 /* ClientServiceMockFactory.swift */, 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */, 831425AE206ABB5300F2EF36 /* EnvironmentReportingMock.swift */, @@ -1152,14 +1155,12 @@ A310881E2837DC0400184942 /* Kind.swift in Sources */, A310881A2837DC0400184942 /* Reference.swift in Sources */, 831188522113ADF700D77CB5 /* KeyedValueCache.swift in Sources */, - A3422BB9283591D30047396B /* ReferenceSpec.swift in Sources */, 831188582113AE0F00D77CB5 /* EventReporter.swift in Sources */, 8311885D2113AE2500D77CB5 /* DarklyService.swift in Sources */, 831188692113AE5900D77CB5 /* ObjcLDConfig.swift in Sources */, A31088222837DC0400184942 /* LDContext.swift in Sources */, 8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */, C43C37E8238DF22D003C1624 /* LDEvaluationDetail.swift in Sources */, - A3422BB5283591D30047396B /* LDContextSpec.swift in Sources */, 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */, C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831188592113AE1200D77CB5 /* FlagStore.swift in Sources */, @@ -1178,7 +1179,6 @@ 29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */, 8347BB0F21F147E100E56BCD /* LDTimer.swift in Sources */, B4C9D43B2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, - A3422BBD283591D30047396B /* KindSpec.swift in Sources */, C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */, B468E71324B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, 8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */, @@ -1212,11 +1212,9 @@ 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */, C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */, - A3422BBC283591D30047396B /* KindSpec.swift in Sources */, 8354AC722243166900CDE602 /* FeatureFlagCache.swift in Sources */, A310881D2837DC0400184942 /* Kind.swift in Sources */, C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */, - A3422BB8283591D30047396B /* ReferenceSpec.swift in Sources */, 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */, 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */, 831EF34D20655E730001C643 /* FlagsUnchangedObserver.swift in Sources */, @@ -1251,7 +1249,6 @@ 831EF36520655E730001C643 /* Thread.swift in Sources */, 83B1D7C92073F354006D1B1C /* CwlSysctl.swift in Sources */, 831EF36620655E730001C643 /* ObjcLDClient.swift in Sources */, - A3422BB4283591D30047396B /* LDContextSpec.swift in Sources */, 831EF36720655E730001C643 /* ObjcLDConfig.swift in Sources */, 831EF36820655E730001C643 /* ObjcLDUser.swift in Sources */, B4C9D43A2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, @@ -1275,14 +1272,12 @@ A310881B2837DC0400184942 /* Kind.swift in Sources */, A31088172837DC0400184942 /* Reference.swift in Sources */, 83A2D6241F51CD7A00EA3BD4 /* LDUser.swift in Sources */, - A3422BB6283591D30047396B /* ReferenceSpec.swift in Sources */, 8354EFE21F26380700C05156 /* Event.swift in Sources */, C408884923033B7500420721 /* ConnectionInformation.swift in Sources */, 831D8B721F71D3E700ED65E8 /* DarklyService.swift in Sources */, A310881F2837DC0400184942 /* LDContext.swift in Sources */, 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */, 8358F25E1F474E5900ECE1AF /* LDChangedFlag.swift in Sources */, - A3422BB2283591D30047396B /* LDContextSpec.swift in Sources */, 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */, C43C37E1236BA050003C1624 /* LDEvaluationDetail.swift in Sources */, 831AAE2C20A9E4F600B46DBA /* Throttler.swift in Sources */, @@ -1301,7 +1296,6 @@ 29A4C47527DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B1206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, C408884723033B3600420721 /* ConnectionInformationStore.swift in Sources */, - A3422BBA283591D30047396B /* KindSpec.swift in Sources */, 83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */, B4C9D4382489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, 8347BB0C21F147E100E56BCD /* LDTimer.swift in Sources */, @@ -1349,6 +1343,7 @@ 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */, 83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */, 832307AA1F7ECA630029815A /* LDConfigStub.swift in Sources */, + A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */, 8354AC77224316F800CDE602 /* FeatureFlagCacheSpec.swift in Sources */, 83EBCBB120D9C7B5003A7142 /* FlagCounterSpec.swift in Sources */, 83B8C2451FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift in Sources */, @@ -1382,14 +1377,12 @@ A310881C2837DC0400184942 /* Kind.swift in Sources */, A31088182837DC0400184942 /* Reference.swift in Sources */, 83D9EC7E2062DEAB004D7FA6 /* FlagChangeObserver.swift in Sources */, - A3422BB7283591D30047396B /* ReferenceSpec.swift in Sources */, 83D9EC7F2062DEAB004D7FA6 /* FlagsUnchangedObserver.swift in Sources */, 83D9EC802062DEAB004D7FA6 /* Event.swift in Sources */, 83D9EC822062DEAB004D7FA6 /* ClientServiceFactory.swift in Sources */, A31088202837DC0400184942 /* LDContext.swift in Sources */, 83D9EC832062DEAB004D7FA6 /* KeyedValueCache.swift in Sources */, 831AAE2D20A9E4F600B46DBA /* Throttler.swift in Sources */, - A3422BB3283591D30047396B /* LDContextSpec.swift in Sources */, C43C37E6238DF22B003C1624 /* LDEvaluationDetail.swift in Sources */, 83D9EC872062DEAB004D7FA6 /* FlagSynchronizer.swift in Sources */, C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, @@ -1408,7 +1401,6 @@ 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B2206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */, - A3422BBB283591D30047396B /* KindSpec.swift in Sources */, 8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */, 8354AC712243166900CDE602 /* FeatureFlagCache.swift in Sources */, C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */, diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index b8ad98cc..e29d96c8 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -242,10 +242,10 @@ final class EventReportingMock: EventReporting { var recordFlagEvaluationEventsCallCount = 0 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) { + var recordFlagEvaluationEventsReceivedArguments: (flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, context: LDContext, includeReason: Bool)? + func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, context: LDContext, includeReason: Bool) { recordFlagEvaluationEventsCallCount += 1 - recordFlagEvaluationEventsReceivedArguments = (flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason) + recordFlagEvaluationEventsReceivedArguments = (flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, context: context, includeReason: includeReason) try! recordFlagEvaluationEventsCallback?() } @@ -273,21 +273,21 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { var retrieveFeatureFlagsCallCount = 0 var retrieveFeatureFlagsCallback: (() throws -> Void)? - var retrieveFeatureFlagsReceivedUserKey: String? + var retrieveFeatureFlagsReceivedContextKey: String? var retrieveFeatureFlagsReturnValue: [LDFlagKey: FeatureFlag]? - func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? { + func retrieveFeatureFlags(contextKey: String) -> [LDFlagKey: FeatureFlag]? { retrieveFeatureFlagsCallCount += 1 - retrieveFeatureFlagsReceivedUserKey = userKey + retrieveFeatureFlagsReceivedContextKey = contextKey try! retrieveFeatureFlagsCallback?() return retrieveFeatureFlagsReturnValue } var storeFeatureFlagsCallCount = 0 var storeFeatureFlagsCallback: (() throws -> Void)? - var storeFeatureFlagsReceivedArguments: (featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date)? - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) { + var storeFeatureFlagsReceivedArguments: (featureFlags: [LDFlagKey: FeatureFlag], contextKey: String, lastUpdated: Date)? + func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], contextKey: String, lastUpdated: Date) { storeFeatureFlagsCallCount += 1 - storeFeatureFlagsReceivedArguments = (featureFlags: featureFlags, userKey: userKey, lastUpdated: lastUpdated) + storeFeatureFlagsReceivedArguments = (featureFlags: featureFlags, contextKey: contextKey, lastUpdated: lastUpdated) try! storeFeatureFlagsCallback?() } } diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 2c010201..99c50059 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -5,13 +5,13 @@ enum LDClientRunMode { } /** - 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. + 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 contexts (LDContext) 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 ### Startup - 1. To customize, configure a `LDConfig` and `LDUser`. The `config` is required, the `user` is optional. Both give you additional control over the feature flags delivered to the LDClient. See `LDConfig` & `LDUser` for more details. + 1. To customize, configure a `LDConfig` and `LDContext`. The `config` is required, the `context` is optional. Both give you additional control over the feature flags delivered to the LDClient. See `LDConfig` & `LDContext` for more details. - The mobileKey set into the `LDConfig` comes from your LaunchDarkly Account settings. If you have multiple projects be sure to choose the correct Mobile key. - 2. Call `LDClient.start(config: user: completion:)` - - If you do not pass in a LDUser, LDClient will create a default for you. + 2. Call `LDClient.start(config: context: completion:)` + - If you do not pass in a LDContext, LDClient will create a default for you. - The optional completion closure allows the LDClient to notify your app when it received flag values. 3. Because LDClient instances are stored statically, you do not have to keep a reference to it in your code. Get the primary instances with `LDClient.get()` @@ -38,9 +38,9 @@ enum LDClientRunMode { public class LDClient { // MARK: - State Controls and Indicators - + private static var instances: [String: LDClient]? - + /** Reports the online/offline state of the LDClient. @@ -91,7 +91,7 @@ public class LDClient { When offline, the SDK does not attempt to communicate with LaunchDarkly servers. Client apps can request feature flag values and set/change feature flag observers while offline. The SDK will collect events while offline. - The SDK protects itself from multiple rapid calls to setOnline(true) by enforcing an increasing delay (called *throttling*) each time setOnline(true) is called within a short time. The first time, the call proceeds normally. For each subsequent call the delay is enforced, and if waiting, increased to a maximum delay. When the delay has elapsed, the `setOnline(true)` will proceed, assuming that the client app has not called `setOnline(false)` during the delay. Therefore a call to setOnline(true) may not immediately result in the LDClient going online. Client app developers should consider this situation abnormal, and take steps to prevent the client app from making multiple rapid setOnline(true) calls. Calls to setOnline(false) are not throttled. Note that calls to `start(config: user: completion:)`, and setting the `config` or `user` can also call `setOnline(true)` under certain conditions. After the delay, the SDK resets and the client app can make a susequent call to setOnline(true) without being throttled. + The SDK protects itself from multiple rapid calls to setOnline(true) by enforcing an increasing delay (called *throttling*) each time setOnline(true) is called within a short time. The first time, the call proceeds normally. For each subsequent call the delay is enforced, and if waiting, increased to a maximum delay. When the delay has elapsed, the `setOnline(true)` will proceed, assuming that the client app has not called `setOnline(false)` during the delay. Therefore a call to setOnline(true) may not immediately result in the LDClient going online. Client app developers should consider this situation abnormal, and take steps to prevent the client app from making multiple rapid setOnline(true) calls. Calls to setOnline(false) are not throttled. Note that calls to `start(config: context: completion:)`, and setting the `config` or `context` can also call `setOnline(true)` under certain conditions. After the delay, the SDK resets and the client app can make a susequent call to setOnline(true) without being throttled. Client apps can set a completion closure called when the setOnline call completes. For unthrottled `setOnline(true)` and all `setOnline(false)` calls, the SDK will call the closure immediately on completion of this method. For throttled `setOnline(true)` calls, the SDK will call the closure after the throttling delay at the completion of the setOnline method. @@ -130,7 +130,7 @@ public class LDClient { } private let internalSetOnlineQueue: DispatchQueue = DispatchQueue(label: "InternalSetOnlineQueue") - + private func go(online goOnline: Bool, reasonOnlineUnavailable: String, completion:(() -> Void)?) { let owner = "SetOnlineOwner" as AnyObject var completed = false @@ -261,41 +261,43 @@ public class LDClient { let config: LDConfig let service: DarklyServiceProvider + private(set) var context: LDContext + // TODO(mmk) Remove this when we are done private(set) var user: LDUser - + /** - The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. - - 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. - - 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. + The LDContext set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the context. See `LDContext` for details about what information can be retained. + + Normally, the client app should create and set the LDContext and pass that into `start(config: context: completion:)`. + + The client app can change the active `context` by calling identify with a new or updated LDContext. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. + + When a new context is set, the LDClient goes offline and sets the new context. If the client was online when the new context 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 context are ready. + + - parameter context: The LDContext set with the desired context. - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. (Optional) */ - public func identify(user: LDUser, completion: (() -> Void)? = nil) { + public func identify(context: LDContext, completion: (() -> Void)? = nil) { let dispatch = DispatchGroup() LDClient.instances?.forEach { _, instance in dispatch.enter() - instance.internalIdentify(newUser: user, completion: dispatch.leave) + instance.internalIdentify(newContext: context, completion: dispatch.leave) } if let completion = completion { dispatch.notify(queue: DispatchQueue.global(), execute: completion) } } - func internalIdentify(newUser: LDUser, completion: (() -> Void)? = nil) { + func internalIdentify(newContext: LDContext, completion: (() -> Void)? = nil) { internalIdentifyQueue.sync { - self.user = newUser - Log.debug(self.typeName(and: #function) + "new user set with key: " + self.user.key ) + self.context = newContext + Log.debug(self.typeName(and: #function) + "new context set with key: " + self.context.fullyQualifiedKey() ) let wasOnline = self.isOnline self.internalSetOnline(false) - let cachedUserFlags = self.flagCache.retrieveFeatureFlags(userKey: self.user.key) ?? [:] - flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedUserFlags)) - self.service.user = self.user + let cachedContextFlags = self.flagCache.retrieveFeatureFlags(contextKey: self.context.fullyQualifiedKey()) ?? [:] + flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedContextFlags)) + self.service.context = self.context self.service.clearFlagResponseCache() flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), pollingInterval: config.flagPollingInterval(runMode: runMode), @@ -304,7 +306,7 @@ public class LDClient { onSyncComplete: self.onFlagSyncComplete) if self.hasStarted { - self.eventReporter.record(IdentifyEvent(user: self.user)) + self.eventReporter.record(IdentifyEvent(context: self.context)) } self.internalSetOnline(wasOnline, completion: completion) @@ -347,7 +349,7 @@ public class LDClient { Log.debug(typeName(and: #function) + "flagKey: \(key), owner: \(String(describing: owner))") flagChangeNotifier.addFlagChangeObserver(FlagChangeObserver(key: key, owner: owner, flagChangeHandler: handler)) } - + /** 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 [LDFlagKey: LDChangedFlag] containing the old and new flag values. See `LDChangedFlag` for details. @@ -402,7 +404,7 @@ public class LDClient { Log.debug(typeName(and: #function) + " owner: \(String(describing: owner))") flagChangeNotifier.addFlagChangeObserver(FlagChangeObserver(keys: LDFlagKey.anyKey, owner: owner, flagCollectionChangeHandler: handler)) } - + /** Sets a handler executed when a flag update leaves the flags unchanged from their previous values. @@ -429,23 +431,23 @@ public class LDClient { Log.debug(typeName(and: #function) + " owner: \(String(describing: owner))") flagChangeNotifier.addFlagsUnchangedObserver(FlagsUnchangedObserver(owner: owner, flagsUnchangedHandler: handler)) } - + /** Sets a handler executed when ConnectionInformation.currentConnectionMode changes. - + The SDK retains only weak references to owner, which allows the client app to freely destroy change owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. - + The SDK executes handlers on the main thread. - + 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. */ @@ -473,17 +475,17 @@ public class LDClient { let oldFlags = flagStore.featureFlags connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) flagStore.replaceStore(newFlags: flagCollection) - self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) + self.updateCacheAndReportChanges(context: self.context, 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) + self.updateCacheAndReportChanges(context: self.context, 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) + self.updateCacheAndReportChanges(context: self.context, oldFlags: oldFlags) case .upToDate: connectionInformation.lastKnownFlagValidity = Date() flagChangeNotifier.notifyUnchanged() @@ -500,9 +502,9 @@ public class LDClient { connectionInformation = ConnectionInformation.synchronizingErrorCheck(synchronizingError: synchronizingError, connectionInformation: connectionInformation) } - private func updateCacheAndReportChanges(user: LDUser, + private func updateCacheAndReportChanges(context: LDContext, oldFlags: [LDFlagKey: FeatureFlag]) { - flagCache.storeFeatureFlags(flagStore.featureFlags, userKey: user.key, lastUpdated: Date()) + flagCache.storeFeatureFlags(flagStore.featureFlags, contextKey: context.fullyQualifiedKey(), lastUpdated: Date()) flagChangeNotifier.notifyObservers(oldFlags: oldFlags, newFlags: flagStore.featureFlags) } @@ -531,7 +533,7 @@ public class LDClient { Log.debug(typeName(and: #function) + "aborted. LDClient not started") return } - let event = CustomEvent(key: key, user: user, data: data ?? .null, metricValue: metricValue) + let event = CustomEvent(key: key, context: context, data: data ?? .null, metricValue: metricValue) Log.debug(typeName(and: #function) + "key: \(key), data: \(String(describing: data)), metricValue: \(String(describing: metricValue))") eventReporter.record(event) } @@ -546,7 +548,7 @@ public class LDClient { public func flush() { LDClient.instances?.forEach { $1.internalFlush() } } - + private func internalFlush() { eventReporter.flush(completion: nil) } @@ -559,7 +561,7 @@ public class LDClient { Log.debug(typeName(and: #function) + "result: success") } } - + @objc private func didCloseEventSource() { Log.debug(typeName(and: #function)) self.connectionInformation = ConnectionInformation.lastSuccessfulConnectionCheck(connectionInformation: self.connectionInformation) @@ -568,21 +570,21 @@ public class LDClient { // MARK: Initializing and Accessing /** - Starts the LDClient using the passed in `config` & `user`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. - Starting the LDClient means setting the `config` & `user`, setting the client online if `config.startOnline` is true (the default setting), and starting event recording. The client app must start the LDClient before it will report feature flag values. If a client does not call `start`, no methods will work. - If the `start` call omits the `user`, the LDClient uses a default `LDUser`. + Starts the LDClient using the passed in `config` & `context`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. + Starting the LDClient means setting the `config` & `context`, setting the client online if `config.startOnline` is true (the default setting), and starting event recording. The client app must start the LDClient before it will report feature flag values. If a client does not call `start`, no methods will work. + If the `start` call omits the `context`, the LDClient uses a default `LDContext`. If the` start` call includes the optional `completion` closure, LDClient calls the `completion` closure when `setOnline(_: completion:)` embedded in the `init` method completes. This method listens for flag updates so the completion will only return once an update has occurred. The `start` call is subject to throttling delays, therefore the `completion` closure call may be delayed. - Subsequent calls to this method cause the LDClient to return. Normally there should only be one call to start. To change `user`, use `identify`. + Subsequent calls to this method cause the LDClient to return. Normally there should only be one call to start. To change `context`, use `identify`. - parameter configuration: The LDConfig that contains the desired configuration. (Required) - - parameter user: The LDUser set with the desired user. If omitted, LDClient sets a default user. (Optional) + - parameter context: The LDContext set with the desired context. If omitted, LDClient sets a default context. (Optional) - parameter completion: Closure called when the embedded `setOnline` call completes. (Optional) */ /// - Tag: start - public static func start(config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) { - start(serviceFactory: nil, config: config, user: user, completion: completion) + public static func start(config: LDConfig, context: LDContext? = nil, completion: (() -> Void)? = nil) { + start(serviceFactory: nil, config: config, context: context, completion: completion) } - static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) { + static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, context: LDContext? = nil, completion: (() -> Void)? = nil) { Log.debug("LDClient starting") if serviceFactory != nil { get()?.close() @@ -611,7 +613,7 @@ public class LDClient { for (name, mobileKey) in mobileKeys { var internalConfig = config internalConfig.mobileKey = mobileKey - let instance = LDClient(serviceFactory: serviceFactory, configuration: internalConfig, startUser: user, completion: completionCheck) + let instance = LDClient(serviceFactory: serviceFactory, configuration: internalConfig, startContext: context, completion: completionCheck) LDClient.instances?[name] = instance } completionCheck() @@ -621,23 +623,23 @@ public class LDClient { See [start](x-source-tag://start) for more information on starting the SDK. - parameter configuration: The LDConfig that contains the desired configuration. (Required) - - parameter user: The LDUser set with the desired user. If omitted, LDClient sets a default user. (Optional) + - parameter context: The LDContext set with the desired context. If omitted, LDClient sets a default context. (Optional) - parameter startWaitSeconds: A TimeInterval that determines when the completion will return if no flags have been returned from the network. - parameter completion: Closure called when the embedded `setOnline` call completes. Takes a Bool that indicates whether the completion timedout as a parameter. (Optional) */ - public static func start(config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { - start(serviceFactory: nil, config: config, user: user, startWaitSeconds: startWaitSeconds, completion: completion) + public static func start(config: LDConfig, context: LDContext? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + start(serviceFactory: nil, config: config, context: context, startWaitSeconds: startWaitSeconds, completion: completion) } - static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, context: LDContext? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { var completed = true let internalCompletedQueue: DispatchQueue = DispatchQueue(label: "TimeOutQueue") if !config.startOnline { - start(serviceFactory: serviceFactory, config: config, user: user) + start(serviceFactory: serviceFactory, config: config, context: context) completion?(completed) } else { let startTime = Date().timeIntervalSince1970 - start(serviceFactory: serviceFactory, config: config, user: user) { + start(serviceFactory: serviceFactory, config: config, context: context) { internalCompletedQueue.async { if startTime + startWaitSeconds > Date().timeIntervalSince1970 && completed { completed = false @@ -666,7 +668,7 @@ public class LDClient { } return internalInstances[environment] } - + // MARK: - Private let serviceFactory: ClientServiceCreating @@ -691,8 +693,8 @@ public class LDClient { } private var _initialized = false private var initializedQueue = DispatchQueue(label: "com.launchdarkly.LDClient.initializedQueue") - - private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startUser: LDUser?, completion: (() -> Void)? = nil) { + + private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startContext: LDContext?, completion: (() -> Void)? = nil) { self.serviceFactory = serviceFactory environmentReporter = self.serviceFactory.makeEnvironmentReporter() flagCache = self.serviceFactory.makeFeatureFlagCache(mobileKey: configuration.mobileKey, maxCachedUsers: configuration.maxCachedUsers) @@ -702,8 +704,10 @@ public class LDClient { config = configuration let anonymousUser = LDUser(environmentReporter: environmentReporter) - user = startUser ?? anonymousUser - service = self.serviceFactory.makeDarklyServiceProvider(config: config, user: user) + user = anonymousUser + let anonymousContext = LDContext(environmentReporting: environmentReporter) + context = startContext ?? anonymousContext + service = self.serviceFactory.makeDarklyServiceProvider(config: config, context: context) diagnosticReporter = self.serviceFactory.makeDiagnosticReporter(service: service) eventReporter = self.serviceFactory.makeEventReporter(service: service) connectionInformation = self.serviceFactory.makeConnectionInformation() @@ -711,14 +715,14 @@ public class LDClient { pollingInterval: config.flagPollingInterval(runMode: runMode), useReport: config.useReport, service: service) - + if let backgroundNotification = environmentReporter.backgroundNotification { NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: backgroundNotification, object: nil) } if let foregroundNotification = environmentReporter.foregroundNotification { NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: foregroundNotification, object: nil) } - + NotificationCenter.default.addObserver(self, selector: #selector(didCloseEventSource), name: Notification.Name(FlagSynchronizer.Constants.didCloseEventSourceName), object: nil) eventReporter = self.serviceFactory.makeEventReporter(service: service, onSyncComplete: onEventSyncComplete) @@ -729,11 +733,11 @@ public class LDClient { onSyncComplete: onFlagSyncComplete) Log.level = environmentReporter.isDebugBuild && config.isDebugMode ? .debug : .noLogging - if let cachedFlags = flagCache.retrieveFeatureFlags(userKey: user.key), !cachedFlags.isEmpty { + if let cachedFlags = flagCache.retrieveFeatureFlags(contextKey: context.fullyQualifiedKey()), !cachedFlags.isEmpty { flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedFlags)) } - eventReporter.record(IdentifyEvent(user: user)) + eventReporter.record(IdentifyEvent(context: context)) self.connectionInformation = ConnectionInformation.uncacheConnectionInformation(config: config, ldClient: self, clientServiceFactory: self.serviceFactory) internalSetOnline(configuration.startOnline) { diff --git a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift index 42c55aff..95e9465b 100644 --- a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -8,7 +8,7 @@ extension LDClient { - 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. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. */ public func boolVariation(forKey flagKey: LDFlagKey, defaultValue: Bool) -> Bool { variationDetailInternal(flagKey, defaultValue, needsReason: false).value @@ -31,7 +31,7 @@ extension LDClient { - 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. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. */ public func intVariation(forKey flagKey: LDFlagKey, defaultValue: Int) -> Int { variationDetailInternal(flagKey, defaultValue, needsReason: false).value @@ -54,7 +54,7 @@ extension LDClient { - 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. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. */ public func doubleVariation(forKey flagKey: LDFlagKey, defaultValue: Double) -> Double { variationDetailInternal(flagKey, defaultValue, needsReason: false).value @@ -77,7 +77,7 @@ extension LDClient { - 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. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. */ public func stringVariation(forKey flagKey: LDFlagKey, defaultValue: String) -> String { variationDetailInternal(flagKey, defaultValue, needsReason: false).value @@ -100,7 +100,7 @@ extension LDClient { - 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. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. */ public func jsonVariation(forKey flagKey: LDFlagKey, defaultValue: LDValue) -> LDValue { variationDetailInternal(flagKey, defaultValue, needsReason: false).value @@ -137,7 +137,7 @@ extension LDClient { value: result.value.toLDValue(), defaultValue: defaultValue.toLDValue(), featureFlag: featureFlag, - user: user, + context: context, includeReason: needsReason) return result } diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index 1cfcefb1..6b65a128 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -49,7 +49,7 @@ struct DynamicKey: CodingKey { 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 + This can be used to represent complex data in a context 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, diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 61ad5280..8bb9fb88 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -10,7 +10,9 @@ public enum ContextBuilderError: Error { } /// TKTK -public struct LDContext: Encodable { +public struct LDContext: Encodable, Equatable { + static let storedIdKey: String = "ldDeviceIdentifier" + internal var kind: Kind = .user fileprivate var contexts: [LDContext] = [] @@ -28,16 +30,40 @@ public struct LDContext: Encodable { self.canonicalizedKey = canonicalizedKey } + init(environmentReporting: EnvironmentReporting) { + self.init(canonicalizedKey: LDContext.defaultKey(environmentReporting: environmentReporting)) + } + public struct Meta: Codable { public var secondary: String? public var privateAttributes: [Reference]? + public var redactedAttributes: [String]? enum CodingKeys: CodingKey { - case secondary, privateAttributes + case secondary, privateAttributes, redactedAttributes } public var isEmpty: Bool { - secondary == nil && (privateAttributes?.isEmpty ?? true) + secondary == nil + && (privateAttributes?.isEmpty ?? true) + && (redactedAttributes?.isEmpty ?? true) + } + + init(secondary: String?, privateAttributes: [Reference]?, redactedAttributes: [String]?) { + self.secondary = secondary + self.privateAttributes = privateAttributes + self.redactedAttributes = redactedAttributes + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let secondary = try container.decodeIfPresent(String.self, forKey: .secondary) + let privateAttributes = try container.decodeIfPresent([Reference].self, forKey: .privateAttributes) + + self.secondary = secondary + self.privateAttributes = privateAttributes + self.redactedAttributes = [] } public func encode(to encoder: Encoder) throws { @@ -47,47 +73,226 @@ public struct LDContext: Encodable { if let privateAttributes = privateAttributes, !privateAttributes.isEmpty { try container.encodeIfPresent(privateAttributes, forKey: .privateAttributes) } + + if let redactedAttributes = redactedAttributes, !redactedAttributes.isEmpty { + try container.encodeIfPresent(redactedAttributes, forKey: .redactedAttributes) + } + } + } + + class PrivateAttributeLookupNode { + var reference: Reference? + var children = SharedDictionary() + + init() { + self.reference = nil + } + + init(reference: Reference) { + self.reference = reference } } - static private func encodeSingleContext(container: inout KeyedEncodingContainer, context: LDContext, discardKind: Bool) throws { + static private func encodeSingleContext(container: inout KeyedEncodingContainer, context: LDContext, discardKind: Bool, allAttributesPrivate: Bool, globalPrivateAttributes: SharedDictionary) throws { if !discardKind { try container.encodeIfPresent(context.kind.description, forKey: DynamicCodingKeys(string: "kind")) } try container.encodeIfPresent(context.key, forKey: DynamicCodingKeys(string: "key")) - try container.encodeIfPresent(context.name, forKey: DynamicCodingKeys(string: "name")) - let meta = Meta(secondary: context.secondary, privateAttributes: context.privateAttributes) + let optionalAttributeNames = context.getOptionalAttributeNames() + var redactedAttributes: [String] = [] + redactedAttributes.reserveCapacity(20) + + for key in optionalAttributeNames { + let reference = Reference(key) + if let value = context.getValue(reference) { + if allAttributesPrivate { + redactedAttributes.append(reference.raw()) + continue + } + + var path: [String] = [] + path.reserveCapacity(10) + try LDContext.writeFilterAttribute(context: context, container: &container, parentPath: path, key: key, value: value, redactedAttributes: &redactedAttributes, globalPrivateAttributes: globalPrivateAttributes) + } + } + + let meta = Meta(secondary: context.secondary, privateAttributes: context.privateAttributes, redactedAttributes: redactedAttributes) if !meta.isEmpty { try container.encodeIfPresent(meta, forKey: DynamicCodingKeys(string: "_meta")) } - if !context.attributes.isEmpty { - try context.attributes.forEach { - try container.encodeIfPresent($0.value, forKey: DynamicCodingKeys(string: $0.key)) + if context.transient { + try container.encodeIfPresent(context.transient, forKey: DynamicCodingKeys(string: "transient")) + } + } + + static private func writeFilterAttribute(context: LDContext, container: inout KeyedEncodingContainer, parentPath: [String], key: String, value: LDValue, redactedAttributes: inout [String], globalPrivateAttributes: SharedDictionary) throws { + var path = parentPath + path.append(key.description) + + let (isReacted, nestedPropertiesAreRedacted) = LDContext.maybeRedact(context: context, parentPath: path, value: value, redactedAttributes: &redactedAttributes, globalPrivateAttributes: globalPrivateAttributes) + + switch value { + case .object(_) where isReacted: + break + case .object(let objectMap): + if !nestedPropertiesAreRedacted { + try container.encode(value, forKey: DynamicCodingKeys(string: key)) + return } + + // TODO(mmk): This might be a problem. We might write a sub container even if all the attributes are completely filtered out. + var subContainer = container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: DynamicCodingKeys(string: key)) + for (key, value) in objectMap { + try writeFilterAttribute(context: context, container: &subContainer, parentPath: path, key: key, value: value, redactedAttributes: &redactedAttributes, globalPrivateAttributes: globalPrivateAttributes) + } + case _ where !isReacted: + try container.encode(value, forKey: DynamicCodingKeys(string: key)) + default: + break + } + } + + static private func maybeRedact(context: LDContext, parentPath: [String], value: LDValue, redactedAttributes: inout [String], globalPrivateAttributes: SharedDictionary) -> (Bool, Bool) { + var (reactedAttrReference, nestedPropertiesAreRedacted) = LDContext.checkGlobalPrivateAttributeReferences(context: context, parentPath: parentPath, globalPrivateAttributes: globalPrivateAttributes) + + if let reactedAttrReference = reactedAttrReference { + redactedAttributes.append(reactedAttrReference.raw()) + return (true, false) } - if context.transient { - try container.encodeIfPresent(context.transient, forKey: DynamicCodingKeys(string: "transient")) + var shouldCheckNestedProperties: Bool = false + if case .object(_) = value { + shouldCheckNestedProperties = true } + + for privateAttribute in context.privateAttributes { + let depth = privateAttribute.depth() + + if depth < parentPath.count { + continue + } + + if !shouldCheckNestedProperties && depth < parentPath.count { + continue + } + + var hasMatch = true + for (index, parentPart) in parentPath.enumerated() { + if let (name, _) = privateAttribute.component(index) { + if name != parentPart { + hasMatch = false + break + } + + continue + } else { + break + } + } + + if hasMatch { + if depth == parentPath.count { + redactedAttributes.append(privateAttribute.raw()) + return (true, false) + } + + nestedPropertiesAreRedacted = true + } + } + + return (false, nestedPropertiesAreRedacted) + } + + internal struct UserInfoKeys { + // TODO(mmk): Everywhere we use DynamicCodingKey, we should CodingUserInfoKey + 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 { var container = encoder.container(keyedBy: DynamicCodingKeys.self) + let allAttributesPrivate = encoder.userInfo[UserInfoKeys.allAttributesPrivate] as? Bool ?? false + let globalPrivateAttributes = encoder.userInfo[UserInfoKeys.globalPrivateAttributes] as? [Reference] ?? [] + let globalDictionary = LDContext.makePrivateAttributeLookupData(references: globalPrivateAttributes) + if isMulti() { try container.encodeIfPresent(kind.description, forKey: DynamicCodingKeys(string: "kind")) for context in contexts { var contextContainer = container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: DynamicCodingKeys(string: context.kind.description)) - try LDContext.encodeSingleContext(container: &contextContainer, context: context, discardKind: true) + try LDContext.encodeSingleContext(container: &contextContainer, context: context, discardKind: true, allAttributesPrivate: allAttributesPrivate, globalPrivateAttributes: globalDictionary) } } else { - try LDContext.encodeSingleContext(container: &container, context: self, discardKind: false) + try LDContext.encodeSingleContext(container: &container, context: self, discardKind: false, allAttributesPrivate: allAttributesPrivate, globalPrivateAttributes: globalDictionary) + } + } + + class SharedDictionary { + private var dict: [K: V] = Dictionary() + var isEmpty: Bool { + dict.isEmpty + } + + func contains(_ key: K) -> Bool { + dict.keys.contains(key) + } + + subscript(key: K) -> V? { + get { return dict[key] } + set { dict[key] = newValue } + } + } + + static private func makePrivateAttributeLookupData(references: [Reference]) -> SharedDictionary { + let returnValue = SharedDictionary() + + for reference in references { + let parentMap = returnValue + + for index in 0...reference.depth() { + if let (name, _) = reference.component(index) { + if !parentMap.contains(name) { + let nextNode = PrivateAttributeLookupNode() + + if index == reference.depth() - 1 { + nextNode.reference = reference + } + + parentMap[name] = nextNode + } + } + } + } + + return returnValue + } + + static private func checkGlobalPrivateAttributeReferences(context: LDContext, parentPath: [String], globalPrivateAttributes: SharedDictionary) -> (Reference?, Bool) { + var lookup = globalPrivateAttributes + if lookup.isEmpty { + return (nil, false) } + + for (index, path) in parentPath.enumerated() { + if let nextNode = lookup[path] { + if index == parentPath.count - 1 { + let name = (nextNode.reference, nextNode.reference == nil) + return name + } else if !nextNode.children.isEmpty { + lookup = nextNode.children + } + } else { + break + } + } + + return (nil, false) } /// TKTK @@ -100,6 +305,15 @@ public struct LDContext: Encodable { return self.kind.isMulti() } + public func contextKeys() -> [String: String] { + guard isMulti() else { + return [kind.description: key ?? ""] + } + + let keys = Dictionary(contexts.map { ($0.kind.description, $0.key ?? "") }) { first, _ in first } + return keys + } + /// TKTK public func getValue(_ reference: Reference) -> LDValue? { if !reference.isValid() { @@ -157,6 +371,20 @@ public struct LDContext: Encodable { return attribute } + func getOptionalAttributeNames() -> [String] { + if isMulti() { + return [] + } + + var attrs = attributes.keys.map { $0.description } + + if name != nil { + attrs.append("name") + } + + return attrs + } + func getTopLevelAddressableAttributeSingleKind(_ name: String) -> LDValue? { switch name { case "kind": @@ -171,6 +399,25 @@ public struct LDContext: Encodable { return self.attributes[name] } } + + /// Default key is the LDContext.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(environmentReporting: 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 + if let vendorUUID = environmentReporting.vendorUUID { + return vendorUUID + } + + if let storedId = UserDefaults.standard.string(forKey: storedIdKey) { + return storedId + } + + let key = UUID().uuidString + UserDefaults.standard.set(key, forKey: storedIdKey) + + return key + } } extension LDContext: Decodable { @@ -235,7 +482,7 @@ extension LDContext: Decodable { let container = try decoder.container(keyedBy: DynamicCodingKeys.self) var multiContextBuilder = LDMultiContextBuilder() - for key in container.allKeys { + for key in container.allKeys { if key.stringValue == "kind" { continue } diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift index b2f634a8..4d97f93f 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift @@ -34,7 +34,7 @@ extension ReferenceError: CustomStringConvertible { } } -public struct Reference: Codable, Equatable { +public struct Reference: Codable, Equatable, Hashable { private var error: ReferenceError? private var rawPath: String private var components: [Component] = [] @@ -137,6 +137,10 @@ public struct Reference: Codable, Equatable { return components.count } + internal func raw() -> String { + return rawPath + } + public func component(_ index: Int) -> (String, Int?)? { if index >= self.depth() { return nil @@ -147,7 +151,7 @@ public struct Reference: Codable, Equatable { } } -private struct Component: Codable, Equatable { +private struct Component: Codable, Equatable, Hashable { fileprivate let name: String fileprivate let value: Int? diff --git a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift index 88d670ba..13454fba 100644 --- a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift +++ b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift @@ -105,6 +105,7 @@ struct DiagnosticConfig: Codable { let useReport: Bool let backgroundPollingDisabled: Bool let evaluationReasonsRequested: Bool + // TODO(mmk) Update this config pattern let maxCachedUsers: Int let mobileKeyCount: Int let diagnosticRecordingIntervalMillis: Int @@ -118,9 +119,10 @@ struct DiagnosticConfig: Codable { connectTimeoutMillis = Int(exactly: round(config.connectionTimeout * 1_000)) ?? .max eventsFlushIntervalMillis = Int(exactly: round(config.eventFlushInterval * 1_000)) ?? .max streamingDisabled = config.streamingMode == .polling - allAttributesPrivate = config.allUserAttributesPrivate + allAttributesPrivate = config.allContextAttributesPrivate pollingIntervalMillis = Int(exactly: round(config.flagPollingInterval * 1_000)) ?? .max backgroundPollingIntervalMillis = Int(exactly: round(config.backgroundFlagPollingInterval * 1_000)) ?? .max + // TODO(mmk) Update this config pattern inlineUsersInEvents = config.inlineUserInEvents useReport = config.useReport backgroundPollingDisabled = !config.enableBackgroundUpdates diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index 9b82eea3..9f9595d8 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -6,7 +6,7 @@ private protocol SubEvent { class Event: Encodable { enum CodingKeys: String, CodingKey { - case key, previousKey, kind, creationDate, user, userKey, value, defaultValue = "default", variation, version, + case key, previousKey, kind, creationDate, context, contextKeys, value, defaultValue = "default", variation, version, data, startDate, endDate, features, reason, metricValue, contextKind, previousContextKind } @@ -42,14 +42,14 @@ class Event: Encodable { class CustomEvent: Event, SubEvent { let key: String - let user: LDUser + let context: LDContext let data: LDValue let metricValue: Double? let creationDate: Date - init(key: String, user: LDUser, data: LDValue = nil, metricValue: Double? = nil, creationDate: Date = Date()) { + init(key: String, context: LDContext, data: LDValue = nil, metricValue: Double? = nil, creationDate: Date = Date()) { self.key = key - self.user = user + self.context = context self.data = data self.metricValue = metricValue self.creationDate = creationDate @@ -60,13 +60,11 @@ class CustomEvent: Event, SubEvent { var container = container try container.encode(key, forKey: .key) if encoder.userInfo[Event.UserInfoKeys.inlineUserInEvents] as? Bool ?? false { - try container.encode(user, forKey: .user) + try container.encode(context, forKey: .context) } else { - try container.encode(user.key, forKey: .userKey) - } - if user.isAnonymous == true { - try container.encode("anonymousUser", forKey: .contextKind) + try container.encode(context.contextKeys(), forKey: .contextKeys) } + if data != .null { try container.encode(data, forKey: .data) } @@ -77,19 +75,19 @@ class CustomEvent: Event, SubEvent { class FeatureEvent: Event, SubEvent { let key: String - let user: LDUser + let context: LDContext 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()) { + init(key: String, context: LDContext, 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.context = context self.includeReason = includeReason self.creationDate = creationDate super.init(kind: isDebug ? .debug : .feature) @@ -99,12 +97,9 @@ class FeatureEvent: Event, SubEvent { 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) + try container.encode(context, forKey: .context) } else { - try container.encode(user.key, forKey: .userKey) - } - if kind == .feature && user.isAnonymous == true { - try container.encode("anonymousUser", forKey: .contextKind) + try container.encode(context.contextKeys(), forKey: .contextKeys) } try container.encodeIfPresent(featureFlag?.variation, forKey: .variation) try container.encodeIfPresent(featureFlag?.versionForEvents, forKey: .version) @@ -118,19 +113,19 @@ class FeatureEvent: Event, SubEvent { } class IdentifyEvent: Event, SubEvent { - let user: LDUser + let context: LDContext let creationDate: Date - init(user: LDUser, creationDate: Date = Date()) { - self.user = user + init(context: LDContext, creationDate: Date = Date()) { + self.context = context 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(context.fullyQualifiedKey(), forKey: .key) + try container.encode(context, forKey: .context) try container.encode(creationDate, forKey: .creationDate) } } diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 0a3e1094..1d64c3ce 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -23,7 +23,7 @@ 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 - any modifications or additions needed. Removing headers is discouraged as it may cause + any modifications or additions needed. Removing headers is discouraged as it may cause requests to fail. - parameter url: The endpoint that is being connected to @@ -115,7 +115,7 @@ public struct LDConfig { static let eventsUrl = URL(string: "https://mobile.launchdarkly.com")! /// 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 static let eventCapacity = 100 @@ -135,10 +135,10 @@ public struct LDConfig { /// The default mode to set LDClient online on a start call. (true) static let startOnline = true - /// The default setting for private user attributes. (false) - static let allUserAttributesPrivate = false - /// The default private user attribute list (nil) - static let privateUserAttributes: [UserAttribute] = [] + /// The default setting for private context attributes. (false) + static let allContextAttributesPrivate = false + /// The default private context attribute list (nil) + static let privateContextAttributes: [Reference] = [] /// 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 @@ -148,7 +148,7 @@ public struct LDConfig { /// The default setting controlling information logged to the console, and modifying some setting ranges to facilitate debugging. (false) static let debugMode = false - + /// The default setting for whether we request evaluation reasons for all flags. (false) static let evaluationReasons = false @@ -259,30 +259,30 @@ public struct LDConfig { } } 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 /** - Treat all user attributes as private for event reporting for all users. + Treat all context attributes as private for event reporting for all contexts. The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - When true, ignores values in either LDConfig.privateUserAttributes or LDUser.privateAttributes. (Default: false) + When true, ignores values in either LDConfig.privateContextAttributes or LDContext.privateAttributes. (Default: false) - See Also: `privateUserAttributes` and `LDUser.privateAttributes` + See Also: `privateContextAttributes` and `LDContext.privateAttributes` */ - public var allUserAttributesPrivate: Bool = Defaults.allUserAttributesPrivate + public var allContextAttributesPrivate: Bool = Defaults.allContextAttributesPrivate /** - User attributes and top level custom dictionary keys to treat as private for event reporting for all users. + Context attributes and top level custom dictionary keys to treat as private for event reporting for all contexts. The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - To set private user attributes for a specific user, see `LDUser.privateAttributes`. (Default: nil) + To set private context attributes for a specific context, see `LDContext.privateAttributes`. (Default: nil) - See Also: `allUserAttributesPrivate` and `LDUser.privateAttributes`. + See Also: `allContextAttributesPrivate` and `LDContext.privateAttributes`. */ - public var privateUserAttributes: [UserAttribute] = Defaults.privateUserAttributes + public var privateContextAttributes: [Reference] = Defaults.privateContextAttributes /** Directs the SDK to use REPORT for HTTP requests for feature flag data. (Default: `false`) @@ -300,10 +300,10 @@ public struct LDConfig { /// Enables logging for debugging. (Default: false) public var isDebugMode: Bool = Defaults.debugMode - + /// Enables requesting evaluation reasons for all flags. (Default: false) public var evaluationReasons: Bool = Defaults.evaluationReasons - + /// An Integer that tells UserEnvironmentFlagCache the maximum number of users to locally cache. Can be set to -1 for unlimited cached users. public var maxCachedUsers: Int = Defaults.maxCachedUsers @@ -381,10 +381,10 @@ public struct LDConfig { public func getSecondaryMobileKeys() -> [String: String] { return _secondaryMobileKeys } - + /// Internal variable for secondaryMobileKeys computed property private var _secondaryMobileKeys: [String: String] - + // Internal constructor to enable automated testing init(mobileKey: String, environmentReporter: EnvironmentReporting) { self.mobileKey = mobileKey @@ -434,8 +434,8 @@ extension LDConfig: Equatable { && lhs.streamingMode == rhs.streamingMode && lhs.enableBackgroundUpdates == rhs.enableBackgroundUpdates && lhs.startOnline == rhs.startOnline - && lhs.allUserAttributesPrivate == rhs.allUserAttributesPrivate - && Set(lhs.privateUserAttributes) == Set(rhs.privateUserAttributes) + && lhs.allContextAttributesPrivate == rhs.allContextAttributesPrivate + && Set(lhs.privateContextAttributes) == Set(rhs.privateContextAttributes) && lhs.useReport == rhs.useReport && lhs.inlineUserInEvents == rhs.inlineUserInEvents && lhs.isDebugMode == rhs.isDebugMode diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 7cc30e85..017a2314 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -14,7 +14,7 @@ extension EventSource: DarklyStreamingProvider {} protocol DarklyServiceProvider: AnyObject { var config: LDConfig { get } - var user: LDUser { get set } + var context: LDContext { get set } var diagnosticCache: DiagnosticCaching? { get } func getFeatureFlags(useReport: Bool, completion: ServiceCompletionHandler?) @@ -46,16 +46,16 @@ final class DarklyService: DarklyServiceProvider { } let config: LDConfig - var user: LDUser + var context: LDContext 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) { + init(config: LDConfig, context: LDContext, serviceFactory: ClientServiceCreating) { self.config = config - self.user = user + self.context = context self.serviceFactory = serviceFactory if !config.mobileKey.isEmpty && !config.diagnosticOptOut { @@ -83,7 +83,7 @@ final class DarklyService: DarklyServiceProvider { guard hasMobileKey(#function) else { return } let encoder = JSONEncoder() encoder.userInfo[LDUser.UserInfoKeys.includePrivateAttributes] = true - guard let userJsonData = try? encoder.encode(user) + guard let contextJsonData = try? encoder.encode(context) else { Log.debug(typeName(and: #function, appending: ": ") + "Aborting. Unable to create flagRequest.") return @@ -93,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: userJsonData), + var request = URLRequest(url: flagRequestUrl(useReport: useReport, getData: contextJsonData), ldHeaders: headers, ldConfig: config) if useReport { request.httpMethod = URLRequest.HTTPMethods.report - request.httpBody = userJsonData + request.httpBody = contextJsonData } self.session.dataTask(with: request) { [weak self] data, response, error in @@ -149,7 +149,7 @@ final class DarklyService: DarklyServiceProvider { errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { let encoder = JSONEncoder() encoder.userInfo[LDUser.UserInfoKeys.includePrivateAttributes] = true - let userJsonData = try? encoder.encode(user) + let userJsonData = try? encoder.encode(context) var streamRequestUrl = config.streamUrl.appendingPathComponent(StreamRequestPath.meval) var connectMethod = HTTPRequestMethod.get diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift index a8bdd949..abbd3a9a 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift @@ -17,7 +17,7 @@ struct HTTPHeaders { struct HeaderValue { static let apiKey = "api_key" static let applicationJson = "application/json" - static let eventSchema3 = "3" + static let eventSchema4 = "4" } private let mobileKey: String @@ -67,7 +67,7 @@ struct HTTPHeaders { var headers = baseHeaders headers[HeaderKey.contentType] = HeaderValue.applicationJson headers[HeaderKey.accept] = HeaderValue.applicationJson - headers[HeaderKey.eventSchema] = HeaderValue.eventSchema3 + headers[HeaderKey.eventSchema] = HeaderValue.eventSchema4 return withAdditionalHeaders(headers) } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index 76ab999b..8e73f12a 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -46,7 +46,7 @@ public final class ObjcLDClient: NSObject { // MARK: - State Controls and Indicators private var ldClient: LDClient - + /** Reports the online/offline state of the LDClient. @@ -110,9 +110,10 @@ public final class ObjcLDClient: NSObject { - parameter user: The ObjcLDUser set with the desired user. */ - @objc public func identify(user: ObjcLDUser) { - ldClient.identify(user: user.user, completion: nil) - } +// TODO(mmk) Come back to this +// @objc public func identify(context: ObjcLDContext) { +// ldClient.identify(context: context.context, completion: nil) +// } /** The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. @@ -126,9 +127,10 @@ public final class ObjcLDClient: NSObject { - parameter user: The ObjcLDUser set with the desired user. - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. (Optional) */ - @objc public func identify(user: ObjcLDUser, completion: (() -> Void)? = nil) { - ldClient.identify(user: user.user, completion: completion) - } +// TODO(mmk) Come back to this +// @objc public func identify(context: ObjcLDContext, completion: (() -> Void)? = nil) { +// ldClient.identify(context: context.context, completion: completion) +// } /** Stops the LDClient. Stopping the client means the LDClient goes offline and stops recording events. LDClient will no longer provide feature flag values, only returning default values. @@ -221,13 +223,13 @@ public final class ObjcLDClient: NSObject { @objc public func integerVariation(forKey key: LDFlagKey, defaultValue: Int) -> Int { ldClient.intVariation(forKey: key, defaultValue: defaultValue) } - + /** See [integerVariation](x-source-tag://integerVariation) 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: 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 { @@ -258,13 +260,13 @@ public final class ObjcLDClient: NSObject { @objc public func doubleVariation(forKey key: LDFlagKey, defaultValue: Double) -> Double { ldClient.doubleVariation(forKey: key, defaultValue: defaultValue) } - + /** See [doubleVariation](x-source-tag://doubleVariation) 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: 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 { @@ -295,13 +297,13 @@ public final class ObjcLDClient: NSObject { @objc public func stringVariation(forKey key: LDFlagKey, defaultValue: String) -> String { ldClient.stringVariation(forKey: key, defaultValue: defaultValue) } - + /** 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. - + - 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 { @@ -330,13 +332,13 @@ public final class ObjcLDClient: NSObject { @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. - + - 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: ObjcLDJSONEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func jsonVariationDetail(forKey key: LDFlagKey, defaultValue: ObjcLDValue) -> ObjcLDJSONEvaluationDetail { @@ -550,9 +552,10 @@ public final class ObjcLDClient: NSObject { - parameter completion: Closure called when the embedded `setOnline` call completes. (Optional) */ /// - Tag: start - @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, completion: (() -> Void)? = nil) { - LDClient.start(config: configuration.config, user: user.user, completion: completion) - } +// TODO(mmk) Come back to this +// @objc public static func start(configuration: ObjcLDConfig, context: ObjcLDContext, completion: (() -> Void)? = nil) { +// LDClient.start(config: configuration.config, context: context.context, completion: completion) +// } /** See [start](x-source-tag://start) for more information on starting the SDK. @@ -562,9 +565,10 @@ public final class ObjcLDClient: NSObject { - parameter startWaitSeconds: A TimeInterval that determines when the completion will return if no flags have been returned from the network. - parameter completion: Closure called when the embedded `setOnline` call completes. Takes a Bool that indicates whether the completion timedout as a parameter. (Optional) */ - @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { - LDClient.start(config: configuration.config, user: user.user, startWaitSeconds: startWaitSeconds, completion: completion) - } +// TODO(mmk) Come back to this +// @objc public static func start(configuration: ObjcLDConfig, context: ObjcLDContext, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { +// LDClient.start(config: configuration.config, context: context.context, startWaitSeconds: startWaitSeconds, completion: completion) +// } private init(client: LDClient) { ldClient = client diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index db8038b0..ba55cc0b 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -84,30 +84,30 @@ public final class ObjcLDConfig: NSObject { } /** - Treat all user attributes as private for event reporting for all users. + Treat all context attributes as private for event reporting for all contexts. The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - When YES, ignores values in either LDConfig.privateUserAttributes or LDUser.privateAttributes. (Default: NO) + When YES, ignores values in either LDConfig.privateContextAttributes or LDContext.privateAttributes. (Default: NO) - See Also: `privateUserAttributes` and `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`) + See Also: `privateContextAttributes` and `LDContext.privateAttributes` (`ObjcLDContext.privateAttributes`) */ - @objc public var allUserAttributesPrivate: Bool { - get { config.allUserAttributesPrivate } - set { config.allUserAttributesPrivate = newValue } + @objc public var allContextAttributesPrivate: Bool { + get { config.allContextAttributesPrivate } + set { config.allContextAttributesPrivate = newValue } } /** - User attributes and top level custom dictionary keys to treat as private for event reporting for all users. + Context attributes and top level custom dictionary keys to treat as private for event reporting for all contexts. The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - To set private user attributes for a specific user, see `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). (Default: `[]`) + To set private context attributes for a specific context, see `LDContext.privateAttributes` (`ObjcLDContext.privateAttributes`). (Default: `[]`) - See Also: `allUserAttributesPrivate` and `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). + See Also: `allContextAttributesPrivate` and `LDContext.privateAttributes` (`ObjcLDContext.privateAttributes`). */ - @objc public var privateUserAttributes: [String] { - get { config.privateUserAttributes.map { $0.name } } - set { config.privateUserAttributes = newValue.map { UserAttribute.forName($0) } } + @objc public var privateContextAttributes: [String] { + get { config.privateContextAttributes.map { $0.raw() } } + set { config.privateContextAttributes = newValue.map { Reference($0) } } } /** diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index 43067d3e..576fd810 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -89,9 +89,9 @@ final class CacheConverter: CacheConverting { } } - cachedEnvData.forEach { mobileKey, users in - users.forEach { userKey, data in - flagCaches[mobileKey]?.storeFeatureFlags(data.flags, userKey: userKey, lastUpdated: data.updated) + cachedEnvData.forEach { mobileKey, contexts in + contexts.forEach { contextKey, data in + flagCaches[mobileKey]?.storeFeatureFlags(data.flags, contextKey: contextKey, lastUpdated: data.updated) } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift index c4720153..f792e31b 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -5,8 +5,8 @@ protocol FeatureFlagCaching { // sourcery: defaultMockValue = KeyedValueCachingMock() var keyedValueCache: KeyedValueCaching { get } - func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) + func retrieveFeatureFlags(contextKey: String) -> [LDFlagKey: FeatureFlag]? + func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], contextKey: String, lastUpdated: Date) } final class FeatureFlagCache: FeatureFlagCaching { @@ -24,25 +24,25 @@ final class FeatureFlagCache: FeatureFlagCaching { self.maxCachedUsers = maxCachedUsers } - func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? { - guard let cachedData = keyedValueCache.data(forKey: "flags-\(Util.sha256base64(userKey))"), + func retrieveFeatureFlags(contextKey: String) -> [LDFlagKey: FeatureFlag]? { + guard let cachedData = keyedValueCache.data(forKey: "flags-\(Util.sha256base64(contextKey))"), let cachedFlags = try? JSONDecoder().decode(FeatureFlagCollection.self, from: cachedData) else { return nil } return cachedFlags.flags } - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) { + func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], contextKey: 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)") + let contextSha = Util.sha256base64(contextKey) + self.keyedValueCache.set(encoded, forKey: "flags-\(contextSha)") 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 + cachedUsers[contextSha] = 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 diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index aa804f8a..a999f71a 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -5,7 +5,7 @@ protocol ClientServiceCreating { func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching func makeFeatureFlagCache(mobileKey: String, maxCachedUsers: Int) -> FeatureFlagCaching func makeCacheConverter() -> CacheConverting - func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider + func makeDarklyServiceProvider(config: LDConfig, context: LDContext) -> DarklyServiceProvider func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, useReport: Bool, service: DarklyServiceProvider) -> LDFlagSynchronizing func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, @@ -37,8 +37,8 @@ final class ClientServiceFactory: ClientServiceCreating { CacheConverter() } - func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider { - DarklyService(config: config, user: user, serviceFactory: self) + func makeDarklyServiceProvider(config: LDConfig, context: LDContext) -> DarklyServiceProvider { + DarklyService(config: config, context: context, serviceFactory: self) } func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, useReport: Bool, service: DarklyServiceProvider) -> LDFlagSynchronizing { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index 8d746de1..b6cd7038 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: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) + func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, context: LDContext, includeReason: Bool) func flush(completion: CompletionClosure?) } @@ -54,18 +54,18 @@ class EventReporter: EventReporting { } // swiftlint:disable:next function_parameter_count - func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) { + func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, context: LDContext, includeReason: Bool) { let recordingFeatureEvent = featureFlag?.trackEvents == true let recordingDebugEvent = featureFlag?.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate) ?? false eventQueue.sync { flagRequestTracker.trackRequest(flagKey: flagKey, reportedValue: value, featureFlag: featureFlag, defaultValue: defaultValue) if recordingFeatureEvent { - let featureEvent = FeatureEvent(key: flagKey, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: false) + let featureEvent = FeatureEvent(key: flagKey, context: context, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: false) recordNoSync(featureEvent) } if recordingDebugEvent { - let debugEvent = FeatureEvent(key: flagKey, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: true) + let debugEvent = FeatureEvent(key: flagKey, context: context, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: true) recordNoSync(debugEvent) } } @@ -76,7 +76,7 @@ class EventReporter: EventReporting { else { return } eventReportTimer = LDTimer(withTimeInterval: service.config.eventFlushInterval, fireQueue: eventQueue, execute: reportEvents) } - + private func stopReporting() { eventReportTimer?.cancel() eventReportTimer = nil @@ -128,9 +128,11 @@ class EventReporter: EventReporting { private func publish(_ events: [Event], _ payloadId: String, _ completion: CompletionClosure?) { 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 }] + [ + Event.UserInfoKeys.inlineUserInEvents: service.config.inlineUserInEvents, + LDContext.UserInfoKeys.allAttributesPrivate: service.config.allContextAttributesPrivate, + LDContext.UserInfoKeys.globalPrivateAttributes: service.config.privateContextAttributes.map { $0 } + ] let encoder = JSONEncoder() encoder.userInfo = encodingConfig encoder.dateEncodingStrategy = .custom { date, encoder in diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index b4bd49ea..495b48e3 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -23,7 +23,7 @@ final class LDClientSpec: QuickSpec { class TestContext { var config: LDConfig! - var user: LDUser! + var context: LDContext! var subject: LDClient! let serviceFactoryMock = ClientServiceMockFactory() // mock getters based on setting up the user & subject @@ -84,7 +84,7 @@ final class LDClientSpec: QuickSpec { let mobileKey = self.serviceFactoryMock.makeFeatureFlagCacheReceivedParameters!.mobileKey let mockCache = FeatureFlagCachingMock() mockCache.retrieveFeatureFlagsCallback = { - mockCache.retrieveFeatureFlagsReturnValue = self.cachedFlags[mobileKey]?[mockCache.retrieveFeatureFlagsReceivedUserKey!] + mockCache.retrieveFeatureFlagsReturnValue = self.cachedFlags[mobileKey]?[mockCache.retrieveFeatureFlagsReceivedContextKey!] } self.serviceFactoryMock.makeFeatureFlagCacheReturnValue = mockCache } @@ -95,27 +95,27 @@ final class LDClientSpec: QuickSpec { config.enableBackgroundUpdates = enableBackgroundUpdates config.eventFlushInterval = 300.0 // 5 min...don't want this to trigger - user = LDUser.stub() + context = LDContext.stub() } - func withUser(_ user: LDUser?) -> TestContext { - self.user = user + func withContext(_ context: LDContext?) -> TestContext { + self.context = context return self } func withCached(flags: [LDFlagKey: FeatureFlag]?) -> TestContext { - withCached(userKey: user.key, flags: flags) + withCached(contextKey: context.fullyQualifiedKey(), flags: flags) } - func withCached(userKey: String, flags: [LDFlagKey: FeatureFlag]?) -> TestContext { + func withCached(contextKey: String, flags: [LDFlagKey: FeatureFlag]?) -> TestContext { var forEnv = cachedFlags[config.mobileKey] ?? [:] - forEnv[userKey] = flags + forEnv[contextKey] = flags cachedFlags[config.mobileKey] = forEnv return self } func start(runMode: LDClientRunMode = .foreground, completion: (() -> Void)? = nil) { - LDClient.start(serviceFactory: serviceFactoryMock, config: config, user: user) { + LDClient.start(serviceFactory: serviceFactoryMock, config: config, context: context) { self.subject = LDClient.get() if runMode == .background { self.subject.setRunMode(.background) @@ -126,7 +126,7 @@ final class LDClientSpec: QuickSpec { } func start(runMode: LDClientRunMode = .foreground, timeOut: TimeInterval, timeOutCompletion: ((_ timedOut: Bool) -> Void)? = nil) { - LDClient.start(serviceFactory: serviceFactoryMock, config: config, user: user, startWaitSeconds: timeOut) { timedOut in + LDClient.start(serviceFactory: serviceFactoryMock, config: config, context: context, startWaitSeconds: timeOut) { timedOut in self.subject = LDClient.get() if runMode == .background { self.subject.setRunMode(.background) @@ -189,21 +189,21 @@ final class LDClientSpec: QuickSpec { expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } it("saves the user") { - expect(testContext.subject.user) == testContext.user - expect(testContext.subject.service.user) == testContext.user + expect(testContext.subject.context) == testContext.context + expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { expect(makeFlagSynchronizerReceivedParameters.service) === testContext.subject.service } - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.context } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 @@ -232,21 +232,21 @@ final class LDClientSpec: QuickSpec { expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } it("saves the user") { - expect(testContext.subject.user) == testContext.user - expect(testContext.subject.service.user) == testContext.user + expect(testContext.subject.context) == testContext.context + expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { expect(makeFlagSynchronizerReceivedParameters.service) === testContext.subject.service } - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.context } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 @@ -260,11 +260,11 @@ final class LDClientSpec: QuickSpec { context("when called without user") { context("after setting user") { beforeEach { - testContext = TestContext(startOnline: true).withUser(nil) + testContext = TestContext(startOnline: true).withContext(nil) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() - testContext.user = LDUser.stub() - testContext.subject.internalIdentify(newUser: testContext.user) + testContext.context = LDContext.stub() + testContext.subject.internalIdentify(newContext: testContext.context) } it("saves the config") { expect(testContext.subject.config) == testContext.config @@ -274,21 +274,21 @@ final class LDClientSpec: QuickSpec { expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } it("saves the user") { - expect(testContext.subject.user) == testContext.user - expect(testContext.subject.service.user) == testContext.user + expect(testContext.subject.context) == testContext.context + expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { - expect(makeFlagSynchronizerReceivedParameters.service.user) == testContext.user + expect(makeFlagSynchronizerReceivedParameters.service.context) == testContext.context } - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 // called on init and subsequent identify - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 2 // both start and internalIdentify - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.context } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 @@ -298,7 +298,7 @@ final class LDClientSpec: QuickSpec { } context("without setting user") { beforeEach { - testContext = TestContext(startOnline: true).withUser(nil) + testContext = TestContext(startOnline: true).withContext(nil) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() } it("saves the config") { @@ -309,19 +309,18 @@ final class LDClientSpec: QuickSpec { expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } it("uses anonymous user") { - expect(testContext.subject.user.key) == LDUser.defaultKey(environmentReporter: testContext.environmentReporterMock) - expect(testContext.subject.user.isAnonymous).to(beTrue()) - expect(testContext.subject.service.user) == testContext.subject.user - expect(testContext.makeFlagSynchronizerService?.user) == testContext.subject.user - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.subject.user + expect(testContext.subject.context.fullyQualifiedKey()) == LDContext.defaultKey(environmentReporting: testContext.environmentReporterMock) + expect(testContext.subject.service.context) == testContext.subject.context + expect(testContext.makeFlagSynchronizerService?.context) == testContext.subject.context + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.subject.context } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.subject.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.subject.context.fullyQualifiedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.subject.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.subject.context } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 @@ -336,7 +335,7 @@ final class LDClientSpec: QuickSpec { withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == cachedFlags @@ -349,7 +348,7 @@ final class LDClientSpec: QuickSpec { withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() expect(testContext.flagStoreMock.replaceStoreCallCount) == 0 @@ -473,21 +472,21 @@ final class LDClientSpec: QuickSpec { expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } it("saves the user") { - expect(testContext.subject.user) == testContext.user - expect(testContext.subject.service.user) == testContext.user + expect(testContext.subject.context) == testContext.context + expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { expect(makeFlagSynchronizerReceivedParameters.service) === testContext.subject.service } - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.context } } } @@ -512,21 +511,21 @@ final class LDClientSpec: QuickSpec { expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } it("saves the user") { - expect(testContext.subject.user) == testContext.user - expect(testContext.subject.service.user) == testContext.user + expect(testContext.subject.context) == testContext.context + expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { - expect(makeFlagSynchronizerReceivedParameters.service.user) == testContext.user + expect(makeFlagSynchronizerReceivedParameters.service.context) == testContext.context } - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.context } } } @@ -541,20 +540,20 @@ final class LDClientSpec: QuickSpec { testContext.start() testContext.featureFlagCachingMock.reset() - let newUser = LDUser.stub() - testContext.subject.internalIdentify(newUser: newUser) + let newContext = LDContext.stub() + testContext.subject.internalIdentify(newContext: newContext) - expect(testContext.subject.user) == newUser - expect(testContext.subject.service.user) == newUser + expect(testContext.subject.context) == newContext + expect(testContext.subject.service.context) == newContext expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 - expect(testContext.makeFlagSynchronizerService?.user) == newUser + expect(testContext.makeFlagSynchronizerService?.context) == newContext 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.retrieveFeatureFlagsReceivedUserKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == newContext.fullyQualifiedKey() expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } @@ -563,33 +562,33 @@ final class LDClientSpec: QuickSpec { testContext.start() testContext.featureFlagCachingMock.reset() - let newUser = LDUser.stub() - testContext.subject.internalIdentify(newUser: newUser) + let newContext = LDContext.stub() + testContext.subject.internalIdentify(newContext: newContext) - expect(testContext.subject.user) == newUser - expect(testContext.subject.service.user) == newUser + expect(testContext.subject.context) == newContext + expect(testContext.subject.service.context) == newContext expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 - expect(testContext.makeFlagSynchronizerService?.user) == newUser + expect(testContext.makeFlagSynchronizerService?.context) == newContext 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.retrieveFeatureFlagsReceivedUserKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == newContext.fullyQualifiedKey() expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } it("when the new user has cached feature flags") { let stubFlags = FlagMaintainingMock.stubFlags() - let newUser = LDUser.stub() - let testContext = TestContext().withCached(userKey: newUser.key, flags: stubFlags) + let newContext = LDContext.stub() + let testContext = TestContext().withCached(contextKey: newContext.fullyQualifiedKey(), flags: stubFlags) testContext.start() testContext.featureFlagCachingMock.reset() - testContext.subject.internalIdentify(newUser: newUser) + testContext.subject.internalIdentify(newContext: newContext) - expect(testContext.subject.user) == newUser + expect(testContext.subject.context) == newContext expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == stubFlags } @@ -704,7 +703,7 @@ final class LDClientSpec: QuickSpec { testContext.subject.track(key: "customEvent", data: "abc", metricValue: 5.0) let receivedEvent = testContext.eventReporterMock.recordReceivedEvent as? CustomEvent expect(receivedEvent?.key) == "customEvent" - expect(receivedEvent?.user) == testContext.user + expect(receivedEvent?.context) == testContext.context expect(receivedEvent?.data) == "abc" expect(receivedEvent?.metricValue) == 5.0 } @@ -748,7 +747,7 @@ final class LDClientSpec: QuickSpec { 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 + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.context) == testContext.context } } } @@ -769,7 +768,7 @@ final class LDClientSpec: QuickSpec { 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 + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.context) == testContext.context } } } @@ -878,7 +877,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 @@ -905,7 +904,7 @@ final class LDClientSpec: QuickSpec { 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?.contextKey) == testContext.context.fullyQualifiedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 @@ -932,7 +931,7 @@ final class LDClientSpec: QuickSpec { 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?.contextKey) == testContext.context.fullyQualifiedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 @@ -1277,7 +1276,7 @@ final class LDClientSpec: QuickSpec { } } } - + private func connectionInformationSpec() { describe("ConnectionInformation") { it("when client was started in foreground") { @@ -1299,7 +1298,7 @@ final class LDClientSpec: QuickSpec { } } } - + private func variationDetailSpec() { describe("variationDetail") { it("when flag doesn't exist") { @@ -1350,7 +1349,7 @@ final class LDClientSpec: QuickSpec { extension FeatureFlagCachingMock { func reset() { retrieveFeatureFlagsCallCount = 0 - retrieveFeatureFlagsReceivedUserKey = nil + retrieveFeatureFlagsReceivedContextKey = nil retrieveFeatureFlagsReturnValue = nil storeFeatureFlagsCallCount = 0 storeFeatureFlagsReceivedArguments = nil diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index e41a117a..bda02d4f 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -28,8 +28,8 @@ final class ClientServiceMockFactory: ClientServiceCreating { return makeCacheConverterReturnValue } - func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider { - DarklyServiceMock(config: config, user: user) + func makeDarklyServiceProvider(config: LDConfig, context: LDContext) -> DarklyServiceProvider { + DarklyServiceMock(config: config, context: context) } var makeFlagSynchronizerCallCount = 0 diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 3e212454..a1d15c6c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -94,14 +94,14 @@ final class DarklyServiceMock: DarklyServiceProvider { } var config: LDConfig - var user: LDUser + var context: LDContext var diagnosticCache: DiagnosticCaching? = nil var activationBlocks = [(testBlock: HTTPStubsTestBlock, callback: ((URLRequest, HTTPStubsDescriptor, HTTPStubsResponse) -> Void))]() - init(config: LDConfig = LDConfig.stub, user: LDUser = LDUser.stub()) { + init(config: LDConfig = LDConfig.stub, context: LDContext = LDContext.stub()) { self.config = config - self.user = user + self.context = context } var stubbedFlagResponse: ServiceResponse? diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift new file mode 100644 index 00000000..ee0a87b5 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift @@ -0,0 +1,58 @@ +import Foundation +@testable import LaunchDarkly + +extension LDContext { + struct StubConstants { + static let key: LDValue = "stub.user.key" + + static let name = "stub.user.name" + static let secondary = "stub.user.secondary" + static let isAnonymous = false + + static let firstName: LDValue = "stub.user.firstName" + static let lastName: LDValue = "stub.user.lastName" + static let country: LDValue = "stub.user.country" + static let ipAddress: LDValue = "stub.user.ipAddress" + static let email: LDValue = "stub.user@email.com" + static let avatar: LDValue = "stub.user.avatar" + 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 stub(key: String? = nil, + environmentReporter: EnvironmentReportingMock? = nil) -> LDContext { + var builder = LDContextBuilder(key: key ?? UUID().uuidString) + + builder.name(StubConstants.name) + builder.secondary(StubConstants.secondary) + builder.transient(StubConstants.isAnonymous) + + builder.trySetValue("firstName", StubConstants.firstName) + builder.trySetValue("lastName", StubConstants.lastName) + builder.trySetValue("country", StubConstants.country) + builder.trySetValue("ip", StubConstants.ipAddress) + builder.trySetValue("email", StubConstants.email) + builder.trySetValue("avatar", StubConstants.avatar) + + for (key, value) in StubConstants.custom { + builder.trySetValue(key, value) + } + + builder.trySetValue("device", StubConstants.device) + builder.trySetValue("os", StubConstants.operatingSystem) + + var context: LDContext? = nil + + if case .success(let ctx) = builder.build() { + context = ctx + } + + return context! + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index 6c9c947c..e2c66bd2 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -178,7 +178,7 @@ final class DiagnosticEventSpec: QuickSpec { customConfig.connectionTimeout = 30.0 customConfig.eventFlushInterval = 60.0 customConfig.streamingMode = .polling - customConfig.allUserAttributesPrivate = true + customConfig.allContextAttributesPrivate = true customConfig.flagPollingInterval = 360.0 customConfig.backgroundFlagPollingInterval = 1_800.0 customConfig.inlineUserInEvents = true diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 380c2e6a..20a9db75 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -6,12 +6,12 @@ import XCTest final class EventSpec: XCTestCase { func testFeatureEventInit() { let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - let user = LDUser.stub() + let context = LDContext.stub() let testDate = Date() - let event = FeatureEvent(key: "abc", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: true, isDebug: false, creationDate: testDate) + let event = FeatureEvent(key: "abc", context: context, 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.context, context) XCTAssertEqual(event.value, true) XCTAssertEqual(event.defaultValue, false) XCTAssertEqual(event.featureFlag, featureFlag) @@ -21,12 +21,12 @@ final class EventSpec: XCTestCase { func testDebugEventInit() { let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - let user = LDUser.stub() + let context = LDContext.stub() let testDate = Date() - let event = FeatureEvent(key: "abc", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true, creationDate: testDate) + let event = FeatureEvent(key: "abc", context: context, 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.context, context) XCTAssertEqual(event.value, true) XCTAssertEqual(event.defaultValue, false) XCTAssertEqual(event.featureFlag, featureFlag) @@ -35,12 +35,12 @@ final class EventSpec: XCTestCase { } func testCustomEventInit() { - let user = LDUser.stub() + let context = LDContext.stub() let testDate = Date() - let event = CustomEvent(key: "abc", user: user, data: ["abc": 123], metricValue: 5.0, creationDate: testDate) + let event = CustomEvent(key: "abc", context: context, data: ["abc": 123], metricValue: 5.0, creationDate: testDate) XCTAssertEqual(event.kind, Event.Kind.custom) XCTAssertEqual(event.key, "abc") - XCTAssertEqual(event.user, user) + XCTAssertEqual(event.context, context) XCTAssertEqual(event.data, ["abc": 123]) XCTAssertEqual(event.metricValue, 5.0) XCTAssertEqual(event.creationDate, testDate) @@ -48,10 +48,10 @@ final class EventSpec: XCTestCase { func testIdentifyEventInit() { let testDate = Date() - let user = LDUser.stub() - let event = IdentifyEvent(user: user, creationDate: testDate) + let context = LDContext.stub() + let event = IdentifyEvent(context: context, creationDate: testDate) XCTAssertEqual(event.kind, Event.Kind.identify) - XCTAssertEqual(event.user, user) + XCTAssertEqual(event.context, context) XCTAssertEqual(event.creationDate, testDate) } @@ -66,51 +66,50 @@ final class EventSpec: XCTestCase { } func testCustomEventEncodingDataAndMetric() { - let user = LDUser.stub() - let event = CustomEvent(key: "event-key", user: user, data: ["abc", 12], metricValue: 0.5) + let context = LDContext.stub() + let event = CustomEvent(key: "event-key", context: context, 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["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } func testCustomEventEncodingAnonUser() { - let anonUser = LDUser() - let event = CustomEvent(key: "event-key", user: anonUser, data: ["key": "val"]) + let context = LDContext.stub() + let event = CustomEvent(key: "event-key", context: context, data: ["key": "val"]) encodesToObject(event) { dict in - XCTAssertEqual(dict.count, 6) + XCTAssertEqual(dict.count, 5) 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["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } func testCustomEventEncodingInlining() { - let user = LDUser.stub() - let event = CustomEvent(key: "event-key", user: user, data: nil, metricValue: 2.5) + let context = LDContext.stub() + let event = CustomEvent(key: "event-key", context: context, 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["context"], encodeToLDValue(context)) XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } func testFeatureEventEncodingNoReasonByDefault() { - let user = LDUser.stub() + let context = LDContext.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) + let event = FeatureEvent(key: "event-key", context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: isDebug) encodesToObject(event) { dict in XCTAssertEqual(dict.count, 8) XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") @@ -120,9 +119,9 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["variation"], 2) XCTAssertEqual(dict["version"], 3) if isDebug { - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) } else { - XCTAssertEqual(dict["userKey"], .string(user.key)) + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) } XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } @@ -130,10 +129,10 @@ final class EventSpec: XCTestCase { } func testFeatureEventEncodingIncludeReason() { - let user = LDUser.stub() + let context = LDContext.stub() 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) + let event = FeatureEvent(key: "event-key", context: context, value: 3, defaultValue: 4, featureFlag: featureFlag, includeReason: true, isDebug: isDebug) encodesToObject(event) { dict in XCTAssertEqual(dict.count, 9) XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") @@ -144,9 +143,9 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["version"], 3) XCTAssertEqual(dict["reason"], ["kind": "OFF"]) if isDebug { - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) } else { - XCTAssertEqual(dict["userKey"], .string(user.key)) + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) } XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } @@ -154,10 +153,10 @@ final class EventSpec: XCTestCase { } func testFeatureEventEncodingTrackReason() { - let user = LDUser.stub() + let context = LDContext.stub() 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) + let event = FeatureEvent(key: "event-key", context: context, value: nil, defaultValue: nil, featureFlag: featureFlag, includeReason: false, isDebug: isDebug) encodesToObject(event) { dict in XCTAssertEqual(dict.count, 7) XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") @@ -166,9 +165,9 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["default"], .null) XCTAssertEqual(dict["reason"], ["kind": "OFF"]) if isDebug { - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) } else { - XCTAssertEqual(dict["userKey"], .string(user.key)) + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) } XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } @@ -176,20 +175,19 @@ final class EventSpec: XCTestCase { } func testFeatureEventEncodingAnonContextKind() { - let user = LDUser() + let context = LDContext.stub() [false, true].forEach { isDebug in - let event = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: nil, includeReason: true, isDebug: isDebug) + let event = FeatureEvent(key: "event-key", context: context, value: true, defaultValue: false, featureFlag: nil, includeReason: true, isDebug: isDebug) encodesToObject(event) { dict in - XCTAssertEqual(dict.count, isDebug ? 6 : 7) + XCTAssertEqual(dict.count, 6) 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)) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) } else { - XCTAssertEqual(dict["userKey"], .string(user.key)) - XCTAssertEqual(dict["contextKind"], "anonymousUser") + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) } XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } @@ -197,10 +195,10 @@ final class EventSpec: XCTestCase { } func testFeatureEventEncodingInlinesUserForDebugOrConfig() { - let user = LDUser.stub() + let context = LDContext.stub() 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 featureEvent = FeatureEvent(key: "event-key", context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: false) + let debugEvent = FeatureEvent(key: "event-key", context: context, 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 @@ -209,19 +207,19 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["value"], true) XCTAssertEqual(dict["default"], false) XCTAssertEqual(dict["version"], 3) - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) }} } - func testIdentifyEventEncoding() { - let user = LDUser.stub() + func testIdentifyEventEncoding() throws { + let context = LDContext.stub() for inlineUser in [true, false] { - let event = IdentifyEvent(user: user) + let event = IdentifyEvent(context: context) 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["key"], .string(context.fullyQualifiedKey())) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } @@ -251,22 +249,23 @@ final class EventSpec: XCTestCase { extension Event: Equatable { public static func == (_ lhs: Event, _ rhs: Event) -> Bool { - let config = [LDUser.UserInfoKeys.includePrivateAttributes: true, Event.UserInfoKeys.inlineUserInEvents: true] + // TODO(mmk) Do we need this inline users stuff? + let config = [LDContext.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 { + static func stub(_ eventKind: Kind, with context: LDContext) -> Event { switch eventKind { case .feature: let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - return FeatureEvent(key: UUID().uuidString, user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: false) + return FeatureEvent(key: UUID().uuidString, context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: false) case .debug: let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - 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)]) + return FeatureEvent(key: UUID().uuidString, context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true) + case .identify: return IdentifyEvent(context: context) + case .custom: return CustomEvent(key: UUID().uuidString, context: context, data: ["custom": .string(UUID().uuidString)]) case .summary: return SummaryEvent(flagRequestTracker: FlagRequestTracker.stub()) } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index 9a9d8155..645be9cf 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -16,8 +16,8 @@ final class LDConfigSpec: XCTestCase { fileprivate static let enableBackgroundUpdates = true fileprivate static let startOnline = false - fileprivate static let allUserAttributesPrivate = true - fileprivate static let privateUserAttributes: [UserAttribute] = [UserAttribute.forName("dummy")] + fileprivate static let allContextAttributesPrivate = true + fileprivate static let privateContextAttributes: [Reference] = [Reference("dummy")] fileprivate static let useReport = true @@ -47,8 +47,8 @@ final class LDConfigSpec: XCTestCase { ("enable background updates", Constants.enableBackgroundUpdates, { c, v in c.enableBackgroundUpdates = v as! Bool }), ("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! [UserAttribute])}), + ("all user attributes private", Constants.allContextAttributesPrivate, { c, v in c.allContextAttributesPrivate = v as! Bool }), + ("private user attributes", Constants.privateContextAttributes, { c, v in c.privateContextAttributes = (v as! [Reference])}), ("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 }), @@ -73,8 +73,8 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.streamingMode, LDConfig.Defaults.streamingMode) XCTAssertEqual(config.enableBackgroundUpdates, LDConfig.Defaults.enableBackgroundUpdates) XCTAssertEqual(config.startOnline, LDConfig.Defaults.startOnline) - XCTAssertEqual(config.allUserAttributesPrivate, LDConfig.Defaults.allUserAttributesPrivate) - XCTAssertEqual(config.privateUserAttributes, LDConfig.Defaults.privateUserAttributes) + XCTAssertEqual(config.allContextAttributesPrivate, LDConfig.Defaults.allContextAttributesPrivate) + XCTAssertEqual(config.privateContextAttributes, LDConfig.Defaults.privateContextAttributes) XCTAssertEqual(config.useReport, LDConfig.Defaults.useReport) XCTAssertEqual(config.inlineUserInEvents, LDConfig.Defaults.inlineUserInEvents) XCTAssertEqual(config.isDebugMode, LDConfig.Defaults.debugMode) @@ -107,8 +107,8 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.streamingMode, Constants.streamingMode, "\(os)") XCTAssertEqual(config.enableBackgroundUpdates, os.isBackgroundEnabled, "\(os)") XCTAssertEqual(config.startOnline, Constants.startOnline, "\(os)") - XCTAssertEqual(config.allUserAttributesPrivate, Constants.allUserAttributesPrivate, "\(os)") - XCTAssertEqual(config.privateUserAttributes, Constants.privateUserAttributes, "\(os)") + XCTAssertEqual(config.allContextAttributesPrivate, Constants.allContextAttributesPrivate, "\(os)") + XCTAssertEqual(config.privateContextAttributes, Constants.privateContextAttributes, "\(os)") XCTAssertEqual(config.useReport, Constants.useReport, "\(os)") XCTAssertEqual(config.inlineUserInEvents, Constants.inlineUserInEvents, "\(os)") XCTAssertEqual(config.isDebugMode, Constants.debugMode, "\(os)") diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index 77831af1..7aac758c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -6,9 +6,9 @@ import LDSwiftEventSource @testable import LaunchDarkly final class DarklyServiceSpec: QuickSpec { - + typealias ServiceResponses = (data: Data?, urlResponse: URLResponse?, error: Error?) - + struct Constants { static let eventCount = 3 static let useGetMethod = false @@ -16,7 +16,7 @@ final class DarklyServiceSpec: QuickSpec { } struct TestContext { - let user = LDUser.stub() + let context = LDContext.stub() var config: LDConfig! var serviceMock: DarklyServiceMock! var serviceFactoryMock: ClientServiceMockFactory = ClientServiceMockFactory() @@ -32,9 +32,9 @@ final class DarklyServiceSpec: QuickSpec { config.useReport = useReport config.diagnosticOptOut = diagnosticOptOut serviceMock = DarklyServiceMock(config: config) - service = DarklyService(config: config, user: user, serviceFactory: serviceFactoryMock) + service = DarklyService(config: config, context: context, serviceFactory: serviceFactoryMock) httpHeaders = HTTPHeaders(config: config, environmentReporter: config.environmentReporter) - } + } func runStubbedGet(statusCode: Int, featureFlags: [LDFlagKey: FeatureFlag]? = nil, flagResponseEtag: String? = nil) { serviceMock.stubFlagRequest(statusCode: statusCode, useReport: config.useReport, flagResponseEtag: flagResponseEtag) @@ -45,7 +45,7 @@ final class DarklyServiceSpec: QuickSpec { } } } - + override func spec() { getFeatureFlagsSpec() flagRequestEtagSpec() @@ -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 = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) - expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedUser + let expectedContext = encodeToLDValue(testContext.context, userInfo: [LDContext.UserInfoKeys.includePrivateAttributes: true]) + expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedContext } 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 = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) - expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedUser + let expectedContext = encodeToLDValue(testContext.context, userInfo: [LDContext.UserInfoKeys.includePrivateAttributes: true]) + expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedContext } 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 = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) - expect(receivedArguments!.url.lastPathComponent.jsonValue) == expectedUser + let expectedContext = encodeToLDValue(testContext.context, userInfo: [LDContext.UserInfoKeys.includePrivateAttributes: true]) + expect(receivedArguments!.url.lastPathComponent.jsonValue) == expectedContext 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 = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) - expect(try? JSONDecoder().decode(LDValue.self, from: receivedArguments!.connectBody!)) == expectedUser + let expectedContext = encodeToLDValue(testContext.context, userInfo: [LDContext.UserInfoKeys.includePrivateAttributes: true]) + expect(try? JSONDecoder().decode(LDValue.self, from: receivedArguments!.connectBody!)) == expectedContext } } } @@ -590,7 +590,7 @@ final class DarklyServiceSpec: QuickSpec { } it("makes a valid request") { expect(eventRequest).toNot(beNil()) - expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventSchema]) == HTTPHeaders.HeaderValue.eventSchema3 + expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventSchema]) == HTTPHeaders.HeaderValue.eventSchema4 expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventPayloadIDHeader]?.count) == 36 } it("calls completion with data, response, and no error") { @@ -612,7 +612,7 @@ final class DarklyServiceSpec: QuickSpec { } it("makes a valid request") { expect(eventRequest).toNot(beNil()) - expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventSchema]) == HTTPHeaders.HeaderValue.eventSchema3 + expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventSchema]) == HTTPHeaders.HeaderValue.eventSchema4 expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventPayloadIDHeader]?.count) == 36 } it("calls completion with error and no data or response") { diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift index 11cce882..8a9e536a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift @@ -36,7 +36,7 @@ final class HTTPHeadersSpec: XCTestCase { "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)") XCTAssertEqual(headers[HTTPHeaders.HeaderKey.contentType], HTTPHeaders.HeaderValue.applicationJson) XCTAssertEqual(headers[HTTPHeaders.HeaderKey.accept], HTTPHeaders.HeaderValue.applicationJson) - XCTAssertEqual(headers[HTTPHeaders.HeaderKey.eventSchema], HTTPHeaders.HeaderValue.eventSchema3) + XCTAssertEqual(headers[HTTPHeaders.HeaderKey.eventSchema], HTTPHeaders.HeaderValue.eventSchema4) } func testDiagnosticRequestDefaultHeaders() { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift index 3989e155..868c97de 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift @@ -27,7 +27,7 @@ final class FeatureFlagCacheSpec: XCTestCase { func testRetrieveNoData() { let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) - XCTAssertNil(flagCache.retrieveFeatureFlags(userKey: "user1")) + XCTAssertNil(flagCache.retrieveFeatureFlags(contextKey: "user1")) XCTAssertEqual(mockValueCache.dataCallCount, 1) XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-\(Util.sha256base64("user1"))") } @@ -35,19 +35,19 @@ final class FeatureFlagCacheSpec: XCTestCase { func testRetrieveInvalidData() { mockValueCache.dataReturnValue = Data("invalid".utf8) let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - XCTAssertNil(flagCache.retrieveFeatureFlags(userKey: "user1")) + XCTAssertNil(flagCache.retrieveFeatureFlags(contextKey: "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) + XCTAssertEqual(flagCache.retrieveFeatureFlags(contextKey: "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") + let retrieved = flagCache.retrieveFeatureFlags(contextKey: "user1") XCTAssertEqual(retrieved, testFlagCollection.flags) XCTAssertEqual(mockValueCache.dataCallCount, 1) XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-\(Util.sha256base64("user1"))") @@ -55,7 +55,7 @@ final class FeatureFlagCacheSpec: XCTestCase { func testStoreCacheDisabled() { let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) - flagCache.storeFeatureFlags([:], userKey: "user1", lastUpdated: Date()) + flagCache.storeFeatureFlags([:], contextKey: "user1", lastUpdated: Date()) XCTAssertEqual(mockValueCache.setCallCount, 0) XCTAssertEqual(mockValueCache.dataCallCount, 0) XCTAssertEqual(mockValueCache.removeObjectCallCount, 0) @@ -77,7 +77,7 @@ final class FeatureFlagCacheSpec: XCTestCase { } } let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: -1) - flagCache.storeFeatureFlags([:], userKey: "user1", lastUpdated: now) + flagCache.storeFeatureFlags([:], contextKey: "user1", lastUpdated: now) XCTAssertEqual(count, 3) } @@ -88,7 +88,7 @@ final class FeatureFlagCacheSpec: XCTestCase { } } let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: Date()) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: "user1", lastUpdated: Date()) XCTAssertEqual(mockValueCache.setCallCount, 2) } @@ -98,7 +98,7 @@ final class FeatureFlagCacheSpec: XCTestCase { 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) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: "user1", lastUpdated: now) XCTAssertEqual(mockValueCache.removeObjectCallCount, 1) XCTAssertEqual(mockValueCache.removeObjectReceivedForKey, "flags-key1") let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) @@ -116,7 +116,7 @@ final class FeatureFlagCacheSpec: XCTestCase { 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) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: "user1", lastUpdated: later) XCTAssertEqual(mockValueCache.removeObjectCallCount, 2) XCTAssertTrue(removedObjects.contains("flags-key1")) XCTAssertTrue(removedObjects.contains("flags-key2")) @@ -129,7 +129,7 @@ final class FeatureFlagCacheSpec: XCTestCase { 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) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: "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/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index 1e0d177e..1787f765 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -12,7 +12,7 @@ final class EventReporterSpec: QuickSpec { struct TestContext { var eventReporter: EventReporter! var config: LDConfig! - var user: LDUser! + var context: LDContext! var serviceMock: DarklyServiceMock! var events: [Event] = [] var lastEventResponseDate: Date? @@ -33,7 +33,7 @@ final class EventReporterSpec: QuickSpec { config.eventCapacity = Event.Kind.allKinds.count config.eventFlushInterval = eventFlushInterval ?? Constants.eventFlushInterval - user = LDUser.stub() + context = LDContext.stub() self.eventStubResponseDate = eventStubResponseDate?.adjustedForHttpUrlHeaderUse serviceMock = DarklyServiceMock() @@ -46,7 +46,7 @@ final class EventReporterSpec: QuickSpec { self.lastEventResponseDate = lastEventResponseDate?.adjustedForHttpUrlHeaderUse eventReporter = EventReporter(service: serviceMock, onSyncComplete: onSyncComplete) (0.. Date: Wed, 1 Jun 2022 15:49:48 -0400 Subject: [PATCH 57/90] Fix majority of tests (#207) --- .swiftlint.yml | 4 +++- ContractTests/.swiftlint.yml | 4 +++- ContractTests/testharness-suppressions.txt | 8 -------- .../Models/ConnectionInformation.swift | 2 +- .../Models/Context/LDContext.swift | 20 +++++++++++-------- .../Networking/DarklyService.swift | 8 ++++---- .../ObjectiveC/ObjcLDConfig.swift | 4 ++-- .../ObjectiveC/ObjcLDEvaluationDetail.swift | 8 ++++---- .../ServiceObjects/ClientServiceFactory.swift | 10 +++++----- .../ServiceObjects/FlagSynchronizer.swift | 10 +++++----- .../Mocks/ClientServiceMockFactory.swift | 14 ++++++------- .../Mocks/DarklyServiceMock.swift | 8 ++++---- 12 files changed, 50 insertions(+), 50 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 59f5d076..e30f6a06 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -2,7 +2,6 @@ disabled_rules: - line_length - - trailing_whitespace opt_in_rules: - contains_over_filter_count @@ -57,6 +56,9 @@ identifier_name: - lhs - rhs +trailing_whitespace: + severity: error + missing_docs: error: - open diff --git a/ContractTests/.swiftlint.yml b/ContractTests/.swiftlint.yml index 888582b3..c8effa12 100644 --- a/ContractTests/.swiftlint.yml +++ b/ContractTests/.swiftlint.yml @@ -1,6 +1,5 @@ disabled_rules: - line_length - - trailing_whitespace opt_in_rules: - contains_over_filter_count @@ -53,4 +52,7 @@ identifier_name: - lhs - rhs +trailing_whitespace: + severity: error + reporter: "xcode" diff --git a/ContractTests/testharness-suppressions.txt b/ContractTests/testharness-suppressions.txt index 78c48e6c..1e082e3f 100644 --- a/ContractTests/testharness-suppressions.txt +++ b/ContractTests/testharness-suppressions.txt @@ -1,9 +1 @@ -streaming/requests/context properties/single kind with all attributes/GET -streaming/requests/context properties/single kind with all attributes/REPORT -streaming/requests/context properties/multi-kind/GET -streaming/requests/context properties/multi-kind/REPORT streaming/updates/flag delete for previously nonexistent flag is applied -polling/requests/context properties/single kind with all attributes/GET -polling/requests/context properties/single kind with all attributes/REPORT -polling/requests/context properties/multi-kind/GET -polling/requests/context properties/multi-kind/REPORT diff --git a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift index c80981b6..dfcf2883 100644 --- a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift +++ b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift @@ -109,7 +109,7 @@ public struct ConnectionInformation: Codable, CustomStringConvertible { connectionInformationVar.lastFailedConnection = Date() return connectionInformationVar } - + // This function is used to ensure we switch from establishing a streaming connection to streaming once we are connected. static func checkEstablishingStreaming(connectionInformation: ConnectionInformation) -> ConnectionInformation { var connectionInformationVar = connectionInformation diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 8bb9fb88..024eab18 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -93,7 +93,7 @@ public struct LDContext: Encodable, Equatable { } } - static private func encodeSingleContext(container: inout KeyedEncodingContainer, context: LDContext, discardKind: Bool, allAttributesPrivate: Bool, globalPrivateAttributes: SharedDictionary) throws { + static private func encodeSingleContext(container: inout KeyedEncodingContainer, context: LDContext, discardKind: Bool, includePrivateAttributes: Bool, allAttributesPrivate: Bool, globalPrivateAttributes: SharedDictionary) throws { if !discardKind { try container.encodeIfPresent(context.kind.description, forKey: DynamicCodingKeys(string: "kind")) } @@ -114,7 +114,7 @@ public struct LDContext: Encodable, Equatable { var path: [String] = [] path.reserveCapacity(10) - try LDContext.writeFilterAttribute(context: context, container: &container, parentPath: path, key: key, value: value, redactedAttributes: &redactedAttributes, globalPrivateAttributes: globalPrivateAttributes) + try LDContext.writeFilterAttribute(context: context, container: &container, parentPath: path, key: key, value: value, redactedAttributes: &redactedAttributes, includePrivateAttributes: includePrivateAttributes, globalPrivateAttributes: globalPrivateAttributes) } } @@ -129,11 +129,11 @@ public struct LDContext: Encodable, Equatable { } } - static private func writeFilterAttribute(context: LDContext, container: inout KeyedEncodingContainer, parentPath: [String], key: String, value: LDValue, redactedAttributes: inout [String], globalPrivateAttributes: SharedDictionary) throws { + static private func writeFilterAttribute(context: LDContext, container: inout KeyedEncodingContainer, parentPath: [String], key: String, value: LDValue, redactedAttributes: inout [String], includePrivateAttributes: Bool, globalPrivateAttributes: SharedDictionary) throws { var path = parentPath path.append(key.description) - let (isReacted, nestedPropertiesAreRedacted) = LDContext.maybeRedact(context: context, parentPath: path, value: value, redactedAttributes: &redactedAttributes, globalPrivateAttributes: globalPrivateAttributes) + let (isReacted, nestedPropertiesAreRedacted) = includePrivateAttributes ? (false, false) : LDContext.maybeRedact(context: context, parentPath: path, value: value, redactedAttributes: &redactedAttributes, globalPrivateAttributes: globalPrivateAttributes) switch value { case .object(_) where isReacted: @@ -147,7 +147,7 @@ public struct LDContext: Encodable, Equatable { // TODO(mmk): This might be a problem. We might write a sub container even if all the attributes are completely filtered out. var subContainer = container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: DynamicCodingKeys(string: key)) for (key, value) in objectMap { - try writeFilterAttribute(context: context, container: &subContainer, parentPath: path, key: key, value: value, redactedAttributes: &redactedAttributes, globalPrivateAttributes: globalPrivateAttributes) + try writeFilterAttribute(context: context, container: &subContainer, parentPath: path, key: key, value: value, redactedAttributes: &redactedAttributes, includePrivateAttributes: includePrivateAttributes, globalPrivateAttributes: globalPrivateAttributes) } case _ where !isReacted: try container.encode(value, forKey: DynamicCodingKeys(string: key)) @@ -217,19 +217,23 @@ public struct LDContext: Encodable, Equatable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: DynamicCodingKeys.self) + 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? [Reference] ?? [] - let globalDictionary = LDContext.makePrivateAttributeLookupData(references: globalPrivateAttributes) + + let allPrivate = !includePrivateAttributes && allAttributesPrivate + let globalPrivate = includePrivateAttributes ? [] : globalPrivateAttributes + let globalDictionary = LDContext.makePrivateAttributeLookupData(references: globalPrivate) if isMulti() { try container.encodeIfPresent(kind.description, forKey: DynamicCodingKeys(string: "kind")) for context in contexts { var contextContainer = container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: DynamicCodingKeys(string: context.kind.description)) - try LDContext.encodeSingleContext(container: &contextContainer, context: context, discardKind: true, allAttributesPrivate: allAttributesPrivate, globalPrivateAttributes: globalDictionary) + try LDContext.encodeSingleContext(container: &contextContainer, context: context, discardKind: true, includePrivateAttributes: includePrivateAttributes, allAttributesPrivate: allPrivate, globalPrivateAttributes: globalDictionary) } } else { - try LDContext.encodeSingleContext(container: &container, context: self, discardKind: false, allAttributesPrivate: allAttributesPrivate, globalPrivateAttributes: globalDictionary) + try LDContext.encodeSingleContext(container: &container, context: self, discardKind: false, includePrivateAttributes: includePrivateAttributes, allAttributesPrivate: allPrivate, globalPrivateAttributes: globalDictionary) } } diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 017a2314..62567f63 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -82,7 +82,7 @@ final class DarklyService: DarklyServiceProvider { func getFeatureFlags(useReport: Bool, completion: ServiceCompletionHandler?) { guard hasMobileKey(#function) else { return } let encoder = JSONEncoder() - encoder.userInfo[LDUser.UserInfoKeys.includePrivateAttributes] = true + encoder.userInfo[LDContext.UserInfoKeys.includePrivateAttributes] = true guard let contextJsonData = try? encoder.encode(context) else { Log.debug(typeName(and: #function, appending: ": ") + "Aborting. Unable to create flagRequest.") @@ -144,11 +144,11 @@ final class DarklyService: DarklyServiceProvider { // MARK: Streaming - func createEventSource(useReport: Bool, - handler: EventHandler, + func createEventSource(useReport: Bool, + handler: EventHandler, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { let encoder = JSONEncoder() - encoder.userInfo[LDUser.UserInfoKeys.includePrivateAttributes] = true + encoder.userInfo[LDContext.UserInfoKeys.includePrivateAttributes] = true let userJsonData = try? encoder.encode(context) var streamRequestUrl = config.streamUrl.appendingPathComponent(StreamRequestPath.meval) diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index ba55cc0b..c3d7f05f 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -8,7 +8,7 @@ import Foundation @objc(LDConfig) public final class ObjcLDConfig: NSObject { var config: 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. @objc public var mobileKey: String { get { config.mobileKey } @@ -189,7 +189,7 @@ public final class ObjcLDConfig: NSObject { @objc public func setSecondaryMobileKeys(_ keys: [String: String]) throws { 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. @objc public init(mobileKey: String) { config = LDConfig(mobileKey: mobileKey) diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift index 3088f09a..32dc672a 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift @@ -9,7 +9,7 @@ public final class ObjcLDBoolEvaluationDetail: NSObject { @objc public let variationIndex: Int /// 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: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 @@ -26,7 +26,7 @@ public final class ObjcLDDoubleEvaluationDetail: NSObject { @objc public let variationIndex: Int /// 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: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 @@ -43,7 +43,7 @@ public final class ObjcLDIntegerEvaluationDetail: NSObject { @objc public let variationIndex: Int /// 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: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 @@ -60,7 +60,7 @@ public final class ObjcLDStringEvaluationDetail: NSObject { @objc public let variationIndex: Int /// 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: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index a999f71a..585baaa9 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -65,11 +65,11 @@ final class ClientServiceFactory: ClientServiceCreating { EventReporter(service: service, onSyncComplete: onSyncComplete) } - func makeStreamingProvider(url: URL, - httpHeaders: [String: String], + func makeStreamingProvider(url: URL, + httpHeaders: [String: String], connectMethod: String, - connectBody: Data?, - handler: EventHandler, + connectBody: Data?, + handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { var config: EventSource.Config = EventSource.Config(handler: handler, url: url) @@ -92,7 +92,7 @@ final class ClientServiceFactory: ClientServiceCreating { func makeThrottler(environmentReporter: EnvironmentReporting) -> Throttling { Throttler(environmentReporter: environmentReporter) } - + func makeConnectionInformation() -> ConnectionInformation { ConnectionInformation(currentConnectionMode: .offline, lastConnectionFailureReason: .none) } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift index 6789c7c6..d4bbc42d 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift @@ -78,13 +78,13 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { private var isOnlineQueue = DispatchQueue(label: "com.launchdarkly.FlagSynchronizer.isOnlineQueue") let pollingInterval: TimeInterval let useReport: Bool - + private var syncQueue = DispatchQueue(label: Constants.queueName, qos: .utility) private var eventSourceStarted: Date? - init(streamingMode: LDStreamingMode, - pollingInterval: TimeInterval, - useReport: Bool, + init(streamingMode: LDStreamingMode, + pollingInterval: TimeInterval, + useReport: Bool, service: DarklyServiceProvider, onSyncComplete: FlagSyncCompleteClosure?) { Log.debug(FlagSynchronizer.typeName(and: #function) + "streamingMode: \(streamingMode), " + "pollingInterval: \(pollingInterval), " + "useReport: \(useReport)") @@ -226,7 +226,7 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { onSyncComplete(result) } } - + // sourcery: noMock deinit { onSyncComplete = nil diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index bda02d4f..d41e56ee 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -75,12 +75,12 @@ final class ClientServiceMockFactory: ClientServiceCreating { } var makeStreamingProviderCallCount = 0 - var makeStreamingProviderReceivedArguments: (url: URL, - httpHeaders: [String: String], - connectMethod: String?, - connectBody: Data?, - handler: EventHandler, - delegate: RequestHeaderTransform?, + var makeStreamingProviderReceivedArguments: (url: URL, + httpHeaders: [String: String], + connectMethod: String?, + connectBody: Data?, + handler: EventHandler, + delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?)? func makeStreamingProvider(url: URL, httpHeaders: [String: String], connectMethod: String, connectBody: Data?, handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { makeStreamingProviderCallCount += 1 @@ -116,7 +116,7 @@ final class ClientServiceMockFactory: ClientServiceCreating { } return throttlingMock } - + func makeConnectionInformation() -> ConnectionInformation { ConnectionInformation(currentConnectionMode: .offline, lastConnectionFailureReason: .none) } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index a1d15c6c..32502f73 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -63,7 +63,7 @@ final class DarklyServiceMock: DarklyServiceProvider { static let trackEvents = true static let debugEventsUntilDate = Date().addingTimeInterval(30.0) static let reason: [String: LDValue] = ["kind": "OFF"] - + static func stubFeatureFlags(debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0)) -> [LDFlagKey: FeatureFlag] { let flagKeys = FlagKeys.knownFlags let featureFlagTuples = flagKeys.map { flagKey in @@ -103,7 +103,7 @@ final class DarklyServiceMock: DarklyServiceProvider { self.config = config self.context = context } - + var stubbedFlagResponse: ServiceResponse? var getFeatureFlagsUseReportCalledValue = [Bool]() var getFeatureFlagsCallCount = 0 @@ -117,7 +117,7 @@ final class DarklyServiceMock: DarklyServiceProvider { func clearFlagResponseCache() { clearFlagResponseCacheCallCount += 1 } - + var createdEventSource: DarklyStreamingProviderMock? var createEventSourceCallCount = 0 var createEventSourceReceivedUseReport: Bool? @@ -132,7 +132,7 @@ final class DarklyServiceMock: DarklyServiceProvider { createdEventSource = mock return mock } - + var stubbedEventResponse: ServiceResponse? var publishEventDataCallCount = 0 var publishedEventData: Data? From 04520ba31eec923fd1b5042e169e2022029101bd Mon Sep 17 00:00:00 2001 From: Louis Chan <91093020+louis-launchdarkly@users.noreply.github.com> Date: Thu, 2 Jun 2022 14:21:29 -0700 Subject: [PATCH 58/90] Adding a new Circle CI test case for the newer Xcode 13.3.1 (#205) * Add the new build and see what happens * Use the available simulator --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9ab3bb79..bf73b43d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -164,6 +164,10 @@ workflows: build: jobs: + - build: + name: Xcode 13.3 - Swift 5.6 + xcode-version: '13.3.1' + ios-sim: 'platform=iOS Simulator,name=iPhone 13,OS=15.4' - build: name: Xcode 13.1 - Swift 5.5 xcode-version: '13.1.0' From 258f1467adc97015f998433ed08b589b097dde70 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Thu, 9 Jun 2022 12:53:54 -0400 Subject: [PATCH 59/90] Update cache to use new hashing key format (#208) The canonical key for a new context could theoretically conflict with the key used in previous iterations of the SDK. For example, an org context with a key of `key` and an old user with a key of `org:key` would have their canonical keys collide. In most places this doesn't actually matter, but for the purposes of caching it certainly does. There is a new method on the context struct now which will return a key suitable for hashing purposes. In addition to the above, while working through the code, I realized we no longer need to include device and OS automatically, so that was removed. --- ContractTests/Source/Models/user.swift | 2 +- .../GeneratedCode/mocks.generated.swift | 9 ------ LaunchDarkly/LaunchDarkly/LDClient.swift | 6 ++-- .../Models/Context/LDContext.swift | 10 ++++++- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 7 +---- .../Cache/FeatureFlagCache.swift | 7 ++--- .../ServiceObjects/EnvironmentReporter.swift | 21 ------------- .../LaunchDarklyTests/LDClientSpec.swift | 30 +++++++++---------- .../Mocks/EnvironmentReportingMock.swift | 1 - .../Mocks/LDContextStub.swift | 6 ---- .../LaunchDarklyTests/Mocks/LDUserStub.swift | 11 +------ .../Models/User/LDUserSpec.swift | 12 +++----- .../Cache/FeatureFlagCacheSpec.swift | 12 ++++---- 13 files changed, 43 insertions(+), 91 deletions(-) diff --git a/ContractTests/Source/Models/user.swift b/ContractTests/Source/Models/user.swift index 7cbba775..bf1e4c85 100644 --- a/ContractTests/Source/Models/user.swift +++ b/ContractTests/Source/Models/user.swift @@ -6,7 +6,7 @@ extension LDUser: Decodable { /// 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 = "privateAttributeNames", secondary + case key, name, firstName, lastName, country, ipAddress = "ip", email, avatar, custom, isAnonymous = "anonymous", privateAttributes = "privateAttributeNames", secondary } public init(from decoder: Decoder) throws { diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index e29d96c8..5c5a1da0 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -128,15 +128,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - var deviceModelSetCount = 0 - var setDeviceModelCallback: (() throws -> Void)? - var deviceModel: String = Constants.deviceModel { - didSet { - deviceModelSetCount += 1 - try! setDeviceModelCallback?() - } - } - var systemVersionSetCount = 0 var setSystemVersionCallback: (() throws -> Void)? var systemVersion: String = Constants.systemVersion { diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 99c50059..55357be2 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -295,7 +295,7 @@ public class LDClient { let wasOnline = self.isOnline self.internalSetOnline(false) - let cachedContextFlags = self.flagCache.retrieveFeatureFlags(contextKey: self.context.fullyQualifiedKey()) ?? [:] + let cachedContextFlags = self.flagCache.retrieveFeatureFlags(contextKey: self.context.fullyQualifiedHashedKey()) ?? [:] flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedContextFlags)) self.service.context = self.context self.service.clearFlagResponseCache() @@ -504,7 +504,7 @@ public class LDClient { private func updateCacheAndReportChanges(context: LDContext, oldFlags: [LDFlagKey: FeatureFlag]) { - flagCache.storeFeatureFlags(flagStore.featureFlags, contextKey: context.fullyQualifiedKey(), lastUpdated: Date()) + flagCache.storeFeatureFlags(flagStore.featureFlags, contextKey: context.fullyQualifiedHashedKey(), lastUpdated: Date()) flagChangeNotifier.notifyObservers(oldFlags: oldFlags, newFlags: flagStore.featureFlags) } @@ -733,7 +733,7 @@ public class LDClient { onSyncComplete: onFlagSyncComplete) Log.level = environmentReporter.isDebugBuild && config.isDebugMode ? .debug : .noLogging - if let cachedFlags = flagCache.retrieveFeatureFlags(contextKey: context.fullyQualifiedKey()), !cachedFlags.isEmpty { + if let cachedFlags = flagCache.retrieveFeatureFlags(contextKey: context.fullyQualifiedHashedKey()), !cachedFlags.isEmpty { flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedFlags)) } diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 024eab18..72b723fb 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -304,6 +304,14 @@ public struct LDContext: Encodable, Equatable { return canonicalizedKey } + func fullyQualifiedHashedKey() -> String { + if kind.isUser() { + return Util.sha256base64(fullyQualifiedKey()) + } + + return Util.sha256base64(fullyQualifiedKey()) + "$" + } + /// TKTK public func isMulti() -> Bool { return self.kind.isMulti() @@ -563,7 +571,7 @@ extension LDContext: Decodable { } enum UserCodingKeys: String, CodingKey { - case key, name, firstName, lastName, country, ip, email, avatar, custom, isAnonymous = "anonymous", device, operatingSystem = "os", config, privateAttributeNames, secondary + case key, name, firstName, lastName, country, ip, email, avatar, custom, isAnonymous = "anonymous", privateAttributeNames, secondary } } diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 3f4552bd..1b2db910 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -108,8 +108,6 @@ public struct LDUser: Encodable, Equatable { self.isAnonymousNullable = true } self.custom = custom ?? [:] - self.custom.merge(["device": .string(environmentReporter.deviceModel), - "os": .string(environmentReporter.systemVersion)]) { lhs, _ in lhs } self.privateAttributes = privateAttributes ?? [] Log.debug(typeName(and: #function) + "user: \(self)") } @@ -118,10 +116,7 @@ public struct LDUser: Encodable, Equatable { Internal initializer that accepts an environment reporter, used for testing */ init(environmentReporter: EnvironmentReporting) { - self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), - custom: ["device": .string(environmentReporter.deviceModel), - "os": .string(environmentReporter.systemVersion)], - isAnonymous: true) + self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), isAnonymous: true) } private func value(for attribute: UserAttribute) -> Any? { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift index f792e31b..94c91251 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -25,7 +25,7 @@ final class FeatureFlagCache: FeatureFlagCaching { } func retrieveFeatureFlags(contextKey: String) -> [LDFlagKey: FeatureFlag]? { - guard let cachedData = keyedValueCache.data(forKey: "flags-\(Util.sha256base64(contextKey))"), + guard let cachedData = keyedValueCache.data(forKey: "flags-\(contextKey)"), let cachedFlags = try? JSONDecoder().decode(FeatureFlagCollection.self, from: cachedData) else { return nil } return cachedFlags.flags @@ -35,14 +35,13 @@ final class FeatureFlagCache: FeatureFlagCaching { guard self.maxCachedUsers != 0, let encoded = try? JSONEncoder().encode(featureFlags) else { return } - let contextSha = Util.sha256base64(contextKey) - self.keyedValueCache.set(encoded, forKey: "flags-\(contextSha)") + self.keyedValueCache.set(encoded, forKey: "flags-\(contextKey)") var cachedUsers: [String: Int64] = [:] if let cacheMetadata = self.keyedValueCache.data(forKey: "cached-users") { cachedUsers = (try? JSONDecoder().decode([String: Int64].self, from: cacheMetadata)) ?? [:] } - cachedUsers[contextSha] = lastUpdated.millisSince1970 + cachedUsers[contextKey] = 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 diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift index 2f88c5ee..a352083c 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -38,8 +38,6 @@ protocol EnvironmentReporting { var isDebugBuild: Bool { get } // sourcery: defaultMockValue = Constants.deviceType var deviceType: String { get } - // sourcery: defaultMockValue = Constants.deviceModel - var deviceModel: String { get } // sourcery: defaultMockValue = Constants.systemVersion var systemVersion: String { get } // sourcery: defaultMockValue = Constants.systemName @@ -69,25 +67,6 @@ struct EnvironmentReporter: EnvironmentReporting { fileprivate static let simulatorModelIdentifier = "SIMULATOR_MODEL_IDENTIFIER" } - var deviceModel: String { - #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 - 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. - var systemInfo = utsname() - _ = uname(&systemInfo) - guard let deviceModel = String(bytes: Data(bytes: &systemInfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii) - else { - return deviceType - } - return deviceModel.trimmingCharacters(in: .controlCharacters) - #endif - } - #if os(iOS) var deviceType: String { UIDevice.current.model } var systemVersion: String { UIDevice.current.systemVersion } diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 495b48e3..737a8fea 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -104,7 +104,7 @@ final class LDClientSpec: QuickSpec { } func withCached(flags: [LDFlagKey: FeatureFlag]?) -> TestContext { - withCached(contextKey: context.fullyQualifiedKey(), flags: flags) + withCached(contextKey: context.fullyQualifiedHashedKey(), flags: flags) } func withCached(contextKey: String, flags: [LDFlagKey: FeatureFlag]?) -> TestContext { @@ -199,7 +199,7 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 @@ -242,7 +242,7 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 @@ -284,7 +284,7 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 // called on init and subsequent identify - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 2 // both start and internalIdentify @@ -316,7 +316,7 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.subject.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.subject.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 @@ -335,7 +335,7 @@ final class LDClientSpec: QuickSpec { withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == cachedFlags @@ -348,7 +348,7 @@ final class LDClientSpec: QuickSpec { withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.flagStoreMock.replaceStoreCallCount) == 0 @@ -482,7 +482,7 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 @@ -521,7 +521,7 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 @@ -553,7 +553,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.flagSynchronizer.isOnline) == true expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == newContext.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == newContext.fullyQualifiedHashedKey() expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } @@ -575,14 +575,14 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.flagSynchronizer.isOnline) == false expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == newContext.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == newContext.fullyQualifiedHashedKey() expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } it("when the new user has cached feature flags") { let stubFlags = FlagMaintainingMock.stubFlags() let newContext = LDContext.stub() - let testContext = TestContext().withCached(contextKey: newContext.fullyQualifiedKey(), flags: stubFlags) + let testContext = TestContext().withCached(contextKey: newContext.fullyQualifiedHashedKey(), flags: stubFlags) testContext.start() testContext.featureFlagCachingMock.reset() @@ -877,7 +877,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 @@ -904,7 +904,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 @@ -931,7 +931,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedKey() + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift index 359141ec..e933cad2 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift @@ -2,7 +2,6 @@ import Foundation extension EnvironmentReportingMock { struct Constants { - static let deviceModel = "deviceModelStub" static let deviceType = "deviceTypeStub" static let systemVersion = "systemVersionStub" static let systemName = "systemNameStub" diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift index ee0a87b5..382fd2df 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift @@ -15,8 +15,6 @@ extension LDContext { static let ipAddress: LDValue = "stub.user.ipAddress" static let email: LDValue = "stub.user@email.com" static let avatar: LDValue = "stub.user.avatar" - 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, @@ -44,11 +42,7 @@ extension LDContext { builder.trySetValue(key, value) } - builder.trySetValue("device", StubConstants.device) - builder.trySetValue("os", StubConstants.operatingSystem) - var context: LDContext? = nil - if case .success(let ctx) = builder.build() { context = ctx } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift index 943be88c..c4a1b558 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift @@ -22,15 +22,6 @@ extension LDUser { "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: LDValue] { - var custom = StubConstants.custom - if includeSystemValues { - custom["device"] = StubConstants.device - custom["os"] = StubConstants.operatingSystem - } - return custom - } } static func stub(key: String? = nil, @@ -43,7 +34,7 @@ extension LDUser { ipAddress: StubConstants.ipAddress, email: StubConstants.email, avatar: StubConstants.avatar, - custom: StubConstants.custom(includeSystemValues: true), + custom: StubConstants.custom, isAnonymous: StubConstants.isAnonymous, secondary: StubConstants.secondary) return user diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 98a641f7..fe1ddd89 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -26,7 +26,7 @@ final class LDUserSpec: QuickSpec { ipAddress: LDUser.StubConstants.ipAddress, email: LDUser.StubConstants.email, avatar: LDUser.StubConstants.avatar, - custom: LDUser.StubConstants.custom(includeSystemValues: true), + custom: LDUser.StubConstants.custom, isAnonymous: LDUser.StubConstants.isAnonymous, privateAttributes: LDUser.optionalAttributes, secondary: LDUser.StubConstants.secondary) @@ -41,7 +41,7 @@ final class LDUserSpec: QuickSpec { 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.custom == LDUser.StubConstants.custom).to(beTrue()) expect(user.privateAttributes) == LDUser.optionalAttributes } it("without setting anonymous") { @@ -67,9 +67,7 @@ final class LDUserSpec: QuickSpec { expect(user.ipAddress).to(beNil()) expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) - expect(user.custom.count) == 2 - expect(user.custom["device"]) == .string(environmentReporter.deviceModel) - expect(user.custom["os"]) == .string(environmentReporter.systemVersion) + expect(user.custom.count) == 0 expect(user.privateAttributes).to(beEmpty()) expect(user.secondary).to(beNil()) } @@ -114,9 +112,7 @@ final class LDUserSpec: QuickSpec { expect(user.ipAddress).to(beNil()) expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) - expect(user.custom.count) == 2 - expect(user.custom["device"]) == .string(environmentReporter.deviceModel) - expect(user.custom["os"]) == .string(environmentReporter.systemVersion) + expect(user.custom.count) == 0 expect(user.privateAttributes).to(beEmpty()) } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift index 868c97de..6196d8b4 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift @@ -29,7 +29,7 @@ final class FeatureFlagCacheSpec: XCTestCase { let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) XCTAssertNil(flagCache.retrieveFeatureFlags(contextKey: "user1")) XCTAssertEqual(mockValueCache.dataCallCount, 1) - XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-\(Util.sha256base64("user1"))") + XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-user1") } func testRetrieveInvalidData() { @@ -50,7 +50,7 @@ final class FeatureFlagCacheSpec: XCTestCase { let retrieved = flagCache.retrieveFeatureFlags(contextKey: "user1") XCTAssertEqual(retrieved, testFlagCollection.flags) XCTAssertEqual(mockValueCache.dataCallCount, 1) - XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-\(Util.sha256base64("user1"))") + XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-user1") } func testStoreCacheDisabled() { @@ -77,7 +77,7 @@ final class FeatureFlagCacheSpec: XCTestCase { } } let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: -1) - flagCache.storeFeatureFlags([:], contextKey: "user1", lastUpdated: now) + flagCache.storeFeatureFlags([:], contextKey: hashedUserKey, lastUpdated: now) XCTAssertEqual(count, 3) } @@ -98,7 +98,7 @@ final class FeatureFlagCacheSpec: XCTestCase { 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, contextKey: "user1", lastUpdated: now) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedUserKey, lastUpdated: now) XCTAssertEqual(mockValueCache.removeObjectCallCount, 1) XCTAssertEqual(mockValueCache.removeObjectReceivedForKey, "flags-key1") let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) @@ -116,7 +116,7 @@ final class FeatureFlagCacheSpec: XCTestCase { let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) var removedObjects: [String] = [] mockValueCache.removeObjectCallback = { removedObjects.append(self.mockValueCache.removeObjectReceivedForKey!) } - flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: "user1", lastUpdated: later) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedUserKey, lastUpdated: later) XCTAssertEqual(mockValueCache.removeObjectCallCount, 2) XCTAssertTrue(removedObjects.contains("flags-key1")) XCTAssertTrue(removedObjects.contains("flags-key2")) @@ -129,7 +129,7 @@ final class FeatureFlagCacheSpec: XCTestCase { let now = Date() mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": "123"]) let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: "user1", lastUpdated: now) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedUserKey, lastUpdated: now) XCTAssertEqual(mockValueCache.removeObjectCallCount, 0) let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) XCTAssertEqual(setMetadata, [hashedUserKey: now.millisSince1970]) From cf8304730ca7e5e95662a9b2bbd79dff0575d0c0 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Thu, 9 Jun 2022 12:54:27 -0400 Subject: [PATCH 60/90] Add tombstone support (#209) Prior to this commit, when a flag was deleted, it was actually removed from memory. This allows for the (admittedly unlikely) situation where an older version of a flag update would be received after this deletion, causing the flag to become "undeleted". By managing a light-weight placeholder, the tombstone ensures we do not allow dead flags to resurrect unless directed to by a later version of the flag. --- .circleci/config.yml | 2 +- ContractTests/Package.swift | 4 +- .../Source/Controllers/SdkController.swift | 238 +++++++++--------- ContractTests/Source/app.swift | 14 -- ContractTests/Source/boot.swift | 6 - ContractTests/Source/configure.swift | 14 -- ContractTests/Source/main.swift | 8 +- ContractTests/Source/routes.swift | 10 +- ContractTests/testharness-suppressions.txt | 1 - .../GeneratedCode/mocks.generated.swift | 19 +- LaunchDarkly/LaunchDarkly/LDClient.swift | 76 +++--- .../Models/FeatureFlag/FeatureFlag.swift | 12 + .../LaunchDarkly/Models/LDConfig.swift | 2 +- .../ServiceObjects/Cache/CacheConverter.swift | 31 ++- .../Cache/FeatureFlagCache.swift | 12 +- .../Cache/KeyedValueCache.swift | 5 + .../ServiceObjects/FlagStore.swift | 98 ++++++-- .../LaunchDarklyTests/LDClientSpec.swift | 58 ++--- .../Mocks/FlagMaintainingMock.swift | 27 +- .../Models/FeatureFlag/FeatureFlagSpec.swift | 41 +++ .../Networking/DarklyServiceSpec.swift | 28 +-- .../Cache/CacheConverterSpec.swift | 4 +- .../Cache/FeatureFlagCacheSpec.swift | 5 +- .../ServiceObjects/FlagStoreSpec.swift | 78 ++++-- 24 files changed, 473 insertions(+), 320 deletions(-) delete mode 100644 ContractTests/Source/app.swift delete mode 100644 ContractTests/Source/boot.swift delete mode 100644 ContractTests/Source/configure.swift diff --git a/.circleci/config.yml b/.circleci/config.yml index bf73b43d..b718056e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 jobs: contract-tests: macos: - xcode: '13.1.0' + xcode: '13.4.1' steps: - checkout diff --git a/ContractTests/Package.swift b/ContractTests/Package.swift index 2bf68a23..324b05e4 100644 --- a/ContractTests/Package.swift +++ b/ContractTests/Package.swift @@ -6,7 +6,7 @@ let package = Package( name: "ContractTests", platforms: [ .iOS(.v10), - .macOS(.v10_12), + .macOS(.v10_15), .watchOS(.v3), .tvOS(.v10) ], @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ Package.Dependency.package(name: "LaunchDarkly", path: ".."), - .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0") + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0") ], targets: [ .target( diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index 59c1b278..132a6d73 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -1,10 +1,20 @@ import Vapor import LaunchDarkly -final class SdkController { +final class SdkController: RouteCollection { private var clients: [Int : LDClient] = [:] private var clientCounter = 0 + func boot(routes: RoutesBuilder) { + routes.get("", use: status) + routes.post("", use: createClient) + routes.delete("", use: shutdown) + + let clientRoutes = routes.grouped("clients") + clientRoutes.post(":id", use: executeCommand) + clientRoutes.delete(":id", use: shutdownClient) + } + func status(_ req: Request) -> StatusResponse { let capabilities = [ "client-side", @@ -18,112 +28,112 @@ final class SdkController { capabilities: capabilities) } - func createClient(_ req: Request) throws -> Future { - return try req.content.decode(CreateInstance.self).map { createInstance in - var config = LDConfig(mobileKey: createInstance.configuration.credential) - config.enableBackgroundUpdates = true - config.isDebugMode = true + func createClient(_ req: Request) throws -> Response { + let createInstance = try req.content.decode(CreateInstance.self) + var config = LDConfig(mobileKey: createInstance.configuration.credential) + config.enableBackgroundUpdates = true + config.isDebugMode = true - if let streaming = createInstance.configuration.streaming { - if let baseUri = streaming.baseUri { - config.streamUrl = URL(string: baseUri)! - } - - // TODO(mmk) Need to hook up initialRetryDelayMs - } else if let polling = createInstance.configuration.polling { - config.streamingMode = .polling - if let baseUri = polling.baseUri { - config.baseUrl = URL(string: baseUri)! - } + if let streaming = createInstance.configuration.streaming { + if let baseUri = streaming.baseUri { + config.streamUrl = URL(string: baseUri)! } - if let events = createInstance.configuration.events { - if let baseUri = events.baseUri { - config.eventsUrl = URL(string: baseUri)! - } + // TODO(mmk) Need to hook up initialRetryDelayMs + } else if let polling = createInstance.configuration.polling { + config.streamingMode = .polling + if let baseUri = polling.baseUri { + config.baseUrl = URL(string: baseUri)! + } + } - if let capacity = events.capacity { - config.eventCapacity = capacity - } + if let events = createInstance.configuration.events { + if let baseUri = events.baseUri { + config.eventsUrl = URL(string: baseUri)! + } - if let enable = events.enableDiagnostics { - config.diagnosticOptOut = !enable - } + if let capacity = events.capacity { + config.eventCapacity = capacity + } - if let allPrivate = events.allAttributesPrivate { - config.allUserAttributesPrivate = allPrivate - } + if let enable = events.enableDiagnostics { + config.diagnosticOptOut = !enable + } - if let globalPrivate = events.globalPrivateAttributes { - config.privateUserAttributes = globalPrivate.map({ UserAttribute.forName($0) }) - } + if let allPrivate = events.allAttributesPrivate { + config.allUserAttributesPrivate = allPrivate + } - if let flushIntervalMs = events.flushIntervalMs { - config.eventFlushInterval = flushIntervalMs - } + if let globalPrivate = events.globalPrivateAttributes { + config.privateUserAttributes = globalPrivate.map({ UserAttribute.forName($0) }) + } - if let inlineUsers = events.inlineUsers { - config.inlineUserInEvents = inlineUsers - } + if let flushIntervalMs = events.flushIntervalMs { + config.eventFlushInterval = flushIntervalMs } - if let tags = createInstance.configuration.tags { - var applicationInfo = ApplicationInfo() - if let id = tags.applicationId { - applicationInfo.applicationIdentifier(id) - } + if let inlineUsers = events.inlineUsers { + config.inlineUserInEvents = inlineUsers + } + } - if let verision = tags.applicationVersion { - applicationInfo.applicationVersion(verision) - } + if let tags = createInstance.configuration.tags { + var applicationInfo = ApplicationInfo() + if let id = tags.applicationId { + applicationInfo.applicationIdentifier(id) + } - config.applicationInfo = applicationInfo + if let verision = tags.applicationVersion { + applicationInfo.applicationVersion(verision) } - let clientSide = createInstance.configuration.clientSide + config.applicationInfo = applicationInfo + } - if let autoAliasingOptOut = clientSide.autoAliasingOptOut { - config.autoAliasingOptOut = autoAliasingOptOut - } + let clientSide = createInstance.configuration.clientSide - if let evaluationReasons = clientSide.evaluationReasons { - config.evaluationReasons = evaluationReasons - } + if let autoAliasingOptOut = clientSide.autoAliasingOptOut { + config.autoAliasingOptOut = autoAliasingOptOut + } - if let useReport = clientSide.useReport { - config.useReport = useReport - } + if let evaluationReasons = clientSide.evaluationReasons { + config.evaluationReasons = evaluationReasons + } - let dispatchSemaphore = DispatchSemaphore(value: 0) - let startWaitSeconds = (createInstance.configuration.startWaitTimeMs ?? 5_000) / 1_000 + if let useReport = clientSide.useReport { + config.useReport = useReport + } - LDClient.start(config:config, user: clientSide.initialUser, startWaitSeconds: startWaitSeconds) { timedOut in - dispatchSemaphore.signal() - } + let dispatchSemaphore = DispatchSemaphore(value: 0) + let startWaitSeconds = (createInstance.configuration.startWaitTimeMs ?? 5_000) / 1_000 - dispatchSemaphore.wait() + LDClient.start(config:config, user: clientSide.initialUser, startWaitSeconds: startWaitSeconds) { timedOut in + dispatchSemaphore.signal() + } - let client = LDClient.get()! + dispatchSemaphore.wait() - self.clientCounter += 1 - self.clients.updateValue(client, forKey: self.clientCounter) + let client = LDClient.get()! - var headers = HTTPHeaders() - headers.add(name: "Location", value: "/clients/\(self.clientCounter)") + self.clientCounter += 1 + self.clients.updateValue(client, forKey: self.clientCounter) - var response = HTTPResponse() - response.status = .ok - response.headers = headers + var headers = HTTPHeaders() + headers.add(name: "Location", value: "/clients/\(self.clientCounter)") - return response - } + let response = Response() + response.status = .ok + response.headers = headers + + return response } func shutdownClient(_ req: Request) throws -> HTTPStatus { - let id = try req.parameters.next(Int.self) - guard let client = self.clients[id] else { - return HTTPStatus.badRequest - } + guard let id = req.parameters.get("id", as: Int.self) + else { throw Abort(.badRequest) } + + guard let client = self.clients[id] + else { return HTTPStatus.badRequest } client.close() clients.removeValue(forKey: id) @@ -131,38 +141,40 @@ final class SdkController { return HTTPStatus.accepted } - func executeCommand(_ req: Request) throws -> Future { - return try req.content.decode(CommandParameters.self).map { commandParameters in - guard let client = self.clients[self.clientCounter] else { - throw Abort(.badRequest) - } - - switch commandParameters.command { - case "evaluate": - let result: EvaluateFlagResponse = try self.evaluate(client, commandParameters.evaluate!) - return CommandResponse.evaluateFlag(result) - case "evaluateAll": - let result: EvaluateAllFlagsResponse = try self.evaluateAll(client, commandParameters.evaluateAll!) - return CommandResponse.evaluateAll(result) - case "identifyEvent": - let semaphore = DispatchSemaphore(value: 0) - client.identify(user: commandParameters.identifyEvent!.user) { - semaphore.signal() - } - semaphore.wait() - case "aliasEvent": - client.alias(context: commandParameters.aliasEvent!.user, previousContext: commandParameters.aliasEvent!.previousUser) - case "customEvent": - let event = commandParameters.customEvent! - client.track(key: event.eventKey, data: event.data, metricValue: event.metricValue) - case "flushEvents": - client.flush() - default: - throw Abort(.badRequest) - } - - return CommandResponse.ok + func executeCommand(_ req: Request) throws -> CommandResponse { + guard let id = req.parameters.get("id", as: Int.self) + else { throw Abort(.badRequest) } + + let commandParameters = try req.content.decode(CommandParameters.self) + guard let client = self.clients[id] else { + throw Abort(.badRequest) } + + switch commandParameters.command { + case "evaluate": + let result: EvaluateFlagResponse = try self.evaluate(client, commandParameters.evaluate!) + return CommandResponse.evaluateFlag(result) + case "evaluateAll": + let result: EvaluateAllFlagsResponse = try self.evaluateAll(client, commandParameters.evaluateAll!) + return CommandResponse.evaluateAll(result) + case "identifyEvent": + let semaphore = DispatchSemaphore(value: 0) + client.identify(user: commandParameters.identifyEvent!.user) { + semaphore.signal() + } + semaphore.wait() + case "aliasEvent": + client.alias(context: commandParameters.aliasEvent!.user, previousContext: commandParameters.aliasEvent!.previousUser) + case "customEvent": + let event = commandParameters.customEvent! + client.track(key: event.eventKey, data: event.data, metricValue: event.metricValue) + case "flushEvents": + client.flush() + default: + throw Abort(.badRequest) + } + + return CommandResponse.ok } func evaluate(_ client: LDClient, _ params: EvaluateFlagParameters) throws -> EvaluateFlagResponse { @@ -177,7 +189,7 @@ final class SdkController { let result = client.boolVariation(forKey: params.flagKey, defaultValue: defaultValue) return EvaluateFlagResponse(value: LDValue.bool(result)) } - throw "Failed to convert \(params.valueType) to bool" + throw Abort(.badRequest, reason: "Failed to convert \(params.valueType) to bool") case "int": if case let LDValue.number(defaultValue) = params.defaultValue { if params.detail { @@ -188,7 +200,7 @@ final class SdkController { let result = client.intVariation(forKey: params.flagKey, defaultValue: Int(defaultValue)) return EvaluateFlagResponse(value: LDValue.number(Double(result))) } - throw "Failed to convert \(params.valueType) to int" + throw Abort(.badRequest, reason: "Failed to convert \(params.valueType) to int") case "double": if case let LDValue.number(defaultValue) = params.defaultValue { if params.detail { @@ -199,7 +211,7 @@ final class SdkController { let result = client.doubleVariation(forKey: params.flagKey, defaultValue: defaultValue) return EvaluateFlagResponse(value: LDValue.number(result), variationIndex: nil, reason: nil) } - throw "Failed to convert \(params.valueType) to bool" + throw Abort(.badRequest, reason: "Failed to convert \(params.valueType) to bool") case "string": if case let LDValue.string(defaultValue) = params.defaultValue { if params.detail { @@ -210,7 +222,7 @@ final class SdkController { let result = client.stringVariation(forKey: params.flagKey, defaultValue: defaultValue) return EvaluateFlagResponse(value: LDValue.string(result), variationIndex: nil, reason: nil) } - throw "Failed to convert \(params.valueType) to string" + throw Abort(.badRequest, reason: "Failed to convert \(params.valueType) to string") default: if params.detail { let result = client.jsonVariationDetail(forKey: params.flagKey, defaultValue: params.defaultValue) diff --git a/ContractTests/Source/app.swift b/ContractTests/Source/app.swift deleted file mode 100644 index 3d24e8a3..00000000 --- a/ContractTests/Source/app.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Vapor - -extension String: Error {} - -public func app(_ env: Environment) throws -> Application { - var config = Config.default() - var env = env - var services = Services.default() - try configure(&config, &env, &services) - let app = try Application(config: config, environment: env, services: services) - try boot(app) - - return app -} diff --git a/ContractTests/Source/boot.swift b/ContractTests/Source/boot.swift deleted file mode 100644 index 9313115b..00000000 --- a/ContractTests/Source/boot.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Vapor - -/// Called after your application has initialized. -public func boot(_ app: Application) throws { - // Your code here -} diff --git a/ContractTests/Source/configure.swift b/ContractTests/Source/configure.swift deleted file mode 100644 index 3c3aca84..00000000 --- a/ContractTests/Source/configure.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Vapor - -/// Called before your application initializes. -public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { - // Register routes to the router - let router = EngineRouter.default() - try routes(router) - services.register(router, as: Router.self) - - // Register middleware - var middlewares = MiddlewareConfig() // Create _empty_ middleware config - middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response - services.register(middlewares) -} diff --git a/ContractTests/Source/main.swift b/ContractTests/Source/main.swift index aa0396e7..011d6f3a 100644 --- a/ContractTests/Source/main.swift +++ b/ContractTests/Source/main.swift @@ -1,9 +1,15 @@ import Foundation +import Vapor let semaphore = DispatchSemaphore(value: 0) DispatchQueue.global(qos: .userInitiated).async { do { - try app(.detect()).run() + var env = try Environment.detect() + try LoggingSystem.bootstrap(from: &env) + let app = Application(env) + defer { app.shutdown() } + try routes(app) + try app.run() } catch { } semaphore.signal() diff --git a/ContractTests/Source/routes.swift b/ContractTests/Source/routes.swift index 33a58076..8d021691 100644 --- a/ContractTests/Source/routes.swift +++ b/ContractTests/Source/routes.swift @@ -1,12 +1,6 @@ import Vapor -public func routes(_ router: Router) throws { +func routes(_ app: Application) throws { let sdkController = SdkController() - router.get("/", use: sdkController.status) - router.post("/", use: sdkController.createClient) - router.delete("/", use: sdkController.shutdown) - - let clientRoutes = router.grouped("clients") - clientRoutes.post(Int.parameter, use: sdkController.executeCommand) - clientRoutes.delete(Int.parameter, use: sdkController.shutdownClient) + try app.register(collection: sdkController) } diff --git a/ContractTests/testharness-suppressions.txt b/ContractTests/testharness-suppressions.txt index 1b99e0d2..87c48761 100644 --- a/ContractTests/testharness-suppressions.txt +++ b/ContractTests/testharness-suppressions.txt @@ -6,7 +6,6 @@ evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/ evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate all flags streaming/requests/user properties/GET streaming/requests/user properties/REPORT -streaming/updates/flag delete for previously nonexistent flag is applied polling/requests/method and headers/GET polling/requests/method and headers/REPORT polling/requests/URL path is computed correctly/base URI has no trailing slash/GET diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index b8ad98cc..7fc8ce00 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -274,8 +274,8 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { var retrieveFeatureFlagsCallCount = 0 var retrieveFeatureFlagsCallback: (() throws -> Void)? var retrieveFeatureFlagsReceivedUserKey: String? - var retrieveFeatureFlagsReturnValue: [LDFlagKey: FeatureFlag]? - func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? { + var retrieveFeatureFlagsReturnValue: StoredItems? + func retrieveFeatureFlags(userKey: String) -> StoredItems? { retrieveFeatureFlagsCallCount += 1 retrieveFeatureFlagsReceivedUserKey = userKey try! retrieveFeatureFlagsCallback?() @@ -284,10 +284,10 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { var storeFeatureFlagsCallCount = 0 var storeFeatureFlagsCallback: (() throws -> Void)? - var storeFeatureFlagsReceivedArguments: (featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date)? - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) { + var storeFeatureFlagsReceivedArguments: (storedItems: StoredItems, userKey: String, lastUpdated: Date)? + func storeFeatureFlags(_ storedItems: StoredItems, userKey: String, lastUpdated: Date) { storeFeatureFlagsCallCount += 1 - storeFeatureFlagsReceivedArguments = (featureFlags: featureFlags, userKey: userKey, lastUpdated: lastUpdated) + storeFeatureFlagsReceivedArguments = (storedItems: storedItems, userKey: userKey, lastUpdated: lastUpdated) try! storeFeatureFlagsCallback?() } } @@ -406,6 +406,15 @@ final class KeyedValueCachingMock: KeyedValueCaching { removeAllCallCount += 1 try! removeAllCallback?() } + + var keysCallCount = 0 + var keysCallback: (() throws -> Void)? + var keysReturnValue: [String]! + func keys() -> [String] { + keysCallCount += 1 + try! keysCallback?() + return keysReturnValue + } } // MARK: - LDFlagSynchronizingMock diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index d7b5fb16..3e5ce38a 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -38,9 +38,9 @@ enum LDClientRunMode { public class LDClient { // MARK: - State Controls and Indicators - + private static var instances: [String: LDClient]? - + /** Reports the online/offline state of the LDClient. @@ -130,7 +130,7 @@ public class LDClient { } private let internalSetOnlineQueue: DispatchQueue = DispatchQueue(label: "InternalSetOnlineQueue") - + private func go(online goOnline: Bool, reasonOnlineUnavailable: String, completion:(() -> Void)?) { let owner = "SetOnlineOwner" as AnyObject var completed = false @@ -262,14 +262,14 @@ public class LDClient { let config: LDConfig let service: DarklyServiceProvider private(set) var user: LDUser - + /** The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. - + 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. - + 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. @@ -295,7 +295,7 @@ public class LDClient { self.internalSetOnline(false) let cachedUserFlags = self.flagCache.retrieveFeatureFlags(userKey: self.user.key) ?? [:] - flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedUserFlags)) + flagStore.replaceStore(newStoredItems: cachedUserFlags) self.service.user = self.user self.service.clearFlagResponseCache() flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), @@ -322,7 +322,7 @@ public class LDClient { public var allFlags: [LDFlagKey: LDValue]? { guard hasStarted else { return nil } - return flagStore.featureFlags.compactMapValues { $0.value } + return flagStore.storedItems.featureFlags.compactMapValues { $0.value } } // MARK: Observing Updates @@ -352,7 +352,7 @@ public class LDClient { Log.debug(typeName(and: #function) + "flagKey: \(key), owner: \(String(describing: owner))") flagChangeNotifier.addFlagChangeObserver(FlagChangeObserver(key: key, owner: owner, flagChangeHandler: handler)) } - + /** 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 [LDFlagKey: LDChangedFlag] containing the old and new flag values. See `LDChangedFlag` for details. @@ -407,7 +407,7 @@ public class LDClient { Log.debug(typeName(and: #function) + " owner: \(String(describing: owner))") flagChangeNotifier.addFlagChangeObserver(FlagChangeObserver(keys: LDFlagKey.anyKey, owner: owner, flagCollectionChangeHandler: handler)) } - + /** Sets a handler executed when a flag update leaves the flags unchanged from their previous values. @@ -434,23 +434,23 @@ public class LDClient { Log.debug(typeName(and: #function) + " owner: \(String(describing: owner))") flagChangeNotifier.addFlagsUnchangedObserver(FlagsUnchangedObserver(owner: owner, flagsUnchangedHandler: handler)) } - + /** Sets a handler executed when ConnectionInformation.currentConnectionMode changes. - + The SDK retains only weak references to owner, which allows the client app to freely destroy change owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. - + The SDK executes handlers on the main thread. - + 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. */ @@ -475,20 +475,20 @@ public class LDClient { Log.debug(typeName(and: #function) + "result: \(result)") switch result { case let .flagCollection(flagCollection): - let oldFlags = flagStore.featureFlags + let oldStoredItems = flagStore.storedItems connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) - flagStore.replaceStore(newFlags: flagCollection) - self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) + flagStore.replaceStore(newStoredItems: StoredItems(items: flagCollection.flags)) + self.updateCacheAndReportChanges(user: self.user, oldStoredItems: oldStoredItems) case let .patch(featureFlag): - let oldFlags = flagStore.featureFlags + let oldStoredItems = flagStore.storedItems connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) flagStore.updateStore(updatedFlag: featureFlag) - self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) + self.updateCacheAndReportChanges(user: self.user, oldStoredItems: oldStoredItems) case let .delete(deleteResponse): - let oldFlags = flagStore.featureFlags + let oldStoredItems = flagStore.storedItems connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) flagStore.deleteFlag(deleteResponse: deleteResponse) - self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) + self.updateCacheAndReportChanges(user: self.user, oldStoredItems: oldStoredItems) case .upToDate: connectionInformation.lastKnownFlagValidity = Date() flagChangeNotifier.notifyUnchanged() @@ -506,9 +506,9 @@ public class LDClient { } private func updateCacheAndReportChanges(user: LDUser, - oldFlags: [LDFlagKey: FeatureFlag]) { - flagCache.storeFeatureFlags(flagStore.featureFlags, userKey: user.key, lastUpdated: Date()) - flagChangeNotifier.notifyObservers(oldFlags: oldFlags, newFlags: flagStore.featureFlags) + oldStoredItems: StoredItems) { + flagCache.storeFeatureFlags(flagStore.storedItems, userKey: user.key, lastUpdated: Date()) + flagChangeNotifier.notifyObservers(oldFlags: oldStoredItems.featureFlags, newFlags: flagStore.storedItems.featureFlags) } // MARK: Events @@ -544,10 +544,10 @@ public class LDClient { /** Tells the SDK to generate an alias event. - Associates two users for analytics purposes. - - This can be helpful in the situation where a person is represented by multiple - LaunchDarkly users. This may happen, for example, when a person initially logs into + Associates two users for analytics purposes. + + This can be helpful in the situation where a person is represented by multiple + LaunchDarkly users. This may happen, for example, when a person initially logs into an application-- the person might be represented by an anonymous user prior to logging in and a different user after logging in, as denoted by a different user key. @@ -574,7 +574,7 @@ public class LDClient { public func flush() { LDClient.instances?.forEach { $1.internalFlush() } } - + private func internalFlush() { eventReporter.flush(completion: nil) } @@ -587,7 +587,7 @@ public class LDClient { Log.debug(typeName(and: #function) + "result: success") } } - + @objc private func didCloseEventSource() { Log.debug(typeName(and: #function)) self.connectionInformation = ConnectionInformation.lastSuccessfulConnectionCheck(connectionInformation: self.connectionInformation) @@ -694,7 +694,7 @@ public class LDClient { } return internalInstances[environment] } - + // MARK: - Private let serviceFactory: ClientServiceCreating @@ -719,7 +719,7 @@ public class LDClient { } private var _initialized = false private var initializedQueue = DispatchQueue(label: "com.launchdarkly.LDClient.initializedQueue") - + private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startUser: LDUser?, completion: (() -> Void)? = nil) { self.serviceFactory = serviceFactory environmentReporter = self.serviceFactory.makeEnvironmentReporter() @@ -739,14 +739,14 @@ public class LDClient { pollingInterval: config.flagPollingInterval(runMode: runMode), useReport: config.useReport, service: service) - + if let backgroundNotification = environmentReporter.backgroundNotification { NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: backgroundNotification, object: nil) } if let foregroundNotification = environmentReporter.foregroundNotification { NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: foregroundNotification, object: nil) } - + NotificationCenter.default.addObserver(self, selector: #selector(didCloseEventSource), name: Notification.Name(FlagSynchronizer.Constants.didCloseEventSourceName), object: nil) eventReporter = self.serviceFactory.makeEventReporter(service: service, onSyncComplete: onEventSyncComplete) @@ -758,7 +758,7 @@ public class LDClient { Log.level = environmentReporter.isDebugBuild && config.isDebugMode ? .debug : .noLogging if let cachedFlags = flagCache.retrieveFeatureFlags(userKey: user.key), !cachedFlags.isEmpty { - flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedFlags)) + flagStore.replaceStore(newStoredItems: cachedFlags) } eventReporter.record(IdentifyEvent(user: user)) diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift index b2357de7..662a4a8b 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift @@ -104,3 +104,15 @@ struct FeatureFlagCollection: Codable { try flags.encode(to: encoder) } } + +struct StoredItemCollection: Codable { + let flags: StoredItems + + init(_ flags: StoredItems) { + self.flags = flags + } + + init(_ collection: FeatureFlagCollection) { + self.flags = StoredItems(items: collection.flags) + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 163f1b8d..16091c9d 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -240,7 +240,7 @@ 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 /// The configuration for application metadata. - public var applicationInfo: ApplicationInfo? = nil + public var applicationInfo: ApplicationInfo? /** Controls the method the SDK uses to keep feature flags updated. (Default: `.streaming`) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index 43067d3e..6e0d7e63 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -91,13 +91,28 @@ final class CacheConverter: CacheConverting { cachedEnvData.forEach { mobileKey, users in users.forEach { userKey, data in - flagCaches[mobileKey]?.storeFeatureFlags(data.flags, userKey: userKey, lastUpdated: data.updated) + flagCaches[mobileKey]?.storeFeatureFlags(StoredItems(items: data.flags), userKey: userKey, lastUpdated: data.updated) } } v6cache.removeObject(forKey: "com.launchDarkly.cachedUserEnvironmentFlags") } + private func convertV7Data(flagCaches: inout [MobileKey: FeatureFlagCaching]) { + for (_, flagCaching) in flagCaches { + flagCaching.keyedValueCache.keys().forEach { key in + guard let cachedData = flagCaching.keyedValueCache.data(forKey: key), + let cachedFlags = try? JSONDecoder().decode(FeatureFlagCollection.self, from: cachedData) + else { return } + + guard let encoded = try? JSONEncoder().encode(StoredItemCollection(cachedFlags)) + else { return } + + flagCaching.keyedValueCache.set(encoded, forKey: key) + } + } + } + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) { var flagCaches: [String: FeatureFlagCaching] = [:] keysToConvert.forEach { mobileKey in @@ -107,17 +122,20 @@ final class CacheConverter: CacheConverting { 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 + cacheVersion == 7 || cacheVersion == 8 else { // Metadata is invalid, remove existing data and attempt migration flagCache.keyedValueCache.removeAll() return } - // Already up to date - flagCaches.removeValue(forKey: mobileKey) + + if cacheVersion == 8 { + // Already up to date + flagCaches.removeValue(forKey: mobileKey) + } } - // Skip migration if all environments are V7 + // Skip migration if all environments are V8 if flagCaches.isEmpty { return } // Remove V5 cache data (migration not supported) @@ -125,9 +143,10 @@ final class CacheConverter: CacheConverting { standardDefaults.removeObject(forKey: "com.launchdarkly.dataManager.userEnvironments") convertV6Data(v6cache: standardDefaults, flagCaches: flagCaches) + convertV7Data(flagCaches: &flagCaches) // Set cache version to skip this logic in the future - if let versionMetadata = try? JSONEncoder().encode(["version": 7]) { + if let versionMetadata = try? JSONEncoder().encode(["version": 8]) { flagCaches.forEach { $0.value.keyedValueCache.set(versionMetadata, forKey: "ld-cache-metadata") } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift index c4720153..a49e0ee9 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -5,8 +5,8 @@ protocol FeatureFlagCaching { // sourcery: defaultMockValue = KeyedValueCachingMock() var keyedValueCache: KeyedValueCaching { get } - func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) + func retrieveFeatureFlags(userKey: String) -> StoredItems? + func storeFeatureFlags(_ storedItems: StoredItems, userKey: String, lastUpdated: Date) } final class FeatureFlagCache: FeatureFlagCaching { @@ -24,15 +24,15 @@ final class FeatureFlagCache: FeatureFlagCaching { self.maxCachedUsers = maxCachedUsers } - func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? { + func retrieveFeatureFlags(userKey: String) -> StoredItems? { guard let cachedData = keyedValueCache.data(forKey: "flags-\(Util.sha256base64(userKey))"), - let cachedFlags = try? JSONDecoder().decode(FeatureFlagCollection.self, from: cachedData) + let cachedFlags = try? JSONDecoder().decode(StoredItemCollection.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) + func storeFeatureFlags(_ storedItems: StoredItems, userKey: String, lastUpdated: Date) { + guard self.maxCachedUsers != 0, let encoded = try? JSONEncoder().encode(StoredItemCollection(storedItems)) else { return } let userSha = Util.sha256base64(userKey) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift index bb6a1de5..a88b78c5 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift @@ -7,6 +7,7 @@ protocol KeyedValueCaching { func dictionary(forKey: String) -> [String: Any]? func removeObject(forKey: String) func removeAll() + func keys() -> [String] } extension UserDefaults: KeyedValueCaching { @@ -17,4 +18,8 @@ extension UserDefaults: KeyedValueCaching { func removeAll() { dictionaryRepresentation().keys.forEach { removeObject(forKey: $0) } } + + func keys() -> [String] { + dictionaryRepresentation().keys.map { String($0) } + } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift index f613ba97..9f9818f3 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -1,9 +1,68 @@ import Foundation +enum StorageItem: Codable { + case item(FeatureFlag) + case tombstone(Int) + + var version: Int { + switch self { + case .item(let flag): + return flag.version ?? 0 + case .tombstone(let version): + return version + } + } + + enum CodingKeys : CodingKey { + case item, tombstone + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .item(flag): + try container.encode(flag, forKey: .item) + case let .tombstone(version): + try container.encode(version, forKey: .tombstone) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if container.allKeys.count != 1 { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Invalid number of keys found, expected one.") + throw DecodingError.typeMismatch(StorageItem.self, context) + } + + switch container.allKeys.first.unsafelyUnwrapped { + case .item: + self = .item(try container.decode(FeatureFlag.self, forKey: .item)) + case .tombstone: + self = .tombstone(try container.decode(Int.self, forKey: .tombstone)) + } + } +} + +typealias StoredItems = [LDFlagKey: StorageItem] +extension StoredItems { + var featureFlags: [LDFlagKey: FeatureFlag] { + self.compactMapValues { + guard case .item(let flag) = $0 else { return nil } + return flag + } + } + + init(items: [LDFlagKey: FeatureFlag]) { + self = items.mapValues { .item($0) } + } +} + protocol FlagMaintaining { - var featureFlags: [LDFlagKey: FeatureFlag] { get } + var storedItems: StoredItems { get } - func replaceStore(newFlags: FeatureFlagCollection) + func replaceStore(newStoredItems: StoredItems) func updateStore(updatedFlag: FeatureFlag) func deleteFlag(deleteResponse: DeleteResponse) func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? @@ -14,23 +73,23 @@ final class FlagStore: FlagMaintaining { fileprivate static let flagQueueLabel = "com.launchdarkly.flagStore.flagQueue" } - var featureFlags: [LDFlagKey: FeatureFlag] { flagQueue.sync { _featureFlags } } + var storedItems: StoredItems { flagQueue.sync { _storedItems } } - private var _featureFlags: [LDFlagKey: FeatureFlag] = [:] + private var _storedItems: StoredItems = [:] // Used with .barrier as reader writer lock on _featureFlags private var flagQueue = DispatchQueue(label: Constants.flagQueueLabel, attributes: .concurrent) init() { } - init(featureFlags: [LDFlagKey: FeatureFlag]) { - Log.debug(typeName(and: #function) + "featureFlags: \(String(describing: featureFlags))") - self._featureFlags = featureFlags + init(storedItems: StoredItems) { + Log.debug(typeName(and: #function) + "storedItems: \(String(describing: storedItems))") + self._storedItems = storedItems } - func replaceStore(newFlags: FeatureFlagCollection) { - Log.debug(typeName(and: #function) + "newFlags: \(String(describing: newFlags))") + func replaceStore(newStoredItems: StoredItems) { + Log.debug(typeName(and: #function) + "newFlags: \(String(describing: newStoredItems))") flagQueue.sync(flags: .barrier) { - self._featureFlags = newFlags.flags + self._storedItems = newStoredItems } } @@ -39,13 +98,13 @@ final class FlagStore: FlagMaintaining { guard self.isValidVersion(for: updatedFlag.flagKey, newVersion: updatedFlag.version) else { Log.debug(self.typeName(and: #function) + "aborted. Invalid version. updateDictionary: \(updatedFlag) " - + "existing flag: \(String(describing: self._featureFlags[updatedFlag.flagKey]))") + + "existing flag: \(String(describing: self._storedItems[updatedFlag.flagKey]))") return } 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) + "prior flag: \(String(describing: self._storedItems[updatedFlag.flagKey]))") + self._storedItems.updateValue(StorageItem.item(updatedFlag), forKey: updatedFlag.flagKey) } } @@ -54,19 +113,19 @@ final class FlagStore: FlagMaintaining { guard self.isValidVersion(for: deleteResponse.key, newVersion: deleteResponse.version) else { Log.debug(self.typeName(and: #function) + "aborted. Invalid version. deleteResponse: \(deleteResponse) " - + "existing flag: \(String(describing: self._featureFlags[deleteResponse.key]))") + + "existing flag: \(String(describing: self._storedItems[deleteResponse.key]))") return } Log.debug(self.typeName(and: #function) + "deleted flag with key: " + deleteResponse.key) - self._featureFlags.removeValue(forKey: deleteResponse.key) + self._storedItems.updateValue(StorageItem.tombstone(deleteResponse.version ?? 0), forKey: deleteResponse.key) } } private func isValidVersion(for flagKey: LDFlagKey, newVersion: Int?) -> Bool { // Currently only called from within barrier, only call on flagQueue // Use only the version, here called "environmentVersion" for comparison. The flagVersion is only used for event reporting. - if let environmentVersion = _featureFlags[flagKey]?.version, + if let environmentVersion = _storedItems[flagKey]?.version, let newEnvironmentVersion = newVersion { return newEnvironmentVersion > environmentVersion } @@ -75,7 +134,12 @@ final class FlagStore: FlagMaintaining { } func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? { - flagQueue.sync { _featureFlags[flagKey] } + flagQueue.sync { + guard case let .item(flag) = _storedItems[flagKey] else { + return nil + } + return flag + } } } diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index a7678449..9d9475a9 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -85,7 +85,7 @@ final class LDClientSpec: QuickSpec { let mobileKey = self.serviceFactoryMock.makeFeatureFlagCacheReceivedParameters!.mobileKey let mockCache = FeatureFlagCachingMock() mockCache.retrieveFeatureFlagsCallback = { - mockCache.retrieveFeatureFlagsReturnValue = self.cachedFlags[mobileKey]?[mockCache.retrieveFeatureFlagsReceivedUserKey!] + mockCache.retrieveFeatureFlagsReturnValue = StoredItems(items: self.cachedFlags[mobileKey]?[mockCache.retrieveFeatureFlagsReceivedUserKey!] ?? [:]) } self.serviceFactoryMock.makeFeatureFlagCacheReturnValue = mockCache } @@ -365,14 +365,14 @@ final class LDClientSpec: QuickSpec { } } it("when called with cached flags for the user and environment") { - let cachedFlags = ["test-flag": FeatureFlag(flagKey: "test-flag")] - let testContext = TestContext().withCached(flags: cachedFlags) + let cachedFlags = ["test-flag": StorageItem.item(FeatureFlag(flagKey: "test-flag"))] + let testContext = TestContext().withCached(flags: cachedFlags.featureFlags) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key - expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == cachedFlags + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags) == cachedFlags expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers @@ -439,7 +439,7 @@ final class LDClientSpec: QuickSpec { context(withCached ? "with cached flags" : "") { beforeEach { if withCached { - _ = testContext.withCached(flags: FlagMaintainingMock.stubFlags()) + _ = testContext.withCached(flags: FlagMaintainingMock.stubStoredItems().featureFlags) } } it("does not complete without timeout") { @@ -615,9 +615,9 @@ final class LDClientSpec: QuickSpec { expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } it("when the new user has cached feature flags") { - let stubFlags = FlagMaintainingMock.stubFlags() + let stubFlags = FlagMaintainingMock.stubStoredItems() let newUser = LDUser.stub() - let testContext = TestContext().withCached(userKey: newUser.key, flags: stubFlags) + let testContext = TestContext().withCached(userKey: newUser.key, flags: stubFlags.featureFlags) testContext.start() testContext.featureFlagCachingMock.reset() @@ -625,7 +625,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.user) == newUser expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == stubFlags + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags) == stubFlags } } } @@ -764,7 +764,7 @@ final class LDClientSpec: QuickSpec { } context("flag store contains the requested value") { beforeEach { - testContext.flagStoreMock.replaceStore(newFlags: FeatureFlagCollection(FlagMaintainingMock.stubFlags())) + testContext.flagStoreMock.replaceStore(newStoredItems: FlagMaintainingMock.stubStoredItems()) } context("non-Optional default value") { it("returns the flag value") { @@ -781,7 +781,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.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?.featureFlag) == testContext.flagStoreMock.storedItems.featureFlags[DarklyServiceMock.FlagKeys.bool] expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user } } @@ -899,30 +899,30 @@ final class LDClientSpec: QuickSpec { testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - let newFlags = ["flag1": FeatureFlag(flagKey: "flag1")] + let newStoredItems = ["flag1": StorageItem.item(FeatureFlag(flagKey: "flag1"))] var updateDate: Date! waitUntil { done in testContext.changeNotifierMock.notifyObserversCallback = done updateDate = Date() - testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection(newFlags))) + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection(newStoredItems.featureFlags))) } expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == newFlags + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags) == newStoredItems expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storedItems) == newStoredItems expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.storedItems.featureFlags expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags) == [:] } func onSyncCompleteStreamingPatchSpec() { - let stubFlags = FlagMaintainingMock.stubFlags() - let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) + let stubFlags = FlagMaintainingMock.stubStoredItems() + let testContext = TestContext(startOnline: true).withCached(flags: stubFlags.featureFlags) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() let updateFlag = FeatureFlag(flagKey: "abc") @@ -938,18 +938,18 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagStoreMock.updateStoreReceivedUpdatedFlag) == updateFlag expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storedItems) == testContext.flagStoreMock.storedItems expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.storedItems.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags.featureFlags).to(beTrue()) } func onSyncCompleteDeleteFlagSpec() { - let stubFlags = FlagMaintainingMock.stubFlags() - let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) + let stubFlags = FlagMaintainingMock.stubStoredItems() + let testContext = TestContext(startOnline: true).withCached(flags: stubFlags.featureFlags) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() let deleteResponse = DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) @@ -965,13 +965,13 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagStoreMock.deleteFlagReceivedDeleteResponse) == deleteResponse expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storedItems.featureFlags) == testContext.flagStoreMock.storedItems.featureFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.storedItems.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags.featureFlags).to(beTrue()) } func onSyncCompleteErrorSpec() { @@ -1296,15 +1296,15 @@ final class LDClientSpec: QuickSpec { } private func allFlagsSpec() { - let stubFlags = FlagMaintainingMock.stubFlags() + let stubFlags = FlagMaintainingMock.stubStoredItems() describe("allFlags") { it("returns all non-null flag values from store") { - let testContext = TestContext().withCached(flags: stubFlags) + let testContext = TestContext().withCached(flags: stubFlags.featureFlags) testContext.start() - expect(testContext.subject.allFlags) == stubFlags.compactMapValues { $0.value } + expect(testContext.subject.allFlags) == stubFlags.featureFlags.compactMapValues { $0.value } } it("returns nil when client is closed") { - let testContext = TestContext().withCached(flags: stubFlags) + let testContext = TestContext().withCached(flags: stubFlags.featureFlags) testContext.start() testContext.subject.close() expect(testContext.subject.allFlags).to(beNil()) diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift index 9db37b2b..aaab2ad9 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift @@ -8,20 +8,20 @@ final class FlagMaintainingMock: FlagMaintaining { innerStore = FlagStore() } - init(flags: [LDFlagKey: FeatureFlag]) { - innerStore = FlagStore(featureFlags: flags) + init(storedItems: StoredItems) { + innerStore = FlagStore(storedItems: storedItems) } - var featureFlags: [LDFlagKey: FeatureFlag] { - innerStore.featureFlags + var storedItems: StoredItems { + innerStore.storedItems } var replaceStoreCallCount = 0 - var replaceStoreReceivedNewFlags: FeatureFlagCollection? - func replaceStore(newFlags: FeatureFlagCollection) { + var replaceStoreReceivedNewFlags: StoredItems? + func replaceStore(newStoredItems: StoredItems) { replaceStoreCallCount += 1 - replaceStoreReceivedNewFlags = newFlags - innerStore.replaceStore(newFlags: newFlags) + replaceStoreReceivedNewFlags = newStoredItems + innerStore.replaceStore(newStoredItems: newStoredItems) } var updateStoreCallCount = 0 @@ -44,9 +44,10 @@ final class FlagMaintainingMock: FlagMaintaining { innerStore.featureFlag(for: flagKey) } - static func stubFlags() -> [LDFlagKey: FeatureFlag] { - var flags = DarklyServiceMock.Constants.stubFeatureFlags() - flags["userKey"] = FeatureFlag(flagKey: "userKey", + static func stubStoredItems() -> StoredItems { + let flags = DarklyServiceMock.Constants.stubFeatureFlags() + var storedItems = StoredItems(items: flags) + storedItems["userKey"] = .item(FeatureFlag(flagKey: "userKey", value: .string(UUID().uuidString), variation: DarklyServiceMock.Constants.variation, version: DarklyServiceMock.Constants.version, @@ -54,7 +55,7 @@ final class FlagMaintainingMock: FlagMaintaining { trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(30.0), reason: DarklyServiceMock.Constants.reason, - trackReason: false) - return flags + trackReason: false)) + return storedItems } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift index 5fdcd457..c1cf256f 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift @@ -133,6 +133,34 @@ final class FeatureFlagSpec: XCTestCase { } } + func testStoredItemCollectionDecodeValid() throws { + let testData: LDValue = [ + "flags": [ + "key1": ["item": ["key": "key1"]], + "key2": ["tombstone": 10] + ] + ] + let encoded = try JSONEncoder().encode(testData) + let storedItemCollection = try JSONDecoder().decode(StoredItemCollection.self, from: encoded) + XCTAssertEqual(storedItemCollection.flags.count, 2) + XCTAssertEqual(storedItemCollection.flags.featureFlags["key1"]?.flagKey, "key1") + XCTAssertNil(storedItemCollection.flags.featureFlags["key2"]) + } + + func testStoredItemCollectionEncoding() { + let collection = StoredItemCollection(["flag-key": .item(FeatureFlag(flagKey: "flag-key"))]) + encodesToObject(collection) { values in + XCTAssertEqual(values.count, 1) + valueIsObject(values["flags"]) { flags in + valueIsObject(flags["flag-key"]) { storageItem in + valueIsObject(storageItem["item"]) { flagValue in + XCTAssertEqual(flagValue["key"], "flag-key") + } + } + } + } + } + func testFlagCollectionDecodeValid() throws { let testData: LDValue = ["key1": [:], "key2": ["key": "key2"]] let flagCollection = try JSONDecoder().decode(FeatureFlagCollection.self, from: JSONEncoder().encode(testData)) @@ -183,6 +211,19 @@ final class FeatureFlagSpec: XCTestCase { } } +extension StorageItem: Equatable { + public static func == (lhs: StorageItem, rhs: StorageItem) -> Bool { + switch (lhs, rhs) { + case let (.item(lFlag), .item(rFlag)): + return lFlag == rFlag + case let (.tombstone(lVersion), .tombstone(rVersion)): + return lVersion == rVersion + default: + return false + } + } +} + extension FeatureFlag: Equatable { public static func == (lhs: FeatureFlag, rhs: FeatureFlag) -> Bool { lhs.flagKey == rhs.flagKey && diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index 77831af1..ee3d0bec 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -22,7 +22,7 @@ final class DarklyServiceSpec: QuickSpec { var serviceFactoryMock: ClientServiceMockFactory = ClientServiceMockFactory() var service: DarklyService! var httpHeaders: HTTPHeaders - let stubFlags = FlagMaintainingMock.stubFlags() + let stubFlags = FlagMaintainingMock.stubStoredItems() init(mobileKey: String = LDConfig.Constants.mockMobileKey, useReport: Bool = Constants.useGetMethod, @@ -83,13 +83,13 @@ final class DarklyServiceSpec: QuickSpec { beforeEach { waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useReportMethod, onActivation: { _ in reportRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useGetMethod, onActivation: { request in getRequestCount += 1 @@ -125,7 +125,7 @@ final class DarklyServiceSpec: QuickSpec { it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.stubFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags.featureFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } @@ -136,13 +136,13 @@ final class DarklyServiceSpec: QuickSpec { testContext.service.flagRequestEtag = requestEtag waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useReportMethod, onActivation: { _ in reportRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useGetMethod, onActivation: { request in getRequestCount += 1 @@ -180,7 +180,7 @@ final class DarklyServiceSpec: QuickSpec { it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.stubFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags.featureFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } @@ -250,13 +250,13 @@ final class DarklyServiceSpec: QuickSpec { beforeEach { waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useGetMethod, onActivation: { _ in getRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useReportMethod, onActivation: { request in reportRequestCount += 1 @@ -291,7 +291,7 @@ final class DarklyServiceSpec: QuickSpec { it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.stubFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags.featureFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } @@ -302,13 +302,13 @@ final class DarklyServiceSpec: QuickSpec { testContext.service.flagRequestEtag = requestEtag waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useGetMethod, onActivation: { _ in getRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useReportMethod, onActivation: { request in reportRequestCount += 1 @@ -344,7 +344,7 @@ final class DarklyServiceSpec: QuickSpec { it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.stubFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags.featureFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } @@ -758,7 +758,7 @@ final class DarklyServiceSpec: QuickSpec { private extension Data { var flagCollection: [LDFlagKey: FeatureFlag]? { - return (try? JSONDecoder().decode(FeatureFlagCollection.self, from: self))?.flags + return (try? JSONDecoder().decode([LDFlagKey: FeatureFlag].self, from: self)) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift index 1d0eb99d..76a52eb2 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift @@ -10,7 +10,7 @@ final class CacheConverterSpec: XCTestCase { private static var upToDateData: Data! override class func setUp() { - upToDateData = try! JSONEncoder().encode(["version": 7]) + upToDateData = try! JSONEncoder().encode(["version": 8]) } override func setUp() { @@ -25,7 +25,9 @@ final class CacheConverterSpec: XCTestCase { func testUpToDate() { let v7valueCacheMock = KeyedValueCachingMock() + v7valueCacheMock.keysReturnValue = ["key1", "key2"] serviceFactory.makeFeatureFlagCacheReturnValue.keyedValueCache = v7valueCacheMock + serviceFactory.makeKeyedValueCacheReturnValue = v7valueCacheMock v7valueCacheMock.dataReturnValue = CacheConverterSpec.upToDateData CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: ["key1", "key2"], maxCachedUsers: 0) XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 2) diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift index 3989e155..72f091cf 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift @@ -5,7 +5,7 @@ import XCTest final class FeatureFlagCacheSpec: XCTestCase { - let testFlagCollection = FeatureFlagCollection(["flag1": FeatureFlag(flagKey: "flag1", variation: 1, flagVersion: 2)]) + let testFlagCollection = StoredItemCollection(["flag1": .item(FeatureFlag(flagKey: "flag1", variation: 1, flagVersion: 2))]) private var serviceFactory: ClientServiceMockFactory! private var mockValueCache: KeyedValueCachingMock { serviceFactory.makeKeyedValueCacheReturnValue } @@ -39,7 +39,7 @@ final class FeatureFlagCacheSpec: XCTestCase { } func testRetrieveEmptyData() throws { - mockValueCache.dataReturnValue = try JSONEncoder().encode(FeatureFlagCollection([:])) + mockValueCache.dataReturnValue = try JSONEncoder().encode(StoredItemCollection([:])) let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) XCTAssertEqual(flagCache.retrieveFeatureFlags(userKey: "user1")?.count, 0) } @@ -72,7 +72,6 @@ final class FeatureFlagCacheSpec: XCTestCase { count += 1 } else if let received = self.mockValueCache.setReceivedArguments { XCTAssertEqual(received.forKey, "flags-\(hashedUserKey)") - XCTAssertEqual(received.value, try JSONEncoder().encode(FeatureFlagCollection([:]))) count += 2 } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift index 591f7cad..4b2d7e40 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift @@ -7,78 +7,102 @@ final class FlagStoreSpec: XCTestCase { let stubFlags = DarklyServiceMock.Constants.stubFeatureFlags() func testInit() { - XCTAssertEqual(FlagStore().featureFlags, [:]) - XCTAssertEqual(FlagStore(featureFlags: self.stubFlags).featureFlags, self.stubFlags) + XCTAssertEqual(FlagStore().storedItems, [:]) + XCTAssertEqual(FlagStore(storedItems: StoredItems(items: self.stubFlags)).storedItems.featureFlags, self.stubFlags) } func testReplaceStore() { - let featureFlags: [LDFlagKey: FeatureFlag] = DarklyServiceMock.Constants.stubFeatureFlags() + let featureFlags = StoredItems(items: DarklyServiceMock.Constants.stubFeatureFlags()) let flagStore = FlagStore() - flagStore.replaceStore(newFlags: FeatureFlagCollection(featureFlags)) - XCTAssertEqual(flagStore.featureFlags, featureFlags) + flagStore.replaceStore(newStoredItems: featureFlags) + XCTAssertEqual(flagStore.storedItems, featureFlags) } func testUpdateStoreNewFlag() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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) + XCTAssertEqual(flagStore.storedItems.count, stubFlags.count + 1) + XCTAssertEqual(flagStore.storedItems.featureFlags["new-int-flag"], flagUpdate) } func testUpdateStoreNewerVersion() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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) + XCTAssertEqual(flagStore.storedItems.count, stubFlags.count) + XCTAssertEqual(flagStore.storedItems.featureFlags[DarklyServiceMock.FlagKeys.int], flagUpdate) } func testUpdateStoreNoVersion() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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) + XCTAssertEqual(flagStore.storedItems.count, stubFlags.count) + XCTAssertEqual(flagStore.storedItems.featureFlags[DarklyServiceMock.FlagKeys.int], flagUpdate) } func testUpdateStoreEarlierOrSameVersion() { let testFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.int) let initialVersion = testFlag.version! - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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) + XCTAssertEqual(flagStore.storedItems.featureFlags, self.stubFlags) } func testDeleteFlagNewerVersion() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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]) + XCTAssertEqual(flagStore.storedItems.count, self.stubFlags.count) + XCTAssertEqual(flagStore.storedItems.featureFlags.count, self.stubFlags.count - 1) + XCTAssertEqual(StorageItem.tombstone(5), flagStore.storedItems[DarklyServiceMock.FlagKeys.int]) } func testDeleteFlagMissingVersion() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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]) + XCTAssertEqual(flagStore.storedItems.count, self.stubFlags.count) + XCTAssertEqual(flagStore.storedItems.featureFlags.count, self.stubFlags.count - 1) + XCTAssertEqual(StorageItem.tombstone(0), flagStore.storedItems[DarklyServiceMock.FlagKeys.int]) } func testDeleteOlderOrNonExistent() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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) + XCTAssertEqual(flagStore.storedItems.featureFlags, self.stubFlags) + } + + func testCannotReplaceDeletedFlagWithOlderVersion() { + let flagStore = FlagStore(storedItems: StoredItems(items: stubFlags)) + let flagUpdate = FeatureFlag(flagKey: "new-int-flag", value: "abc", version: 0) + flagStore.updateStore(updatedFlag: flagUpdate) + XCTAssertEqual(stubFlags.count + 1, flagStore.storedItems.count) + XCTAssertEqual(stubFlags.count + 1, flagStore.storedItems.featureFlags.count) + + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: "new-int-flag", version: 1)) + XCTAssertEqual(stubFlags.count + 1, flagStore.storedItems.count) + XCTAssertEqual(stubFlags.count, flagStore.storedItems.featureFlags.count) + + flagStore.updateStore(updatedFlag: flagUpdate) + XCTAssertEqual(stubFlags.count + 1, flagStore.storedItems.count) + XCTAssertEqual(stubFlags.count, flagStore.storedItems.featureFlags.count) } func testFeatureFlag() { - let flagStore = FlagStore(featureFlags: stubFlags) - flagStore.featureFlags.forEach { flagKey, featureFlag in - XCTAssertEqual(flagStore.featureFlag(for: flagKey), featureFlag) + let flagStore = FlagStore(storedItems: StoredItems(items: stubFlags)) + flagStore.storedItems.forEach { flagKey, featureFlag in + guard case .item(let flag) = featureFlag + else { + XCTAssertNil(flagStore.featureFlag(for: flagKey)) + return + } + + XCTAssertEqual(flagStore.featureFlag(for: flagKey), flag) } XCTAssertNil(flagStore.featureFlag(for: DarklyServiceMock.FlagKeys.unknown)) } From c25f75c56f7f7e528aa36e77afa1fc1d3d43a006 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 15 Jun 2022 17:17:04 -0400 Subject: [PATCH 61/90] Update with main (#211) * Adding a new Circle CI test case for the newer Xcode 13.3.1 (#205) * Add the new build and see what happens * Use the available simulator * Add tombstone support (#209) Prior to this commit, when a flag was deleted, it was actually removed from memory. This allows for the (admittedly unlikely) situation where an older version of a flag update would be received after this deletion, causing the flag to become "undeleted". By managing a light-weight placeholder, the tombstone ensures we do not allow dead flags to resurrect unless directed to by a later version of the flag. * Fix missing merge issues Co-authored-by: Louis Chan <91093020+louis-launchdarkly@users.noreply.github.com> --- .circleci/config.yml | 6 +- ContractTests/Package.swift | 4 +- .../Source/Controllers/SdkController.swift | 286 +++++++++--------- ContractTests/Source/app.swift | 14 - ContractTests/Source/boot.swift | 6 - ContractTests/Source/configure.swift | 14 - ContractTests/Source/main.swift | 8 +- ContractTests/Source/routes.swift | 10 +- ContractTests/testharness-suppressions.txt | 1 - .../GeneratedCode/mocks.generated.swift | 19 +- LaunchDarkly/LaunchDarkly/LDClient.swift | 26 +- .../Models/FeatureFlag/FeatureFlag.swift | 12 + .../ServiceObjects/Cache/CacheConverter.swift | 31 +- .../Cache/FeatureFlagCache.swift | 12 +- .../Cache/KeyedValueCache.swift | 5 + .../ServiceObjects/FlagStore.swift | 98 ++++-- .../LaunchDarklyTests/LDClientSpec.swift | 58 ++-- .../Mocks/FlagMaintainingMock.swift | 27 +- .../Models/FeatureFlag/FeatureFlagSpec.swift | 41 +++ .../Networking/DarklyServiceSpec.swift | 28 +- .../Cache/CacheConverterSpec.swift | 4 +- .../Cache/FeatureFlagCacheSpec.swift | 5 +- .../ServiceObjects/FlagStoreSpec.swift | 78 +++-- 23 files changed, 475 insertions(+), 318 deletions(-) delete mode 100644 ContractTests/Source/app.swift delete mode 100644 ContractTests/Source/boot.swift delete mode 100644 ContractTests/Source/configure.swift diff --git a/.circleci/config.yml b/.circleci/config.yml index 064ad4c3..8fef7c61 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 jobs: contract-tests: macos: - xcode: '13.1.0' + xcode: '13.4.1' steps: - checkout @@ -165,6 +165,10 @@ workflows: build: jobs: + - build: + name: Xcode 13.3 - Swift 5.6 + xcode-version: '13.3.1' + ios-sim: 'platform=iOS Simulator,name=iPhone 13,OS=15.4' - build: name: Xcode 13.1 - Swift 5.5 xcode-version: '13.1.0' diff --git a/ContractTests/Package.swift b/ContractTests/Package.swift index 25418fae..786cd2dc 100644 --- a/ContractTests/Package.swift +++ b/ContractTests/Package.swift @@ -6,7 +6,7 @@ let package = Package( name: "ContractTests", platforms: [ .iOS(.v10), - .macOS(.v10_12), + .macOS(.v10_15), .watchOS(.v3), .tvOS(.v10) ], @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ Package.Dependency.package(name: "LaunchDarkly", path: ".."), - .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0") + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0") ], targets: [ .target( diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index c2a997fe..c5358152 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -1,10 +1,20 @@ import Vapor import LaunchDarkly -final class SdkController { +final class SdkController: RouteCollection { private var clients: [Int: LDClient] = [:] private var clientCounter = 0 + func boot(routes: RoutesBuilder) { + routes.get("", use: status) + routes.post("", use: createClient) + routes.delete("", use: shutdown) + + let clientRoutes = routes.grouped("clients") + clientRoutes.post(":id", use: executeCommand) + clientRoutes.delete(":id", use: shutdownClient) + } + func status(_ req: Request) -> StatusResponse { let capabilities = [ "client-side", @@ -19,104 +29,104 @@ final class SdkController { capabilities: capabilities) } - func createClient(_ req: Request) throws -> Future { - return try req.content.decode(CreateInstance.self).map { createInstance in - var config = LDConfig(mobileKey: createInstance.configuration.credential) - config.enableBackgroundUpdates = true - config.isDebugMode = true + func createClient(_ req: Request) throws -> Response { + let createInstance = try req.content.decode(CreateInstance.self) + var config = LDConfig(mobileKey: createInstance.configuration.credential) + config.enableBackgroundUpdates = true + config.isDebugMode = true - if let streaming = createInstance.configuration.streaming { - if let baseUri = streaming.baseUri { - config.streamUrl = URL(string: baseUri)! - } - - // TODO(mmk) Need to hook up initialRetryDelayMs - } else if let polling = createInstance.configuration.polling { - config.streamingMode = .polling - if let baseUri = polling.baseUri { - config.baseUrl = URL(string: baseUri)! - } + if let streaming = createInstance.configuration.streaming { + if let baseUri = streaming.baseUri { + config.streamUrl = URL(string: baseUri)! } - if let events = createInstance.configuration.events { - if let baseUri = events.baseUri { - config.eventsUrl = URL(string: baseUri)! - } + // TODO(mmk) Need to hook up initialRetryDelayMs + } else if let polling = createInstance.configuration.polling { + config.streamingMode = .polling + if let baseUri = polling.baseUri { + config.baseUrl = URL(string: baseUri)! + } + } - if let capacity = events.capacity { - config.eventCapacity = capacity - } + if let events = createInstance.configuration.events { + if let baseUri = events.baseUri { + config.eventsUrl = URL(string: baseUri)! + } - if let enable = events.enableDiagnostics { - config.diagnosticOptOut = !enable - } + if let capacity = events.capacity { + config.eventCapacity = capacity + } - if let allPrivate = events.allAttributesPrivate { - config.allContextAttributesPrivate = allPrivate - } + if let enable = events.enableDiagnostics { + config.diagnosticOptOut = !enable + } - if let globalPrivate = events.globalPrivateAttributes { - config.privateContextAttributes = globalPrivate.map { Reference($0) } - } + if let allPrivate = events.allAttributesPrivate { + config.allContextAttributesPrivate = allPrivate + } - if let flushIntervalMs = events.flushIntervalMs { - config.eventFlushInterval = flushIntervalMs - } + if let globalPrivate = events.globalPrivateAttributes { + config.privateContextAttributes = globalPrivate.map{ Reference($0) } } - if let tags = createInstance.configuration.tags { - var applicationInfo = ApplicationInfo() - if let id = tags.applicationId { - applicationInfo.applicationIdentifier(id) - } + if let flushIntervalMs = events.flushIntervalMs { + config.eventFlushInterval = flushIntervalMs + } + } - if let verision = tags.applicationVersion { - applicationInfo.applicationVersion(verision) - } + if let tags = createInstance.configuration.tags { + var applicationInfo = ApplicationInfo() + if let id = tags.applicationId { + applicationInfo.applicationIdentifier(id) + } - config.applicationInfo = applicationInfo + if let verision = tags.applicationVersion { + applicationInfo.applicationVersion(verision) } - let clientSide = createInstance.configuration.clientSide + config.applicationInfo = applicationInfo + } - if let evaluationReasons = clientSide.evaluationReasons { - config.evaluationReasons = evaluationReasons - } + let clientSide = createInstance.configuration.clientSide - if let useReport = clientSide.useReport { - config.useReport = useReport - } + if let evaluationReasons = clientSide.evaluationReasons { + config.evaluationReasons = evaluationReasons + } - let dispatchSemaphore = DispatchSemaphore(value: 0) - let startWaitSeconds = (createInstance.configuration.startWaitTimeMs ?? 5_000) / 1_000 + if let useReport = clientSide.useReport { + config.useReport = useReport + } - LDClient.start(config: config, context: clientSide.initialContext, startWaitSeconds: startWaitSeconds) { _ in - dispatchSemaphore.signal() - } + let dispatchSemaphore = DispatchSemaphore(value: 0) + let startWaitSeconds = (createInstance.configuration.startWaitTimeMs ?? 5_000) / 1_000 - dispatchSemaphore.wait() + LDClient.start(config:config, context: clientSide.initialContext, startWaitSeconds: startWaitSeconds) { timedOut in + dispatchSemaphore.signal() + } - let client = LDClient.get()! + dispatchSemaphore.wait() - self.clientCounter += 1 - self.clients.updateValue(client, forKey: self.clientCounter) + let client = LDClient.get()! - var headers = HTTPHeaders() - headers.add(name: "Location", value: "/clients/\(self.clientCounter)") + self.clientCounter += 1 + self.clients.updateValue(client, forKey: self.clientCounter) - var response = HTTPResponse() - response.status = .ok - response.headers = headers + var headers = HTTPHeaders() + headers.add(name: "Location", value: "/clients/\(self.clientCounter)") - return response - } + let response = Response() + response.status = .ok + response.headers = headers + + return response } func shutdownClient(_ req: Request) throws -> HTTPStatus { - let id = try req.parameters.next(Int.self) - guard let client = self.clients[id] else { - return HTTPStatus.badRequest - } + guard let id = req.parameters.get("id", as: Int.self) + else { throw Abort(.badRequest) } + + guard let client = self.clients[id] + else { return HTTPStatus.badRequest } client.close() clients.removeValue(forKey: id) @@ -124,82 +134,84 @@ final class SdkController { return HTTPStatus.accepted } - func executeCommand(_ req: Request) throws -> Future { - return try req.content.decode(CommandParameters.self).map { commandParameters in - guard let client = self.clients[self.clientCounter] else { - throw Abort(.badRequest) - } + func executeCommand(_ req: Request) throws -> CommandResponse { + guard let id = req.parameters.get("id", as: Int.self) + else { throw Abort(.badRequest) } - switch commandParameters.command { - case "evaluate": - let result: EvaluateFlagResponse = try self.evaluate(client, commandParameters.evaluate!) - return CommandResponse.evaluateFlag(result) - case "evaluateAll": - let result: EvaluateAllFlagsResponse = try self.evaluateAll(client, commandParameters.evaluateAll!) - return CommandResponse.evaluateAll(result) - case "identifyEvent": - let semaphore = DispatchSemaphore(value: 0) - client.identify(context: commandParameters.identifyEvent!.context) { - semaphore.signal() - } - semaphore.wait() - case "customEvent": - let event = commandParameters.customEvent! - client.track(key: event.eventKey, data: event.data, metricValue: event.metricValue) - case "flushEvents": - client.flush() - case "contextBuild": - let contextBuild = commandParameters.contextBuild! - - do { - if let singleParams = contextBuild.single { - let context = try SdkController.buildSingleContextFromParams(singleParams) - - let encoder = JSONEncoder() - let output = try encoder.encode(context) - - let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) - return CommandResponse.contextBuild(response) - } + let commandParameters = try req.content.decode(CommandParameters.self) + guard let client = self.clients[id] else { + throw Abort(.badRequest) + } - if let multiParams = contextBuild.multi { - var multiContextBuilder = LDMultiContextBuilder() - try multiParams.forEach { - multiContextBuilder.addContext(try SdkController.buildSingleContextFromParams($0)) - } + switch commandParameters.command { + case "evaluate": + let result: EvaluateFlagResponse = try self.evaluate(client, commandParameters.evaluate!) + return CommandResponse.evaluateFlag(result) + case "evaluateAll": + let result: EvaluateAllFlagsResponse = try self.evaluateAll(client, commandParameters.evaluateAll!) + return CommandResponse.evaluateAll(result) + case "identifyEvent": + let semaphore = DispatchSemaphore(value: 0) + client.identify(context: commandParameters.identifyEvent!.context) { + semaphore.signal() + } + semaphore.wait() + case "customEvent": + let event = commandParameters.customEvent! + client.track(key: event.eventKey, data: event.data, metricValue: event.metricValue) + case "flushEvents": + client.flush() + case "contextBuild": + let contextBuild = commandParameters.contextBuild! + + do { + if let singleParams = contextBuild.single { + let context = try SdkController.buildSingleContextFromParams(singleParams) - let context = try multiContextBuilder.build().get() - let encoder = JSONEncoder() - let output = try encoder.encode(context) + let encoder = JSONEncoder() + let output = try encoder.encode(context) - let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) - return CommandResponse.contextBuild(response) - } - } catch { - let response = ContextBuildResponse(output: nil, error: error.localizedDescription) + let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) return CommandResponse.contextBuild(response) } - case "contextConvert": - let convertRequest = commandParameters.contextConvert! - do { - let decoder = JSONDecoder() - let context: LDContext = try decoder.decode(LDContext.self, from: convertRequest.input) + if let multiParams = contextBuild.multi { + var multiContextBuilder = LDMultiContextBuilder() + try multiParams.forEach { + multiContextBuilder.addContext(try SdkController.buildSingleContextFromParams($0)) + } + + let context = try multiContextBuilder.build().get() let encoder = JSONEncoder() let output = try encoder.encode(context) let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) return CommandResponse.contextBuild(response) - } catch { - let response = ContextBuildResponse(output: nil, error: error.localizedDescription) - return CommandResponse.contextBuild(response) } - default: - throw Abort(.badRequest) + } catch { + let response = ContextBuildResponse(output: nil, error: error.localizedDescription) + return CommandResponse.contextBuild(response) } - - return CommandResponse.ok + case "contextConvert": + let convertRequest = commandParameters.contextConvert! + do { + let decoder = JSONDecoder() + let context: LDContext = try decoder.decode(LDContext.self, from: Data(convertRequest.input.utf8)) + + let encoder = JSONEncoder() + let output = try encoder.encode(context) + + let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) + return CommandResponse.contextBuild(response) + } catch { + let response = ContextBuildResponse(output: nil, error: error.localizedDescription) + return CommandResponse.contextBuild(response) + } + default: + throw Abort(.badRequest) } + + return CommandResponse.ok } static func buildSingleContextFromParams(_ params: SingleContextParameters) throws -> LDContext { @@ -244,7 +256,7 @@ final class SdkController { let result = client.boolVariation(forKey: params.flagKey, defaultValue: defaultValue) return EvaluateFlagResponse(value: LDValue.bool(result)) } - throw "Failed to convert \(params.valueType) to bool" + throw Abort(.badRequest, reason: "Failed to convert \(params.valueType) to bool") case "int": if case let LDValue.number(defaultValue) = params.defaultValue { if params.detail { @@ -255,7 +267,7 @@ final class SdkController { let result = client.intVariation(forKey: params.flagKey, defaultValue: Int(defaultValue)) return EvaluateFlagResponse(value: LDValue.number(Double(result))) } - throw "Failed to convert \(params.valueType) to int" + throw Abort(.badRequest, reason: "Failed to convert \(params.valueType) to int") case "double": if case let LDValue.number(defaultValue) = params.defaultValue { if params.detail { @@ -266,7 +278,7 @@ final class SdkController { let result = client.doubleVariation(forKey: params.flagKey, defaultValue: defaultValue) return EvaluateFlagResponse(value: LDValue.number(result), variationIndex: nil, reason: nil) } - throw "Failed to convert \(params.valueType) to bool" + throw Abort(.badRequest, reason: "Failed to convert \(params.valueType) to bool") case "string": if case let LDValue.string(defaultValue) = params.defaultValue { if params.detail { @@ -277,7 +289,7 @@ final class SdkController { let result = client.stringVariation(forKey: params.flagKey, defaultValue: defaultValue) return EvaluateFlagResponse(value: LDValue.string(result), variationIndex: nil, reason: nil) } - throw "Failed to convert \(params.valueType) to string" + throw Abort(.badRequest, reason: "Failed to convert \(params.valueType) to string") default: if params.detail { let result = client.jsonVariationDetail(forKey: params.flagKey, defaultValue: params.defaultValue) diff --git a/ContractTests/Source/app.swift b/ContractTests/Source/app.swift deleted file mode 100644 index 0a0094e7..00000000 --- a/ContractTests/Source/app.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Vapor - -extension String: Error {} - -func app(_ env: Environment) throws -> Application { - var config = Config.default() - var env = env - var services = Services.default() - try configure(&config, &env, &services) - let app = try Application(config: config, environment: env, services: services) - try boot(app) - - return app -} diff --git a/ContractTests/Source/boot.swift b/ContractTests/Source/boot.swift deleted file mode 100644 index 603be99f..00000000 --- a/ContractTests/Source/boot.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Vapor - -/// Called after your application has initialized. -func boot(_ app: Application) throws { - // Your code here -} diff --git a/ContractTests/Source/configure.swift b/ContractTests/Source/configure.swift deleted file mode 100644 index 8b1a9ee0..00000000 --- a/ContractTests/Source/configure.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Vapor - -/// Called before your application initializes. -func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { - // Register routes to the router - let router = EngineRouter.default() - try routes(router) - services.register(router, as: Router.self) - - // Register middleware - var middlewares = MiddlewareConfig() // Create _empty_ middleware config - middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response - services.register(middlewares) -} diff --git a/ContractTests/Source/main.swift b/ContractTests/Source/main.swift index 0cf94baa..a1538ddd 100644 --- a/ContractTests/Source/main.swift +++ b/ContractTests/Source/main.swift @@ -1,10 +1,16 @@ import Foundation +import Vapor let semaphore = DispatchSemaphore(value: 0) DispatchQueue.global(qos: .userInitiated).async { do { - try app(.detect()).run() + var env = try Environment.detect() + try LoggingSystem.bootstrap(from: &env) + let app = Application(env) + defer { app.shutdown() } + try routes(app) + try app.run() } catch { } semaphore.signal() diff --git a/ContractTests/Source/routes.swift b/ContractTests/Source/routes.swift index f2c84590..8d021691 100644 --- a/ContractTests/Source/routes.swift +++ b/ContractTests/Source/routes.swift @@ -1,12 +1,6 @@ import Vapor -func routes(_ router: Router) throws { +func routes(_ app: Application) throws { let sdkController = SdkController() - router.get("/", use: sdkController.status) - router.post("/", use: sdkController.createClient) - router.delete("/", use: sdkController.shutdown) - - let clientRoutes = router.grouped("clients") - clientRoutes.post(Int.parameter, use: sdkController.executeCommand) - clientRoutes.delete(Int.parameter, use: sdkController.shutdownClient) + try app.register(collection: sdkController) } diff --git a/ContractTests/testharness-suppressions.txt b/ContractTests/testharness-suppressions.txt index 1e082e3f..e69de29b 100644 --- a/ContractTests/testharness-suppressions.txt +++ b/ContractTests/testharness-suppressions.txt @@ -1 +0,0 @@ -streaming/updates/flag delete for previously nonexistent flag is applied diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index 5c5a1da0..8487f1d1 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -265,8 +265,8 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { var retrieveFeatureFlagsCallCount = 0 var retrieveFeatureFlagsCallback: (() throws -> Void)? var retrieveFeatureFlagsReceivedContextKey: String? - var retrieveFeatureFlagsReturnValue: [LDFlagKey: FeatureFlag]? - func retrieveFeatureFlags(contextKey: String) -> [LDFlagKey: FeatureFlag]? { + var retrieveFeatureFlagsReturnValue: StoredItems? + func retrieveFeatureFlags(contextKey: String) -> StoredItems? { retrieveFeatureFlagsCallCount += 1 retrieveFeatureFlagsReceivedContextKey = contextKey try! retrieveFeatureFlagsCallback?() @@ -275,10 +275,10 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { var storeFeatureFlagsCallCount = 0 var storeFeatureFlagsCallback: (() throws -> Void)? - var storeFeatureFlagsReceivedArguments: (featureFlags: [LDFlagKey: FeatureFlag], contextKey: String, lastUpdated: Date)? - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], contextKey: String, lastUpdated: Date) { + var storeFeatureFlagsReceivedArguments: (storedItems: StoredItems, contextKey: String, lastUpdated: Date)? + func storeFeatureFlags(_ storedItems: StoredItems, contextKey: String, lastUpdated: Date) { storeFeatureFlagsCallCount += 1 - storeFeatureFlagsReceivedArguments = (featureFlags: featureFlags, contextKey: contextKey, lastUpdated: lastUpdated) + storeFeatureFlagsReceivedArguments = (storedItems: storedItems, contextKey: contextKey, lastUpdated: lastUpdated) try! storeFeatureFlagsCallback?() } } @@ -397,6 +397,15 @@ final class KeyedValueCachingMock: KeyedValueCaching { removeAllCallCount += 1 try! removeAllCallback?() } + + var keysCallCount = 0 + var keysCallback: (() throws -> Void)? + var keysReturnValue: [String]! + func keys() -> [String] { + keysCallCount += 1 + try! keysCallback?() + return keysReturnValue + } } // MARK: - LDFlagSynchronizingMock diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 55357be2..a6f06bc9 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -296,7 +296,7 @@ public class LDClient { self.internalSetOnline(false) let cachedContextFlags = self.flagCache.retrieveFeatureFlags(contextKey: self.context.fullyQualifiedHashedKey()) ?? [:] - flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedContextFlags)) + flagStore.replaceStore(newStoredItems: cachedContextFlags) self.service.context = self.context self.service.clearFlagResponseCache() flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), @@ -319,7 +319,7 @@ public class LDClient { public var allFlags: [LDFlagKey: LDValue]? { guard hasStarted else { return nil } - return flagStore.featureFlags.compactMapValues { $0.value } + return flagStore.storedItems.featureFlags.compactMapValues { $0.value } } // MARK: Observing Updates @@ -472,20 +472,20 @@ public class LDClient { Log.debug(typeName(and: #function) + "result: \(result)") switch result { case let .flagCollection(flagCollection): - let oldFlags = flagStore.featureFlags + let oldStoredItems = flagStore.storedItems connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) - flagStore.replaceStore(newFlags: flagCollection) - self.updateCacheAndReportChanges(context: self.context, oldFlags: oldFlags) + flagStore.replaceStore(newStoredItems: StoredItems(items: flagCollection.flags)) + self.updateCacheAndReportChanges(context: self.context, oldStoredItems: oldStoredItems) case let .patch(featureFlag): - let oldFlags = flagStore.featureFlags + let oldStoredItems = flagStore.storedItems connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) flagStore.updateStore(updatedFlag: featureFlag) - self.updateCacheAndReportChanges(context: self.context, oldFlags: oldFlags) + self.updateCacheAndReportChanges(context: self.context, oldStoredItems: oldStoredItems) case let .delete(deleteResponse): - let oldFlags = flagStore.featureFlags + let oldStoredItems = flagStore.storedItems connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) flagStore.deleteFlag(deleteResponse: deleteResponse) - self.updateCacheAndReportChanges(context: self.context, oldFlags: oldFlags) + self.updateCacheAndReportChanges(context: self.context, oldStoredItems: oldStoredItems) case .upToDate: connectionInformation.lastKnownFlagValidity = Date() flagChangeNotifier.notifyUnchanged() @@ -503,9 +503,9 @@ public class LDClient { } private func updateCacheAndReportChanges(context: LDContext, - oldFlags: [LDFlagKey: FeatureFlag]) { - flagCache.storeFeatureFlags(flagStore.featureFlags, contextKey: context.fullyQualifiedHashedKey(), lastUpdated: Date()) - flagChangeNotifier.notifyObservers(oldFlags: oldFlags, newFlags: flagStore.featureFlags) + oldStoredItems: StoredItems) { + flagCache.storeFeatureFlags(flagStore.storedItems, contextKey: context.fullyQualifiedHashedKey(), lastUpdated: Date()) + flagChangeNotifier.notifyObservers(oldFlags: oldStoredItems.featureFlags, newFlags: flagStore.storedItems.featureFlags) } // MARK: Events @@ -734,7 +734,7 @@ public class LDClient { Log.level = environmentReporter.isDebugBuild && config.isDebugMode ? .debug : .noLogging if let cachedFlags = flagCache.retrieveFeatureFlags(contextKey: context.fullyQualifiedHashedKey()), !cachedFlags.isEmpty { - flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedFlags)) + flagStore.replaceStore(newStoredItems: cachedFlags) } eventReporter.record(IdentifyEvent(context: context)) diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift index b2357de7..662a4a8b 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift @@ -104,3 +104,15 @@ struct FeatureFlagCollection: Codable { try flags.encode(to: encoder) } } + +struct StoredItemCollection: Codable { + let flags: StoredItems + + init(_ flags: StoredItems) { + self.flags = flags + } + + init(_ collection: FeatureFlagCollection) { + self.flags = StoredItems(items: collection.flags) + } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index 576fd810..c90e98c1 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -91,13 +91,28 @@ final class CacheConverter: CacheConverting { cachedEnvData.forEach { mobileKey, contexts in contexts.forEach { contextKey, data in - flagCaches[mobileKey]?.storeFeatureFlags(data.flags, contextKey: contextKey, lastUpdated: data.updated) + flagCaches[mobileKey]?.storeFeatureFlags(StoredItems(items: data.flags), contextKey: contextKey, lastUpdated: data.updated) } } v6cache.removeObject(forKey: "com.launchDarkly.cachedUserEnvironmentFlags") } + private func convertV7Data(flagCaches: inout [MobileKey: FeatureFlagCaching]) { + for (_, flagCaching) in flagCaches { + flagCaching.keyedValueCache.keys().forEach { key in + guard let cachedData = flagCaching.keyedValueCache.data(forKey: key), + let cachedFlags = try? JSONDecoder().decode(FeatureFlagCollection.self, from: cachedData) + else { return } + + guard let encoded = try? JSONEncoder().encode(StoredItemCollection(cachedFlags)) + else { return } + + flagCaching.keyedValueCache.set(encoded, forKey: key) + } + } + } + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) { var flagCaches: [String: FeatureFlagCaching] = [:] keysToConvert.forEach { mobileKey in @@ -107,17 +122,20 @@ final class CacheConverter: CacheConverting { 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 + cacheVersion == 7 || cacheVersion == 8 else { // Metadata is invalid, remove existing data and attempt migration flagCache.keyedValueCache.removeAll() return } - // Already up to date - flagCaches.removeValue(forKey: mobileKey) + + if cacheVersion == 8 { + // Already up to date + flagCaches.removeValue(forKey: mobileKey) + } } - // Skip migration if all environments are V7 + // Skip migration if all environments are V8 if flagCaches.isEmpty { return } // Remove V5 cache data (migration not supported) @@ -125,9 +143,10 @@ final class CacheConverter: CacheConverting { standardDefaults.removeObject(forKey: "com.launchdarkly.dataManager.userEnvironments") convertV6Data(v6cache: standardDefaults, flagCaches: flagCaches) + convertV7Data(flagCaches: &flagCaches) // Set cache version to skip this logic in the future - if let versionMetadata = try? JSONEncoder().encode(["version": 7]) { + if let versionMetadata = try? JSONEncoder().encode(["version": 8]) { flagCaches.forEach { $0.value.keyedValueCache.set(versionMetadata, forKey: "ld-cache-metadata") } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift index 94c91251..8de11fdc 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -5,8 +5,8 @@ protocol FeatureFlagCaching { // sourcery: defaultMockValue = KeyedValueCachingMock() var keyedValueCache: KeyedValueCaching { get } - func retrieveFeatureFlags(contextKey: String) -> [LDFlagKey: FeatureFlag]? - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], contextKey: String, lastUpdated: Date) + func retrieveFeatureFlags(contextKey: String) -> StoredItems? + func storeFeatureFlags(_ storedItems: StoredItems, contextKey: String, lastUpdated: Date) } final class FeatureFlagCache: FeatureFlagCaching { @@ -24,15 +24,15 @@ final class FeatureFlagCache: FeatureFlagCaching { self.maxCachedUsers = maxCachedUsers } - func retrieveFeatureFlags(contextKey: String) -> [LDFlagKey: FeatureFlag]? { + func retrieveFeatureFlags(contextKey: String) -> StoredItems? { guard let cachedData = keyedValueCache.data(forKey: "flags-\(contextKey)"), - let cachedFlags = try? JSONDecoder().decode(FeatureFlagCollection.self, from: cachedData) + let cachedFlags = try? JSONDecoder().decode(StoredItemCollection.self, from: cachedData) else { return nil } return cachedFlags.flags } - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], contextKey: String, lastUpdated: Date) { - guard self.maxCachedUsers != 0, let encoded = try? JSONEncoder().encode(featureFlags) + func storeFeatureFlags(_ storedItems: StoredItems, contextKey: String, lastUpdated: Date) { + guard self.maxCachedUsers != 0, let encoded = try? JSONEncoder().encode(StoredItemCollection(storedItems)) else { return } self.keyedValueCache.set(encoded, forKey: "flags-\(contextKey)") diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift index bb6a1de5..a88b78c5 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift @@ -7,6 +7,7 @@ protocol KeyedValueCaching { func dictionary(forKey: String) -> [String: Any]? func removeObject(forKey: String) func removeAll() + func keys() -> [String] } extension UserDefaults: KeyedValueCaching { @@ -17,4 +18,8 @@ extension UserDefaults: KeyedValueCaching { func removeAll() { dictionaryRepresentation().keys.forEach { removeObject(forKey: $0) } } + + func keys() -> [String] { + dictionaryRepresentation().keys.map { String($0) } + } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift index f613ba97..9f9818f3 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -1,9 +1,68 @@ import Foundation +enum StorageItem: Codable { + case item(FeatureFlag) + case tombstone(Int) + + var version: Int { + switch self { + case .item(let flag): + return flag.version ?? 0 + case .tombstone(let version): + return version + } + } + + enum CodingKeys : CodingKey { + case item, tombstone + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .item(flag): + try container.encode(flag, forKey: .item) + case let .tombstone(version): + try container.encode(version, forKey: .tombstone) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if container.allKeys.count != 1 { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Invalid number of keys found, expected one.") + throw DecodingError.typeMismatch(StorageItem.self, context) + } + + switch container.allKeys.first.unsafelyUnwrapped { + case .item: + self = .item(try container.decode(FeatureFlag.self, forKey: .item)) + case .tombstone: + self = .tombstone(try container.decode(Int.self, forKey: .tombstone)) + } + } +} + +typealias StoredItems = [LDFlagKey: StorageItem] +extension StoredItems { + var featureFlags: [LDFlagKey: FeatureFlag] { + self.compactMapValues { + guard case .item(let flag) = $0 else { return nil } + return flag + } + } + + init(items: [LDFlagKey: FeatureFlag]) { + self = items.mapValues { .item($0) } + } +} + protocol FlagMaintaining { - var featureFlags: [LDFlagKey: FeatureFlag] { get } + var storedItems: StoredItems { get } - func replaceStore(newFlags: FeatureFlagCollection) + func replaceStore(newStoredItems: StoredItems) func updateStore(updatedFlag: FeatureFlag) func deleteFlag(deleteResponse: DeleteResponse) func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? @@ -14,23 +73,23 @@ final class FlagStore: FlagMaintaining { fileprivate static let flagQueueLabel = "com.launchdarkly.flagStore.flagQueue" } - var featureFlags: [LDFlagKey: FeatureFlag] { flagQueue.sync { _featureFlags } } + var storedItems: StoredItems { flagQueue.sync { _storedItems } } - private var _featureFlags: [LDFlagKey: FeatureFlag] = [:] + private var _storedItems: StoredItems = [:] // Used with .barrier as reader writer lock on _featureFlags private var flagQueue = DispatchQueue(label: Constants.flagQueueLabel, attributes: .concurrent) init() { } - init(featureFlags: [LDFlagKey: FeatureFlag]) { - Log.debug(typeName(and: #function) + "featureFlags: \(String(describing: featureFlags))") - self._featureFlags = featureFlags + init(storedItems: StoredItems) { + Log.debug(typeName(and: #function) + "storedItems: \(String(describing: storedItems))") + self._storedItems = storedItems } - func replaceStore(newFlags: FeatureFlagCollection) { - Log.debug(typeName(and: #function) + "newFlags: \(String(describing: newFlags))") + func replaceStore(newStoredItems: StoredItems) { + Log.debug(typeName(and: #function) + "newFlags: \(String(describing: newStoredItems))") flagQueue.sync(flags: .barrier) { - self._featureFlags = newFlags.flags + self._storedItems = newStoredItems } } @@ -39,13 +98,13 @@ final class FlagStore: FlagMaintaining { guard self.isValidVersion(for: updatedFlag.flagKey, newVersion: updatedFlag.version) else { Log.debug(self.typeName(and: #function) + "aborted. Invalid version. updateDictionary: \(updatedFlag) " - + "existing flag: \(String(describing: self._featureFlags[updatedFlag.flagKey]))") + + "existing flag: \(String(describing: self._storedItems[updatedFlag.flagKey]))") return } 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) + "prior flag: \(String(describing: self._storedItems[updatedFlag.flagKey]))") + self._storedItems.updateValue(StorageItem.item(updatedFlag), forKey: updatedFlag.flagKey) } } @@ -54,19 +113,19 @@ final class FlagStore: FlagMaintaining { guard self.isValidVersion(for: deleteResponse.key, newVersion: deleteResponse.version) else { Log.debug(self.typeName(and: #function) + "aborted. Invalid version. deleteResponse: \(deleteResponse) " - + "existing flag: \(String(describing: self._featureFlags[deleteResponse.key]))") + + "existing flag: \(String(describing: self._storedItems[deleteResponse.key]))") return } Log.debug(self.typeName(and: #function) + "deleted flag with key: " + deleteResponse.key) - self._featureFlags.removeValue(forKey: deleteResponse.key) + self._storedItems.updateValue(StorageItem.tombstone(deleteResponse.version ?? 0), forKey: deleteResponse.key) } } private func isValidVersion(for flagKey: LDFlagKey, newVersion: Int?) -> Bool { // Currently only called from within barrier, only call on flagQueue // Use only the version, here called "environmentVersion" for comparison. The flagVersion is only used for event reporting. - if let environmentVersion = _featureFlags[flagKey]?.version, + if let environmentVersion = _storedItems[flagKey]?.version, let newEnvironmentVersion = newVersion { return newEnvironmentVersion > environmentVersion } @@ -75,7 +134,12 @@ final class FlagStore: FlagMaintaining { } func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? { - flagQueue.sync { _featureFlags[flagKey] } + flagQueue.sync { + guard case let .item(flag) = _storedItems[flagKey] else { + return nil + } + return flag + } } } diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 737a8fea..ac9850f1 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -84,7 +84,7 @@ final class LDClientSpec: QuickSpec { let mobileKey = self.serviceFactoryMock.makeFeatureFlagCacheReceivedParameters!.mobileKey let mockCache = FeatureFlagCachingMock() mockCache.retrieveFeatureFlagsCallback = { - mockCache.retrieveFeatureFlagsReturnValue = self.cachedFlags[mobileKey]?[mockCache.retrieveFeatureFlagsReceivedContextKey!] + mockCache.retrieveFeatureFlagsReturnValue = StoredItems(items: self.cachedFlags[mobileKey]?[mockCache.retrieveFeatureFlagsReceivedContextKey!] ?? [:]) } self.serviceFactoryMock.makeFeatureFlagCacheReturnValue = mockCache } @@ -330,14 +330,14 @@ final class LDClientSpec: QuickSpec { } } it("when called with cached flags for the user and environment") { - let cachedFlags = ["test-flag": FeatureFlag(flagKey: "test-flag")] - let testContext = TestContext().withCached(flags: cachedFlags) + let cachedFlags = ["test-flag": StorageItem.item(FeatureFlag(flagKey: "test-flag"))] + let testContext = TestContext().withCached(flags: cachedFlags.featureFlags) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() - expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == cachedFlags + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags) == cachedFlags expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers @@ -404,7 +404,7 @@ final class LDClientSpec: QuickSpec { context(withCached ? "with cached flags" : "") { beforeEach { if withCached { - _ = testContext.withCached(flags: FlagMaintainingMock.stubFlags()) + _ = testContext.withCached(flags: FlagMaintainingMock.stubStoredItems().featureFlags) } } it("does not complete without timeout") { @@ -580,9 +580,9 @@ final class LDClientSpec: QuickSpec { expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } it("when the new user has cached feature flags") { - let stubFlags = FlagMaintainingMock.stubFlags() + let stubFlags = FlagMaintainingMock.stubStoredItems() let newContext = LDContext.stub() - let testContext = TestContext().withCached(contextKey: newContext.fullyQualifiedHashedKey(), flags: stubFlags) + let testContext = TestContext().withCached(contextKey: newContext.fullyQualifiedHashedKey(), flags: stubFlags.featureFlags) testContext.start() testContext.featureFlagCachingMock.reset() @@ -590,7 +590,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.context) == newContext expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == stubFlags + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags) == stubFlags } } } @@ -729,7 +729,7 @@ final class LDClientSpec: QuickSpec { } context("flag store contains the requested value") { beforeEach { - testContext.flagStoreMock.replaceStore(newFlags: FeatureFlagCollection(FlagMaintainingMock.stubFlags())) + testContext.flagStoreMock.replaceStore(newStoredItems: FlagMaintainingMock.stubStoredItems()) } context("non-Optional default value") { it("returns the flag value") { @@ -746,7 +746,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.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?.featureFlag) == testContext.flagStoreMock.storedItems.featureFlags[DarklyServiceMock.FlagKeys.bool] expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.context) == testContext.context } } @@ -864,30 +864,30 @@ final class LDClientSpec: QuickSpec { testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - let newFlags = ["flag1": FeatureFlag(flagKey: "flag1")] + let newStoredItems = ["flag1": StorageItem.item(FeatureFlag(flagKey: "flag1"))] var updateDate: Date! waitUntil { done in testContext.changeNotifierMock.notifyObserversCallback = done updateDate = Date() - testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection(newFlags))) + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection(newStoredItems.featureFlags))) } expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == newFlags + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags) == newStoredItems expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storedItems) == newStoredItems expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.storedItems.featureFlags expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags) == [:] } func onSyncCompleteStreamingPatchSpec() { - let stubFlags = FlagMaintainingMock.stubFlags() - let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) + let stubFlags = FlagMaintainingMock.stubStoredItems() + let testContext = TestContext(startOnline: true).withCached(flags: stubFlags.featureFlags) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() let updateFlag = FeatureFlag(flagKey: "abc") @@ -903,18 +903,18 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagStoreMock.updateStoreReceivedUpdatedFlag) == updateFlag expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storedItems) == testContext.flagStoreMock.storedItems expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.storedItems.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags.featureFlags).to(beTrue()) } func onSyncCompleteDeleteFlagSpec() { - let stubFlags = FlagMaintainingMock.stubFlags() - let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) + let stubFlags = FlagMaintainingMock.stubStoredItems() + let testContext = TestContext(startOnline: true).withCached(flags: stubFlags.featureFlags) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() let deleteResponse = DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) @@ -930,13 +930,13 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagStoreMock.deleteFlagReceivedDeleteResponse) == deleteResponse expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storedItems.featureFlags) == testContext.flagStoreMock.storedItems.featureFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.storedItems.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags.featureFlags).to(beTrue()) } func onSyncCompleteErrorSpec() { @@ -1261,15 +1261,15 @@ final class LDClientSpec: QuickSpec { } private func allFlagsSpec() { - let stubFlags = FlagMaintainingMock.stubFlags() + let stubFlags = FlagMaintainingMock.stubStoredItems() describe("allFlags") { it("returns all non-null flag values from store") { - let testContext = TestContext().withCached(flags: stubFlags) + let testContext = TestContext().withCached(flags: stubFlags.featureFlags) testContext.start() - expect(testContext.subject.allFlags) == stubFlags.compactMapValues { $0.value } + expect(testContext.subject.allFlags) == stubFlags.featureFlags.compactMapValues { $0.value } } it("returns nil when client is closed") { - let testContext = TestContext().withCached(flags: stubFlags) + let testContext = TestContext().withCached(flags: stubFlags.featureFlags) testContext.start() testContext.subject.close() expect(testContext.subject.allFlags).to(beNil()) diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift index 9db37b2b..aaab2ad9 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift @@ -8,20 +8,20 @@ final class FlagMaintainingMock: FlagMaintaining { innerStore = FlagStore() } - init(flags: [LDFlagKey: FeatureFlag]) { - innerStore = FlagStore(featureFlags: flags) + init(storedItems: StoredItems) { + innerStore = FlagStore(storedItems: storedItems) } - var featureFlags: [LDFlagKey: FeatureFlag] { - innerStore.featureFlags + var storedItems: StoredItems { + innerStore.storedItems } var replaceStoreCallCount = 0 - var replaceStoreReceivedNewFlags: FeatureFlagCollection? - func replaceStore(newFlags: FeatureFlagCollection) { + var replaceStoreReceivedNewFlags: StoredItems? + func replaceStore(newStoredItems: StoredItems) { replaceStoreCallCount += 1 - replaceStoreReceivedNewFlags = newFlags - innerStore.replaceStore(newFlags: newFlags) + replaceStoreReceivedNewFlags = newStoredItems + innerStore.replaceStore(newStoredItems: newStoredItems) } var updateStoreCallCount = 0 @@ -44,9 +44,10 @@ final class FlagMaintainingMock: FlagMaintaining { innerStore.featureFlag(for: flagKey) } - static func stubFlags() -> [LDFlagKey: FeatureFlag] { - var flags = DarklyServiceMock.Constants.stubFeatureFlags() - flags["userKey"] = FeatureFlag(flagKey: "userKey", + static func stubStoredItems() -> StoredItems { + let flags = DarklyServiceMock.Constants.stubFeatureFlags() + var storedItems = StoredItems(items: flags) + storedItems["userKey"] = .item(FeatureFlag(flagKey: "userKey", value: .string(UUID().uuidString), variation: DarklyServiceMock.Constants.variation, version: DarklyServiceMock.Constants.version, @@ -54,7 +55,7 @@ final class FlagMaintainingMock: FlagMaintaining { trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(30.0), reason: DarklyServiceMock.Constants.reason, - trackReason: false) - return flags + trackReason: false)) + return storedItems } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift index 5fdcd457..c1cf256f 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift @@ -133,6 +133,34 @@ final class FeatureFlagSpec: XCTestCase { } } + func testStoredItemCollectionDecodeValid() throws { + let testData: LDValue = [ + "flags": [ + "key1": ["item": ["key": "key1"]], + "key2": ["tombstone": 10] + ] + ] + let encoded = try JSONEncoder().encode(testData) + let storedItemCollection = try JSONDecoder().decode(StoredItemCollection.self, from: encoded) + XCTAssertEqual(storedItemCollection.flags.count, 2) + XCTAssertEqual(storedItemCollection.flags.featureFlags["key1"]?.flagKey, "key1") + XCTAssertNil(storedItemCollection.flags.featureFlags["key2"]) + } + + func testStoredItemCollectionEncoding() { + let collection = StoredItemCollection(["flag-key": .item(FeatureFlag(flagKey: "flag-key"))]) + encodesToObject(collection) { values in + XCTAssertEqual(values.count, 1) + valueIsObject(values["flags"]) { flags in + valueIsObject(flags["flag-key"]) { storageItem in + valueIsObject(storageItem["item"]) { flagValue in + XCTAssertEqual(flagValue["key"], "flag-key") + } + } + } + } + } + func testFlagCollectionDecodeValid() throws { let testData: LDValue = ["key1": [:], "key2": ["key": "key2"]] let flagCollection = try JSONDecoder().decode(FeatureFlagCollection.self, from: JSONEncoder().encode(testData)) @@ -183,6 +211,19 @@ final class FeatureFlagSpec: XCTestCase { } } +extension StorageItem: Equatable { + public static func == (lhs: StorageItem, rhs: StorageItem) -> Bool { + switch (lhs, rhs) { + case let (.item(lFlag), .item(rFlag)): + return lFlag == rFlag + case let (.tombstone(lVersion), .tombstone(rVersion)): + return lVersion == rVersion + default: + return false + } + } +} + extension FeatureFlag: Equatable { public static func == (lhs: FeatureFlag, rhs: FeatureFlag) -> Bool { lhs.flagKey == rhs.flagKey && diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index 7aac758c..d031dac8 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -22,7 +22,7 @@ final class DarklyServiceSpec: QuickSpec { var serviceFactoryMock: ClientServiceMockFactory = ClientServiceMockFactory() var service: DarklyService! var httpHeaders: HTTPHeaders - let stubFlags = FlagMaintainingMock.stubFlags() + let stubFlags = FlagMaintainingMock.stubStoredItems() init(mobileKey: String = LDConfig.Constants.mockMobileKey, useReport: Bool = Constants.useGetMethod, @@ -83,13 +83,13 @@ final class DarklyServiceSpec: QuickSpec { beforeEach { waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useReportMethod, onActivation: { _ in reportRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useGetMethod, onActivation: { request in getRequestCount += 1 @@ -125,7 +125,7 @@ final class DarklyServiceSpec: QuickSpec { it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.stubFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags.featureFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } @@ -136,13 +136,13 @@ final class DarklyServiceSpec: QuickSpec { testContext.service.flagRequestEtag = requestEtag waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useReportMethod, onActivation: { _ in reportRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useGetMethod, onActivation: { request in getRequestCount += 1 @@ -180,7 +180,7 @@ final class DarklyServiceSpec: QuickSpec { it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.stubFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags.featureFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } @@ -250,13 +250,13 @@ final class DarklyServiceSpec: QuickSpec { beforeEach { waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useGetMethod, onActivation: { _ in getRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useReportMethod, onActivation: { request in reportRequestCount += 1 @@ -291,7 +291,7 @@ final class DarklyServiceSpec: QuickSpec { it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.stubFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags.featureFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } @@ -302,13 +302,13 @@ final class DarklyServiceSpec: QuickSpec { testContext.service.flagRequestEtag = requestEtag waitUntil { done in testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useGetMethod, onActivation: { _ in getRequestCount += 1 }) testContext.serviceMock.stubFlagRequest(statusCode: HTTPURLResponse.StatusCodes.ok, - featureFlags: testContext.stubFlags, + featureFlags: testContext.stubFlags.featureFlags, useReport: Constants.useReportMethod, onActivation: { request in reportRequestCount += 1 @@ -344,7 +344,7 @@ final class DarklyServiceSpec: QuickSpec { it("calls completion with data, response, and no error") { expect(responses).toNot(beNil()) expect(responses?.data).toNot(beNil()) - expect(responses?.data?.flagCollection) == testContext.stubFlags + expect(responses?.data?.flagCollection) == testContext.stubFlags.featureFlags expect(responses?.urlResponse?.httpStatusCode) == HTTPURLResponse.StatusCodes.ok expect(responses?.error).to(beNil()) } @@ -758,7 +758,7 @@ final class DarklyServiceSpec: QuickSpec { private extension Data { var flagCollection: [LDFlagKey: FeatureFlag]? { - return (try? JSONDecoder().decode(FeatureFlagCollection.self, from: self))?.flags + return (try? JSONDecoder().decode([LDFlagKey: FeatureFlag].self, from: self)) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift index 1d0eb99d..76a52eb2 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift @@ -10,7 +10,7 @@ final class CacheConverterSpec: XCTestCase { private static var upToDateData: Data! override class func setUp() { - upToDateData = try! JSONEncoder().encode(["version": 7]) + upToDateData = try! JSONEncoder().encode(["version": 8]) } override func setUp() { @@ -25,7 +25,9 @@ final class CacheConverterSpec: XCTestCase { func testUpToDate() { let v7valueCacheMock = KeyedValueCachingMock() + v7valueCacheMock.keysReturnValue = ["key1", "key2"] serviceFactory.makeFeatureFlagCacheReturnValue.keyedValueCache = v7valueCacheMock + serviceFactory.makeKeyedValueCacheReturnValue = v7valueCacheMock v7valueCacheMock.dataReturnValue = CacheConverterSpec.upToDateData CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: ["key1", "key2"], maxCachedUsers: 0) XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 2) diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift index 6196d8b4..aa2ce83b 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift @@ -5,7 +5,7 @@ import XCTest final class FeatureFlagCacheSpec: XCTestCase { - let testFlagCollection = FeatureFlagCollection(["flag1": FeatureFlag(flagKey: "flag1", variation: 1, flagVersion: 2)]) + let testFlagCollection = StoredItemCollection(["flag1": .item(FeatureFlag(flagKey: "flag1", variation: 1, flagVersion: 2))]) private var serviceFactory: ClientServiceMockFactory! private var mockValueCache: KeyedValueCachingMock { serviceFactory.makeKeyedValueCacheReturnValue } @@ -39,7 +39,7 @@ final class FeatureFlagCacheSpec: XCTestCase { } func testRetrieveEmptyData() throws { - mockValueCache.dataReturnValue = try JSONEncoder().encode(FeatureFlagCollection([:])) + mockValueCache.dataReturnValue = try JSONEncoder().encode(StoredItemCollection([:])) let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) XCTAssertEqual(flagCache.retrieveFeatureFlags(contextKey: "user1")?.count, 0) } @@ -72,7 +72,6 @@ final class FeatureFlagCacheSpec: XCTestCase { count += 1 } else if let received = self.mockValueCache.setReceivedArguments { XCTAssertEqual(received.forKey, "flags-\(hashedUserKey)") - XCTAssertEqual(received.value, try JSONEncoder().encode(FeatureFlagCollection([:]))) count += 2 } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift index 591f7cad..39ff59dc 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift @@ -7,78 +7,102 @@ final class FlagStoreSpec: XCTestCase { let stubFlags = DarklyServiceMock.Constants.stubFeatureFlags() func testInit() { - XCTAssertEqual(FlagStore().featureFlags, [:]) - XCTAssertEqual(FlagStore(featureFlags: self.stubFlags).featureFlags, self.stubFlags) + XCTAssertEqual(FlagStore().storedItems, [:]) + XCTAssertEqual(FlagStore(storedItems: StoredItems(items: self.stubFlags)).storedItems.featureFlags, self.stubFlags) } func testReplaceStore() { - let featureFlags: [LDFlagKey: FeatureFlag] = DarklyServiceMock.Constants.stubFeatureFlags() + let featureFlags = StoredItems(items: DarklyServiceMock.Constants.stubFeatureFlags()) let flagStore = FlagStore() - flagStore.replaceStore(newFlags: FeatureFlagCollection(featureFlags)) - XCTAssertEqual(flagStore.featureFlags, featureFlags) + flagStore.replaceStore(newStoredItems: featureFlags) + XCTAssertEqual(flagStore.storedItems, featureFlags) } func testUpdateStoreNewFlag() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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) + XCTAssertEqual(flagStore.storedItems.count, stubFlags.count + 1) + XCTAssertEqual(flagStore.storedItems.featureFlags["new-int-flag"], flagUpdate) } func testUpdateStoreNewerVersion() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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) + XCTAssertEqual(flagStore.storedItems.count, stubFlags.count) + XCTAssertEqual(flagStore.storedItems.featureFlags[DarklyServiceMock.FlagKeys.int], flagUpdate) } func testUpdateStoreNoVersion() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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) + XCTAssertEqual(flagStore.storedItems.count, stubFlags.count) + XCTAssertEqual(flagStore.storedItems.featureFlags[DarklyServiceMock.FlagKeys.int], flagUpdate) } func testUpdateStoreEarlierOrSameVersion() { let testFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.int) let initialVersion = testFlag.version! - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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) + XCTAssertEqual(flagStore.storedItems.featureFlags, self.stubFlags) } func testDeleteFlagNewerVersion() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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]) + XCTAssertEqual(flagStore.storedItems.count, self.stubFlags.count) + XCTAssertEqual(flagStore.storedItems.featureFlags.count, self.stubFlags.count - 1) + XCTAssertEqual(StorageItem.tombstone(5), flagStore.storedItems[DarklyServiceMock.FlagKeys.int]) } func testDeleteFlagMissingVersion() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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]) + XCTAssertEqual(flagStore.storedItems.count, self.stubFlags.count) + XCTAssertEqual(flagStore.storedItems.featureFlags.count, self.stubFlags.count - 1) + XCTAssertEqual(StorageItem.tombstone(0), flagStore.storedItems[DarklyServiceMock.FlagKeys.int]) } func testDeleteOlderOrNonExistent() { - let flagStore = FlagStore(featureFlags: stubFlags) + let flagStore = FlagStore(storedItems: StoredItems(items: 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) + XCTAssertEqual(flagStore.storedItems.featureFlags, self.stubFlags) + } + + func testCannotReplaceDeletedFlagWithOlderVersion() { + let flagStore = FlagStore(storedItems: StoredItems(items: stubFlags)) + let flagUpdate = FeatureFlag(flagKey: "new-int-flag", value: "abc", version: 0) + flagStore.updateStore(updatedFlag: flagUpdate) + XCTAssertEqual(stubFlags.count + 1, flagStore.storedItems.count) + XCTAssertEqual(stubFlags.count + 1, flagStore.storedItems.featureFlags.count) + + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: "new-int-flag", version: 1)) + XCTAssertEqual(stubFlags.count + 1, flagStore.storedItems.count) + XCTAssertEqual(stubFlags.count, flagStore.storedItems.featureFlags.count) + + flagStore.updateStore(updatedFlag: flagUpdate) + XCTAssertEqual(stubFlags.count + 1, flagStore.storedItems.count) + XCTAssertEqual(stubFlags.count, flagStore.storedItems.featureFlags.count) } func testFeatureFlag() { - let flagStore = FlagStore(featureFlags: stubFlags) - flagStore.featureFlags.forEach { flagKey, featureFlag in - XCTAssertEqual(flagStore.featureFlag(for: flagKey), featureFlag) + let flagStore = FlagStore(storedItems: StoredItems(items: stubFlags)) + flagStore.storedItems.forEach { flagKey, featureFlag in + guard case .item(let flag) = featureFlag + else { + XCTAssertNil(flagStore.featureFlag(for: flagKey)) + return + } + + XCTAssertEqual(flagStore.featureFlag(for: flagKey), flag) } XCTAssertNil(flagStore.featureFlag(for: DarklyServiceMock.FlagKeys.unknown)) } From 790603f092bd5a693d3fa9efc135ce455bdc6dde Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 22 Jun 2022 15:49:56 -0400 Subject: [PATCH 62/90] Update Objective C bindings to work with contexts (#213) --- .jazzy.yaml | 2 + LaunchDarkly.xcodeproj/project.pbxproj | 20 ++++ .../Models/Context/Reference.swift | 10 +- .../ObjectiveC/ObjcLDClient.swift | 28 ++--- .../ObjectiveC/ObjcLDContext.swift | 110 ++++++++++++++++++ .../ObjectiveC/ObjcLDReference.swift | 38 ++++++ .../Models/Context/LDContextSpec.swift | 15 ++- 7 files changed, 200 insertions(+), 23 deletions(-) create mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift create mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDReference.swift diff --git a/.jazzy.yaml b/.jazzy.yaml index d9d3f5b1..85be7878 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -47,6 +47,8 @@ custom_categories: - ObjcLDClient - ObjcLDConfig - ObjcLDUser + - ObjcLDReference + - ObjcLDContext - ObjcLDChangedFlag - ObjcLDValue - ObjcLDValueType diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 36033147..0b0e88e8 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -213,6 +213,14 @@ A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088252837DCA900184942 /* ReferenceSpec.swift */; }; A31088292837DCA900184942 /* KindSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088262837DCA900184942 /* KindSpec.swift */; }; A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33A5F7928466D04000C29C7 /* LDContextStub.swift */; }; + A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; + A36EDFC92853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; + A36EDFCA2853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; + A36EDFCB2853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; + A36EDFCD2853C50B00D91B05 /* ObjcLDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */; }; + A36EDFCE2853C50B00D91B05 /* ObjcLDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */; }; + A36EDFCF2853C50B00D91B05 /* ObjcLDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */; }; + A36EDFD02853C50B00D91B05 /* ObjcLDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */; }; B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; }; B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.swift */; }; B467791324D8AEEC00897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791224D8AEEC00897F00 /* LDSwiftEventSourceStatic */; }; @@ -416,6 +424,8 @@ A31088252837DCA900184942 /* ReferenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceSpec.swift; sourceTree = ""; }; A31088262837DCA900184942 /* KindSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KindSpec.swift; sourceTree = ""; }; A33A5F7928466D04000C29C7 /* LDContextStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextStub.swift; sourceTree = ""; }; + A36EDFC72853883400D91B05 /* ObjcLDReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDReference.swift; sourceTree = ""; }; + A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDContext.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 = ""; }; B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = ""; }; @@ -634,12 +644,14 @@ 835E1D341F63332C00184DB4 /* ObjectiveC */ = { isa = PBXGroup; children = ( + A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */, 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */, 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */, 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */, 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */, B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */, 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */, + A36EDFC72853883400D91B05 /* ObjcLDReference.swift */, ); path = ObjectiveC; sourceTree = ""; @@ -1150,6 +1162,7 @@ 831188572113AE0B00D77CB5 /* FlagChangeNotifier.swift in Sources */, 8311884D2113ADE200D77CB5 /* FlagsUnchangedObserver.swift in Sources */, 8311885F2113AE2D00D77CB5 /* HTTPURLRequest.swift in Sources */, + A36EDFD02853C50B00D91B05 /* ObjcLDContext.swift in Sources */, B4C9D4362489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 831188452113ADC500D77CB5 /* LDClient.swift in Sources */, A310881E2837DC0400184942 /* Kind.swift in Sources */, @@ -1184,6 +1197,7 @@ 8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */, 8311885B2113AE1D00D77CB5 /* Throttler.swift in Sources */, 8311884E2113ADE500D77CB5 /* Event.swift in Sources */, + A36EDFCB2853883400D91B05 /* ObjcLDReference.swift in Sources */, 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */, 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */, B4C9D4312489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, @@ -1202,6 +1216,7 @@ buildActionMask = 2147483647; files = ( B468E71224B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, + A36EDFCF2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, 831EF34320655E730001C643 /* LDCommon.swift in Sources */, 831EF34420655E730001C643 /* LDConfig.swift in Sources */, A31088212837DC0400184942 /* LDContext.swift in Sources */, @@ -1212,6 +1227,7 @@ 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */, C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */, + A36EDFCA2853883400D91B05 /* ObjcLDReference.swift in Sources */, 8354AC722243166900CDE602 /* FeatureFlagCache.swift in Sources */, A310881D2837DC0400184942 /* Kind.swift in Sources */, C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */, @@ -1267,6 +1283,7 @@ 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */, 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */, B4C9D4332489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, + A36EDFCD2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, 8354EFE51F263DAC00C05156 /* FeatureFlag.swift in Sources */, 8372668C20D4439600BD1088 /* DateFormatter.swift in Sources */, A310881B2837DC0400184942 /* Kind.swift in Sources */, @@ -1301,6 +1318,7 @@ 8347BB0C21F147E100E56BCD /* LDTimer.swift in Sources */, B468E71024B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, 8354AC702243166900CDE602 /* FeatureFlagCache.swift in Sources */, + A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */, 8358F2621F47747F00ECE1AF /* FlagChangeObserver.swift in Sources */, 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */, 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */, @@ -1372,6 +1390,7 @@ 83D9EC782062DEAB004D7FA6 /* LDUser.swift in Sources */, B4C9D4342489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, + A36EDFCE2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */, 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */, A310881C2837DC0400184942 /* Kind.swift in Sources */, @@ -1406,6 +1425,7 @@ C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */, B4C9D4392489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, + A36EDFC92853883400D91B05 /* ObjcLDReference.swift in Sources */, 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */, 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */, 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift index 4d97f93f..8550dd92 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift @@ -1,6 +1,6 @@ import Foundation -enum ReferenceError: Codable, Equatable, Error { +public enum ReferenceError: Codable, Equatable, Error { case empty case doubleSlash case invalidEscapeSequence @@ -25,7 +25,7 @@ enum ReferenceError: Codable, Equatable, Error { } extension ReferenceError: CustomStringConvertible { - var description: String { + public var description: String { switch self { case .empty: return "empty" case .doubleSlash: return "doubleSlash" @@ -129,11 +129,11 @@ public struct Reference: Codable, Equatable, Hashable { return error == nil } - internal func getError() -> ReferenceError? { + public func getError() -> ReferenceError? { return error } - public func depth() -> Int { + internal func depth() -> Int { return components.count } @@ -141,7 +141,7 @@ public struct Reference: Codable, Equatable, Hashable { return rawPath } - public func component(_ index: Int) -> (String, Int?)? { + internal func component(_ index: Int) -> (String, Int?)? { if index >= self.depth() { return nil } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index 8e73f12a..2803748c 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -110,10 +110,9 @@ public final class ObjcLDClient: NSObject { - parameter user: The ObjcLDUser set with the desired user. */ -// TODO(mmk) Come back to this -// @objc public func identify(context: ObjcLDContext) { -// ldClient.identify(context: context.context, completion: nil) -// } + @objc public func identify(context: ObjcLDContext) { + ldClient.identify(context: context.context, completion: nil) + } /** The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. @@ -127,10 +126,9 @@ public final class ObjcLDClient: NSObject { - parameter user: The ObjcLDUser set with the desired user. - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. (Optional) */ -// TODO(mmk) Come back to this -// @objc public func identify(context: ObjcLDContext, completion: (() -> Void)? = nil) { -// ldClient.identify(context: context.context, completion: completion) -// } + @objc public func identify(context: ObjcLDContext, completion: (() -> Void)? = nil) { + ldClient.identify(context: context.context, completion: completion) + } /** Stops the LDClient. Stopping the client means the LDClient goes offline and stops recording events. LDClient will no longer provide feature flag values, only returning default values. @@ -552,10 +550,9 @@ public final class ObjcLDClient: NSObject { - parameter completion: Closure called when the embedded `setOnline` call completes. (Optional) */ /// - Tag: start -// TODO(mmk) Come back to this -// @objc public static func start(configuration: ObjcLDConfig, context: ObjcLDContext, completion: (() -> Void)? = nil) { -// LDClient.start(config: configuration.config, context: context.context, completion: completion) -// } + @objc public static func start(configuration: ObjcLDConfig, context: ObjcLDContext, completion: (() -> Void)? = nil) { + LDClient.start(config: configuration.config, context: context.context, completion: completion) + } /** See [start](x-source-tag://start) for more information on starting the SDK. @@ -565,10 +562,9 @@ public final class ObjcLDClient: NSObject { - parameter startWaitSeconds: A TimeInterval that determines when the completion will return if no flags have been returned from the network. - parameter completion: Closure called when the embedded `setOnline` call completes. Takes a Bool that indicates whether the completion timedout as a parameter. (Optional) */ -// TODO(mmk) Come back to this -// @objc public static func start(configuration: ObjcLDConfig, context: ObjcLDContext, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { -// LDClient.start(config: configuration.config, context: context.context, startWaitSeconds: startWaitSeconds, completion: completion) -// } + @objc public static func start(configuration: ObjcLDConfig, context: ObjcLDContext, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + LDClient.start(config: configuration.config, context: context.context, startWaitSeconds: startWaitSeconds, completion: completion) + } private init(client: LDClient) { ldClient = client diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift new file mode 100644 index 00000000..bfe337d8 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift @@ -0,0 +1,110 @@ +import Foundation + +@objc(LDContext) +public final class ObjcLDContext: NSObject { + var context: LDContext + + init(_ context: LDContext) { + self.context = context + } + + @objc public func fullyQualifiedKey() -> String { context.fullyQualifiedKey() } + @objc public func isMulti() -> Bool { context.isMulti() } + @objc public func contextKeys() -> [String: String] { context.contextKeys() } + @objc public func getValue(reference: ObjcLDReference) -> ObjcLDValue? { + guard let value = context.getValue(reference.reference) + else { return nil } + + return ObjcLDValue(wrappedValue: value) + } +} + +@objc(LDContextBuilder) +public final class ObjcLDContextBuilder: NSObject { + var builder: LDContextBuilder + + @objc public init(key: String) { + builder = LDContextBuilder(key: key) + } + + // Initializer to wrap the Swift LDContextBuilder into ObjcLDContextBuilder for use in + // Objective-C apps. + init(_ builder: LDContextBuilder) { + self.builder = builder + } + + @objc public func kind(kind: String) { builder.kind(kind) } + @objc public func key(key: String) { builder.key(key) } + @objc public func name(name: String) { builder.name(name) } + @objc public func secondary(secondary: String) { builder.secondary(secondary) } + @objc public func transient(transient: Bool) { builder.transient(transient) } + @objc public func addPrivateAttribute(reference: ObjcLDReference) { builder.addPrivateAttribute(reference.reference) } + @objc public func removePrivateAttribute(reference: ObjcLDReference) { builder.removePrivateAttribute(reference.reference) } + + @discardableResult + @objc public func trySetValue(name: String, value: ObjcLDValue) -> Bool { + builder.trySetValue(name, value.wrappedValue) + } + + @objc public func build() -> ContextBuilderResult { + switch builder.build() { + case .success(let context): + return ContextBuilderResult.fromSuccess(context) + case .failure(let error): + return ContextBuilderResult.fromError(error) + } + } +} + +@objc(LDMultiContextBuilder) +public final class ObjcLDMultiContextBuilder: NSObject { + var builder: LDMultiContextBuilder + + @objc public override init() { + builder = LDMultiContextBuilder() + } + + @objc public func addContext(context: ObjcLDContext) { + builder.addContext(context.context) + } + + // Initializer to wrap the Swift LDMultiContextBuilder into ObjcLDMultiContextBuilder for use in + // Objective-C apps. + init(_ builder: LDMultiContextBuilder) { + self.builder = builder + } + + @objc public func build() -> ContextBuilderResult { + switch builder.build() { + case .success(let context): + return ContextBuilderResult.fromSuccess(context) + case .failure(let error): + return ContextBuilderResult.fromError(error) + } + } +} + +@objc public class ContextBuilderResult: NSObject { + @objc public private(set) var success: ObjcLDContext? + @objc public private(set) var failure: NSError? + + private override init() { + super.init() + success = nil + failure = nil + } + + public static func fromSuccess(_ success: LDContext) -> ContextBuilderResult { + ContextBuilderResult(success, nil) + } + + public static func fromError(_ error: ContextBuilderError) -> ContextBuilderResult { + ContextBuilderResult(nil, error) + } + + private convenience init(_ arg1: LDContext?, _ arg2: ContextBuilderError?) { + self.init() + success = arg1.map { ObjcLDContext($0) } + failure = arg2.map { $0 as NSError } + } +} diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDReference.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDReference.swift new file mode 100644 index 00000000..5b1c7994 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDReference.swift @@ -0,0 +1,38 @@ +import Foundation + +@objc(Reference) +public final class ObjcLDReference: NSObject { + var reference: Reference + + @objc public init(value: String) { + reference = Reference(value) + } + + // Initializer to wrap the Swift Reference into ObjcLDReference for use in + // Objective-C apps. + init(_ reference: Reference) { + self.reference = reference + } + + @objc public func isValid() -> Bool { reference.isValid() } + + @objc public func getError() -> NSError? { + guard let error = reference.getError() + else { return nil } + + return error as NSError + } +} + +@objc(ReferenceError) +public final class ObjcLDReferenceError: NSObject { + var error: ReferenceError + + // Initializer to wrap the Swift ReferenceError into ObjcLDReferenceError for use in + // Objective-C apps. + init(_ error: ReferenceError) { + self.error = error + } + + override public var description: String { self.error.description } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift index ed3bb97a..6a9f569c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift @@ -4,8 +4,6 @@ import XCTest @testable import LaunchDarkly final class LDContextSpec: XCTestCase { - // TOOD(mmk) Make sure we cannot make a context with a kind of kind - func testBuildCanCreateSimpleContext() throws { var builder = LDContextBuilder(key: "context-key") builder.name("Name") @@ -15,6 +13,19 @@ final class LDContextSpec: XCTestCase { XCTAssertFalse(context.isMulti()) } + func testBuilderWillNotAcceptKindOfTypeKind() { + var builder = LDContextBuilder(key: "context-key") + builder.kind("kind") + + guard case .failure(let error) = builder.build() + else { + XCTFail("Builder should not create context with kind 'kind'") + return + } + + XCTAssertEqual(error, ContextBuilderError.invalidKind) + } + func testBuilderCanHandleMissingKind() throws { var builder = LDContextBuilder(key: "key") From da4cd17f3f5b7c6c3b52a296cc109bff7a983fde Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 22 Jun 2022 16:18:41 -0400 Subject: [PATCH 63/90] Expand documentation to include context classes (#210) There are still some areas that are pending further documentation. Other SDKs are also waiting for this same documentation. But this commit does include some standard language we have adopted in other SDKs and so helps move the needle forward somewhat. --- .jazzy.yaml | 4 + .../LaunchDarkly/Models/Context/Kind.swift | 4 +- .../Models/Context/LDContext.swift | 171 +++++++++++++++--- .../Models/Context/Reference.swift | 70 ++++++- 4 files changed, 223 insertions(+), 26 deletions(-) diff --git a/.jazzy.yaml b/.jazzy.yaml index 85be7878..f5e19d1d 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -18,6 +18,10 @@ custom_categories: - LDClient - LDConfig - LDUser + - LDContext + - LDContextBuilder + - Reference + - LDMultiContextBuilder - LDEvaluationDetail - LDValue diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift index edb76b9b..60852312 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift @@ -23,11 +23,11 @@ public enum Kind: Codable, Equatable, Hashable { try container.encode(self.description) } - public func isMulti() -> Bool { + internal func isMulti() -> Bool { self == .multi || self == .custom("multi") } - public func isUser() -> Bool { + internal func isUser() -> Bool { self == .user || self == .custom("user") || self == .custom("") } diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 72b723fb..e11702e8 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -9,7 +9,14 @@ public enum ContextBuilderError: Error { case duplicateKinds } -/// TKTK +/// LDContext is a collection of attributes that can be referenced in flag evaluations and analytics +/// events. +/// +/// (TKTK - some conceptual text here, and/or a link to a docs page) +/// +/// To create an LDContext of a single kind, such as a user, you may use `LDContextBuilder`. +/// +/// To create an LDContext with multiple kinds, use `LDMultiContextBuilder`. public struct LDContext: Encodable, Equatable { static let storedIdKey: String = "ldDeviceIdentifier" @@ -34,16 +41,16 @@ public struct LDContext: Encodable, Equatable { self.init(canonicalizedKey: LDContext.defaultKey(environmentReporting: environmentReporting)) } - public struct Meta: Codable { - public var secondary: String? - public var privateAttributes: [Reference]? - public var redactedAttributes: [String]? + struct Meta: Codable { + var secondary: String? + var privateAttributes: [Reference]? + var redactedAttributes: [String]? enum CodingKeys: CodingKey { case secondary, privateAttributes, redactedAttributes } - public var isEmpty: Bool { + var isEmpty: Bool { secondary == nil && (privateAttributes?.isEmpty ?? true) && (redactedAttributes?.isEmpty ?? true) @@ -55,7 +62,7 @@ public struct LDContext: Encodable, Equatable { self.redactedAttributes = redactedAttributes } - public init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let secondary = try container.decodeIfPresent(String.self, forKey: .secondary) @@ -66,7 +73,7 @@ public struct LDContext: Encodable, Equatable { self.redactedAttributes = [] } - public func encode(to encoder: Encoder) throws { + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(secondary, forKey: .secondary) @@ -577,7 +584,15 @@ extension LDContext: Decodable { extension LDContext: TypeIdentifying {} -/// TKTK +/// Contains methods for building a single kind `LDContext` with a specified key, defaulting to kind +/// "user". +/// +/// You may use these methods to set additional attributes and/or change the kind before calling +/// `LDContextBuilder.build()`. If you do not change any values, the defaults for the `LDContext` are that its +/// kind is "user", its key is set to whatever value you passed to `LDContextBuilder.init(key:)`, its transient attribute +/// is false, and it has no values for any other attributes. +/// +/// To define a multi-kind LDContext, see `LDMultiContextBuilder`. public struct LDContextBuilder { private var kind: String = Kind.user.description @@ -595,27 +610,77 @@ public struct LDContextBuilder { // never allowed to be empty. fileprivate var allowEmptyKey: Bool = false - /// TKTK + /// Create a new LDContextBuilder with the provided `key`. public init(key: String) { self.key = key } - /// TKTK + /// Sets the LDContext's kind attribute. + /// + /// Every LDContext has a kind. Setting it to an empty string is equivalent to the default kind + /// of "user". This value is case-sensitive. Validation rules are as follows: + /// + /// - It may only contain letters, numbers, and the characters ".", "_", and "-". + /// - It cannot equal the literal string "kind". + /// - It cannot equal "multi". + /// + /// If the value is invalid, you will receive an error when `LDContextBuilder.build()` is called. public mutating func kind(_ kind: String) { self.kind = kind } - /// TKTK + /// Sets the LDContext's key attribute. + /// + /// Every LDContext has a key, which is always a string. There are no restrictions on its value. + /// It may be an empty string. + /// + /// The key attribute can be referenced by flag rules, flag target lists, and segments. public mutating func key(_ key: String) { self.key = key } - /// TKTK + /// Sets the LDContext's name attribute. + /// + /// This attribute is optional. It has the following special rules: + /// + /// - Unlike most other attributes, it is always a string if it is specified. + /// - The LaunchDarkly dashboard treats this attribute as the preferred display name for users. public mutating func name(_ name: String) { self.name = name } - /// TKTK + /// Sets the value of any attribute for the Context. + /// + /// This includes only attributes that are addressable in evaluations -- not metadata such as + /// secondary or private. If `name` is "privateAttributeNames", it is ignored and no + /// attribute is set. + /// + /// This method uses the `LDValue` type to represent a value of any JSON type: null, + /// boolean, number, string, array, or object. For all attribute names that do not have special + /// meaning to LaunchDarkly, you may use any of those types. Values of different JSON types are + /// always treated as different values: for instance, null, false, and the empty string "" are + /// not the same, and the number 1 is not the same as the string "1". + /// + /// The following attribute names have special restrictions on their value types, and any value + /// of an unsupported type will be ignored (leaving the attribute unchanged): + /// + /// - "kind", "key": Must be a string. See `LDContextBuilder.kind(_:)` and `LDContextBuilder.key(_:)`. + /// + /// - "name": Must be a string or null. See `LDContextBuilder.name(_:)`. + /// + /// - "transient": Must be a boolean. See `LDContextBuilder.transient(_:)`. + /// + /// Values that are JSON arrays or objects have special behavior when referenced in + /// flag/segment rules. + /// + /// A value of `LDValue.null` is equivalent to removing any current non-default value + /// of the attribute. Null is not a valid attribute value in the LaunchDarkly model; any + /// expressions in feature flags that reference an attribute with a null value will behave as + /// if the attribute did not exist. + /// + /// This method returns true for success, or false if the parameters + /// violated one of the restrictions described above (for instance, + /// attempting to set "key" to a value that was not a string). @discardableResult public mutating func trySetValue(_ name: String, _ value: LDValue) -> Bool { switch (name, value) { @@ -659,27 +724,67 @@ public struct LDContextBuilder { return true } - /// TKTK + /// Sets a secondary key for the LDContext. + /// + /// This affects feature flag targeting + /// + /// as follows: if you have chosen to bucket users by a specific attribute, the secondary key + /// (if set) is used to further distinguish between users who are otherwise identical according + /// to that attribute. This value is not addressable as an attribute in evaluations: that is, a + /// rule clause cannot use the attribute name "secondary". + /// + /// Setting this value to an empty string is not the same as leaving it unset. public mutating func secondary(_ secondary: String) { self.secondary = secondary } - /// TKTK + /// Sets whether the LDContext is only intended for flag evaluations and should not be indexed by + /// LaunchDarkly. + /// + /// The default value is false. False means that this LDContext represents an entity such as a + /// user that you want to be able to see on the LaunchDarkly dashboard. + /// + /// Setting transient to true excludes this LDContext from the database that is used by the + /// dashboard. It does not exclude it from analytics event data, so it is not the same as + /// making attributes private; all non-private attributes will still be included in events and + /// data export. + /// + /// This value is also addressable in evaluations as the attribute name "transient". It is + /// always treated as a boolean true or false in evaluations. public mutating func transient(_ transient: Bool) { self.transient = transient } - /// TKTK + /// Provide a reference to designate any number of LDContext attributes as private: that is, + /// their values will not be sent to LaunchDarkly. + /// + /// (TKTK: possibly move some of this conceptual information to a non-platform-specific docs page and/or + /// have docs team copyedit it here) + /// + /// See `Reference` for details on how to construct a valid reference. + /// + /// This action only affects analytics events that involve this particular LDContext. To mark some (or all) + /// LDContext attributes as private for all uses, use the overall event configuration for the SDK. + /// + /// The attributes "kind" and "key", and the metadata properties set by secondary and transient, + /// cannot be made private. public mutating func addPrivateAttribute(_ reference: Reference) { self.privateAttributes.append(reference) } - /// TKTK + /// Remove any reference provided through `addPrivateAttribute(_:)`. If the reference was + /// added more than once, this method will remove all instances of it. public mutating func removePrivateAttribute(_ reference: Reference) { self.privateAttributes.removeAll { $0 == reference } } - /// TKTK + /// Creates a LDContext from the current LDContextBuilder properties. + /// + /// The LDContext is immutable and will not be affected by any subsequent actions on the + /// LDContextBuilder. + /// + /// It is possible to specify invalid attributes for a LDContextBuilder, such as an empty key. + /// In those situations, this method returns a Result.failure public func build() -> Result { guard let kind = Kind(self.kind) else { return Result.failure(.invalidKind) @@ -709,19 +814,39 @@ public struct LDContextBuilder { extension LDContextBuilder: TypeIdentifying { } -/// TKTK +/// Contains method for building a multi-kind `LDContext`. +/// +/// Use this type if you need to construct a LDContext that has multiple kind values, each with its +/// own nested LDContext. To define a single-kind context, use `LDContextBuilder` instead. +/// +/// Obtain an instance of LDMultiContextBuilder by calling `LDMultiContextBuilder.init()`; then, call +/// `LDMultiContextBuilder.addContext(_:)` to specify the nested LDContext for each kind. +/// LDMultiContextBuilder setters return a reference the same builder, so they can be chained +/// together. public struct LDMultiContextBuilder { private var contexts: [LDContext] = [] - /// TKTK + /// Create a new LDMultiContextBuilder with the provided `key`. public init() {} - /// TKTK + /// Adds a nested context for a specific kind to a LDMultiContextBuilder. + /// + /// It is invalid to add more than one context with the same Kind. This error is detected when + /// you call `LDMultiContextBuilder.build()`. public mutating func addContext(_ context: LDContext) { contexts.append(context) } - /// TKTK + /// Creates a LDContext from the current properties. + /// + /// The LDContext is immutable and will not be affected by any subsequent actions on the + /// LDMultiContextBuilder. + /// + /// It is possible for a LDMultiContextBuilder to represent an invalid state. In those + /// situations, a Result.failure will be returned. + /// + /// If only one context kind was added to the builder, `build` returns a single-kind context rather + /// than a multi-kind context. public func build() -> Result { if contexts.isEmpty { return Result.failure(.emptyMultiKind) diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift index 8550dd92..94afc7fa 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift @@ -34,6 +34,46 @@ extension ReferenceError: CustomStringConvertible { } } +/// Represents an attribute name or path expression identifying a value within a Context. +/// +/// This can be used to retrieve a value with `LDContext.getValue(_:)`, or to identify an attribute or +/// nested value that should be considered private with +/// `LDContextBuilder.addPrivateAttribute(_:)` (the SDK configuration can also have a list of +/// private attribute references). +/// +/// This is represented as a separate type, rather than just a string, so that validation and parsing can +/// be done ahead of time if an attribute reference will be used repeatedly later (such as in flag +/// evaluations). +/// +/// If the string starts with '/', then this is treated as a slash-delimited path reference where the +/// first component is the name of an attribute, and subsequent components are the names of nested JSON +/// object properties (or, if they are numeric, the indices of JSON array elements). In this syntax, the +/// escape sequences "~0" and "~1" represent '~' and '/' respectively within a path component. +/// +/// If the string does not start with '/', then it is treated as the literal name of an attribute. +/// +/// For instance, if the JSON representation of a context is as follows-- +/// +/// ```json +/// { +/// "kind": "user", +/// "key": "123", +/// "name": "xyz", +/// "address": { +/// "street": "99 Main St.", +/// "city": "Westview" +/// }, +/// "groups": [ "p", "q" ], +/// "a/b": "ok" +/// } +/// ``` +/// +/// -- then +/// +/// - Reference("name") or Reference("/name") would refer to the value "xyz" +/// - Reference("/address/street") would refer to the value "99 Main St." +/// - Reference("/groups/0") would refer to the value "p" +/// - Reference("a/b") or Reference("/a~1b") would refer to the value "ok" public struct Reference: Codable, Equatable, Hashable { private var error: ReferenceError? private var rawPath: String @@ -74,6 +114,11 @@ public struct Reference: Codable, Equatable, Hashable { return Result.success(output) } + /// Construct a new Reference. + /// + /// This constructor always returns a Reference that preserves the original string, even if + /// validation fails, so that serializing the Reference to JSON will produce the original + /// string. public init(_ value: String) { rawPath = value @@ -125,10 +170,13 @@ public struct Reference: Codable, Equatable, Hashable { try container.encode(self.rawPath) } + /// Returns whether or not the reference provided is valid. public func isValid() -> Bool { return error == nil } + /// If the reference is invalid, this method will return an error description; otherwise, it + /// will return an empty string. public func getError() -> ReferenceError? { return error } @@ -137,11 +185,31 @@ public struct Reference: Codable, Equatable, Hashable { return components.count } + /// Returns raw string that was passed into constructor. internal func raw() -> String { return rawPath } - internal func component(_ index: Int) -> (String, Int?)? { + /// Retrieves a single path component from the attribute reference. + /// + /// For a simple attribute reference such as "name" with no leading slash, + /// if index is zero, `component` returns the attribute name and None. + /// + /// For an attribute reference with a leading slash, if index is less than + /// `Reference.depth()`, `component` returns the path component as a string + /// for its first value. The second value is an `Int?` that is the integer + /// value of that string if applicable, or None if the string does not + /// represent an integer; this is used to implement a "find a value by + /// index within a JSON array" behavior similar to JSON Pointer. + /// + /// If index is out of range, it returns None. + /// + /// ``` + /// Reference("a").component(0); // returns ("a", nil) + /// Reference("/a/b").component(1); // returns ("b", nil) + /// Reference("/a/3").component(1); // returns ("3", 3) + /// ``` + public func component(_ index: Int) -> (String, Int?)? { if index >= self.depth() { return nil } From 5a6748b1dd4379899925f61d5d7277ecb746accc Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Fri, 24 Jun 2022 16:38:42 -0400 Subject: [PATCH 64/90] Add some missing JSON handling tests (#212) We rely heavily on the SDK test harness to verify the behavior of this SDK, particularly when it comes to Context JSON encoding and decoding. However, it is useful to get some basic coverage in place in this SDK. This allows us to iterate faster, and allows us to test things on a more granular level than the SDK test harness is able. --- LaunchDarkly.xcodeproj/project.pbxproj | 20 +-- .../Models/Context/KindSpec.swift | 26 ++++ .../Models/Context/LDContextCodableSpec.swift | 136 ++++++++++++++++++ 3 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 0b0e88e8..0fc0a797 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -212,7 +212,9 @@ A31088272837DCA900184942 /* LDContextSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088242837DCA900184942 /* LDContextSpec.swift */; }; A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088252837DCA900184942 /* ReferenceSpec.swift */; }; A31088292837DCA900184942 /* KindSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088262837DCA900184942 /* KindSpec.swift */; }; + A31285D42863AFE900D84CF4 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = A31285D32863AFE900D84CF4 /* LDSwiftEventSourceStatic */; }; A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33A5F7928466D04000C29C7 /* LDContextStub.swift */; }; + A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */; }; A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; A36EDFC92853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; A36EDFCA2853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; @@ -223,7 +225,6 @@ A36EDFD02853C50B00D91B05 /* ObjcLDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */; }; B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; }; B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.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 */; }; B467791924D8AEFC00897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791824D8AEFC00897F00 /* LDSwiftEventSourceStatic */; }; @@ -424,6 +425,7 @@ A31088252837DCA900184942 /* ReferenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceSpec.swift; sourceTree = ""; }; A31088262837DCA900184942 /* KindSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KindSpec.swift; sourceTree = ""; }; A33A5F7928466D04000C29C7 /* LDContextStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextStub.swift; sourceTree = ""; }; + A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextCodableSpec.swift; sourceTree = ""; }; A36EDFC72853883400D91B05 /* ObjcLDReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDReference.swift; sourceTree = ""; }; A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDContext.swift; sourceTree = ""; }; B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCacheSpec.swift; sourceTree = ""; }; @@ -463,7 +465,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B467791324D8AEEC00897F00 /* LDSwiftEventSourceStatic in Frameworks */, + A31285D42863AFE900D84CF4 /* LDSwiftEventSourceStatic in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -805,6 +807,7 @@ A31088242837DCA900184942 /* LDContextSpec.swift */, A31088252837DCA900184942 /* ReferenceSpec.swift */, A31088262837DCA900184942 /* KindSpec.swift */, + A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */, ); path = Context; sourceTree = ""; @@ -920,7 +923,7 @@ ); name = LaunchDarkly_iOS; packageProductDependencies = ( - B467791224D8AEEC00897F00 /* LDSwiftEventSourceStatic */, + A31285D32863AFE900D84CF4 /* LDSwiftEventSourceStatic */, ); productName = Darkly; productReference = 8354EFC21F22491C00C05156 /* LaunchDarkly.framework */; @@ -1371,6 +1374,7 @@ 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, 831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */, 838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */, + A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */, 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */, 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */, 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */, @@ -1888,6 +1892,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + A31285D32863AFE900D84CF4 /* LDSwiftEventSourceStatic */ = { + isa = XCSwiftPackageProductDependency; + package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; + productName = LDSwiftEventSourceStatic; + }; B445A6E324C0D1E3000BAD6D /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; @@ -1908,11 +1917,6 @@ package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; productName = LDSwiftEventSource; }; - B467791224D8AEEC00897F00 /* LDSwiftEventSourceStatic */ = { - isa = XCSwiftPackageProductDependency; - package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; - productName = LDSwiftEventSourceStatic; - }; B467791424D8AEF300897F00 /* LDSwiftEventSourceStatic */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift index ca6e3f71..611f51a4 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift @@ -43,4 +43,30 @@ final class KindSpec: XCTestCase { XCTAssertEqual(Kind("multi"), .multi) XCTAssertEqual(Kind("org"), .custom("org")) } + + func testKindCanEncodeAndDecodeAppropriately() throws { + // I know it seems silly to have these test cases be arrays instead of + // simple strings. However, if I can please kindly direct your + // attention to https://github.com/apple/swift-corelibs-foundation/issues/4402 + // you will see that older versions had an issue encoding and decoding JSON + // fragments like simple strings. + // + // Using an array like this is a simple but effective workaround. + let testCases = [ + ("[\"user\"]", Kind("user"), true, false), + ("[\"multi\"]", Kind("multi"), false, true), + ("[\"org\"]", Kind("org"), false, false) + ] + + for (json, expectedKind, isUser, isMulti) in testCases { + let kindJson = Data(json.utf8) + let kinds = try JSONDecoder().decode([Kind].self, from: kindJson) + + XCTAssertEqual(expectedKind, kinds[0]) + XCTAssertEqual(isUser, kinds[0].isUser()) + XCTAssertEqual(isMulti, kinds[0].isMulti()) + + try XCTAssertEqual(kindJson, JSONEncoder().encode(kinds)) + } + } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift new file mode 100644 index 00000000..d6954ac9 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift @@ -0,0 +1,136 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class LDContextCodableSpec: XCTestCase { + func testUserFormatIsConvertedToSingleContextFormat() throws { + let testCases = [ + ("{\"key\": \"foo\"}", "{\"key\": \"foo\", \"kind\": \"user\"}"), + ("{\"key\" : \"foo\", \"name\" : \"bar\"}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"name\" : \"bar\"}"), + ("{\"key\" : \"foo\", \"custom\" : {\"a\" : \"b\"}}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"a\" : \"b\"}"), + ("{\"key\" : \"foo\", \"anonymous\" : true}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"transient\" : true}"), + ("{\"key\" : \"foo\", \"secondary\" : \"bar\"}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"_meta\" : {\"secondary\" : \"bar\"}}"), + ("{\"key\" : \"foo\", \"ip\" : \"1\", \"privateAttributeNames\" : [\"ip\"]}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"ip\" : \"1\", \"_meta\" : { \"privateAttributes\" : [\"ip\"]} }") + ] + + for (userJson, explicitFormat) in testCases { + let userContext = try JSONDecoder().decode(LDContext.self, from: Data(userJson.utf8)) + let explicitContext = try JSONDecoder().decode(LDContext.self, from: Data(explicitFormat.utf8)) + + XCTAssertEqual(userContext, explicitContext) + } + } + + func testSingleContextKindsAreDecodedAndEncodedWithoutLossOfInformation() throws { + let testCases = [ + "{\"kind\":\"org\",\"key\":\"foo\"}", + "{\"kind\":\"user\",\"key\":\"foo\"}", + "{\"kind\":\"foo\",\"key\":\"bar\",\"transient\":true}", + "{\"kind\":\"foo\",\"key\":\"bar\",\"name\":\"Foo\",\"_meta\":{\"privateAttributes\":[\"a\"],\"secondary\":\"baz\"}}", + "{\"kind\":\"foo\",\"key\":\"bar\",\"object\":{\"a\":\"b\"}}" + ] + + for json in testCases { + let context = try JSONDecoder().decode(LDContext.self, from: Data(json.utf8)) + let output = try JSONEncoder().encode(context) + let outputJson = String(data: output, encoding: .utf8) + + XCTAssertEqual(json, outputJson) + } + } + + func testAttributeRetractionWorksCorrectly() throws { + let json = """ + { + "kind":"foo", + "key":"bar", + "name":"Foo", + "a": "should be removed", + "b": { + "c": "should be removed", + "d": "should be retained" + }, + "_meta":{ + "privateAttributes":["a", "/b/c"], + "secondary":"baz" + } + } + """ + + let context = try JSONDecoder().decode(LDContext.self, from: Data(json.utf8)) + let output = try JSONEncoder().encode(context) + let outputJson = String(data: output, encoding: .utf8) + + XCTAssertTrue(outputJson!.contains("should be retained")) + XCTAssertFalse(outputJson!.contains("should be removed")) + } + + func testGlobalAttributeRetractionWorksCorrectly() throws { + let json = """ + { + "kind":"foo", + "key":"bar", + "name":"Foo", + "a": "should be removed", + "b": { + "c": "should be removed", + "d": "should be retained" + }, + "_meta":{ + "privateAttributes":["a", "/b/c"], + "secondary":"baz" + } + } + """ + + let context = try JSONDecoder().decode(LDContext.self, from: Data(json.utf8)) + + let encodingConfig: [CodingUserInfoKey: Any] = + [ + LDContext.UserInfoKeys.globalPrivateAttributes: [Reference("a"), Reference("/b/c")] + ] + let encoder = JSONEncoder() + encoder.userInfo = encodingConfig + let output = try encoder.encode(context) + let outputJson = String(data: output, encoding: .utf8) + + XCTAssertTrue(outputJson!.contains("should be retained")) + XCTAssertFalse(outputJson!.contains("should be removed")) + } + + func testCanDecodeIntoMultiContextCorrectly() throws { + let json = """ + { + "kind": "multi", + "user": { + "key": "foo-key", + }, + "bar": { + "key": "bar-key" + }, + "baz": { + "key": "baz-key", + "transient": true + } + } + """ + let context = try JSONDecoder().decode(LDContext.self, from: Data(json.utf8)) + + let userBuilder = LDContextBuilder(key: "foo-key") + var barBuilder = LDContextBuilder(key: "bar-key") + barBuilder.kind("bar") + + var bazBuilder = LDContextBuilder(key: "baz-key") + bazBuilder.kind("baz") + bazBuilder.transient(true) + + var multiBuilder = LDMultiContextBuilder() + multiBuilder.addContext(try userBuilder.build().get()) + multiBuilder.addContext(try barBuilder.build().get()) + multiBuilder.addContext(try bazBuilder.build().get()) + let expectedContext = try multiBuilder.build().get() + + XCTAssertEqual(expectedContext, context) + } +} From fe1aa6f3896f5aa73b85d2ada2490845b5dd1c5d Mon Sep 17 00:00:00 2001 From: Ember Stevens Date: Tue, 28 Jun 2022 12:36:43 -0700 Subject: [PATCH 65/90] Updates URLs --- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 2 +- LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 2 +- README.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 3f4552bd..2c16339a 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -22,7 +22,7 @@ public struct LDUser: Encodable, Equatable { /// 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/flags/targeting-users#percentage-rollouts) for more information on it's use for percentage rollout bucketing. + /// The secondary key for the user. Read the [documentation](https://docs.launchdarkly.com/home/flags/rollouts) for more information on it's use for percentage rollout bucketing. public var secondary: String? /// Client app defined name for the user. (Default: nil) public var name: String? diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index d6192219..68d531db 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -32,7 +32,7 @@ public final class ObjcLDUser: NSObject { @objc public var key: String { return user.key } - /// The secondary key for the user. See the [documentation](https://docs.launchdarkly.com/home/flags/targeting-users#percentage-rollouts) for more information on it's use for percentage rollout bucketing. + /// The secondary key for the user. Read the [documentation](https://docs.launchdarkly.com/home/flags/rollouts) for more information on it's use for percentage rollout bucketing. @objc public var secondary: String? { get { user.secondary } set { user.secondary = newValue } diff --git a/README.md b/README.md index 93813cc5..6b072392 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ If you prefer not to use the aforementioned dependency managers, it is possible Learn more ----------- -Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/docs/ios-sdk-reference). +Read our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/client-side/ios). Testing ------- @@ -109,7 +109,7 @@ About LaunchDarkly * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. -* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. * Explore LaunchDarkly * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides From a8bc65f5f09eaaff64fdf9685bd91cc9be617c9b Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Fri, 1 Jul 2022 15:35:58 -0400 Subject: [PATCH 66/90] Favor anonymous over transient (#216) After much discussion and debate about how backwards compatibility concerns would be addressed, we have decided that we are going to keep the name anonymous and abandon transient. --- .../Source/Controllers/SdkController.swift | 4 +- ContractTests/Source/Models/command.swift | 4 +- .../Models/Context/LDContext.swift | 39 +++++++++---------- .../ObjectiveC/ObjcLDContext.swift | 2 +- .../Mocks/LDContextStub.swift | 2 +- .../Models/Context/LDContextCodableSpec.swift | 8 ++-- .../Models/Context/LDContextSpec.swift | 8 ++-- 7 files changed, 32 insertions(+), 35 deletions(-) diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index c5358152..9acf8105 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -225,8 +225,8 @@ final class SdkController: RouteCollection { contextBuilder.name(name) } - if let transient = params.transient { - contextBuilder.transient(transient) + if let anonymous = params.anonymous { + contextBuilder.anonymous(anonymous) } if let secondary = params.secondary { diff --git a/ContractTests/Source/Models/command.swift b/ContractTests/Source/Models/command.swift index c2167226..448ae7af 100644 --- a/ContractTests/Source/Models/command.swift +++ b/ContractTests/Source/Models/command.swift @@ -82,13 +82,13 @@ struct SingleContextParameters: Content, Decodable { var kind: String? var key: String var name: String? - var transient: Bool? + var anonymous: Bool? var secondary: String? var privateAttribute: [String]? var custom: [String:LDValue]? private enum CodingKeys: String, CodingKey { - case kind, key, name, transient, secondary, privateAttribute = "private", custom + case kind, key, name, anonymous, secondary, privateAttribute = "private", custom } } diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index e11702e8..965fa154 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -25,7 +25,7 @@ public struct LDContext: Encodable, Equatable { // Meta attributes fileprivate var name: String? - fileprivate var transient: Bool = false + fileprivate var anonymous: Bool = false fileprivate var secondary: String? internal var privateAttributes: [Reference] = [] @@ -131,8 +131,8 @@ public struct LDContext: Encodable, Equatable { try container.encodeIfPresent(meta, forKey: DynamicCodingKeys(string: "_meta")) } - if context.transient { - try container.encodeIfPresent(context.transient, forKey: DynamicCodingKeys(string: "transient")) + if context.anonymous { + try container.encodeIfPresent(context.anonymous, forKey: DynamicCodingKeys(string: "anonymous")) } } @@ -412,8 +412,8 @@ public struct LDContext: Encodable, Equatable { return self.key.map { .string($0) } case "name": return self.name.map { .string($0) } - case "transient": - return .bool(self.transient) + case "anonymous": + return .bool(self.anonymous) default: return self.attributes[name] } @@ -487,7 +487,7 @@ extension LDContext: Decodable { custom.forEach { contextBuilder.trySetValue($0.key, $0.value) } let isAnonymous = try values.decodeIfPresent(Bool.self, forKey: .isAnonymous) ?? false - contextBuilder.transient(isAnonymous) + contextBuilder.anonymous(isAnonymous) let privateAttributeNames = try values.decodeIfPresent([String].self, forKey: .privateAttributeNames) ?? [] privateAttributeNames.forEach { contextBuilder.addPrivateAttribute(Reference($0)) } @@ -589,7 +589,7 @@ extension LDContext: TypeIdentifying {} /// /// You may use these methods to set additional attributes and/or change the kind before calling /// `LDContextBuilder.build()`. If you do not change any values, the defaults for the `LDContext` are that its -/// kind is "user", its key is set to whatever value you passed to `LDContextBuilder.init(key:)`, its transient attribute +/// kind is "user", its key is set to whatever value you passed to `LDContextBuilder.init(key:)`, its anonymous attribute /// is false, and it has no values for any other attributes. /// /// To define a multi-kind LDContext, see `LDMultiContextBuilder`. @@ -598,7 +598,7 @@ public struct LDContextBuilder { // Meta attributes private var name: String? - private var transient: Bool = false + private var anonymous: Bool = false private var secondary: String? private var privateAttributes: [Reference] = [] @@ -668,7 +668,7 @@ public struct LDContextBuilder { /// /// - "name": Must be a string or null. See `LDContextBuilder.name(_:)`. /// - /// - "transient": Must be a boolean. See `LDContextBuilder.transient(_:)`. + /// - "anonymous": Must be a boolean. See `LDContextBuilder.anonymous(_:)`. /// /// Values that are JSON arrays or objects have special behavior when referenced in /// flag/segment rules. @@ -699,9 +699,9 @@ public struct LDContextBuilder { self.name(val) case ("name", _): return false - case ("transient", .bool(let val)): - self.transient(val) - case ("transient", _): + case ("anonymous", .bool(let val)): + self.anonymous(val) + case ("anonymous", _): return false case ("secondary", .string(let val)): self.secondary(val) @@ -710,9 +710,6 @@ public struct LDContextBuilder { case ("privateAttributeNames", _): Log.debug(typeName(and: #function) + ": The privateAttributeNames property has been replaced with privateAttributes. Refusing to set a property named privateAttributeNames.") return false - case ("anonymous", _): - Log.debug(typeName(and: #function) + ": The anonymous property has been replaced with transient. Refusing to set a property named anonymous.") - return false case (_, .null): self.attributes.removeValue(forKey: name) return false @@ -744,15 +741,15 @@ public struct LDContextBuilder { /// The default value is false. False means that this LDContext represents an entity such as a /// user that you want to be able to see on the LaunchDarkly dashboard. /// - /// Setting transient to true excludes this LDContext from the database that is used by the + /// Setting anonymous to true excludes this LDContext from the database that is used by the /// dashboard. It does not exclude it from analytics event data, so it is not the same as /// making attributes private; all non-private attributes will still be included in events and /// data export. /// - /// This value is also addressable in evaluations as the attribute name "transient". It is + /// This value is also addressable in evaluations as the attribute name "anonymous". It is /// always treated as a boolean true or false in evaluations. - public mutating func transient(_ transient: Bool) { - self.transient = transient + public mutating func anonymous(_ anonymous: Bool) { + self.anonymous = anonymous } /// Provide a reference to designate any number of LDContext attributes as private: that is, @@ -766,7 +763,7 @@ public struct LDContextBuilder { /// This action only affects analytics events that involve this particular LDContext. To mark some (or all) /// LDContext attributes as private for all uses, use the overall event configuration for the SDK. /// - /// The attributes "kind" and "key", and the metadata properties set by secondary and transient, + /// The attributes "kind" and "key", and the metadata properties set by secondary and anonymous, /// cannot be made private. public mutating func addPrivateAttribute(_ reference: Reference) { self.privateAttributes.append(reference) @@ -802,7 +799,7 @@ public struct LDContextBuilder { context.kind = kind context.contexts = [] context.name = self.name - context.transient = self.transient + context.anonymous = self.anonymous context.secondary = self.secondary context.privateAttributes = self.privateAttributes context.key = self.key diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift index bfe337d8..150efbec 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift @@ -37,7 +37,7 @@ public final class ObjcLDContextBuilder: NSObject { @objc public func key(key: String) { builder.key(key) } @objc public func name(name: String) { builder.name(name) } @objc public func secondary(secondary: String) { builder.secondary(secondary) } - @objc public func transient(transient: Bool) { builder.transient(transient) } + @objc public func anonymous(anonymous: Bool) { builder.anonymous(anonymous) } @objc public func addPrivateAttribute(reference: ObjcLDReference) { builder.addPrivateAttribute(reference.reference) } @objc public func removePrivateAttribute(reference: ObjcLDReference) { builder.removePrivateAttribute(reference.reference) } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift index 382fd2df..eb9d21f9 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift @@ -29,7 +29,7 @@ extension LDContext { builder.name(StubConstants.name) builder.secondary(StubConstants.secondary) - builder.transient(StubConstants.isAnonymous) + builder.anonymous(StubConstants.isAnonymous) builder.trySetValue("firstName", StubConstants.firstName) builder.trySetValue("lastName", StubConstants.lastName) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift index d6954ac9..2b1dd43f 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift @@ -9,7 +9,7 @@ final class LDContextCodableSpec: XCTestCase { ("{\"key\": \"foo\"}", "{\"key\": \"foo\", \"kind\": \"user\"}"), ("{\"key\" : \"foo\", \"name\" : \"bar\"}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"name\" : \"bar\"}"), ("{\"key\" : \"foo\", \"custom\" : {\"a\" : \"b\"}}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"a\" : \"b\"}"), - ("{\"key\" : \"foo\", \"anonymous\" : true}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"transient\" : true}"), + ("{\"key\" : \"foo\", \"anonymous\" : true}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"anonymous\" : true}"), ("{\"key\" : \"foo\", \"secondary\" : \"bar\"}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"_meta\" : {\"secondary\" : \"bar\"}}"), ("{\"key\" : \"foo\", \"ip\" : \"1\", \"privateAttributeNames\" : [\"ip\"]}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"ip\" : \"1\", \"_meta\" : { \"privateAttributes\" : [\"ip\"]} }") ] @@ -26,7 +26,7 @@ final class LDContextCodableSpec: XCTestCase { let testCases = [ "{\"kind\":\"org\",\"key\":\"foo\"}", "{\"kind\":\"user\",\"key\":\"foo\"}", - "{\"kind\":\"foo\",\"key\":\"bar\",\"transient\":true}", + "{\"kind\":\"foo\",\"key\":\"bar\",\"anonymous\":true}", "{\"kind\":\"foo\",\"key\":\"bar\",\"name\":\"Foo\",\"_meta\":{\"privateAttributes\":[\"a\"],\"secondary\":\"baz\"}}", "{\"kind\":\"foo\",\"key\":\"bar\",\"object\":{\"a\":\"b\"}}" ] @@ -111,7 +111,7 @@ final class LDContextCodableSpec: XCTestCase { }, "baz": { "key": "baz-key", - "transient": true + "anonymous": true } } """ @@ -123,7 +123,7 @@ final class LDContextCodableSpec: XCTestCase { var bazBuilder = LDContextBuilder(key: "baz-key") bazBuilder.kind("baz") - bazBuilder.transient(true) + bazBuilder.anonymous(true) var multiBuilder = LDMultiContextBuilder() multiBuilder.addContext(try userBuilder.build().get()) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift index 6a9f569c..a9a77ece 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift @@ -180,7 +180,7 @@ final class LDContextSpec: XCTestCase { ("kind", .string("org")), ("key", .string("my-key")), ("name", .string("my-name")), - ("transient", .bool(true)), + ("anonymous", .bool(true)), ("attr", .string("my-attr")), ("/starts-with-slash", .string("love that prefix")), ("/crazy~0name", .string("still works")), @@ -207,7 +207,7 @@ final class LDContextSpec: XCTestCase { var builder = LDContextBuilder(key: "my-key") builder.kind("org") builder.name("my-name") - builder.transient(true) + builder.anonymous(true) builder.secondary("my-secondary") builder.trySetValue("attr", .string("my-attr")) builder.trySetValue("starts-with-slash", .string("love that prefix")) @@ -231,7 +231,7 @@ final class LDContextSpec: XCTestCase { builder.key("org") builder.kind("org") builder.name("my-name") - builder.transient(true) + builder.anonymous(true) builder.trySetValue("attr", .string("my-attr")) multibuilder.addContext(try builder.build().get()) @@ -242,7 +242,7 @@ final class LDContextSpec: XCTestCase { ("kind", LDValue.string("multi")), ("key", nil), ("name", nil), - ("transient", nil), + ("anonymous", nil), ("attr", nil) ] From 2f20293d97c849e94585c9fe07cb1af705d86d9a Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 11 Jul 2022 15:27:13 -0400 Subject: [PATCH 67/90] Generate stable context keys per kind (#218) Customers historically have had the ability to build users with automatically generated keys. With the conversion to context, we need to ensure we handle key generation and caching on a per kind basis. --- LaunchDarkly/LaunchDarkly/LDClient.swift | 2 +- .../Models/Context/LDContext.swift | 79 ++++++++++++------- .../ObjectiveC/ObjcLDContext.swift | 4 + .../LaunchDarklyTests/LDClientSpec.swift | 2 +- .../Models/Context/LDContextSpec.swift | 40 ++++++++++ 5 files changed, 97 insertions(+), 30 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index a6f06bc9..3a308d5c 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -705,7 +705,7 @@ public class LDClient { config = configuration let anonymousUser = LDUser(environmentReporter: environmentReporter) user = anonymousUser - let anonymousContext = LDContext(environmentReporting: environmentReporter) + let anonymousContext = LDContext() context = startContext ?? anonymousContext service = self.serviceFactory.makeDarklyServiceProvider(config: config, context: context) diagnosticReporter = self.serviceFactory.makeDiagnosticReporter(service: service) diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 965fa154..6527423a 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -37,8 +37,8 @@ public struct LDContext: Encodable, Equatable { self.canonicalizedKey = canonicalizedKey } - init(environmentReporting: EnvironmentReporting) { - self.init(canonicalizedKey: LDContext.defaultKey(environmentReporting: environmentReporting)) + init() { + self.init(canonicalizedKey: LDContext.defaultKey(kind: Kind.user)) } struct Meta: Codable { @@ -214,6 +214,20 @@ public struct LDContext: Encodable, Equatable { return (false, nestedPropertiesAreRedacted) } + static internal func defaultKey(kind: Kind) -> String { + // ldDeviceIdentifier is used for users to be compatible with + // older SDKs + let storedIdKey = kind.isUser() ? "ldDeviceIdentifier" : "ldGeneratedContextKey:\(kind)" + if let storedId = UserDefaults.standard.string(forKey: storedIdKey) { + return storedId + } + + let key = UUID().uuidString + UserDefaults.standard.set(key, forKey: storedIdKey) + + return key + } + internal struct UserInfoKeys { // TODO(mmk): Everywhere we use DynamicCodingKey, we should CodingUserInfoKey static let includePrivateAttributes = CodingUserInfoKey(rawValue: "LD_includePrivateAttributes")! @@ -418,25 +432,6 @@ public struct LDContext: Encodable, Equatable { return self.attributes[name] } } - - /// Default key is the LDContext.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(environmentReporting: 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 - if let vendorUUID = environmentReporting.vendorUUID { - return vendorUUID - } - - if let storedId = UserDefaults.standard.string(forKey: storedIdKey) { - return storedId - } - - let key = UUID().uuidString - UserDefaults.standard.set(key, forKey: storedIdKey) - - return key - } } extension LDContext: Decodable { @@ -584,6 +579,11 @@ extension LDContext: Decodable { extension LDContext: TypeIdentifying {} +enum LDContextBuilderKey { + case generateKey + case key(String) +} + /// Contains methods for building a single kind `LDContext` with a specified key, defaulting to kind /// "user". /// @@ -602,7 +602,7 @@ public struct LDContextBuilder { private var secondary: String? private var privateAttributes: [Reference] = [] - private var key: String? + private var key: LDContextBuilderKey private var attributes: [String: LDValue] = [:] // Contexts that were deserialized from implicit user formats @@ -610,9 +610,22 @@ public struct LDContextBuilder { // never allowed to be empty. fileprivate var allowEmptyKey: Bool = false + /// Create a new LDContextBuilder. + /// + /// By default, this builder will create an anonymous LDContext + /// with a generated key. This key will be cached locally and + /// reused for the same context kind. + /// + /// If `LDContextBuilder.key` is called, a key will no longer be + /// generated and the anonymous status will match the value + /// provided by `LDContextBuilder.anonymous` or false by default. + public init() { + self.key = .generateKey + } + /// Create a new LDContextBuilder with the provided `key`. public init(key: String) { - self.key = key + self.key = .key(key) } /// Sets the LDContext's kind attribute. @@ -636,7 +649,7 @@ public struct LDContextBuilder { /// /// The key attribute can be referenced by flag rules, flag target lists, and segments. public mutating func key(_ key: String) { - self.key = key + self.key = .key(key) } /// Sets the LDContext's name attribute. @@ -791,18 +804,28 @@ public struct LDContextBuilder { return Result.failure(.requiresMultiBuilder) } - if !allowEmptyKey && self.key?.isEmpty ?? true { + var contextKey = "" + var anonymous = self.anonymous + switch self.key { + case let .key(key): + contextKey = key + case .generateKey: + contextKey = LDContext.defaultKey(kind: kind) + anonymous = true + } + + if !allowEmptyKey && contextKey.isEmpty { return Result.failure(.emptyKey) } - var context = LDContext(canonicalizedKey: canonicalizeKeyForKind(kind: kind, key: self.key!, omitUserKind: true)) + var context = LDContext(canonicalizedKey: canonicalizeKeyForKind(kind: kind, key: contextKey, omitUserKind: true)) context.kind = kind context.contexts = [] context.name = self.name - context.anonymous = self.anonymous + context.anonymous = anonymous context.secondary = self.secondary context.privateAttributes = self.privateAttributes - context.key = self.key + context.key = contextKey context.attributes = self.attributes return Result.success(context) diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift index 150efbec..513861a6 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift @@ -23,6 +23,10 @@ public final class ObjcLDContext: NSObject { public final class ObjcLDContextBuilder: NSObject { var builder: LDContextBuilder + @objc public override init() { + builder = LDContextBuilder() + } + @objc public init(key: String) { builder = LDContextBuilder(key: key) } diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index ac9850f1..e03dd311 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -309,7 +309,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } it("uses anonymous user") { - expect(testContext.subject.context.fullyQualifiedKey()) == LDContext.defaultKey(environmentReporting: testContext.environmentReporterMock) + expect(testContext.subject.context.fullyQualifiedKey()) == LDContext.defaultKey(kind: testContext.subject.context.kind) expect(testContext.subject.service.context) == testContext.subject.context expect(testContext.makeFlagSynchronizerService?.context) == testContext.subject.context expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.subject.context diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift index a9a77ece..743a0c9b 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift @@ -37,6 +37,46 @@ final class LDContextSpec: XCTestCase { XCTAssertTrue(context.kind.isUser()) } + func testBuilderWillForceAnonymousToTrueForGeneratedKeys() throws { + var builder = LDContextBuilder() + builder.anonymous(false) + + let context = try builder.build().get() + XCTAssertFalse(context.fullyQualifiedKey().isEmpty) + + guard case let .bool(anonymous) = context.getValue(Reference("anonymous")) + else { + XCTFail("Anonymous could not be retrieved") + return + } + + XCTAssertTrue(anonymous) + } + + func testBuilderWillGenerateSameKeyForSameContextKind() throws { + var builder = LDContextBuilder() + let userContext1 = try builder.build().get() + let userContext2 = try builder.build().get() + + builder.kind("org") + + let orgContext1 = try builder.build().get() + let orgContext2 = try builder.build().get() + + builder.kind("user") + let userContext3 = try builder.build().get() + + // All user keys are the same + XCTAssertEqual(userContext1.fullyQualifiedKey(), userContext2.fullyQualifiedKey()) + XCTAssertEqual(userContext1.fullyQualifiedKey(), userContext3.fullyQualifiedKey()) + + // All org keys are the same + XCTAssertEqual(orgContext1.fullyQualifiedKey(), orgContext2.fullyQualifiedKey()) + + // But they aren't equal to each other + XCTAssertNotEqual(userContext1.fullyQualifiedKey(), orgContext1.fullyQualifiedKey()) + } + func testSingleContextHasCorrectCanonicalKey() throws { let tests: [(String, String, String)] = [ ("key", "user", "key"), From f2e6e49773ff3e630e5fc4401801c67927484950 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 18 Jul 2022 11:24:50 -0400 Subject: [PATCH 68/90] Remove legacy LDUser classes (#214) --- .jazzy.yaml | 3 - ContractTests/Source/Models/client.swift | 2 - ContractTests/Source/Models/user.swift | 30 --- LaunchDarkly.xcodeproj/project.pbxproj | 46 ---- .../GeneratedCode/mocks.generated.swift | 6 +- LaunchDarkly/LaunchDarkly/LDClient.swift | 8 +- .../Models/Context/LDContext.swift | 1 - .../Models/Context/Reference.swift | 23 +- .../LaunchDarkly/Models/DiagnosticEvent.swift | 10 +- LaunchDarkly/LaunchDarkly/Models/Event.swift | 6 +- .../FeatureFlag/LDEvaluationDetail.swift | 2 +- .../LaunchDarkly/Models/LDConfig.swift | 22 +- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 206 ------------------ .../LaunchDarkly/Models/UserAttribute.swift | 81 ------- .../Networking/DarklyService.swift | 10 +- .../ObjectiveC/ObjcLDClient.swift | 40 ++-- .../ObjectiveC/ObjcLDConfig.swift | 16 +- .../ObjectiveC/ObjcLDEvaluationDetail.swift | 10 +- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 128 ----------- .../ServiceObjects/Cache/CacheConverter.swift | 17 +- .../Cache/FeatureFlagCache.swift | 28 +-- .../ServiceObjects/ClientServiceFactory.swift | 6 +- .../ServiceObjects/EventReporter.swift | 2 +- .../LaunchDarklyTests/LDClientSpec.swift | 50 ++--- .../Mocks/ClientServiceMockFactory.swift | 6 +- .../Mocks/LDContextStub.swift | 30 +-- .../LaunchDarklyTests/Mocks/LDUserStub.swift | 42 ---- .../Models/DiagnosticEventSpec.swift | 20 +- .../LaunchDarklyTests/Models/EventSpec.swift | 17 +- .../Models/LDConfigSpec.swift | 20 +- .../Models/User/LDUserSpec.swift | 121 ---------- .../Networking/DarklyServiceSpec.swift | 8 +- .../Cache/CacheConverterSpec.swift | 4 +- .../Cache/FeatureFlagCacheSpec.swift | 72 +++--- 34 files changed, 209 insertions(+), 884 deletions(-) delete mode 100644 ContractTests/Source/Models/user.swift delete mode 100644 LaunchDarkly/LaunchDarkly/Models/LDUser.swift delete mode 100644 LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift delete mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift diff --git a/.jazzy.yaml b/.jazzy.yaml index f5e19d1d..a9b0ba52 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -17,7 +17,6 @@ custom_categories: children: - LDClient - LDConfig - - LDUser - LDContext - LDContextBuilder - Reference @@ -40,7 +39,6 @@ custom_categories: - name: Other Types children: - - UserAttribute - LDStreamingMode - LDFlagKey - LDInvalidArgumentError @@ -50,7 +48,6 @@ custom_categories: children: - ObjcLDClient - ObjcLDConfig - - ObjcLDUser - ObjcLDReference - ObjcLDContext - ObjcLDChangedFlag diff --git a/ContractTests/Source/Models/client.swift b/ContractTests/Source/Models/client.swift index c8e4eccc..069f84b8 100644 --- a/ContractTests/Source/Models/client.swift +++ b/ContractTests/Source/Models/client.swift @@ -43,8 +43,6 @@ struct TagParameters: Content { } struct ClientSideParameters: Content { - // TODO(mmk) Remove this user when you have converted everything - var initialUser: LDUser? var initialContext: LDContext? var evaluationReasons: Bool? var useReport: Bool? diff --git a/ContractTests/Source/Models/user.swift b/ContractTests/Source/Models/user.swift deleted file mode 100644 index bf1e4c85..00000000 --- a/ContractTests/Source/Models/user.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation -import LaunchDarkly - -extension LDUser: Decodable { - - /// 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", privateAttributes = "privateAttributeNames", secondary - } - - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - self.init() - - key = try values.decodeIfPresent(String.self, forKey: .key) ?? "" - name = try values.decodeIfPresent(String.self, forKey: .name) - firstName = try values.decodeIfPresent(String.self, forKey: .firstName) - lastName = try values.decodeIfPresent(String.self, forKey: .lastName) - country = try values.decodeIfPresent(String.self, forKey: .country) - ipAddress = try values.decodeIfPresent(String.self, forKey: .ipAddress) - email = try values.decodeIfPresent(String.self, forKey: .email) - avatar = try values.decodeIfPresent(String.self, forKey: .avatar) - custom = try values.decodeIfPresent([String: LDValue].self, forKey: .custom) ?? [:] - isAnonymous = try values.decodeIfPresent(Bool.self, forKey: .isAnonymous) ?? false - _ = try values.decodeIfPresent([String].self, forKey: .privateAttributes) - privateAttributes = (try values.decodeIfPresent([String].self, forKey: .privateAttributes) ?? []).map { UserAttribute.forName($0) } - secondary = try values.decodeIfPresent(String.self, forKey: .secondary) - } -} diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 0fc0a797..09d3bf14 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -7,10 +7,6 @@ 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 */; }; 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 */; }; @@ -28,7 +24,6 @@ 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6C4B51F4DE7630055351C /* LDCommon.swift */; }; 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 */; }; 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 */; }; @@ -52,7 +47,6 @@ 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 */; }; - 8311886A2113AE5D00D77CB5 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */; }; 8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */; }; 831188702113C2D300D77CB5 /* LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8354EFC51F22491C00C05156 /* LaunchDarkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; 831188712113C50A00D77CB5 /* LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8354EFC51F22491C00C05156 /* LaunchDarkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -72,7 +66,6 @@ 831EF34320655E730001C643 /* LDCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6C4B51F4DE7630055351C /* LDCommon.swift */; }; 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 */; }; 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 */; }; @@ -94,7 +87,6 @@ 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 */; }; - 831EF36820655E730001C643 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */; }; 831EF36A20655E730001C643 /* ObjcLDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */; }; 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832307A51F7D8D720029815A /* URLRequestSpec.swift */; }; 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */; }; @@ -127,7 +119,6 @@ 8358F2621F47747F00ECE1AF /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; 835E1D3F1F63450A00184DB4 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; - 835E1D411F63450A00184DB4 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */; }; 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, ); }; }; @@ -145,7 +136,6 @@ 83906A7B21190B7700D7D3C5 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8372668B20D4439600BD1088 /* DateFormatter.swift */; }; 8392FFA32033565700320914 /* HTTPURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8392FFA22033565700320914 /* HTTPURLResponse.swift */; }; 83A0E6B1203B557F00224298 /* FeatureFlagSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A0E6B0203B557F00224298 /* FeatureFlagSpec.swift */; }; - 83A2D6241F51CD7A00EA3BD4 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; 83B1D7C92073F354006D1B1C /* CwlSysctl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B1D7C82073F354006D1B1C /* CwlSysctl.swift */; }; 83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6C4B51F4DE7630055351C /* LDCommon.swift */; }; 83B6E3F1222EFA3800FF2A6A /* ThreadSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6E3F0222EFA3800FF2A6A /* ThreadSpec.swift */; }; @@ -159,7 +149,6 @@ 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 */; }; 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 */; }; @@ -181,19 +170,16 @@ 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 */; }; 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 */; }; 83EBCBB120D9C7B5003A7142 /* FlagCounterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB020D9C7B5003A7142 /* FlagCounterSpec.swift */; }; 83EBCBB320DABE1B003A7142 /* FlagRequestTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */; }; 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 */; }; 83EF67931F9945E800403126 /* EventSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67921F9945E800403126 /* EventSpec.swift */; }; - 83EF67951F994BAD00403126 /* LDUserStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67941F994BAD00403126 /* LDUserStub.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 */; }; @@ -339,7 +325,6 @@ /* End PBXCopyFilesBuildPhase section */ /* 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 = ""; }; @@ -382,7 +367,6 @@ 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlagChangeObserver.swift; sourceTree = ""; }; 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDClient.swift; sourceTree = ""; }; 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 = ""; }; 8372668B20D4439600BD1088 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 837406D321F760640087B22B /* LDTimerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDTimerSpec.swift; sourceTree = ""; }; @@ -394,7 +378,6 @@ 83906A762118EB9000D7D3C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8392FFA22033565700320914 /* HTTPURLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLResponse.swift; sourceTree = ""; }; 83A0E6B0203B557F00224298 /* FeatureFlagSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagSpec.swift; sourceTree = ""; }; - 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUser.swift; sourceTree = ""; }; 83B1D7C82073F354006D1B1C /* CwlSysctl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CwlSysctl.swift; sourceTree = ""; }; 83B6C4B51F4DE7630055351C /* LDCommon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDCommon.swift; sourceTree = ""; }; 83B6E3F0222EFA3800FF2A6A /* ThreadSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSpec.swift; sourceTree = ""; }; @@ -409,12 +392,10 @@ 83DDBEF51FA24A7E00E428B6 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.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 = ""; }; 83EF67921F9945E800403126 /* EventSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSpec.swift; sourceTree = ""; }; - 83EF67941F994BAD00403126 /* LDUserStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDUserStub.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 = ""; }; @@ -637,8 +618,6 @@ 8354EFDE1F26380700C05156 /* Event.swift */, 83EBCB9D20D9A0A1003A7142 /* FeatureFlag */, 8354EFDD1F26380700C05156 /* LDConfig.swift */, - 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */, - 29A4C47427DA6266005B8D34 /* UserAttribute.swift */, ); path = Models; sourceTree = ""; @@ -649,7 +628,6 @@ A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */, 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */, 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */, - 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */, 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */, B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */, 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */, @@ -673,7 +651,6 @@ 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */, 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */, 832307A91F7ECA630029815A /* LDConfigStub.swift */, - 83EF67941F994BAD00403126 /* LDUserStub.swift */, A33A5F7928466D04000C29C7 /* LDContextStub.swift */, 838F96791FBA551A009CFC45 /* ClientServiceMockFactory.swift */, 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */, @@ -723,14 +700,6 @@ path = FlagChange; sourceTree = ""; }; - 83EBCBA620D9A23E003A7142 /* User */ = { - isa = PBXGroup; - children = ( - 83E2E2051F9E7AC7007514E9 /* LDUserSpec.swift */, - ); - path = User; - sourceTree = ""; - }; 83EBCBA720D9A251003A7142 /* FeatureFlag */ = { isa = PBXGroup; children = ( @@ -763,7 +732,6 @@ children = ( A31088232837DCA900184942 /* Context */, 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */, - 83EBCBA620D9A23E003A7142 /* User */, 83EBCBA720D9A251003A7142 /* FeatureFlag */, 83EF67921F9945E800403126 /* EventSpec.swift */, B4F689132497B2FC004D3CE0 /* DiagnosticEventSpec.swift */, @@ -1159,7 +1127,6 @@ buildActionMask = 2147483647; files = ( 83906A7B21190B7700D7D3C5 /* DateFormatter.swift in Sources */, - 8311886A2113AE5D00D77CB5 /* ObjcLDUser.swift in Sources */, 831188502113ADEF00D77CB5 /* EnvironmentReporter.swift in Sources */, 831188682113AE5600D77CB5 /* ObjcLDClient.swift in Sources */, 831188572113AE0B00D77CB5 /* FlagChangeNotifier.swift in Sources */, @@ -1192,7 +1159,6 @@ 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 */, @@ -1204,7 +1170,6 @@ 832D68A5224A38FC005F052A /* CacheConverter.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 */, 831188442113ADC200D77CB5 /* LDConfig.swift in Sources */, @@ -1224,7 +1189,6 @@ 831EF34420655E730001C643 /* LDConfig.swift in Sources */, A31088212837DC0400184942 /* LDContext.swift in Sources */, 831EF34520655E730001C643 /* LDClient.swift in Sources */, - 831EF34620655E730001C643 /* LDUser.swift in Sources */, 830DB3B02239B54900D65D25 /* URLResponse.swift in Sources */, B4C9D4352489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */, @@ -1252,7 +1216,6 @@ 831EF35A20655E730001C643 /* HTTPHeaders.swift in Sources */, 831EF35B20655E730001C643 /* DarklyService.swift in Sources */, 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */, - 29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */, C443A40723145FEE00145710 /* ConnectionInformationStore.swift in Sources */, 29FE129A280413D4008CC918 /* Util.swift in Sources */, 831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */, @@ -1269,7 +1232,6 @@ 83B1D7C92073F354006D1B1C /* CwlSysctl.swift in Sources */, 831EF36620655E730001C643 /* ObjcLDClient.swift in Sources */, 831EF36720655E730001C643 /* ObjcLDConfig.swift in Sources */, - 831EF36820655E730001C643 /* ObjcLDUser.swift in Sources */, B4C9D43A2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, 831EF36A20655E730001C643 /* ObjcLDChangedFlag.swift in Sources */, ); @@ -1291,7 +1253,6 @@ 8372668C20D4439600BD1088 /* DateFormatter.swift in Sources */, A310881B2837DC0400184942 /* Kind.swift in Sources */, A31088172837DC0400184942 /* Reference.swift in Sources */, - 83A2D6241F51CD7A00EA3BD4 /* LDUser.swift in Sources */, 8354EFE21F26380700C05156 /* Event.swift in Sources */, C408884923033B7500420721 /* ConnectionInformation.swift in Sources */, 831D8B721F71D3E700ED65E8 /* DarklyService.swift in Sources */, @@ -1308,12 +1269,10 @@ 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 */, 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 */, @@ -1353,13 +1312,11 @@ 831AAE3020A9E75D00B46DBA /* ThrottlerSpec.swift in Sources */, 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */, 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */, - 83E2E2061F9E7AC7007514E9 /* LDUserSpec.swift in Sources */, 83A0E6B1203B557F00224298 /* FeatureFlagSpec.swift in Sources */, A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */, 83EBCBB720DABE93003A7142 /* FlagRequestTrackerSpec.swift in Sources */, B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */, B46F344125E6DB7D0078D45F /* DiagnosticReporterSpec.swift in Sources */, - 83EF67951F994BAD00403126 /* LDUserStub.swift in Sources */, B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */, 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */, 83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */, @@ -1391,7 +1348,6 @@ 83D9EC762062DEAB004D7FA6 /* LDConfig.swift in Sources */, 83EBCBB420DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, 83D9EC772062DEAB004D7FA6 /* LDClient.swift in Sources */, - 83D9EC782062DEAB004D7FA6 /* LDUser.swift in Sources */, B4C9D4342489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, A36EDFCE2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, @@ -1421,7 +1377,6 @@ 83D9EC8D2062DEAB004D7FA6 /* DarklyService.swift in Sources */, 83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */, 83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */, - 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B2206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */, 8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */, @@ -1437,7 +1392,6 @@ B4C9D42F2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 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 */, ); diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index 8487f1d1..9062aa79 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -12,10 +12,10 @@ final class CacheConvertingMock: CacheConverting { var convertCacheDataCallCount = 0 var convertCacheDataCallback: (() throws -> Void)? - var convertCacheDataReceivedArguments: (serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int)? - func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) { + var convertCacheDataReceivedArguments: (serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedContexts: Int)? + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedContexts: Int) { convertCacheDataCallCount += 1 - convertCacheDataReceivedArguments = (serviceFactory: serviceFactory, keysToConvert: keysToConvert, maxCachedUsers: maxCachedUsers) + convertCacheDataReceivedArguments = (serviceFactory: serviceFactory, keysToConvert: keysToConvert, maxCachedContexts: maxCachedContexts) try! convertCacheDataCallback?() } } diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 3a308d5c..e5710ff8 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -262,8 +262,6 @@ public class LDClient { let config: LDConfig let service: DarklyServiceProvider private(set) var context: LDContext - // TODO(mmk) Remove this when we are done - private(set) var user: LDUser /** The LDContext set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the context. See `LDContext` for details about what information can be retained. @@ -597,7 +595,7 @@ public class LDClient { let serviceFactory = serviceFactory ?? ClientServiceFactory() var keys = [config.mobileKey] keys.append(contentsOf: config.getSecondaryMobileKeys().values) - serviceFactory.makeCacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: keys, maxCachedUsers: config.maxCachedUsers) + serviceFactory.makeCacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: keys, maxCachedContexts: config.maxCachedContexts) LDClient.instances = [:] var mobileKeys = config.getSecondaryMobileKeys() @@ -697,14 +695,12 @@ public class LDClient { private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startContext: LDContext?, completion: (() -> Void)? = nil) { self.serviceFactory = serviceFactory environmentReporter = self.serviceFactory.makeEnvironmentReporter() - flagCache = self.serviceFactory.makeFeatureFlagCache(mobileKey: configuration.mobileKey, maxCachedUsers: configuration.maxCachedUsers) + flagCache = self.serviceFactory.makeFeatureFlagCache(mobileKey: configuration.mobileKey, maxCachedContexts: configuration.maxCachedContexts) flagStore = self.serviceFactory.makeFlagStore() flagChangeNotifier = self.serviceFactory.makeFlagChangeNotifier() throttler = self.serviceFactory.makeThrottler(environmentReporter: environmentReporter) config = configuration - let anonymousUser = LDUser(environmentReporter: environmentReporter) - user = anonymousUser let anonymousContext = LDContext() context = startContext ?? anonymousContext service = self.serviceFactory.makeDarklyServiceProvider(config: config, context: context) diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 6527423a..acc8c9c3 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -229,7 +229,6 @@ public struct LDContext: Encodable, Equatable { } internal struct UserInfoKeys { - // TODO(mmk): Everywhere we use DynamicCodingKey, we should CodingUserInfoKey static let includePrivateAttributes = CodingUserInfoKey(rawValue: "LD_includePrivateAttributes")! static let allAttributesPrivate = CodingUserInfoKey(rawValue: "LD_allAttributesPrivate")! static let globalPrivateAttributes = CodingUserInfoKey(rawValue: "LD_globalPrivateAttributes")! diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift index 94afc7fa..4d223a91 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift @@ -186,30 +186,11 @@ public struct Reference: Codable, Equatable, Hashable { } /// Returns raw string that was passed into constructor. - internal func raw() -> String { + public func raw() -> String { return rawPath } - /// Retrieves a single path component from the attribute reference. - /// - /// For a simple attribute reference such as "name" with no leading slash, - /// if index is zero, `component` returns the attribute name and None. - /// - /// For an attribute reference with a leading slash, if index is less than - /// `Reference.depth()`, `component` returns the path component as a string - /// for its first value. The second value is an `Int?` that is the integer - /// value of that string if applicable, or None if the string does not - /// represent an integer; this is used to implement a "find a value by - /// index within a JSON array" behavior similar to JSON Pointer. - /// - /// If index is out of range, it returns None. - /// - /// ``` - /// Reference("a").component(0); // returns ("a", nil) - /// Reference("/a/b").component(1); // returns ("b", nil) - /// Reference("/a/3").component(1); // returns ("3", 3) - /// ``` - public func component(_ index: Int) -> (String, Int?)? { + internal func component(_ index: Int) -> (String, Int?)? { if index >= self.depth() { return nil } diff --git a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift index 13454fba..30270723 100644 --- a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift +++ b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift @@ -101,12 +101,11 @@ struct DiagnosticConfig: Codable { let allAttributesPrivate: Bool let pollingIntervalMillis: Int let backgroundPollingIntervalMillis: Int - let inlineUsersInEvents: Bool + let inlineContextsInEvents: Bool let useReport: Bool let backgroundPollingDisabled: Bool let evaluationReasonsRequested: Bool - // TODO(mmk) Update this config pattern - let maxCachedUsers: Int + let maxCachedContexts: Int let mobileKeyCount: Int let diagnosticRecordingIntervalMillis: Int let customHeaders: Bool @@ -122,13 +121,12 @@ struct DiagnosticConfig: Codable { allAttributesPrivate = config.allContextAttributesPrivate pollingIntervalMillis = Int(exactly: round(config.flagPollingInterval * 1_000)) ?? .max backgroundPollingIntervalMillis = Int(exactly: round(config.backgroundFlagPollingInterval * 1_000)) ?? .max - // TODO(mmk) Update this config pattern - inlineUsersInEvents = config.inlineUserInEvents + inlineContextsInEvents = config.inlineContextInEvents useReport = config.useReport backgroundPollingDisabled = !config.enableBackgroundUpdates evaluationReasonsRequested = config.evaluationReasons // While the SDK treats all negative values as unlimited, for consistency we only send -1 for diagnostics - maxCachedUsers = config.maxCachedUsers >= 0 ? config.maxCachedUsers : -1 + maxCachedContexts = config.maxCachedContexts >= 0 ? config.maxCachedContexts : -1 mobileKeyCount = 1 + (config.getSecondaryMobileKeys().count) diagnosticRecordingIntervalMillis = Int(exactly: round(config.diagnosticRecordingInterval * 1_000)) ?? .max customHeaders = !config.additionalHeaders.isEmpty || config.headerDelegate != nil diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index 9f9595d8..42c2f4ef 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -25,7 +25,7 @@ class Event: Encodable { } struct UserInfoKeys { - static let inlineUserInEvents = CodingUserInfoKey(rawValue: "LD_inlineUserInEvents")! + static let inlineContextInEvents = CodingUserInfoKey(rawValue: "LD_inlineContextInEvents")! } func encode(to encoder: Encoder) throws { @@ -59,7 +59,7 @@ class CustomEvent: Event, SubEvent { 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 { + if encoder.userInfo[Event.UserInfoKeys.inlineContextInEvents] as? Bool ?? false { try container.encode(context, forKey: .context) } else { try container.encode(context.contextKeys(), forKey: .contextKeys) @@ -96,7 +96,7 @@ class FeatureEvent: Event, SubEvent { 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 { + if kind == .debug || encoder.userInfo[Event.UserInfoKeys.inlineContextInEvents] as? Bool ?? false { try container.encode(context, forKey: .context) } else { try container.encode(context.contextKeys(), forKey: .contextKeys) diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift index 4b5b46a9..5ccc9675 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift @@ -5,7 +5,7 @@ import Foundation explanation of how it is calculated. */ public final class LDEvaluationDetail { - /// The value of the flag for the current user. + /// The value of the flag for the current context. 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 let variationIndex: Int? diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 1d64c3ce..dcbc714a 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -4,7 +4,7 @@ import Foundation public enum LDStreamingMode { /** 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. + is updated in the dashboard, the stream notifies the SDK of changes to the evaluation result for the current context. 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. @@ -143,8 +143,8 @@ public struct LDConfig { /// 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) - static let inlineUserInEvents = false + /// The default setting controlling the amount of context data sent in events. When true the SDK will generate events using the full LDContext, excluding private attributes. When false the SDK will generate events using only `LDContext.contextKeys()`. (false) + static let inlineContextInEvents = false /// The default setting controlling information logged to the console, and modifying some setting ranges to facilitate debugging. (false) static let debugMode = false @@ -152,8 +152,8 @@ public struct LDConfig { /// The default setting for whether we request evaluation reasons for all flags. (false) static let evaluationReasons = false - /// The default setting for the maximum number of locally cached users. (5) - static let maxCachedUsers = 5 + /// The default setting for the maximum number of locally cached contexts. (5) + static let maxCachedContexts = 5 /// The default setting for whether sending diagnostic data is disabled. (false) static let diagnosticOptOut = false @@ -294,9 +294,9 @@ public struct LDConfig { private static let flagRetryStatusCodes = [HTTPURLResponse.StatusCodes.methodNotAllowed, HTTPURLResponse.StatusCodes.badRequest, HTTPURLResponse.StatusCodes.notImplemented] /** - Controls how the SDK reports the user in analytics event reports. When set to true, event reports will contain the user attributes, except attributes marked as private. When set to false, event reports will contain the user's key only, reducing the size of event reports. (Default: false) + Controls how the SDK reports the context in analytics event reports. When set to true, event reports will contain the context attributes, except attributes marked as private. When set to false, event reports will contain the context keys only, reducing the size of event reports. (Default: false) */ - public var inlineUserInEvents: Bool = Defaults.inlineUserInEvents + public var inlineContextInEvents: Bool = Defaults.inlineContextInEvents /// Enables logging for debugging. (Default: false) public var isDebugMode: Bool = Defaults.debugMode @@ -304,8 +304,8 @@ public struct LDConfig { /// Enables requesting evaluation reasons for all flags. (Default: false) public var evaluationReasons: Bool = Defaults.evaluationReasons - /// An Integer that tells UserEnvironmentFlagCache the maximum number of users to locally cache. Can be set to -1 for unlimited cached users. - public var maxCachedUsers: Int = Defaults.maxCachedUsers + /// An Integer that tells ContextEnvironmentFlagCache the maximum number of contexts to locally cache. Can be set to -1 for unlimited cached contexts. + public var maxCachedContexts: Int = Defaults.maxCachedContexts /** Set to true to opt out of sending diagnostic data. (Default: false) @@ -437,10 +437,10 @@ extension LDConfig: Equatable { && lhs.allContextAttributesPrivate == rhs.allContextAttributesPrivate && Set(lhs.privateContextAttributes) == Set(rhs.privateContextAttributes) && lhs.useReport == rhs.useReport - && lhs.inlineUserInEvents == rhs.inlineUserInEvents + && lhs.inlineContextInEvents == rhs.inlineContextInEvents && lhs.isDebugMode == rhs.isDebugMode && lhs.evaluationReasons == rhs.evaluationReasons - && lhs.maxCachedUsers == rhs.maxCachedUsers + && lhs.maxCachedContexts == rhs.maxCachedContexts && lhs.diagnosticOptOut == rhs.diagnosticOptOut && lhs.diagnosticRecordingInterval == rhs.diagnosticRecordingInterval && lhs.wrapperName == rhs.wrapperName diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift deleted file mode 100644 index 1b2db910..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ /dev/null @@ -1,206 +0,0 @@ -import Foundation - -/** - 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 { - - static let optionalAttributes = UserAttribute.BuiltIn.allBuiltIns.filter { $0.name != "key" && $0.name != "anonymous"} - - 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. - public var key: String - /// The secondary key for the user. See the [documentation](https://docs.launchdarkly.com/home/flags/targeting-users#percentage-rollouts) for more information on it's use for percentage rollout bucketing. - public var secondary: String? - /// Client app defined name for the user. (Default: nil) - public var name: String? - /// Client app defined first name for the user. (Default: nil) - public var firstName: String? - /// Client app defined last name for the user. (Default: nil) - public var lastName: String? - /// Client app defined country for the user. (Default: nil) - public var country: String? - /// Client app defined ipAddress for the user. (Default: nil) - public var ipAddress: String? - /// Client app defined email address for the user. (Default: nil) - 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, 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 { - get { isAnonymousNullable == true } - set { isAnonymousNullable = newValue } - } - - /** - Whether or not the user is anonymous, if that has been specified (or set due to the lack of a `key` property). - - Although the `isAnonymous` property defaults to `false` in terms of LaunchDarkly's indexing behavior, for historical - reasons flag evaluation may behave differently if the value is explicitly set to `false` verses being omitted. This - field allows treating the property as optional for consisent evaluation with other LaunchDarkly SDKs. - */ - public var isAnonymousNullable: Bool? - - /** - 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 most built-in attributes and all top level `custom` dictionary keys here. (Default: []]) - See Also: `LDConfig.allUserAttributesPrivate` and `LDConfig.privateUserAttributes`. - */ - public var privateAttributes: [UserAttribute] - - 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. - - parameter name: Client app defined name for the user. (Default: nil) - - parameter firstName: Client app defined first name for the user. (Default: nil) - - parameter lastName: Client app defined last name for the user. (Default: nil) - - parameter country: Client app defined country for the user. (Default: nil) - - parameter ipAddress: Client app defined ipAddress for the user. (Default: nil) - - parameter email: Client app defined email address for the user. (Default: nil) - - 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 privateAttributes: Client app defined privateAttributes for the user. (Default: nil) - - parameter secondary: Secondary attribute value. (Default: nil) - */ - public init(key: String? = nil, - name: String? = nil, - firstName: String? = nil, - lastName: String? = nil, - country: String? = nil, - ipAddress: String? = nil, - email: String? = nil, - avatar: String? = nil, - custom: [String: LDValue]? = nil, - isAnonymous: Bool? = nil, - privateAttributes: [UserAttribute]? = nil, - secondary: String? = nil) { - let environmentReporter = EnvironmentReporter() - let selectedKey = key ?? LDUser.defaultKey(environmentReporter: environmentReporter) - self.key = selectedKey - self.secondary = secondary - self.name = name - self.firstName = firstName - self.lastName = lastName - self.country = country - self.ipAddress = ipAddress - self.email = email - self.avatar = avatar - self.isAnonymousNullable = isAnonymous - if isAnonymous == nil && selectedKey == LDUser.defaultKey(environmentReporter: environmentReporter) { - self.isAnonymousNullable = true - } - self.custom = custom ?? [:] - self.privateAttributes = privateAttributes ?? [] - Log.debug(typeName(and: #function) + "user: \(self)") - } - - /** - Internal initializer that accepts an environment reporter, used for testing - */ - init(environmentReporter: EnvironmentReporting) { - self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), isAnonymous: true) - } - - private func value(for attribute: UserAttribute) -> Any? { - if let builtInGetter = attribute.builtInGetter { - return builtInGetter(self) - } - return custom[attribute.name] - } - - 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")!) - - if let anonymous = isAnonymousNullable { - try container.encode(anonymous, 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 { - // 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 - } - if let storedId = UserDefaults.standard.string(forKey: storedIdKey) { - return storedId - } - let key = UUID().uuidString - UserDefaults.standard.set(key, forKey: storedIdKey) - return key - } -} - -/// Class providing ObjC interoperability with the LDUser struct -@objc final class LDUserWrapper: NSObject { - let wrapped: LDUser - - init(user: LDUser) { - wrapped = user - super.init() - } -} - -extension LDUser: TypeIdentifying { } diff --git a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift deleted file mode 100644 index 069b45bc..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift +++ /dev/null @@ -1,81 +0,0 @@ -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 } - /// 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] - } - - 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 - } - return UserAttribute(name) - } - - let name: String - let builtInGetter: ((LDUser) -> Any?)? - - init(_ name: String, builtInGetter: ((LDUser) -> Any?)? = nil) { - self.name = name - 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 { - 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/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 62567f63..2f9cfb39 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -32,8 +32,8 @@ final class DarklyService: DarklyServiceProvider { } struct FlagRequestPath { - static let get = "msdk/evalx/users" - static let report = "msdk/evalx/user" + static let get = "msdk/evalx/contexts" + static let report = "msdk/evalx/context" } struct StreamRequestPath { @@ -149,7 +149,7 @@ final class DarklyService: DarklyServiceProvider { errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { let encoder = JSONEncoder() encoder.userInfo[LDContext.UserInfoKeys.includePrivateAttributes] = true - let userJsonData = try? encoder.encode(context) + let contextJsonData = try? encoder.encode(context) var streamRequestUrl = config.streamUrl.appendingPathComponent(StreamRequestPath.meval) var connectMethod = HTTPRequestMethod.get @@ -157,9 +157,9 @@ final class DarklyService: DarklyServiceProvider { if useReport { connectMethod = HTTPRequestMethod.report - connectBody = userJsonData + connectBody = contextJsonData } else { - streamRequestUrl.appendPathComponent(userJsonData?.base64UrlEncodedString ?? "", isDirectory: false) + streamRequestUrl.appendPathComponent(contextJsonData?.base64UrlEncodedString ?? "", isDirectory: false) } return serviceFactory.makeStreamingProvider(url: shouldGetReasons(url: streamRequestUrl), diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index 2803748c..9552bcf0 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -1,16 +1,16 @@ import Foundation /** - 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. + 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 context (LDContext) 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. ### Objc Classes - 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. + 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 `LDContext`. 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`). 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 LDContext (`ObjcLDContxt`). Both give you additional control over the feature flags delivered to the LDClient. See `ObjcLDConfig` & `ObjcLDContext` 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. + 2. Call `[ObjcLDClient startWithConfig: context: completion:]` (`ObjcLDClient.startWithConfig(_:config:context:completion:)`) + - If you do not pass in a LDContext, LDCLient will create a default for you. - The optional completion closure allows the LDClient to notify your app when it has gone online. 3. Because the LDClient is a singleton, you do not have to keep a reference to it in your code. @@ -79,7 +79,7 @@ public final class ObjcLDClient: NSObject { When offline, the SDK does not attempt to communicate with LaunchDarkly servers. Client apps can request feature flag values and set/change feature flag observers while offline. The SDK will collect events while offline. - The SDK protects itself from multiple rapid calls to `setOnline:YES` by enforcing an increasing delay (called *throttling*) each time `setOnline:YES` is called within a short time. The first time, the call proceeds normally. For each subsequent call the delay is enforced, and if waiting, increased to a maximum delay. When the delay has elapsed, the `setOnline:YES` will proceed, assuming that the client app has not called `setOnline:NO` during the delay. Therefore a call to `setOnline:YES` may not immediately result in the LDClient going online. Client app developers should consider this situation abnormal, and take steps to prevent the client app from making multiple rapid `setOnline:YES` calls. Calls to `setOnline:NO` are not throttled. Note that calls to `start(config: user: completion:)`, and setting the `config` or `user` can also call `setOnline:YES` under certain conditions. After the delay, the SDK resets and the client app can make a susequent call to `setOnline:YES` without being throttled. + The SDK protects itself from multiple rapid calls to `setOnline:YES` by enforcing an increasing delay (called *throttling*) each time `setOnline:YES` is called within a short time. The first time, the call proceeds normally. For each subsequent call the delay is enforced, and if waiting, increased to a maximum delay. When the delay has elapsed, the `setOnline:YES` will proceed, assuming that the client app has not called `setOnline:NO` during the delay. Therefore a call to `setOnline:YES` may not immediately result in the LDClient going online. Client app developers should consider this situation abnormal, and take steps to prevent the client app from making multiple rapid `setOnline:YES` calls. Calls to `setOnline:NO` are not throttled. Note that calls to `start(config: context: completion:)`, and setting the `config` or `context` can also call `setOnline:YES` under certain conditions. After the delay, the SDK resets and the client app can make a susequent call to `setOnline:YES` without being throttled. Client apps can set a completion block called when the setOnline call completes. For unthrottled `setOnline:YES` and all `setOnline:NO` calls, the SDK will call the block immediately on completion of this method. For throttled `setOnline:YES` calls, the SDK will call the block after the throttling delay at the completion of the setOnline method. @@ -104,26 +104,26 @@ public final class ObjcLDClient: NSObject { } /** - The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. + The LDContext set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the context. See `LDContext` for details about what information can be retained. - The client app can change the current LDUser by calling this method. 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). + The client app can change the current LDContext by calling this method. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. When a new context is set, the LDClient goes offline and sets the new context. If the client was online when the new context was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). - - parameter user: The ObjcLDUser set with the desired user. + - parameter context: The ObjcLDContext set with the desired context. */ @objc public func identify(context: ObjcLDContext) { ldClient.identify(context: context.context, completion: nil) } /** - The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. + The LDContext set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the context. See `LDContext` for details about what information can be retained. - Normally, the client app should create and set the LDUser and pass that into `start(config: user: completion:)`. + Normally, the client app should create and set the LDContext and pass that into `start(config: context: 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 `context` by calling identify with a new or updated LDContext. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. If the client app does not create a LDContext, LDClient creates an anonymous default context, which can affect the feature flags delivered to the LDClient. - 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 context is set, the LDClient goes offline and sets the new context. If the client was online when the new context 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 `context`, 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 context are ready. - - parameter user: The ObjcLDUser set with the desired user. + - parameter context: The ObjcLDContext set with the desired context. - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. (Optional) */ @objc public func identify(context: ObjcLDContext, completion: (() -> Void)? = nil) { @@ -540,13 +540,13 @@ public final class ObjcLDClient: NSObject { } /** - Starts the LDClient using the passed in `config` & `user`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. - Starting the LDClient means setting the `config` & `user`, setting the client online if `config.startOnline` is true (the default setting), and starting event recording. The client app must start the LDClient before it will report feature flag values. If a client does not call `start`, no methods will work. - If the `start` call omits the `user`, the LDClient uses the default `user` if it was never set. + Starts the LDClient using the passed in `config` & `context`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. + Starting the LDClient means setting the `config` & `context`, setting the client online if `config.startOnline` is true (the default setting), and starting event recording. The client app must start the LDClient before it will report feature flag values. If a client does not call `start`, no methods will work. + If the `start` call omits the `context`, the LDClient uses the default `context` if it was never set. If the` start` call includes the optional `completion` closure, LDClient calls the `completion` closure when `setOnline(_: completion:)` embedded in the `init` method completes. This method listens for flag updates so the completion will only return once an update has occurred. The `start` call is subject to throttling delays, therefore the `completion` closure call may be delayed. - Subsequent calls to this method cause the LDClient to return. Normally there should only be one call to start. To change `user`, use `identify`. + Subsequent calls to this method cause the LDClient to return. Normally there should only be one call to start. To change `context`, use `identify`. - parameter configuration: The LDConfig that contains the desired configuration. (Required) - - parameter user: The LDUser set with the desired user. If omitted, LDClient sets a default user. (Optional) + - parameter context: The LDContext set with the desired context. If omitted, LDClient sets a default context. (Optional) - parameter completion: Closure called when the embedded `setOnline` call completes. (Optional) */ /// - Tag: start @@ -558,7 +558,7 @@ public final class ObjcLDClient: NSObject { See [start](x-source-tag://start) for more information on starting the SDK. - parameter configuration: The LDConfig that contains the desired configuration. (Required) - - parameter user: The LDUser set with the desired user. If omitted, LDClient sets a default user.. (Optional) + - parameter context: The LDContext set with the desired context. If omitted, LDClient sets a default context.. (Optional) - parameter startWaitSeconds: A TimeInterval that determines when the completion will return if no flags have been returned from the network. - parameter completion: Closure called when the embedded `setOnline` call completes. Takes a Bool that indicates whether the completion timedout as a parameter. (Optional) */ diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index c3d7f05f..e48b28ba 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -119,11 +119,11 @@ public final class ObjcLDConfig: NSObject { } /** - Controls how the SDK reports the user in analytics event reports. When set to YES, event reports will contain the user attributes, except attributes marked as private. When set to NO, event reports will contain the user's key only, reducing the size of event reports. (Default: NO) + Controls how the SDK reports the context[f in analytics event reports. When set to YES, event reports will contain the context attributes, except attributes marked as private. When set to NO, event reports will contain the context keys only, reducing the size of event reports. (Default: NO) */ - @objc public var inlineUserInEvents: Bool { - get { config.inlineUserInEvents } - set { config.inlineUserInEvents = newValue } + @objc public var inlineContextInEvents: Bool { + get { config.inlineContextInEvents } + set { config.inlineContextInEvents = newValue } } /// Enables logging for debugging. (Default: NO) @@ -138,10 +138,10 @@ public final class ObjcLDConfig: NSObject { 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) - @objc public var maxCachedUsers: Int { - get { config.maxCachedUsers } - set { config.maxCachedUsers = newValue } + /// An Integer that tells ContextEnvironmentFlagCache the maximum number of contexts to locally cache. Can be set to -1 for unlimited cached contexts. (Default: 5) + @objc public var maxCachedContexts: Int { + get { config.maxCachedContexts } + set { config.maxCachedContexts = newValue } } /** diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift index 32dc672a..38880f68 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift @@ -3,7 +3,7 @@ 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. + /// The value of the flag for the current context. @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 @@ -20,7 +20,7 @@ public final class ObjcLDBoolEvaluationDetail: NSObject { /// 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. + /// The value of the flag for the current context. @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 @@ -37,7 +37,7 @@ public final class ObjcLDDoubleEvaluationDetail: NSObject { /// 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. + /// The value of the flag for the current context. @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 @@ -54,7 +54,7 @@ public final class ObjcLDIntegerEvaluationDetail: NSObject { /// 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. + /// The value of the flag for the current context. @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 @@ -71,7 +71,7 @@ public final class ObjcLDStringEvaluationDetail: NSObject { /// 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. + /// The value of the flag for the current context. @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 diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift deleted file mode 100644 index d6192219..00000000 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ /dev/null @@ -1,128 +0,0 @@ -import Foundation - -/** - 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. - */ -@objc (LDUser) -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 - } - /// The secondary key for the user. See the [documentation](https://docs.launchdarkly.com/home/flags/targeting-users#percentage-rollouts) 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) - @objc public var name: String? { - get { user.name } - set { user.name = newValue } - } - /// 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) - @objc public var lastName: String? { - get { user.lastName } - set { user.lastName = newValue } - } - /// 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) - @objc public var ipAddress: String? { - get { user.ipAddress } - set { user.ipAddress = newValue } - } - /// 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) - @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. 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 { - get { user.isAnonymous } - set { user.isAnonymous = newValue } - } - - /** - 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 `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`. - - */ - @objc public var privateAttributes: [String] { - get { user.privateAttributes.map { $0.name } } - set { user.privateAttributes = newValue.map { UserAttribute.forName($0) } } - } - - /** - Initializer to create a LDUser. Client configurable attributes are set to their default value. The SDK will automatically set `key`, `device`, `operatingSystem`, and `isAnonymous` attributes. The SDK embeds `device` and `operatingSystem` into the `custom` dictionary for transmission to LaunchDarkly. - */ - @objc override public init() { - user = LDUser() - } - - /** - Initializer to create a LDUser with a specific key. Other client configurable attributes are set to their default value. The SDK will automatically set `key`, `device`, `operatingSystem`, and `isAnonymous` attributes. 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. - */ - @objc public init(key: String) { - user = LDUser(key: key) - } - - // Initializer to wrap the Swift LDUser into ObjcLDUser for use in Objective-C apps. - init(_ user: LDUser) { - self.user = user - } - - /// 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 } - return user == otherUser.user - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index c90e98c1..8151c3f0 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -2,7 +2,7 @@ import Foundation // sourcery: autoMockable protocol CacheConverting { - func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedContexts: Int) } // Cache model in SDK versions >=4.0.0 <6.0.0. Migration is not supported for earlier versions. @@ -101,6 +101,17 @@ final class CacheConverter: CacheConverting { private func convertV7Data(flagCaches: inout [MobileKey: FeatureFlagCaching]) { for (_, flagCaching) in flagCaches { flagCaching.keyedValueCache.keys().forEach { key in + // Deal with renaming context-users cache key + if key == "context-users" { + guard let data = flagCaching.keyedValueCache.data(forKey: "cached-users") + else { + return; + } + flagCaching.keyedValueCache.removeObject(forKey: "cached-users") + flagCaching.keyedValueCache.set(data, forKey: "cached-contexts") + return + } + guard let cachedData = flagCaching.keyedValueCache.data(forKey: key), let cachedFlags = try? JSONDecoder().decode(FeatureFlagCollection.self, from: cachedData) else { return } @@ -113,10 +124,10 @@ final class CacheConverter: CacheConverting { } } - func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) { + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedContexts: Int) { var flagCaches: [String: FeatureFlagCaching] = [:] keysToConvert.forEach { mobileKey in - let flagCache = serviceFactory.makeFeatureFlagCache(mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) + let flagCache = serviceFactory.makeFeatureFlagCache(mobileKey: mobileKey, maxCachedContexts: maxCachedContexts) flagCaches[mobileKey] = flagCache // Get current cache version and return if up to date guard let cacheVersionData = flagCache.keyedValueCache.data(forKey: "ld-cache-metadata") diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift index 8de11fdc..7984f7fb 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -11,9 +11,9 @@ protocol FeatureFlagCaching { final class FeatureFlagCache: FeatureFlagCaching { let keyedValueCache: KeyedValueCaching - let maxCachedUsers: Int + let maxCachedContexts: Int - init(serviceFactory: ClientServiceCreating, mobileKey: MobileKey, maxCachedUsers: Int) { + init(serviceFactory: ClientServiceCreating, mobileKey: MobileKey, maxCachedContexts: Int) { let cacheKey: String if let bundleId = Bundle.main.bundleIdentifier { cacheKey = "\(Util.sha256base64(bundleId)).\(Util.sha256base64(mobileKey))" @@ -21,7 +21,7 @@ final class FeatureFlagCache: FeatureFlagCaching { cacheKey = Util.sha256base64(mobileKey) } self.keyedValueCache = serviceFactory.makeKeyedValueCache(cacheKey: "com.launchdarkly.client.\(cacheKey)") - self.maxCachedUsers = maxCachedUsers + self.maxCachedContexts = maxCachedContexts } func retrieveFeatureFlags(contextKey: String) -> StoredItems? { @@ -32,25 +32,25 @@ final class FeatureFlagCache: FeatureFlagCaching { } func storeFeatureFlags(_ storedItems: StoredItems, contextKey: String, lastUpdated: Date) { - guard self.maxCachedUsers != 0, let encoded = try? JSONEncoder().encode(StoredItemCollection(storedItems)) + guard self.maxCachedContexts != 0, let encoded = try? JSONEncoder().encode(StoredItemCollection(storedItems)) else { return } self.keyedValueCache.set(encoded, forKey: "flags-\(contextKey)") - var cachedUsers: [String: Int64] = [:] - if let cacheMetadata = self.keyedValueCache.data(forKey: "cached-users") { - cachedUsers = (try? JSONDecoder().decode([String: Int64].self, from: cacheMetadata)) ?? [:] + var cachedContexts: [String: Int64] = [:] + if let cacheMetadata = self.keyedValueCache.data(forKey: "cached-contexts") { + cachedContexts = (try? JSONDecoder().decode([String: Int64].self, from: cacheMetadata)) ?? [:] } - cachedUsers[contextKey] = 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) + cachedContexts[contextKey] = lastUpdated.millisSince1970 + if cachedContexts.count > self.maxCachedContexts && self.maxCachedContexts > 0 { + let sorted = cachedContexts.sorted { $0.value < $1.value } + sorted.prefix(cachedContexts.count - self.maxCachedContexts).forEach { sha, _ in + cachedContexts.removeValue(forKey: sha) self.keyedValueCache.removeObject(forKey: "flags-\(sha)") } } - if let encoded = try? JSONEncoder().encode(cachedUsers) { - self.keyedValueCache.set(encoded, forKey: "cached-users") + if let encoded = try? JSONEncoder().encode(cachedContexts) { + self.keyedValueCache.set(encoded, forKey: "cached-contexts") } } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index 585baaa9..baf0784f 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -3,7 +3,7 @@ import LDSwiftEventSource protocol ClientServiceCreating { func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching - func makeFeatureFlagCache(mobileKey: String, maxCachedUsers: Int) -> FeatureFlagCaching + func makeFeatureFlagCache(mobileKey: String, maxCachedContexts: Int) -> FeatureFlagCaching func makeCacheConverter() -> CacheConverting func makeDarklyServiceProvider(config: LDConfig, context: LDContext) -> DarklyServiceProvider func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, useReport: Bool, service: DarklyServiceProvider) -> LDFlagSynchronizing @@ -29,8 +29,8 @@ final class ClientServiceFactory: ClientServiceCreating { UserDefaults(suiteName: cacheKey)! } - func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedUsers: Int) -> FeatureFlagCaching { - FeatureFlagCache(serviceFactory: self, mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) + func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedContexts: Int) -> FeatureFlagCaching { + FeatureFlagCache(serviceFactory: self, mobileKey: mobileKey, maxCachedContexts: maxCachedContexts) } func makeCacheConverter() -> CacheConverting { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index b6cd7038..b0325fbc 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -129,7 +129,7 @@ class EventReporter: EventReporting { private func publish(_ events: [Event], _ payloadId: String, _ completion: CompletionClosure?) { let encodingConfig: [CodingUserInfoKey: Any] = [ - Event.UserInfoKeys.inlineUserInEvents: service.config.inlineUserInEvents, + Event.UserInfoKeys.inlineContextInEvents: service.config.inlineContextInEvents, LDContext.UserInfoKeys.allAttributesPrivate: service.config.allContextAttributesPrivate, LDContext.UserInfoKeys.globalPrivateAttributes: service.config.privateContextAttributes.map { $0 } ] diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index e03dd311..4b8a5d94 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -26,7 +26,7 @@ final class LDClientSpec: QuickSpec { var context: LDContext! var subject: LDClient! let serviceFactoryMock = ClientServiceMockFactory() - // mock getters based on setting up the user & subject + // mock getters based on setting up the context & subject var serviceMock: DarklyServiceMock! { subject.service as? DarklyServiceMock } @@ -188,7 +188,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("saves the user") { + it("saves the context") { expect(testContext.subject.context) == testContext.context expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) @@ -197,7 +197,7 @@ final class LDClientSpec: QuickSpec { } expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } @@ -207,7 +207,7 @@ final class LDClientSpec: QuickSpec { } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } it("starts in foreground") { @@ -231,7 +231,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("saves the user") { + it("saves the context") { expect(testContext.subject.context) == testContext.context expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) @@ -240,7 +240,7 @@ final class LDClientSpec: QuickSpec { } expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } @@ -250,15 +250,15 @@ final class LDClientSpec: QuickSpec { } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } it("starts in foreground") { expect(testContext.subject.runMode) == .foreground } } - context("when called without user") { - context("after setting user") { + context("when called without context") { + context("after setting context") { beforeEach { testContext = TestContext(startOnline: true).withContext(nil) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() @@ -273,7 +273,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("saves the user") { + it("saves the context") { expect(testContext.subject.context) == testContext.context expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) @@ -282,7 +282,7 @@ final class LDClientSpec: QuickSpec { } expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 // called on init and subsequent identify expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } @@ -292,11 +292,11 @@ final class LDClientSpec: QuickSpec { } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } - context("without setting user") { + context("without setting context") { beforeEach { testContext = TestContext(startOnline: true).withContext(nil) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() @@ -308,13 +308,13 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("uses anonymous user") { + it("uses anonymous context") { expect(testContext.subject.context.fullyQualifiedKey()) == LDContext.defaultKey(kind: testContext.subject.context.kind) expect(testContext.subject.service.context) == testContext.subject.context expect(testContext.makeFlagSynchronizerService?.context) == testContext.subject.context expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.subject.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.subject.context.fullyQualifiedHashedKey() } @@ -324,12 +324,12 @@ final class LDClientSpec: QuickSpec { } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } } - it("when called with cached flags for the user and environment") { + it("when called with cached flags for the context and environment") { let cachedFlags = ["test-flag": StorageItem.item(FeatureFlag(flagKey: "test-flag"))] let testContext = TestContext().withCached(flags: cachedFlags.featureFlags) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() @@ -340,10 +340,10 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags) == cachedFlags expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } - it("when called without cached flags for the user") { + it("when called without cached flags for the context") { let testContext = TestContext() withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() @@ -353,7 +353,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagStoreMock.replaceStoreCallCount) == 0 expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } @@ -471,7 +471,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("saves the user") { + it("saves the context") { expect(testContext.subject.context) == testContext.context expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) @@ -480,7 +480,7 @@ final class LDClientSpec: QuickSpec { } expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } @@ -510,7 +510,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("saves the user") { + it("saves the context") { expect(testContext.subject.context) == testContext.context expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) @@ -519,7 +519,7 @@ final class LDClientSpec: QuickSpec { } expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } @@ -579,7 +579,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } - it("when the new user has cached feature flags") { + it("when the new context has cached feature flags") { let stubFlags = FlagMaintainingMock.stubStoredItems() let newContext = LDContext.stub() let testContext = TestContext().withCached(contextKey: newContext.fullyQualifiedHashedKey(), flags: stubFlags.featureFlags) diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index d41e56ee..d62c5761 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -15,10 +15,10 @@ final class ClientServiceMockFactory: ClientServiceCreating { var makeFeatureFlagCacheReturnValue = FeatureFlagCachingMock() var makeFeatureFlagCacheCallback: (() -> Void)? var makeFeatureFlagCacheCallCount = 0 - var makeFeatureFlagCacheReceivedParameters: (mobileKey: MobileKey, maxCachedUsers: Int)? = nil - func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedUsers: Int = 5) -> FeatureFlagCaching { + var makeFeatureFlagCacheReceivedParameters: (mobileKey: MobileKey, maxCachedContexts: Int)? = nil + func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedContexts: Int = 5) -> FeatureFlagCaching { makeFeatureFlagCacheCallCount += 1 - makeFeatureFlagCacheReceivedParameters = (mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) + makeFeatureFlagCacheReceivedParameters = (mobileKey: mobileKey, maxCachedContexts: maxCachedContexts) makeFeatureFlagCacheCallback?() return makeFeatureFlagCacheReturnValue } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift index eb9d21f9..ec090a2d 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift @@ -3,24 +3,24 @@ import Foundation extension LDContext { struct StubConstants { - static let key: LDValue = "stub.user.key" + static let key: LDValue = "stub.context.key" - static let name = "stub.user.name" - static let secondary = "stub.user.secondary" + static let name = "stub.context.name" + static let secondary = "stub.context.secondary" static let isAnonymous = false - static let firstName: LDValue = "stub.user.firstName" - static let lastName: LDValue = "stub.user.lastName" - static let country: LDValue = "stub.user.country" - static let ipAddress: LDValue = "stub.user.ipAddress" - static let email: LDValue = "stub.user@email.com" - static let avatar: LDValue = "stub.user.avatar" - 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 let firstName: LDValue = "stub.context.firstName" + static let lastName: LDValue = "stub.context.lastName" + static let country: LDValue = "stub.context.country" + static let ipAddress: LDValue = "stub.context.ipAddress" + static let email: LDValue = "stub.context@email.com" + static let avatar: LDValue = "stub.context.avatar" + static let custom: [String: LDValue] = ["stub.context.custom.keyA": "stub.context.custom.valueA", + "stub.context.custom.keyB": true, + "stub.context.custom.keyC": 1027, + "stub.context.custom.keyD": 2.71828, + "stub.context.custom.keyE": [0, 1, 2], + "stub.context.custom.keyF": ["1": 1, "2": 2, "3": 3]] } static func stub(key: String? = nil, diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift deleted file mode 100644 index c4a1b558..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -@testable import LaunchDarkly - -extension LDUser { - struct StubConstants { - static let key = "stub.user.key" - static let secondary = "stub.user.secondary" - static let userKey = "userKey" - static let name = "stub.user.name" - static let firstName = "stub.user.firstName" - static let lastName = "stub.user.lastName" - static let isAnonymous = false - static let country = "stub.user.country" - static let ipAddress = "stub.user.ipAddress" - static let email = "stub.user@email.com" - static let avatar = "stub.user.avatar" - 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 stub(key: String? = nil, - environmentReporter: EnvironmentReportingMock? = nil) -> LDUser { - let user = LDUser(key: key ?? UUID().uuidString, - name: StubConstants.name, - firstName: StubConstants.firstName, - lastName: StubConstants.lastName, - country: StubConstants.country, - ipAddress: StubConstants.ipAddress, - email: StubConstants.email, - avatar: StubConstants.avatar, - custom: StubConstants.custom, - isAnonymous: StubConstants.isAnonymous, - secondary: StubConstants.secondary) - return user - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index e2c66bd2..bdd28998 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -181,11 +181,11 @@ final class DiagnosticEventSpec: QuickSpec { customConfig.allContextAttributesPrivate = true customConfig.flagPollingInterval = 360.0 customConfig.backgroundFlagPollingInterval = 1_800.0 - customConfig.inlineUserInEvents = true + customConfig.inlineContextInEvents = true customConfig.useReport = true customConfig.enableBackgroundUpdates = true customConfig.evaluationReasons = true - customConfig.maxCachedUsers = -2 + customConfig.maxCachedContexts = -2 try! customConfig.setSecondaryMobileKeys(["test": "foobar1", "debug": "foobar2"]) customConfig.diagnosticRecordingInterval = 600.0 customConfig.wrapperName = "ReactNative" @@ -212,11 +212,11 @@ final class DiagnosticEventSpec: QuickSpec { expect(diagnosticConfig.allAttributesPrivate) == false expect(diagnosticConfig.pollingIntervalMillis) == 300_000 expect(diagnosticConfig.backgroundPollingIntervalMillis) == 3_600_000 - expect(diagnosticConfig.inlineUsersInEvents) == false + expect(diagnosticConfig.inlineContextsInEvents) == false expect(diagnosticConfig.useReport) == false expect(diagnosticConfig.backgroundPollingDisabled) == true expect(diagnosticConfig.evaluationReasonsRequested) == false - expect(diagnosticConfig.maxCachedUsers) == 5 + expect(diagnosticConfig.maxCachedContexts) == 5 expect(diagnosticConfig.mobileKeyCount) == 1 expect(diagnosticConfig.diagnosticRecordingIntervalMillis) == 900_000 expect(diagnosticConfig.customHeaders) == false @@ -235,12 +235,12 @@ final class DiagnosticEventSpec: QuickSpec { expect(diagnosticConfig.allAttributesPrivate) == true expect(diagnosticConfig.pollingIntervalMillis) == 360_000 expect(diagnosticConfig.backgroundPollingIntervalMillis) == 1_800_000 - expect(diagnosticConfig.inlineUsersInEvents) == true + expect(diagnosticConfig.inlineContextsInEvents) == true expect(diagnosticConfig.useReport) == true expect(diagnosticConfig.backgroundPollingDisabled) == false expect(diagnosticConfig.evaluationReasonsRequested) == true // All negative values become -1 for consistency - expect(diagnosticConfig.maxCachedUsers) == -1 + expect(diagnosticConfig.maxCachedContexts) == -1 expect(diagnosticConfig.mobileKeyCount) == 3 expect(diagnosticConfig.diagnosticRecordingIntervalMillis) == 600_000 expect(diagnosticConfig.customHeaders) == false @@ -281,11 +281,11 @@ final class DiagnosticEventSpec: QuickSpec { 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["inlineContextsInEvents"]) == .bool(diagnosticConfig.inlineContextsInEvents) 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["maxCachedContexts"]) == .number(Double(diagnosticConfig.maxCachedContexts)) expect(decoded["mobileKeyCount"]) == .number(Double(diagnosticConfig.mobileKeyCount)) expect(decoded["diagnosticRecordingIntervalMillis"]) == .number(Double(diagnosticConfig.diagnosticRecordingIntervalMillis)) } @@ -302,11 +302,11 @@ final class DiagnosticEventSpec: QuickSpec { expect(decoded?.allAttributesPrivate) == diagnosticConfig.allAttributesPrivate expect(decoded?.pollingIntervalMillis) == diagnosticConfig.pollingIntervalMillis expect(decoded?.backgroundPollingIntervalMillis) == diagnosticConfig.backgroundPollingIntervalMillis - expect(decoded?.inlineUsersInEvents) == diagnosticConfig.inlineUsersInEvents + expect(decoded?.inlineContextsInEvents) == diagnosticConfig.inlineContextsInEvents expect(decoded?.useReport) == diagnosticConfig.useReport expect(decoded?.backgroundPollingDisabled) == diagnosticConfig.backgroundPollingDisabled expect(decoded?.evaluationReasonsRequested) == diagnosticConfig.evaluationReasonsRequested - expect(decoded?.maxCachedUsers) == diagnosticConfig.maxCachedUsers + expect(decoded?.maxCachedContexts) == diagnosticConfig.maxCachedContexts expect(decoded?.mobileKeyCount) == diagnosticConfig.mobileKeyCount expect(decoded?.diagnosticRecordingIntervalMillis) == diagnosticConfig.diagnosticRecordingIntervalMillis } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 20a9db75..9649333c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -79,7 +79,7 @@ final class EventSpec: XCTestCase { } } - func testCustomEventEncodingAnonUser() { + func testCustomEventEncodingAnonContext() { let context = LDContext.stub() let event = CustomEvent(key: "event-key", context: context, data: ["key": "val"]) encodesToObject(event) { dict in @@ -95,7 +95,7 @@ final class EventSpec: XCTestCase { func testCustomEventEncodingInlining() { let context = LDContext.stub() let event = CustomEvent(key: "event-key", context: context, data: nil, metricValue: 2.5) - encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: true]) { dict in + encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineContextInEvents: true]) { dict in XCTAssertEqual(dict.count, 5) XCTAssertEqual(dict["kind"], "custom") XCTAssertEqual(dict["key"], "event-key") @@ -194,13 +194,13 @@ final class EventSpec: XCTestCase { } } - func testFeatureEventEncodingInlinesUserForDebugOrConfig() { + func testFeatureEventEncodingInlinesContextForDebugOrConfig() { let context = LDContext.stub() let featureFlag = FeatureFlag(flagKey: "flag-key", version: 3) let featureEvent = FeatureEvent(key: "event-key", context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: false) let debugEvent = FeatureEvent(key: "event-key", context: context, 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]) + let encodedFeature = encodeToLDValue(featureEvent, userInfo: [Event.UserInfoKeys.inlineContextInEvents: true]) + let encodedDebug = encodeToLDValue(debugEvent, userInfo: [Event.UserInfoKeys.inlineContextInEvents: false]) [encodedFeature, encodedDebug].forEach { valueIsObject($0) { dict in XCTAssertEqual(dict.count, 7) XCTAssertEqual(dict["key"], "event-key") @@ -213,9 +213,9 @@ final class EventSpec: XCTestCase { func testIdentifyEventEncoding() throws { let context = LDContext.stub() - for inlineUser in [true, false] { + for inlineContext in [true, false] { let event = IdentifyEvent(context: context) - encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: inlineUser]) { dict in + encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineContextInEvents: inlineContext]) { dict in XCTAssertEqual(dict.count, 4) XCTAssertEqual(dict["kind"], "identify") XCTAssertEqual(dict["key"], .string(context.fullyQualifiedKey())) @@ -249,8 +249,7 @@ final class EventSpec: XCTestCase { extension Event: Equatable { public static func == (_ lhs: Event, _ rhs: Event) -> Bool { - // TODO(mmk) Do we need this inline users stuff? - let config = [LDContext.UserInfoKeys.includePrivateAttributes: true, Event.UserInfoKeys.inlineUserInEvents: true] + let config = [LDContext.UserInfoKeys.includePrivateAttributes: true, Event.UserInfoKeys.inlineContextInEvents: true] return encodeToLDValue(lhs, userInfo: config) == encodeToLDValue(rhs, userInfo: config) } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index 645be9cf..b5fc1464 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -21,11 +21,11 @@ final class LDConfigSpec: XCTestCase { fileprivate static let useReport = true - fileprivate static let inlineUserInEvents = true + fileprivate static let inlineContextInEvents = true fileprivate static let debugMode = true fileprivate static let evaluationReasons = true - fileprivate static let maxCachedUsers = -1 + fileprivate static let maxCachedContexts = -1 fileprivate static let diagnosticOptOut = true fileprivate static let diagnosticRecordingInterval: TimeInterval = 600.0 fileprivate static let wrapperName = "ReactNative" @@ -47,12 +47,12 @@ final class LDConfigSpec: XCTestCase { ("enable background updates", Constants.enableBackgroundUpdates, { c, v in c.enableBackgroundUpdates = v as! Bool }), ("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.allContextAttributesPrivate, { c, v in c.allContextAttributesPrivate = v as! Bool }), - ("private user attributes", Constants.privateContextAttributes, { c, v in c.privateContextAttributes = (v as! [Reference])}), + ("all context attributes private", Constants.allContextAttributesPrivate, { c, v in c.allContextAttributesPrivate = v as! Bool }), + ("private context attributes", Constants.privateContextAttributes, { c, v in c.privateContextAttributes = (v as! [Reference])}), ("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 }), + ("inline context in events", Constants.inlineContextInEvents, { c, v in c.inlineContextInEvents = v as! Bool }), ("evaluation reasons", Constants.evaluationReasons, { c, v in c.evaluationReasons = v as! Bool }), - ("max cached users", Constants.maxCachedUsers, { c, v in c.maxCachedUsers = v as! Int }), + ("max cached contexts", Constants.maxCachedContexts, { c, v in c.maxCachedContexts = v as! Int }), ("diagnostic opt out", Constants.diagnosticOptOut, { c, v in c.diagnosticOptOut = v as! Bool }), ("diagnostic recording interval", Constants.diagnosticRecordingInterval, { c, v in c.diagnosticRecordingInterval = v as! TimeInterval }), ("wrapper name", Constants.wrapperName, { c, v in c.wrapperName = v as! String? }), @@ -76,10 +76,10 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.allContextAttributesPrivate, LDConfig.Defaults.allContextAttributesPrivate) XCTAssertEqual(config.privateContextAttributes, LDConfig.Defaults.privateContextAttributes) XCTAssertEqual(config.useReport, LDConfig.Defaults.useReport) - XCTAssertEqual(config.inlineUserInEvents, LDConfig.Defaults.inlineUserInEvents) + XCTAssertEqual(config.inlineContextInEvents, LDConfig.Defaults.inlineContextInEvents) XCTAssertEqual(config.isDebugMode, LDConfig.Defaults.debugMode) XCTAssertEqual(config.evaluationReasons, LDConfig.Defaults.evaluationReasons) - XCTAssertEqual(config.maxCachedUsers, LDConfig.Defaults.maxCachedUsers) + XCTAssertEqual(config.maxCachedContexts, LDConfig.Defaults.maxCachedContexts) XCTAssertEqual(config.diagnosticOptOut, LDConfig.Defaults.diagnosticOptOut) XCTAssertEqual(config.diagnosticRecordingInterval, LDConfig.Defaults.diagnosticRecordingInterval) XCTAssertEqual(config.wrapperName, LDConfig.Defaults.wrapperName) @@ -110,10 +110,10 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.allContextAttributesPrivate, Constants.allContextAttributesPrivate, "\(os)") XCTAssertEqual(config.privateContextAttributes, Constants.privateContextAttributes, "\(os)") XCTAssertEqual(config.useReport, Constants.useReport, "\(os)") - XCTAssertEqual(config.inlineUserInEvents, Constants.inlineUserInEvents, "\(os)") + XCTAssertEqual(config.inlineContextInEvents, Constants.inlineContextInEvents, "\(os)") XCTAssertEqual(config.isDebugMode, Constants.debugMode, "\(os)") XCTAssertEqual(config.evaluationReasons, Constants.evaluationReasons, "\(os)") - XCTAssertEqual(config.maxCachedUsers, Constants.maxCachedUsers, "\(os)") + XCTAssertEqual(config.maxCachedContexts, Constants.maxCachedContexts, "\(os)") XCTAssertEqual(config.diagnosticOptOut, Constants.diagnosticOptOut, "\(os)") XCTAssertEqual(config.diagnosticRecordingInterval, Constants.diagnosticRecordingInterval, "\(os)") XCTAssertEqual(config.wrapperName, Constants.wrapperName, "\(os)") diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift deleted file mode 100644 index fe1ddd89..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ /dev/null @@ -1,121 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class LDUserSpec: QuickSpec { - - override func spec() { - initSpec() - } - - private func initSpec() { - initSubSpec() - initWithEnvironmentReporterSpec() - } - - private func initSubSpec() { - var user: LDUser! - describe("init") { - 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, - isAnonymous: LDUser.StubConstants.isAnonymous, - privateAttributes: LDUser.optionalAttributes, - 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.isAnonymousNullable) == 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).to(beTrue()) - expect(user.privateAttributes) == LDUser.optionalAttributes - } - it("without setting anonymous") { - user = LDUser(key: "abc") - expect(user.isAnonymous) == false - expect(user.isAnonymousNullable).to(beNil()) - } - context("called without optional elements") { - var environmentReporter: EnvironmentReporter! - beforeEach { - user = LDUser() - environmentReporter = EnvironmentReporter() - } - it("creates a LDUser without optional elements") { - expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) - expect(user.isAnonymous) == true - expect(user.isAnonymousNullable) == true - - 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.count) == 0 - expect(user.privateAttributes).to(beEmpty()) - expect(user.secondary).to(beNil()) - } - } - context("called without a key multiple times") { - var users = [LDUser]() - beforeEach { - while users.count < 3 { - users.append(LDUser()) - } - } - it("creates each LDUser with the default key and isAnonymous set") { - let environmentReporter = EnvironmentReporter() - users.forEach { user in - expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) - expect(user.isAnonymous) == true - expect(user.isAnonymousNullable) == true - } - } - } - } - } - - private func initWithEnvironmentReporterSpec() { - describe("initWithEnvironmentReporter") { - var user: LDUser! - var environmentReporter: EnvironmentReportingMock! - beforeEach { - environmentReporter = EnvironmentReportingMock() - user = LDUser(environmentReporter: environmentReporter) - } - it("creates a user with system values matching the environment reporter") { - expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) - expect(user.isAnonymous) == true - expect(user.isAnonymousNullable) == true - - 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.count) == 0 - - expect(user.privateAttributes).to(beEmpty()) - } - } - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index d031dac8..cf826c5a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -106,7 +106,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/contexts/ expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) @@ -159,7 +159,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/contexts/ expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) @@ -273,7 +273,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/context; httpBody contains the context dictionary expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasSuffix(DarklyService.FlagRequestPath.report)).to(beTrue()) @@ -325,7 +325,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/context; httpBody contains the context dictionary expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasSuffix(DarklyService.FlagRequestPath.report)).to(beTrue()) diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift index 76a52eb2..3ce4afe7 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift @@ -18,7 +18,7 @@ final class CacheConverterSpec: XCTestCase { } func testNoKeysGiven() { - CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: [], maxCachedUsers: 0) + CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: [], maxCachedContexts: 0) XCTAssertEqual(serviceFactory.makeKeyedValueCacheCallCount, 0) XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 0) } @@ -29,7 +29,7 @@ final class CacheConverterSpec: XCTestCase { serviceFactory.makeFeatureFlagCacheReturnValue.keyedValueCache = v7valueCacheMock serviceFactory.makeKeyedValueCacheReturnValue = v7valueCacheMock v7valueCacheMock.dataReturnValue = CacheConverterSpec.upToDateData - CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: ["key1", "key2"], maxCachedUsers: 0) + CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: ["key1", "key2"], maxCachedContexts: 0) XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 2) XCTAssertEqual(v7valueCacheMock.dataCallCount, 2) } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift index aa2ce83b..93c092a7 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift @@ -15,8 +15,8 @@ final class FeatureFlagCacheSpec: XCTestCase { } func testInit() { - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) - XCTAssertEqual(flagCache.maxCachedUsers, 2) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 2) + XCTAssertEqual(flagCache.maxCachedContexts, 2) XCTAssertEqual(serviceFactory.makeKeyedValueCacheCallCount, 1) let bundleHashed = Util.sha256base64(Bundle.main.bundleIdentifier!) let keyHashed = Util.sha256base64("abc") @@ -26,36 +26,36 @@ final class FeatureFlagCacheSpec: XCTestCase { } func testRetrieveNoData() { - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) - XCTAssertNil(flagCache.retrieveFeatureFlags(contextKey: "user1")) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 0) + XCTAssertNil(flagCache.retrieveFeatureFlags(contextKey: "context1")) XCTAssertEqual(mockValueCache.dataCallCount, 1) - XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-user1") + XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-context1") } func testRetrieveInvalidData() { mockValueCache.dataReturnValue = Data("invalid".utf8) - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - XCTAssertNil(flagCache.retrieveFeatureFlags(contextKey: "user1")) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 1) + XCTAssertNil(flagCache.retrieveFeatureFlags(contextKey: "context1")) } func testRetrieveEmptyData() throws { mockValueCache.dataReturnValue = try JSONEncoder().encode(StoredItemCollection([:])) - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) - XCTAssertEqual(flagCache.retrieveFeatureFlags(contextKey: "user1")?.count, 0) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 2) + XCTAssertEqual(flagCache.retrieveFeatureFlags(contextKey: "context1")?.count, 0) } func testRetrieveValidData() throws { mockValueCache.dataReturnValue = try JSONEncoder().encode(testFlagCollection) - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - let retrieved = flagCache.retrieveFeatureFlags(contextKey: "user1") + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 1) + let retrieved = flagCache.retrieveFeatureFlags(contextKey: "context1") XCTAssertEqual(retrieved, testFlagCollection.flags) XCTAssertEqual(mockValueCache.dataCallCount, 1) - XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-user1") + XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-context1") } func testStoreCacheDisabled() { - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) - flagCache.storeFeatureFlags([:], contextKey: "user1", lastUpdated: Date()) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 0) + flagCache.storeFeatureFlags([:], contextKey: "context1", lastUpdated: Date()) XCTAssertEqual(mockValueCache.setCallCount, 0) XCTAssertEqual(mockValueCache.dataCallCount, 0) XCTAssertEqual(mockValueCache.removeObjectCallCount, 0) @@ -63,20 +63,20 @@ final class FeatureFlagCacheSpec: XCTestCase { func testStoreEmptyData() throws { let now = Date() - let hashedUserKey = Util.sha256base64("user1") var count = 0 mockValueCache.setCallback = { - if self.mockValueCache.setReceivedArguments?.forKey == "cached-users" { + if self.mockValueCache.setReceivedArguments?.forKey == "cached-contexts" { let setData = self.mockValueCache.setReceivedArguments!.value - XCTAssertEqual(setData, try JSONEncoder().encode([hashedUserKey: now.millisSince1970])) + XCTAssertEqual(setData, try JSONEncoder().encode(["context1": now.millisSince1970])) count += 1 } else if let received = self.mockValueCache.setReceivedArguments { - XCTAssertEqual(received.forKey, "flags-\(hashedUserKey)") + XCTAssertEqual(received.forKey, "flags-context1") + XCTAssertEqual(received.value, try JSONEncoder().encode(StoredItemCollection([:]))) count += 2 } } - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: -1) - flagCache.storeFeatureFlags([:], contextKey: hashedUserKey, lastUpdated: now) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: -1) + flagCache.storeFeatureFlags([:], contextKey: "context1", lastUpdated: now) XCTAssertEqual(count, 3) } @@ -86,51 +86,51 @@ final class FeatureFlagCacheSpec: XCTestCase { XCTAssertEqual(received.value, try JSONEncoder().encode(self.testFlagCollection)) } } - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: "user1", lastUpdated: Date()) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: "context1", lastUpdated: Date()) XCTAssertEqual(mockValueCache.setCallCount, 2) } - func testStoreMaxCachedUsersStored() throws { - let hashedUserKey = Util.sha256base64("user1") + func testStoreMaxCachedContextsStored() throws { + let hashedContextKey = Util.sha256base64("context1") 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, contextKey: hashedUserKey, lastUpdated: now) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedContextKey, 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]) + XCTAssertEqual(setMetadata, [hashedContextKey: now.millisSince1970]) } - func testStoreAboveMaxCachedUsersStored() throws { - let hashedUserKey = Util.sha256base64("user1") + func testStoreAboveMaxCachedContextsStored() throws { + let hashedContextKey = Util.sha256base64("context1") 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) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 2) var removedObjects: [String] = [] mockValueCache.removeObjectCallback = { removedObjects.append(self.mockValueCache.removeObjectReceivedForKey!) } - flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedUserKey, lastUpdated: later) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedContextKey, 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]) + XCTAssertEqual(setMetadata, [hashedContextKey: later.millisSince1970, "key3": later.millisSince1970]) } func testStoreInvalidMetadataStored() throws { - let hashedUserKey = Util.sha256base64("user1") + let hashedContxtKey = Util.sha256base64("context1") let now = Date() mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": "123"]) - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedUserKey, lastUpdated: now) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedContxtKey, lastUpdated: now) XCTAssertEqual(mockValueCache.removeObjectCallCount, 0) let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) - XCTAssertEqual(setMetadata, [hashedUserKey: now.millisSince1970]) + XCTAssertEqual(setMetadata, [hashedContxtKey: now.millisSince1970]) } } From 61a99d9ff8d8251b2a2a178b5e8cbadb8f155895 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 18 Jul 2022 11:26:01 -0400 Subject: [PATCH 69/90] Remove restriction on setting attribute names (#219) --- .../LaunchDarkly/Models/Context/LDContext.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index acc8c9c3..92401a59 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -661,11 +661,7 @@ public struct LDContextBuilder { self.name = name } - /// Sets the value of any attribute for the Context. - /// - /// This includes only attributes that are addressable in evaluations -- not metadata such as - /// secondary or private. If `name` is "privateAttributeNames", it is ignored and no - /// attribute is set. + /// Sets the value of any attribute for the Context except for private attributes. /// /// This method uses the `LDValue` type to represent a value of any JSON type: null, /// boolean, number, string, array, or object. For all attribute names that do not have special @@ -680,6 +676,8 @@ public struct LDContextBuilder { /// /// - "name": Must be a string or null. See `LDContextBuilder.name(_:)`. /// + /// - "secondary": Must be a string. See `LDContextBuilder.secondary(_:)`. + /// /// - "anonymous": Must be a boolean. See `LDContextBuilder.anonymous(_:)`. /// /// Values that are JSON arrays or objects have special behavior when referenced in @@ -719,9 +717,6 @@ public struct LDContextBuilder { self.secondary(val) case ("secondary", _): return false - case ("privateAttributeNames", _): - Log.debug(typeName(and: #function) + ": The privateAttributeNames property has been replaced with privateAttributes. Refusing to set a property named privateAttributeNames.") - return false case (_, .null): self.attributes.removeValue(forKey: name) return false From 4481b2888fe756162076c679e611b2aa81d71051 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 18 Jul 2022 11:46:09 -0400 Subject: [PATCH 70/90] Update xcode image version (#217) According to the CircleCI announcement on [June 2nd, 2022][1], several xcode images are going to be deprecated and removed. This commit bumps our xcode versions to a supported image version. [1]: https://discuss.circleci.com/t/xcode-image-deprecation/44294 --- .circleci/config.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b718056e..38e829e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -43,9 +43,6 @@ jobs: type: string ios-sim: type: string - ssh-fix: - type: boolean - default: false build-doc: type: boolean default: false @@ -62,16 +59,6 @@ jobs: steps: - checkout - # 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 fix - command: git config --global --unset url.ssh://git@github.aaakk.us.kg.insteadof - - run: name: Setup for builds command: | @@ -178,11 +165,6 @@ workflows: name: Xcode 12.5 - Swift 5.4 xcode-version: '12.5.1' ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=14.5' - - build: - 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.7 - Swift 5.2 xcode-version: '11.7.0' From 8bed2c895088c662c0e9daf2d43f407e552882e1 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Tue, 30 Aug 2022 13:41:30 -0400 Subject: [PATCH 71/90] Update LDSwiftEventSource to v2.0.0 (#220) --- LaunchDarkly.podspec | 2 +- LaunchDarkly.xcodeproj/project.pbxproj | 50 +++++++++++++------------- Package.swift | 4 +-- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index aa202b0d..65ae118d 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.1' + es.dependency 'LDSwiftEventSource', '2.0.0' end end diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 23e2408d..ffb404a2 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -197,12 +197,12 @@ 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 */; }; + A322B4CD28BE4D9E00A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4CC28BE4D9E00A212ED /* LDSwiftEventSource */; }; + A322B4CF28BE4DA800A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4CE28BE4DA800A212ED /* LDSwiftEventSource */; }; + A322B4D128BE4DB200A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4D028BE4DB200A212ED /* LDSwiftEventSource */; }; + A322B4D328BE4DBA00A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4D228BE4DBA00A212ED /* LDSwiftEventSource */; }; B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; }; B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.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 */; }; - B467791924D8AEFC00897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791824D8AEFC00897F00 /* LDSwiftEventSourceStatic */; }; B468E71024B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */; }; B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */; }; B468E71224B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */; }; @@ -414,7 +414,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B467791924D8AEFC00897F00 /* LDSwiftEventSourceStatic in Frameworks */, + A322B4D328BE4DBA00A212ED /* LDSwiftEventSource in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -422,7 +422,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B467791724D8AEF800897F00 /* LDSwiftEventSourceStatic in Frameworks */, + A322B4D128BE4DB200A212ED /* LDSwiftEventSource in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -430,7 +430,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B467791324D8AEEC00897F00 /* LDSwiftEventSourceStatic in Frameworks */, + A322B4CD28BE4D9E00A212ED /* LDSwiftEventSource in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -449,7 +449,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B467791524D8AEF300897F00 /* LDSwiftEventSourceStatic in Frameworks */, + A322B4CF28BE4DA800A212ED /* LDSwiftEventSource in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -814,7 +814,7 @@ ); name = LaunchDarkly_tvOS; packageProductDependencies = ( - B467791824D8AEFC00897F00 /* LDSwiftEventSourceStatic */, + A322B4D228BE4DBA00A212ED /* LDSwiftEventSource */, ); productName = Darkly_tvOS; productReference = 831188382113A16900D77CB5 /* LaunchDarkly_tvOS.framework */; @@ -838,7 +838,7 @@ ); name = LaunchDarkly_macOS; packageProductDependencies = ( - B467791624D8AEF800897F00 /* LDSwiftEventSourceStatic */, + A322B4D028BE4DB200A212ED /* LDSwiftEventSource */, ); productName = Darkly_macOS; productReference = 831EF33B20655D700001C643 /* LaunchDarkly_macOS.framework */; @@ -862,7 +862,7 @@ ); name = LaunchDarkly_iOS; packageProductDependencies = ( - B467791224D8AEEC00897F00 /* LDSwiftEventSourceStatic */, + A322B4CC28BE4D9E00A212ED /* LDSwiftEventSource */, ); productName = Darkly; productReference = 8354EFC21F22491C00C05156 /* LaunchDarkly.framework */; @@ -911,7 +911,7 @@ ); name = LaunchDarkly_watchOS; packageProductDependencies = ( - B467791424D8AEF300897F00 /* LDSwiftEventSourceStatic */, + A322B4CE28BE4DA800A212ED /* LDSwiftEventSource */, ); productName = "Darkly-watchOS"; productReference = 83D9EC6B2062DBB7004D7FA6 /* LaunchDarkly_watchOS.framework */; @@ -1776,7 +1776,7 @@ repositoryURL = "https://github.com/LaunchDarkly/swift-eventsource.git"; requirement = { kind = exactVersion; - version = 1.3.1; + version = 2.0.0; }; }; B4903D9624BD61B200F087C4 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { @@ -1806,45 +1806,45 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - B445A6E324C0D1E3000BAD6D /* LDSwiftEventSource */ = { + A322B4CC28BE4D9E00A212ED /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; productName = LDSwiftEventSource; }; - B445A6F224C0D21E000BAD6D /* LDSwiftEventSource */ = { + A322B4CE28BE4DA800A212ED /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; productName = LDSwiftEventSource; }; - B445A6F824C0D232000BAD6D /* LDSwiftEventSource */ = { + A322B4D028BE4DB200A212ED /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; productName = LDSwiftEventSource; }; - B445A6FA24C0D237000BAD6D /* LDSwiftEventSource */ = { + A322B4D228BE4DBA00A212ED /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; productName = LDSwiftEventSource; }; - B467791224D8AEEC00897F00 /* LDSwiftEventSourceStatic */ = { + B445A6E324C0D1E3000BAD6D /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; - productName = LDSwiftEventSourceStatic; + productName = LDSwiftEventSource; }; - B467791424D8AEF300897F00 /* LDSwiftEventSourceStatic */ = { + B445A6F224C0D21E000BAD6D /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; - productName = LDSwiftEventSourceStatic; + productName = LDSwiftEventSource; }; - B467791624D8AEF800897F00 /* LDSwiftEventSourceStatic */ = { + B445A6F824C0D232000BAD6D /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; - productName = LDSwiftEventSourceStatic; + productName = LDSwiftEventSource; }; - B467791824D8AEFC00897F00 /* LDSwiftEventSourceStatic */ = { + B445A6FA24C0D237000BAD6D /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; - productName = LDSwiftEventSourceStatic; + productName = LDSwiftEventSource; }; B4903D9724BD61B200F087C4 /* OHHTTPStubsSwift */ = { isa = XCSwiftPackageProductDependency; diff --git a/Package.swift b/Package.swift index 30d74f3b..3073a343 100644 --- a/Package.swift +++ b/Package.swift @@ -19,13 +19,13 @@ 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.1")) + .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("2.0.0")) ], targets: [ .target( name: "LaunchDarkly", dependencies: [ - .product(name: "LDSwiftEventSourceStatic", package: "LDSwiftEventSource") + .product(name: "LDSwiftEventSource", package: "LDSwiftEventSource") ], path: "LaunchDarkly/LaunchDarkly", exclude: ["Support"]), From 68294a0956040d7bdb150182db9a893c9b4c7390 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 3 Oct 2022 08:48:41 -0400 Subject: [PATCH 72/90] Remove array indexing reference support (#221) --- .../Models/Context/LDContext.swift | 22 +++----------- .../Models/Context/Reference.swift | 29 +++++-------------- .../Models/Context/LDContextSpec.swift | 5 ++-- .../Models/Context/ReferenceSpec.swift | 25 ++++++++-------- 4 files changed, 26 insertions(+), 55 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 92401a59..68cdcf04 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -189,7 +189,7 @@ public struct LDContext: Encodable, Equatable { var hasMatch = true for (index, parentPart) in parentPath.enumerated() { - if let (name, _) = privateAttribute.component(index) { + if let name = privateAttribute.component(index) { if name != parentPart { hasMatch = false break @@ -280,7 +280,7 @@ public struct LDContext: Encodable, Equatable { let parentMap = returnValue for index in 0...reference.depth() { - if let (name, _) = reference.component(index) { + if let name = reference.component(index) { if !parentMap.contains(name) { let nextNode = PrivateAttributeLookupNode() @@ -352,7 +352,7 @@ public struct LDContext: Encodable, Equatable { return nil } - guard let (component, _) = reference.component(0) else { + guard let component = reference.component(0) else { return nil } @@ -370,24 +370,10 @@ public struct LDContext: Encodable, Equatable { } for depth in 1.. Result { if !part.contains("~") { @@ -128,11 +126,11 @@ public struct Reference: Codable, Equatable, Hashable { } if value.prefix(1) != "/" { - components = [Component(name: value, value: nil)] + components = [value] return } - var referenceComponents: [Component] = [] + var referenceComponents: [String] = [] let parts = value.components(separatedBy: "/") for (index, part) in parts.enumerated() { if index == 0 { @@ -149,7 +147,7 @@ public struct Reference: Codable, Equatable, Hashable { let result = Reference.unescapePath(part) switch result { case .success(let unescapedPath): - referenceComponents.append(Component(name: unescapedPath, value: Int(part))) + referenceComponents.append(unescapedPath) case .failure(let err): error = err return @@ -190,22 +188,11 @@ public struct Reference: Codable, Equatable, Hashable { return rawPath } - internal func component(_ index: Int) -> (String, Int?)? { + internal func component(_ index: Int) -> String? { if index >= self.depth() { return nil } - let component = self.components[index] - return (component.name, component.value) - } -} - -private struct Component: Codable, Equatable, Hashable { - fileprivate let name: String - fileprivate let value: Int? - - init(name: String, value: Int?) { - self.name = name - self.value = value + return self.components[index] } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift index 743a0c9b..ca34cf6c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift @@ -234,14 +234,13 @@ final class LDContextSpec: XCTestCase { ("secondary", nil), // Can index arrays and objects ("/my-map/array", .array([.string("first"), .string("second")])), - ("/my-map/array/1", .string("second")), - ("/my-map/array/2", nil), + ("/my-map/1", .bool(true)), ("my-map/missing", nil), ("/starts-with-slash/1", nil) ] let array: [LDValue] = [.string("first"), .string("second")] - let map: [String: LDValue] = ["array": .array(array)] + let map: [String: LDValue] = ["array": .array(array), "1": .bool(true)] for (input, expectedValue) in tests { var builder = LDContextBuilder(key: "my-key") diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift index d5263a14..0cded180 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift @@ -31,7 +31,7 @@ final class ReferenceSpec: XCTestCase { let ref = Reference(test) XCTAssertTrue(ref.isValid()) XCTAssertEqual(1, ref.depth()) - XCTAssertEqual(test, ref.component(0)?.0) + XCTAssertEqual(test, ref.component(0)) } } @@ -47,27 +47,26 @@ final class ReferenceSpec: XCTestCase { let ref = Reference(ref) XCTAssertTrue(ref.isValid()) XCTAssertEqual(1, ref.depth()) - XCTAssertEqual(expected, ref.component(0)?.0) + XCTAssertEqual(expected, ref.component(0)) } } func testHandlesSubcomponents() { - let tests: [(String, Int, Int, String, Int?)] = [ - ("/a/b", 2, 0, "a", nil), - ("/a/b", 2, 1, "b", nil), - ("/a~1b/c", 2, 0, "a/b", nil), - ("/a~1b/c", 2, 1, "c", nil), - ("/a/10/20/30x", 4, 1, "10", 10), - ("/a/10/20/30x", 4, 2, "20", 20), - ("/a/10/20/30x", 4, 3, "30x", nil) + let tests: [(String, Int, Int, String)] = [ + ("/a/b", 2, 0, "a"), + ("/a/b", 2, 1, "b"), + ("/a~1b/c", 2, 0, "a/b"), + ("/a~1b/c", 2, 1, "c"), + ("/a/10/20/30x", 4, 1, "10"), + ("/a/10/20/30x", 4, 2, "20"), + ("/a/10/20/30x", 4, 3, "30x") ] - for (input, expectedLength, index, expectedName, expectedValue) in tests { + for (input, expectedLength, index, expectedName) in tests { let reference = Reference(input) XCTAssertEqual(expectedLength, reference.depth()) - XCTAssertEqual(expectedName, reference.component(index)?.0) - XCTAssertEqual(expectedValue, reference.component(index)?.1) + XCTAssertEqual(expectedName, reference.component(index)) } } From b8257d8e52e81e4ab853d05bb74092d673b1d32b Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 12 Oct 2022 12:25:56 -0400 Subject: [PATCH 73/90] Raise minimum OS versions (#222) With the [release of Xcode 14][released], Apple dropped support for a few different OS versions. This commit also drops support for those versions in what will become the v7.0 release for this package. This commit also updates our dependency on the LDSwiftEventSource package. Those OS versions were bumped in https://github.com/launchdarkly/swift-eventsource/pull/51 [released]: https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes --- .circleci/config.yml | 6 +++- ContractTests/Package.swift | 6 ++-- LaunchDarkly.podspec | 10 +++---- LaunchDarkly.xcodeproj/project.pbxproj | 30 +++++++++++-------- .../xcschemes/LaunchDarkly_iOS.xcscheme | 2 +- .../ServiceObjects/FlagStore.swift | 2 +- Package.swift | 10 +++---- README.md | 8 ++--- 8 files changed, 42 insertions(+), 32 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 38e829e0..66e49cbd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 jobs: contract-tests: macos: - xcode: '13.4.1' + xcode: '14.0.1' steps: - checkout @@ -151,6 +151,10 @@ workflows: build: jobs: + - build: + name: Xcode 14.0 - Swift 5.7 + xcode-version: '14.0.1' + ios-sim: 'platform=iOS Simulator,name=iPhone 14,OS=16.0' - build: name: Xcode 13.3 - Swift 5.6 xcode-version: '13.3.1' diff --git a/ContractTests/Package.swift b/ContractTests/Package.swift index 324b05e4..658a3500 100644 --- a/ContractTests/Package.swift +++ b/ContractTests/Package.swift @@ -5,10 +5,10 @@ import PackageDescription let package = Package( name: "ContractTests", platforms: [ - .iOS(.v10), + .iOS(.v11), .macOS(.v10_15), - .watchOS(.v3), - .tvOS(.v10) + .watchOS(.v4), + .tvOS(.v11) ], products: [ .executable( diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 77419b2a..78219243 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -21,10 +21,10 @@ Pod::Spec.new do |ld| ld.author = { "LaunchDarkly" => "sdks@launchdarkly.com" } - ld.ios.deployment_target = "10.0" - ld.watchos.deployment_target = "3.0" - ld.tvos.deployment_target = "10.0" - ld.osx.deployment_target = "10.12" + ld.ios.deployment_target = "11.0" + ld.watchos.deployment_target = "4.0" + ld.tvos.deployment_target = "11.0" + ld.osx.deployment_target = "10.13" ld.source = { :git => ld.homepage + '.git', :tag => ld.version} @@ -35,6 +35,6 @@ Pod::Spec.new do |ld| ld.swift_version = '5.0' ld.subspec 'Core' do |es| - es.dependency 'LDSwiftEventSource', '2.0.0' + es.dependency 'LDSwiftEventSource', '3.0.0' end end diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index d131df94..53b6f4af 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -848,7 +848,7 @@ isa = PBXNativeTarget; buildConfigurationList = 8354EFD61F22491C00C05156 /* Build configuration list for PBXNativeTarget "LaunchDarkly_iOS" */; buildPhases = ( - 835E1CFE1F61AC0600184DB4 /* ShellScript */, + 835E1CFE1F61AC0600184DB4 /* Run Script */, 8354EFBD1F22491C00C05156 /* Sources */, 8354EFBE1F22491C00C05156 /* Frameworks */, 8354EFBF1F22491C00C05156 /* Headers */, @@ -1023,6 +1023,7 @@ /* Begin PBXShellScriptBuildPhase section */ 830C2AC120741687001D645D /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -1036,6 +1037,7 @@ }; 830C2AC2207416A5001D645D /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -1049,6 +1051,7 @@ }; 833FD9F821C01333001F80EB /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -1066,6 +1069,7 @@ }; 83411A561FABCA2200E5CF39 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -1077,13 +1081,15 @@ shellPath = /bin/sh; 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 */ = { + 835E1CFE1F61AC0600184DB4 /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -1527,8 +1533,8 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "$(PROJECT_DIR)/Framework/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - MACOSX_DEPLOYMENT_TARGET = 10.12; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -1536,10 +1542,10 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 10.0; + TVOS_DEPLOYMENT_TARGET = 11.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 3.0; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; @@ -1592,19 +1598,19 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "$(PROJECT_DIR)/Framework/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - MACOSX_DEPLOYMENT_TARGET = 10.12; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 10.0; + TVOS_DEPLOYMENT_TARGET = 11.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 3.0; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Release; }; @@ -1776,7 +1782,7 @@ repositoryURL = "https://github.com/LaunchDarkly/swift-eventsource.git"; requirement = { kind = exactVersion; - version = 2.0.0; + version = 3.0.0; }; }; B4903D9624BD61B200F087C4 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { diff --git a/LaunchDarkly.xcodeproj/xcshareddata/xcschemes/LaunchDarkly_iOS.xcscheme b/LaunchDarkly.xcodeproj/xcshareddata/xcschemes/LaunchDarkly_iOS.xcscheme index 68502378..230f5d25 100644 --- a/LaunchDarkly.xcodeproj/xcshareddata/xcschemes/LaunchDarkly_iOS.xcscheme +++ b/LaunchDarkly.xcodeproj/xcshareddata/xcschemes/LaunchDarkly_iOS.xcscheme @@ -3,7 +3,7 @@ LastUpgradeVersion = "1020" version = "1.7"> Date: Fri, 28 Oct 2022 16:49:13 -0400 Subject: [PATCH 74/90] Add Objective C bindings for application info (#223) --- LaunchDarkly.xcodeproj/project.pbxproj | 10 +++++ .../LaunchDarkly/Models/LDConfig.swift | 2 +- .../ObjectiveC/ObjcLDApplicationInfo.swift | 43 +++++++++++++++++++ .../ObjectiveC/ObjcLDConfig.swift | 5 +++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDApplicationInfo.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 005255a9..ba1d54c4 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -201,6 +201,10 @@ A322B4CF28BE4DA800A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4CE28BE4DA800A212ED /* LDSwiftEventSource */; }; A322B4D128BE4DB200A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4D028BE4DB200A212ED /* LDSwiftEventSource */; }; A322B4D328BE4DBA00A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4D228BE4DBA00A212ED /* LDSwiftEventSource */; }; + A3799D4529033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */; }; + A3799D4629033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */; }; + A3799D4729033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */; }; + A3799D4829033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */; }; B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; }; B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.swift */; }; B468E71024B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */; }; @@ -393,6 +397,7 @@ 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 = ""; }; + A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDApplicationInfo.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 = ""; }; B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = ""; }; @@ -616,6 +621,7 @@ 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */, B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */, 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */, + A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */, ); path = ObjectiveC; sourceTree = ""; @@ -1148,6 +1154,7 @@ 830DB3B12239B54900D65D25 /* URLResponse.swift in Sources */, 831188512113ADF400D77CB5 /* ClientServiceFactory.swift in Sources */, 831188442113ADC200D77CB5 /* LDConfig.swift in Sources */, + A3799D4829033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */, 83906A7721190B1900D7D3C5 /* FlagRequestTracker.swift in Sources */, B495A8A52787762C0051977C /* LDClientVariation.swift in Sources */, 831188622113AE3A00D77CB5 /* Data.swift in Sources */, @@ -1174,6 +1181,7 @@ 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */, 831EF34D20655E730001C643 /* FlagsUnchangedObserver.swift in Sources */, 831EF34E20655E730001C643 /* Event.swift in Sources */, + A3799D4729033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */, C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831EF35020655E730001C643 /* ClientServiceFactory.swift in Sources */, 831EF35120655E730001C643 /* KeyedValueCache.swift in Sources */, @@ -1259,6 +1267,7 @@ B4C9D42E2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 83B8C2471FE4071F0082B8A9 /* HTTPURLResponse.swift in Sources */, 830DB3AE2239B54900D65D25 /* URLResponse.swift in Sources */, + A3799D4529033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */, 83DDBEF61FA24A7E00E428B6 /* Data.swift in Sources */, B495A8A22787762C0051977C /* LDClientVariation.swift in Sources */, 838F96781FBA504A009CFC45 /* ClientServiceFactory.swift in Sources */, @@ -1357,6 +1366,7 @@ B4C9D42F2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 83D9EC992062DEAB004D7FA6 /* ObjcLDConfig.swift in Sources */, 830DB3AF2239B54900D65D25 /* URLResponse.swift in Sources */, + A3799D4629033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */, 83D9EC9A2062DEAB004D7FA6 /* ObjcLDUser.swift in Sources */, B495A8A32787762C0051977C /* LDClientVariation.swift in Sources */, 83D9EC9C2062DEAB004D7FA6 /* ObjcLDChangedFlag.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 16091c9d..2160e84a 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -22,7 +22,7 @@ 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 + or on every poll request. This function should return a copy of the headers received with any modifications or additions needed. Removing headers is discouraged as it may cause requests to fail. diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDApplicationInfo.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDApplicationInfo.swift new file mode 100644 index 00000000..da71a69b --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDApplicationInfo.swift @@ -0,0 +1,43 @@ +import Foundation + +/** + Use LDApplicationInfo to define application metadata. + + These properties are optional and informational. They may be used in LaunchDarkly analytics or other product features, + but they do not affect feature flag evaluations. + */ +@objc(LDApplicationInfo) +public final class ObjcLDApplicationInfo: NSObject { + internal var applicationInfo: ApplicationInfo + + @objc override public init() { + applicationInfo = ApplicationInfo() + } + + internal init(_ applicationInfo: ApplicationInfo?) { + if let appInfo = applicationInfo { + self.applicationInfo = appInfo + } else { + self.applicationInfo = ApplicationInfo() + } + } + + /// A unique identifier representing the application where the LaunchDarkly SDK is running. + /// + /// This can be specified as any string value as long as it only uses the following characters: + /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other + /// characters will be ignored. + @objc public func applicationIdentifier(_ applicationId: String) { + applicationInfo.applicationIdentifier(applicationId) + } + + /// A unique identifier representing the version of the application where the LaunchDarkly SDK + /// is running. + /// + /// This can be specified as any string value as long as it only uses the following characters: + /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other + /// characters will be ignored. + @objc public func applicationVersion(_ applicationVersion: String) { + applicationInfo.applicationVersion(applicationVersion) + } +} diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index db8038b0..4b330d81 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -57,6 +57,11 @@ public final class ObjcLDConfig: NSObject { get { config.backgroundFlagPollingInterval } set { config.backgroundFlagPollingInterval = newValue } } + /// The application info meta data. + @objc public var applicationInfo: ObjcLDApplicationInfo { + get { ObjcLDApplicationInfo(config.applicationInfo) } + set { config.applicationInfo = newValue.applicationInfo } + } /// The minimum interval between feature flag requests. Used only for polling mode. (5 minutes) @objc public var minFlagPollingInterval: TimeInterval { From 1f4922f1a01e1ac74b5f5119cb6ef3831551a202 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 2 Nov 2022 13:50:05 -0400 Subject: [PATCH 75/90] Remove support for secondary attribute (#224) As decided in the [spec], we are removing the special behavior of the secondary attribute. Going forward, secondary will be treated like any other attribute, and will no longer be included when determining the bucket for a context. [spec]: https://launchdarkly.atlassian.net/wiki/spaces/ENG/pages/2165212563/Consistent+and+Transparent+Rollout+Behavior+Unifying+Percent+Rollout+and+Traffic+Allocation --- .../Source/Controllers/SdkController.swift | 4 -- ContractTests/Source/Models/command.swift | 3 +- .../Models/Context/LDContext.swift | 57 ++++--------------- .../ObjectiveC/ObjcLDContext.swift | 1 - .../Mocks/LDContextStub.swift | 2 - .../Models/Context/LDContextCodableSpec.swift | 14 ++++- .../Models/Context/LDContextSpec.swift | 3 - 7 files changed, 24 insertions(+), 60 deletions(-) diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index 9acf8105..48d96b95 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -229,10 +229,6 @@ final class SdkController: RouteCollection { contextBuilder.anonymous(anonymous) } - if let secondary = params.secondary { - contextBuilder.secondary(secondary) - } - if let privateAttributes = params.privateAttribute { privateAttributes.forEach { contextBuilder.addPrivateAttribute(Reference($0)) } } diff --git a/ContractTests/Source/Models/command.swift b/ContractTests/Source/Models/command.swift index 448ae7af..b19d5e6a 100644 --- a/ContractTests/Source/Models/command.swift +++ b/ContractTests/Source/Models/command.swift @@ -83,12 +83,11 @@ struct SingleContextParameters: Content, Decodable { var key: String var name: String? var anonymous: Bool? - var secondary: String? var privateAttribute: [String]? var custom: [String:LDValue]? private enum CodingKeys: String, CodingKey { - case kind, key, name, anonymous, secondary, privateAttribute = "private", custom + case kind, key, name, anonymous, privateAttribute = "private", custom } } diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 68cdcf04..d18a60ec 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -26,7 +26,6 @@ public struct LDContext: Encodable, Equatable { // Meta attributes fileprivate var name: String? fileprivate var anonymous: Bool = false - fileprivate var secondary: String? internal var privateAttributes: [Reference] = [] fileprivate var key: String? @@ -42,22 +41,19 @@ public struct LDContext: Encodable, Equatable { } struct Meta: Codable { - var secondary: String? var privateAttributes: [Reference]? var redactedAttributes: [String]? enum CodingKeys: CodingKey { - case secondary, privateAttributes, redactedAttributes + case privateAttributes, redactedAttributes } var isEmpty: Bool { - secondary == nil - && (privateAttributes?.isEmpty ?? true) + (privateAttributes?.isEmpty ?? true) && (redactedAttributes?.isEmpty ?? true) } - init(secondary: String?, privateAttributes: [Reference]?, redactedAttributes: [String]?) { - self.secondary = secondary + init(privateAttributes: [Reference]?, redactedAttributes: [String]?) { self.privateAttributes = privateAttributes self.redactedAttributes = redactedAttributes } @@ -65,17 +61,14 @@ public struct LDContext: Encodable, Equatable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let secondary = try container.decodeIfPresent(String.self, forKey: .secondary) let privateAttributes = try container.decodeIfPresent([Reference].self, forKey: .privateAttributes) - self.secondary = secondary self.privateAttributes = privateAttributes self.redactedAttributes = [] } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(secondary, forKey: .secondary) if let privateAttributes = privateAttributes, !privateAttributes.isEmpty { try container.encodeIfPresent(privateAttributes, forKey: .privateAttributes) @@ -125,7 +118,7 @@ public struct LDContext: Encodable, Equatable { } } - let meta = Meta(secondary: context.secondary, privateAttributes: context.privateAttributes, redactedAttributes: redactedAttributes) + let meta = Meta(privateAttributes: context.privateAttributes, redactedAttributes: redactedAttributes) if !meta.isEmpty { try container.encodeIfPresent(meta, forKey: DynamicCodingKeys(string: "_meta")) @@ -441,6 +434,9 @@ extension LDContext: Decodable { var contextBuilder = LDContextBuilder(key: key) contextBuilder.allowEmptyKey = true + let custom = try values.decodeIfPresent([String: LDValue].self, forKey: .custom) ?? [:] + custom.forEach { contextBuilder.trySetValue($0.key, $0.value) } + if let name = try values.decodeIfPresent(String.self, forKey: .name) { contextBuilder.name(name) } @@ -450,6 +446,9 @@ extension LDContext: Decodable { if let lastName = try values.decodeIfPresent(String.self, forKey: .lastName) { contextBuilder.trySetValue("lastName", .string(lastName)) } + if let secondary = try values.decodeIfPresent(String.self, forKey: .secondary) { + contextBuilder.trySetValue("secondary", .string(secondary)) + } if let country = try values.decodeIfPresent(String.self, forKey: .country) { contextBuilder.trySetValue("country", .string(country)) } @@ -463,19 +462,12 @@ extension LDContext: Decodable { contextBuilder.trySetValue("avatar", .string(avatar)) } - let custom = try values.decodeIfPresent([String: LDValue].self, forKey: .custom) ?? [:] - custom.forEach { contextBuilder.trySetValue($0.key, $0.value) } - let isAnonymous = try values.decodeIfPresent(Bool.self, forKey: .isAnonymous) ?? false contextBuilder.anonymous(isAnonymous) let privateAttributeNames = try values.decodeIfPresent([String].self, forKey: .privateAttributeNames) ?? [] privateAttributeNames.forEach { contextBuilder.addPrivateAttribute(Reference($0)) } - if let secondary = try values.decodeIfPresent(String.self, forKey: .secondary) { - contextBuilder.secondary(secondary) - } - self = try contextBuilder.build().get() case .some("multi"): let container = try decoder.container(keyedBy: DynamicCodingKeys.self) @@ -517,10 +509,6 @@ extension LDContext: Decodable { continue case "_meta": if let meta = try container.decodeIfPresent(LDContext.Meta.self, forKey: DynamicCodingKeys(string: "_meta")) { - if let secondary = meta.secondary { - contextBuilder.secondary(secondary) - } - if let privateAttributes = meta.privateAttributes { privateAttributes.forEach { contextBuilder.addPrivateAttribute($0) } } @@ -584,7 +572,6 @@ public struct LDContextBuilder { // Meta attributes private var name: String? private var anonymous: Bool = false - private var secondary: String? private var privateAttributes: [Reference] = [] private var key: LDContextBuilderKey @@ -662,8 +649,6 @@ public struct LDContextBuilder { /// /// - "name": Must be a string or null. See `LDContextBuilder.name(_:)`. /// - /// - "secondary": Must be a string. See `LDContextBuilder.secondary(_:)`. - /// /// - "anonymous": Must be a boolean. See `LDContextBuilder.anonymous(_:)`. /// /// Values that are JSON arrays or objects have special behavior when referenced in @@ -699,10 +684,6 @@ public struct LDContextBuilder { self.anonymous(val) case ("anonymous", _): return false - case ("secondary", .string(let val)): - self.secondary(val) - case ("secondary", _): - return false case (_, .null): self.attributes.removeValue(forKey: name) return false @@ -714,20 +695,6 @@ public struct LDContextBuilder { return true } - /// Sets a secondary key for the LDContext. - /// - /// This affects feature flag targeting - /// - /// as follows: if you have chosen to bucket users by a specific attribute, the secondary key - /// (if set) is used to further distinguish between users who are otherwise identical according - /// to that attribute. This value is not addressable as an attribute in evaluations: that is, a - /// rule clause cannot use the attribute name "secondary". - /// - /// Setting this value to an empty string is not the same as leaving it unset. - public mutating func secondary(_ secondary: String) { - self.secondary = secondary - } - /// Sets whether the LDContext is only intended for flag evaluations and should not be indexed by /// LaunchDarkly. /// @@ -756,8 +723,7 @@ public struct LDContextBuilder { /// This action only affects analytics events that involve this particular LDContext. To mark some (or all) /// LDContext attributes as private for all uses, use the overall event configuration for the SDK. /// - /// The attributes "kind" and "key", and the metadata properties set by secondary and anonymous, - /// cannot be made private. + /// The attributes "kind" and "key", and the metadata properties set by anonymous, cannot be made private. public mutating func addPrivateAttribute(_ reference: Reference) { self.privateAttributes.append(reference) } @@ -803,7 +769,6 @@ public struct LDContextBuilder { context.contexts = [] context.name = self.name context.anonymous = anonymous - context.secondary = self.secondary context.privateAttributes = self.privateAttributes context.key = contextKey context.attributes = self.attributes diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift index 513861a6..c81838f2 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift @@ -40,7 +40,6 @@ public final class ObjcLDContextBuilder: NSObject { @objc public func kind(kind: String) { builder.kind(kind) } @objc public func key(key: String) { builder.key(key) } @objc public func name(name: String) { builder.name(name) } - @objc public func secondary(secondary: String) { builder.secondary(secondary) } @objc public func anonymous(anonymous: Bool) { builder.anonymous(anonymous) } @objc public func addPrivateAttribute(reference: ObjcLDReference) { builder.addPrivateAttribute(reference.reference) } @objc public func removePrivateAttribute(reference: ObjcLDReference) { builder.removePrivateAttribute(reference.reference) } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift index ec090a2d..97ebb090 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift @@ -6,7 +6,6 @@ extension LDContext { static let key: LDValue = "stub.context.key" static let name = "stub.context.name" - static let secondary = "stub.context.secondary" static let isAnonymous = false static let firstName: LDValue = "stub.context.firstName" @@ -28,7 +27,6 @@ extension LDContext { var builder = LDContextBuilder(key: key ?? UUID().uuidString) builder.name(StubConstants.name) - builder.secondary(StubConstants.secondary) builder.anonymous(StubConstants.isAnonymous) builder.trySetValue("firstName", StubConstants.firstName) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift index 2b1dd43f..d2f2c71c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift @@ -10,7 +10,7 @@ final class LDContextCodableSpec: XCTestCase { ("{\"key\" : \"foo\", \"name\" : \"bar\"}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"name\" : \"bar\"}"), ("{\"key\" : \"foo\", \"custom\" : {\"a\" : \"b\"}}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"a\" : \"b\"}"), ("{\"key\" : \"foo\", \"anonymous\" : true}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"anonymous\" : true}"), - ("{\"key\" : \"foo\", \"secondary\" : \"bar\"}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"_meta\" : {\"secondary\" : \"bar\"}}"), + ("{\"key\" : \"foo\", \"secondary\" : \"bar\"}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"secondary\" : \"bar\"}"), ("{\"key\" : \"foo\", \"ip\" : \"1\", \"privateAttributeNames\" : [\"ip\"]}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"ip\" : \"1\", \"_meta\" : { \"privateAttributes\" : [\"ip\"]} }") ] @@ -22,12 +22,22 @@ final class LDContextCodableSpec: XCTestCase { } } + func testUserCustomAttributesAreOverriddenByOldBuiltIns() throws { + let userJson = "{\"key\" : \"foo\", \"anonymous\" : true, \"secondary\": \"my secondary\", \"custom\": {\"anonymous\": false, \"secondary\": \"custom secondary\"}}" + let explicitFormat = "{\"kind\" : \"user\", \"key\" : \"foo\", \"anonymous\" : true, \"secondary\": \"my secondary\"}" + + let userContext = try JSONDecoder().decode(LDContext.self, from: Data(userJson.utf8)) + let explicitContext = try JSONDecoder().decode(LDContext.self, from: Data(explicitFormat.utf8)) + + XCTAssertEqual(userContext, explicitContext) + } + func testSingleContextKindsAreDecodedAndEncodedWithoutLossOfInformation() throws { let testCases = [ "{\"kind\":\"org\",\"key\":\"foo\"}", "{\"kind\":\"user\",\"key\":\"foo\"}", "{\"kind\":\"foo\",\"key\":\"bar\",\"anonymous\":true}", - "{\"kind\":\"foo\",\"key\":\"bar\",\"name\":\"Foo\",\"_meta\":{\"privateAttributes\":[\"a\"],\"secondary\":\"baz\"}}", + "{\"kind\":\"foo\",\"key\":\"bar\",\"name\":\"Foo\",\"_meta\":{\"privateAttributes\":[\"a\"]}}", "{\"kind\":\"foo\",\"key\":\"bar\",\"object\":{\"a\":\"b\"}}" ] diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift index ca34cf6c..80bda5e9 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift @@ -7,7 +7,6 @@ final class LDContextSpec: XCTestCase { func testBuildCanCreateSimpleContext() throws { var builder = LDContextBuilder(key: "context-key") builder.name("Name") - builder.secondary("Secondary") let context = try builder.build().get() XCTAssertFalse(context.isMulti()) @@ -231,7 +230,6 @@ final class LDContextSpec: XCTestCase { ("/a//b", nil), // Hidden meta attributes ("privateAttributes", nil), - ("secondary", nil), // Can index arrays and objects ("/my-map/array", .array([.string("first"), .string("second")])), ("/my-map/1", .bool(true)), @@ -247,7 +245,6 @@ final class LDContextSpec: XCTestCase { builder.kind("org") builder.name("my-name") builder.anonymous(true) - builder.secondary("my-secondary") builder.trySetValue("attr", .string("my-attr")) builder.trySetValue("starts-with-slash", .string("love that prefix")) builder.trySetValue("crazy~name", .string("still works")) From 094f14179f6f699d143184040fa1d3838a96f8f5 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Sun, 20 Nov 2022 17:44:27 -0500 Subject: [PATCH 76/90] Add support for legacy LDUser (#225) To help ease the upgrade path, we are going to allow consumers of the SDK to continue providing LDUser objects to key methods in the LDClient instead of the newer LDContext type. --- LaunchDarkly.xcodeproj/project.pbxproj | 50 ++++ LaunchDarkly/LaunchDarkly/LDClient.swift | 46 ++++ .../Models/Context/LDContext.swift | 2 - .../Models/Context/Reference.swift | 19 ++ LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 255 ++++++++++++++++++ .../LaunchDarkly/Models/UserAttribute.swift | 81 ++++++ .../ObjectiveC/ObjcLDClient.swift | 36 +++ .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 138 ++++++++++ .../LaunchDarklyTests/Mocks/LDUserStub.swift | 51 ++++ .../Models/Context/LDContextCodableSpec.swift | 3 +- .../Models/User/LDUserSpec.swift | 119 ++++++++ .../Models/User/LDUserToContextSpec.swift | 66 +++++ 12 files changed, 863 insertions(+), 3 deletions(-) create mode 100644 LaunchDarkly/LaunchDarkly/Models/LDUser.swift create mode 100644 LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift create mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 80cfb2bd..3356eaac 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -203,6 +203,21 @@ A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088252837DCA900184942 /* ReferenceSpec.swift */; }; A31088292837DCA900184942 /* KindSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088262837DCA900184942 /* KindSpec.swift */; }; A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33A5F7928466D04000C29C7 /* LDContextStub.swift */; }; + A349D0332926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; + A349D0342926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; + A349D0352926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; + A349D0362926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; + A349D0372926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; + A349D0382926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; + A349D0392926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; + A349D03A2926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; + A349D03C2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; + A349D03D2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; + A349D03E2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; + A349D03F2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; + A355845529281CD70023D8EE /* LDUserSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D06B2926CB2600DD5DE9 /* LDUserSpec.swift */; }; + A355845729281CF00023D8EE /* LDUserStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A355845629281CF00023D8EE /* LDUserStub.swift */; }; + A355845929281E610023D8EE /* LDUserToContextSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A355845829281E610023D8EE /* LDUserToContextSpec.swift */; }; A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */; }; A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; A36EDFC92853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; @@ -410,6 +425,12 @@ A31088252837DCA900184942 /* ReferenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceSpec.swift; sourceTree = ""; }; A31088262837DCA900184942 /* KindSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KindSpec.swift; sourceTree = ""; }; A33A5F7928466D04000C29C7 /* LDContextStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextStub.swift; sourceTree = ""; }; + A349D0312926CA0600DD5DE9 /* UserAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAttribute.swift; sourceTree = ""; }; + A349D0322926CA0600DD5DE9 /* LDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUser.swift; sourceTree = ""; }; + A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDUser.swift; sourceTree = ""; }; + A349D06B2926CB2600DD5DE9 /* LDUserSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUserSpec.swift; sourceTree = ""; }; + A355845629281CF00023D8EE /* LDUserStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUserStub.swift; sourceTree = ""; }; + A355845829281E610023D8EE /* LDUserToContextSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUserToContextSpec.swift; sourceTree = ""; }; A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextCodableSpec.swift; sourceTree = ""; }; A36EDFC72853883400D91B05 /* ObjcLDReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDReference.swift; sourceTree = ""; }; A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDContext.swift; sourceTree = ""; }; @@ -617,6 +638,8 @@ 8354EFE61F263E4200C05156 /* Models */ = { isa = PBXGroup; children = ( + A349D0322926CA0600DD5DE9 /* LDUser.swift */, + A349D0312926CA0600DD5DE9 /* UserAttribute.swift */, A31088132837DC0400184942 /* Context */, C408884823033B7500420721 /* ConnectionInformation.swift */, B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */, @@ -630,6 +653,7 @@ 835E1D341F63332C00184DB4 /* ObjectiveC */ = { isa = PBXGroup; children = ( + A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */, A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */, 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */, 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */, @@ -654,6 +678,7 @@ 83CFE7CF1F7AD89D0010544E /* Mocks */ = { isa = PBXGroup; children = ( + A355845629281CF00023D8EE /* LDUserStub.swift */, 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */, 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */, 832307A91F7ECA630029815A /* LDConfigStub.swift */, @@ -736,6 +761,7 @@ 83EF67911F9945CE00403126 /* Models */ = { isa = PBXGroup; children = ( + A349D06A2926CB1100DD5DE9 /* User */, A31088232837DCA900184942 /* Context */, 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */, 83EBCBA720D9A251003A7142 /* FeatureFlag */, @@ -786,6 +812,15 @@ path = Context; sourceTree = ""; }; + A349D06A2926CB1100DD5DE9 /* User */ = { + isa = PBXGroup; + children = ( + A355845829281E610023D8EE /* LDUserToContextSpec.swift */, + A349D06B2926CB2600DD5DE9 /* LDUserSpec.swift */, + ); + path = User; + sourceTree = ""; + }; B467790E24D8AECA00897F00 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1146,7 +1181,9 @@ 8311885F2113AE2D00D77CB5 /* HTTPURLRequest.swift in Sources */, A36EDFD02853C50B00D91B05 /* ObjcLDContext.swift in Sources */, B4C9D4362489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, + A349D03A2926CA0600DD5DE9 /* LDUser.swift in Sources */, 831188452113ADC500D77CB5 /* LDClient.swift in Sources */, + A349D03F2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, A310881E2837DC0400184942 /* Kind.swift in Sources */, A310881A2837DC0400184942 /* Reference.swift in Sources */, 831188522113ADF700D77CB5 /* KeyedValueCache.swift in Sources */, @@ -1179,6 +1216,7 @@ 8311885B2113AE1D00D77CB5 /* Throttler.swift in Sources */, 8311884E2113ADE500D77CB5 /* Event.swift in Sources */, A36EDFCB2853883400D91B05 /* ObjcLDReference.swift in Sources */, + A349D0362926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */, 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */, B4C9D4312489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, @@ -1216,17 +1254,20 @@ 831EF34D20655E730001C643 /* FlagsUnchangedObserver.swift in Sources */, A31088192837DC0400184942 /* Reference.swift in Sources */, 831EF34E20655E730001C643 /* Event.swift in Sources */, + A349D03E2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, A3799D4729033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */, C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831EF35020655E730001C643 /* ClientServiceFactory.swift in Sources */, 831EF35120655E730001C643 /* KeyedValueCache.swift in Sources */, 831AAE2E20A9E4F600B46DBA /* Throttler.swift in Sources */, 831EF35520655E730001C643 /* FlagSynchronizer.swift in Sources */, + A349D0392926CA0600DD5DE9 /* LDUser.swift in Sources */, B4C9D4302489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 831EF35620655E730001C643 /* FlagChangeNotifier.swift in Sources */, 831EF35720655E730001C643 /* EventReporter.swift in Sources */, 831EF35820655E730001C643 /* FlagStore.swift in Sources */, 831EF35920655E730001C643 /* Log.swift in Sources */, + A349D0352926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 831EF35A20655E730001C643 /* HTTPHeaders.swift in Sources */, 831EF35B20655E730001C643 /* DarklyService.swift in Sources */, 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */, @@ -1263,7 +1304,9 @@ 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */, B4C9D4332489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, A36EDFCD2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, + A349D0372926CA0600DD5DE9 /* LDUser.swift in Sources */, 8354EFE51F263DAC00C05156 /* FeatureFlag.swift in Sources */, + A349D03C2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, 8372668C20D4439600BD1088 /* DateFormatter.swift in Sources */, A310881B2837DC0400184942 /* Kind.swift in Sources */, A31088172837DC0400184942 /* Reference.swift in Sources */, @@ -1296,6 +1339,7 @@ 8354AC702243166900CDE602 /* FeatureFlagCache.swift in Sources */, A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */, 8358F2621F47747F00ECE1AF /* FlagChangeObserver.swift in Sources */, + A349D0332926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */, 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */, 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */, @@ -1324,6 +1368,7 @@ 83EF67931F9945E800403126 /* EventSpec.swift in Sources */, 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */, 83B6E3F1222EFA3800FF2A6A /* ThreadSpec.swift in Sources */, + A355845729281CF00023D8EE /* LDUserStub.swift in Sources */, 831AAE3020A9E75D00B46DBA /* ThrottlerSpec.swift in Sources */, 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */, 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */, @@ -1342,6 +1387,7 @@ 83B8C2451FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift in Sources */, 8335299E1FC37727001166F8 /* FlagMaintainingMock.swift in Sources */, 83383A5120460DD30024D975 /* SynchronizingErrorSpec.swift in Sources */, + A355845929281E610023D8EE /* LDUserToContextSpec.swift in Sources */, 83B9A080204F56F4000C3F17 /* FlagChangeObserverSpec.swift in Sources */, 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, 831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */, @@ -1350,6 +1396,7 @@ 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */, 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */, 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */, + A355845529281CD70023D8EE /* LDUserSpec.swift in Sources */, 838F967A1FBA551A009CFC45 /* ClientServiceMockFactory.swift in Sources */, A31088292837DCA900184942 /* KindSpec.swift in Sources */, ); @@ -1367,7 +1414,9 @@ 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, A36EDFCE2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */, + A349D0382926CA0600DD5DE9 /* LDUser.swift in Sources */, 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */, + A349D03D2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, A310881C2837DC0400184942 /* Kind.swift in Sources */, A31088182837DC0400184942 /* Reference.swift in Sources */, 83D9EC7E2062DEAB004D7FA6 /* FlagChangeObserver.swift in Sources */, @@ -1400,6 +1449,7 @@ B4C9D4392489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, A36EDFC92853883400D91B05 /* ObjcLDReference.swift in Sources */, + A349D0342926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */, 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */, 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index e5710ff8..03ffab9b 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -286,6 +286,20 @@ public class LDClient { } } + /** + Deprecated identify method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context:completion:)` for details. + */ + public func identify(user: LDUser, completion: (() -> Void)? = nil) { + switch user.toContext() { + case .failure(let error): + Log.debug(self.typeName(and: #function) + "user created an invalid context: SDK identified context WILL NOT CHANGE: " + error.localizedDescription ) + case .success(let context): + identify(context: context, completion: completion) + } + } + func internalIdentify(newContext: LDContext, completion: (() -> Void)? = nil) { internalIdentifyQueue.sync { self.context = newContext @@ -582,6 +596,22 @@ public class LDClient { start(serviceFactory: nil, config: config, context: context, completion: completion) } + /** + Deprecated start method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(config:context:completion:)` for details. + */ + public static func start(config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) { + switch user?.toContext() { + case nil: + start(serviceFactory: nil, config: config, context: nil, completion: completion) + case .failure(let error): + Log.debug(self.typeName(and: #function) + "user created an invalid context: " + error.localizedDescription ) + case .success(let context): + start(serviceFactory: nil, config: config, context: context, completion: completion) + } + } + static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, context: LDContext? = nil, completion: (() -> Void)? = nil) { Log.debug("LDClient starting") if serviceFactory != nil { @@ -629,6 +659,22 @@ public class LDClient { start(serviceFactory: nil, config: config, context: context, startWaitSeconds: startWaitSeconds, completion: completion) } + /** + Deprecated start method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(config:context:startWaitSeconds:completion:)` for details. + */ + public static func start(config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + switch user?.toContext() { + case nil: + start(serviceFactory: nil, config: config, context: nil, startWaitSeconds: startWaitSeconds, completion: completion) + case .failure(let error): + Log.debug(self.typeName(and: #function) + "user created an invalid context: " + error.localizedDescription ) + case .success(let context): + start(serviceFactory: nil, config: config, context: context, startWaitSeconds: startWaitSeconds, completion: completion) + } + } + static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, context: LDContext? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { var completed = true let internalCompletedQueue: DispatchQueue = DispatchQueue(label: "TimeOutQueue") diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index d18a60ec..de8c7b83 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -686,10 +686,8 @@ public struct LDContextBuilder { return false case (_, .null): self.attributes.removeValue(forKey: name) - return false case (_, _): self.attributes.updateValue(value, forKey: name) - return false } return true diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift index dbf8be18..0d6ddad1 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift @@ -157,6 +157,25 @@ public struct Reference: Codable, Equatable, Hashable { components = referenceComponents } + private init() { + rawPath = "" + components = [] + error = nil + } + + public init(literal value: String) { + if value.isEmpty { + self.init(value) + return + } + + self.init() + let str = value.replacingOccurrences(of: "~", with: "~0").replacingOccurrences(of: "/", with: "~1") + self.rawPath = str + self.components = [value] + self.error = nil + } + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let reference = try container.decode(String.self) diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift new file mode 100644 index 00000000..96fe405e --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -0,0 +1,255 @@ +import Foundation + +/** + 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 { + + static let optionalAttributes = UserAttribute.BuiltIn.allBuiltIns.filter { $0.name != "key" && $0.name != "anonymous"} + + 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. + public var key: String + /// The secondary key for the user. Read the [documentation](https://docs.launchdarkly.com/home/flags/rollouts) for more information on it's use for percentage rollout bucketing. + public var secondary: String? + /// Client app defined name for the user. (Default: nil) + public var name: String? + /// Client app defined first name for the user. (Default: nil) + public var firstName: String? + /// Client app defined last name for the user. (Default: nil) + public var lastName: String? + /// Client app defined country for the user. (Default: nil) + public var country: String? + /// Client app defined ipAddress for the user. (Default: nil) + public var ipAddress: String? + /// Client app defined email address for the user. (Default: nil) + 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, 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 { + get { isAnonymousNullable == true } + set { isAnonymousNullable = newValue } + } + + /** + Whether or not the user is anonymous, if that has been specified (or set due to the lack of a `key` property). + + Although the `isAnonymous` property defaults to `false` in terms of LaunchDarkly's indexing behavior, for historical + reasons flag evaluation may behave differently if the value is explicitly set to `false` verses being omitted. This + field allows treating the property as optional for consisent evaluation with other LaunchDarkly SDKs. + */ + public var isAnonymousNullable: Bool? + + /** + 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 most built-in attributes and all top level `custom` dictionary keys here. (Default: []]) + See Also: `LDConfig.allUserAttributesPrivate` and `LDConfig.privateUserAttributes`. + */ + public var privateAttributes: [UserAttribute] + + 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. + - parameter name: Client app defined name for the user. (Default: nil) + - parameter firstName: Client app defined first name for the user. (Default: nil) + - parameter lastName: Client app defined last name for the user. (Default: nil) + - parameter country: Client app defined country for the user. (Default: nil) + - parameter ipAddress: Client app defined ipAddress for the user. (Default: nil) + - parameter email: Client app defined email address for the user. (Default: nil) + - 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 privateAttributes: Client app defined privateAttributes for the user. (Default: nil) + - parameter secondary: Secondary attribute value. (Default: nil) + */ + public init(key: String? = nil, + name: String? = nil, + firstName: String? = nil, + lastName: String? = nil, + country: String? = nil, + ipAddress: String? = nil, + email: String? = nil, + avatar: String? = nil, + custom: [String: LDValue]? = nil, + isAnonymous: Bool? = nil, + privateAttributes: [UserAttribute]? = nil, + secondary: String? = nil) { + let environmentReporter = EnvironmentReporter() + let selectedKey = key ?? LDUser.defaultKey(environmentReporter: environmentReporter) + self.key = selectedKey + self.secondary = secondary + self.name = name + self.firstName = firstName + self.lastName = lastName + self.country = country + self.ipAddress = ipAddress + self.email = email + self.avatar = avatar + self.isAnonymousNullable = isAnonymous + if isAnonymous == nil && selectedKey == LDUser.defaultKey(environmentReporter: environmentReporter) { + self.isAnonymousNullable = true + } + self.custom = custom ?? [:] + self.privateAttributes = privateAttributes ?? [] + Log.debug(typeName(and: #function) + "user: \(self)") + } + + /** + Internal initializer that accepts an environment reporter, used for testing + */ + init(environmentReporter: EnvironmentReporting) { + self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), isAnonymous: true) + } + + private func value(for attribute: UserAttribute) -> Any? { + if let builtInGetter = attribute.builtInGetter { + return builtInGetter(self) + } + return custom[attribute.name] + } + + struct UserInfoKeys { + static let includePrivateAttributes = CodingUserInfoKey(rawValue: "LD_includePrivateAttributes")! + static let allAttributesPrivate = CodingUserInfoKey(rawValue: "LD_allAttributesPrivate")! + static let globalPrivateAttributes = CodingUserInfoKey(rawValue: "LD_globalPrivateAttributes")! + } + + /** + Internal helper method to convert an LDUser to an LDContext. + + Ideally we would do this as the LDUser was being built. However, the LDUser properties are publicly accessible, which makes that approach problematic. + */ + internal func toContext() -> Result { + var contextBuilder = LDContextBuilder(key: key) + + // Custom attributes must be processed first in case built-in attributes + // need to override those values + custom.forEach { (key, value) in + contextBuilder.trySetValue(key, value) + } + + if let name = name { + contextBuilder.name(name) + } + + contextBuilder.anonymous(isAnonymous) + + if let firstName = firstName { + contextBuilder.trySetValue("firstName", firstName.toLDValue()) + } + if let lastName = lastName { + contextBuilder.trySetValue("lastName", lastName.toLDValue()) + } + if let country = country { + contextBuilder.trySetValue("country", country.toLDValue()) + } + if let ipAddress = ipAddress { + contextBuilder.trySetValue("ipAddress", ipAddress.toLDValue()) + } + if let email = email { + contextBuilder.trySetValue("email", email.toLDValue()) + } + if let avatar = avatar { + contextBuilder.trySetValue("avatar", avatar.toLDValue()) + } + if let secondary = secondary { + contextBuilder.trySetValue("secondary", secondary.toLDValue()) + } + + privateAttributes.forEach { privateAttribute in + contextBuilder.addPrivateAttribute(Reference(literal: privateAttribute.name)) + } + + return contextBuilder.build() + } + + 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")!) + + if let anonymous = isAnonymousNullable { + try container.encode(anonymous, 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 { + // 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 + } + if let storedId = UserDefaults.standard.string(forKey: storedIdKey) { + return storedId + } + let key = UUID().uuidString + UserDefaults.standard.set(key, forKey: storedIdKey) + return key + } +} + +/// Class providing ObjC interoperability with the LDUser struct +@objc final class LDUserWrapper: NSObject { + let wrapped: LDUser + + init(user: LDUser) { + wrapped = user + super.init() + } +} + +extension LDUser: TypeIdentifying { } diff --git a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift new file mode 100644 index 00000000..069b45bc --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift @@ -0,0 +1,81 @@ +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 } + /// 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] + } + + 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 + } + return UserAttribute(name) + } + + let name: String + let builtInGetter: ((LDUser) -> Any?)? + + init(_ name: String, builtInGetter: ((LDUser) -> Any?)? = nil) { + self.name = name + 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 { + 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/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index 9552bcf0..9efc6dc8 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -114,6 +114,15 @@ public final class ObjcLDClient: NSObject { ldClient.identify(context: context.context, completion: nil) } + /** + Deprecated identify method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context)` for details. + */ + @objc public func identify(user: ObjcLDUser) { + ldClient.identify(user: user.user, completion: nil) + } + /** The LDContext set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the context. See `LDContext` for details about what information can be retained. @@ -130,6 +139,15 @@ public final class ObjcLDClient: NSObject { ldClient.identify(context: context.context, completion: completion) } + /** + Deprecated identify method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context:completion:)` for details. + */ + @objc public func identify(user: ObjcLDUser, completion: (() -> Void)? = nil) { + ldClient.identify(user: user.user, completion: completion) + } + /** Stops the LDClient. Stopping the client means the LDClient goes offline and stops recording events. LDClient will no longer provide feature flag values, only returning default values. @@ -554,6 +572,15 @@ public final class ObjcLDClient: NSObject { LDClient.start(config: configuration.config, context: context.context, completion: completion) } + /** + Deprecated start method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(configuration:context:completion:)` for details. + */ + @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, completion: (() -> Void)? = nil) { + LDClient.start(config: configuration.config, user: user.user, completion: completion) + } + /** See [start](x-source-tag://start) for more information on starting the SDK. @@ -566,6 +593,15 @@ public final class ObjcLDClient: NSObject { LDClient.start(config: configuration.config, context: context.context, startWaitSeconds: startWaitSeconds, completion: completion) } + /** + Deprecated start method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(configuration:context:startWaitSeconds:completion:)` for details. + */ + @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + LDClient.start(config: configuration.config, user: user.user, startWaitSeconds: startWaitSeconds, completion: completion) + } + private init(client: LDClient) { ldClient = client } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift new file mode 100644 index 00000000..9f968c60 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -0,0 +1,138 @@ +import Foundation + +/** + 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. + */ +@objc (LDUser) +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 + } + /// The secondary key for the user. Read the [documentation](https://docs.launchdarkly.com/home/flags/rollouts) 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) + @objc public var name: String? { + get { user.name } + set { user.name = newValue } + } + /// 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) + @objc public var lastName: String? { + get { user.lastName } + set { user.lastName = newValue } + } + /// 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) + @objc public var ipAddress: String? { + get { user.ipAddress } + set { user.ipAddress = newValue } + } + /// 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) + @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. 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 { + get { user.isAnonymous } + set { user.isAnonymous = newValue } + } + + /** + 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 `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`. + + */ + @objc public var privateAttributes: [String] { + get { user.privateAttributes.map { $0.name } } + set { user.privateAttributes = newValue.map { UserAttribute.forName($0) } } + } + + /** + Initializer to create a LDUser. Client configurable attributes are set to their default value. The SDK will automatically set `key`, `device`, `operatingSystem`, and `isAnonymous` attributes. The SDK embeds `device` and `operatingSystem` into the `custom` dictionary for transmission to LaunchDarkly. + */ + @objc override public init() { + user = LDUser() + } + + /** + Initializer to create a LDUser with a specific key. Other client configurable attributes are set to their default value. The SDK will automatically set `key`, `device`, `operatingSystem`, and `isAnonymous` attributes. 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. + */ + @objc public init(key: String) { + user = LDUser(key: key) + } + + // Initializer to wrap the Swift LDUser into ObjcLDUser for use in Objective-C apps. + init(_ user: LDUser) { + self.user = user + } + + /// 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 } + return user == otherUser.user + } + + /// Convert a legacy LDUser to the newer LDContext + @objc public func toContext() -> ContextBuilderResult { + switch self.user.toContext() { + case .success(let context): + return ContextBuilderResult.fromSuccess(context) + case .failure(let error): + return ContextBuilderResult.fromError(error) + } + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift new file mode 100644 index 00000000..943be88c --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift @@ -0,0 +1,51 @@ +import Foundation +@testable import LaunchDarkly + +extension LDUser { + struct StubConstants { + static let key = "stub.user.key" + static let secondary = "stub.user.secondary" + static let userKey = "userKey" + static let name = "stub.user.name" + static let firstName = "stub.user.firstName" + static let lastName = "stub.user.lastName" + static let isAnonymous = false + static let country = "stub.user.country" + static let ipAddress = "stub.user.ipAddress" + static let email = "stub.user@email.com" + static let avatar = "stub.user.avatar" + 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: LDValue] { + var custom = StubConstants.custom + if includeSystemValues { + custom["device"] = StubConstants.device + custom["os"] = StubConstants.operatingSystem + } + return custom + } + } + + static func stub(key: String? = nil, + environmentReporter: EnvironmentReportingMock? = nil) -> LDUser { + let user = LDUser(key: key ?? UUID().uuidString, + name: StubConstants.name, + firstName: StubConstants.firstName, + lastName: StubConstants.lastName, + country: StubConstants.country, + ipAddress: StubConstants.ipAddress, + email: StubConstants.email, + avatar: StubConstants.avatar, + custom: StubConstants.custom(includeSystemValues: true), + isAnonymous: StubConstants.isAnonymous, + secondary: StubConstants.secondary) + return user + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift index d2f2c71c..a7d03522 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift @@ -61,8 +61,9 @@ final class LDContextCodableSpec: XCTestCase { "c": "should be removed", "d": "should be retained" }, + "/complex/attribute": "should be removed", "_meta":{ - "privateAttributes":["a", "/b/c"], + "privateAttributes":["a", "/b/c", "~1complex~1attribute"], "secondary":"baz" } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift new file mode 100644 index 00000000..6e5ffcb2 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -0,0 +1,119 @@ +import Foundation +import Quick +import Nimble +@testable import LaunchDarkly + +final class LDUserSpec: QuickSpec { + + override func spec() { + initSpec() + } + + private func initSpec() { + initSubSpec() + initWithEnvironmentReporterSpec() + } + + private func initSubSpec() { + var user: LDUser! + describe("init") { + 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.optionalAttributes, + 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.isAnonymousNullable) == 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.optionalAttributes + } + it("without setting anonymous") { + user = LDUser(key: "abc") + expect(user.isAnonymous) == false + expect(user.isAnonymousNullable).to(beNil()) + } + context("called without optional elements") { + var environmentReporter: EnvironmentReporter! + beforeEach { + user = LDUser() + environmentReporter = EnvironmentReporter() + } + it("creates a LDUser without optional elements") { + expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) + expect(user.isAnonymous) == true + expect(user.isAnonymousNullable) == true + + 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.privateAttributes).to(beEmpty()) + expect(user.secondary).to(beNil()) + } + } + context("called without a key multiple times") { + var users = [LDUser]() + beforeEach { + while users.count < 3 { + users.append(LDUser()) + } + } + it("creates each LDUser with the default key and isAnonymous set") { + let environmentReporter = EnvironmentReporter() + users.forEach { user in + expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) + expect(user.isAnonymous) == true + expect(user.isAnonymousNullable) == true + } + } + } + } + } + + private func initWithEnvironmentReporterSpec() { + describe("initWithEnvironmentReporter") { + var user: LDUser! + var environmentReporter: EnvironmentReportingMock! + beforeEach { + environmentReporter = EnvironmentReportingMock() + user = LDUser(environmentReporter: environmentReporter) + } + it("creates a user with system values matching the environment reporter") { + expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) + expect(user.isAnonymous) == true + expect(user.isAnonymousNullable) == true + + 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.privateAttributes).to(beEmpty()) + } + } + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift new file mode 100644 index 00000000..76aa62e1 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift @@ -0,0 +1,66 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class LDUserToContextSpec: XCTestCase { + func testSimpleUserIsConvertedToSimpleContext() throws { + let user = LDUser(key: "user-key") + let builder = LDContextBuilder(key: "user-key") + let context = try builder.build().get() + let encoder = JSONEncoder() + let encodedContext = try encoder.encode(context) + let encodedUserContext = try encoder.encode(user.toContext().get()) + + XCTAssertEqual(encodedContext, encodedUserContext) + } + + func testComplexUserConversion() throws { + var user = LDUser(key: "user-key") + user.name = "Example user" + user.firstName = "Example" + user.lastName = "user" + user.country = "United States" + user.ipAddress = "192.168.1.1" + user.email = "example@test.com" + user.avatar = "profile.jpg" + user.custom = ["/nested/attribute": "here is a nested attribute"] + user.isAnonymous = true + user.privateAttributes = [UserAttribute("/nested/attribute")] + user.secondary = "secondary" + + var builder = LDContextBuilder(key: "user-key") + builder.name("Example user") + builder.trySetValue("firstName", "Example".toLDValue()) + builder.trySetValue("lastName", "user".toLDValue()) + builder.trySetValue("country", "United States".toLDValue()) + builder.trySetValue("ipAddress", "192.168.1.1".toLDValue()) + builder.trySetValue("email", "example@test.com".toLDValue()) + builder.trySetValue("avatar", "profile.jpg".toLDValue()) + builder.trySetValue("/nested/attribute", "here is a nested attribute".toLDValue()) + builder.anonymous(true) + builder.addPrivateAttribute(Reference(literal: "/nested/attribute")) + builder.trySetValue("secondary", "secondary".toLDValue()) + + let context = try builder.build().get() + let userContext = try user.toContext().get() + + XCTAssertEqual(context, userContext) + } + + func testUserAttributeRedactionWorksAsExpected() throws { + var user = LDUser(key: "user-key") + user.custom = [ + "a": "should be removed", + "b": "should be retained", + "/nested/attribute/path": "should be removed" + ] + user.privateAttributes = [UserAttribute("a"), UserAttribute("/nested/attribute/path")] + let context = try user.toContext().get() + let output = try JSONEncoder().encode(context) + let outputJson = String(data: output, encoding: .utf8) + + XCTAssertTrue(outputJson!.contains("should be retained")) + XCTAssertFalse(outputJson!.contains("should be removed")) + } +} From c3f098e1f895109468875b99309db29aaab26183 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 5 Dec 2022 10:29:20 -0800 Subject: [PATCH 77/90] Rlamb/sc 178317/remove seconary non nullable anonymous (#227) --- .../Models/Context/LDContext.swift | 5 +-- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 36 +++++-------------- .../LaunchDarkly/Models/UserAttribute.swift | 4 +-- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 7 ---- .../LaunchDarklyTests/Mocks/LDUserStub.swift | 4 +-- .../Models/Context/LDContextCodableSpec.swift | 7 ++-- .../Models/User/LDUserSpec.swift | 12 +------ .../Models/User/LDUserToContextSpec.swift | 2 -- 8 files changed, 16 insertions(+), 61 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index de8c7b83..df5b8cd8 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -446,9 +446,6 @@ extension LDContext: Decodable { if let lastName = try values.decodeIfPresent(String.self, forKey: .lastName) { contextBuilder.trySetValue("lastName", .string(lastName)) } - if let secondary = try values.decodeIfPresent(String.self, forKey: .secondary) { - contextBuilder.trySetValue("secondary", .string(secondary)) - } if let country = try values.decodeIfPresent(String.self, forKey: .country) { contextBuilder.trySetValue("country", .string(country)) } @@ -546,7 +543,7 @@ extension LDContext: Decodable { } enum UserCodingKeys: String, CodingKey { - case key, name, firstName, lastName, country, ip, email, avatar, custom, isAnonymous = "anonymous", privateAttributeNames, secondary + case key, name, firstName, lastName, country, ip, email, avatar, custom, isAnonymous = "anonymous", privateAttributeNames } } diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 96fe405e..0bfb1667 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -22,8 +22,6 @@ public struct LDUser: Encodable, Equatable { /// 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. Read the [documentation](https://docs.launchdarkly.com/home/flags/rollouts) for more information on it's use for percentage rollout bucketing. - public var secondary: String? /// Client app defined name for the user. (Default: nil) public var name: String? /// Client app defined first name for the user. (Default: nil) @@ -40,20 +38,8 @@ public struct LDUser: Encodable, Equatable { public var avatar: String? /// 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 { - get { isAnonymousNullable == true } - set { isAnonymousNullable = newValue } - } - - /** - Whether or not the user is anonymous, if that has been specified (or set due to the lack of a `key` property). - - Although the `isAnonymous` property defaults to `false` in terms of LaunchDarkly's indexing behavior, for historical - reasons flag evaluation may behave differently if the value is explicitly set to `false` verses being omitted. This - field allows treating the property as optional for consisent evaluation with other LaunchDarkly SDKs. - */ - public var isAnonymousNullable: Bool? + /// 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: false) + public var isAnonymous: Bool /** Client app defined privateAttributes for the user. @@ -78,7 +64,6 @@ public struct LDUser: Encodable, Equatable { - 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 privateAttributes: Client app defined privateAttributes for the user. (Default: nil) - - parameter secondary: Secondary attribute value. (Default: nil) */ public init(key: String? = nil, name: String? = nil, @@ -90,12 +75,10 @@ public struct LDUser: Encodable, Equatable { avatar: String? = nil, custom: [String: LDValue]? = nil, isAnonymous: Bool? = nil, - privateAttributes: [UserAttribute]? = nil, - secondary: String? = nil) { + privateAttributes: [UserAttribute]? = nil) { let environmentReporter = EnvironmentReporter() let selectedKey = key ?? LDUser.defaultKey(environmentReporter: environmentReporter) self.key = selectedKey - self.secondary = secondary self.name = name self.firstName = firstName self.lastName = lastName @@ -103,9 +86,11 @@ public struct LDUser: Encodable, Equatable { self.ipAddress = ipAddress self.email = email self.avatar = avatar - self.isAnonymousNullable = isAnonymous if isAnonymous == nil && selectedKey == LDUser.defaultKey(environmentReporter: environmentReporter) { - self.isAnonymousNullable = true + self.isAnonymous = true + } else { + // If not nil, use the value, otherwise false. + self.isAnonymous = isAnonymous ?? false; } self.custom = custom ?? [:] self.privateAttributes = privateAttributes ?? [] @@ -170,9 +155,6 @@ public struct LDUser: Encodable, Equatable { if let avatar = avatar { contextBuilder.trySetValue("avatar", avatar.toLDValue()) } - if let secondary = secondary { - contextBuilder.trySetValue("secondary", secondary.toLDValue()) - } privateAttributes.forEach { privateAttribute in contextBuilder.addPrivateAttribute(Reference(literal: privateAttribute.name)) @@ -194,8 +176,8 @@ public struct LDUser: Encodable, Equatable { var container = encoder.container(keyedBy: DynamicKey.self) try container.encode(key, forKey: DynamicKey(stringValue: "key")!) - if let anonymous = isAnonymousNullable { - try container.encode(anonymous, forKey: DynamicKey(stringValue: "anonymous")!) + if isAnonymous { + try container.encode(true, forKey: DynamicKey(stringValue: "anonymous")!) } try LDUser.optionalAttributes.forEach { attribute in diff --git a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift index 069b45bc..d3095972 100644 --- a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift +++ b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift @@ -17,8 +17,6 @@ public class UserAttribute: Equatable, Hashable { 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 } /// Represents the IP address attribute. public static let ip = UserAttribute("ip") { $0.ipAddress } // swiftlint:disable:this identifier_name /// Represents the email address attribute. @@ -36,7 +34,7 @@ public class UserAttribute: Equatable, Hashable { /// 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] + static let allBuiltIns = [key, ip, email, name, avatar, firstName, lastName, country, anonymous] } static var builtInMap = { return BuiltIn.allBuiltIns.reduce(into: [:]) { $0[$1.name] = $1 } }() diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index 9f968c60..e7479ca8 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -11,8 +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 { "secondary" } /// LDUser name attribute used to make `name` private @objc public class var attributeName: String { "name" } /// LDUser firstName attribute used to make `firstName` private @@ -32,11 +30,6 @@ public final class ObjcLDUser: NSObject { @objc public var key: String { return user.key } - /// The secondary key for the user. Read the [documentation](https://docs.launchdarkly.com/home/flags/rollouts) 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) @objc public var name: String? { get { user.name } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift index 943be88c..67023d96 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift @@ -4,7 +4,6 @@ import Foundation extension LDUser { struct StubConstants { static let key = "stub.user.key" - static let secondary = "stub.user.secondary" static let userKey = "userKey" static let name = "stub.user.name" static let firstName = "stub.user.firstName" @@ -44,8 +43,7 @@ extension LDUser { email: StubConstants.email, avatar: StubConstants.avatar, custom: StubConstants.custom(includeSystemValues: true), - isAnonymous: StubConstants.isAnonymous, - secondary: StubConstants.secondary) + isAnonymous: StubConstants.isAnonymous) return user } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift index a7d03522..f85b353d 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift @@ -10,7 +10,7 @@ final class LDContextCodableSpec: XCTestCase { ("{\"key\" : \"foo\", \"name\" : \"bar\"}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"name\" : \"bar\"}"), ("{\"key\" : \"foo\", \"custom\" : {\"a\" : \"b\"}}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"a\" : \"b\"}"), ("{\"key\" : \"foo\", \"anonymous\" : true}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"anonymous\" : true}"), - ("{\"key\" : \"foo\", \"secondary\" : \"bar\"}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"secondary\" : \"bar\"}"), + ("{\"key\" : \"foo\"}", "{\"kind\" : \"user\", \"key\" : \"foo\"}"), ("{\"key\" : \"foo\", \"ip\" : \"1\", \"privateAttributeNames\" : [\"ip\"]}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"ip\" : \"1\", \"_meta\" : { \"privateAttributes\" : [\"ip\"]} }") ] @@ -24,7 +24,8 @@ final class LDContextCodableSpec: XCTestCase { func testUserCustomAttributesAreOverriddenByOldBuiltIns() throws { let userJson = "{\"key\" : \"foo\", \"anonymous\" : true, \"secondary\": \"my secondary\", \"custom\": {\"anonymous\": false, \"secondary\": \"custom secondary\"}}" - let explicitFormat = "{\"kind\" : \"user\", \"key\" : \"foo\", \"anonymous\" : true, \"secondary\": \"my secondary\"}" + // Secondary is not supported, so we should get the custom version for that. + let explicitFormat = "{\"kind\" : \"user\", \"key\" : \"foo\", \"anonymous\" : true, \"secondary\": \"custom secondary\"}" let userContext = try JSONDecoder().decode(LDContext.self, from: Data(userJson.utf8)) let explicitContext = try JSONDecoder().decode(LDContext.self, from: Data(explicitFormat.utf8)) @@ -64,7 +65,6 @@ final class LDContextCodableSpec: XCTestCase { "/complex/attribute": "should be removed", "_meta":{ "privateAttributes":["a", "/b/c", "~1complex~1attribute"], - "secondary":"baz" } } """ @@ -90,7 +90,6 @@ final class LDContextCodableSpec: XCTestCase { }, "_meta":{ "privateAttributes":["a", "/b/c"], - "secondary":"baz" } } """ diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 6e5ffcb2..f2f59e54 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -28,15 +28,12 @@ final class LDUserSpec: QuickSpec { avatar: LDUser.StubConstants.avatar, custom: LDUser.StubConstants.custom(includeSystemValues: true), isAnonymous: LDUser.StubConstants.isAnonymous, - privateAttributes: LDUser.optionalAttributes, - secondary: LDUser.StubConstants.secondary) + privateAttributes: LDUser.optionalAttributes) 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.isAnonymousNullable) == LDUser.StubConstants.isAnonymous expect(user.country) == LDUser.StubConstants.country expect(user.ipAddress) == LDUser.StubConstants.ipAddress expect(user.email) == LDUser.StubConstants.email @@ -46,8 +43,6 @@ final class LDUserSpec: QuickSpec { } it("without setting anonymous") { user = LDUser(key: "abc") - expect(user.isAnonymous) == false - expect(user.isAnonymousNullable).to(beNil()) } context("called without optional elements") { var environmentReporter: EnvironmentReporter! @@ -58,7 +53,6 @@ final class LDUserSpec: QuickSpec { it("creates a LDUser without optional elements") { expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) expect(user.isAnonymous) == true - expect(user.isAnonymousNullable) == true expect(user.name).to(beNil()) expect(user.firstName).to(beNil()) @@ -68,7 +62,6 @@ final class LDUserSpec: QuickSpec { expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) expect(user.privateAttributes).to(beEmpty()) - expect(user.secondary).to(beNil()) } } context("called without a key multiple times") { @@ -83,7 +76,6 @@ final class LDUserSpec: QuickSpec { users.forEach { user in expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) expect(user.isAnonymous) == true - expect(user.isAnonymousNullable) == true } } } @@ -101,9 +93,7 @@ final class LDUserSpec: QuickSpec { it("creates a user with system values matching the environment reporter") { expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) expect(user.isAnonymous) == true - expect(user.isAnonymousNullable) == true - expect(user.secondary).to(beNil()) expect(user.name).to(beNil()) expect(user.firstName).to(beNil()) expect(user.lastName).to(beNil()) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift index 76aa62e1..8faef78d 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift @@ -27,7 +27,6 @@ final class LDUserToContextSpec: XCTestCase { user.custom = ["/nested/attribute": "here is a nested attribute"] user.isAnonymous = true user.privateAttributes = [UserAttribute("/nested/attribute")] - user.secondary = "secondary" var builder = LDContextBuilder(key: "user-key") builder.name("Example user") @@ -40,7 +39,6 @@ final class LDUserToContextSpec: XCTestCase { builder.trySetValue("/nested/attribute", "here is a nested attribute".toLDValue()) builder.anonymous(true) builder.addPrivateAttribute(Reference(literal: "/nested/attribute")) - builder.trySetValue("secondary", "secondary".toLDValue()) let context = try builder.build().get() let userContext = try user.toContext().get() From 8cdb5394a970bec307a8a5fbb2448f2e092494b8 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 5 Dec 2022 15:51:34 -0600 Subject: [PATCH 78/90] Add user type tests (#228) --- ContractTests/.swiftlint.yml | 16 +++++-- .../Source/Controllers/SdkController.swift | 25 ++++++++--- ContractTests/Source/Models/client.swift | 1 + ContractTests/Source/Models/command.swift | 5 ++- ContractTests/Source/Models/user.swift | 29 ++++++++++++ .../GeneratedCode/mocks.generated.swift | 2 +- LaunchDarkly/LaunchDarkly/LDClient.swift | 2 +- .../Models/Context/LDContext.swift | 4 +- .../FeatureFlag/FlagRequestTracker.swift | 14 ++++-- .../ObjectiveC/ObjcLDClient.swift | 2 +- .../ServiceObjects/Cache/CacheConverter.swift | 2 +- .../ServiceObjects/EventReporter.swift | 16 ++++--- .../Models/Context/LDContextSpec.swift | 6 +-- .../LaunchDarklyTests/Models/EventSpec.swift | 8 ++-- .../FlagRequestTracking/FlagCounterSpec.swift | 44 ++++++++++--------- .../FlagRequestTrackerSpec.swift | 22 +++++----- .../ServiceObjects/EventReporterSpec.swift | 16 +++---- 17 files changed, 141 insertions(+), 73 deletions(-) create mode 100644 ContractTests/Source/Models/user.swift diff --git a/ContractTests/.swiftlint.yml b/ContractTests/.swiftlint.yml index c8effa12..7ee54985 100644 --- a/ContractTests/.swiftlint.yml +++ b/ContractTests/.swiftlint.yml @@ -1,5 +1,9 @@ +# See test subconfiguration at `LaunchDarkly/LaunchDarklyTests/.swiftlint.yml` + disabled_rules: + - cyclomatic_complexity - line_length + - todo opt_in_rules: - contains_over_filter_count @@ -11,6 +15,7 @@ opt_in_rules: - flatmap_over_map_reduce - implicitly_unwrapped_optional - let_var_whitespace + - missing_docs - redundant_nil_coalescing - sorted_first_last - trailing_closure @@ -24,8 +29,8 @@ included: excluded: function_body_length: - warning: 50 - error: 70 + warning: 70 + error: 90 type_body_length: warning: 300 @@ -37,7 +42,7 @@ file_length: identifier_name: min_length: # only min_length - warning: 3 # only warning + warning: 2 # only warning max_length: warning: 50 error: 60 @@ -55,4 +60,9 @@ identifier_name: trailing_whitespace: severity: error +missing_docs: + error: + - open + - public + reporter: "xcode" diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index 48d96b95..f3d5e394 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -21,7 +21,8 @@ final class SdkController: RouteCollection { "mobile", "service-endpoints", "strongly-typed", - "tags" + "tags", + "user-type" ] return StatusResponse( @@ -66,7 +67,7 @@ final class SdkController: RouteCollection { } if let globalPrivate = events.globalPrivateAttributes { - config.privateContextAttributes = globalPrivate.map{ Reference($0) } + config.privateContextAttributes = globalPrivate.map { Reference($0) } } if let flushIntervalMs = events.flushIntervalMs { @@ -100,8 +101,14 @@ final class SdkController: RouteCollection { let dispatchSemaphore = DispatchSemaphore(value: 0) let startWaitSeconds = (createInstance.configuration.startWaitTimeMs ?? 5_000) / 1_000 - LDClient.start(config:config, context: clientSide.initialContext, startWaitSeconds: startWaitSeconds) { timedOut in - dispatchSemaphore.signal() + if let context = clientSide.initialContext { + LDClient.start(config: config, context: context, startWaitSeconds: startWaitSeconds) { _ in + dispatchSemaphore.signal() + } + } else if let user = clientSide.initialUser { + LDClient.start(config: config, user: user, startWaitSeconds: startWaitSeconds) { _ in + dispatchSemaphore.signal() + } } dispatchSemaphore.wait() @@ -152,8 +159,14 @@ final class SdkController: RouteCollection { return CommandResponse.evaluateAll(result) case "identifyEvent": let semaphore = DispatchSemaphore(value: 0) - client.identify(context: commandParameters.identifyEvent!.context) { - semaphore.signal() + if let context = commandParameters.identifyEvent!.context { + client.identify(context: context) { + semaphore.signal() + } + } else if let user = commandParameters.identifyEvent!.user { + client.identify(user: user) { + semaphore.signal() + } } semaphore.wait() case "customEvent": diff --git a/ContractTests/Source/Models/client.swift b/ContractTests/Source/Models/client.swift index 069f84b8..a072c4e7 100644 --- a/ContractTests/Source/Models/client.swift +++ b/ContractTests/Source/Models/client.swift @@ -44,6 +44,7 @@ struct TagParameters: Content { struct ClientSideParameters: Content { var initialContext: LDContext? + var initialUser: LDUser? var evaluationReasons: Bool? var useReport: Bool? } diff --git a/ContractTests/Source/Models/command.swift b/ContractTests/Source/Models/command.swift index b19d5e6a..567f784c 100644 --- a/ContractTests/Source/Models/command.swift +++ b/ContractTests/Source/Models/command.swift @@ -70,7 +70,8 @@ struct CustomEventParameters: Content { } struct IdentifyEventParameters: Content, Decodable { - var context: LDContext + var context: LDContext? + var user: LDUser? } struct ContextBuildParameters: Content, Decodable { @@ -84,7 +85,7 @@ struct SingleContextParameters: Content, Decodable { var name: String? var anonymous: Bool? var privateAttribute: [String]? - var custom: [String:LDValue]? + var custom: [String: LDValue]? private enum CodingKeys: String, CodingKey { case kind, key, name, anonymous, privateAttribute = "private", custom diff --git a/ContractTests/Source/Models/user.swift b/ContractTests/Source/Models/user.swift new file mode 100644 index 00000000..51542c93 --- /dev/null +++ b/ContractTests/Source/Models/user.swift @@ -0,0 +1,29 @@ +import Foundation +import LaunchDarkly + +extension LDUser: Decodable { + + /// 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 = "privateAttributeNames" + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.init() + + key = try values.decodeIfPresent(String.self, forKey: .key) ?? "" + name = try values.decodeIfPresent(String.self, forKey: .name) + firstName = try values.decodeIfPresent(String.self, forKey: .firstName) + lastName = try values.decodeIfPresent(String.self, forKey: .lastName) + country = try values.decodeIfPresent(String.self, forKey: .country) + ipAddress = try values.decodeIfPresent(String.self, forKey: .ipAddress) + email = try values.decodeIfPresent(String.self, forKey: .email) + avatar = try values.decodeIfPresent(String.self, forKey: .avatar) + custom = try values.decodeIfPresent([String: LDValue].self, forKey: .custom) ?? [:] + isAnonymous = try values.decodeIfPresent(Bool.self, forKey: .isAnonymous) ?? false + _ = try values.decodeIfPresent([String].self, forKey: .privateAttributes) + privateAttributes = (try values.decodeIfPresent([String].self, forKey: .privateAttributes) ?? []).map { UserAttribute.forName($0) } + } +} diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index 9062aa79..4a0ba753 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -215,7 +215,7 @@ final class EventReportingMock: EventReporting { var lastEventResponseDateSetCount = 0 var setLastEventResponseDateCallback: (() throws -> Void)? - var lastEventResponseDate: Date? = nil { + var lastEventResponseDate: Date = Date.distantPast { didSet { lastEventResponseDateSetCount += 1 try! setLastEventResponseDateCallback?() diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 03ffab9b..8bfd4492 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -131,7 +131,7 @@ public class LDClient { private let internalSetOnlineQueue: DispatchQueue = DispatchQueue(label: "InternalSetOnlineQueue") - private func go(online goOnline: Bool, reasonOnlineUnavailable: String, completion:(() -> Void)?) { + private func go(online goOnline: Bool, reasonOnlineUnavailable: String, completion: (() -> Void)?) { let owner = "SetOnlineOwner" as AnyObject var completed = false let internalCompletedQueue = DispatchQueue(label: "com.launchdarkly.LDClient.goCompletedQueue") diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index df5b8cd8..b3d2a482 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -136,7 +136,7 @@ public struct LDContext: Encodable, Equatable { let (isReacted, nestedPropertiesAreRedacted) = includePrivateAttributes ? (false, false) : LDContext.maybeRedact(context: context, parentPath: path, value: value, redactedAttributes: &redactedAttributes, globalPrivateAttributes: globalPrivateAttributes) switch value { - case .object(_) where isReacted: + case .object where isReacted: break case .object(let objectMap): if !nestedPropertiesAreRedacted { @@ -165,7 +165,7 @@ public struct LDContext: Encodable, Equatable { } var shouldCheckNestedProperties: Bool = false - if case .object(_) = value { + if case .object = value { shouldCheckNestedProperties = true } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift index 909696c8..8bd8465e 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift @@ -4,13 +4,13 @@ struct FlagRequestTracker { let startDate = Date() var flagCounters: [LDFlagKey: FlagCounter] = [:] - mutating func trackRequest(flagKey: LDFlagKey, reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue) { + mutating func trackRequest(flagKey: LDFlagKey, reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue, context: LDContext) { if flagCounters[flagKey] == nil { flagCounters[flagKey] = FlagCounter() } guard let flagCounter = flagCounters[flagKey] else { return } - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) + flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue, context: context) Log.debug(typeName(and: #function) + "\n\tflagKey: \(flagKey)" + "\n\treportedValue: \(reportedValue), " @@ -26,7 +26,7 @@ extension FlagRequestTracker: TypeIdentifying { } final class FlagCounter: Encodable { enum CodingKeys: String, CodingKey { - case defaultValue = "default", counters + case defaultValue = "default", counters, contextKinds } enum CounterCodingKeys: String, CodingKey { @@ -35,8 +35,9 @@ final class FlagCounter: Encodable { private(set) var defaultValue: LDValue = .null private(set) var flagValueCounters: [CounterKey: CounterValue] = [:] + private(set) var contextKinds: Set = Set() - func trackRequest(reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue) { + func trackRequest(reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue, context: LDContext) { self.defaultValue = defaultValue let key = CounterKey(variation: featureFlag?.variation, version: featureFlag?.versionForEvents) if let counter = flagValueCounters[key] { @@ -44,11 +45,16 @@ final class FlagCounter: Encodable { } else { flagValueCounters[key] = CounterValue(value: reportedValue) } + + context.contextKeys().forEach { kind, _ in + contextKinds.insert(kind) + } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(defaultValue, forKey: .defaultValue) + try container.encode(contextKinds, forKey: .contextKinds) var countersContainer = container.nestedUnkeyedContainer(forKey: .counters) try flagValueCounters.forEach { (key, value) in var counterContainer = countersContainer.nestedContainer(keyedBy: CounterCodingKeys.self) diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index 9efc6dc8..f1db29b0 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -88,7 +88,7 @@ public final class ObjcLDClient: NSObject { - parameter goOnline: Desired online/offline mode for the LDClient - parameter completion: Completion block called when setOnline completes. (Optional) */ - @objc public func setOnline(_ goOnline: Bool, completion:(() -> Void)? = nil) { + @objc public func setOnline(_ goOnline: Bool, completion: (() -> Void)? = nil) { ldClient.setOnline(goOnline, completion: completion) } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index 8151c3f0..2ee646c5 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -105,7 +105,7 @@ final class CacheConverter: CacheConverting { if key == "context-users" { guard let data = flagCaching.keyedValueCache.data(forKey: "cached-users") else { - return; + return } flagCaching.keyedValueCache.removeObject(forKey: "cached-users") flagCaching.keyedValueCache.set(data, forKey: "cached-contexts") diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index b0325fbc..3736c813 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -5,7 +5,8 @@ typealias EventSyncCompleteClosure = ((SynchronizingError?) -> Void) protocol EventReporting { // sourcery: defaultMockValue = false var isOnline: Bool { get set } - var lastEventResponseDate: Date? { get } + // sourcery: defaultMockValue = Date.distantPast + var lastEventResponseDate: Date { get } func record(_ event: Event) // swiftlint:disable:next function_parameter_count @@ -19,7 +20,7 @@ class EventReporter: EventReporting { set { timerQueue.sync { newValue ? startReporting() : stopReporting() } } } - private (set) var lastEventResponseDate: Date? + private (set) var lastEventResponseDate: Date let service: DarklyServiceProvider @@ -37,6 +38,7 @@ class EventReporter: EventReporting { init(service: DarklyServiceProvider, onSyncComplete: EventSyncCompleteClosure?) { self.service = service self.onSyncComplete = onSyncComplete + self.lastEventResponseDate = Date() } func record(_ event: Event) { @@ -59,7 +61,7 @@ class EventReporter: EventReporting { let recordingDebugEvent = featureFlag?.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate) ?? false eventQueue.sync { - flagRequestTracker.trackRequest(flagKey: flagKey, reportedValue: value, featureFlag: featureFlag, defaultValue: defaultValue) + flagRequestTracker.trackRequest(flagKey: flagKey, reportedValue: value, featureFlag: featureFlag, defaultValue: defaultValue, context: context) if recordingFeatureEvent { let featureEvent = FeatureEvent(key: flagKey, context: context, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: false) recordNoSync(featureEvent) @@ -163,7 +165,11 @@ class EventReporter: EventReporting { 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 + let serverTime = response?.headerDate ?? self.lastEventResponseDate + if serverTime > self.lastEventResponseDate { + self.lastEventResponseDate = serverTime + } + Log.debug(self.typeName(and: #function) + "Completed sending \(sentEvents) event(s)") self.reportSyncComplete(nil) return false @@ -204,7 +210,7 @@ extension EventReporter: TypeIdentifying { } #if DEBUG extension EventReporter { - func setLastEventResponseDate(_ date: Date?) { + func setLastEventResponseDate(_ date: Date) { lastEventResponseDate = date } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift index 80bda5e9..953c8d4a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift @@ -123,7 +123,7 @@ final class LDContextSpec: XCTestCase { func testMultikindBuilderRequiresContext() throws { let multiBuilder = LDMultiContextBuilder() switch multiBuilder.build() { - case .success(_): + case .success: XCTFail("Multibuilder should have failed to build.") case .failure(let error): XCTAssertEqual(error, .emptyMultiKind) @@ -145,7 +145,7 @@ final class LDContextSpec: XCTestCase { multiBuilder.addContext(multiContext) switch multiBuilder.build() { - case .success(_): + case .success: XCTFail("Multibuilder should have failed to build with a multi-context.") case .failure(let error): XCTAssertEqual(error, .nestedMultiKind) @@ -159,7 +159,7 @@ final class LDContextSpec: XCTestCase { multiBuilder.addContext(try LDContextBuilder(key: "second").build().get()) switch multiBuilder.build() { - case .success(_): + case .success: XCTFail("Multibuilder should have failed to build.") case .failure(let error): XCTAssertEqual(error, .duplicateKinds) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 9649333c..f7e73fb7 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -228,8 +228,8 @@ final class EventSpec: XCTestCase { 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) + flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true, context: LDContext.stub()) + flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true, context: LDContext.stub()) let event = SummaryEvent(flagRequestTracker: flagRequestTracker, endDate: Date()) encodesToObject(event) { dict in XCTAssertEqual(dict.count, 4) @@ -239,8 +239,8 @@ final class EventSpec: XCTestCase { 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) + counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true, context: LDContext.stub()) + counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true, context: LDContext.stub()) XCTAssertEqual(features["bool-flag"], encodeToLDValue(counter)) } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift index 3186dc39..b235bbbe 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift @@ -16,7 +16,7 @@ final class FlagCounterSpec: XCTestCase { func testTrackRequestInitialKnown() { let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 2, flagVersion: 3) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 1) let counter = flagCounter.flagValueCounters.first! @@ -30,8 +30,8 @@ final class FlagCounterSpec: XCTestCase { 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: testValue, featureFlag: featureFlag, defaultValue: "e") - flagCounter.trackRequest(reportedValue: "b", featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: "e", context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: "b", featureFlag: secondFeatureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 1) let counter = flagCounter.flagValueCounters.first! @@ -45,8 +45,8 @@ final class FlagCounterSpec: XCTestCase { 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: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) - flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 2) let counter1 = flagCounter.flagValueCounters.first { key, _ in key.variation == 2 }! @@ -63,8 +63,8 @@ final class FlagCounterSpec: XCTestCase { 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: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) - flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 2) let counter1 = flagCounter.flagValueCounters.first { key, _ in key.version == 3 }! @@ -81,8 +81,8 @@ final class FlagCounterSpec: XCTestCase { let featureFlag = 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: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) - flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 1) let counter = flagCounter.flagValueCounters.first! @@ -94,7 +94,7 @@ final class FlagCounterSpec: XCTestCase { func testTrackRequestInitialUnknown() { let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 1) let counter = flagCounter.flagValueCounters.first! @@ -106,8 +106,8 @@ final class FlagCounterSpec: XCTestCase { func testTrackRequestSecondUnknown() { let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue) - flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue, context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 1) let counter = flagCounter.flagValueCounters.first! @@ -121,8 +121,8 @@ final class FlagCounterSpec: XCTestCase { 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) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: unknownFlag1, defaultValue: testDefaultValue, context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: unknownFlag2, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 2) let counter1 = flagCounter.flagValueCounters.first { key, _ in key.variation == 1 }! @@ -140,11 +140,12 @@ final class FlagCounterSpec: XCTestCase { func testEncoding() { let featureFlag = FeatureFlag(flagKey: "unused", variation: 3, version: 2, flagVersion: 5) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: "a", featureFlag: featureFlag, defaultValue: "b") - flagCounter.trackRequest(reportedValue: "a", featureFlag: featureFlag, defaultValue: "b") + flagCounter.trackRequest(reportedValue: "a", featureFlag: featureFlag, defaultValue: "b", context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: "a", featureFlag: featureFlag, defaultValue: "b", context: LDContext.stub()) encodesToObject(flagCounter) { dict in - XCTAssertEqual(dict.count, 2) + XCTAssertEqual(dict.count, 3) XCTAssertEqual(dict["default"], "b") + XCTAssertEqual(dict["contextKinds"], ["user"]) valueIsArray(dict["counters"]) { counters in XCTAssertEqual(counters.count, 1) valueIsObject(counters[0]) { counter in @@ -158,10 +159,11 @@ final class FlagCounterSpec: XCTestCase { } let flagCounterNulls = FlagCounter() - flagCounterNulls.trackRequest(reportedValue: nil, featureFlag: nil, defaultValue: nil) + flagCounterNulls.trackRequest(reportedValue: nil, featureFlag: nil, defaultValue: nil, context: LDContext.stub()) encodesToObject(flagCounterNulls) { dict in - XCTAssertEqual(dict.count, 2) + XCTAssertEqual(dict.count, 3) XCTAssertEqual(dict["default"], .null) + XCTAssertEqual(dict["contextKinds"], ["user"]) valueIsArray(dict["counters"]) { counters in XCTAssertEqual(counters.count, 1) valueIsObject(counters[0]) { counter in @@ -186,11 +188,11 @@ extension FlagCounter { if flagKey.isKnown { featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey) for _ in 0.. Date: Tue, 6 Dec 2022 11:37:54 -0600 Subject: [PATCH 79/90] Expand code doc in preparation for u2c release (#226) --- .../LaunchDarkly/Models/Context/Kind.swift | 12 ++ .../Models/Context/LDContext.swift | 90 +++++++-- .../Models/Context/Reference.swift | 15 ++ LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 12 +- .../ObjectiveC/ObjcLDContext.swift | 191 ++++++++++++++++++ .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 7 +- 6 files changed, 298 insertions(+), 29 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift index 60852312..45926d22 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift @@ -1,8 +1,20 @@ import Foundation +/// Kind is an enumeration set by the application to describe what kind of entity an `LDContext` +/// represents. The meaning of this is completely up to the application. When no Kind is +/// specified, the default is `Kind.user`. +/// +/// For a multi-context (see `LDMultiContextBuilder`), the Kind is always `Kind.multi`; +/// there is a specific Kind for each of the individual Contexts within it. public enum Kind: Codable, Equatable, Hashable { + /// user is both the default Kind and also the kind used for legacy users in earlier versions of this SDK. case user + + /// multi is only usable by constructing a multi-context using `LDMultiContextBuilder`. Attempting to set + /// a context kind to multi directly will result in an invalid context. case multi + + /// The custom case handles arbitrarily defined contexts (e.g. org, account, server). case custom(String) public init(from decoder: Decoder) throws { diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index b3d2a482..59a38254 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -1,19 +1,24 @@ import Foundation +/// Enumeration representing various modes of failures when constructing an `LDContext`. public enum ContextBuilderError: Error { + /// The provided kind either contains invalid characters, or is the disallowed kind "kind". case invalidKind + /// The `LDMultiContextBuilder` must be used when attempting to build a multi-context. case requiresMultiBuilder + /// The JSON representations for the context was missing the "key" property. case emptyKey + /// Attempted to build a multi-context without providing any contexts. case emptyMultiKind + /// A multi-context cannot contain another multi-context. case nestedMultiKind + /// Attempted to build a multi-context containing 2 or more contexts with the same kind. case duplicateKinds } /// LDContext is a collection of attributes that can be referenced in flag evaluations and analytics /// events. /// -/// (TKTK - some conceptual text here, and/or a link to a docs page) -/// /// To create an LDContext of a single kind, such as a user, you may use `LDContextBuilder`. /// /// To create an LDContext with multiple kinds, use `LDMultiContextBuilder`. @@ -312,7 +317,10 @@ public struct LDContext: Encodable, Equatable { return (nil, false) } - /// TKTK + /// FullyQualifiedKey returns a string that describes the entire Context based on Kind and Key values. + /// + /// This value is used whenever LaunchDarkly needs a string identifier based on all of the Kind and + /// Key values in the context; the SDK may use this for caching previously seen contexts, for instance. public func fullyQualifiedKey() -> String { return canonicalizedKey } @@ -325,11 +333,12 @@ public struct LDContext: Encodable, Equatable { return Util.sha256base64(fullyQualifiedKey()) + "$" } - /// TKTK + /// - Returns: true if the `LDContext` is a multi-context; false otherwise. public func isMulti() -> Bool { return self.kind.isMulti() } + //// - Returns: A hash mapping a context's kind to its key. public func contextKeys() -> [String: String] { guard isMulti() else { return [kind.description: key ?? ""] @@ -339,7 +348,20 @@ public struct LDContext: Encodable, Equatable { return keys } - /// TKTK + /// Looks up the value of any attribute of the `LDContext`, or a value contained within an + /// attribute, based on a `Reference`. This includes only attributes that are addressable in evaluations. + /// + /// This implements the same behavior that the SDK uses to resolve attribute references during a flag + /// evaluation. In a context, the `Reference` can represent a simple attribute name-- either a + /// built-in one like "name" or "key", or a custom attribute that was set by `LDContextBuilder.trySetValue(...)`-- + /// or, it can be a slash-delimited path using a JSON-Pointer-like syntax. See `Reference` for more details. + /// + /// For a multi-context, the only supported attribute name is "kind". + /// + /// If the value is found, the return value is the attribute value, using the type `LDValue` to + /// represent a value of any JSON type. + /// + /// If there is no such attribute, or if the `Reference` is invalid, the return value is nil. public func getValue(_ reference: Reference) -> LDValue? { if !reference.isValid() { return nil @@ -354,7 +376,7 @@ public struct LDContext: Encodable, Equatable { return .string(String(kind)) } - Log.debug(typeName(and: #function) + ": Cannot get non-kind attribute from multi-kind context") + Log.debug(typeName(and: #function) + ": Cannot get non-kind attribute from multi-context") return nil } @@ -562,7 +584,7 @@ enum LDContextBuilderKey { /// kind is "user", its key is set to whatever value you passed to `LDContextBuilder.init(key:)`, its anonymous attribute /// is false, and it has no values for any other attributes. /// -/// To define a multi-kind LDContext, see `LDMultiContextBuilder`. +/// To define a multi-context, see `LDMultiContextBuilder`. public struct LDContextBuilder { private var kind: String = Kind.user.description @@ -710,15 +732,53 @@ public struct LDContextBuilder { /// Provide a reference to designate any number of LDContext attributes as private: that is, /// their values will not be sent to LaunchDarkly. /// - /// (TKTK: possibly move some of this conceptual information to a non-platform-specific docs page and/or - /// have docs team copyedit it here) + /// This action only affects analytics events that involve this particular `LDContext`. To mark some (or all) + /// Context attributes as private for all contexts, use the overall event configuration for the SDK. + /// + /// In this example, firstName is marked as private, but lastName is not: + /// + /// ```swift + /// var builder = LDContextBuilder(key: "my-key") + /// builder.kind("org") + /// builder.trySetValue("firstName", "Pierre") + /// builder.trySetValue("lastName", "Menard") + /// builder.addPrivate(Reference("firstName")) + /// + /// let context = try builder.build().get() + /// ``` + /// + /// The attributes "kind", "key", and "anonymous" cannot be made private. + /// + /// This is a metadata property, rather than an attribute that can be addressed in evaluations: that is, + /// a rule clause that references the attribute name "private" will not use this value, but instead will + /// use whatever value (if any) you have set for that name with `trySetValue(...)`. + /// + /// # Designating an entire attribute as private + /// + /// If the parameter is an attribute name such as "email" that does not start with a '/' character, the + /// entire attribute is private. + /// + /// # Designating a property within a JSON object as private + /// + /// If the parameter starts with a '/' character, it is interpreted as a slash-delimited path to a + /// property within a JSON object. The first path component is an attribute name, and each following + /// component is a property name. + /// + /// For instance, suppose that the attribute "address" had the following JSON object value: + /// {"street": {"line1": "abc", "line2": "def"}, "city": "ghi"} /// - /// See `Reference` for details on how to construct a valid reference. + /// - Calling either addPrivateAttribute(Reference("address")) or addPrivateAddress(Reference("/address")) would + /// cause the entire "address" attribute to be private. + /// - Calling addPrivateAttribute("/address/street") would cause the "street" property to be private, so that + /// only {"city": "ghi"} is included in analytics. + /// - Calling addPrivateAttribute("/address/street/line2") would cause only "line2" within "street" to be private, + /// so that {"street": {"line1": "abc"}, "city": "ghi"} is included in analytics. /// - /// This action only affects analytics events that involve this particular LDContext. To mark some (or all) - /// LDContext attributes as private for all uses, use the overall event configuration for the SDK. + /// This syntax deliberately resembles JSON Pointer, but other JSON Pointer features such as array + /// indexing are not supported. /// - /// The attributes "kind" and "key", and the metadata properties set by anonymous, cannot be made private. + /// If an attribute's actual name starts with a '/' character, you must use the same escaping syntax as + /// JSON Pointer: replace "~" with "~0", and "/" with "~1". public mutating func addPrivateAttribute(_ reference: Reference) { self.privateAttributes.append(reference) } @@ -774,7 +834,7 @@ public struct LDContextBuilder { extension LDContextBuilder: TypeIdentifying { } -/// Contains method for building a multi-kind `LDContext`. +/// Contains method for building a multi-context. /// /// Use this type if you need to construct a LDContext that has multiple kind values, each with its /// own nested LDContext. To define a single-kind context, use `LDContextBuilder` instead. @@ -806,7 +866,7 @@ public struct LDMultiContextBuilder { /// situations, a Result.failure will be returned. /// /// If only one context kind was added to the builder, `build` returns a single-kind context rather - /// than a multi-kind context. + /// than a multi-context. public func build() -> Result { if contexts.isEmpty { return Result.failure(.emptyMultiKind) diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift index 0d6ddad1..7ab095a4 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift @@ -1,8 +1,23 @@ import Foundation +/// An enumeration describing the individual failure conditions which may occur when constructing a `Reference`. public enum ReferenceError: Codable, Equatable, Error { + /// empty means that you tried to create a `Reference` from an empty string, or a string that consisted only of a + /// slash. + /// + /// For details of the attribute reference syntax, see `Reference`. case empty + + /// doubleSlash means that an attribute reference contained a double slash or trailing slash causing one path + /// component to be empty, such as "/a//b" or "/a/b/". + /// + /// For details of the attribute reference syntax, see `Reference`. case doubleSlash + + /// invalidEscapeSequence means that an attribute reference contained contained a "~" character that was not + /// followed by "0" or "1". + /// + /// For details of the attribute reference syntax, see `Reference`. case invalidEscapeSequence public init(from decoder: Decoder) throws { diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 0bfb1667..7926b503 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -3,16 +3,8 @@ import Foundation /** 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. + The usage of LDUser is no longer recommended and is retained only to ease the adoption of the `LDContext` class. New + code using this SDK should make use of the `LDContextBuilder` to construct an equivalent `Kind.user` kind context. */ public struct LDUser: Encodable, Equatable { diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift index c81838f2..8742407c 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift @@ -1,5 +1,11 @@ import Foundation +/// LDContext is a collection of attributes that can be referenced in flag evaluations and analytics +/// events. +/// +/// To create an LDContext of a single kind, such as a user, you may use `LDContextBuilder`. +/// +/// To create an LDContext with multiple kinds, use `LDMultiContextBuilder`. @objc(LDContext) public final class ObjcLDContext: NSObject { var context: LDContext @@ -8,9 +14,29 @@ public final class ObjcLDContext: NSObject { self.context = context } + /// FullyQualifiedKey returns a string that describes the entire Context based on Kind and Key values. + /// + /// This value is used whenever LaunchDarkly needs a string identifier based on all of the Kind and + /// Key values in the context; the SDK may use this for caching previously seen contexts, for instance. @objc public func fullyQualifiedKey() -> String { context.fullyQualifiedKey() } + /// - Returns: true if the `LDContext` is a multi-context; false otherwise. @objc public func isMulti() -> Bool { context.isMulti() } + //// - Returns: A hash mapping a context's kind to its key. @objc public func contextKeys() -> [String: String] { context.contextKeys() } + /// Looks up the value of any attribute of the `LDContext`, or a value contained within an + /// attribute, based on a `Reference`. This includes only attributes that are addressable in evaluations. + /// + /// This implements the same behavior that the SDK uses to resolve attribute references during a flag + /// evaluation. In a context, the `Reference` can represent a simple attribute name-- either a + /// built-in one like "name" or "key", or a custom attribute that was set by `LDContextBuilder.trySetValue(...)`-- + /// or, it can be a slash-delimited path using a JSON-Pointer-like syntax. See `Reference` for more details. + /// + /// For a multi-context, the only supported attribute name is "kind". + /// + /// If the value is found, the return value is the attribute value, using the type `LDValue` to + /// represent a value of any JSON type. + /// + /// If there is no such attribute, or if the `Reference` is invalid, the return value is nil. @objc public func getValue(reference: ObjcLDReference) -> ObjcLDValue? { guard let value = context.getValue(reference.reference) else { return nil } @@ -19,14 +45,31 @@ public final class ObjcLDContext: NSObject { } } +/// Contains methods for building a single kind `LDContext` with a specified key, defaulting to kind +/// "user". +/// +/// You may use these methods to set additional attributes and/or change the kind before calling +/// `LDContextBuilder.build()`. If you do not change any values, the defaults for the `LDContext` are that its +/// kind is "user", its key is set to whatever value you passed to `LDContextBuilder.init(key:)`, its anonymous attribute +/// is false, and it has no values for any other attributes. +/// +/// To define a multi-context, see `LDMultiContextBuilder`. @objc(LDContextBuilder) public final class ObjcLDContextBuilder: NSObject { var builder: LDContextBuilder + /// Create a new LDContextBuilder. + /// + /// By default, this builder will create an anonymous LDContext with a generated key. This key will be cached + /// locally and reused for the same context kind. + /// + /// If `LDContextBuilder.key` is called, a key will no longer be generated and the anonymous status will match the + /// value provided by `LDContextBuilder.anonymous` or false by default. @objc public override init() { builder = LDContextBuilder() } + /// Create a new LDContextBuilder with the provided `key`. @objc public init(key: String) { builder = LDContextBuilder(key: key) } @@ -37,18 +80,140 @@ public final class ObjcLDContextBuilder: NSObject { self.builder = builder } + /// Sets the LDContext's kind attribute. + /// + /// Every LDContext has a kind. Setting it to an empty string is equivalent to the default kind + /// of "user". This value is case-sensitive. Validation rules are as follows: + /// + /// - It may only contain letters, numbers, and the characters ".", "_", and "-". + /// - It cannot equal the literal string "kind". + /// - It cannot equal "multi". + /// + /// If the value is invalid, you will receive an error when `LDContextBuilder.build()` is called. @objc public func kind(kind: String) { builder.kind(kind) } + /// Sets the LDContext's key attribute. + /// + /// Every LDContext has a key, which is always a string. There are no restrictions on its value other than it cannot + /// be empty. + /// + /// The key attribute can be referenced by flag rules, flag target lists, and segments. @objc public func key(key: String) { builder.key(key) } + /// Sets the LDContext's name attribute. + /// + /// This attribute is optional. It has the following special rules: + /// + /// - Unlike most other attributes, it is always a string if it is specified. + /// - The LaunchDarkly dashboard treats this attribute as the preferred display name for users. @objc public func name(name: String) { builder.name(name) } + /// Sets whether the LDContext is only intended for flag evaluations and should not be indexed by + /// LaunchDarkly. + /// + /// The default value is false. False means that this LDContext represents an entity such as a + /// user that you want to be able to see on the LaunchDarkly dashboard. + /// + /// Setting anonymous to true excludes this LDContext from the database that is used by the + /// dashboard. It does not exclude it from analytics event data, so it is not the same as + /// making attributes private; all non-private attributes will still be included in events and + /// data export. + /// + /// This value is also addressable in evaluations as the attribute name "anonymous". It is + /// always treated as a boolean true or false in evaluations. @objc public func anonymous(anonymous: Bool) { builder.anonymous(anonymous) } + /// Provide a reference to designate any number of LDContext attributes as private: that is, + /// their values will not be sent to LaunchDarkly. + /// + /// This action only affects analytics events that involve this particular `LDContext`. To mark some (or all) + /// Context attributes as private for all contexts, use the overall event configuration for the SDK. + /// + /// In this example, firstName is marked as private, but lastName is not: + /// + /// ```swift + /// var builder = LDContextBuilder(key: "my-key") + /// builder.kind("org") + /// builder.trySetValue("firstName", "Pierre") + /// builder.trySetValue("lastName", "Menard") + /// builder.addPrivate(Reference("firstName")) + /// + /// let context = try builder.build().get() + /// ``` + /// + /// The attributes "kind", "key", and "anonymous" cannot be made private. + /// + /// This is a metadata property, rather than an attribute that can be addressed in evaluations: that is, + /// a rule clause that references the attribute name "private" will not use this value, but instead will + /// use whatever value (if any) you have set for that name with `trySetValue(...)`. + /// + /// # Designating an entire attribute as private + /// + /// If the parameter is an attribute name such as "email" that does not start with a '/' character, the + /// entire attribute is private. + /// + /// # Designating a property within a JSON object as private + /// + /// If the parameter starts with a '/' character, it is interpreted as a slash-delimited path to a + /// property within a JSON object. The first path component is an attribute name, and each following + /// component is a property name. + /// + /// For instance, suppose that the attribute "address" had the following JSON object value: + /// {"street": {"line1": "abc", "line2": "def"}, "city": "ghi"} + /// + /// - Calling either addPrivateAttribute(Reference("address")) or addPrivateAddress(Reference("/address")) would + /// cause the entire "address" attribute to be private. + /// - Calling addPrivateAttribute("/address/street") would cause the "street" property to be private, so that + /// only {"city": "ghi"} is included in analytics. + /// - Calling addPrivateAttribute("/address/street/line2") would cause only "line2" within "street" to be private, + /// so that {"street": {"line1": "abc"}, "city": "ghi"} is included in analytics. + /// + /// This syntax deliberately resembles JSON Pointer, but other JSON Pointer features such as array + /// indexing are not supported. + /// + /// If an attribute's actual name starts with a '/' character, you must use the same escaping syntax as + /// JSON Pointer: replace "~" with "~0", and "/" with "~1". @objc public func addPrivateAttribute(reference: ObjcLDReference) { builder.addPrivateAttribute(reference.reference) } + /// Remove any reference provided through `addPrivateAttribute(_:)`. If the reference was + /// added more than once, this method will remove all instances of it. @objc public func removePrivateAttribute(reference: ObjcLDReference) { builder.removePrivateAttribute(reference.reference) } + /// Sets the value of any attribute for the Context except for private attributes. + /// + /// This method uses the `LDValue` type to represent a value of any JSON type: null, + /// boolean, number, string, array, or object. For all attribute names that do not have special + /// meaning to LaunchDarkly, you may use any of those types. Values of different JSON types are + /// always treated as different values: for instance, null, false, and the empty string "" are + /// not the same, and the number 1 is not the same as the string "1". + /// + /// The following attribute names have special restrictions on their value types, and any value + /// of an unsupported type will be ignored (leaving the attribute unchanged): + /// + /// - "kind", "key": Must be a string. See `LDContextBuilder.kind(_:)` and `LDContextBuilder.key(_:)`. + /// + /// - "name": Must be a string or null. See `LDContextBuilder.name(_:)`. + /// + /// - "anonymous": Must be a boolean. See `LDContextBuilder.anonymous(_:)`. + /// + /// Values that are JSON arrays or objects have special behavior when referenced in + /// flag/segment rules. + /// + /// A value of `LDValue.null` is equivalent to removing any current non-default value + /// of the attribute. Null is not a valid attribute value in the LaunchDarkly model; any + /// expressions in feature flags that reference an attribute with a null value will behave as + /// if the attribute did not exist. + /// + /// This method returns true for success, or false if the parameters + /// violated one of the restrictions described above (for instance, + /// attempting to set "key" to a value that was not a string). @discardableResult @objc public func trySetValue(name: String, value: ObjcLDValue) -> Bool { builder.trySetValue(name, value.wrappedValue) } + /// Creates a LDContext from the current LDContextBuilder properties. + /// + /// The LDContext is immutable and will not be affected by any subsequent actions on the + /// LDContextBuilder. + /// + /// It is possible to specify invalid attributes for a LDContextBuilder, such as an empty key. + /// In those situations, this method returns a Result.failure @objc public func build() -> ContextBuilderResult { switch builder.build() { case .success(let context): @@ -59,6 +224,15 @@ public final class ObjcLDContextBuilder: NSObject { } } +/// Contains method for building a multi-context. +/// +/// Use this type if you need to construct a LDContext that has multiple kind values, each with its +/// own nested LDContext. To define a single-kind context, use `LDContextBuilder` instead. +/// +/// Obtain an instance of LDMultiContextBuilder by calling `LDMultiContextBuilder.init()`; then, call +/// `LDMultiContextBuilder.addContext(_:)` to specify the nested LDContext for each kind. +/// LDMultiContextBuilder setters return a reference the same builder, so they can be chained +/// together. @objc(LDMultiContextBuilder) public final class ObjcLDMultiContextBuilder: NSObject { var builder: LDMultiContextBuilder @@ -67,6 +241,10 @@ public final class ObjcLDMultiContextBuilder: NSObject { builder = LDMultiContextBuilder() } + /// Adds a nested context for a specific kind to a LDMultiContextBuilder. + /// + /// It is invalid to add more than one context with the same Kind. This error is detected when + /// you call `LDMultiContextBuilder.build()`. @objc public func addContext(context: ObjcLDContext) { builder.addContext(context.context) } @@ -77,6 +255,16 @@ public final class ObjcLDMultiContextBuilder: NSObject { self.builder = builder } + /// Creates a LDContext from the current properties. + /// + /// The LDContext is immutable and will not be affected by any subsequent actions on the + /// LDMultiContextBuilder. + /// + /// It is possible for a LDMultiContextBuilder to represent an invalid state. In those + /// situations, a Result.failure will be returned. + /// + /// If only one context kind was added to the builder, `build` returns a single-kind context rather + /// than a multi-context. @objc public func build() -> ContextBuilderResult { switch builder.build() { case .success(let context): @@ -87,6 +275,7 @@ public final class ObjcLDMultiContextBuilder: NSObject { } } +/// An NSObject which mimics Swift's Result type, specifically for the `LDContext` type. @objc public class ContextBuilderResult: NSObject { @objc public private(set) var success: ObjcLDContext? @objc public private(set) var failure: NSError? @@ -97,10 +286,12 @@ public final class ObjcLDMultiContextBuilder: NSObject { failure = nil } + /// Create a "success" result with the provided `LDContext`. public static func fromSuccess(_ success: LDContext) -> ContextBuilderResult { ContextBuilderResult(success, nil) } + /// Create an "error" result with the provided `LDContext`. public static func fromError(_ error: ContextBuilderError) -> ContextBuilderResult { ContextBuilderResult(nil, error) } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index e7479ca8..b9b41921 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -1,11 +1,10 @@ import Foundation /** - 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. + LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. - 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. + The usage of LDUser is no longer recommended and is retained only to ease the adoption of the `LDContext` class. New + code using this SDK should make use of the `LDContextBuilder` to construct an equivalent `Kind.user` kind context. */ @objc (LDUser) public final class ObjcLDUser: NSObject { From ef1faca8f6d79a1657c543b2594334bbe5020d74 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Tue, 6 Dec 2022 12:19:28 -0600 Subject: [PATCH 80/90] Final updates for u2c release (#229) This commit addresses a few last standing items I noticed with the iOS SDK: 1. We had temporarily disabled the contract tests in CI until the published version supported client side tests. This has now been re-enabled. 2. I restored the LDUser and ObjcLDUser objects a few commits ago, but failed to add them back to the generated docs configuration. 3. I noticed the inline configuration option hadn't been removed (not sure how I missed that). --- .circleci/config.yml | 31 +++++++++---------- .jazzy.yaml | 2 ++ .../LaunchDarkly/Models/DiagnosticEvent.swift | 2 -- LaunchDarkly/LaunchDarkly/Models/Event.swift | 12 ++----- .../LaunchDarkly/Models/LDConfig.swift | 9 ------ .../ObjectiveC/ObjcLDConfig.swift | 8 ----- .../ServiceObjects/EventReporter.swift | 1 - .../Models/DiagnosticEventSpec.swift | 7 +---- .../LaunchDarklyTests/Models/EventSpec.swift | 22 ++++++------- .../Models/LDConfigSpec.swift | 5 --- 10 files changed, 29 insertions(+), 70 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b651837..66e49cbd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,22 +21,21 @@ jobs: - store_test_results: path: /tmp/contract-test-swiftlint-results.xml - # Disabled temporarily until v2 of the SDK test harness supports client side SDKs - # - run: - # name: Install required ssl libraries - # command: brew install libressl - # - run: - # name: make test output directory - # command: mkdir /tmp/test-results - # - run: make build-contract-tests - # - run: - # command: make start-contract-test-service - # background: true - # - run: - # name: run contract tests - # command: TEST_HARNESS_PARAMS="-junit /tmp/test-results/contract-tests-junit.xml" make run-contract-tests - # - store_test_results: - # path: /tmp/test-results/ + - run: + name: Install required ssl libraries + command: brew install libressl + - run: + name: make test output directory + command: mkdir /tmp/test-results + - run: make build-contract-tests + - run: + command: make start-contract-test-service + background: true + - run: + name: run contract tests + command: TEST_HARNESS_PARAMS="-junit /tmp/test-results/contract-tests-junit.xml" make run-contract-tests + - store_test_results: + path: /tmp/test-results/ build: parameters: diff --git a/.jazzy.yaml b/.jazzy.yaml index a9b0ba52..e055ff36 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -19,6 +19,7 @@ custom_categories: - LDConfig - LDContext - LDContextBuilder + - LDUser - Reference - LDMultiContextBuilder - LDEvaluationDetail @@ -51,6 +52,7 @@ custom_categories: - ObjcLDReference - ObjcLDContext - ObjcLDChangedFlag + - ObjcLDUser - ObjcLDValue - ObjcLDValueType diff --git a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift index 30270723..7b51e86f 100644 --- a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift +++ b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift @@ -101,7 +101,6 @@ struct DiagnosticConfig: Codable { let allAttributesPrivate: Bool let pollingIntervalMillis: Int let backgroundPollingIntervalMillis: Int - let inlineContextsInEvents: Bool let useReport: Bool let backgroundPollingDisabled: Bool let evaluationReasonsRequested: Bool @@ -121,7 +120,6 @@ struct DiagnosticConfig: Codable { allAttributesPrivate = config.allContextAttributesPrivate pollingIntervalMillis = Int(exactly: round(config.flagPollingInterval * 1_000)) ?? .max backgroundPollingIntervalMillis = Int(exactly: round(config.backgroundFlagPollingInterval * 1_000)) ?? .max - inlineContextsInEvents = config.inlineContextInEvents useReport = config.useReport backgroundPollingDisabled = !config.enableBackgroundUpdates evaluationReasonsRequested = config.evaluationReasons diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index 42c2f4ef..3b4fa17c 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -24,10 +24,6 @@ class Event: Encodable { self.kind = kind } - struct UserInfoKeys { - static let inlineContextInEvents = CodingUserInfoKey(rawValue: "LD_inlineContextInEvents")! - } - func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(kind.rawValue, forKey: .kind) @@ -59,11 +55,7 @@ class CustomEvent: Event, SubEvent { fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { var container = container try container.encode(key, forKey: .key) - if encoder.userInfo[Event.UserInfoKeys.inlineContextInEvents] as? Bool ?? false { - try container.encode(context, forKey: .context) - } else { - try container.encode(context.contextKeys(), forKey: .contextKeys) - } + try container.encode(context.contextKeys(), forKey: .contextKeys) if data != .null { try container.encode(data, forKey: .data) @@ -96,7 +88,7 @@ class FeatureEvent: Event, SubEvent { 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.inlineContextInEvents] as? Bool ?? false { + if kind == .debug { try container.encode(context, forKey: .context) } else { try container.encode(context.contextKeys(), forKey: .contextKeys) diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index d72abcb3..bbd40808 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -143,9 +143,6 @@ public struct LDConfig { /// 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 context data sent in events. When true the SDK will generate events using the full LDContext, excluding private attributes. When false the SDK will generate events using only `LDContext.contextKeys()`. (false) - static let inlineContextInEvents = false - /// The default setting controlling information logged to the console, and modifying some setting ranges to facilitate debugging. (false) static let debugMode = false @@ -293,11 +290,6 @@ public struct LDConfig { public var useReport: Bool = Defaults.useReport private static let flagRetryStatusCodes = [HTTPURLResponse.StatusCodes.methodNotAllowed, HTTPURLResponse.StatusCodes.badRequest, HTTPURLResponse.StatusCodes.notImplemented] - /** - Controls how the SDK reports the context in analytics event reports. When set to true, event reports will contain the context attributes, except attributes marked as private. When set to false, event reports will contain the context keys only, reducing the size of event reports. (Default: false) - */ - public var inlineContextInEvents: Bool = Defaults.inlineContextInEvents - /// Enables logging for debugging. (Default: false) public var isDebugMode: Bool = Defaults.debugMode @@ -437,7 +429,6 @@ extension LDConfig: Equatable { && lhs.allContextAttributesPrivate == rhs.allContextAttributesPrivate && Set(lhs.privateContextAttributes) == Set(rhs.privateContextAttributes) && lhs.useReport == rhs.useReport - && lhs.inlineContextInEvents == rhs.inlineContextInEvents && lhs.isDebugMode == rhs.isDebugMode && lhs.evaluationReasons == rhs.evaluationReasons && lhs.maxCachedContexts == rhs.maxCachedContexts diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index 860eacbf..7966535e 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -123,14 +123,6 @@ public final class ObjcLDConfig: NSObject { set { config.useReport = newValue } } - /** - Controls how the SDK reports the context[f in analytics event reports. When set to YES, event reports will contain the context attributes, except attributes marked as private. When set to NO, event reports will contain the context keys only, reducing the size of event reports. (Default: NO) - */ - @objc public var inlineContextInEvents: Bool { - get { config.inlineContextInEvents } - set { config.inlineContextInEvents = newValue } - } - /// Enables logging for debugging. (Default: NO) @objc public var debugMode: Bool { get { config.isDebugMode } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index 3736c813..73ea2104 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -131,7 +131,6 @@ class EventReporter: EventReporting { private func publish(_ events: [Event], _ payloadId: String, _ completion: CompletionClosure?) { let encodingConfig: [CodingUserInfoKey: Any] = [ - Event.UserInfoKeys.inlineContextInEvents: service.config.inlineContextInEvents, LDContext.UserInfoKeys.allAttributesPrivate: service.config.allContextAttributesPrivate, LDContext.UserInfoKeys.globalPrivateAttributes: service.config.privateContextAttributes.map { $0 } ] diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index bdd28998..2ebb4158 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -181,7 +181,6 @@ final class DiagnosticEventSpec: QuickSpec { customConfig.allContextAttributesPrivate = true customConfig.flagPollingInterval = 360.0 customConfig.backgroundFlagPollingInterval = 1_800.0 - customConfig.inlineContextInEvents = true customConfig.useReport = true customConfig.enableBackgroundUpdates = true customConfig.evaluationReasons = true @@ -212,7 +211,6 @@ final class DiagnosticEventSpec: QuickSpec { expect(diagnosticConfig.allAttributesPrivate) == false expect(diagnosticConfig.pollingIntervalMillis) == 300_000 expect(diagnosticConfig.backgroundPollingIntervalMillis) == 3_600_000 - expect(diagnosticConfig.inlineContextsInEvents) == false expect(diagnosticConfig.useReport) == false expect(diagnosticConfig.backgroundPollingDisabled) == true expect(diagnosticConfig.evaluationReasonsRequested) == false @@ -235,7 +233,6 @@ final class DiagnosticEventSpec: QuickSpec { expect(diagnosticConfig.allAttributesPrivate) == true expect(diagnosticConfig.pollingIntervalMillis) == 360_000 expect(diagnosticConfig.backgroundPollingIntervalMillis) == 1_800_000 - expect(diagnosticConfig.inlineContextsInEvents) == true expect(diagnosticConfig.useReport) == true expect(diagnosticConfig.backgroundPollingDisabled) == false expect(diagnosticConfig.evaluationReasonsRequested) == true @@ -270,7 +267,7 @@ final class DiagnosticEventSpec: QuickSpec { } it("encodes correct values to keys") { encodesToObject(diagnosticConfig) { decoded in - expect(decoded.count) == 18 + expect(decoded.count) == 17 expect(decoded["customBaseURI"]) == .bool(diagnosticConfig.customBaseURI) expect(decoded["customEventsURI"]) == .bool(diagnosticConfig.customEventsURI) expect(decoded["customStreamURI"]) == .bool(diagnosticConfig.customStreamURI) @@ -281,7 +278,6 @@ final class DiagnosticEventSpec: QuickSpec { expect(decoded["allAttributesPrivate"]) == .bool(diagnosticConfig.allAttributesPrivate) expect(decoded["pollingIntervalMillis"]) == .number(Double(diagnosticConfig.pollingIntervalMillis)) expect(decoded["backgroundPollingIntervalMillis"]) == .number(Double(diagnosticConfig.backgroundPollingIntervalMillis)) - expect(decoded["inlineContextsInEvents"]) == .bool(diagnosticConfig.inlineContextsInEvents) expect(decoded["useReport"]) == .bool(diagnosticConfig.useReport) expect(decoded["backgroundPollingDisabled"]) == .bool(diagnosticConfig.backgroundPollingDisabled) expect(decoded["evaluationReasonsRequested"]) == .bool(diagnosticConfig.evaluationReasonsRequested) @@ -302,7 +298,6 @@ final class DiagnosticEventSpec: QuickSpec { expect(decoded?.allAttributesPrivate) == diagnosticConfig.allAttributesPrivate expect(decoded?.pollingIntervalMillis) == diagnosticConfig.pollingIntervalMillis expect(decoded?.backgroundPollingIntervalMillis) == diagnosticConfig.backgroundPollingIntervalMillis - expect(decoded?.inlineContextsInEvents) == diagnosticConfig.inlineContextsInEvents expect(decoded?.useReport) == diagnosticConfig.useReport expect(decoded?.backgroundPollingDisabled) == diagnosticConfig.backgroundPollingDisabled expect(decoded?.evaluationReasonsRequested) == diagnosticConfig.evaluationReasonsRequested diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index f7e73fb7..e61700cf 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -95,12 +95,12 @@ final class EventSpec: XCTestCase { func testCustomEventEncodingInlining() { let context = LDContext.stub() let event = CustomEvent(key: "event-key", context: context, data: nil, metricValue: 2.5) - encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineContextInEvents: true]) { dict in + encodesToObject(event) { dict in XCTAssertEqual(dict.count, 5) XCTAssertEqual(dict["kind"], "custom") XCTAssertEqual(dict["key"], "event-key") XCTAssertEqual(dict["metricValue"], 2.5) - XCTAssertEqual(dict["context"], encodeToLDValue(context)) + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } @@ -194,14 +194,12 @@ final class EventSpec: XCTestCase { } } - func testFeatureEventEncodingInlinesContextForDebugOrConfig() { + func testFeatureEventEncodingInlinesContextForDebug() { let context = LDContext.stub() let featureFlag = FeatureFlag(flagKey: "flag-key", version: 3) - let featureEvent = FeatureEvent(key: "event-key", context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: false) let debugEvent = FeatureEvent(key: "event-key", context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true) - let encodedFeature = encodeToLDValue(featureEvent, userInfo: [Event.UserInfoKeys.inlineContextInEvents: true]) - let encodedDebug = encodeToLDValue(debugEvent, userInfo: [Event.UserInfoKeys.inlineContextInEvents: false]) - [encodedFeature, encodedDebug].forEach { valueIsObject($0) { dict in + let encodedDebug = encodeToLDValue(debugEvent) + [encodedDebug].forEach { valueIsObject($0) { dict in XCTAssertEqual(dict.count, 7) XCTAssertEqual(dict["key"], "event-key") XCTAssertEqual(dict["value"], true) @@ -213,15 +211,13 @@ final class EventSpec: XCTestCase { func testIdentifyEventEncoding() throws { let context = LDContext.stub() - for inlineContext in [true, false] { - let event = IdentifyEvent(context: context) - encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineContextInEvents: inlineContext]) { dict in - XCTAssertEqual(dict.count, 4) + let event = IdentifyEvent(context: context) + encodesToObject(event) { dict in + XCTAssertEqual(dict.count, 4) XCTAssertEqual(dict["kind"], "identify") XCTAssertEqual(dict["key"], .string(context.fullyQualifiedKey())) XCTAssertEqual(dict["context"], encodeToLDValue(context)) XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) - } } } @@ -249,7 +245,7 @@ final class EventSpec: XCTestCase { extension Event: Equatable { public static func == (_ lhs: Event, _ rhs: Event) -> Bool { - let config = [LDContext.UserInfoKeys.includePrivateAttributes: true, Event.UserInfoKeys.inlineContextInEvents: true] + let config = [LDContext.UserInfoKeys.includePrivateAttributes: true] return encodeToLDValue(lhs, userInfo: config) == encodeToLDValue(rhs, userInfo: config) } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index b5fc1464..360bb266 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -21,8 +21,6 @@ final class LDConfigSpec: XCTestCase { fileprivate static let useReport = true - fileprivate static let inlineContextInEvents = true - fileprivate static let debugMode = true fileprivate static let evaluationReasons = true fileprivate static let maxCachedContexts = -1 @@ -50,7 +48,6 @@ final class LDConfigSpec: XCTestCase { ("all context attributes private", Constants.allContextAttributesPrivate, { c, v in c.allContextAttributesPrivate = v as! Bool }), ("private context attributes", Constants.privateContextAttributes, { c, v in c.privateContextAttributes = (v as! [Reference])}), ("use report", Constants.useReport, { c, v in c.useReport = v as! Bool }), - ("inline context in events", Constants.inlineContextInEvents, { c, v in c.inlineContextInEvents = v as! Bool }), ("evaluation reasons", Constants.evaluationReasons, { c, v in c.evaluationReasons = v as! Bool }), ("max cached contexts", Constants.maxCachedContexts, { c, v in c.maxCachedContexts = v as! Int }), ("diagnostic opt out", Constants.diagnosticOptOut, { c, v in c.diagnosticOptOut = v as! Bool }), @@ -76,7 +73,6 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.allContextAttributesPrivate, LDConfig.Defaults.allContextAttributesPrivate) XCTAssertEqual(config.privateContextAttributes, LDConfig.Defaults.privateContextAttributes) XCTAssertEqual(config.useReport, LDConfig.Defaults.useReport) - XCTAssertEqual(config.inlineContextInEvents, LDConfig.Defaults.inlineContextInEvents) XCTAssertEqual(config.isDebugMode, LDConfig.Defaults.debugMode) XCTAssertEqual(config.evaluationReasons, LDConfig.Defaults.evaluationReasons) XCTAssertEqual(config.maxCachedContexts, LDConfig.Defaults.maxCachedContexts) @@ -110,7 +106,6 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.allContextAttributesPrivate, Constants.allContextAttributesPrivate, "\(os)") XCTAssertEqual(config.privateContextAttributes, Constants.privateContextAttributes, "\(os)") XCTAssertEqual(config.useReport, Constants.useReport, "\(os)") - XCTAssertEqual(config.inlineContextInEvents, Constants.inlineContextInEvents, "\(os)") XCTAssertEqual(config.isDebugMode, Constants.debugMode, "\(os)") XCTAssertEqual(config.evaluationReasons, Constants.evaluationReasons, "\(os)") XCTAssertEqual(config.maxCachedContexts, Constants.maxCachedContexts, "\(os)") From e52d9bff74f67b7b4d38ca5e85ceab1dfd4a5474 Mon Sep 17 00:00:00 2001 From: Louis Chan <91093020+louis-launchdarkly@users.noreply.github.com> Date: Tue, 28 Feb 2023 13:22:35 -0800 Subject: [PATCH 81/90] fix: Remove privateAttributes from the event payload (#230) The event payload based on our specifications should only have `redactedAttributes`, not `privateAttributes` in the payload. No private attribute values were sent even before this fix, only the field names were duplicated. Please see the Slack threads linked in the shortcut ticket for more details. --- LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift | 4 +++- .../Models/Context/LDContextCodableSpec.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 59a38254..2c00eddf 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -75,7 +75,9 @@ public struct LDContext: Encodable, Equatable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - if let privateAttributes = privateAttributes, !privateAttributes.isEmpty { + let includePrivateAttributes = encoder.userInfo[UserInfoKeys.includePrivateAttributes] as? Bool ?? false + + if let privateAttributes = privateAttributes, !privateAttributes.isEmpty, includePrivateAttributes { try container.encodeIfPresent(privateAttributes, forKey: .privateAttributes) } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift index f85b353d..f6a68e88 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift @@ -44,7 +44,9 @@ final class LDContextCodableSpec: XCTestCase { for json in testCases { let context = try JSONDecoder().decode(LDContext.self, from: Data(json.utf8)) - let output = try JSONEncoder().encode(context) + let jsonEncoder = JSONEncoder() + jsonEncoder.userInfo = [LDContext.UserInfoKeys.includePrivateAttributes:true] + let output = try jsonEncoder.encode(context) let outputJson = String(data: output, encoding: .utf8) XCTAssertEqual(json, outputJson) From 73bcda66b86d0c6eaec89772ce0eb30e10b2338c Mon Sep 17 00:00:00 2001 From: tanderson-ld <127344469+tanderson-ld@users.noreply.github.com> Date: Fri, 5 May 2023 12:31:54 -0500 Subject: [PATCH 82/90] fix: Updating trySet to allow kind to be changed. (#231) **Requirements** - [ ] I have added test coverage for new or changed functionality (Skipping this, that seems like a silly test to add) - [x] I have followed the repository's [pull request submission guidelines](../blob/v6/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** https://app.shortcut.com/launchdarkly/story/198156/fix-ios-builder-tryset-for-kind --- .circleci/config.yml | 9 ++++++++- LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 66e49cbd..cc1e6a42 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 jobs: contract-tests: macos: - xcode: '14.0.1' + xcode: '14.2.0' steps: - checkout @@ -59,6 +59,13 @@ jobs: steps: - checkout + # XCode11 has a bug related to respecting system known_hosts. The CircleCI checkout + # step is automatically setting all git URLs to use ssh. Since we do not have any + # private dependencies in this repo, it is ok to not us SSH for fetching the dependencies + - run: + name: Remove Git SSH restriction (XCode 11 bug workaround) + command: git config --global --remove-section url."ssh://git@github.com" + - run: name: Setup for builds command: | diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 2c00eddf..03fc6f9e 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -689,8 +689,8 @@ public struct LDContextBuilder { case ("", _): Log.debug(typeName(and: #function) + ": Provided attribute is empty. Ignoring.") return false - case ("kind", .string(kind)): - self.kind(kind) + case ("kind", .string(let val)): + self.kind(val) case ("kind", _): return false case ("key", .string(let val)): From eb47584ee4aa974d25ecb924711c623295097e02 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 5 Jun 2023 12:01:35 -0400 Subject: [PATCH 83/90] feat: Specify TLS v1.2 as required minimum (#232) TLS 1.1 has been deprecated for a long time and has been dropped by most major providers. The LaunchDarkly APIs haven't accepted anything lower than TLS 1.2 for a long time. We are therefore forcing TLS 1.2 as the minimum going forward in this library. --- LaunchDarkly.podspec | 2 +- LaunchDarkly.xcodeproj/project.pbxproj | 2 +- LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift | 7 +++++++ Package.swift | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 7718c05e..431aabe2 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', '3.0.0' + es.dependency 'LDSwiftEventSource', '3.1.0' end end diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 36e5015d..3c6a5f5c 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -1882,7 +1882,7 @@ repositoryURL = "https://github.com/LaunchDarkly/swift-eventsource.git"; requirement = { kind = exactVersion; - version = 3.0.0; + version = 3.1.0; }; }; B4903D9624BD61B200F087C4 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 2f9cfb39..846d37ad 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -67,6 +67,13 @@ final class DarklyService: DarklyServiceProvider { self.httpHeaders = HTTPHeaders(config: config, environmentReporter: serviceFactory.makeEnvironmentReporter()) // URLSessionConfiguration is a class, but `.default` creates a new instance. This does not effect other session configuration. let sessionConfig = URLSessionConfiguration.default + + if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + sessionConfig.tlsMinimumSupportedProtocolVersion = .TLSv12 + } else { + sessionConfig.tlsMinimumSupportedProtocol = .tlsProtocol12 + } + // We always revalidate the cache which we handle manually sessionConfig.requestCachePolicy = .reloadIgnoringLocalCacheData sessionConfig.urlCache = nil diff --git a/Package.swift b/Package.swift index fac7dbe4..77482d4b 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("3.0.0")) + .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("3.1.0")) ], targets: [ .target( From ab8058f93adf661ab9332682dd784ad2f855c08b Mon Sep 17 00:00:00 2001 From: Ember Stevens Date: Wed, 7 Jun 2023 21:16:39 -0700 Subject: [PATCH 84/90] Updates daily flag count --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2270d51..d4d555fb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ LaunchDarkly SDK for iOS LaunchDarkly overview ------------------------- -[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) From e0bffba39544e5681ebd1f9df97bc42d4f4148ce Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 12 Jun 2023 13:21:05 -0400 Subject: [PATCH 85/90] chore: Bump event source to 3.1.1 (#234) This doesn't actually change any behavior because we override the default retry behavior, and that is all that changed since 3.1.0. However, we do not want the SDK to drift too far from the eventsource implementation, so I try to update it each time. --- 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 a65cfd68..316b0464 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', '3.1.0' + es.dependency 'LDSwiftEventSource', '3.1.1' end end diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 1f9b19ad..aacf0360 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -1882,7 +1882,7 @@ repositoryURL = "https://github.com/LaunchDarkly/swift-eventsource.git"; requirement = { kind = exactVersion; - version = 3.1.0; + version = 3.1.1; }; }; B4903D9624BD61B200F087C4 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { diff --git a/Package.swift b/Package.swift index 77482d4b..8aab3b45 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("3.1.0")) + .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("3.1.1")) ], targets: [ .target( From ae553699990e8cb1854db94a97bd2eecd6b7e39c Mon Sep 17 00:00:00 2001 From: "ld-repository-standards[bot]" <113625520+ld-repository-standards[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 22:45:26 +0000 Subject: [PATCH 86/90] Add file CODEOWNERS --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..7d0dac3c --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Repository Maintainers +* @launchdarkly/team-sdk From 9c3989e89d250124280962d2b8e47d285804b4b8 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Fri, 21 Jul 2023 11:53:31 -0400 Subject: [PATCH 87/90] feat: Deprecate LDUser related functionality (#238) LDUser functionality has been replaced with the more flexible LDContext concept which all LaunchDarkly SDKs will support. We retained LDUser through the first major release to aide with the adoption transition, but as we near the next release, we need to alert customers that support will be ending. --- LaunchDarkly/LaunchDarkly/LDClient.swift | 3 +++ LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 3 +++ LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift | 1 + LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift | 4 ++++ LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 1 + 5 files changed, 12 insertions(+) diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 8bfd4492..d9a2505f 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -291,6 +291,7 @@ public class LDClient { This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context:completion:)` for details. */ + @available(*, deprecated, message: "Use identify(context:completion:)") public func identify(user: LDUser, completion: (() -> Void)? = nil) { switch user.toContext() { case .failure(let error): @@ -601,6 +602,7 @@ public class LDClient { This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(config:context:completion:)` for details. */ + @available(*, deprecated, message: "Use start(config:context:completion:)") public static func start(config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) { switch user?.toContext() { case nil: @@ -664,6 +666,7 @@ public class LDClient { This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(config:context:startWaitSeconds:completion:)` for details. */ + @available(*, deprecated, message: "Use start(config:context:startWithSeconds:completion:)") public static func start(config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { switch user?.toContext() { case nil: diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 7926b503..659fe07e 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -6,6 +6,7 @@ import Foundation The usage of LDUser is no longer recommended and is retained only to ease the adoption of the `LDContext` class. New code using this SDK should make use of the `LDContextBuilder` to construct an equivalent `Kind.user` kind context. */ +@available(*, deprecated, message: "Use LDContextBuilder to construct a context of kind 'user'") public struct LDUser: Encodable, Equatable { static let optionalAttributes = UserAttribute.BuiltIn.allBuiltIns.filter { $0.name != "key" && $0.name != "anonymous"} @@ -217,6 +218,7 @@ public struct LDUser: Encodable, Equatable { } /// Class providing ObjC interoperability with the LDUser struct +@available(*, deprecated) @objc final class LDUserWrapper: NSObject { let wrapped: LDUser @@ -226,4 +228,5 @@ public struct LDUser: Encodable, Equatable { } } +@available(*, deprecated) extension LDUser: TypeIdentifying { } diff --git a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift index d3095972..f59a3761 100644 --- a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift +++ b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift @@ -9,6 +9,7 @@ import Foundation reference guides [Setting user attributes](https://docs.launchdarkly.com/home/users/attributes) and [Targeting users](https://docs.launchdarkly.com/home/flags/targeting-users). */ +@available(*, deprecated) public class UserAttribute: Equatable, Hashable { /** diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index f1db29b0..cdcd9dd2 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -119,6 +119,7 @@ public final class ObjcLDClient: NSObject { This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context)` for details. */ + @available(*, deprecated, message: "Use identify(context)") @objc public func identify(user: ObjcLDUser) { ldClient.identify(user: user.user, completion: nil) } @@ -144,6 +145,7 @@ public final class ObjcLDClient: NSObject { This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context:completion:)` for details. */ + @available(*, deprecated, message: "Use identify(context:completion:)") @objc public func identify(user: ObjcLDUser, completion: (() -> Void)? = nil) { ldClient.identify(user: user.user, completion: completion) } @@ -577,6 +579,7 @@ public final class ObjcLDClient: NSObject { This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(configuration:context:completion:)` for details. */ + @available(*, deprecated, message: "Use start(configuration:context:completion:)") @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, completion: (() -> Void)? = nil) { LDClient.start(config: configuration.config, user: user.user, completion: completion) } @@ -598,6 +601,7 @@ public final class ObjcLDClient: NSObject { This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(configuration:context:startWaitSeconds:completion:)` for details. */ + @available(*, deprecated, message: "Use start(configuration:context:startWaitSeconds:completion:)") @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { LDClient.start(config: configuration.config, user: user.user, startWaitSeconds: startWaitSeconds, completion: completion) } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index b9b41921..7c2166ff 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -7,6 +7,7 @@ import Foundation code using this SDK should make use of the `LDContextBuilder` to construct an equivalent `Kind.user` kind context. */ @objc (LDUser) +@available(*, deprecated, message: "Use ObjcLDContextBuilder to construct a context of kind 'user'") public final class ObjcLDUser: NSObject { var user: LDUser From 1214b70162b778fb275c117f56f34b8ee11784f2 Mon Sep 17 00:00:00 2001 From: tanderson-ld <127344469+tanderson-ld@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:26:16 -0500 Subject: [PATCH 88/90] Ta/merging public v8 to private (#248) Merging public into private. --------- Co-authored-by: ld-repository-standards[bot] <113625520+ld-repository-standards[bot]@users.noreply.github.com> Co-authored-by: Kane Parkinson <93555788+kparkinson-ld@users.noreply.github.com> --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..10f1d1ac --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Reporting and Fixing Security Issues + +Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty. + +Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors. From a34a460be1417eccf84be0d82245353c78d8687e Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Fri, 8 Sep 2023 10:51:05 -0400 Subject: [PATCH 89/90] feat: Introduce new int-based LDValue.init method (#253) --- .circleci/config.yml | 20 ++------- LaunchDarkly/LaunchDarkly/LDCommon.swift | 15 +++++++ LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 45 ++++++++++++++----- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cc1e6a42..f0a20bc4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -163,21 +163,7 @@ workflows: xcode-version: '14.0.1' ios-sim: 'platform=iOS Simulator,name=iPhone 14,OS=16.0' - build: - name: Xcode 13.3 - Swift 5.6 - xcode-version: '13.3.1' - ios-sim: 'platform=iOS Simulator,name=iPhone 13,OS=15.4' - - build: - 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 - - build: - name: Xcode 12.5 - Swift 5.4 - xcode-version: '12.5.1' - ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=14.5' - - build: - name: Xcode 11.7 - Swift 5.2 - xcode-version: '11.7.0' - ios-sim: 'platform=iOS Simulator,name=iPhone 8,OS=12.4' + name: Xcode 13.4.1 - Swift 5.6 + xcode-version: '13.4.1' + ios-sim: 'platform=iOS Simulator,name=iPhone 13,OS=15.5' - contract-tests diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index 6b65a128..0c524848 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -93,10 +93,25 @@ public enum LDValue: Codable, self = .bool(booleanLiteral) } + /// Create an LDValue representation from the provided Double value. + /// + /// This method DOES NOT truncate the provided Double. As JSON numeric + /// values are always treated as double-precision, this method will + /// store the given Double as it. + @available(*, deprecated, message: "Use LDValue.init(integerLiteral: Int) or LDValue.init(floatLiteral: Double)") public init(integerLiteral: Double) { self = .number(integerLiteral) } + /// Create an LDValue representation from the provided Int. + /// + /// All JSON numeric types are represented as double-precision so the + /// provided Int will be cast to a Double. + public init(integerLiteral: Int) { + self = .number(Double(integerLiteral)) + } + + /// Create an LDValue representation from the provided Double. public init(floatLiteral: Double) { self = .number(floatLiteral) } diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 659fe07e..08a66126 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -13,7 +13,8 @@ public struct LDUser: Encodable, Equatable { 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 /// Client app defined name for the user. (Default: nil) public var name: String? @@ -29,15 +30,21 @@ 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, see `privateAttributes` for details. (Default: [:]) + /// 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: false) + /// 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: false) public var isAnonymous: Bool /** 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 most built-in attributes and all top level `custom` dictionary keys here. (Default: []]) + + 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] @@ -45,8 +52,13 @@ public struct LDUser: Encodable, Equatable { 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. + 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. - parameter name: Client app defined name for the user. (Default: nil) - parameter firstName: Client app defined first name for the user. (Default: nil) - parameter lastName: Client app defined last name for the user. (Default: nil) @@ -54,8 +66,11 @@ public struct LDUser: Encodable, Equatable { - parameter ipAddress: Client app defined ipAddress for the user. (Default: nil) - parameter email: Client app defined email address for the user. (Default: nil) - 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 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 privateAttributes: Client app defined privateAttributes for the user. (Default: nil) */ public init(key: String? = nil, @@ -113,7 +128,8 @@ public struct LDUser: Encodable, Equatable { /** Internal helper method to convert an LDUser to an LDContext. - Ideally we would do this as the LDUser was being built. However, the LDUser properties are publicly accessible, which makes that approach problematic. + Ideally we would do this as the LDUser was being built. However, the LDUser properties are publicly accessible, + which makes that approach problematic. */ internal func toContext() -> Result { var contextBuilder = LDContextBuilder(key: key) @@ -200,11 +216,16 @@ public struct LDUser: Encodable, Equatable { } } - /// 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 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 } From d27410de89e10fb8c7472c47a094fb70a3f870e0 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Fri, 8 Sep 2023 11:03:51 -0400 Subject: [PATCH 90/90] preparing release 8.3.0 --- CHANGELOG.md | 4 ++++ LaunchDarkly.podspec | 2 +- LaunchDarkly.xcodeproj/project.pbxproj | 20 +++++++++---------- .../ServiceObjects/EnvironmentReporter.swift | 2 +- README.md | 6 +++--- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01d96b22..4d1bb8ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [8.3.0] - 2023-09-08 +### Changed: +- Deprecated `LDValue.init(integerLiteral: Double)` as this method signature is misleading. A new `LDValue.init(integerLiteral: Int)` signature has been added for clarity. + ## [8.2.0] - 2023-08-02 ### Changed: - Deprecated LDUser and related functionality. Use LDContext instead. To learn more, read https://docs.launchdarkly.com/home/contexts. diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 57460160..6cfdf9d2 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |ld| ld.name = "LaunchDarkly" - ld.version = "8.2.0" + ld.version = "8.3.0" ld.summary = "iOS SDK for LaunchDarkly" ld.description = <<-DESC diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 2ba43d3e..9056bf1b 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -1503,7 +1503,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 8.3.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1526,7 +1526,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 8.3.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1549,7 +1549,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 8.3.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1570,7 +1570,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 8.3.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1614,7 +1614,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DYLIB_COMPATIBILITY_VERSION = 8.0.0; - DYLIB_CURRENT_VERSION = 8.2.0; + DYLIB_CURRENT_VERSION = 8.3.0; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_VERSION = E; @@ -1685,7 +1685,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DYLIB_COMPATIBILITY_VERSION = 8.0.0; - DYLIB_CURRENT_VERSION = 8.2.0; + DYLIB_CURRENT_VERSION = 8.3.0; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_VERSION = E; @@ -1724,7 +1724,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 = 8.2.0; + MARKETING_VERSION = 8.3.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1744,7 +1744,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 = 8.2.0; + MARKETING_VERSION = 8.3.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1786,7 +1786,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 8.3.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; @@ -1808,7 +1808,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 8.3.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 10f4cdf6..8eb903c4 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -102,7 +102,7 @@ struct EnvironmentReporter: EnvironmentReporting { #endif var shouldThrottleOnlineCalls: Bool { !isDebugBuild } - let sdkVersion = "8.2.0" + let sdkVersion = "8.3.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 ffbbd90b..5c3bede5 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: "8.2.0")) + .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor(from: "8.3.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', '~> 8.2' + pod 'LaunchDarkly', '~> 8.3' 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" ~> 8.2 +github "launchdarkly/ios-client-sdk" ~> 8.3 ``` ### Manual installation