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/.jazzy.yaml b/.jazzy.yaml index 4b73ab27..d9d3f5b1 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -19,6 +19,7 @@ custom_categories: - LDConfig - LDUser - LDEvaluationDetail + - LDValue - name: Flag Change Observers children: @@ -35,11 +36,11 @@ custom_categories: - name: Other Types children: + - UserAttribute - LDStreamingMode - - LDFlagValueConvertible - LDFlagKey - LDInvalidArgumentError - - LDErrorHandler + - RequestHeaderTransform - name: Objective-C Core Interfaces children: @@ -47,6 +48,8 @@ custom_categories: - ObjcLDConfig - ObjcLDUser - ObjcLDChangedFlag + - ObjcLDValue + - ObjcLDValueType - name: Objective-C EvaluationDetail Wrappers children: @@ -54,14 +57,4 @@ custom_categories: - ObjcLDIntegerEvaluationDetail - ObjcLDDoubleEvaluationDetail - ObjcLDStringEvaluationDetail - - ObjcLDArrayEvaluationDetail - - ObjcLDDictionaryEvaluationDetail - - - name: Objective-C ChangedFlag Wrappers - children: - - ObjcLDBoolChangedFlag - - ObjcLDIntegerChangedFlag - - ObjcLDDoubleChangedFlag - - ObjcLDStringChangedFlag - - ObjcLDArrayChangedFlag - - ObjcLDDictionaryChangedFlag + - ObjcLDJSONEvaluationDetail diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea33631..f46316c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,58 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [6.0.0] - 2022-05-04 + +Note that Objective-C bridging types are prefixed by `Objc`, but that prefix is not exposed to code written in Objective-C. For example, changes listed to `ObjcLDClient` are changes to the class referred to as `LDClient` from within Objective-C. + +### Added +- Added the `LDValue` class to represent any data type that is allowed in JSON. This new type is used to provide more type safety when representing complex or non-statically determined data types. The SDK also provides the bridge types `ObjcLDValue` and `ObjcLDValueType` for Objective-C interoperability. +- Added the `UserAttribute` class which provides a less error-prone way to refer to user attribute names in configuration. +- Added the type specific variation functions, `boolVariation`, `intVariation`, `doubleVariation`, `stringVariation`, and `jsonVariation`, to `LDClient`. +- Added the type specific detailed variation functions, `boolVariationDetail`, `intVariationDetail`, `doubleVariationDetail`, `stringVariationDetail`, and `jsonVariationDetail`, to `LDClient`. +- Added `jsonVariation` and `jsonVariationDetail` to `ObjcLDClient`. These functions allow evaluating feature flags where the provided `defaultValue` and the resulting variation can be any valid JSON data type. +- Added `ObjcLDJSONEvaluationDetail` for retrieving the detailed evaluation information of arbitrary type flag variations. +- Added `ObjcLDChangedFlagHandler` type alias for new non-type specific Objective-C flag observers. + +### Changed (API) +- `LDClient.track(key: data: metricValue:)` no longer `throws`. +- The type of the `data` parameter of `LDClient.track(key: data: metricValue:)` has changed from `Any?` to `LDValue?`. +- `ObjcLDClient.track(key: data:)` and `ObjcLDClient.track(key: data: metricValue:)` no longer `throws`. In Objective-C this change means that the `track` functions no longer accept a `error:` parameter. +- The type of the `data` parameter of `ObjcLDClient.track(key: data:)` and `ObjcLDClient.track(key: data: metricValue)` has changed from `Any?` to `ObjLDValue?`. In Objective-C this would be a change from `id _Nullable` to `LDValue * _Nullable`. +- `LDClient.allFlags` now has the type `[LDFlagKey: LDValue]?` rather than `[LDFlagKey: Any]?`. +- `ObjcLDClient.allFlags` now has the type `[String: ObjcLDValue]?` rather than `[String: Any]?`. In Objective-C this would be a change from `NSDictionary * _Nullable` to `NSDictionary * _Nullable`. +- The type of the `LDUser.custom` dictionary, and the corresponding `LDUser.init` parameter has been changed from `[String: Any]?` to `[String: LDValue]`. +- The type of the `ObjcLDUser.custom` property has been changed from `[String: Any]?` to `[String: ObjcLDValue]`. In Objective-C this would be a change from `NSDictionary * _Nullable` to `NSDictionary * _Nonnull`. +- The type of the `LDUser.privateAttributes` property, and the corresponding `LDUser.init` parameter, have been changed from `[String]?` to `[UserAttribute]`. +- The type of the `ObjcLDUser.privateAttributes` property has been changed from `[String]?` to `[String]`. In Objective-C this would be a change from `NSArray * _Nullable` to `NSArray * _Nonnull`. +- The types of the properties `LDChangedFlag.oldValue` and `LDChangedFlag.newValue` have been changed from `Any?` to `LDValue`. +- The type of the `LDConfig.privateUserAttributes` property has been changed from `[String]?` to `[UserAttribute]`. +- `ObjcLDConfig.privateUserAttributes` now has the non-optional type `[String]` rather than `[String]?`. In Objective-C this would be a change from `NSArray * _Nullable` to `NSArray * _Nonnull`. +- The type of the `LDEvaluationDetail.reason` property has been changed from `[String: Any]` to `[String: LDValue]`. +- The type of the `reason` property of `ObjcLDBoolEvaluationDetail`, `ObjcLDIntegerEvaluationDetail`, `ObjcLDDoubleEvaluationDetail`, and `ObjcLDStringEvaluationDetail` has been changed from `[String: Any]?` to `[String: ObjcLDValue]?`. In Objective-C this would be a change from `NSDictionary * _Nullable` to `NSDictionary * _Nullable`. + +### Changed (behavioral) +- The `Equatable` instance for `LDUser` has been changed to compare all user properties, rather than just the `key` property. +- Using `"custom"` as a private attribute name in `LDConfig.privateUserAttributes` or `LDUser.privateAttributes` will no longer set all `LDUser` custom attributes private. +- The automatically set `device` and `operatingSystem` custom attributes can now be set private. +- SDK versions from 4.0.0 and less than 6.0.0 supported migration of cached flag data from any SDK version of at least 2.3.3. The 6.0.0 release only supports migration of cached flag data from SDK versions of at least 4.0.0. + +### Removed +- Removed `LDClient.variation(forKey: defaultValue:)` and `LDClient.variationDetail(forKey: defaultValue:)` functions. Please use the new type-specific variation functions instead (e.g. `LDClient.boolVariation(forKey: defaultValue:)`). +- Removed the `LDFlagValueConvertible` protocol which was previously used to constrain the parameters and return types of the variation functions. +- `LDErrorHandler` and `LDClient.observeError(owner: handler:)` have been removed. Please use `ConnectionInformation` instead. +- Removed the `LDUser.init(userDictionary: )` and `ObjcLDUser.init(userDictionary: )` initializers, please use the explicit initializers instead. +- Removed `LDUser.CodingKeys`. To refer to user attributes, please use `UserAttribute` instead. +- Removed `LDUser.privatizableAttributes` and `ObjcLDUser.privatizableAttributes`. +- Removed `ObjcLDUser.attributeCustom`. +- The `LDUser.device` and `LDUser.operatingSystem` properties, and the corresponding `LDUser.init` parameters have been removed. These fields will be included in the `LDUser.custom` dictionary. +- The `ObjcLDUser.device` and `ObjcLDUser.operatingSystem` properties have been removed. These fields will be included in the `ObjcLDUser.custom` dictionary. +- The `ObjcLDClient` functions, `arrayVariation`, `arrayVariationDetail`, `dictionaryVariation`, and `dictionaryVariationDetail`, have been removed. Please use `ObjcLDClient.jsonVariation` and `ObjcLDClient.jsonVariationDetail` instead. +- The per-type instances of `ObjcLDChangedFlag` have been removed. Please use the base class `ObjcLDChangedFlag`, which now provides `oldValue` and `newValue` `ObjcLDValue` properties. The removed classes are `ObjcLDBoolChangedFlag`, `ObjcLDIntegerChangedFlag`, `ObjcLDDoubleChangedFlag`, `ObjcLDStringChangedFlag`, `ObjcLDArrayChangedFlag`, and `ObjcLDDictionaryChangedFlag`. +- The classes `ObjcLDArrayEvaluationDetail` and `ObjcLDDictionaryEvaluationDetail` have been removed. Please use `ObjcLDJSONEvaluationDetail` instead. +- The type aliases, `ObjcLDBoolChangedFlagHandler`, `ObjcLDIntegerChangedFlagHandler`, `ObjcLDDoubleChangedFlagHandler`, `ObjcLDStringChangedFlagHandler`, `ObjcLDArrayChangedFlagHandler`, and `ObjcLDDictionaryChangedFlagHandler`, have been removed. Please use `ObjcLDChangedFlagHandler` instead. +- The `ObjcLDClient` functions, `observeBool`, `observeInteger`, `observeDouble`, `observeString`, `observeArray`, and `observeDictionary`, have been removed. Please use the non-type specific `ObjcLDClient.observe(key: owner: handler:)` instead. + ## [5.4.5] - 2022-03-11 ### Fixed - Fixed race condition in `LDSwiftEventSource` that could cause a crash if the stream is explicitly stopped (such as when `identify` is called) while the stream is waiting to reconnect. diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 001a59fd..7b7bc322 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |ld| ld.name = "LaunchDarkly" - ld.version = "5.4.5" + ld.version = "6.0.0" ld.summary = "iOS SDK for LaunchDarkly" ld.description = <<-DESC diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index d43b8410..0d448bc2 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -7,6 +7,18 @@ 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 */; }; + 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 */; }; + 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 */; }; @@ -17,9 +29,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 */; }; - 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 */; }; @@ -38,11 +47,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 */; }; @@ -67,9 +73,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 */; }; - 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 */; }; @@ -86,11 +89,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 */; }; @@ -99,22 +99,12 @@ 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 */; }; 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 */; }; - 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 */; }; @@ -122,21 +112,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 */; }; @@ -151,28 +131,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 */; }; - 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 */; }; - 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 */; }; @@ -191,28 +155,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 */; }; - 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 */; }; - 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 */; }; - 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 */; }; @@ -229,18 +176,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 */; }; @@ -249,16 +192,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 */; }; @@ -271,6 +211,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 */; }; @@ -304,7 +248,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 */ @@ -371,6 +314,9 @@ /* 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 = ""; }; 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersSpec.swift; sourceTree = ""; }; 830DB3AD2239B54900D65D25 /* URLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponse.swift; sourceTree = ""; }; @@ -389,23 +335,15 @@ 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 = ""; }; - 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 = ""; }; - 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; }; 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; }; @@ -421,17 +359,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 = ""; }; - 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 = ""; }; - 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 = ""; }; @@ -448,36 +379,25 @@ 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 = ""; }; - 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 = ""; }; 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 = ""; }; @@ -487,7 +407,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,7 +481,6 @@ children = ( B46F344025E6DB7D0078D45F /* DiagnosticReporterSpec.swift */, 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */, - 833631CA221B5DFA00BA53EE /* ErrorNotifierSpec.swift */, 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */, 83B8C2441FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift */, 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */, @@ -595,36 +513,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 */, - 83D1522F224D92D30054B6D4 /* DeprecatedCacheModelV4.swift */, - 83D1522A224D91B90054B6D4 /* DeprecatedCacheModelV3.swift */, - 832D68A6224A4668005F052A /* DeprecatedCacheModelV2.swift */, B4C9D4322489C8FD004A9B03 /* DiagnosticCache.swift */, ); path = Cache; @@ -633,14 +528,8 @@ 8354AC75224316C700CDE602 /* Cache */ = { isa = PBXGroup; children = ( - 83D5597D1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift */, - 8354AC76224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift */, + 8354AC76224316F800CDE602 /* FeatureFlagCacheSpec.swift */, 832D68AB224B3321005F052A /* CacheConverterSpec.swift */, - B43D5ACF25FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift */, - 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */, - 83D15238225455D40054B6D4 /* DeprecatedCacheModelV4Spec.swift */, - 83D15236225400CE0054B6D4 /* DeprecatedCacheModelV3Spec.swift */, - 83D15234225299890054B6D4 /* DeprecatedCacheModelV2Spec.swift */, B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */, ); path = Cache; @@ -674,6 +563,8 @@ children = ( 83B6C4B51F4DE7630055351C /* LDCommon.swift */, 8354EFDC1F26380700C05156 /* LDClient.swift */, + B495A8A12787762C0051977C /* LDClientVariation.swift */, + 29FE1297280413D4008CC918 /* Util.swift */, 8354EFE61F263E4200C05156 /* Models */, 83FEF8D91F2666BF001CF12C /* ServiceObjects */, 831D8B701F71D3A600ED65E8 /* Networking */, @@ -705,14 +596,13 @@ 8354EFE61F263E4200C05156 /* Models */ = { isa = PBXGroup; children = ( - 8354AC5F224150C300CDE602 /* Cache */, C408884823033B7500420721 /* ConnectionInformation.swift */, B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */, - 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */, 8354EFDE1F26380700C05156 /* Event.swift */, 83EBCB9D20D9A0A1003A7142 /* FeatureFlag */, 8354EFDD1F26380700C05156 /* LDConfig.swift */, 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */, + 29A4C47427DA6266005B8D34 /* UserAttribute.swift */, ); path = Models; sourceTree = ""; @@ -725,6 +615,7 @@ 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */, 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */, B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */, + 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */, ); path = ObjectiveC; sourceTree = ""; @@ -748,7 +639,6 @@ 838F96791FBA551A009CFC45 /* ClientServiceMockFactory.swift */, 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */, 831425AE206ABB5300F2EF36 /* EnvironmentReportingMock.swift */, - C48ED690242D27E200464F5F /* DeprecatedCacheMock.swift */, ); path = Mocks; sourceTree = ""; @@ -756,8 +646,6 @@ 83D17EA81FCDA16300B2823C /* Extensions */ = { isa = PBXGroup; children = ( - 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */, - 832EA060203D03B700A93C0E /* AnyComparerSpec.swift */, 83B6E3F0222EFA3800FF2A6A /* ThreadSpec.swift */, ); path = Extensions; @@ -766,11 +654,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 */, ); @@ -782,7 +667,6 @@ children = ( 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */, 83EBCB9F20D9A143003A7142 /* FlagChange */, - 83EBCBA020D9A168003A7142 /* FlagValue */, C43C37E0236BA050003C1624 /* LDEvaluationDetail.swift */, 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */, ); @@ -800,16 +684,6 @@ path = FlagChange; sourceTree = ""; }; - 83EBCBA020D9A168003A7142 /* FlagValue */ = { - isa = PBXGroup; - children = ( - 838838401F5EFADF0023D11B /* LDFlagValue.swift */, - 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */, - 838838421F5EFB9C0023D11B /* LDFlagBaseTypeConvertible.swift */, - ); - path = FlagValue; - sourceTree = ""; - }; 83EBCBA620D9A23E003A7142 /* User */ = { isa = PBXGroup; children = ( @@ -852,8 +726,6 @@ 83EBCBA620D9A23E003A7142 /* User */, 83EBCBA720D9A251003A7142 /* FeatureFlag */, 83EF67921F9945E800403126 /* EventSpec.swift */, - 83883DDE220B6D4B00EEAB95 /* ErrorObserverSpec.swift */, - 8354AC672241586D00CDE602 /* Cache */, B4F689132497B2FC004D3CE0 /* DiagnosticEventSpec.swift */, ); path = Models; @@ -867,7 +739,6 @@ 83B1D7C82073F354006D1B1C /* CwlSysctl.swift */, B4C9D4372489E20A004A9B03 /* DiagnosticReporter.swift */, 831425B0206B030100F2EF36 /* EnvironmentReporter.swift */, - 83883DD9220B6A9A00EEAB95 /* ErrorNotifier.swift */, 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */, 8358F25F1F476AD800ECE1AF /* FlagChangeNotifier.swift */, 831D8B731F72994600ED65E8 /* FlagStore.swift */, @@ -1161,7 +1032,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; @@ -1174,7 +1045,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; @@ -1191,7 +1062,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; @@ -1204,7 +1075,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; @@ -1217,7 +1088,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 */ @@ -1228,62 +1099,51 @@ files = ( 83906A7B21190B7700D7D3C5 /* DateFormatter.swift in Sources */, 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 */, 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 */, - 83D1522E224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, C443A40823145FEE00145710 /* ConnectionInformationStore.swift in Sources */, - 831188662113AE4A00D77CB5 /* AnyComparer.swift in Sources */, - 831188492113ADD400D77CB5 /* LDFlagBaseTypeConvertible.swift in Sources */, + 29F9D1A12812E005008D12C0 /* ObjcLDValue.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 */, 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 */, 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 */, - 83D15233224D92D30054B6D4 /* DeprecatedCacheModelV4.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 */, 831188622113AE3A00D77CB5 /* Data.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1292,23 +1152,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 */, - 83D15232224D92D30054B6D4 /* DeprecatedCacheModelV4.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 */, 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 */, @@ -1321,29 +1175,25 @@ 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 */, 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 */, + 29A4C47727DA6266005B8D34 /* UserAttribute.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 */, + 29F9D1A02812E005008D12C0 /* ObjcLDValue.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 */, @@ -1351,7 +1201,6 @@ 831EF36820655E730001C643 /* ObjcLDUser.swift in Sources */, B4C9D43A2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, 831EF36A20655E730001C643 /* ObjcLDChangedFlag.swift in Sources */, - 83D1522D224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1361,12 +1210,9 @@ files = ( 831D8B6F1F71532300ED65E8 /* HTTPHeaders.swift in Sources */, 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 */, @@ -1377,46 +1223,38 @@ 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 */, + 29F9D19E2812E005008D12C0 /* ObjcLDValue.swift in Sources */, 8358F2601F476AD800ECE1AF /* FlagChangeNotifier.swift in Sources */, - 838838451F5EFBAF0023D11B /* LDFlagValueConvertible.swift in Sources */, - 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 */, + 29A4C47527DA6266005B8D34 /* UserAttribute.swift in Sources */, 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 */, - 83D15230224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */, 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */, B4C9D42E2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 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; @@ -1426,53 +1264,40 @@ 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 */, 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 */, 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 */, - 832EA061203D03B700A93C0E /* AnyComparerSpec.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 */, - 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; @@ -1483,16 +1308,10 @@ files = ( 83D9EC752062DEAB004D7FA6 /* LDCommon.swift in Sources */, 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 */, - 83D9EC7B2062DEAB004D7FA6 /* LDFlagBaseTypeConvertible.swift in Sources */, 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */, 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */, @@ -1500,45 +1319,40 @@ 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 */, + 29F9D19F2812E005008D12C0 /* ObjcLDValue.swift in Sources */, 83D9EC8B2062DEAB004D7FA6 /* Log.swift in Sources */, - 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 */, + 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 */, - 83D15231224D92D30054B6D4 /* DeprecatedCacheModelV4.swift in Sources */, 83D9EC982062DEAB004D7FA6 /* ObjcLDClient.swift in Sources */, 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 */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1583,7 +1397,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1606,7 +1420,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1629,7 +1443,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1650,7 +1464,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1693,11 +1507,11 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DYLIB_COMPATIBILITY_VERSION = 5.4.0; - DYLIB_CURRENT_VERSION = 5.4.5; + DYLIB_COMPATIBILITY_VERSION = 6.0.0; + DYLIB_CURRENT_VERSION = 6.0.0; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - FRAMEWORK_VERSION = B; + FRAMEWORK_VERSION = C; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1764,11 +1578,11 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DYLIB_COMPATIBILITY_VERSION = 5.4.0; - DYLIB_CURRENT_VERSION = 5.4.5; + DYLIB_COMPATIBILITY_VERSION = 6.0.0; + DYLIB_CURRENT_VERSION = 6.0.0; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - FRAMEWORK_VERSION = B; + FRAMEWORK_VERSION = C; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -1804,7 +1618,7 @@ INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1824,7 +1638,7 @@ INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1866,7 +1680,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; @@ -1888,7 +1702,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.4.5; + MARKETING_VERSION = 6.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index 7a9069f3..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,215 +111,184 @@ 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?() } } } -// 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 { 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 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 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?() } } @@ -327,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?() } } @@ -392,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?() } } @@ -425,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?() } } } @@ -456,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 ac863ba1..00000000 --- a/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// Any.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - -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 506a922d..9ca0b4c7 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 { @@ -13,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/Date.swift b/LaunchDarkly/LaunchDarkly/Extensions/Date.swift index b1e84ac7..238f81bd 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 { @@ -17,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/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 deleted file mode 100644 index 5ff12c8c..00000000 --- a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// Dictionary.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - -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() - } - - var base64UrlEncodedString: String? { - jsonData?.base64UrlEncodedString - } -} - -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 a1289e1b..00000000 --- a/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// JSONSerialization.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - -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/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 e26ce92d..dab69a55 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 @@ -26,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 { @@ -125,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 { @@ -277,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) @@ -303,13 +294,8 @@ public class LDClient { let wasOnline = self.isOnline 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 { - // Deprecated behavior of setting flags from user init dictionary - flagStore.replaceStore(newFlags: user.flagStore?.featureFlags ?? [:], 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), @@ -319,7 +305,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) @@ -332,184 +318,11 @@ 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. - - 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. - */ - public var allFlags: [LDFlagKey: Any]? { + /// 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 } - return flagStore.featureFlags.allFlagValues + return flagStore.featureFlags.compactMapValues { $0.value } } // MARK: Observing Updates @@ -521,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. @@ -549,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. @@ -579,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. @@ -613,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. @@ -638,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. @@ -662,36 +469,26 @@ 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) { 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() @@ -706,14 +503,11 @@ 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, 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) } @@ -722,29 +516,27 @@ 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) - - - 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 = 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) } @@ -769,7 +561,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)) } /** @@ -787,13 +579,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") } } @@ -829,7 +620,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() @@ -845,7 +639,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() @@ -879,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) } } } @@ -893,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? { @@ -908,7 +699,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 @@ -933,13 +723,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() - 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() throttler = self.serviceFactory.makeThrottler(environmentReporter: environmentReporter) @@ -949,7 +734,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), @@ -973,12 +757,11 @@ 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(Event.identifyEvent(user: user)) + eventReporter.record(IdentifyEvent(user: user)) self.connectionInformation = ConnectionInformation.uncacheConnectionInformation(config: config, ldClient: self, clientServiceFactory: self.serviceFactory) internalSetOnline(configuration.startOnline) { @@ -990,16 +773,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..42c55aff --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -0,0 +1,207 @@ +import Foundation + +extension LDClient { + // MARK: Flag variation methods + + /** + 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) + } + + 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 { + 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 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 + } + + 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 + } + + func toLDValue() -> LDValue { + return .number(Double(self)) + } +} + +extension Double: LDValueConvertible { + init?(fromLDValue value: LDValue) { + guard case .number(let value) = value + else { return nil } + self = value + } + + func toLDValue() -> LDValue { + return .number(self) + } +} + +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 094284f1..1cfcefb1 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. @@ -20,18 +13,156 @@ 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) { self.localizedDescription = description } } + +struct DynamicKey: CodingKey { + let intValue: Int? = nil + let stringValue: String + + init?(intValue: Int) { + return nil + } + + init?(stringValue: String) { + self.stringValue = stringValue + } +} + +/** + 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, + 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 + + /// 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: ()) { + 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)!) } + } + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift deleted file mode 100644 index 0f924177..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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 -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 56cb65f9..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// 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 -// 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/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..a0a76f87 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -1,23 +1,13 @@ -// -// Event.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - 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 { +class Event: Encodable { enum CodingKeys: String, CodingKey { - case key, previousKey, kind, creationDate, user, userKey, - value, defaultValue = "default", variation, version, - data, endDate, 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 { @@ -26,156 +16,166 @@ struct Event { 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: Any? - let defaultValue: Any? - let featureFlag: FeatureFlag? - let data: Any? - 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: Any? = nil, - defaultValue: Any? = nil, - featureFlag: FeatureFlag? = nil, - data: Any? = nil, - flagRequestTracker: FlagRequestTracker? = nil, - endDate: Date? = nil, - includeReason: Bool = false, - metricValue: Double? = nil) { + + fileprivate init(kind: Kind) { self.kind = kind + } + + struct UserInfoKeys { + static let inlineUserInEvents = CodingUserInfoKey(rawValue: "LD_inlineUserInEvents")! + } + + 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) + } + } +} + +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.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 + self.creationDate = creationDate + super.init(kind: .alias) } - // 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))") - return Event(kind: .feature, key: key, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason) + 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) } +} - // 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))") - return Event(kind: .debug, key: key, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason) - } +class CustomEvent: Event, SubEvent { + let key: String + let user: LDUser + let data: LDValue + let metricValue: Double? + let creationDate: Date - 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") - } - } - return Event(kind: .custom, key: key, user: user, data: data, metricValue: metricValue) + 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) } - static func identifyEvent(user: LDUser) -> Event { - Log.debug(typeName(and: #function) + "key: " + user.key) - return Event(kind: .identify, key: user.key, user: 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.encode(user.key, forKey: .userKey) + } + if user.isAnonymous == true { + try container.encode("anonymousUser", forKey: .contextKind) + } + if data != .null { + try container.encode(data, forKey: .data) + } + try container.encodeIfPresent(metricValue, forKey: .metricValue) + try container.encode(creationDate, forKey: .creationDate) } +} - 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) - } +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 - 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)) + 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) } - 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) + 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 { - eventDictionary[CodingKeys.userKey.rawValue] = user?.key + try container.encode(user.key, forKey: .userKey) } - if kind.isAlwaysIncludeValueKinds { - eventDictionary[CodingKeys.value.rawValue] = value ?? NSNull() - eventDictionary[CodingKeys.defaultValue.rawValue] = defaultValue ?? NSNull() + if kind == .feature && user.isAnonymous == true { + try container.encode("anonymousUser", forKey: .contextKind) } - 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 - 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.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(reason, forKey: .reason) } - eventDictionary[CodingKeys.endDate.rawValue] = endDate?.millisSince1970 - eventDictionary[CodingKeys.reason.rawValue] = includeReason || featureFlag?.trackReason ?? false ? featureFlag?.reason : nil - eventDictionary[CodingKeys.metricValue.rawValue] = metricValue + try container.encode(creationDate, forKey: .creationDate) + } +} - if kind.needsContextKind && (user?.isAnonymous == true) { - eventDictionary[CodingKeys.contextKind.rawValue] = "anonymousUser" - } +class IdentifyEvent: Event, SubEvent { + let user: LDUser + let creationDate: Date - if kind == .alias { - eventDictionary[CodingKeys.contextKind.rawValue] = self.contextKind - eventDictionary[CodingKeys.previousContextKind.rawValue] = self.previousContextKind - } + init(user: LDUser, creationDate: Date = Date()) { + self.user = user + self.creationDate = creationDate + super.init(kind: .identify) + } - return eventDictionary + 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) } } -extension Array where Element == [String: Any] { - var jsonData: Data? { - guard JSONSerialization.isValidJSONObject(self) - else { return nil } - return try? JSONSerialization.data(withJSONObject: self, options: []) +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) } -} -extension Event: TypeIdentifying { } + 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/FeatureFlag/FeatureFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift index cf1f07b8..b2357de7 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift @@ -1,43 +1,36 @@ -// -// FeatureFlag.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation -struct FeatureFlag { +struct FeatureFlag: Codable { enum CodingKeys: String, CodingKey, CaseIterable { case flagKey = "key", value, variation, version, flagVersion, trackEvents, debugEventsUntilDate, reason, trackReason } 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? /// 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 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? = nil, + trackEvents: Bool = false, debugEventsUntilDate: Date? = nil, - reason: [String: Any]? = nil, - trackReason: Bool? = 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 @@ -47,110 +40,67 @@ struct FeatureFlag { 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, - debugEventsUntilDate: Date(millisSince1970: dictionary.debugEventsUntilDate), - reason: dictionary.reason, - trackReason: dictionary.trackReason) + 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) } - 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 ?? NSNull() - dictionaryValue[CodingKeys.debugEventsUntilDate.rawValue] = debugEventsUntilDate?.millisSince1970 ?? NSNull() - dictionaryValue[CodingKeys.reason.rawValue] = reason ?? NSNull() - dictionaryValue[CodingKeys.trackReason.rawValue] = trackReason ?? NSNull() - return dictionaryValue + 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)) ?? .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([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) + 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) + 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(reason, forKey: .reason) } + if trackReason { try container.encode(true, forKey: .trackReason) } } 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) } } -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 - } +struct FeatureFlagCollection: Codable { + let flags: [LDFlagKey: FeatureFlag] - var version: Int? { - self[FeatureFlag.CodingKeys.version.rawValue] as? Int + init(_ flags: [LDFlagKey: FeatureFlag]) { + self.flags = flags } - 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 + 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 } - 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 + func encode(to encoder: Encoder) throws { + try flags.encode(to: encoder) } } 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..b029bb1e 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift @@ -1,24 +1,21 @@ -// -// LDChangedFlag.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - 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 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. */ 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/Models/FeatureFlag/FlagRequestTracker.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift index 191da4a7..909696c8 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift @@ -1,21 +1,10 @@ -// -// FlagRequestTracker.swift -// LaunchDarkly -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation struct FlagRequestTracker { - enum CodingKeys: String, CodingKey { - case startDate, features - } - 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() } @@ -24,15 +13,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") - } - - var dictionaryValue: [String: Any] { - [CodingKeys.startDate.rawValue: startDate.millisSince1970, - CodingKeys.features.rawValue: flagCounters.mapValues { $0.dictionaryValue }] + + "\n\tdefaultValue: \(defaultValue)\n") } var hasLoggedRequests: Bool { !flagCounters.isEmpty } @@ -40,15 +24,19 @@ struct FlagRequestTracker { 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: Any? - var flagValueCounters: [CounterKey: CounterValue] = [:] + private(set) var defaultValue: LDValue = .null + private(set) 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] { @@ -58,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 ?? 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 ?? NSNull(), - CodingKeys.counters.rawValue: counters] } } @@ -81,10 +69,10 @@ struct CounterKey: Equatable, Hashable { } class CounterValue { - let value: Any? - var count: Int = 1 + let value: LDValue + private(set) var count: Int = 1 - init(value: Any?) { + init(value: LDValue) { self.value = value } 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 - } -} diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift deleted file mode 100644 index 138858c2..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// 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. -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 fb778d45..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// 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. -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 749a971d..4b5b46a9 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 /** @@ -13,13 +6,13 @@ 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: Any]? + public let 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/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index ad7715f6..1b47e295 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 @@ -67,9 +68,9 @@ 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 `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 @@ -204,14 +209,17 @@ 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: [String]? = Defaults.privateUserAttributes + public var privateUserAttributes: [UserAttribute] = 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] @@ -360,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 c501fe51..681df574 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -1,40 +1,22 @@ -// -// LDUser.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// 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 { +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 - } - - /** - 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 + [CodingKeys.custom.rawValue] } - - 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" @@ -56,27 +38,20 @@ public struct LDUser { 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) - public var custom: [String: Any]? + /// 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 - /// 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. 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: [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. - public var objcLdUser: ObjcLDUser { ObjcLDUser(self) } + public var privateAttributes: [UserAttribute] - internal var flagStore: FlagMaintaining? + 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. @@ -90,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) */ @@ -103,11 +76,9 @@ public struct LDUser { ipAddress: String? = nil, email: String? = nil, avatar: String? = nil, - custom: [String: Any]? = nil, + custom: [String: LDValue]? = nil, isAnonymous: Bool? = nil, - device: String? = nil, - operatingSystem: String? = nil, - privateAttributes: [String]? = nil, + privateAttributes: [UserAttribute]? = nil, secondary: String? = nil) { let environmentReporter = EnvironmentReporter() let selectedKey = key ?? LDUser.defaultKey(environmentReporter: environmentReporter) @@ -120,37 +91,11 @@ 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.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`. - - 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 - 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 - - flagStore = FlagStore(featureFlagDictionary: userDictionary[CodingKeys.config.rawValue] as? [String: Any]) + 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)") } @@ -158,74 +103,67 @@ 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: ["device": .string(environmentReporter.deviceModel), + "os": .string(environmentReporter.systemVersion)], + 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 device - case CodingKeys.operatingSystem.rawValue: return operatingSystem - case CodingKeys.config.rawValue: return flagStore?.featureFlags - case CodingKeys.privateAttributes.rawValue: return privateAttributes - default: return nil + private func value(for attribute: UserAttribute) -> Any? { + if let builtInGetter = attribute.builtInGetter { + return builtInGetter(self) } + return custom[attribute.name] } - /// Returns the custom dictionary without the SDK set device and operatingSystem attributes - var customWithoutSdkSetAttributes: [String: Any] { - custom?.filter { key, _ in !LDUser.sdkSetAttributes.contains(key) } ?? [:] + + struct UserInfoKeys { + static let includePrivateAttributes = CodingUserInfoKey(rawValue: "LD_includePrivateAttributes")! + static let allAttributesPrivate = CodingUserInfoKey(rawValue: "LD_allAttributesPrivate")! + static let globalPrivateAttributes = CodingUserInfoKey(rawValue: "LD_globalPrivateAttributes")! } - /// 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 ?? []) + 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] ?? [] - dictionary[CodingKeys.key.rawValue] = key - dictionary[CodingKeys.isAnonymous.rawValue] = isAnonymous + let allPrivate = !includePrivateAttributes && allAttributesPrivate + let privateAttributeNames = includePrivateAttributes ? [] : (privateAttributes.map { $0.name } + globalPrivateAttributes) - 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 + var redactedAttributes: [String] = [] + + 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")!) + } + + 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 customDictionary = [String: Any]() - customWithoutSdkSetAttributes.forEach { attrName, attrVal in - if !includePrivate && combinedPrivateAttributes.contains(where: [CodingKeys.custom.rawValue, attrName].contains ) { + var nestedContainer: KeyedEncodingContainer? + try custom.forEach { attrName, attrVal in + if allPrivate || privateAttributeNames.contains(attrName) { redactedAttributes.append(attrName) } else { - customDictionary[attrName] = attrVal + if nestedContainer == nil { + nestedContainer = container.nestedContainer(keyedBy: DynamicKey.self, forKey: DynamicKey(stringValue: "custom")!) + } + try nestedContainer!.encode(attrVal, forKey: DynamicKey(stringValue: attrName)!) } } - 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 { + try container.encode(Set(redactedAttributes).sorted(), forKey: DynamicKey(stringValue: "privateAttrs")!) } - - 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.) @@ -245,13 +183,6 @@ 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 - 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 @@ -262,82 +193,4 @@ extension LDUser: Equatable { } } -extension LDUserWrapper: NSCoding { - 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) - encoder.encode([Keys.featureFlags: wrapped.flagStore?.featureFlags.dictionaryValue.withNullValuesRemoved], forKey: LDUser.CodingKeys.config.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 - 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) - } - - /// 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 { } - -#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 - && secondary == otherUser.secondary - && name == otherUser.name - && firstName == otherUser.firstName - && lastName == otherUser.lastName - && country == otherUser.country - && ipAddress == otherUser.ipAddress - && email == otherUser.email - && avatar == otherUser.avatar - && AnyComparer.isEqual(custom, to: otherUser.custom) - && isAnonymous == otherUser.isAnonymous - && device == otherUser.device - && operatingSystem == otherUser.operatingSystem - && privateAttributes == otherUser.privateAttributes - } - } -#endif 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/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 31048af9..7cc30e85 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 @@ -27,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?) } @@ -88,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 @@ -98,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 @@ -152,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 @@ -176,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/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..1af8618b 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 /** @@ -14,171 +7,16 @@ import Foundation */ @objc(LDChangedFlag) public class ObjcLDChangedFlag: NSObject { - fileprivate let changedFlag: LDChangedFlag - fileprivate var sourceValue: Any? { - changedFlag.oldValue ?? changedFlag.newValue - } - /// 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 as? Bool) ?? false - } - /// The changed flag's value after it changed - @objc public var newValue: Bool { - (changedFlag.newValue as? Bool) ?? false - } - - 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 as? Int) ?? 0 - } - /// The changed flag's value after it changed - @objc public var newValue: Int { - (changedFlag.newValue 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 as? Double) ?? 0.0 - } - /// The changed flag's value after it changed - @objc public var newValue: Double { - (changedFlag.newValue as? Double) ?? 0.0 - } - - 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 as? String) - } - /// The changed flag's value after it changed - @objc public var newValue: String? { - (changedFlag.newValue as? String) - } - - 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 as? [Any] - } - /// The changed flag's value after it changed - @objc public var newValue: [Any]? { - changedFlag.newValue 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 as? [String: Any] - } - /// The changed flag's value after it changed - @objc public var newValue: [String: Any]? { - changedFlag.newValue 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 ?? newValue - 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 f8a9a8c2..b9dfcfc9 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 /** @@ -14,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. @@ -22,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) @@ -173,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. @@ -195,7 +182,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) } /** @@ -207,20 +194,16 @@ 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 { 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. @@ -236,7 +219,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) } /** @@ -248,27 +231,23 @@ 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 { 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. @@ -277,7 +256,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) } /** @@ -289,133 +268,84 @@ 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 { 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, which may be nil. - - 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. + 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. - 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. 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? { - ldClient.variation(forKey: key, defaultValue: defaultValue) + @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. 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 { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDStringEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) + @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 { 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, which may be nil.. - - 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. + 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. - 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. 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 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. - 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. + - 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) + @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 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.. - - 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. The default value may be nil. - - - 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 - */ - /// - 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. The default value may be nil. - - - 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) - } - /** Returns a dictionary with the flag keys and their values. If the LDClient is not started, returns nil. @@ -423,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 } + @objc public var allFlags: [LDFlagKey: ObjcLDValue]? { ldClient.allFlags?.mapValues { ObjcLDValue(wrappedValue: $0) } } // MARK: - Feature Flag Updates @@ -437,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. @@ -593,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. @@ -608,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) }) } } @@ -625,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) }) } } @@ -658,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. @@ -686,71 +480,11 @@ public final class ObjcLDClient: NSObject { } /** - 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 - - - parameter changedFlag: The LDBoolChangedFlag 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 + Handler passed to the client app when a feature flag value changes - - parameter changedFlag: The LDStringChangedFlag 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 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 @@ -769,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) throws { - try ldClient.track(key: key, data: data, metricValue: nil) + @objc public func track(key: String, data: ObjcLDValue? = nil) { + ldClient.track(key: key, data: data?.wrappedValue, metricValue: nil) } /** @@ -790,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) throws { - try ldClient.track(key: key, data: 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 6276bb93..db8038b0 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 /** @@ -108,13 +101,13 @@ 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) + 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 } - 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/ObjcLDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift index 97bd7a8e..3088f09a 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift @@ -1,84 +1,84 @@ -// -// ObjcLDEvaluationDetail.swift -// LaunchDarkly_iOS -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - 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 35eea6c8..d6192219 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 /** @@ -18,52 +11,22 @@ 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 - } + @objc public class var attributeSecondary: String { "secondary" } /// LDUser name attribute used to make `name` private - @objc public class var attributeName: String { - LDUser.CodingKeys.name.rawValue - } + @objc public class var attributeName: String { "name" } /// LDUser firstName attribute used to make `firstName` private - @objc public class var attributeFirstName: String { - LDUser.CodingKeys.firstName.rawValue - } + @objc public class var attributeFirstName: String { "firstName" } /// LDUser lastName attribute used to make `lastName` private - @objc public class var attributeLastName: String { - LDUser.CodingKeys.lastName.rawValue - } + @objc public class var attributeLastName: String { "lastName" } /// LDUser country attribute used to make `country` private - @objc public class var attributeCountry: String { - LDUser.CodingKeys.country.rawValue - } + @objc public class var attributeCountry: String { "country" } /// LDUser ipAddress attribute used to make `ipAddress` private - @objc public class var attributeIPAddress: String { - LDUser.CodingKeys.ipAddress.rawValue - } + @objc public class var attributeIPAddress: String { "ip" } /// LDUser email attribute used to make `email` private - @objc public class var attributeEmail: String { - LDUser.CodingKeys.email.rawValue - } + @objc public class var attributeEmail: String { "email" } /// 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 - } + @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 { @@ -109,39 +72,30 @@ 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 } - set { user.custom = 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 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. 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 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 } - set { user.privateAttributes = newValue } + @objc public var privateAttributes: [String] { + get { user.privateAttributes.map { $0.name } } + set { user.privateAttributes = newValue.map { UserAttribute.forName($0) } } } /** @@ -165,15 +119,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/LaunchDarkly/ObjectiveC/ObjcLDValue.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift new file mode 100644 index 00000000..9ac75a8b --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift @@ -0,0 +1,137 @@ +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` 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)) + } + + /// 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 7f562c64..43067d3e 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -1,69 +1,167 @@ -// -// CacheConverter.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - 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 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 } - let currentCache: FeatureFlagCaching - private(set) var deprecatedCaches = [DeprecatedCacheModel: DeprecatedCache]() + private func convertV6Data(v6cache: KeyedValueCaching, flagCaches: [MobileKey: FeatureFlagCaching]) { + guard let cachedV6Data = v6cache.dictionary(forKey: "com.launchDarkly.cachedUserEnvironmentFlags") + else { return } + + 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: 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 { convertValue($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") + } } } } extension Date { func isExpired(expirationDate: Date) -> Bool { - let stringEquivalentDate = self.stringEquivalentDate - let stringEquivalentExpirationDate = expirationDate.stringEquivalentDate - return stringEquivalentDate.isEarlierThan(stringEquivalentExpirationDate) + 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/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 deleted file mode 100644 index 911a3134..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// DeprecatedCache.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -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, 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 -} - -extension Dictionary where Key == String, Value == Any { - var lastUpdated: Date? { - (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/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/Cache/DeprecatedCacheModelV5.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift deleted file mode 100644 index 3c0103fe..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// 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 -/* 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, - 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/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/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift new file mode 100644 index 00000000..c4720153 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -0,0 +1,57 @@ +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 64bc88e9..bb6a1de5 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift @@ -1,18 +1,20 @@ -// -// KeyedValueCache.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - 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 db7a4bee..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// UserEnvironmentCache.swift -// LaunchDarkly -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -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 { pair1, pair2 -> Bool in - pair2.value.lastUpdated.isEarlierThan(pair1.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 30e749fc..aa804f8a 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -1,18 +1,10 @@ -// -// ClientServiceFactory.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - 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,7 +18,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 @@ -34,25 +25,16 @@ protocol ClientServiceCreating { } final class ClientServiceFactory: ClientServiceCreating { - func makeKeyedValueCache() -> KeyedValueCaching { - UserDefaults.standard - } - - func makeFeatureFlagCache(maxCachedUsers: Int) -> FeatureFlagCaching { - UserEnvironmentFlagCache(withKeyedValueCache: makeKeyedValueCache(), maxCachedUsers: maxCachedUsers) + func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching { + UserDefaults(suiteName: cacheKey)! } - func makeCacheConverter(maxCachedUsers: Int) -> CacheConverting { - CacheConverter(serviceFactory: self, maxCachedUsers: maxCachedUsers) + func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedUsers: Int) -> FeatureFlagCaching { + FeatureFlagCache(serviceFactory: self, mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) } - 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()) - } + func makeCacheConverter() -> CacheConverting { + CacheConverter() } func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider { @@ -110,10 +92,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 7eb83e50..6f17bb9f 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) @@ -129,13 +122,9 @@ 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" + let sdkVersion = "6.0.0" // Unfortunately, the following does not function in certain configurations, such as when included through SPM // var sdkVersion: String { // Bundle(for: LDClient.self).infoDictionary?["CFBundleShortVersionString"] as? String ?? "5.x" diff --git a/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..8d746de1 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -1,18 +1,6 @@ -// -// EventReporter.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - 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 @@ -21,37 +9,26 @@ 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?) } 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 } @@ -77,32 +54,30 @@ 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 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) + if recordingDebugEvent { + let debugEvent = FeatureEvent(key: flagKey, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: true) recordNoSync(debugEvent) } } } - 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 } @@ -121,19 +96,21 @@ class EventReporter: EventReporting { guard isOnline else { Log.debug(typeName(and: #function) + "aborted. EventReporter is offline") - reportSyncComplete(.error(.isOffline)) + reportSyncComplete(.isOffline) completion?() 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 { Log.debug(typeName(and: #function) + "aborted. Event store is empty") - reportSyncComplete(.success([])) + reportSyncComplete(nil) completion?() return } @@ -144,20 +121,35 @@ class EventReporter: EventReporting { service.diagnosticCache?.recordEventsInLastBatch(eventsInLastBatch: toPublish.count) - DispatchQueue.main.async { + DispatchQueue.global().async { self.publish(toPublish, UUID().uuidString, completion) } } 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) + 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?() } } @@ -167,17 +159,17 @@ 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)") - self.reportSyncComplete(.success(sentEvents)) + Log.debug(self.typeName(and: #function) + "Completed sending \(sentEvents) event(s)") + 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 } @@ -186,9 +178,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 } @@ -196,7 +188,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/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift index f11138c5..9938eb90 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 @@ -97,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: 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 @@ -120,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 4866a317..f613ba97 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -1,18 +1,11 @@ -// -// FlagStore.swift -// LaunchDarkly -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - 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? } @@ -21,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] = [:] @@ -33,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) + 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. Malformed update dictionary. updateDictionary: \(String(describing: updateDictionary))") - return - } - guard self.isValidVersion(for: flagKey, newVersion: newFlag.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) } } @@ -134,7 +80,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/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift index 74213bb9..6789c7c6 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 @@ -42,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" @@ -83,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? @@ -106,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() @@ -119,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() @@ -148,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 @@ -159,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 @@ -241,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?) { @@ -308,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)) @@ -336,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 flagDictionary = try? JSONSerialization.jsonDictionary(with: data) + let flagCollection = try? JSONDecoder().decode(FeatureFlagCollection.self, from: data) else { reportDataError(messageEvent.data.data(using: .utf8)) return } - reportSuccess(flagDictionary: flagDictionary, eventType: updateType) - case nil: + 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 deleteResponse = try? JSONDecoder().decode(DeleteResponse.self, from: data) + else { + reportDataError(messageEvent.data.data(using: .utf8)) + return + } + reportSyncComplete(.delete(deleteResponse)) + default: Log.debug(typeName(and: #function) + "aborted. Unknown event type.") reportSyncComplete(.error(.unknownEventType(eventType))) return @@ -379,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/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/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/AnyComparerSpec.swift b/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift deleted file mode 100644 index a0d7c2f6..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/AnyComparerSpec.swift +++ /dev/null @@ -1,290 +0,0 @@ -// -// AnyComparerSpec.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - -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 deleted file mode 100644 index a5fc6819..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift +++ /dev/null @@ -1,240 +0,0 @@ -// -// DictionarySpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DictionarySpec: QuickSpec { - public override func spec() { - symmetricDifferenceSpec() - withNullValuesRemovedSpec() - dictionarySpec() - } - - 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") { - beforeEach { - otherDictionary = [:] - } - it("returns all keys in subject") { - expect(dictionary.symmetricDifference(otherDictionary)) == dictionary.keys.sorted() - } - } - context("when subject is empty") { - beforeEach { - dictionary = [:] - } - it("returns all keys in other") { - 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") { - expect(dictionary.symmetricDifference(otherDictionary)) == [addedKey] - } - } - context("when other has an added key") { - let addedKey = "addedKey" - beforeEach { - otherDictionary[addedKey] = true - } - it("returns the different key") { - expect(dictionary.symmetricDifference(otherDictionary)) == [addedKey] - } - } - context("when other has a different key") { - let addedKeyA = "addedKeyA" - let addedKeyB = "addedKeyB" - beforeEach { - 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") { - 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") { - 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") { - 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") { - 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") { - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different dictionary value") { - let differingKey = DarklyServiceMock.FlagKeys.dictionary - beforeEach { - 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] - } - } - } - } - - 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()) - } - } - 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()) - } - } - 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()) - } - } - } - } -} - -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 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/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 4f7f8f33..a7678449 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 @@ -16,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 } @@ -27,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 { @@ -43,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 } @@ -58,9 +45,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 } @@ -73,9 +57,6 @@ final class LDClientSpec: QuickSpec { var makeFlagSynchronizerService: DarklyServiceProvider? { serviceFactoryMock.makeFlagSynchronizerReceivedParameters?.service } - var observedError: Error? { - errorNotifierMock.notifyObserversReceivedError - } var onSyncComplete: FlagSyncCompleteClosure? { serviceFactoryMock.onFlagSyncComplete } @@ -100,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) @@ -179,42 +163,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 } } } @@ -262,18 +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?.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 - 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 @@ -307,18 +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?.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 - 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 @@ -351,18 +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?.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 - 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") { @@ -386,58 +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?.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 - 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] } } } - 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 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.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == cachedFlags + + 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("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.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + + 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?.keysToConvert) == [testContext.config.mobileKey] } } @@ -466,14 +418,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 @@ -508,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 } } } @@ -554,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) @@ -571,18 +517,11 @@ 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?.kind) == .identify - expect(testContext.recordedEvent?.key) == testContext.user.key - } - 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.recordedEvent as? IdentifyEvent)?.user) == testContext.user } } } @@ -601,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) @@ -618,18 +556,11 @@ 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?.kind) == .identify - expect(testContext.recordedEvent?.key) == testContext.user.key - } - 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.recordedEvent as? IdentifyEvent)?.user) == testContext.user } } } @@ -638,320 +569,186 @@ 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() - - 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 - } + it("when the client is online") { + let testContext = TestContext(startOnline: true) + testContext.start() + testContext.featureFlagCachingMock.reset() + + 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.retrieveFeatureFlagsReceivedUserKey) == newUser.key + + expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } - 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() - 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.retrieveFeatureFlagsReceivedUserKey) == newUser.key + + expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } - 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.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.replaceStoreReceivedNewFlags?.flags) == stubFlags } } } 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 event: LaunchDarkly.Event! - var priorRecordedEvents: Int! - context("when started") { - beforeEach { - priorRecordedEvents = 0 - } - context("and online") { - beforeEach { - testContext = TestContext(startOnline: true) - testContext.start() - event = Event.stub(.custom, with: testContext.user) - 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") { - expect(try testContext.subject.track(key: event.key!)).toNot(throwError()) - expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents - } - it("flushes the event reporter") { - expect(testContext.eventReporterMock.flushCallCount) == 1 - } - } - context("and offline") { - beforeEach { - testContext = TestContext() - testContext.start() - event = Event.stub(.custom, with: testContext.user) - 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") { - expect(try testContext.subject.track(key: event.key!)).toNot(throwError()) - 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() - event = Event.stub(.custom, with: testContext.user) - 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") { - expect(try testContext.subject.track(key: event.key!)).toNot(throwError()) - 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") { - var event: LaunchDarkly.Event! - beforeEach { - testContext = TestContext() + it("records a custom event") { + let 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()) - } - } - context("when client was stopped") { - var priorRecordedEvents: Int! - beforeEach { - testContext.subject.close() - priorRecordedEvents = testContext.eventReporterMock.recordCallCount - - try! testContext.subject.track(key: event.key!, data: event.data) - } - it("does not record any more events") { - expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents - } + 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?.data) == "abc" + expect(receivedEvent?.metricValue) == 5.0 + } + 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 } } } @@ -967,71 +764,23 @@ 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") { - // 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.array, defaultValue: DefaultFlagValues.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") { - _ = 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?.featureFlag) == testContext.flagStoreMock.featureFlags[DarklyServiceMock.FlagKeys.bool] - 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()) + 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") { - // 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?) + _ = testContext.subject.boolVariation(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(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue).to(beNil()) + 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 } @@ -1040,62 +789,19 @@ 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 - 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) == 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") { - _ = 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?.featureFlag).to(beNil()) - 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()) + 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") { - // 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?) + _ = 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).to(beNil()) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue).to(beNil()) + 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 } @@ -1161,19 +867,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 } } } @@ -1186,163 +883,112 @@ final class LDClientSpec: QuickSpec { } private func onSyncCompleteSuccessSpec() { - context("polling") { - onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .polling) - } - context("streaming ping") { - onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .streaming, eventType: .ping) - } - context("streaming put") { - onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .streaming, eventType: .put) + it("flag collection") { + self.onSyncCompleteSuccessReplacingFlagsSpec() } - 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]! + private func onSyncCompleteSuccessReplacingFlagsSpec() { + let testContext = TestContext(startOnline: true) + testContext.start() + testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() + + let newFlags = ["flag1": FeatureFlag(flagKey: "flag1")] var updateDate: Date! + waitUntil { done in + testContext.changeNotifierMock.notifyObserversCallback = done + updateDate = Date() + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection(newFlags))) + } - beforeEach { - testContext = TestContext(startOnline: true) - testContext.start() - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() + expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == newFlags - 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?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) - 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.isWithin(Constants.updateThreshold, of: updateDate)) == true - 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(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags) == [:] } 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, - 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.isWithin(Constants.updateThreshold, of: updateDate)) == true - 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()) + let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) + testContext.start() + testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() + let updateFlag = FeatureFlag(flagKey: "abc") + + var updateDate: Date! + waitUntil { done in + testContext.changeNotifierMock.notifyObserversCallback = done + updateDate = Date() + testContext.onSyncComplete?(.patch(updateFlag)) } + + expect(testContext.flagStoreMock.updateStoreCallCount) == 1 + 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?.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()) } 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 deleteResponse = DeleteResponse(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.isWithin(Constants.updateThreshold, of: updateDate)) == true - 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?(.delete(deleteResponse)) } + + expect(testContext.flagStoreMock.deleteFlagCallCount) == 1 + 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?.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()) } func onSyncCompleteErrorSpec() { - func runTest(_ ctx: String, _ err: SynchronizingError, testError: @escaping ((SynchronizingError) -> 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)) - } - } - 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("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) } - } + func runTest(_ ctx: String, + _ err: SynchronizingError, + testError: @escaping ((ConnectionInformation.LastConnectionFailureReason) -> Void)) { + 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) } } @@ -1351,9 +997,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, @@ -1361,82 +1007,61 @@ 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() { - 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 } } } @@ -1445,32 +1070,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 } } } @@ -1482,124 +1100,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) } } } @@ -1607,121 +1188,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) } } } @@ -1756,16 +1297,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") { - expect(AnyComparer.isEqual(testContext.subject.allFlags, to: stubFlags.compactMapValues { $0.value })).to(beTrue()) + let testContext = TestContext().withCached(flags: stubFlags) + testContext.start() + expect(testContext.subject.allFlags) == stubFlags.compactMapValues { $0.value } } it("returns nil when client is closed") { + let testContext = TestContext().withCached(flags: stubFlags) + testContext.start() testContext.subject.close() expect(testContext.subject.allFlags).to(beNil()) } @@ -1773,110 +1313,69 @@ 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.boolVariationDetail(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool).reason + if let errorKind = detail?["errorKind"] { + 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() - testContext.start() - } - it("returns true") { - expect(testContext.subject.isInitialized) == true - } - it("and then stopped returns false") { - testContext.subject.close() - expect(testContext.subject.isInitialized) == false - } + 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] { - 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 - } - } + 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)) + + testContext.subject.close() + expect(testContext.subject.isInitialized) == false } } } @@ -1885,7 +1384,7 @@ final class LDClientSpec: QuickSpec { extension FeatureFlagCachingMock { func reset() { retrieveFeatureFlagsCallCount = 0 - retrieveFeatureFlagsReceivedArguments = nil + retrieveFeatureFlagsReceivedUserKey = nil retrieveFeatureFlagsReturnValue = nil storeFeatureFlagsCallCount = 0 storeFeatureFlagsReceivedArguments = nil @@ -1899,10 +1398,3 @@ extension OperatingSystem { } private class ErrorMock: Error { } - -extension CacheConvertingMock { - func reset() { - convertCacheDataCallCount = 0 - convertCacheDataReceivedArguments = nil - } -} 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..e41a117a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -1,44 +1,31 @@ -// -// ClientServiceMockFactory.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation 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 { @@ -129,10 +116,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..3e212454 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 @@ -24,28 +17,21 @@ 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 { - 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 var knownFlags: [Any] { - [bool, int, double, string, array, dictionary, null] - } - - 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 @@ -57,36 +43,9 @@ 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 { - 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)! @@ -98,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 { + trackEvents: Bool = true, + 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: FlagValues.value(from: flagKey), + variation: variation, + version: version(for: flagKey, useAlternateVersion: useAlternateVersion), + flagVersion: flagVersion, + trackEvents: trackEvents, + debugEventsUntilDate: debugEventsUntilDate, + reason: nil, + trackReason: false) } } @@ -237,17 +134,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)) } @@ -287,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 { @@ -307,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) @@ -326,32 +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)" - } - - // 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) + "Flag request stub using method \(useReport ? URLRequest.HTTPMethods.report : URLRequest.HTTPMethods.get) with response status code \(statusCode)" } // MARK: Publish Event @@ -370,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) } } @@ -407,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 88c5d44f..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// DeprecatedCacheMock.swift -// LaunchDarklyTests -// -// Copyright © 2020 Catamorphic Co. All rights reserved. -// -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/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..9db37b2b 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift @@ -1,19 +1,7 @@ -// -// FlagMaintainingMock.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - 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() { @@ -29,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/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..0f08112a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift @@ -1,45 +1,14 @@ -// -// LDEventSourceMock.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation 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/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift index 4a1b51e9..943be88c 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 @@ -21,20 +14,20 @@ 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 - custom[CodingKeys.operatingSystem.rawValue] = StubConstants.operatingSystem + custom["device"] = StubConstants.device + custom["os"] = StubConstants.operatingSystem } return custom } @@ -52,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/Cache/CacheableEnvironmentFlagsSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift deleted file mode 100644 index cf86824c..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// CacheableEnvironmentFlagsSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -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 b3b1b897..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// CacheableUserEnvironmentsSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -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..(_ scheme: CodingScheme, _ subject: T?) -> T? { - let encoded = try? scheme.encode(subject) - return try? scheme.decode(T.self, from: encoded!) - } - - 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 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 + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() - 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 func loadAndRestore(_ subject: T?) -> T? { + let encoded = try? encoder.encode(subject) + return try? decoder.decode(T.self, from: encoded!) } } - -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/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..4bf2efd9 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -1,1007 +1,282 @@ -// -// EventSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class EventSpec: QuickSpec { - struct Constants { - static let eventKey = "EventSpec.Event.Key" - } - - 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] - } - - override func spec() { - initSpec() - aliasSpec() - featureEventSpec() - debugEventSpec() - customEventSpec() - identifyEventSpec() - summaryEventSpec() - dictionaryValueSpec() - eventDictionarySpec() - } - - 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(AnyComparer.isEqual(event.value, to: true)).to(beTrue()) - expect(AnyComparer.isEqual(event.defaultValue, to: false)).to(beTrue()) - expect(event.featureFlag?.allPropertiesMatch(featureFlag)).to(beTrue()) - expect(event.data).toNot(beNil()) - expect(AnyComparer.isEqual(event.data, to: CustomEvent.dictionaryData)).to(beTrue()) - 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).to(beNil()) - expect(event.defaultValue).to(beNil()) - expect(event.featureFlag).to(beNil()) - expect(event.data).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - expect(event.endDate).to(beNil()) - } - } - } - } - - 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" - } - } - } - } - - 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") { - expect(event.kind) == Event.Kind.feature - 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.featureFlag?.allPropertiesMatch(featureFlag)).to(beTrue()) - - expect(event.data).to(beNil()) - expect(event.endDate).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - } - } - } - - 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") { - expect(event.kind) == Event.Kind.debug - 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.featureFlag?.allPropertiesMatch(featureFlag)).to(beTrue()) - - expect(event.data).to(beNil()) - expect(event.endDate).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - } - } - } - - private func customEventSpec() { - var user: LDUser! - var event: Event! - beforeEach { - user = LDUser.stub() - } - describe("customEvent") { - 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()) - - 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.value).to(beNil()) - expect(event.defaultValue).to(beNil()) - 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()) +import XCTest - 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()) +@testable import LaunchDarkly - expect(event.value).to(beNil()) - expect(event.defaultValue).to(beNil()) - expect(event.endDate).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - } - } - } +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) } - 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).to(beNil()) - expect(event.defaultValue).to(beNil()) - expect(event.data).to(beNil()) - expect(event.endDate).to(beNil()) - expect(event.flagRequestTracker).to(beNil()) - } + 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, featureFlag) + XCTAssertEqual(event.includeReason, true) + XCTAssertEqual(event.creationDate, testDate) + } + + 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, featureFlag) + XCTAssertEqual(event.includeReason, false) + XCTAssertEqual(event.creationDate, testDate) + } + + 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) + } + + 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) + } + + 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) + } + + 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))) } } - 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) == flagRequestTracker - - 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.featureFlag).to(beNil()) - expect(event.data).to(beNil()) - } - } - 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 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"], .number(Double(event.creationDate.millisSince1970))) } } - private func dictionaryValueSpec() { - describe("dictionaryValue") { - dictionaryValueFeatureEventSpec() - dictionaryValueIdentifyEventSpec() - dictionaryValueAliasEventSpec() - dictionaryValueCustomEventSpec() - dictionaryValueDebugEventSpec() - dictionaryValueSummaryEventSpec() + 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"], .number(Double(event.creationDate.millisSince1970))) } } - private func dictionaryValueFeatureEventSpec() { - var config: LDConfig! + func testCustomEventEncodingInlining() { 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 non-user elements") { - expect(eventDictionary.eventKind) == .feature - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) - 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.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.eventUser).to(beNil()) - } - } - 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 non-user elements") { - expect(eventDictionary.eventKind) == .feature - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) - 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.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") { - 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.eventKind) == .feature - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) - 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.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion) == featureFlag.version - expect(eventDictionary.eventData).to(beNil()) - } - } - 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.eventKind) == .feature - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) - 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.eventVariation) == featureFlag.variation - expect(eventDictionary.eventVersion).to(beNil()) - expect(eventDictionary.eventData).to(beNil()) - } - } - 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.eventKind) == .feature - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) - 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.eventData).to(beNil()) - } - it("creates a dictionary with the user key only") { - expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUser).to(beNil()) - } - } - 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" - } + 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"], .number(Double(event.creationDate.millisSince1970))) } } - private func dictionaryValueIdentifyEventSpec() { - var config: LDConfig! + func testFeatureEventEncodingNoReasonByDefault() { 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.eventKind) == .identify - expect(eventDictionary.eventKey) == user.key - expect(eventDictionary.eventCreationDate?.isWithin(0.1, of: event.creationDate!)).to(beTrue()) - 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()) - } + 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 + 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"], .number(Double(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.eventKind) == .alias - expect(eventDictionary.eventKey) == user1.key - expect(eventDictionary.eventPreviousKey) == user2.key - expect(eventDictionary.eventContextKind) == "user" - expect(eventDictionary.eventPreviousContextKind) == "user" - } - it("unknown to known") { - let eventDictionary = Event.aliasEvent(newUser: user1, oldUser: anonUser1).dictionaryValue(config: config) - expect(eventDictionary.eventKind) == .alias - expect(eventDictionary.eventKey) == user1.key - expect(eventDictionary.eventPreviousKey) == anonUser1.key - expect(eventDictionary.eventContextKind) == "user" - expect(eventDictionary.eventPreviousContextKind) == "anonymousUser" - } - it("unknown to unknown") { - let eventDictionary = Event.aliasEvent(newUser: anonUser1, oldUser: anonUser2).dictionaryValue(config: config) - expect(eventDictionary.eventKind) == .alias - expect(eventDictionary.eventKey) == anonUser1.key - expect(eventDictionary.eventPreviousKey) == anonUser2.key - expect(eventDictionary.eventContextKind) == "anonymousUser" - expect(eventDictionary.eventPreviousContextKind) == "anonymousUser" - } - } - } - - private func dictionaryValueCustomEventSpec() { - var config: LDConfig! + func testFeatureEventEncodingIncludeReason() { let user = LDUser.stub() - var event: Event! - var eventDictionary: [String: Any]! - var metricValue: Double! - context("custom event") { - beforeEach { - config = LDConfig.stub - } - metricValue = 0.5 - 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") - } - eventDictionary = event.dictionaryValue(config: config) - } - 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(AnyComparer.isEqual(eventDictionary.eventData, to: eventData)).to(beTrue()) - expect(eventDictionary.eventValue).to(beNil()) - expect(eventDictionary.eventDefaultValue).to(beNil()) - expect(eventDictionary.eventVariation).to(beNil()) - expect(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUser).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") - } - eventDictionary = event.dictionaryValue(config: config) - } - 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.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.eventUser).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") - } - config.inlineUserInEvents = false // Default value, here for clarity - eventDictionary = event.dictionaryValue(config: config) - } - 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(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(eventDictionary.eventUserKey) == user.key - expect(eventDictionary.eventUser).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") - } - config.inlineUserInEvents = true - eventDictionary = event.dictionaryValue(config: config) - } - 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(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(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") - } - eventDictionary = event.dictionaryValue(config: config) - expect(eventDictionary.eventContextKind) == "anonymousUser" - } + 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"], .number(Double(event.creationDate.millisSince1970))) } } } - private func dictionaryValueDebugEventSpec() { - var config: LDConfig! + func testFeatureEventEncodingTrackReason() { 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 non-user elements") { - config.inlineUserInEvents = inlineUser - eventDictionary = event.dictionaryValue(config: config) - - expect(eventDictionary.eventKind) == .debug - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) - 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.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()) - } - } - } - 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.eventKind) == .debug - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) - 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") { - 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.eventKind) == .debug - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) - 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") { - 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.eventKind) == .debug - expect(eventDictionary.eventKey) == Constants.eventKey - expect(eventDictionary.eventCreationDate?.isWithin(0.001, of: event.creationDate!)).to(beTrue()) - 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()) - } + 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 + 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"], .number(Double(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.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()) - 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()) - } - - expect(eventDictionary.eventKey).to(beNil()) - expect(eventDictionary.eventCreationDate).to(beNil()) - expect(eventDictionary.eventUser).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()) + 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: true, isDebug: isDebug) + encodesToObject(event) { dict in + 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"], .number(Double(event.creationDate.millisSince1970))) } } } - // Dictionary extension methods that extract an event key, or creationDateMillis, and compare them with another dictionary - private func eventDictionarySpec() { - let config = LDConfig.stub + func testFeatureEventEncodingInlinesUserForDebugOrConfig() { 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?.isWithin(0.001, of: event.endDate)).to(beTrue()) - } - it("returns nil when the dictionary does not contain the event kind") { - eventDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) - 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 - } - } - } + 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)) + }} + } + + func testIdentifyEventEncoding() { + let user = LDUser.stub() + 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"], .number(Double(event.creationDate.millisSince1970))) } } } -} -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 - } - var eventUser: LDUser? { - if let userDictionary = eventUserDictionary { - return LDUser(userDictionary: userDictionary) - } - return nil - } - 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 eventCreationDateMillis: Int64? { - self[Event.CodingKeys.creationDate.rawValue] as? Int64 - } - var eventContextKind: String? { - self[Event.CodingKeys.contextKind.rawValue] as? String - } - 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, eventEndDate.isWithin(0.001, of: other.eventEndDate) - else { return false } - return true + 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"], .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() + counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) + counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) + XCTAssertEqual(features["bool-flag"], encodeToLDValue(counter)) + } } - 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: 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) } } @@ -1010,33 +285,18 @@ extension 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 (try? Event.customEvent(key: UUID().uuidString, user: user, data: ["custom": 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") } } - 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) - } - } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift index 7415338b..5fdcd457 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FeatureFlagSpec.swift @@ -1,770 +1,198 @@ -// -// FeatureFlagSpec.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation -import Quick -import Nimble -@testable import LaunchDarkly +import XCTest -final class FeatureFlagSpec: QuickSpec { +@testable import LaunchDarkly - struct Constants { - static let extraDictionaryKey = "FeatureFlagSpec.dictionary.key" - static let extraDictionaryValue = "FeatureFlagSpec.dictionary.value" +final class FeatureFlagSpec: XCTestCase { + func testInitMinimal() { + let featureFlag = FeatureFlag(flagKey: "abc") + XCTAssertEqual(featureFlag.flagKey, "abc") + XCTAssertEqual(featureFlag.value, .null) + XCTAssertNil(featureFlag.variation) + XCTAssertNil(featureFlag.version) + XCTAssertFalse(featureFlag.trackEvents) + XCTAssertNil(featureFlag.debugEventsUntilDate) + XCTAssertNil(featureFlag.reason) + XCTAssertFalse(featureFlag.trackReason) } - override func spec() { - initSpec() - dictionaryValueSpec() - equalsSpec() - shouldCreateDebugEventsSpec() - collectionSpec() + func testInitAll() { + let reason = DarklyServiceMock.Constants.reason + let debugEventsUntilDate = Date().addingTimeInterval(30.0) + let featureFlag = FeatureFlag(flagKey: "abc", + value: 123, + variation: 2, + version: 3, + flagVersion: 4, + trackEvents: true, + debugEventsUntilDate: debugEventsUntilDate, + reason: reason, + trackReason: false) + + XCTAssertEqual(featureFlag.flagKey, "abc") + XCTAssertEqual(featureFlag.value, 123) + XCTAssertEqual(featureFlag.variation, 2) + XCTAssertEqual(featureFlag.version, 3) + XCTAssertEqual(featureFlag.flagVersion, 4) + XCTAssertEqual(featureFlag.trackEvents, true) + XCTAssertEqual(featureFlag.debugEventsUntilDate, debugEventsUntilDate) + XCTAssertEqual(featureFlag.reason, reason) + XCTAssertEqual(featureFlag.trackReason, false) } - func initSpec() { - describe("init") { - var featureFlag: FeatureFlag! - context("when elements exist") { - var variation = 0 - var flagVersion: Int { variation + 1 } - var version: Int { flagVersion + 1 } - let trackEvents = true - let debugEventsUntilDate = Date().addingTimeInterval(30.0) - let reason = DarklyServiceMock.Constants.reason - let trackReason = false - it("creates a feature flag with matching elements") { - DarklyServiceMock.FlagKeys.knownFlags.forEach { flagKey in - let value = DarklyServiceMock.FlagValues.value(from: flagKey) - variation += 1 - - featureFlag = FeatureFlag(flagKey: flagKey, value: value, variation: variation, version: version, flagVersion: flagVersion, trackEvents: trackEvents, debugEventsUntilDate: debugEventsUntilDate, reason: reason, trackReason: trackReason) - - expect(featureFlag.flagKey) == flagKey - expect(AnyComparer.isEqual(featureFlag.value, to: value, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlag.variation) == variation - expect(featureFlag.version) == version - expect(featureFlag.flagVersion) == flagVersion - expect(featureFlag.trackEvents) == trackEvents - expect(featureFlag.debugEventsUntilDate) == debugEventsUntilDate - expect(AnyComparer.isEqual(featureFlag.reason, to: reason, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlag.trackReason) == trackReason - } - } - } - context("when elements don't exist") { - beforeEach { - featureFlag = FeatureFlag(flagKey: DarklyServiceMock.FlagKeys.unknown) - } - it("creates a feature flag with nil elements") { - expect(featureFlag).toNot(beNil()) - expect(featureFlag.flagKey) == DarklyServiceMock.FlagKeys.unknown - expect(featureFlag.value).to(beNil()) - expect(featureFlag.variation).to(beNil()) - expect(featureFlag.version).to(beNil()) - expect(featureFlag.trackEvents).to(beNil()) - expect(featureFlag.debugEventsUntilDate).to(beNil()) - expect(featureFlag.reason).to(beNil()) - expect(featureFlag.trackReason).to(beNil()) - } - } - } - - describe("init with dictionary") { - var variation = 0 - var flagVersion: Int { variation + 1 } - var version: Int { flagVersion + 1 } - let trackEvents = true - var featureFlag: FeatureFlag? - context("when elements make the whole dictionary") { - it("creates a feature flag with all elements") { - DarklyServiceMock.FlagKeys.knownFlags.forEach { flagKey in - let value = DarklyServiceMock.FlagValues.value(from: flagKey) - variation += 1 - let dictionaryFromElements = Dictionary(flagKey: flagKey, value: value, variation: variation, version: version, flagVersion: flagVersion, trackEvents: trackEvents) - - featureFlag = FeatureFlag(dictionary: dictionaryFromElements) - - expect(featureFlag?.flagKey) == flagKey - expect(AnyComparer.isEqual(featureFlag?.value, to: value, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlag?.variation) == variation - expect(featureFlag?.version) == version - expect(featureFlag?.flagVersion) == flagVersion - expect(featureFlag?.trackEvents) == trackEvents - } - } - } - context("when elements are part of the dictionary") { - it("creates a feature flag with all elements") { - DarklyServiceMock.FlagKeys.knownFlags.forEach { flagKey in - let value = DarklyServiceMock.FlagValues.value(from: flagKey) - variation += 1 - let dictionaryFromElements = Dictionary(flagKey: flagKey, - value: value, - variation: variation, - version: version, - flagVersion: flagVersion, - trackEvents: trackEvents, - includeExtraDictionaryItems: true) - - featureFlag = FeatureFlag(dictionary: dictionaryFromElements) - - expect(featureFlag?.flagKey) == flagKey - expect(AnyComparer.isEqual(featureFlag?.value, to: value, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlag?.variation) == variation - expect(featureFlag?.version) == version - expect(featureFlag?.flagVersion) == flagVersion - expect(featureFlag?.trackEvents) == trackEvents - } - } - } - context("when dictionary only contains the key and value") { - it("it creates a feature flag with the key and value only") { - DarklyServiceMock.FlagKeys.knownFlags.forEach { flagKey in - let value = DarklyServiceMock.FlagValues.value(from: flagKey) - let dictionaryFromElements = Dictionary(flagKey: flagKey, value: value, variation: nil, version: nil, flagVersion: nil, trackEvents: nil) - - featureFlag = FeatureFlag(dictionary: dictionaryFromElements) - - expect(featureFlag?.flagKey) == flagKey - expect(AnyComparer.isEqual(featureFlag?.value, to: value, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlag?.variation).to(beNil()) - expect(featureFlag?.version).to(beNil()) - expect(featureFlag?.flagVersion).to(beNil()) - expect(featureFlag?.trackEvents).to(beNil()) - } - } - } - context("when dictionary only contains the key and variation") { - beforeEach { - let dictionaryFromElements = Dictionary(flagKey: DarklyServiceMock.FlagKeys.null, - value: nil, - variation: DarklyServiceMock.Constants.variation, - version: nil, - flagVersion: nil, - trackEvents: nil) - - featureFlag = FeatureFlag(dictionary: dictionaryFromElements) - } - it("it creates a feature flag with the key and variation only") { - expect(featureFlag?.flagKey) == DarklyServiceMock.FlagKeys.null - expect(featureFlag?.value).to(beNil()) - expect(featureFlag?.variation) == DarklyServiceMock.Constants.variation - expect(featureFlag?.version).to(beNil()) - expect(featureFlag?.flagVersion).to(beNil()) - expect(featureFlag?.trackEvents).to(beNil()) - } - } - context("when dictionary only contains the key and version") { - beforeEach { - let dictionaryFromElements = Dictionary(flagKey: DarklyServiceMock.FlagKeys.null, - value: nil, - variation: nil, - version: DarklyServiceMock.Constants.version, - flagVersion: nil, trackEvents: nil) - - featureFlag = FeatureFlag(dictionary: dictionaryFromElements) - } - it("it creates a feature flag with the key and version only") { - expect(featureFlag?.flagKey) == DarklyServiceMock.FlagKeys.null - expect(featureFlag?.value).to(beNil()) - expect(featureFlag?.variation).to(beNil()) - expect(featureFlag?.version) == DarklyServiceMock.Constants.version - expect(featureFlag?.flagVersion).to(beNil()) - expect(featureFlag?.trackEvents).to(beNil()) - } - } - context("when dictionary only contains the key and flagVersion") { - beforeEach { - let dictionaryFromElements = Dictionary(flagKey: DarklyServiceMock.FlagKeys.null, - value: nil, - variation: nil, - version: nil, - flagVersion: DarklyServiceMock.Constants.flagVersion, - trackEvents: nil) + func testDecodeMinimal() throws { + let minimal: LDValue = ["key": "flag-key"] + let flag = try JSONDecoder().decode(FeatureFlag.self, from: try JSONEncoder().encode(minimal)) + XCTAssertEqual(flag.flagKey, "flag-key") + XCTAssertEqual(flag.value, .null) + XCTAssertNil(flag.variation) + XCTAssertNil(flag.version) + XCTAssertNil(flag.flagVersion) + XCTAssertFalse(flag.trackEvents) + XCTAssertNil(flag.debugEventsUntilDate) + XCTAssertNil(flag.reason) + XCTAssertFalse(flag.trackReason) + } - featureFlag = FeatureFlag(dictionary: dictionaryFromElements) - } - it("it creates a feature flag with the key and version only") { - expect(featureFlag?.flagKey) == DarklyServiceMock.FlagKeys.null - expect(featureFlag?.value).to(beNil()) - expect(featureFlag?.variation).to(beNil()) - expect(featureFlag?.version).to(beNil()) - expect(featureFlag?.flagVersion) == DarklyServiceMock.Constants.flagVersion - expect(featureFlag?.trackEvents).to(beNil()) - } - } - context("when dictionary only contains the key and trackEvents") { - beforeEach { - let dictionaryFromElements = Dictionary(flagKey: DarklyServiceMock.FlagKeys.null, value: nil, variation: nil, version: nil, flagVersion: nil, trackEvents: trackEvents) + func testDecodeFull() throws { + 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)) + XCTAssertEqual(flag.flagKey, "flag-key") + XCTAssertEqual(flag.value, [1, 2, 3]) + XCTAssertEqual(flag.variation, 2) + XCTAssertEqual(flag.version, 3) + XCTAssertEqual(flag.flagVersion, 4) + XCTAssertEqual(flag.trackEvents, false) + XCTAssertEqual(flag.debugEventsUntilDate?.millisSince1970, now) + XCTAssertEqual(flag.reason, ["kind": "OFF"]) + XCTAssertEqual(flag.trackReason, true) + } - featureFlag = FeatureFlag(dictionary: dictionaryFromElements) - } - it("it creates a feature flag with the key and trackEvents") { - expect(featureFlag?.flagKey) == DarklyServiceMock.FlagKeys.null - expect(featureFlag?.value).to(beNil()) - expect(featureFlag?.variation).to(beNil()) - expect(featureFlag?.version).to(beNil()) - expect(featureFlag?.flagVersion).to(beNil()) - expect(featureFlag?.trackEvents) == trackEvents - } - } - context("when dictionary does not contain the flag key") { - beforeEach { - let dictionaryFromElements = Dictionary(flagKey: nil, - value: DarklyServiceMock.FlagValues.bool, - variation: variation, - version: version, - flagVersion: flagVersion, - trackEvents: trackEvents) + func testDecodeExtra() throws { + let extra: LDValue = ["key": "flag-key", "unused": "foo"] + let flag = try JSONDecoder().decode(FeatureFlag.self, from: try JSONEncoder().encode(extra)) + XCTAssertEqual(flag.flagKey, "flag-key") + } - featureFlag = FeatureFlag(dictionary: dictionaryFromElements) - } - it("it does not create a feature flag") { - expect(featureFlag).to(beNil()) - } - } - context("when the dictionary does not contain any element") { - beforeEach { - featureFlag = FeatureFlag(dictionary: DarklyServiceMock.FlagValues.dictionary) - } - it("it does not create a feature flag") { - expect(featureFlag).to(beNil()) - } - } - context("when the dictionary is nil") { - beforeEach { - featureFlag = FeatureFlag(dictionary: nil) - } - it("returns nil") { - expect(featureFlag).to(beNil()) - } - } + func testDecodeMissingKey() throws { + let testData = try JSONEncoder().encode([:] as LDValue) + XCTAssertThrowsError(try JSONDecoder().decode(FeatureFlag.self, from: testData)) { err in + guard let err = err as? DecodingError, case .keyNotFound = err + else { return XCTFail("Expected key not found error") } } } - func dictionaryValueSpec() { - var featureFlags: [LDFlagKey: FeatureFlag]! - describe("dictionaryValue") { - context("with elements") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags() - } - it("creates a dictionary with all elements including nil value representations") { - featureFlags.forEach { flagKey, featureFlag in - let featureFlagDictionary = featureFlag.dictionaryValue - - expect(featureFlagDictionary.flagKey) == flagKey - expect(AnyComparer.isEqual(featureFlagDictionary.value, to: featureFlag.value, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlagDictionary.variation) == featureFlag.variation - expect(featureFlagDictionary.version) == featureFlag.version - expect(featureFlagDictionary.flagVersion) == featureFlag.flagVersion - expect(featureFlagDictionary.trackEvents) == featureFlag.trackEvents - } - } - } - context("without elements") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false, trackEvents: nil, debugEventsUntilDate: nil) - } - it("creates a dictionary with the value including nil value and version representations") { - featureFlags.forEach { flagKey, featureFlag in - let featureFlagDictionary = featureFlag.dictionaryValue - - expect(featureFlagDictionary).toNot(beNil()) - expect(featureFlagDictionary.flagKey) == flagKey - expect(AnyComparer.isEqual(featureFlagDictionary.value, to: featureFlag.value, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlagDictionary.variation).to(beNil()) - expect(featureFlagDictionary.version).to(beNil()) - expect(featureFlagDictionary.flagVersion).to(beNil()) - expect(featureFlagDictionary.trackEvents).to(beNil()) - } - } + func testDecodeMismatchedType() throws { + 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 { + XCTAssertThrowsError(try JSONDecoder().decode(FeatureFlag.self, from: $0)) { err in + guard let err = err as? DecodingError, case .typeMismatch = err + else { return XCTFail("Expected type mismatch error") } } } + } - describe("dictionary restores to feature flag") { - context("with elements") { - var featureFlags: [LDFlagKey: FeatureFlag]! - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags() - } - it("creates a feature flag with the same elements as the original") { - featureFlags.forEach { flagKey, featureFlag in - let reinflatedFlag = FeatureFlag(dictionary: featureFlag.dictionaryValue) - - expect(reinflatedFlag).toNot(beNil()) - expect(reinflatedFlag?.flagKey) == flagKey - expect(AnyComparer.isEqual(reinflatedFlag?.value, to: featureFlag.value, considerNilAndNullEqual: true)).to(beTrue()) - expect(reinflatedFlag?.version) == featureFlag.version - expect(reinflatedFlag?.flagVersion) == featureFlag.flagVersion - expect(reinflatedFlag?.trackEvents) == featureFlag.trackEvents - } - } - } - context("dictionary has null value") { - var reinflatedFlag: FeatureFlag? - beforeEach { - let featureFlag = FeatureFlag(flagKey: DarklyServiceMock.FlagKeys.dictionary, - value: DarklyServiceMock.FlagValues.dictionary.appendNull(), - variation: DarklyServiceMock.Constants.variation, - version: DarklyServiceMock.Constants.version, - flagVersion: DarklyServiceMock.Constants.flagVersion, - trackEvents: DarklyServiceMock.Constants.trackEvents, - debugEventsUntilDate: DarklyServiceMock.Constants.debugEventsUntilDate, - reason: DarklyServiceMock.Constants.reason, - trackReason: false) - - reinflatedFlag = FeatureFlag(dictionary: featureFlag.dictionaryValue) - } - it("creates a feature flag with the same elements as the original") { - expect(reinflatedFlag).toNot(beNil()) - expect(reinflatedFlag?.flagKey) == DarklyServiceMock.FlagKeys.dictionary - expect(AnyComparer.isEqual(reinflatedFlag?.value, to: DarklyServiceMock.FlagValues.dictionary.appendNull())).to(beTrue()) - expect(reinflatedFlag?.version) == DarklyServiceMock.Constants.version - expect(reinflatedFlag?.flagVersion) == DarklyServiceMock.Constants.flagVersion - expect(reinflatedFlag?.trackEvents) == DarklyServiceMock.Constants.trackEvents - } - } + func testEncodeMinimal() { + let flag = FeatureFlag(flagKey: "flag-key") + encodesToObject(flag) { value in + XCTAssertEqual(value.count, 1) + XCTAssertEqual(value["key"], "flag-key") } } - func equalsSpec() { - var originalFlags: [LDFlagKey: FeatureFlag]! - var otherFlag: FeatureFlag! - describe("equals") { - context("when elements exist") { - beforeEach { - originalFlags = DarklyServiceMock.Constants.stubFeatureFlags() - } - context("when variation and version match") { - it("returns true") { - originalFlags.forEach { _, originalFlag in - otherFlag = FeatureFlag(copying: originalFlag) - - expect(originalFlag == otherFlag).to(beTrue()) - } - } - } - context("when keys differ") { - it("returns false") { - originalFlags.forEach { _, originalFlag in - otherFlag = FeatureFlag(flagKey: "dummyFlagKey", - value: originalFlag.value, - variation: originalFlag.variation, - version: originalFlag.version, - flagVersion: originalFlag.flagVersion, - trackEvents: originalFlag.trackEvents, - debugEventsUntilDate: originalFlag.debugEventsUntilDate, - reason: DarklyServiceMock.Constants.reason, - trackReason: false) - - expect(originalFlag == otherFlag).to(beFalse()) - } - } - } - 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. - originalFlags.forEach { _, originalFlag in - if originalFlag.value == nil { - return - } - otherFlag = FeatureFlag(copying: originalFlag, value: DarklyServiceMock.FlagValues.alternate(originalFlag.value)) - - expect(originalFlag == otherFlag).to(beTrue()) - } - } - } - context("when variations differ") { - context("when both variations exist") { - it("returns false") { - originalFlags.forEach { _, originalFlag in - otherFlag = FeatureFlag(copying: originalFlag, variation: DarklyServiceMock.Constants.variation + 1) - - expect(originalFlag == otherFlag).to(beFalse()) - } - } - } - context("when one variation is missing") { - beforeEach { - originalFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false) - } - it("returns false") { - originalFlags.forEach { _, originalFlag in - otherFlag = FeatureFlag(copying: originalFlag, variation: DarklyServiceMock.Constants.variation) - - expect(originalFlag == otherFlag).to(beFalse()) - } - } - } - } - context("when versions differ") { - context("when both versions exist") { - it("returns false") { - originalFlags.forEach { _, originalFlag in - otherFlag = FeatureFlag(copying: originalFlag, version: DarklyServiceMock.Constants.version + 1) - - expect(originalFlag == otherFlag).to(beFalse()) - } - } - } - context("when one version is missing") { - beforeEach { - originalFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVersions: false) - } - it("returns false") { - originalFlags.forEach { _, originalFlag in - otherFlag = FeatureFlag(copying: originalFlag, version: DarklyServiceMock.Constants.version) - - expect(originalFlag == otherFlag).to(beFalse()) - } - } - } - } - context("when flagVersions differ") { - it("returns true") { - originalFlags.forEach { _, originalFlag in - otherFlag = FeatureFlag(copying: originalFlag, flagVersion: DarklyServiceMock.Constants.flagVersion + 1) - - expect(originalFlag == otherFlag).to(beTrue()) - } - } - } - context("when trackEvents differ") { - it("returns true") { - originalFlags.forEach { _, originalFlag in - otherFlag = FeatureFlag(copying: originalFlag, trackEvents: false) - - expect(originalFlag == otherFlag).to(beTrue()) - } - } - } - context("when debugEventsUntilDate differ") { - it("returns true") { - originalFlags.forEach { _, originalFlag in - otherFlag = FeatureFlag(copying: originalFlag, debugEventsUntilDate: Date()) - - expect(originalFlag == otherFlag).to(beTrue()) - } - } - } - } - context("when value only exists") { - beforeEach { - originalFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false) - } - it("returns true") { - originalFlags.forEach { flagKey, originalFlag in - otherFlag = FeatureFlag(flagKey: flagKey, value: originalFlag.value, trackReason: false) - - expect(originalFlag == otherFlag).to(beTrue()) - } - } - } + func testEncodeFull() { + 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 + XCTAssertEqual(value.count, 9) + XCTAssertEqual(value["key"], "flag-key") + XCTAssertEqual(value["value"], [1, 2, 3]) + XCTAssertEqual(value["variation"], 2) + XCTAssertEqual(value["version"], 3) + XCTAssertEqual(value["flagVersion"], 4) + XCTAssertEqual(value["trackEvents"], true) + XCTAssertEqual(value["debugEventsUntilDate"], .number(Double(now.millisSince1970))) + XCTAssertEqual(value["reason"], ["kind": "OFF"]) + XCTAssertEqual(value["trackReason"], true) } } - 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 - } - } - 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 - } - } - 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 - } - } - } - 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 - } - } - } - 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 - } - } + func testEncodeOmitsDefaults() { + let flag = FeatureFlag(flagKey: "flag-key", trackEvents: false, trackReason: false) + encodesToObject(flag) { value in + XCTAssertEqual(value.count, 1) + XCTAssertEqual(value["key"], "flag-key") } } - func collectionSpec() { - describe("dictionaryValue") { - var featureFlags: [LDFlagKey: FeatureFlag]! - var featureFlagDictionaries: [LDFlagKey: Any]! - var featureFlagDictionary: [String: Any]? - context("when not excising nil values") { - context("with elements") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags() - - featureFlagDictionaries = featureFlags.dictionaryValue - } - it("creates a matching dictionary that includes nil representations") { - featureFlags.forEach { flagKey, featureFlag in - featureFlagDictionary = featureFlagDictionaries[flagKey] as? [String: Any] - - expect(featureFlagDictionary).toNot(beNil()) - expect(featureFlagDictionary?.flagKey) == featureFlag.flagKey - expect(AnyComparer.isEqual(featureFlagDictionary?.value, to: featureFlag.value, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlagDictionary?.variation) == featureFlag.variation - expect(featureFlagDictionary?.version) == featureFlag.version - expect(featureFlagDictionary?.flagVersion) == featureFlag.flagVersion - } - } - } - context("without elements") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false) - - featureFlagDictionaries = featureFlags.dictionaryValue - } - it("creates a matching dictionary that includes nil representations") { - featureFlags.forEach { flagKey, featureFlag in - featureFlagDictionary = featureFlagDictionaries[flagKey] as? [String: Any] - - expect(featureFlagDictionary).toNot(beNil()) - expect(featureFlagDictionary?.flagKey) == featureFlag.flagKey - expect(AnyComparer.isEqual(featureFlagDictionary?.value, to: featureFlag.value, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlagDictionary?.variation).to(beNil()) - expect(featureFlagDictionary?.version).to(beNil()) - expect(featureFlagDictionary?.flagVersion).to(beNil()) - } - } - } - } - context("when excising nil values") { - context("with elements") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: true) - - featureFlagDictionaries = featureFlags.dictionaryValue.withNullValuesRemoved - } - it("creates a matching dictionary that excludes nil value representations") { - featureFlags.forEach { flagKey, featureFlag in - featureFlagDictionary = featureFlagDictionaries[flagKey] as? [String: Any] - - expect(featureFlagDictionary?.flagKey) == featureFlag.flagKey - if featureFlag.value == nil { - expect(featureFlagDictionary?.value).to(beNil()) - } else { - expect(AnyComparer.isEqual(featureFlagDictionary?.value, to: featureFlag.value)).to(beTrue()) - } - expect(featureFlagDictionary?.variation) == featureFlag.variation - expect(featureFlagDictionary?.version) == featureFlag.version - expect(featureFlagDictionary?.flagVersion) == featureFlag.flagVersion - } - } - } - context("without elements") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: true, includeVariations: false, includeVersions: false, includeFlagVersions: false) - featureFlagDictionaries = featureFlags.dictionaryValue.withNullValuesRemoved - } - it("creates a matching dictionary that includes nil representations") { - featureFlags.forEach { flagKey, featureFlag in - featureFlagDictionary = featureFlagDictionaries[flagKey] as? [String: Any] + func testFlagCollectionDecodeValid() throws { + let testData: LDValue = ["key1": [:], "key2": ["key": "key2"]] + let flagCollection = try JSONDecoder().decode(FeatureFlagCollection.self, from: JSONEncoder().encode(testData)) + XCTAssertEqual(flagCollection.flags.count, 2) + XCTAssertEqual(flagCollection.flags["key1"]?.flagKey, "key1") + XCTAssertEqual(flagCollection.flags["key2"]?.flagKey, "key2") + } - expect(featureFlagDictionary?.flagKey) == featureFlag.flagKey - if featureFlag.value is NSNull { - expect(featureFlagDictionary?.value).to(beNil()) - } else { - expect(AnyComparer.isEqual(featureFlagDictionary?.value, to: featureFlag.value)).to(beTrue()) - } - expect(featureFlagDictionary?.variation).to(beNil()) - expect(featureFlagDictionary?.version).to(beNil()) - expect(featureFlagDictionary?.flagVersion).to(beNil()) - } - } - } - } + func testFlagCollectionDecodeConflicting() throws { + let testData = try JSONEncoder().encode(["flag-key": ["key": "flag-key2"]] as LDValue) + XCTAssertThrowsError(try JSONDecoder().decode(FeatureFlagCollection.self, from: testData)) { err in + guard let err = err as? DecodingError, case .dataCorrupted = err + else { return XCTFail("Expected data corrupted error") } } + } - describe("flagCollection") { - var flagDictionaries: [LDFlagKey: Any]! - var flagDictionary: [String: Any]? - var featureFlags: [LDFlagKey: FeatureFlag]? - var featureFlag: FeatureFlag? - context("dictionary has feature flag elements") { - beforeEach { - flagDictionaries = DarklyServiceMock.Constants.stubFeatureFlags().dictionaryValue - - featureFlags = flagDictionaries.flagCollection - } - it("creates matching FeatureFlags with flag elements") { - flagDictionaries.forEach { flagKey, object in - flagDictionary = object as? [String: Any] - featureFlag = featureFlags?[flagKey] - - expect(featureFlag?.flagKey) == flagDictionary?.flagKey - expect(AnyComparer.isEqual(featureFlag?.value, to: flagDictionary?.value, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlag?.variation) == flagDictionary?.variation - expect(featureFlag?.version) == flagDictionary?.version - expect(featureFlag?.flagVersion) == flagDictionary?.flagVersion - } - } - } - context("dictionary has flag values without nil version placeholders") { - beforeEach { - flagDictionaries = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, includeVariations: false, includeVersions: false, includeFlagVersions: false) - .dictionaryValue.withNullValuesRemoved - - featureFlags = flagDictionaries.flagCollection - } - it("creates matching FeatureFlags without missing elements") { - flagDictionaries.forEach { flagKey, object in - flagDictionary = object as? [String: Any] - featureFlag = featureFlags?[flagKey] - - expect(featureFlag?.flagKey) == flagDictionary?.flagKey - expect(AnyComparer.isEqual(featureFlag?.value, to: flagDictionary?.value, considerNilAndNullEqual: true)).to(beTrue()) - expect(featureFlag?.variation).to(beNil()) - expect(featureFlag?.version).to(beNil()) - expect(featureFlag?.flagVersion).to(beNil()) - } - } - } - context("dictionary already has FeatureFlag values") { - beforeEach { - flagDictionaries = DarklyServiceMock.Constants.stubFeatureFlags() - - featureFlags = flagDictionaries.flagCollection - } - it("returns the existing FeatureFlag dictionary") { - expect(featureFlags == flagDictionaries).to(beTrue()) - } - } - context("dictionary does not convert into FeatureFlags") { - beforeEach { - flagDictionaries = Dictionary(flagKey: nil, value: true, variation: 1, version: 2, flagVersion: 3, trackEvents: nil) - - featureFlags = flagDictionaries.flagCollection - } - it("returns nil") { - expect(featureFlags).to(beNil()) - } + func testFlagCollectionEncoding() { + encodesToObject(FeatureFlagCollection(["flag-key": FeatureFlag(flagKey: "flag-key")])) { values in + XCTAssertEqual(values.count, 1) + valueIsObject(values["flag-key"]) { flagValue in + XCTAssertEqual(flagValue["key"], "flag-key") } } } -} -extension Dictionary where Key == String, Value == Any { - init(flagKey: String?, value: Any?, variation: Int?, version: Int?, flagVersion: Int?, trackEvents: Bool?, includeExtraDictionaryItems: Bool = false) { - self.init() - if let flagKey = flagKey { - self[FeatureFlag.CodingKeys.flagKey.rawValue] = flagKey - } - if let value = value { - self[FeatureFlag.CodingKeys.value.rawValue] = value - } - if let variation = variation { - self[FeatureFlag.CodingKeys.variation.rawValue] = variation - } - if let version = version { - self[FeatureFlag.CodingKeys.version.rawValue] = version - } - if let flagVersion = flagVersion { - self[FeatureFlag.CodingKeys.flagVersion.rawValue] = flagVersion - } - if let trackEvents = trackEvents { - self[FeatureFlag.CodingKeys.trackEvents.rawValue] = trackEvents - } - if includeExtraDictionaryItems { - self[FeatureFlagSpec.Constants.extraDictionaryKey] = FeatureFlagSpec.Constants.extraDictionaryValue - } + func testShouldCreateDebugEvents() { + // When debugEventsUntilDate doesn't exist should yield false + var flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: nil) + XCTAssertFalse(flag.shouldCreateDebugEvents(lastEventReportResponseTime: Date())) + // When lastEventReportResponseTime is nil should use current system time + flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(1.0)) + XCTAssertTrue(flag.shouldCreateDebugEvents(lastEventReportResponseTime: nil)) + flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(-1.0)) + XCTAssertFalse(flag.shouldCreateDebugEvents(lastEventReportResponseTime: nil)) + // Otherwise should use lastEventReportResponseTime + let lastEventResponseDate = Date().addingTimeInterval(-30.0) + flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(-29.0)) + XCTAssertTrue(flag.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate)) + flag = FeatureFlag(flagKey: "test-key", trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(-31.0)) + XCTAssertFalse(flag.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate)) } -} -extension AnyComparer { - static func isEqual(_ value: Any?, to other: Any?, considerNilAndNullEqual: Bool = false) -> 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/FlagChange/FlagChangeObserverSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagChange/FlagChangeObserverSpec.swift index 5c30ae87..e3f6c86b 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 @@ -46,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)) - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift index 68a161af..3186dc39 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift @@ -1,231 +1,192 @@ -// -// FlagCounterSpec.swift -// LaunchDarklyTests -// -// Copyright © 2018 Catamorphic Co. All rights reserved. -// - import Foundation 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() - XCTAssertNil(flagCounter.defaultValue) + XCTAssertEqual(flagCounter.defaultValue, .null) XCTAssert(flagCounter.flagValueCounters.isEmpty) } func testTrackRequestInitialKnown() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() - 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 - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === 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 = Placeholder() - let defaultValue = Placeholder() 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) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === 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 = Placeholder() - let defaultValue = Placeholder() 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) - 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.valueCounterVersion, 5) - XCTAssertEqual(counter1.valueCounterVariation, 2) - XCTAssertNil(counter1.valueCounterIsUnknown) - XCTAssertEqual(counter1.valueCounterCount, 1) - - XCTAssert(counter2.valueCounterReportedValue as! Placeholder === 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 = Placeholder() - let defaultValue = Placeholder() 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) - 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.valueCounterVersion, 3) - XCTAssertEqual(counter1.valueCounterVariation, 2) - XCTAssertNil(counter1.valueCounterIsUnknown) - XCTAssertEqual(counter1.valueCounterCount, 1) - - XCTAssert(counter2.valueCounterReportedValue as! Placeholder === reportedValue) - XCTAssertEqual(counter2.valueCounterVersion, 5) - XCTAssertEqual(counter2.valueCounterVariation, 2) - XCTAssertNil(counter2.valueCounterIsUnknown) - XCTAssertEqual(counter2.valueCounterCount, 1) - } - - func testTrackRequestKnownMissingFlagVersionsMatchingVersions() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() + 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 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 - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === reportedValue) - XCTAssertEqual(counter.valueCounterVersion, 10) - XCTAssertEqual(counter.valueCounterVariation, 2) - XCTAssertNil(counter.valueCounterIsUnknown) - XCTAssertEqual(counter.valueCounterCount, 2) - } - - func testTrackRequestKnownMissingFlagVersionsDifferentVersions() { - let reportedValue = Placeholder() - let defaultValue = Placeholder() - 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) - 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.valueCounterVersion, 5) - XCTAssertEqual(counter1.valueCounterVariation, 2) - XCTAssertNil(counter1.valueCounterIsUnknown) - XCTAssertEqual(counter1.valueCounterCount, 1) - - XCTAssert(counter2.valueCounterReportedValue as! Placeholder === 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 = Placeholder() - let defaultValue = Placeholder() let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: nil, defaultValue: defaultValue) - let result = flagCounter.dictionaryValue - XCTAssert(result.flagCounterDefaultValue as! Placeholder === defaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === 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 = Placeholder() - let defaultValue = Placeholder() 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) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === reportedValue) - XCTAssertNil(counter.valueCounterVersion) - XCTAssertNil(counter.valueCounterVariation) - XCTAssertEqual(counter.valueCounterIsUnknown, true) - XCTAssertEqual(counter.valueCounterCount, 2) - } - - func testTrackRequestSecondUnknownWithDifferentValues() { - let initialReportedValue = Placeholder() - let initialDefaultValue = Placeholder() - let secondReportedValue = Placeholder() - let secondDefaultValue = Placeholder() + 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 - XCTAssert(result.flagCounterDefaultValue as! Placeholder === secondDefaultValue) - let counters = result.flagCounterFlagValueCounters - XCTAssertEqual(counters?.count, 1) - let counter = counters![0] - XCTAssert(counter.valueCounterReportedValue as! Placeholder === 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) + } + } + } } } -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) 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] - } - var valueCounterVariation: Int? { - self[FlagCounter.CodingKeys.variation.rawValue] as? Int - } - var valueCounterVersion: Int? { - self[FlagCounter.CodingKeys.version.rawValue] as? Int - } - var valueCounterIsUnknown: Bool? { - self[FlagCounter.CodingKeys.unknown.rawValue] as? Bool - } - var valueCounterCount: Int? { - self[FlagCounter.CodingKeys.count.rawValue] as? Int - } - var flagCounterDefaultValue: Any? { - self[FlagCounter.CodingKeys.defaultValue.rawValue] - } - 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 7f8184cd..55d7a69e 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 @@ -13,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) @@ -22,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() { @@ -89,26 +68,20 @@ extension FlagRequestTracker { } } -extension FlagRequestTracker: Equatable { - public static func == (lhs: FlagRequestTracker, rhs: FlagRequestTracker) -> Bool { - if !lhs.startDate.isWithin(0.001, of: rhs.startDate) { - return false - } - return lhs.flagCounters == rhs.flagCounters +extension LDFlagKey { + var isKnown: Bool { + DarklyServiceMock.FlagKeys.knownFlags.contains(self) } } -extension Dictionary where Key == String, Value == Any { - var flagRequestTrackerStartDateMillis: Int64? { - self[FlagRequestTracker.CodingKeys.startDate.rawValue] as? Int64 - } - 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/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index 4f3c3baa..baca06dc 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 @@ -24,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 @@ -56,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 }), @@ -83,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 a557f156..4cc99f1e 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 @@ -12,81 +5,43 @@ import Nimble final class LDUserSpec: QuickSpec { - struct Constants { - fileprivate static let userCount = 3 - } - override func spec() { initSpec() - dictionaryValueSpec() - isEqualSpec() } private func initSpec() { initSubSpec() - initFromDictionarySpec() initWithEnvironmentReporterSpec() } 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.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.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 } context("called without optional elements") { var environmentReporter: EnvironmentReporter! @@ -105,17 +60,17 @@ 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.privateAttributes).to(beNil()) + expect(user.custom.count) == 2 + expect(user.custom["device"]) == .string(environmentReporter.deviceModel) + expect(user.custom["os"]) == .string(environmentReporter.systemVersion) + expect(user.privateAttributes).to(beEmpty()) expect(user.secondary).to(beNil()) } } context("called without a key multiple times") { var users = [LDUser]() beforeEach { - while users.count < Constants.userCount { + while users.count < 3 { users.append(LDUser()) } } @@ -130,93 +85,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.privatizableAttributes - 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(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.privateAttributes) == LDUser.privatizableAttributes - } - } - 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.device).toNot(beNil()) - expect(user.operatingSystem).toNot(beNil()) - - expect(user.custom).toNot(beNil()) - expect(user.customWithoutSdkSetAttributes.isEmpty) == true - expect(user.privateAttributes).to(beNil()) - } - } - 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.device).to(beNil()) - expect(user.operatingSystem).to(beNil()) - expect(user.custom).to(beNil()) - expect(user.privateAttributes).to(beNil()) - } - } - } - } - private func initWithEnvironmentReporterSpec() { describe("initWithEnvironmentReporter") { var user: LDUser! @@ -237,800 +105,12 @@ 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["device"]) == .string(environmentReporter.deviceModel) + expect(user.custom["os"]) == .string(environmentReporter.systemVersion) - expect(user.custom).to(beNil()) - expect(user.privateAttributes).to(beNil()) + expect(user.privateAttributes).to(beEmpty()) } } } - - 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() { - 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 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() - } - } - 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: 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() - } - } - 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("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) - } - } - } - } - } - 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()) - - expect({ user.customPrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - expect({ user.customPublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - } - - // 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! - } - 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()) - - expect({ user.customPrivateKeysAppearInPrivateAttrsWhenRedacted(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - expect({ user.customPublicOrMissingKeysDontAppearInPrivateAttrs(userDictionary: userDictionary, - privateAttributes: privateAttributesForTest) }).to(match()) - } - - // creates a dictionary without flag config - expect(userDictionary.flagConfig).to(beNil()) - } - } - } - 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()) - - // creates a dictionary without redacted attributes - expect(userDictionary.redactedAttributes).to(beNil()) - - // creates a dictionary without flag config - expect(userDictionary.flagConfig).to(beNil()) - } - } - } - } - } - 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("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("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) - } - } - } - } - } - } - } - - private func isEqualSpec() { - var user: LDUser! - var otherUser: LDUser! - - 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 - } - } - context("with no properties set") { - it("returns true") { - user = LDUser() - otherUser = user - expect(user.isEqual(to: otherUser)) == true - } - } - } - 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", true, ["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") { - 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 { - 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 - return dictionary - } -} - -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] - } } diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index a7f9e506..77831af1 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 @@ -33,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 @@ -48,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 @@ -67,7 +51,7 @@ final class DarklyServiceSpec: QuickSpec { flagRequestEtagSpec() clearFlagRequestCacheSpec() createEventSourceSpec() - publishEventDictionariesSpec() + publishEventDataSpec() diagnosticCacheSpec() publishDiagnosticSpec() @@ -126,12 +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()) - 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 = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedUser } else { fail("request path is missing") } @@ -183,12 +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()) - 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 = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedUser } else { fail("request path is missing") } @@ -562,7 +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()) - expect(LDUser(base64urlEncodedString: receivedArguments!.url.lastPathComponent)?.isEqual(to: testContext.user)) == 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()) @@ -582,28 +559,30 @@ 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 = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(try? JSONDecoder().decode(LDValue.self, from: receivedArguments!.connectBody!)) == expectedUser } } } } - 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() } @@ -625,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() } @@ -646,27 +625,9 @@ final class DarklyServiceSpec: QuickSpec { var responses: ServiceResponses! var eventsPublished = false beforeEach { - testContext = TestContext(mobileKey: "", useReport: Constants.useGetMethod, includeMockEventDictionaries: true) + testContext = TestContext(mobileKey: "", useReport: Constants.useGetMethod) 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.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 } @@ -797,22 +758,15 @@ final class DarklyServiceSpec: QuickSpec { private extension Data { var flagCollection: [LDFlagKey: FeatureFlag]? { - guard let flagDictionary = try? JSONSerialization.jsonDictionary(with: self, options: .allowFragments) - else { return nil } - return flagDictionary.flagCollection + return (try? JSONDecoder().decode(FeatureFlagCollection.self, from: self))?.flags } } -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) +private extension String { + var jsonValue: LDValue? { + let base64encodedString = self.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") + guard let data = Data(base64Encoded: base64encodedString) else { return nil } - self.init(userDictionary: userDictionary) + return try? JSONDecoder().decode(LDValue.self, from: data) } } 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 8fc6e45d..1d0eb99d 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift @@ -1,125 +1,34 @@ -// -// CacheConverterSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - import Foundation -import Quick -import Nimble +import XCTest + @testable import LaunchDarkly -final class CacheConverterSpec: QuickSpec { - struct TestContext { - var clientServiceFactoryMock = ClientServiceMockFactory() - var cacheConverter: CacheConverter - var user: LDUser - var config: LDConfig - var featureFlagCachingMock: FeatureFlagCachingMock { - clientServiceFactoryMock.makeFeatureFlagCacheReturnValue - } - var expiredCacheThreshold: Date +final class CacheConverterSpec: XCTestCase { + + private var serviceFactory: ClientServiceMockFactory! - init(createCacheData: Bool = false, deprecatedCacheData: DeprecatedCacheModel? = nil) { - cacheConverter = CacheConverter(serviceFactory: clientServiceFactoryMock, maxCachedUsers: LDConfig.Defaults.maxCachedUsers) - expiredCacheThreshold = Date().addingTimeInterval(CacheConverter.Constants.maxAge) - if createCacheData { - let (users, userEnvironmentFlagsCollection, mobileKeys) = CacheableUserEnvironmentFlags.stubCollection() - user = users[users.count / 2] - config = LDConfig(mobileKey: mobileKeys[mobileKeys.count / 2], environmentReporter: EnvironmentReportingMock()) - featureFlagCachingMock.retrieveFeatureFlagsReturnValue = userEnvironmentFlagsCollection[user.key]?.environmentFlags[config.mobileKey]?.featureFlags - } else { - user = LDUser.stub() - config = LDConfig.stub - featureFlagCachingMock.retrieveFeatureFlagsReturnValue = nil - } - DeprecatedCacheModel.allCases.forEach { model in - deprecatedCacheMock(for: model).retrieveFlagsReturnValue = (nil, nil) - } - if let deprecatedCacheData = deprecatedCacheData { - let age = Date().addingTimeInterval(CacheConverter.Constants.maxAge + 1.0) - deprecatedCacheMock(for: deprecatedCacheData).retrieveFlagsReturnValue = (FlagMaintainingMock.stubFlags(), age) - } - } + private static var upToDateData: Data! - func deprecatedCacheMock(for version: DeprecatedCacheModel) -> 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, .version4, .version3, .version2, 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? - .isWithin(0.5, of: testContext.expiredCacheThreshold)) == true - } - } - 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 ae48c41d..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift +++ /dev/null @@ -1,160 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -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] -} - -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 { tuple1, tuple2 in - tuple1.lastUpdated.isEarlierThan(tuple2.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 { tuple in - tuple.lastUpdated.isEarlierThan(expirationDate) ? tuple.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 - } - } - } - 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 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/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 deleted file mode 100644 index a274c01d..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// DeprecatedCacheModelV5Spec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DeprecatedCacheModelV5Spec: QuickSpec, CacheModelTestInterface { - let cacheKey = DeprecatedCacheModelV5.CacheKeys.userEnvironments - let supportsMultiEnv = true - - 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 = dictionaryValueWithAllAttributes() - userDictionary.setLastUpdated(lastUpdated) - 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/DiagnosticCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift index fb233483..91cc6a08 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 @@ -94,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()) @@ -260,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/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 88f58642..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// KeyedValueCacheSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - -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 19654032..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift +++ /dev/null @@ -1,277 +0,0 @@ -// -// UserEnvironmentCacheSpec.swift -// LaunchDarklyTests -// -// Copyright © 2019 Catamorphic Co. All rights reserved. -// - -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.isEarlierThan($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/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/EnvironmentReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift index d7183e79..500057c0 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 @@ -17,27 +10,9 @@ final class EnvironmentReporterSpec: QuickSpec { } private func integrationHarnessSpec() { - 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. -// context("for the integration harness") { -// beforeEach { -// environmentReporter = EnvironmentReporter() -// } -// it("should not throttle online calls") { -// expect(environmentReporter.shouldThrottleOnlineCalls) == false -// } -// } - context("not for the integration harness") { - beforeEach { - environmentReporter = EnvironmentReporter() - } - it("should throttle online calls") { - expect(environmentReporter.shouldThrottleOnlineCalls) == true - } - } + it("should throttle online calls for release build") { + expect(EnvironmentReporter().shouldThrottleOnlineCalls) == false } } } 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..1e0d177e 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -1,21 +1,12 @@ -// -// EventReporterSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - import Foundation import Quick import Nimble -import XCTest @testable import LaunchDarkly final class EventReporterSpec: QuickSpec { struct Constants { static let eventFlushInterval: TimeInterval = 10.0 static let eventFlushIntervalHalfSecond: TimeInterval = 0.5 - static let defaultValue = false } struct TestContext { @@ -24,15 +15,9 @@ 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! - var featureFlagWithReason: FeatureFlag! - 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, @@ -42,8 +27,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 @@ -68,11 +51,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) { @@ -82,21 +60,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() } @@ -112,7 +82,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 } } } @@ -135,7 +105,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") { @@ -148,7 +118,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") { @@ -160,7 +130,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 } } } @@ -174,7 +144,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") { @@ -186,8 +156,8 @@ 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.eventReporter.eventStoreKeys) == testContext.eventKeys + expect(testContext.serviceMock.publishEventDataCallCount) == 0 + expect(testContext.eventReporter.eventStore) == testContext.events } } } @@ -204,8 +174,8 @@ 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.eventReporter.eventStoreKeys) == testContext.eventKeys + expect(testContext.serviceMock.publishEventDataCallCount) == 0 + expect(testContext.eventReporter.eventStore) == testContext.events } it("does not record a dropped event to diagnosticCache") { expect(testContext.diagnosticCache.incrementDroppedEventCountCallCount) == 0 @@ -222,9 +192,8 @@ 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.eventReporter.eventStoreKeys) == testContext.eventKeys - expect(testContext.eventReporter.eventStoreKeys.contains(extraEvent.key!)) == false + expect(testContext.serviceMock.publishEventDataCallCount) == 0 + expect(testContext.eventReporter.eventStore) == testContext.events } it("records a dropped event to diagnosticCache") { expect(testContext.diagnosticCache.incrementDroppedEventCountCallCount) == 1 @@ -243,11 +212,14 @@ 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") { 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 @@ -260,28 +232,26 @@ 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 + erOnline() + 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 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") { 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 @@ -293,28 +263,20 @@ 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 + 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 - 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") { 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 @@ -326,22 +288,21 @@ 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 + erOnline() + 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 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,19 +317,13 @@ final class EventReporterSpec: QuickSpec { } } it("does not report events") { - expect(testContext.eventReporter.isOnline) == true - expect(testContext.eventReporter.isReportingActive) == true - expect(testContext.serviceMock.publishEventDictionariesCallCount) == 0 + erOnline() + expect(testContext.serviceMock.publishEventDataCallCount) == 0 expect(testContext.diagnosticCache.recordEventsInLastBatchCallCount) == 0 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()) } } } @@ -387,18 +342,22 @@ 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 + 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!) + 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()) 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 @@ -420,19 +379,23 @@ 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 + 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!) + 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()) 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 @@ -456,18 +419,22 @@ 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 + 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!) + 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()) 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 @@ -492,13 +459,12 @@ 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 + expect(testContext.eventReporter.eventStore) == testContext.events 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 @@ -508,301 +474,138 @@ 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! - 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!, - 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.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 - } - } - 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!, - 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.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 - } - } - context("when trackEvents is off") { - beforeEach { - testContext = TestContext(trackEvents: false) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: 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()) - } - 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 - } - } - 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) - } - 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 - } - } - 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) - } - 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 - } - } - } - 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) - } - 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 - } - } - 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) - } - 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 - } - } - } + 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 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) - } - 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])) - } - 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 - } + 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 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) - } - 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])) - } - 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 - } + 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 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) - } - 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 - } + 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 eventTrackingContext is nil") { - beforeEach { - testContext = TestContext(trackEvents: nil) - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: 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 - } + 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 multiple flag requests are made") { - context("serially") { - beforeEach { - testContext = TestContext(trackEvents: false) - for _ in 1...3 { - testContext.eventReporter.recordFlagEvaluationEvents(flagKey: testContext.flagKey, - value: 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).toNot(beNil()) - expect(AnyComparer.isEqual(flagValueCounter?.value, to: testContext.featureFlag.value)).to(beTrue()) - expect(flagValueCounter?.count) == 3 - } - } - context("concurrently") { - let requestQueue = DispatchQueue(label: "com.launchdarkly.test.eventReporterSpec.flagRequestTracking.concurrent", qos: .userInitiated, attributes: .concurrent) - 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: testContext.featureFlag.value!, - defaultValue: Constants.defaultValue, - featureFlag: testContext.featureFlag, - user: testContext.user, - includeReason: false) - recordFlagEvaluationCompletion() - } - } - } - } - 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) == 5 - } - } + 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" } - } - } - - private func trackFlagRequestSpec() { - context("record summary event") { - 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) + 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" } - 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?.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?.count) == 1 + 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)) + + 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 + 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" } } } @@ -820,25 +623,25 @@ 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") { - 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.publishEventDictionariesCallCount) == 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() } } } @@ -846,15 +649,6 @@ final class EventReporterSpec: QuickSpec { } } -extension EventReporter { - var eventStoreKeys: [String] { eventStore.compactMap { $0.key } } - 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 @@ -868,11 +662,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) } } - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift index 44024fca..33576b86 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 @@ -41,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 @@ -61,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 @@ -78,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 @@ -425,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 } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift index a37bc73b..591f7cad 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift @@ -1,234 +1,85 @@ -// -// FlagStoreSpec.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - 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 507d5060..722916cd 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 @@ -14,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 } @@ -35,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 @@ -46,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() { @@ -121,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 } } } @@ -181,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 } } } @@ -310,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 @@ -404,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))") } } } @@ -438,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))") } } } @@ -505,68 +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()) - 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()) - 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))") } } } @@ -582,58 +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()) - expect(flagDictionary == FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1)).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))") } } } @@ -647,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") { @@ -689,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") { @@ -731,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") { @@ -765,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") { @@ -785,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") { @@ -805,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") { @@ -833,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") { @@ -854,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") { @@ -892,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 @@ -940,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) @@ -999,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() } } @@ -1011,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 { @@ -1027,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 } @@ -1048,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 @@ -1066,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 @@ -1084,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 } @@ -1105,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 @@ -1123,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 @@ -1146,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/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() } 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/ServiceObjects/ThrottlerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift index 49717f9f..daafa73f 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift @@ -122,7 +122,9 @@ final class ThrottlerSpec: QuickSpec { func maxDelaySpec() { it("limits delay to maxDelay") { - let throttler = Throttler(maxDelay: 1.0) + let envReporter = EnvironmentReportingMock() + envReporter.shouldThrottleOnlineCalls = true + let throttler = Throttler(maxDelay: 1.0, environmentReporter: envReporter) (0..<10).forEach { _ in throttler.runThrottled { } } let callDate = Date() var runDate: Date? diff --git a/LaunchDarkly/LaunchDarklyTests/TestUtil.swift b/LaunchDarkly/LaunchDarklyTests/TestUtil.swift index 6fe9e6fd..523fa82a 100644 --- a/LaunchDarkly/LaunchDarklyTests/TestUtil.swift +++ b/LaunchDarkly/LaunchDarklyTests/TestUtil.swift @@ -1,13 +1,8 @@ -// -// TestUtil.swift -// LaunchDarklyTests -// -// Copyright © 2020 Catamorphic Co. All rights reserved. -// - import XCTest import Foundation +@testable import LaunchDarkly + func symmetricAssertEqual(_ exp1: @autoclosure () throws -> T, _ exp2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "") { @@ -21,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) +} diff --git a/README.md b/README.md index 54afd70f..3f872bc5 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) @@ -49,7 +49,7 @@ To include LaunchDarkly in a Swift package, simply add it to the dependencies se ```swift dependencies: [ - .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor(from: "5.4.5")) + .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor(from: "6.0.0")) ] ``` @@ -60,7 +60,7 @@ To use the [CocoaPods](https://cocoapods.org) dependency manager to integrate La ```ruby use_frameworks! target 'YourTargetName' do - pod 'LaunchDarkly', '~> 5.4' + pod 'LaunchDarkly', '~> 6.0' end ``` @@ -71,7 +71,7 @@ To use the [Carthage](https://github.com/Carthage/Carthage) dependency manager t To integrate LaunchDarkly into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "launchdarkly/ios-client-sdk" ~> 5.4 +github "launchdarkly/ios-client-sdk" ~> 6.0 ``` ### Manual installation 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 %} }