diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index afb3c46e..6adc8c12 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -11,6 +11,10 @@ 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; 29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; 29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; + 29FE1298280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; + 29FE1299280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; + 29FE129A280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; + 29FE129B280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */; }; 830DB3AE2239B54900D65D25 /* URLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AD2239B54900D65D25 /* URLResponse.swift */; }; @@ -21,8 +25,6 @@ 831188442113ADC200D77CB5 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; 831188452113ADC500D77CB5 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; 831188462113ADCA00D77CB5 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; - 831188472113ADCD00D77CB5 /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; - 831188482113ADD100D77CB5 /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 8311884B2113ADDA00D77CB5 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -41,11 +43,8 @@ 8311885D2113AE2500D77CB5 /* DarklyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D8B711F71D3E700ED65E8 /* DarklyService.swift */; }; 8311885E2113AE2900D77CB5 /* HTTPURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C2461FE4071F0082B8A9 /* HTTPURLResponse.swift */; }; 8311885F2113AE2D00D77CB5 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; - 831188602113AE3400D77CB5 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67891F97CFEC00403126 /* Dictionary.swift */; }; 831188622113AE3A00D77CB5 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEF51FA24A7E00E428B6 /* Data.swift */; }; - 831188642113AE4200D77CB5 /* JSONSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */; }; 831188652113AE4600D77CB5 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFD1FA24F9600E428B6 /* Date.swift */; }; - 831188662113AE4A00D77CB5 /* AnyComparer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5611FB4D66600550A95 /* AnyComparer.swift */; }; 831188672113AE4D00D77CB5 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D2AAE2061AAA000B4AC3C /* Thread.swift */; }; 831188682113AE5600D77CB5 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 831188692113AE5900D77CB5 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; @@ -70,8 +69,6 @@ 831EF34420655E730001C643 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; 831EF34520655E730001C643 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; 831EF34620655E730001C643 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; - 831EF34720655E730001C643 /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; - 831EF34820655E730001C643 /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -88,11 +85,8 @@ 831EF35B20655E730001C643 /* DarklyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D8B711F71D3E700ED65E8 /* DarklyService.swift */; }; 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C2461FE4071F0082B8A9 /* HTTPURLResponse.swift */; }; 831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; - 831EF35E20655E730001C643 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67891F97CFEC00403126 /* Dictionary.swift */; }; 831EF36020655E730001C643 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEF51FA24A7E00E428B6 /* Data.swift */; }; - 831EF36220655E730001C643 /* JSONSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */; }; 831EF36320655E730001C643 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFD1FA24F9600E428B6 /* Date.swift */; }; - 831EF36420655E730001C643 /* AnyComparer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5611FB4D66600550A95 /* AnyComparer.swift */; }; 831EF36520655E730001C643 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D2AAE2061AAA000B4AC3C /* Thread.swift */; }; 831EF36620655E730001C643 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 831EF36720655E730001C643 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; @@ -101,10 +95,6 @@ 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832307A51F7D8D720029815A /* URLRequestSpec.swift */; }; 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */; }; 832307AA1F7ECA630029815A /* LDConfigStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832307A91F7ECA630029815A /* LDConfigStub.swift */; }; - 832D689D224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */; }; - 832D689E224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */; }; - 832D689F224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */; }; - 832D68A0224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */; }; 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832D68A1224A38FC005F052A /* CacheConverter.swift */; }; @@ -118,21 +108,11 @@ 8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8347BB0B21F147E100E56BCD /* LDTimer.swift */; }; 8347BB0E21F147E100E56BCD /* LDTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8347BB0B21F147E100E56BCD /* LDTimer.swift */; }; 8347BB0F21F147E100E56BCD /* LDTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8347BB0B21F147E100E56BCD /* LDTimer.swift */; }; - 8354AC612241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */; }; - 8354AC622241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */; }; - 8354AC632241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */; }; - 8354AC642241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */; }; - 8354AC662241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC652241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift */; }; - 8354AC6922418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */; }; - 8354AC6A22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */; }; - 8354AC6B22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */; }; - 8354AC6C22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */; }; - 8354AC6E22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6D22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift */; }; - 8354AC702243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */; }; - 8354AC712243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */; }; - 8354AC722243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */; }; - 8354AC732243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */; }; - 8354AC77224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC76224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift */; }; + 8354AC702243166900CDE602 /* FeatureFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */; }; + 8354AC712243166900CDE602 /* FeatureFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */; }; + 8354AC722243166900CDE602 /* FeatureFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */; }; + 8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */; }; + 8354AC77224316F800CDE602 /* FeatureFlagCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354AC76224316F800CDE602 /* FeatureFlagCacheSpec.swift */; }; 8354EFCC1F22491C00C05156 /* LaunchDarkly.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8354EFC21F22491C00C05156 /* LaunchDarkly.framework */; }; 8354EFE01F26380700C05156 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; 8354EFE11F26380700C05156 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; @@ -147,18 +127,12 @@ 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */; }; 835E4C54206BDF8D004C6E6C /* EnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831425B0206B030100F2EF36 /* EnvironmentReporter.swift */; }; 835E4C57206BF7E3004C6E6C /* LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8354EFC51F22491C00C05156 /* LaunchDarkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 8370DF6C225E40B800F84810 /* DeprecatedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */; }; - 8370DF6D225E40B800F84810 /* DeprecatedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */; }; - 8370DF6E225E40B800F84810 /* DeprecatedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */; }; - 8370DF6F225E40B800F84810 /* DeprecatedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */; }; 8372668C20D4439600BD1088 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8372668B20D4439600BD1088 /* DateFormatter.swift */; }; 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8372668B20D4439600BD1088 /* DateFormatter.swift */; }; 8372668E20D4439600BD1088 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8372668B20D4439600BD1088 /* DateFormatter.swift */; }; 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837406D321F760640087B22B /* LDTimerSpec.swift */; }; 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */; }; 837EF3742059C237009D628A /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837EF3732059C237009D628A /* Log.swift */; }; - 838838411F5EFADF0023D11B /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; - 838838451F5EFBAF0023D11B /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; 838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D8B761F72A4B400ED65E8 /* FlagSynchronizerSpec.swift */; }; 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838F96731FB9F024009CFC45 /* LDClientSpec.swift */; }; 838F96781FBA504A009CFC45 /* ClientServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838F96771FBA504A009CFC45 /* ClientServiceFactory.swift */; }; @@ -177,16 +151,11 @@ 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */; }; 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */; }; 83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */; }; - 83D1523B22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */; }; - 83D17EAA1FCDA18C00B2823C /* DictionarySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */; }; 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */; }; - 83D5597E1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D5597D1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift */; }; 83D9EC752062DEAB004D7FA6 /* LDCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6C4B51F4DE7630055351C /* LDCommon.swift */; }; 83D9EC762062DEAB004D7FA6 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; 83D9EC772062DEAB004D7FA6 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; 83D9EC782062DEAB004D7FA6 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; - 83D9EC792062DEAB004D7FA6 /* LDFlagValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838401F5EFADF0023D11B /* LDFlagValue.swift */; }; - 83D9EC7A2062DEAB004D7FA6 /* LDFlagValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */; }; 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 83D9EC7E2062DEAB004D7FA6 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -203,18 +172,14 @@ 83D9EC8D2062DEAB004D7FA6 /* DarklyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D8B711F71D3E700ED65E8 /* DarklyService.swift */; }; 83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C2461FE4071F0082B8A9 /* HTTPURLResponse.swift */; }; 83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; - 83D9EC902062DEAB004D7FA6 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67891F97CFEC00403126 /* Dictionary.swift */; }; 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEF51FA24A7E00E428B6 /* Data.swift */; }; - 83D9EC942062DEAB004D7FA6 /* JSONSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */; }; 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFD1FA24F9600E428B6 /* Date.swift */; }; - 83D9EC962062DEAB004D7FA6 /* AnyComparer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5611FB4D66600550A95 /* AnyComparer.swift */; }; 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D2AAE2061AAA000B4AC3C /* Thread.swift */; }; 83D9EC982062DEAB004D7FA6 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 83D9EC992062DEAB004D7FA6 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; 83D9EC9A2062DEAB004D7FA6 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */; }; 83D9EC9C2062DEAB004D7FA6 /* ObjcLDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */; }; 83DDBEF61FA24A7E00E428B6 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEF51FA24A7E00E428B6 /* Data.swift */; }; - 83DDBEFC1FA24B2700E428B6 /* JSONSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */; }; 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFD1FA24F9600E428B6 /* Date.swift */; }; 83DDBF001FA2589900E428B6 /* FlagStoreSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */; }; 83E2E2061F9E7AC7007514E9 /* LDUserSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83E2E2051F9E7AC7007514E9 /* LDUserSpec.swift */; }; @@ -223,16 +188,13 @@ 83EBCBB420DABE1B003A7142 /* FlagRequestTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */; }; 83EBCBB520DABE1B003A7142 /* FlagRequestTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */; }; 83EBCBB720DABE93003A7142 /* FlagRequestTrackerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB620DABE93003A7142 /* FlagRequestTrackerSpec.swift */; }; - 83EF678A1F97CFEC00403126 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67891F97CFEC00403126 /* Dictionary.swift */; }; 83EF67931F9945E800403126 /* EventSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67921F9945E800403126 /* EventSpec.swift */; }; 83EF67951F994BAD00403126 /* LDUserStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67941F994BAD00403126 /* LDUserStub.swift */; }; - 83F0A5621FB4D66600550A95 /* AnyComparer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5611FB4D66600550A95 /* AnyComparer.swift */; }; 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */; }; 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FEF8DC1F266742001CF12C /* FlagSynchronizer.swift */; }; 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */; }; B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; }; B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.swift */; }; - B43D5AD025FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43D5ACF25FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift */; }; B467791324D8AEEC00897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791224D8AEEC00897F00 /* LDSwiftEventSourceStatic */; }; B467791524D8AEF300897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791424D8AEF300897F00 /* LDSwiftEventSourceStatic */; }; B467791724D8AEF800897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791624D8AEF800897F00 /* LDSwiftEventSourceStatic */; }; @@ -282,7 +244,6 @@ C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; - C48ED691242D27E200464F5F /* DeprecatedCacheMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48ED690242D27E200464F5F /* DeprecatedCacheMock.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -350,6 +311,7 @@ /* Begin PBXFileReference section */ 29A4C47427DA6266005B8D34 /* UserAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAttribute.swift; sourceTree = ""; }; + 29FE1297280413D4008CC918 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLRequest.swift; sourceTree = ""; }; 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersSpec.swift; sourceTree = ""; }; 830DB3AD2239B54900D65D25 /* URLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponse.swift; sourceTree = ""; }; @@ -368,7 +330,6 @@ 832307A51F7D8D720029815A /* URLRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestSpec.swift; sourceTree = ""; }; 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDEventSourceMock.swift; sourceTree = ""; }; 832307A91F7ECA630029815A /* LDConfigStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDConfigStub.swift; sourceTree = ""; }; - 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV5.swift; sourceTree = ""; }; 832D68A1224A38FC005F052A /* CacheConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverter.swift; sourceTree = ""; }; 832D68AB224B3321005F052A /* CacheConverterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheConverterSpec.swift; sourceTree = ""; }; 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagMaintainingMock.swift; sourceTree = ""; }; @@ -376,12 +337,8 @@ 83396BC81F7C3711000E256E /* DarklyServiceSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarklyServiceSpec.swift; sourceTree = ""; }; 83411A5D1FABDA8700E5CF39 /* mocks.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = mocks.generated.swift; path = LaunchDarkly/GeneratedCode/mocks.generated.swift; sourceTree = SOURCE_ROOT; }; 8347BB0B21F147E100E56BCD /* LDTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDTimer.swift; sourceTree = ""; }; - 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheableEnvironmentFlags.swift; sourceTree = ""; }; - 8354AC652241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheableEnvironmentFlagsSpec.swift; sourceTree = ""; }; - 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheableUserEnvironmentFlags.swift; sourceTree = ""; }; - 8354AC6D22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheableUserEnvironmentFlagsSpec.swift; sourceTree = ""; }; - 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEnvironmentFlagCache.swift; sourceTree = ""; }; - 8354AC76224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEnvironmentFlagCacheSpec.swift; sourceTree = ""; }; + 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagCache.swift; sourceTree = ""; }; + 8354AC76224316F800CDE602 /* FeatureFlagCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagCacheSpec.swift; sourceTree = ""; }; 8354EFC21F22491C00C05156 /* LaunchDarkly.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LaunchDarkly.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8354EFC51F22491C00C05156 /* LaunchDarkly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LaunchDarkly.h; sourceTree = ""; }; 8354EFCB1F22491C00C05156 /* LaunchDarklyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LaunchDarklyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -397,13 +354,10 @@ 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDConfig.swift; sourceTree = ""; }; 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDUser.swift; sourceTree = ""; }; 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDChangedFlag.swift; sourceTree = ""; }; - 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCache.swift; sourceTree = ""; }; 8372668B20D4439600BD1088 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 837406D321F760640087B22B /* LDTimerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDTimerSpec.swift; sourceTree = ""; }; 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentReporterSpec.swift; sourceTree = ""; }; 837EF3732059C237009D628A /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; - 838838401F5EFADF0023D11B /* LDFlagValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDFlagValue.swift; sourceTree = ""; }; - 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDFlagValueConvertible.swift; sourceTree = ""; }; 838F96731FB9F024009CFC45 /* LDClientSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDClientSpec.swift; sourceTree = ""; }; 838F96771FBA504A009CFC45 /* ClientServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientServiceFactory.swift; sourceTree = ""; }; 838F96791FBA551A009CFC45 /* ClientServiceMockFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientServiceMockFactory.swift; sourceTree = ""; }; @@ -420,29 +374,22 @@ 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagsUnchangedObserver.swift; sourceTree = ""; }; 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventReporterSpec.swift; sourceTree = ""; }; 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarklyServiceMock.swift; sourceTree = ""; }; - 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelV5Spec.swift; sourceTree = ""; }; - 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionarySpec.swift; sourceTree = ""; }; 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedValueCache.swift; sourceTree = ""; }; - 83D5597D1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedValueCacheSpec.swift; sourceTree = ""; }; 83D9EC6B2062DBB7004D7FA6 /* LaunchDarkly_watchOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LaunchDarkly_watchOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 83DDBEF51FA24A7E00E428B6 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; - 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONSerialization.swift; sourceTree = ""; }; 83DDBEFD1FA24F9600E428B6 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagStoreSpec.swift; sourceTree = ""; }; 83E2E2051F9E7AC7007514E9 /* LDUserSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDUserSpec.swift; sourceTree = ""; }; 83EBCBB020D9C7B5003A7142 /* FlagCounterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagCounterSpec.swift; sourceTree = ""; }; 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagRequestTracker.swift; sourceTree = ""; }; 83EBCBB620DABE93003A7142 /* FlagRequestTrackerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagRequestTrackerSpec.swift; sourceTree = ""; }; - 83EF67891F97CFEC00403126 /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; 83EF67921F9945E800403126 /* EventSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSpec.swift; sourceTree = ""; }; 83EF67941F994BAD00403126 /* LDUserStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDUserStub.swift; sourceTree = ""; }; - 83F0A5611FB4D66600550A95 /* AnyComparer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyComparer.swift; sourceTree = ""; }; 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDConfigSpec.swift; sourceTree = ""; }; 83FEF8DC1F266742001CF12C /* FlagSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlagSynchronizer.swift; sourceTree = ""; }; 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventReporter.swift; sourceTree = ""; }; B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCacheSpec.swift; sourceTree = ""; }; B4265EB024E7390C001CFD2C /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = ""; }; - B43D5ACF25FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheModelSpec.swift; sourceTree = ""; }; B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = ""; }; B46F344025E6DB7D0078D45F /* DiagnosticReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticReporterSpec.swift; sourceTree = ""; }; B495A8A12787762C0051977C /* LDClientVariation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDClientVariation.swift; sourceTree = ""; }; @@ -455,7 +402,6 @@ C43C37E0236BA050003C1624 /* LDEvaluationDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDEvaluationDetail.swift; sourceTree = ""; }; C443A4092315AA4D00145710 /* NetworkReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkReporter.swift; sourceTree = ""; }; C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionModeChangeObserver.swift; sourceTree = ""; }; - C48ED690242D27E200464F5F /* DeprecatedCacheMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedCacheMock.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -562,33 +508,13 @@ path = LaunchDarkly/GeneratedCode; sourceTree = ""; }; - 8354AC5F224150C300CDE602 /* Cache */ = { - isa = PBXGroup; - children = ( - 8354AC602241511D00CDE602 /* CacheableEnvironmentFlags.swift */, - 8354AC6822418C0600CDE602 /* CacheableUserEnvironmentFlags.swift */, - ); - path = Cache; - sourceTree = ""; - }; - 8354AC672241586D00CDE602 /* Cache */ = { - isa = PBXGroup; - children = ( - 8354AC652241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift */, - 8354AC6D22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift */, - ); - path = Cache; - sourceTree = ""; - }; 8354AC742243168800CDE602 /* Cache */ = { isa = PBXGroup; children = ( C408884623033B3600420721 /* ConnectionInformationStore.swift */, 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */, - 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */, + 8354AC6F2243166900CDE602 /* FeatureFlagCache.swift */, 832D68A1224A38FC005F052A /* CacheConverter.swift */, - 8370DF6B225E40B800F84810 /* DeprecatedCache.swift */, - 832D689C224A3896005F052A /* DeprecatedCacheModelV5.swift */, B4C9D4322489C8FD004A9B03 /* DiagnosticCache.swift */, ); path = Cache; @@ -597,11 +523,8 @@ 8354AC75224316C700CDE602 /* Cache */ = { isa = PBXGroup; children = ( - 83D5597D1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift */, - 8354AC76224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift */, + 8354AC76224316F800CDE602 /* FeatureFlagCacheSpec.swift */, 832D68AB224B3321005F052A /* CacheConverterSpec.swift */, - B43D5ACF25FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift */, - 83D1523A22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift */, B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */, ); path = Cache; @@ -636,6 +559,7 @@ 83B6C4B51F4DE7630055351C /* LDCommon.swift */, 8354EFDC1F26380700C05156 /* LDClient.swift */, B495A8A12787762C0051977C /* LDClientVariation.swift */, + 29FE1297280413D4008CC918 /* Util.swift */, 8354EFE61F263E4200C05156 /* Models */, 83FEF8D91F2666BF001CF12C /* ServiceObjects */, 831D8B701F71D3A600ED65E8 /* Networking */, @@ -667,7 +591,6 @@ 8354EFE61F263E4200C05156 /* Models */ = { isa = PBXGroup; children = ( - 8354AC5F224150C300CDE602 /* Cache */, C408884823033B7500420721 /* ConnectionInformation.swift */, B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */, 8354EFDE1F26380700C05156 /* Event.swift */, @@ -710,7 +633,6 @@ 838F96791FBA551A009CFC45 /* ClientServiceMockFactory.swift */, 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */, 831425AE206ABB5300F2EF36 /* EnvironmentReportingMock.swift */, - C48ED690242D27E200464F5F /* DeprecatedCacheMock.swift */, ); path = Mocks; sourceTree = ""; @@ -718,7 +640,6 @@ 83D17EA81FCDA16300B2823C /* Extensions */ = { isa = PBXGroup; children = ( - 83D17EA91FCDA18C00B2823C /* DictionarySpec.swift */, 83B6E3F0222EFA3800FF2A6A /* ThreadSpec.swift */, ); path = Extensions; @@ -727,11 +648,8 @@ 83E2E2071F9FF9A0007514E9 /* Extensions */ = { isa = PBXGroup; children = ( - 83EF67891F97CFEC00403126 /* Dictionary.swift */, 83DDBEF51FA24A7E00E428B6 /* Data.swift */, - 83DDBEFB1FA24B2700E428B6 /* JSONSerialization.swift */, 83DDBEFD1FA24F9600E428B6 /* Date.swift */, - 83F0A5611FB4D66600550A95 /* AnyComparer.swift */, 831D2AAE2061AAA000B4AC3C /* Thread.swift */, 8372668B20D4439600BD1088 /* DateFormatter.swift */, ); @@ -743,7 +661,6 @@ children = ( 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */, 83EBCB9F20D9A143003A7142 /* FlagChange */, - 83EBCBA020D9A168003A7142 /* FlagValue */, C43C37E0236BA050003C1624 /* LDEvaluationDetail.swift */, 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */, ); @@ -761,15 +678,6 @@ path = FlagChange; sourceTree = ""; }; - 83EBCBA020D9A168003A7142 /* FlagValue */ = { - isa = PBXGroup; - children = ( - 838838401F5EFADF0023D11B /* LDFlagValue.swift */, - 838838441F5EFBAF0023D11B /* LDFlagValueConvertible.swift */, - ); - path = FlagValue; - sourceTree = ""; - }; 83EBCBA620D9A23E003A7142 /* User */ = { isa = PBXGroup; children = ( @@ -812,7 +720,6 @@ 83EBCBA620D9A23E003A7142 /* User */, 83EBCBA720D9A251003A7142 /* FeatureFlag */, 83EF67921F9945E800403126 /* EventSpec.swift */, - 8354AC672241586D00CDE602 /* Cache */, B4F689132497B2FC004D3CE0 /* DiagnosticEventSpec.swift */, ); path = Models; @@ -1186,7 +1093,6 @@ files = ( 83906A7B21190B7700D7D3C5 /* DateFormatter.swift in Sources */, 8311886A2113AE5D00D77CB5 /* ObjcLDUser.swift in Sources */, - 8370DF6F225E40B800F84810 /* DeprecatedCache.swift in Sources */, 831188502113ADEF00D77CB5 /* EnvironmentReporter.swift in Sources */, 831188682113AE5600D77CB5 /* ObjcLDClient.swift in Sources */, 831188572113AE0B00D77CB5 /* FlagChangeNotifier.swift in Sources */, @@ -1196,23 +1102,18 @@ 831188452113ADC500D77CB5 /* LDClient.swift in Sources */, 831188522113ADF700D77CB5 /* KeyedValueCache.swift in Sources */, 831188582113AE0F00D77CB5 /* EventReporter.swift in Sources */, - 831188642113AE4200D77CB5 /* JSONSerialization.swift in Sources */, 8311885D2113AE2500D77CB5 /* DarklyService.swift in Sources */, - 8354AC6C22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, 831188692113AE5900D77CB5 /* ObjcLDConfig.swift in Sources */, - 831188602113AE3400D77CB5 /* Dictionary.swift in Sources */, 8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */, C43C37E8238DF22D003C1624 /* LDEvaluationDetail.swift in Sources */, 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */, C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831188592113AE1200D77CB5 /* FlagStore.swift in Sources */, C443A40D2315AA4D00145710 /* NetworkReporter.swift in Sources */, + 29FE129B280413D4008CC918 /* Util.swift in Sources */, 831188652113AE4600D77CB5 /* Date.swift in Sources */, 831188672113AE4D00D77CB5 /* Thread.swift in Sources */, - 832D68A0224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, - 8354AC642241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, C443A40823145FEE00145710 /* ConnectionInformationStore.swift in Sources */, - 831188662113AE4A00D77CB5 /* AnyComparer.swift in Sources */, 8311885C2113AE2200D77CB5 /* HTTPHeaders.swift in Sources */, 831188562113AE0800D77CB5 /* FlagSynchronizer.swift in Sources */, 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */, @@ -1224,17 +1125,15 @@ B4C9D43B2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */, B468E71324B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, - 8354AC732243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, + 8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */, 8311885B2113AE1D00D77CB5 /* Throttler.swift in Sources */, 8311884E2113ADE500D77CB5 /* Event.swift in Sources */, 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */, - 831188482113ADD100D77CB5 /* LDFlagValueConvertible.swift in Sources */, 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */, B4C9D4312489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 831188462113ADCA00D77CB5 /* LDUser.swift in Sources */, 830DB3B12239B54900D65D25 /* URLResponse.swift in Sources */, 831188512113ADF400D77CB5 /* ClientServiceFactory.swift in Sources */, - 831188472113ADCD00D77CB5 /* LDFlagValue.swift in Sources */, 831188442113ADC200D77CB5 /* LDConfig.swift in Sources */, 83906A7721190B1900D7D3C5 /* FlagRequestTracker.swift in Sources */, B495A8A52787762C0051977C /* LDClientVariation.swift in Sources */, @@ -1246,21 +1145,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8354AC632241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, B468E71224B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, 831EF34320655E730001C643 /* LDCommon.swift in Sources */, 831EF34420655E730001C643 /* LDConfig.swift in Sources */, 831EF34520655E730001C643 /* LDClient.swift in Sources */, - 832D689F224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, 831EF34620655E730001C643 /* LDUser.swift in Sources */, - 831EF34720655E730001C643 /* LDFlagValue.swift in Sources */, 830DB3B02239B54900D65D25 /* URLResponse.swift in Sources */, - 831EF34820655E730001C643 /* LDFlagValueConvertible.swift in Sources */, B4C9D4352489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */, C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */, - 8354AC722243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, + 8354AC722243166900CDE602 /* FeatureFlagCache.swift in Sources */, C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */, 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */, 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */, @@ -1280,21 +1175,17 @@ 831EF35B20655E730001C643 /* DarklyService.swift in Sources */, 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */, 29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */, - 8354AC6B22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, C443A40723145FEE00145710 /* ConnectionInformationStore.swift in Sources */, + 29FE129A280413D4008CC918 /* Util.swift in Sources */, 831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */, - 831EF35E20655E730001C643 /* Dictionary.swift in Sources */, 835E4C54206BDF8D004C6E6C /* EnvironmentReporter.swift in Sources */, 8372668E20D4439600BD1088 /* DateFormatter.swift in Sources */, - 8370DF6E225E40B800F84810 /* DeprecatedCache.swift in Sources */, C43C37E7238DF22C003C1624 /* LDEvaluationDetail.swift in Sources */, 831EF36020655E730001C643 /* Data.swift in Sources */, 83EBCBB520DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, - 831EF36220655E730001C643 /* JSONSerialization.swift in Sources */, 8347BB0E21F147E100E56BCD /* LDTimer.swift in Sources */, B495A8A42787762C0051977C /* LDClientVariation.swift in Sources */, 831EF36320655E730001C643 /* Date.swift in Sources */, - 831EF36420655E730001C643 /* AnyComparer.swift in Sources */, 831EF36520655E730001C643 /* Thread.swift in Sources */, 83B1D7C92073F354006D1B1C /* CwlSysctl.swift in Sources */, 831EF36620655E730001C643 /* ObjcLDClient.swift in Sources */, @@ -1311,7 +1202,6 @@ files = ( 831D8B6F1F71532300ED65E8 /* HTTPHeaders.swift in Sources */, 835E1D3F1F63450A00184DB4 /* ObjcLDClient.swift in Sources */, - 8370DF6C225E40B800F84810 /* DeprecatedCache.swift in Sources */, 83EBCBB320DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, 837EF3742059C237009D628A /* Log.swift in Sources */, 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */, @@ -1325,21 +1215,16 @@ 831D8B721F71D3E700ED65E8 /* DarklyService.swift in Sources */, 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */, 8358F25E1F474E5900ECE1AF /* LDChangedFlag.swift in Sources */, - 8354AC6922418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */, C43C37E1236BA050003C1624 /* LDEvaluationDetail.swift in Sources */, 831AAE2C20A9E4F600B46DBA /* Throttler.swift in Sources */, 8354EFE11F26380700C05156 /* LDConfig.swift in Sources */, + 29FE1298280413D4008CC918 /* Util.swift in Sources */, C443A40F23186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, - 83F0A5621FB4D66600550A95 /* AnyComparer.swift in Sources */, C443A40A2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831D8B741F72994600ED65E8 /* FlagStore.swift in Sources */, 8358F2601F476AD800ECE1AF /* FlagChangeNotifier.swift in Sources */, - 838838451F5EFBAF0023D11B /* LDFlagValueConvertible.swift in Sources */, - 832D689D224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, - 838838411F5EFADF0023D11B /* LDFlagValue.swift in Sources */, 835E1D411F63450A00184DB4 /* ObjcLDUser.swift in Sources */, - 8354AC612241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */, 831D2AAF2061AAA000B4AC3C /* Thread.swift in Sources */, 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */, @@ -1348,13 +1233,11 @@ 831425B1206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, C408884723033B3600420721 /* ConnectionInformationStore.swift in Sources */, 83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */, - 83EF678A1F97CFEC00403126 /* Dictionary.swift in Sources */, B4C9D4382489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, 8347BB0C21F147E100E56BCD /* LDTimer.swift in Sources */, B468E71024B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, - 8354AC702243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, + 8354AC702243166900CDE602 /* FeatureFlagCache.swift in Sources */, 8358F2621F47747F00ECE1AF /* FlagChangeObserver.swift in Sources */, - 83DDBEFC1FA24B2700E428B6 /* JSONSerialization.swift in Sources */, 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */, 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */, 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */, @@ -1374,7 +1257,6 @@ 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */, 8392FFA32033565700320914 /* HTTPURLResponse.swift in Sources */, 83411A5F1FABDA8700E5CF39 /* mocks.generated.swift in Sources */, - 83D5597E1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift in Sources */, 831CE0661F853A1700A13A3A /* Match.swift in Sources */, 83DDBF001FA2589900E428B6 /* FlagStoreSpec.swift in Sources */, B4F689142497B2FC004D3CE0 /* DiagnosticEventSpec.swift in Sources */, @@ -1383,29 +1265,23 @@ 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */, 83B6E3F1222EFA3800FF2A6A /* ThreadSpec.swift in Sources */, 831AAE3020A9E75D00B46DBA /* ThrottlerSpec.swift in Sources */, - C48ED691242D27E200464F5F /* DeprecatedCacheMock.swift in Sources */, 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */, 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */, 83E2E2061F9E7AC7007514E9 /* LDUserSpec.swift in Sources */, - 83D17EAA1FCDA18C00B2823C /* DictionarySpec.swift in Sources */, 83A0E6B1203B557F00224298 /* FeatureFlagSpec.swift in Sources */, 83EBCBB720DABE93003A7142 /* FlagRequestTrackerSpec.swift in Sources */, - 8354AC662241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift in Sources */, B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */, B46F344125E6DB7D0078D45F /* DiagnosticReporterSpec.swift in Sources */, - 83D1523B22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift in Sources */, 83EF67951F994BAD00403126 /* LDUserStub.swift in Sources */, B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */, 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */, 83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */, - B43D5AD025FBE1C30022EC90 /* DeprecatedCacheModelSpec.swift in Sources */, 832307AA1F7ECA630029815A /* LDConfigStub.swift in Sources */, - 8354AC77224316F800CDE602 /* UserEnvironmentFlagCacheSpec.swift in Sources */, + 8354AC77224316F800CDE602 /* FeatureFlagCacheSpec.swift in Sources */, 83EBCBB120D9C7B5003A7142 /* FlagCounterSpec.swift in Sources */, 83B8C2451FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift in Sources */, 8335299E1FC37727001166F8 /* FlagMaintainingMock.swift in Sources */, 83383A5120460DD30024D975 /* SynchronizingErrorSpec.swift in Sources */, - 8354AC6E22418C1F00CDE602 /* CacheableUserEnvironmentFlagsSpec.swift in Sources */, 83B9A080204F56F4000C3F17 /* FlagChangeObserverSpec.swift in Sources */, 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, 831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */, @@ -1423,12 +1299,9 @@ files = ( 83D9EC752062DEAB004D7FA6 /* LDCommon.swift in Sources */, 83D9EC762062DEAB004D7FA6 /* LDConfig.swift in Sources */, - 8370DF6D225E40B800F84810 /* DeprecatedCache.swift in Sources */, 83EBCBB420DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, 83D9EC772062DEAB004D7FA6 /* LDClient.swift in Sources */, 83D9EC782062DEAB004D7FA6 /* LDUser.swift in Sources */, - 83D9EC792062DEAB004D7FA6 /* LDFlagValue.swift in Sources */, - 83D9EC7A2062DEAB004D7FA6 /* LDFlagValueConvertible.swift in Sources */, B4C9D4342489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */, @@ -1437,36 +1310,31 @@ 83D9EC7F2062DEAB004D7FA6 /* FlagsUnchangedObserver.swift in Sources */, 83D9EC802062DEAB004D7FA6 /* Event.swift in Sources */, 83D9EC822062DEAB004D7FA6 /* ClientServiceFactory.swift in Sources */, - 8354AC6A22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, 83D9EC832062DEAB004D7FA6 /* KeyedValueCache.swift in Sources */, 831AAE2D20A9E4F600B46DBA /* Throttler.swift in Sources */, C43C37E6238DF22B003C1624 /* LDEvaluationDetail.swift in Sources */, 83D9EC872062DEAB004D7FA6 /* FlagSynchronizer.swift in Sources */, C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, + 29FE1299280413D4008CC918 /* Util.swift in Sources */, 83D9EC882062DEAB004D7FA6 /* FlagChangeNotifier.swift in Sources */, C443A40B2315AA4D00145710 /* NetworkReporter.swift in Sources */, 83D9EC892062DEAB004D7FA6 /* EventReporter.swift in Sources */, 83D9EC8A2062DEAB004D7FA6 /* FlagStore.swift in Sources */, 83D9EC8B2062DEAB004D7FA6 /* Log.swift in Sources */, - 832D689E224A3896005F052A /* DeprecatedCacheModelV5.swift in Sources */, - 8354AC622241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83D9EC8C2062DEAB004D7FA6 /* HTTPHeaders.swift in Sources */, C443A40623145FED00145710 /* ConnectionInformationStore.swift in Sources */, 83D9EC8D2062DEAB004D7FA6 /* DarklyService.swift in Sources */, 83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */, 83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */, - 83D9EC902062DEAB004D7FA6 /* Dictionary.swift in Sources */, 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B2206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */, 8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */, - 8354AC712243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, + 8354AC712243166900CDE602 /* FeatureFlagCache.swift in Sources */, C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */, B4C9D4392489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, - 83D9EC942062DEAB004D7FA6 /* JSONSerialization.swift in Sources */, 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */, - 83D9EC962062DEAB004D7FA6 /* AnyComparer.swift in Sources */, 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */, 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */, 83D9EC982062DEAB004D7FA6 /* ObjcLDClient.swift in Sources */, diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index dbfdd106..b8ad98cc 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -11,12 +11,12 @@ import LDSwiftEventSource final class CacheConvertingMock: CacheConverting { var convertCacheDataCallCount = 0 - var convertCacheDataCallback: (() -> Void)? - var convertCacheDataReceivedArguments: (user: LDUser, config: LDConfig)? - func convertCacheData(for user: LDUser, and config: LDConfig) { + var convertCacheDataCallback: (() throws -> Void)? + var convertCacheDataReceivedArguments: (serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int)? + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) { convertCacheDataCallCount += 1 - convertCacheDataReceivedArguments = (user: user, config: config) - convertCacheDataCallback?() + convertCacheDataReceivedArguments = (serviceFactory: serviceFactory, keysToConvert: keysToConvert, maxCachedUsers: maxCachedUsers) + try! convertCacheDataCallback?() } } @@ -24,17 +24,17 @@ final class CacheConvertingMock: CacheConverting { final class DarklyStreamingProviderMock: DarklyStreamingProvider { var startCallCount = 0 - var startCallback: (() -> Void)? + var startCallback: (() throws -> Void)? func start() { startCallCount += 1 - startCallback?() + try! startCallback?() } var stopCallCount = 0 - var stopCallback: (() -> Void)? + var stopCallback: (() throws -> Void)? func stop() { stopCallCount += 1 - stopCallback?() + try! stopCallback?() } } @@ -42,55 +42,55 @@ final class DarklyStreamingProviderMock: DarklyStreamingProvider { final class DiagnosticCachingMock: DiagnosticCaching { var lastStatsSetCount = 0 - var setLastStatsCallback: (() -> Void)? + var setLastStatsCallback: (() throws -> Void)? var lastStats: DiagnosticStats? = nil { didSet { lastStatsSetCount += 1 - setLastStatsCallback?() + try! setLastStatsCallback?() } } var getDiagnosticIdCallCount = 0 - var getDiagnosticIdCallback: (() -> Void)? + var getDiagnosticIdCallback: (() throws -> Void)? var getDiagnosticIdReturnValue: DiagnosticId! func getDiagnosticId() -> DiagnosticId { getDiagnosticIdCallCount += 1 - getDiagnosticIdCallback?() + try! getDiagnosticIdCallback?() return getDiagnosticIdReturnValue } var getCurrentStatsAndResetCallCount = 0 - var getCurrentStatsAndResetCallback: (() -> Void)? + var getCurrentStatsAndResetCallback: (() throws -> Void)? var getCurrentStatsAndResetReturnValue: DiagnosticStats! func getCurrentStatsAndReset() -> DiagnosticStats { getCurrentStatsAndResetCallCount += 1 - getCurrentStatsAndResetCallback?() + try! getCurrentStatsAndResetCallback?() return getCurrentStatsAndResetReturnValue } var incrementDroppedEventCountCallCount = 0 - var incrementDroppedEventCountCallback: (() -> Void)? + var incrementDroppedEventCountCallback: (() throws -> Void)? func incrementDroppedEventCount() { incrementDroppedEventCountCallCount += 1 - incrementDroppedEventCountCallback?() + try! incrementDroppedEventCountCallback?() } var recordEventsInLastBatchCallCount = 0 - var recordEventsInLastBatchCallback: (() -> Void)? + var recordEventsInLastBatchCallback: (() throws -> Void)? var recordEventsInLastBatchReceivedEventsInLastBatch: Int? func recordEventsInLastBatch(eventsInLastBatch: Int) { recordEventsInLastBatchCallCount += 1 recordEventsInLastBatchReceivedEventsInLastBatch = eventsInLastBatch - recordEventsInLastBatchCallback?() + try! recordEventsInLastBatchCallback?() } var addStreamInitCallCount = 0 - var addStreamInitCallback: (() -> Void)? + var addStreamInitCallback: (() throws -> Void)? var addStreamInitReceivedStreamInit: DiagnosticStreamInit? func addStreamInit(streamInit: DiagnosticStreamInit) { addStreamInitCallCount += 1 addStreamInitReceivedStreamInit = streamInit - addStreamInitCallback?() + try! addStreamInitCallback?() } } @@ -98,12 +98,12 @@ final class DiagnosticCachingMock: DiagnosticCaching { final class DiagnosticReportingMock: DiagnosticReporting { var setModeCallCount = 0 - var setModeCallback: (() -> Void)? + var setModeCallback: (() throws -> Void)? var setModeReceivedArguments: (runMode: LDClientRunMode, online: Bool)? func setMode(_ runMode: LDClientRunMode, online: Bool) { setModeCallCount += 1 setModeReceivedArguments = (runMode: runMode, online: online) - setModeCallback?() + try! setModeCallback?() } } @@ -111,101 +111,101 @@ final class DiagnosticReportingMock: DiagnosticReporting { final class EnvironmentReportingMock: EnvironmentReporting { var isDebugBuildSetCount = 0 - var setIsDebugBuildCallback: (() -> Void)? + var setIsDebugBuildCallback: (() throws -> Void)? var isDebugBuild: Bool = true { didSet { isDebugBuildSetCount += 1 - setIsDebugBuildCallback?() + try! setIsDebugBuildCallback?() } } var deviceTypeSetCount = 0 - var setDeviceTypeCallback: (() -> Void)? + var setDeviceTypeCallback: (() throws -> Void)? var deviceType: String = Constants.deviceType { didSet { deviceTypeSetCount += 1 - setDeviceTypeCallback?() + try! setDeviceTypeCallback?() } } var deviceModelSetCount = 0 - var setDeviceModelCallback: (() -> Void)? + var setDeviceModelCallback: (() throws -> Void)? var deviceModel: String = Constants.deviceModel { didSet { deviceModelSetCount += 1 - setDeviceModelCallback?() + try! setDeviceModelCallback?() } } var systemVersionSetCount = 0 - var setSystemVersionCallback: (() -> Void)? + var setSystemVersionCallback: (() throws -> Void)? var systemVersion: String = Constants.systemVersion { didSet { systemVersionSetCount += 1 - setSystemVersionCallback?() + try! setSystemVersionCallback?() } } var systemNameSetCount = 0 - var setSystemNameCallback: (() -> Void)? + var setSystemNameCallback: (() throws -> Void)? var systemName: String = Constants.systemName { didSet { systemNameSetCount += 1 - setSystemNameCallback?() + try! setSystemNameCallback?() } } var operatingSystemSetCount = 0 - var setOperatingSystemCallback: (() -> Void)? + var setOperatingSystemCallback: (() throws -> Void)? var operatingSystem: OperatingSystem = .iOS { didSet { operatingSystemSetCount += 1 - setOperatingSystemCallback?() + try! setOperatingSystemCallback?() } } var backgroundNotificationSetCount = 0 - var setBackgroundNotificationCallback: (() -> Void)? + var setBackgroundNotificationCallback: (() throws -> Void)? var backgroundNotification: Notification.Name? = EnvironmentReporter().backgroundNotification { didSet { backgroundNotificationSetCount += 1 - setBackgroundNotificationCallback?() + try! setBackgroundNotificationCallback?() } } var foregroundNotificationSetCount = 0 - var setForegroundNotificationCallback: (() -> Void)? + var setForegroundNotificationCallback: (() throws -> Void)? var foregroundNotification: Notification.Name? = EnvironmentReporter().foregroundNotification { didSet { foregroundNotificationSetCount += 1 - setForegroundNotificationCallback?() + try! setForegroundNotificationCallback?() } } var vendorUUIDSetCount = 0 - var setVendorUUIDCallback: (() -> Void)? + var setVendorUUIDCallback: (() throws -> Void)? var vendorUUID: String? = Constants.vendorUUID { didSet { vendorUUIDSetCount += 1 - setVendorUUIDCallback?() + try! setVendorUUIDCallback?() } } var sdkVersionSetCount = 0 - var setSdkVersionCallback: (() -> Void)? + var setSdkVersionCallback: (() throws -> Void)? var sdkVersion: String = Constants.sdkVersion { didSet { sdkVersionSetCount += 1 - setSdkVersionCallback?() + try! setSdkVersionCallback?() } } var shouldThrottleOnlineCallsSetCount = 0 - var setShouldThrottleOnlineCallsCallback: (() -> Void)? + var setShouldThrottleOnlineCallsCallback: (() throws -> Void)? var shouldThrottleOnlineCalls: Bool = true { didSet { shouldThrottleOnlineCallsSetCount += 1 - setShouldThrottleOnlineCallsCallback?() + try! setShouldThrottleOnlineCallsCallback?() } } } @@ -214,81 +214,81 @@ final class EnvironmentReportingMock: EnvironmentReporting { final class EventReportingMock: EventReporting { var isOnlineSetCount = 0 - var setIsOnlineCallback: (() -> Void)? + var setIsOnlineCallback: (() throws -> Void)? var isOnline: Bool = false { didSet { isOnlineSetCount += 1 - setIsOnlineCallback?() + try! setIsOnlineCallback?() } } var lastEventResponseDateSetCount = 0 - var setLastEventResponseDateCallback: (() -> Void)? + var setLastEventResponseDateCallback: (() throws -> Void)? var lastEventResponseDate: Date? = nil { didSet { lastEventResponseDateSetCount += 1 - setLastEventResponseDateCallback?() + try! setLastEventResponseDateCallback?() } } var recordCallCount = 0 - var recordCallback: (() -> Void)? + var recordCallback: (() throws -> Void)? var recordReceivedEvent: Event? func record(_ event: Event) { recordCallCount += 1 recordReceivedEvent = event - recordCallback?() + try! recordCallback?() } var recordFlagEvaluationEventsCallCount = 0 - var recordFlagEvaluationEventsCallback: (() -> Void)? + var recordFlagEvaluationEventsCallback: (() throws -> Void)? var recordFlagEvaluationEventsReceivedArguments: (flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool)? func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) { recordFlagEvaluationEventsCallCount += 1 recordFlagEvaluationEventsReceivedArguments = (flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason) - recordFlagEvaluationEventsCallback?() + try! recordFlagEvaluationEventsCallback?() } var flushCallCount = 0 - var flushCallback: (() -> Void)? + var flushCallback: (() throws -> Void)? var flushReceivedCompletion: CompletionClosure? func flush(completion: CompletionClosure?) { flushCallCount += 1 flushReceivedCompletion = completion - flushCallback?() + try! flushCallback?() } } // MARK: - FeatureFlagCachingMock final class FeatureFlagCachingMock: FeatureFlagCaching { - var maxCachedUsersSetCount = 0 - var setMaxCachedUsersCallback: (() -> Void)? - var maxCachedUsers: Int = 5 { + var keyedValueCacheSetCount = 0 + var setKeyedValueCacheCallback: (() throws -> Void)? + var keyedValueCache: KeyedValueCaching = KeyedValueCachingMock() { didSet { - maxCachedUsersSetCount += 1 - setMaxCachedUsersCallback?() + keyedValueCacheSetCount += 1 + try! setKeyedValueCacheCallback?() } } var retrieveFeatureFlagsCallCount = 0 - var retrieveFeatureFlagsCallback: (() -> Void)? - var retrieveFeatureFlagsReceivedArguments: (userKey: String, mobileKey: String)? + var retrieveFeatureFlagsCallback: (() throws -> Void)? + var retrieveFeatureFlagsReceivedUserKey: String? var retrieveFeatureFlagsReturnValue: [LDFlagKey: FeatureFlag]? - func retrieveFeatureFlags(forUserWithKey userKey: String, andMobileKey mobileKey: String) -> [LDFlagKey: FeatureFlag]? { + func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? { retrieveFeatureFlagsCallCount += 1 - retrieveFeatureFlagsReceivedArguments = (userKey: userKey, mobileKey: mobileKey) - retrieveFeatureFlagsCallback?() + retrieveFeatureFlagsReceivedUserKey = userKey + try! retrieveFeatureFlagsCallback?() return retrieveFeatureFlagsReturnValue } var storeFeatureFlagsCallCount = 0 - var storeFeatureFlagsCallback: (() -> Void)? - var storeFeatureFlagsReceivedArguments: (featureFlags: [LDFlagKey: FeatureFlag], userKey: String, mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode)? - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode) { + var storeFeatureFlagsCallback: (() throws -> Void)? + var storeFeatureFlagsReceivedArguments: (featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date)? + func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) { storeFeatureFlagsCallCount += 1 - storeFeatureFlagsReceivedArguments = (featureFlags: featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated, storeMode: storeMode) - storeFeatureFlagsCallback?() + storeFeatureFlagsReceivedArguments = (featureFlags: featureFlags, userKey: userKey, lastUpdated: lastUpdated) + try! storeFeatureFlagsCallback?() } } @@ -296,64 +296,64 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { final class FlagChangeNotifyingMock: FlagChangeNotifying { var addFlagChangeObserverCallCount = 0 - var addFlagChangeObserverCallback: (() -> Void)? + var addFlagChangeObserverCallback: (() throws -> Void)? var addFlagChangeObserverReceivedObserver: FlagChangeObserver? func addFlagChangeObserver(_ observer: FlagChangeObserver) { addFlagChangeObserverCallCount += 1 addFlagChangeObserverReceivedObserver = observer - addFlagChangeObserverCallback?() + try! addFlagChangeObserverCallback?() } var addFlagsUnchangedObserverCallCount = 0 - var addFlagsUnchangedObserverCallback: (() -> Void)? + var addFlagsUnchangedObserverCallback: (() throws -> Void)? var addFlagsUnchangedObserverReceivedObserver: FlagsUnchangedObserver? func addFlagsUnchangedObserver(_ observer: FlagsUnchangedObserver) { addFlagsUnchangedObserverCallCount += 1 addFlagsUnchangedObserverReceivedObserver = observer - addFlagsUnchangedObserverCallback?() + try! addFlagsUnchangedObserverCallback?() } var addConnectionModeChangedObserverCallCount = 0 - var addConnectionModeChangedObserverCallback: (() -> Void)? + var addConnectionModeChangedObserverCallback: (() throws -> Void)? var addConnectionModeChangedObserverReceivedObserver: ConnectionModeChangedObserver? func addConnectionModeChangedObserver(_ observer: ConnectionModeChangedObserver) { addConnectionModeChangedObserverCallCount += 1 addConnectionModeChangedObserverReceivedObserver = observer - addConnectionModeChangedObserverCallback?() + try! addConnectionModeChangedObserverCallback?() } var removeObserverCallCount = 0 - var removeObserverCallback: (() -> Void)? + var removeObserverCallback: (() throws -> Void)? var removeObserverReceivedOwner: LDObserverOwner? func removeObserver(owner: LDObserverOwner) { removeObserverCallCount += 1 removeObserverReceivedOwner = owner - removeObserverCallback?() + try! removeObserverCallback?() } var notifyConnectionModeChangedObserversCallCount = 0 - var notifyConnectionModeChangedObserversCallback: (() -> Void)? + var notifyConnectionModeChangedObserversCallback: (() throws -> Void)? var notifyConnectionModeChangedObserversReceivedConnectionMode: ConnectionInformation.ConnectionMode? func notifyConnectionModeChangedObservers(connectionMode: ConnectionInformation.ConnectionMode) { notifyConnectionModeChangedObserversCallCount += 1 notifyConnectionModeChangedObserversReceivedConnectionMode = connectionMode - notifyConnectionModeChangedObserversCallback?() + try! notifyConnectionModeChangedObserversCallback?() } var notifyUnchangedCallCount = 0 - var notifyUnchangedCallback: (() -> Void)? + var notifyUnchangedCallback: (() throws -> Void)? func notifyUnchanged() { notifyUnchangedCallCount += 1 - notifyUnchangedCallback?() + try! notifyUnchangedCallback?() } var notifyObserversCallCount = 0 - var notifyObserversCallback: (() -> Void)? + var notifyObserversCallback: (() throws -> Void)? var notifyObserversReceivedArguments: (oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag])? func notifyObservers(oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) { notifyObserversCallCount += 1 notifyObserversReceivedArguments = (oldFlags: oldFlags, newFlags: newFlags) - notifyObserversCallback?() + try! notifyObserversCallback?() } } @@ -361,32 +361,50 @@ final class FlagChangeNotifyingMock: FlagChangeNotifying { final class KeyedValueCachingMock: KeyedValueCaching { var setCallCount = 0 - var setCallback: (() -> Void)? - var setReceivedArguments: (value: Any?, forKey: String)? - func set(_ value: Any?, forKey: String) { + var setCallback: (() throws -> Void)? + var setReceivedArguments: (value: Data, forKey: String)? + func set(_ value: Data, forKey: String) { setCallCount += 1 setReceivedArguments = (value: value, forKey: forKey) - setCallback?() + try! setCallback?() + } + + var dataCallCount = 0 + var dataCallback: (() throws -> Void)? + var dataReceivedForKey: String? + var dataReturnValue: Data? + func data(forKey: String) -> Data? { + dataCallCount += 1 + dataReceivedForKey = forKey + try! dataCallback?() + return dataReturnValue } var dictionaryCallCount = 0 - var dictionaryCallback: (() -> Void)? + var dictionaryCallback: (() throws -> Void)? var dictionaryReceivedForKey: String? - var dictionaryReturnValue: [String: Any]? = nil + var dictionaryReturnValue: [String: Any]? func dictionary(forKey: String) -> [String: Any]? { dictionaryCallCount += 1 dictionaryReceivedForKey = forKey - dictionaryCallback?() + try! dictionaryCallback?() return dictionaryReturnValue } var removeObjectCallCount = 0 - var removeObjectCallback: (() -> Void)? + var removeObjectCallback: (() throws -> Void)? var removeObjectReceivedForKey: String? func removeObject(forKey: String) { removeObjectCallCount += 1 removeObjectReceivedForKey = forKey - removeObjectCallback?() + try! removeObjectCallback?() + } + + var removeAllCallCount = 0 + var removeAllCallback: (() throws -> Void)? + func removeAll() { + removeAllCallCount += 1 + try! removeAllCallback?() } } @@ -394,29 +412,29 @@ final class KeyedValueCachingMock: KeyedValueCaching { final class LDFlagSynchronizingMock: LDFlagSynchronizing { var isOnlineSetCount = 0 - var setIsOnlineCallback: (() -> Void)? + var setIsOnlineCallback: (() throws -> Void)? var isOnline: Bool = false { didSet { isOnlineSetCount += 1 - setIsOnlineCallback?() + try! setIsOnlineCallback?() } } var streamingModeSetCount = 0 - var setStreamingModeCallback: (() -> Void)? + var setStreamingModeCallback: (() throws -> Void)? var streamingMode: LDStreamingMode = .streaming { didSet { streamingModeSetCount += 1 - setStreamingModeCallback?() + try! setStreamingModeCallback?() } } var pollingIntervalSetCount = 0 - var setPollingIntervalCallback: (() -> Void)? + var setPollingIntervalCallback: (() throws -> Void)? var pollingInterval: TimeInterval = 60_000 { didSet { pollingIntervalSetCount += 1 - setPollingIntervalCallback?() + try! setPollingIntervalCallback?() } } } @@ -425,18 +443,18 @@ final class LDFlagSynchronizingMock: LDFlagSynchronizing { final class ThrottlingMock: Throttling { var runThrottledCallCount = 0 - var runThrottledCallback: (() -> Void)? + var runThrottledCallback: (() throws -> Void)? var runThrottledReceivedRunClosure: RunClosure? func runThrottled(_ runClosure: @escaping RunClosure) { runThrottledCallCount += 1 runThrottledReceivedRunClosure = runClosure - runThrottledCallback?() + try! runThrottledCallback?() } var cancelThrottledRunCallCount = 0 - var cancelThrottledRunCallback: (() -> Void)? + var cancelThrottledRunCallback: (() throws -> Void)? func cancelThrottledRun() { cancelThrottledRunCallCount += 1 - cancelThrottledRunCallback?() + try! cancelThrottledRunCallback?() } } diff --git a/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift b/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift deleted file mode 100644 index bca2a855..00000000 --- a/LaunchDarkly/LaunchDarkly/Extensions/AnyComparer.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation - -struct AnyComparer { - private init() { } - - // If editing this method to add classes here, update AnySpec with tests that verify the comparison for that class - // swiftlint:disable:next cyclomatic_complexity - static func isEqual(_ value: Any, to other: Any) -> Bool { - switch (value, other) { - case let (value, other) as (Bool, Bool): - if value != other { - return false - } - case let (value, other) as (Int, Int): - if value != other { - return false - } - case let (value, other) as (Int, Double): - if Double(value) != other { - return false - } - case let (value, other) as (Double, Int): - if value != Double(other) { - return false - } - case let (value, other) as (Int64, Int64): - if value != other { - return false - } - case let (value, other) as (Int64, Double): - if Double(value) != other { - return false - } - case let (value, other) as (Double, Int64): - if value != Double(other) { - return false - } - case let (value, other) as (Double, Double): - if value != other { - return false - } - case let (value, other) as (String, String): - if value != other { - return false - } - case let (value, other) as ([Any], [Any]): - if value.count != other.count { - return false - } - for index in 0.. Bool { - guard let nonNilValue = value, let nonNilOther = other - else { - return value == nil && other == nil - } - return isEqual(nonNilValue, to: nonNilOther) - } - - static func isEqual(_ value: Any, to other: Any?) -> Bool { - guard let other = other - else { - return false - } - return isEqual(value, to: other) - } - - static func isEqual(_ value: Any?, to other: Any) -> Bool { - guard let value = value - else { - return false - } - return isEqual(value, to: other) - } -} diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Data.swift b/LaunchDarkly/LaunchDarkly/Extensions/Data.swift index fe3e8a32..9ca0b4c7 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/Data.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/Data.swift @@ -6,6 +6,6 @@ extension Data { } var jsonDictionary: [String: Any]? { - try? JSONSerialization.jsonDictionary(with: self, options: [.allowFragments]) + try? JSONSerialization.jsonObject(with: self, options: [.allowFragments]) as? [String: Any] } } diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift b/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift deleted file mode 100644 index e70fce44..00000000 --- a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Foundation - -extension Dictionary where Key == String { - var jsonString: String? { - guard let encodedDictionary = jsonData - else { return nil } - return String(data: encodedDictionary, encoding: .utf8) - } - - var jsonData: Data? { - guard JSONSerialization.isValidJSONObject(self) - else { return nil } - return try? JSONSerialization.data(withJSONObject: self, options: []) - } - - func symmetricDifference(_ other: [String: Any]) -> [String] { - let leftKeys: Set = Set(self.keys) - let rightKeys: Set = Set(other.keys) - let differingKeys = leftKeys.symmetricDifference(rightKeys) - let matchingKeys = leftKeys.intersection(rightKeys) - let matchingKeysWithDifferentValues = matchingKeys.filter { key -> Bool in - !AnyComparer.isEqual(self[key], to: other[key]) - } - return differingKeys.union(matchingKeysWithDifferentValues).sorted() - } -} - -extension Dictionary where Key == String, Value == Any { - var withNullValuesRemoved: [String: Any] { - (self as [String: Any?]).compactMapValues { value in - if value is NSNull { - return nil - } - if let dictionary = value as? [String: Any] { - return dictionary.withNullValuesRemoved - } - if let arr = value as? [Any] { - return arr.withNullValuesRemoved - } - return value - } - } -} - -private extension Array where Element == Any { - var withNullValuesRemoved: [Any] { - (self as [Any?]).compactMap { value in - if value is NSNull { - return nil - } - if let arr = value as? [Any] { - return arr.withNullValuesRemoved - } - if let dict = value as? [String: Any] { - return dict.withNullValuesRemoved - } - return value - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift b/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift deleted file mode 100644 index aad475c1..00000000 --- a/LaunchDarkly/LaunchDarkly/Extensions/JSONSerialization.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -extension JSONSerialization { - static func jsonDictionary(with data: Data, options: JSONSerialization.ReadingOptions = []) throws -> [String: Any] { - guard let decodedDictionary = try JSONSerialization.jsonObject(with: data, options: options) as? [String: Any] - else { - throw LDInvalidArgumentError("JSON is not an object") - } - return decodedDictionary - } -} diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 4dcab792..a0ab4ff3 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -116,10 +116,10 @@ public class LDClient { private func internalSetOnline(_ goOnline: Bool, completion: (() -> Void)? = nil) { internalSetOnlineQueue.sync { guard goOnline, self.canGoOnline - else { - // go offline, which is not throttled - self.go(online: false, reasonOnlineUnavailable: self.reasonOnlineUnavailable(goOnline: goOnline), completion: completion) - return + else { + // go offline, which is not throttled + self.go(online: false, reasonOnlineUnavailable: self.reasonOnlineUnavailable(goOnline: goOnline), completion: completion) + return } self.throttler.runThrottled { @@ -294,9 +294,8 @@ public class LDClient { let wasOnline = self.isOnline self.internalSetOnline(false) - cacheConverter.convertCacheData(for: user, and: config) - let cachedUserFlags = self.flagCache.retrieveFeatureFlags(forUserWithKey: self.user.key, andMobileKey: self.config.mobileKey) ?? [:] - flagStore.replaceStore(newFlags: cachedUserFlags, completion: nil) + let cachedUserFlags = self.flagCache.retrieveFeatureFlags(userKey: self.user.key) ?? [:] + flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedUserFlags)) self.service.user = self.user self.service.clearFlagResponseCache() flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), @@ -326,7 +325,7 @@ public class LDClient { LDClient will not provide any source or change information, only flag keys and flag values. The client app should convert the feature flag value into the desired type. */ - public var allFlags: [LDFlagKey: Any]? { + public var allFlags: [LDFlagKey: LDValue]? { guard hasStarted else { return nil } return flagStore.featureFlags.compactMapValues { $0.value } @@ -487,23 +486,21 @@ public class LDClient { private func onFlagSyncComplete(result: FlagSyncResult) { Log.debug(typeName(and: #function) + "result: \(result)") switch result { - case let .success(flagDictionary, streamingEvent): + case let .flagCollection(flagCollection): let oldFlags = flagStore.featureFlags connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) - switch streamingEvent { - case nil, .ping?, .put?: - flagStore.replaceStore(newFlags: flagDictionary) { - self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) - } - case .patch?: - flagStore.updateStore(updateDictionary: flagDictionary) { - self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) - } - case .delete?: - flagStore.deleteFlag(deleteDictionary: flagDictionary) { - self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) - } - } + flagStore.replaceStore(newFlags: flagCollection) + self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) + case let .patch(featureFlag): + let oldFlags = flagStore.featureFlags + connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) + flagStore.updateStore(updatedFlag: featureFlag) + self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) + case let .delete(deleteResponse): + let oldFlags = flagStore.featureFlags + connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) + flagStore.deleteFlag(deleteResponse: deleteResponse) + self.updateCacheAndReportChanges(user: self.user, oldFlags: oldFlags) case .upToDate: connectionInformation.lastKnownFlagValidity = Date() flagChangeNotifier.notifyUnchanged() @@ -522,7 +519,7 @@ public class LDClient { private func updateCacheAndReportChanges(user: LDUser, oldFlags: [LDFlagKey: FeatureFlag]) { - flagCache.storeFeatureFlags(flagStore.featureFlags, userKey: user.key, mobileKey: config.mobileKey, lastUpdated: Date(), storeMode: .async) + flagCache.storeFeatureFlags(flagStore.featureFlags, userKey: user.key, lastUpdated: Date()) flagChangeNotifier.notifyObservers(oldFlags: oldFlags, newFlags: flagStore.featureFlags) } @@ -635,7 +632,10 @@ public class LDClient { return } - let internalUser = user + let serviceFactory = serviceFactory ?? ClientServiceFactory() + var keys = [config.mobileKey] + keys.append(contentsOf: config.getSecondaryMobileKeys().values) + serviceFactory.makeCacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: keys, maxCachedUsers: config.maxCachedUsers) LDClient.instances = [:] var mobileKeys = config.getSecondaryMobileKeys() @@ -651,7 +651,7 @@ public class LDClient { for (name, mobileKey) in mobileKeys { var internalConfig = config internalConfig.mobileKey = mobileKey - let instance = LDClient(serviceFactory: serviceFactory ?? ClientServiceFactory(), configuration: internalConfig, startUser: internalUser, completion: completionCheck) + let instance = LDClient(serviceFactory: serviceFactory, configuration: internalConfig, startUser: user, completion: completionCheck) LDClient.instances?[name] = instance } completionCheck() @@ -714,7 +714,6 @@ public class LDClient { let serviceFactory: ClientServiceCreating private(set) var flagCache: FeatureFlagCaching - private(set) var cacheConverter: CacheConverting private(set) var flagSynchronizer: LDFlagSynchronizing var flagChangeNotifier: FlagChangeNotifying private(set) var eventReporter: EventReporting @@ -739,9 +738,8 @@ public class LDClient { private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startUser: LDUser?, completion: (() -> Void)? = nil) { self.serviceFactory = serviceFactory environmentReporter = self.serviceFactory.makeEnvironmentReporter() - flagCache = self.serviceFactory.makeFeatureFlagCache(maxCachedUsers: configuration.maxCachedUsers) + flagCache = self.serviceFactory.makeFeatureFlagCache(mobileKey: configuration.mobileKey, maxCachedUsers: configuration.maxCachedUsers) flagStore = self.serviceFactory.makeFlagStore() - cacheConverter = self.serviceFactory.makeCacheConverter(maxCachedUsers: configuration.maxCachedUsers) flagChangeNotifier = self.serviceFactory.makeFlagChangeNotifier() throttler = self.serviceFactory.makeThrottler(environmentReporter: environmentReporter) @@ -774,9 +772,8 @@ public class LDClient { onSyncComplete: onFlagSyncComplete) Log.level = environmentReporter.isDebugBuild && config.isDebugMode ? .debug : .noLogging - cacheConverter.convertCacheData(for: user, and: config) - if let cachedFlags = flagCache.retrieveFeatureFlags(forUserWithKey: user.key, andMobileKey: config.mobileKey), !cachedFlags.isEmpty { - flagStore.replaceStore(newFlags: cachedFlags, completion: nil) + if let cachedFlags = flagCache.retrieveFeatureFlags(userKey: user.key), !cachedFlags.isEmpty { + flagStore.replaceStore(newFlags: FeatureFlagCollection(cachedFlags)) } eventReporter.record(IdentifyEvent(user: user)) diff --git a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift index 41e0bfe1..b93bc158 100644 --- a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -1,129 +1,170 @@ import Foundation extension LDClient { - // MARK: Retrieving Flag Values /** - Returns the variation for the given feature flag. If the flag does not exist, cannot be cast to the correct return - type, or the LDClient is not started, returns the default value. - - A *variation* is a specific flag value. For example a boolean feature flag has 2 variations, *true* and *false*. - You can create feature flags with more than 2 variations using other feature flag types. See `LDFlagValue` for the - available types. - - When online, the LDClient has two modes for maintaining feature flag values: *streaming* and *polling*. The client - app requests the mode by setting the `config.streamingMode`, see `LDConfig` for details. - - A call to `variation` records events reported later. Recorded events allow clients to analyze usage and assist in - debugging issues. - - ### Usage - ```` - let boolFeatureFlagValue = LDClient.get()!.variation(forKey: "bool-flag-key", defaultValue: false) //boolFeatureFlagValue is a Bool - ```` - **Important** The default value tells the SDK the type of the feature flag. In several cases, the feature flag type - cannot be determined by the values sent from the server. It is possible to provide a default value with a type that - does not match the feature flag value's type. The SDK will attempt to convert the feature flag's value into the - type of the default value in the variation request. If that cast fails, the SDK will not be able to determine the - correct return type, and will always return the default value. - - Pay close attention to the type of the default value for collections. If the default value collection type is more - restrictive than the feature flag, the sdk will return the default value even though the feature flag is present - because it cannot convert the feature flag into the type requested via the default value. For example, if the - feature flag has the type `[String: Any]`, but the default value has the type `[String: Int]`, the sdk will not be - able to convert the flags into the requested type, and will return the default value. - - To avoid this, make sure the default value type matches the expected feature flag type. Either specify the default - value type to be the feature flag type, or cast the default value to the feature flag type prior to making the - variation request. In the above example, either specify that the default value's type is `[String: Any]`: - ```` - let defaultValue: [String: Any] = ["a": 1, "b": 2] //dictionary type would be [String: Int] without the type specifier - ```` - or cast the default value into the feature flag type prior to calling variation: - ```` - let dictionaryFlagValue = LDClient.get()!.variation(forKey: "dictionary-key", defaultValue: ["a": 1, "b": 2] as [String: Any]) - ```` - - - parameter forKey: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value to return if the feature flag key does not exist. - - - returns: The requested feature flag value, or the default value if the flag is missing or cannot be cast to the default value type, or the client is not started - */ - /// - Tag: variation - public func variation(forKey flagKey: LDFlagKey, defaultValue: T) -> T { - variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: false) + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func boolVariation(forKey flagKey: LDFlagKey, defaultValue: Bool) -> Bool { + variationDetailInternal(flagKey, defaultValue, needsReason: false).value } /** - Returns the LDEvaluationDetail for the given feature flag. LDEvaluationDetail gives you more insight into why your - variation contains the specified value. If the flag does not exist, cannot be cast to the correct return type, or - the LDClient is not started, returns an LDEvaluationDetail with the default value. - See [variation](x-source-tag://variation) + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func boolVariationDetail(forKey flagKey: LDFlagKey, defaultValue: Bool) -> LDEvaluationDetail { + variationDetailInternal(flagKey, defaultValue, needsReason: true) + } + + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func intVariation(forKey flagKey: LDFlagKey, defaultValue: Int) -> Int { + variationDetailInternal(flagKey, defaultValue, needsReason: false).value + } - - parameter forKey: The LDFlagKey for the requested feature flag. - - parameter defaultValue: The default value value to return if the feature flag key does not exist. + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func intVariationDetail(forKey flagKey: LDFlagKey, defaultValue: Int) -> LDEvaluationDetail { + variationDetailInternal(flagKey, defaultValue, needsReason: true) + } - - returns: LDEvaluationDetail which wraps the requested feature flag value, or the default value, which variation was served, and the evaluation reason. + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. */ - public func variationDetail(forKey flagKey: LDFlagKey, defaultValue: T) -> LDEvaluationDetail { - let featureFlag = flagStore.featureFlag(for: flagKey) - let reason = checkErrorKinds(featureFlag: featureFlag) ?? featureFlag?.reason - let value = variationInternal(forKey: flagKey, defaultValue: defaultValue, includeReason: true) - return LDEvaluationDetail(value: value, variationIndex: featureFlag?.variation, reason: reason) + public func doubleVariation(forKey flagKey: LDFlagKey, defaultValue: Double) -> Double { + variationDetailInternal(flagKey, defaultValue, needsReason: false).value + } + + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func doubleVariationDetail(forKey flagKey: LDFlagKey, defaultValue: Double) -> LDEvaluationDetail { + variationDetailInternal(flagKey, defaultValue, needsReason: true) + } + + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func stringVariation(forKey flagKey: LDFlagKey, defaultValue: String) -> String { + variationDetailInternal(flagKey, defaultValue, needsReason: false).value + } + + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func stringVariationDetail(forKey flagKey: LDFlagKey, defaultValue: String) -> LDEvaluationDetail { + variationDetailInternal(flagKey, defaultValue, needsReason: true) + } + + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func jsonVariation(forKey flagKey: LDFlagKey, defaultValue: LDValue) -> LDValue { + variationDetailInternal(flagKey, defaultValue, needsReason: false).value } - private func checkErrorKinds(featureFlag: FeatureFlag?) -> [String: Any]? { - if !hasStarted { - return ["kind": "ERROR", "errorKind": "CLIENT_NOT_READY"] - } else if featureFlag == nil { - return ["kind": "ERROR", "errorKind": "FLAG_NOT_FOUND"] + /** + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + */ + public func jsonVariationDetail(forKey flagKey: LDFlagKey, defaultValue: LDValue) -> LDEvaluationDetail { + variationDetailInternal(flagKey, defaultValue, needsReason: true) + } + + private func variationDetailInternal(_ flagKey: LDFlagKey, _ defaultValue: T, needsReason: Bool) -> LDEvaluationDetail { + var result: LDEvaluationDetail + let featureFlag = flagStore.featureFlag(for: flagKey) + if let featureFlag = featureFlag { + if featureFlag.value == .null { + result = LDEvaluationDetail(value: defaultValue, variationIndex: featureFlag.variation, reason: featureFlag.reason) + } else if let convertedValue = T(fromLDValue: featureFlag.value) { + result = LDEvaluationDetail(value: convertedValue, variationIndex: featureFlag.variation, reason: featureFlag.reason) + } else { + result = LDEvaluationDetail(value: defaultValue, variationIndex: nil, reason: ["kind": "ERROR", "errorKind": "WRONG_TYPE"]) + } } else { - return nil + Log.debug(typeName(and: #function) + " Unknown feature flag \(flagKey); returning default value") + result = LDEvaluationDetail(value: defaultValue, variationIndex: nil, reason: ["kind": "ERROR", "errorKind": "FLAG_NOT_FOUND"]) } + eventReporter.recordFlagEvaluationEvents(flagKey: flagKey, + value: result.value.toLDValue(), + defaultValue: defaultValue.toLDValue(), + featureFlag: featureFlag, + user: user, + includeReason: needsReason) + return result } +} - private func variationInternal(forKey flagKey: LDFlagKey, defaultValue: T, includeReason: Bool) -> T { - guard hasStarted - else { - Log.debug(typeName(and: #function) + "returning defaultValue: \(defaultValue)." + " LDClient not started.") - return defaultValue - } - let featureFlag = flagStore.featureFlag(for: flagKey) - let value = (featureFlag?.value as? T) ?? defaultValue - let failedConversionMessage = self.failedConversionMessage(featureFlag: featureFlag, defaultValue: defaultValue) - Log.debug(typeName(and: #function) + "flagKey: \(flagKey), value: \(value), defaultValue: \(defaultValue), " + - "featureFlag: \(String(describing: featureFlag)), reason: \(featureFlag?.reason?.description ?? "nil"). \(failedConversionMessage)") - eventReporter.recordFlagEvaluationEvents(flagKey: flagKey, value: LDValue.fromAny(value), defaultValue: LDValue.fromAny(defaultValue), featureFlag: featureFlag, user: user, includeReason: includeReason) - return value +private protocol LDValueConvertible { + init?(fromLDValue: LDValue) + func toLDValue() -> LDValue +} + +extension Bool: LDValueConvertible { + init?(fromLDValue value: LDValue) { + guard case .bool(let value) = value + else { return nil } + self = value } - private func failedConversionMessage(featureFlag: FeatureFlag?, defaultValue: T) -> String { - if featureFlag == nil { - return " Feature flag not found." - } - if featureFlag?.value is T { - return "" - } - return " LDClient was unable to convert the feature flag to the requested type (\(T.self))." - + (isCollection(defaultValue) ? " The defaultValue type is a collection. Make sure the element of the defaultValue's type is not too restrictive for the actual feature flag type." : "") + func toLDValue() -> LDValue { + return .bool(self) + } +} + +extension Int: LDValueConvertible { + init?(fromLDValue value: LDValue) { + guard case .number(let value) = value, let intValue = Int(exactly: value.rounded()) + else { return nil } + self = intValue } - private func isCollection(_ object: T) -> Bool { - let collectionsTypes = ["Set", "Array", "Dictionary"] - let typeString = String(describing: type(of: object)) + func toLDValue() -> LDValue { + return .number(Double(self)) + } +} - for type in collectionsTypes { - if typeString.contains(type) { return true } - } - return false +extension Double: LDValueConvertible { + init?(fromLDValue value: LDValue) { + guard case .number(let value) = value + else { return nil } + self = value + } + + func toLDValue() -> LDValue { + return .number(self) } } -private extension Optional { - var stringValue: String { - guard let value = self - else { - return "" - } - return "\(value)" +extension String: LDValueConvertible { + init?(fromLDValue value: LDValue) { + guard case .string(let value) = value + else { return nil } + self = value + } + + func toLDValue() -> LDValue { + return .string(self) + } +} + +extension LDValue: LDValueConvertible { + init?(fromLDValue value: LDValue) { + self = value + } + + func toLDValue() -> LDValue { + return self } } diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index afd2b173..ff51353a 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -151,9 +151,7 @@ public enum LDValue: Codable, } func booleanValue() -> Bool { - if case .bool(let val) = self { - return val - } + if case .bool(let val) = self { return val } return false } @@ -166,16 +164,12 @@ public enum LDValue: Codable, } func doubleValue() -> Double { - if case .number(let val) = self { - return val - } + if case .number(let val) = self { return val } return 0 } func stringValue() -> String { - if case .string(let val) = self { - return val - } + if case .string(let val) = self { return val } return "" } diff --git a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift deleted file mode 100644 index 3aaa3923..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableEnvironmentFlags.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation - -// Data structure used to cache feature flags for a specific user from a specific environment -struct CacheableEnvironmentFlags { - enum CodingKeys: String, CodingKey, CaseIterable { - case userKey, mobileKey, featureFlags - } - - let userKey: String - let mobileKey: String - let featureFlags: [LDFlagKey: FeatureFlag] - - init(userKey: String, mobileKey: String, featureFlags: [LDFlagKey: FeatureFlag]) { - (self.userKey, self.mobileKey, self.featureFlags) = (userKey, mobileKey, featureFlags) - } - - var dictionaryValue: [String: Any] { - [CodingKeys.userKey.rawValue: userKey, - CodingKeys.mobileKey.rawValue: mobileKey, - CodingKeys.featureFlags.rawValue: featureFlags.dictionaryValue.withNullValuesRemoved] - } - - init?(dictionary: [String: Any]) { - guard let userKey = dictionary[CodingKeys.userKey.rawValue] as? String, - let mobileKey = dictionary[CodingKeys.mobileKey.rawValue] as? String, - let featureFlags = (dictionary[CodingKeys.featureFlags.rawValue] as? [String: Any])?.flagCollection - else { return nil } - self.init(userKey: userKey, mobileKey: mobileKey, featureFlags: featureFlags) - } -} diff --git a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift b/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift deleted file mode 100644 index d6a3d0d7..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/Cache/CacheableUserEnvironmentFlags.swift +++ /dev/null @@ -1,93 +0,0 @@ -import Foundation - -// Data structure used to cache feature flags for a specific user for multiple environments -// Cache model in use from 4.0.0 -/* -[: [ - “userKey”: , //CacheableUserEnvironmentFlags dictionary - “environmentFlags”: [ - : [ - “userKey”: , //CacheableEnvironmentFlags dictionary - “mobileKey”: , - “featureFlags”: [ - : [ - “key”: , //FeatureFlag dictionary - “version”: , - “flagVersion”: , - “variation”: , - “value”: , - “trackEvents”: , - “debugEventsUntilDate”: , - "reason: , - "trackReason": - ] - ] - ] - ], - “lastUpdated”: - ] -] -*/ -struct CacheableUserEnvironmentFlags { - enum CodingKeys: String, CodingKey, CaseIterable { - case userKey, environmentFlags, lastUpdated - } - - let userKey: String - let environmentFlags: [MobileKey: CacheableEnvironmentFlags] - let lastUpdated: Date - - init(userKey: String, environmentFlags: [MobileKey: CacheableEnvironmentFlags], lastUpdated: Date) { - self.userKey = userKey - self.environmentFlags = environmentFlags - self.lastUpdated = lastUpdated - } - - init?(dictionary: [String: Any]) { - guard let userKey = dictionary[CodingKeys.userKey.rawValue] as? String, - let environmentFlagsDictionary = dictionary[CodingKeys.environmentFlags.rawValue] as? [MobileKey: [LDFlagKey: Any]], - let lastUpdated = (dictionary[CodingKeys.lastUpdated.rawValue] as? String)?.dateValue - else { return nil } - let environmentFlags = environmentFlagsDictionary.compactMapValues { cacheableEnvironmentFlagsDictionary in - CacheableEnvironmentFlags(dictionary: cacheableEnvironmentFlagsDictionary) - } - self.init(userKey: userKey, environmentFlags: environmentFlags, lastUpdated: lastUpdated) - } - - init?(object: Any) { - guard let dictionary = object as? [String: Any] - else { return nil } - self.init(dictionary: dictionary) - } - - var dictionaryValue: [String: Any] { - [CodingKeys.userKey.rawValue: userKey, - CodingKeys.lastUpdated.rawValue: lastUpdated.stringValue, - CodingKeys.environmentFlags.rawValue: environmentFlags.compactMapValues { $0.dictionaryValue } ] - } -} - -extension DateFormatter { - /// Date formatter configured to format dates to/from the format 2018-08-13T19:06:38.123Z - class var ldDateFormatter: DateFormatter { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - formatter.timeZone = TimeZone(identifier: "UTC") - return formatter - } -} - -extension Date { - /// Date string using the format 2018-08-13T19:06:38.123Z - var stringValue: String { DateFormatter.ldDateFormatter.string(from: self) } - - // When a date is converted to JSON, the resulting string is not as precise as the original date (only to the nearest .001s) - // By converting the date to json, then back into a date, the result can be compared with any date re-inflated from json - /// Date truncated to the nearest millisecond, which is the precision for string formatted dates - var stringEquivalentDate: Date { stringValue.dateValue } -} - -extension String { - /// Date converted from a string using the format 2018-08-13T19:06:38.123Z - var dateValue: Date { DateFormatter.ldDateFormatter.date(from: self) ?? Date() } -} diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index e33b9832..a0a76f87 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -138,7 +138,7 @@ class FeatureEvent: Event, SubEvent { try container.encode(value, forKey: .value) try container.encode(defaultValue, forKey: .defaultValue) if let reason = includeReason || featureFlag?.trackReason ?? false ? featureFlag?.reason : nil { - try container.encode(LDValue.fromAny(reason), forKey: .reason) + try container.encode(reason, forKey: .reason) } try container.encode(creationDate, forKey: .creationDate) } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift index 2e7abae4..b2357de7 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift @@ -7,7 +7,7 @@ struct FeatureFlag: Codable { } let flagKey: LDFlagKey - let value: Any? + let value: LDValue let variation: Int? /// The "environment" version. It changes whenever any feature flag in the environment changes. Used for version comparisons for streaming patch and delete. let version: Int? @@ -15,22 +15,22 @@ struct FeatureFlag: Codable { let flagVersion: Int? let trackEvents: Bool let debugEventsUntilDate: Date? - let reason: [String: Any]? + let reason: [String: LDValue]? let trackReason: Bool var versionForEvents: Int? { flagVersion ?? version } init(flagKey: LDFlagKey, - value: Any? = nil, + value: LDValue = .null, variation: Int? = nil, version: Int? = nil, flagVersion: Int? = nil, trackEvents: Bool = false, debugEventsUntilDate: Date? = nil, - reason: [String: Any]? = nil, + reason: [String: LDValue]? = nil, trackReason: Bool = false) { self.flagKey = flagKey - self.value = value is NSNull ? nil : value + self.value = value self.variation = variation self.version = version self.flagVersion = flagVersion @@ -40,21 +40,6 @@ struct FeatureFlag: Codable { self.trackReason = trackReason } - init?(dictionary: [String: Any]?) { - guard let dictionary = dictionary, - let flagKey = dictionary.flagKey - else { return nil } - self.init(flagKey: flagKey, - value: dictionary.value, - variation: dictionary.variation, - version: dictionary.version, - flagVersion: dictionary.flagVersion, - trackEvents: dictionary.trackEvents ?? false, - debugEventsUntilDate: Date(millisSince1970: dictionary.debugEventsUntilDate), - reason: dictionary.reason, - trackReason: dictionary.trackReason ?? false) - } - init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let flagKey = try container.decode(LDFlagKey.self, forKey: .flagKey) @@ -68,21 +53,20 @@ struct FeatureFlag: Codable { throw DecodingError.dataCorruptedError(forKey: .flagKey, in: container, debugDescription: description) } self.flagKey = flagKey - self.value = (try container.decodeIfPresent(LDValue.self, forKey: .value))?.toAny() + self.value = (try container.decodeIfPresent(LDValue.self, forKey: .value)) ?? .null self.variation = try container.decodeIfPresent(Int.self, forKey: .variation) self.version = try container.decodeIfPresent(Int.self, forKey: .version) self.flagVersion = try container.decodeIfPresent(Int.self, forKey: .flagVersion) self.trackEvents = (try container.decodeIfPresent(Bool.self, forKey: .trackEvents)) ?? false self.debugEventsUntilDate = Date(millisSince1970: try container.decodeIfPresent(Int64.self, forKey: .debugEventsUntilDate)) - self.reason = (try container.decodeIfPresent(LDValue.self, forKey: .reason))?.toAny() as? [String: Any] + self.reason = try container.decodeIfPresent([String: LDValue].self, forKey: .reason) self.trackReason = (try container.decodeIfPresent(Bool.self, forKey: .trackReason)) ?? false } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(flagKey, forKey: .flagKey) - let val = LDValue.fromAny(value) - if val != .null { try container.encode(val, forKey: .value) } + if value != .null { try container.encode(value, forKey: .value) } try container.encodeIfPresent(variation, forKey: .variation) try container.encodeIfPresent(version, forKey: .version) try container.encodeIfPresent(flagVersion, forKey: .flagVersion) @@ -90,24 +74,10 @@ struct FeatureFlag: Codable { if let debugEventsUntilDate = debugEventsUntilDate { try container.encode(debugEventsUntilDate.millisSince1970, forKey: .debugEventsUntilDate) } - if reason != nil { try container.encode(LDValue.fromAny(reason), forKey: .reason) } + if reason != nil { try container.encode(reason, forKey: .reason) } if trackReason { try container.encode(true, forKey: .trackReason) } } - var dictionaryValue: [String: Any] { - var dictionaryValue = [String: Any]() - dictionaryValue[CodingKeys.flagKey.rawValue] = flagKey - dictionaryValue[CodingKeys.value.rawValue] = value ?? NSNull() - dictionaryValue[CodingKeys.variation.rawValue] = variation ?? NSNull() - dictionaryValue[CodingKeys.version.rawValue] = version ?? NSNull() - dictionaryValue[CodingKeys.flagVersion.rawValue] = flagVersion ?? NSNull() - dictionaryValue[CodingKeys.trackEvents.rawValue] = trackEvents ? true : NSNull() - dictionaryValue[CodingKeys.debugEventsUntilDate.rawValue] = debugEventsUntilDate?.millisSince1970 ?? NSNull() - dictionaryValue[CodingKeys.reason.rawValue] = reason ?? NSNull() - dictionaryValue[CodingKeys.trackReason.rawValue] = trackReason ? true : NSNull() - return dictionaryValue - } - func shouldCreateDebugEvents(lastEventReportResponseTime: Date?) -> Bool { (lastEventReportResponseTime ?? Date()) <= (debugEventsUntilDate ?? Date.distantPast) } @@ -116,8 +86,8 @@ struct FeatureFlag: Codable { struct FeatureFlagCollection: Codable { let flags: [LDFlagKey: FeatureFlag] - init(_ flags: [FeatureFlag]) { - self.flags = Dictionary(uniqueKeysWithValues: flags.map { ($0.flagKey, $0) }) + init(_ flags: [LDFlagKey: FeatureFlag]) { + self.flags = flags } init(from decoder: Decoder) throws { @@ -134,74 +104,3 @@ struct FeatureFlagCollection: Codable { try flags.encode(to: encoder) } } - -extension FeatureFlag: Equatable { - static func == (lhs: FeatureFlag, rhs: FeatureFlag) -> Bool { - lhs.flagKey == rhs.flagKey && - lhs.variation == rhs.variation && - lhs.version == rhs.version && - AnyComparer.isEqual(lhs.reason, to: rhs.reason) && - lhs.trackReason == rhs.trackReason - } -} - -extension Dictionary where Key == LDFlagKey, Value == FeatureFlag { - var dictionaryValue: [String: Any] { self.compactMapValues { $0.dictionaryValue } } -} - -extension Dictionary where Key == String, Value == Any { - var flagKey: String? { - self[FeatureFlag.CodingKeys.flagKey.rawValue] as? String - } - - var value: Any? { - self[FeatureFlag.CodingKeys.value.rawValue] - } - - var variation: Int? { - self[FeatureFlag.CodingKeys.variation.rawValue] as? Int - } - - var version: Int? { - self[FeatureFlag.CodingKeys.version.rawValue] as? Int - } - - var flagVersion: Int? { - self[FeatureFlag.CodingKeys.flagVersion.rawValue] as? Int - } - - var trackEvents: Bool? { - self[FeatureFlag.CodingKeys.trackEvents.rawValue] as? Bool - } - - var debugEventsUntilDate: Int64? { - self[FeatureFlag.CodingKeys.debugEventsUntilDate.rawValue] as? Int64 - } - - var reason: [String: Any]? { - self[FeatureFlag.CodingKeys.reason.rawValue] as? [String: Any] - } - - var trackReason: Bool? { - self[FeatureFlag.CodingKeys.trackReason.rawValue] as? Bool - } - - var flagCollection: [LDFlagKey: FeatureFlag]? { - guard !(self is [LDFlagKey: FeatureFlag]) - else { - return self as? [LDFlagKey: FeatureFlag] - } - let flagCollection = [LDFlagKey: FeatureFlag](uniqueKeysWithValues: compactMap { flagKey, value -> (LDFlagKey, FeatureFlag)? in - var elementDictionary = value as? [String: Any] - if elementDictionary?[FeatureFlag.CodingKeys.flagKey.rawValue] == nil { - elementDictionary?[FeatureFlag.CodingKeys.flagKey.rawValue] = flagKey - } - guard let featureFlag = FeatureFlag(dictionary: elementDictionary) - else { return nil } - return (flagKey, featureFlag) - }) - guard flagCollection.count == self.count - else { return nil } - return flagCollection - } -} diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift deleted file mode 100644 index 6c888ba7..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValue.swift +++ /dev/null @@ -1,128 +0,0 @@ -import Foundation - -/// Defines the types and values of a feature flag. The SDK limits feature flags to these types by use of the `LDFlagValueConvertible` protocol, which uses this type. Client app developers should not construct an LDFlagValue. -enum LDFlagValue: Equatable { - /// Bool flag value - case bool(Bool) - /// Int flag value - case int(Int) - /// Double flag value - case double(Double) - /// String flag value - case string(String) - /// Array flag value - case array([LDFlagValue]) - /// Dictionary flag value - case dictionary([LDFlagKey: LDFlagValue]) - /// Null flag value - case null -} - -// The commented out code in this file is intended to support automated typing from the json, which is not implemented in the 4.0.0 release. When that capability can be supported with later Swift versions, uncomment this code to support it. - -// MARK: - Bool - -// extension LDFlagValue: ExpressibleByBooleanLiteral { -// init(_ value: Bool) { -// self = .bool(value) -// } -// -// public init(booleanLiteral value: Bool) { -// self.init(value) -// } -// } - -// MARK: - Int - -// extension LDFlagValue: ExpressibleByIntegerLiteral { -// public init(_ value: Int) { -// self = .int(value) -// } -// -// public init(integerLiteral value: Int) { -// self.init(value) -// } -// } - -// MARK: - Double - -// extension LDFlagValue: ExpressibleByFloatLiteral { -// public init(_ value: FloatLiteralType) { -// self = .double(value) -// } -// -// public init(floatLiteral value: FloatLiteralType) { -// self.init(value) -// } -// } - -// MARK: - String - -// extension LDFlagValue: ExpressibleByStringLiteral { -// public init(_ value: StringLiteralType) { -// self = .string(value) -// } -// -// public init(unicodeScalarLiteral value: StringLiteralType) { -// self.init(value) -// } -// -// public init(extendedGraphemeClusterLiteral value: StringLiteralType) { -// self.init(value) -// } -// -// public init(stringLiteral value: StringLiteralType) { -// self.init(value) -// } -// } - -// MARK: - Array - -// extension LDFlagValue: ExpressibleByArrayLiteral { -// public init(_ collection: Collection) where Collection.Iterator.Element == LDFlagValue { -// self = .array(Array(collection)) -// } -// -// public init(arrayLiteral elements: LDFlagValue...) { -// self.init(elements) -// } -// } - -extension LDFlagValue { - var flagValueArray: [LDFlagValue]? { - guard case let .array(array) = self - else { return nil } - return array - } -} - -// MARK: - Dictionary - -// extension LDFlagValue: ExpressibleByDictionaryLiteral { -// public typealias Key = LDFlagKey -// public typealias Value = LDFlagValue -// -// public init(_ keyValuePairs: Dictionary) where Dictionary.Iterator.Element == (Key, Value) { -// var dictionary = [Key: Value]() -// for (key, value) in keyValuePairs { -// dictionary[key] = value -// } -// self.init(dictionary) -// } -// -// public init(dictionaryLiteral elements: (Key, Value)...) { -// self.init(elements) -// } -// -// public init(_ dictionary: Dictionary) { -// self = .dictionary(dictionary) -// } -// } - -extension LDFlagValue { - var flagValueDictionary: [LDFlagKey: LDFlagValue]? { - guard case let .dictionary(value) = self - else { return nil } - return value - } -} diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift deleted file mode 100644 index 2f3cf1cb..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagValue/LDFlagValueConvertible.swift +++ /dev/null @@ -1,118 +0,0 @@ -import Foundation - -/// Protocol used by the SDK to limit feature flag types to those representable on LaunchDarkly servers. Client app developers should not need to use this protocol. The protocol is public because `LDClient.variation(forKey:defaultValue:)` and `LDClient.variationDetail(forKey:defaultValue:)` return a type that conforms to this protocol. See `LDFlagValue` for types that LaunchDarkly feature flags can take. -public protocol LDFlagValueConvertible { -// This commented out code here and in each extension will be used to support automatic typing. Version `4.0.0` does not support that capability. When that capability is added, uncomment this code. -// func toLDFlagValue() -> LDFlagValue -} - -/// :nodoc: -extension Bool: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// return .bool(self) -// } -} - -/// :nodoc: -extension Int: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// return .int(self) -// } -} - -/// :nodoc: -extension Double: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// return .double(self) -// } -} - -/// :nodoc: -extension String: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// return .string(self) -// } -} - -/// :nodoc: -extension Array where Element: LDFlagValueConvertible { -// func toLDFlagValue() -> LDFlagValue { -// let flagValues = self.map { (element) in -// element.toLDFlagValue() -// } -// return .array(flagValues) -// } -} - -/// :nodoc: -extension Array: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// guard let flags = self as? [LDFlagValueConvertible] -// else { -// return .null -// } -// let flagValues = flags.map { (element) in -// element.toLDFlagValue() -// } -// return .array(flagValues) -// } -} - -/// :nodoc: -extension Dictionary where Value: LDFlagValueConvertible { -// func toLDFlagValue() -> LDFlagValue { -// var flagValues = [LDFlagKey: LDFlagValue]() -// for (key, value) in self { -// flagValues[String(describing: key)] = value.toLDFlagValue() -// } -// return .dictionary(flagValues) -// } -} - -/// :nodoc: -extension Dictionary: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// if let flagValueDictionary = self as? [LDFlagKey: LDFlagValue] { -// return .dictionary(flagValueDictionary) -// } -// guard let flagValues = Dictionary.convertToFlagValues(self as? [LDFlagKey: LDFlagValueConvertible]) -// else { -// return .null -// } -// return .dictionary(flagValues) -// } -// -// static func convertToFlagValues(_ dictionary: [LDFlagKey: LDFlagValueConvertible]?) -> [LDFlagKey: LDFlagValue]? { -// guard let dictionary = dictionary -// else { -// return nil -// } -// var flagValues = [LDFlagKey: LDFlagValue]() -// for (key, value) in dictionary { -// flagValues[String(describing: key)] = value.toLDFlagValue() -// } -// return flagValues -// } -} - -/// :nodoc: -extension NSNull: LDFlagValueConvertible { -// public func toLDFlagValue() -> LDFlagValue { -// return .null -// } -} - -// extension LDFlagValueConvertible { -// func isEqual(to other: LDFlagValueConvertible) -> Bool { -// switch (self.toLDFlagValue(), other.toLDFlagValue()) { -// case (.bool(let value), .bool(let otherValue)): return value == otherValue -// case (.int(let value), .int(let otherValue)): return value == otherValue -// case (.double(let value), .double(let otherValue)): return value == otherValue -// case (.string(let value), .string(let otherValue)): return value == otherValue -// case (.array(let value), .array(let otherValue)): return value == otherValue -// case (.dictionary(let value), .dictionary(let otherValue)): return value == otherValue -// case (.null, .null): return true -// default: return false -// } -// } -// } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift index ceb12c10..5b467085 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift @@ -10,9 +10,9 @@ public final class LDEvaluationDetail { /// The index of the returned value within the flag's list of variations, or `nil` if the default was returned. public internal(set) var variationIndex: Int? /// A structure representing the main factor that influenced the resultant flag evaluation value. - public internal(set) var reason: [String: Any]? + public internal(set) var reason: [String: LDValue]? - internal init(value: T, variationIndex: Int?, reason: [String: Any]?) { + internal init(value: T, variationIndex: Int?, reason: [String: LDValue]?) { self.value = value self.variationIndex = variationIndex self.reason = reason diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 10257a42..91ec678b 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -50,9 +50,6 @@ public struct LDUser: Encodable { */ public var privateAttributes: [UserAttribute] - /// An NSObject wrapper for the Swift LDUser struct. Intended for use in mixed apps when Swift code needs to pass a user into an Objective-C method. - public var objcLdUser: ObjcLDUser { ObjcLDUser(self) } - var contextKind: String { isAnonymous ? "anonymousUser" : "user" } /** @@ -199,10 +196,4 @@ extension LDUser: Equatable { } } -extension LDUserWrapper { - struct Keys { - fileprivate static let featureFlags = "featuresJsonDictionary" - } -} - extension LDUser: TypeIdentifying { } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift index 73eba2a7..a02a0cad 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift @@ -29,11 +29,11 @@ public class ObjcLDChangedFlag: NSObject { public final class ObjcLDBoolChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: Bool { - (changedFlag.oldValue.toAny() as? Bool) ?? false + changedFlag.oldValue.booleanValue() } /// The changed flag's value after it changed @objc public var newValue: Bool { - (changedFlag.newValue.toAny() as? Bool) ?? false + changedFlag.newValue.booleanValue() } override init(_ changedFlag: LDChangedFlag) { @@ -75,11 +75,11 @@ public final class ObjcLDIntegerChangedFlag: ObjcLDChangedFlag { public final class ObjcLDDoubleChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: Double { - (changedFlag.oldValue.toAny() as? Double) ?? 0.0 + changedFlag.oldValue.doubleValue() } /// The changed flag's value after it changed @objc public var newValue: Double { - (changedFlag.newValue.toAny() as? Double) ?? 0.0 + changedFlag.newValue.doubleValue() } override init(_ changedFlag: LDChangedFlag) { @@ -98,11 +98,11 @@ public final class ObjcLDDoubleChangedFlag: ObjcLDChangedFlag { public final class ObjcLDStringChangedFlag: ObjcLDChangedFlag { /// The changed flag's value before it changed @objc public var oldValue: String? { - (changedFlag.oldValue.toAny() as? String) + changedFlag.oldValue.stringValue() } /// The changed flag's value after it changed @objc public var newValue: String? { - (changedFlag.newValue.toAny() as? String) + changedFlag.newValue.stringValue() } override init(_ changedFlag: LDChangedFlag) { diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index cc0fb65d..3d345679 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -188,7 +188,7 @@ public final class ObjcLDClient: NSObject { */ /// - Tag: boolVariation @objc public func boolVariation(forKey key: LDFlagKey, defaultValue: Bool) -> Bool { - ldClient.variation(forKey: key, defaultValue: defaultValue) + ldClient.boolVariation(forKey: key, defaultValue: defaultValue) } /** @@ -200,8 +200,8 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDBoolEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func boolVariationDetail(forKey key: LDFlagKey, defaultValue: Bool) -> ObjcLDBoolEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDBoolEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) + let evaluationDetail = ldClient.boolVariationDetail(forKey: key, defaultValue: defaultValue) + return ObjcLDBoolEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) } /** @@ -229,7 +229,7 @@ public final class ObjcLDClient: NSObject { */ /// - Tag: integerVariation @objc public func integerVariation(forKey key: LDFlagKey, defaultValue: Int) -> Int { - ldClient.variation(forKey: key, defaultValue: defaultValue) + ldClient.intVariation(forKey: key, defaultValue: defaultValue) } /** @@ -241,8 +241,8 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDIntegerEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func integerVariationDetail(forKey key: LDFlagKey, defaultValue: Int) -> ObjcLDIntegerEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDIntegerEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) + let evaluationDetail = ldClient.intVariationDetail(forKey: key, defaultValue: defaultValue) + return ObjcLDIntegerEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) } /** @@ -270,7 +270,7 @@ public final class ObjcLDClient: NSObject { */ /// - Tag: doubleVariation @objc public func doubleVariation(forKey key: LDFlagKey, defaultValue: Double) -> Double { - ldClient.variation(forKey: key, defaultValue: defaultValue) + ldClient.doubleVariation(forKey: key, defaultValue: defaultValue) } /** @@ -282,8 +282,8 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDDoubleEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func doubleVariationDetail(forKey key: LDFlagKey, defaultValue: Double) -> ObjcLDDoubleEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDDoubleEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) + let evaluationDetail = ldClient.doubleVariationDetail(forKey: key, defaultValue: defaultValue) + return ObjcLDDoubleEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) } /** @@ -311,7 +311,7 @@ public final class ObjcLDClient: NSObject { */ /// - Tag: stringVariation @objc public func stringVariation(forKey key: LDFlagKey, defaultValue: String) -> String { - ldClient.variation(forKey: key, defaultValue: defaultValue) + ldClient.stringVariation(forKey: key, defaultValue: defaultValue) } /** @@ -323,8 +323,8 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDStringEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func stringVariationDetail(forKey key: LDFlagKey, defaultValue: String) -> ObjcLDStringEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDStringEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) + let evaluationDetail = ldClient.stringVariationDetail(forKey: key, defaultValue: defaultValue) + return ObjcLDStringEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() ?? NSNull() }) } /** @@ -351,9 +351,9 @@ public final class ObjcLDClient: NSObject { - returns: The requested NSArray feature flag value, or the default value if the flag is missing or cannot be cast to a NSArray, or the client is not started */ /// - Tag: arrayVariation - @objc public func arrayVariation(forKey key: LDFlagKey, defaultValue: [Any]) -> [Any] { - ldClient.variation(forKey: key, defaultValue: defaultValue) - } +// @objc public func arrayVariation(forKey key: LDFlagKey, defaultValue: [Any]) -> [Any] { +// ldClient.variation(forKey: key, defaultValue: defaultValue) +// } /** See [arrayVariation](x-source-tag://arrayVariation) for more information on variation methods. @@ -363,10 +363,10 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDArrayEvaluationDetail containing your value as well as useful information on why that value was returned. */ - @objc public func arrayVariationDetail(forKey key: LDFlagKey, defaultValue: [Any]) -> ObjcLDArrayEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDArrayEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) - } +// @objc public func arrayVariationDetail(forKey key: LDFlagKey, defaultValue: [Any]) -> ObjcLDArrayEvaluationDetail { +// let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) +// return ObjcLDArrayEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() }) +// } /** Returns the NSDictionary variation for the given feature flag. If the flag does not exist, cannot be cast to a NSDictionary, or the LDClient is not started, returns the default value. @@ -392,9 +392,9 @@ public final class ObjcLDClient: NSObject { - returns: The requested NSDictionary feature flag value, or the default value if the flag is missing or cannot be cast to a NSDictionary, or the client is not started */ /// - Tag: dictionaryVariation - @objc public func dictionaryVariation(forKey key: LDFlagKey, defaultValue: [String: Any]) -> [String: Any] { - ldClient.variation(forKey: key, defaultValue: defaultValue) - } +// @objc public func dictionaryVariation(forKey key: LDFlagKey, defaultValue: [String: Any]) -> [String: Any] { +// ldClient.variation(forKey: key, defaultValue: defaultValue) +// } /** See [dictionaryVariation](x-source-tag://dictionaryVariation) for more information on variation methods. @@ -404,10 +404,10 @@ public final class ObjcLDClient: NSObject { - returns: ObjcLDDictionaryEvaluationDetail containing your value as well as useful information on why that value was returned. */ - @objc public func dictionaryVariationDetail(forKey key: LDFlagKey, defaultValue: [String: Any]) -> ObjcLDDictionaryEvaluationDetail { - let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) - return ObjcLDDictionaryEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason) - } +// @objc public func dictionaryVariationDetail(forKey key: LDFlagKey, defaultValue: [String: Any]) -> ObjcLDDictionaryEvaluationDetail { +// let evaluationDetail = ldClient.variationDetail(forKey: key, defaultValue: defaultValue) +// return ObjcLDDictionaryEvaluationDetail(value: evaluationDetail.value, variationIndex: evaluationDetail.variationIndex, reason: evaluationDetail.reason?.mapValues { $0.toAny() }) +// } /** Returns a dictionary with the flag keys and their values. If the LDClient is not started, returns nil. @@ -416,7 +416,7 @@ public final class ObjcLDClient: NSObject { LDClient will not provide any source or change information, only flag keys and flag values. The client app should convert the feature flag value into the desired type. */ - @objc public var allFlags: [LDFlagKey: Any]? { ldClient.allFlags } + @objc public var allFlags: [LDFlagKey: Any]? { ldClient.allFlags?.mapValues { $0.toAny() ?? NSNull() } } // MARK: - Feature Flag Updates diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index 070f5a66..084cf592 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -2,53 +2,124 @@ import Foundation // sourcery: autoMockable protocol CacheConverting { - func convertCacheData(for user: LDUser, and config: LDConfig) + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) } -// CacheConverter is not thread-safe; run it from a single thread and don't allow other threads to call convertCacheData or data corruption could occur +// Cache model in SDK versions >=4.0.0 <6.0.0. Migration is not supported for earlier versions. +// +// [: [ +// “userKey”: , +// “environmentFlags”: [ +// : [ +// “userKey”: , +// “mobileKey”: , +// “featureFlags”: [ +// : [ +// “key”: , +// “version”: , +// “flagVersion”: , +// “variation”: , +// “value”: , +// “trackEvents”: , +// “debugEventsUntilDate”: , +// "reason: , +// "trackReason": +// ] +// ] +// ] +// ], +// “lastUpdated”: +// ] +// ] + final class CacheConverter: CacheConverting { - struct Constants { - static let maxAge: TimeInterval = -90.0 * 24 * 60 * 60 // 90 days - } + init() { } - struct CacheKeys { - static let ldUserModelDictionary = "ldUserModelDictionary" - static let cachedDataKeyStub = "com.launchdarkly.test.deprecatedCache.cachedDataKey" - } + private func convertV6Data(v6cache: KeyedValueCaching, flagCaches: [MobileKey: FeatureFlagCaching]) { + guard let cachedV6Data = v6cache.dictionary(forKey: "com.launchDarkly.cachedUserEnvironmentFlags") + else { return } - let currentCache: FeatureFlagCaching - private(set) var deprecatedCaches = [DeprecatedCacheModel: DeprecatedCache]() + var cachedEnvData: [MobileKey: [String: (updated: Date, flags: [LDFlagKey: FeatureFlag])]] = [:] + cachedV6Data.forEach { userKey, userDict in + guard let userDict = userDict as? [String: Any], + let userDictUserKey = userDict["userKey"] as? String, + let lastUpdated = (userDict["lastUpdated"] as? String)?.dateValue, + let envsDict = userDict["environmentFlags"] as? [String: Any], + userKey == userDictUserKey + else { return } + envsDict.forEach { mobileKey, envDict in + guard flagCaches.keys.contains(mobileKey), + let envDict = envDict as? [String: Any], + let envUserKey = envDict["userKey"] as? String, + let envMobileKey = envDict["mobileKey"] as? String, + let envFlags = envDict["featureFlags"] as? [String: Any], + envUserKey == userKey && envMobileKey == mobileKey + else { return } - init(serviceFactory: ClientServiceCreating, maxCachedUsers: Int) { - currentCache = serviceFactory.makeFeatureFlagCache(maxCachedUsers: maxCachedUsers) - DeprecatedCacheModel.allCases.forEach { version in - deprecatedCaches[version] = serviceFactory.makeDeprecatedCacheModel(version) + var userEnvFlags: [LDFlagKey: FeatureFlag] = [:] + envFlags.forEach { flagKey, flagDict in + guard let flagDict = flagDict as? [String: Any] + else { return } + let flag = FeatureFlag(flagKey: flagKey, + value: LDValue.fromAny(flagDict["value"]), + variation: flagDict["variation"] as? Int, + version: flagDict["version"] as? Int, + flagVersion: flagDict["flagVersion"] as? Int, + trackEvents: flagDict["trackEvents"] as? Bool ?? false, + debugEventsUntilDate: Date(millisSince1970: flagDict["debugEventsUntilDate"] as? Int64), + reason: (flagDict["reason"] as? [String: Any])?.mapValues { LDValue.fromAny($0) }, + trackReason: flagDict["trackReason"] as? Bool ?? false) + userEnvFlags[flagKey] = flag + } + var otherEnvData = cachedEnvData[mobileKey] ?? [:] + otherEnvData[userKey] = (lastUpdated, userEnvFlags) + cachedEnvData[mobileKey] = otherEnvData + } } - } - func convertCacheData(for user: LDUser, and config: LDConfig) { - convertCacheData(for: user, mobileKey: config.mobileKey) - removeData() + cachedEnvData.forEach { mobileKey, users in + users.forEach { userKey, data in + flagCaches[mobileKey]?.storeFeatureFlags(data.flags, userKey: userKey, lastUpdated: data.updated) + } + } + + v6cache.removeObject(forKey: "com.launchDarkly.cachedUserEnvironmentFlags") } - private func convertCacheData(for user: LDUser, mobileKey: String) { - guard currentCache.retrieveFeatureFlags(forUserWithKey: user.key, andMobileKey: mobileKey) == nil - else { return } - for deprecatedCacheModel in DeprecatedCacheModel.allCases { - let deprecatedCache = deprecatedCaches[deprecatedCacheModel] - guard let cachedData = deprecatedCache?.retrieveFlags(for: user.key, and: mobileKey), - let cachedFlags = cachedData.featureFlags - else { continue } - currentCache.storeFeatureFlags(cachedFlags, userKey: user.key, mobileKey: mobileKey, lastUpdated: cachedData.lastUpdated ?? Date(), storeMode: .sync) - return // If we hit on a cached user, bailout since we converted the flags for that userKey-mobileKey combination; This prefers newer caches over older + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) { + var flagCaches: [String: FeatureFlagCaching] = [:] + keysToConvert.forEach { mobileKey in + let flagCache = serviceFactory.makeFeatureFlagCache(mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) + flagCaches[mobileKey] = flagCache + // Get current cache version and return if up to date + guard let cacheVersionData = flagCache.keyedValueCache.data(forKey: "ld-cache-metadata") + else { return } // Convert those that do not have a version + guard let cacheVersion = (try? JSONDecoder().decode([String: Int].self, from: cacheVersionData))?["version"], + cacheVersion == 7 + else { + // Metadata is invalid, remove existing data and attempt migration + flagCache.keyedValueCache.removeAll() + return + } + // Already up to date + flagCaches.removeValue(forKey: mobileKey) } - } - private func removeData() { - let maxAge = Date().addingTimeInterval(Constants.maxAge) - deprecatedCaches.values.forEach { deprecatedCache in - deprecatedCache.removeData(olderThan: maxAge) + // Skip migration if all environments are V7 + if flagCaches.isEmpty { return } + + // Remove V5 cache data (migration not supported) + let standardDefaults = serviceFactory.makeKeyedValueCache(cacheKey: nil) + standardDefaults.removeObject(forKey: "com.launchdarkly.dataManager.userEnvironments") + + convertV6Data(v6cache: standardDefaults, flagCaches: flagCaches) + + // Set cache version to skip this logic in the future + if let versionMetadata = try? JSONEncoder().encode(["version": 7]) { + flagCaches.forEach { + $0.value.keyedValueCache.set(versionMetadata, forKey: "ld-cache-metadata") + } } } } @@ -58,3 +129,28 @@ extension Date { self.stringEquivalentDate < expirationDate.stringEquivalentDate } } + +extension DateFormatter { + /// Date formatter configured to format dates to/from the format 2018-08-13T19:06:38.123Z + class var ldDateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + formatter.timeZone = TimeZone(identifier: "UTC") + return formatter + } +} + +extension Date { + /// Date string using the format 2018-08-13T19:06:38.123Z + var stringValue: String { DateFormatter.ldDateFormatter.string(from: self) } + + // When a date is converted to JSON, the resulting string is not as precise as the original date (only to the nearest .001s) + // By converting the date to json, then back into a date, the result can be compared with any date re-inflated from json + /// Date truncated to the nearest millisecond, which is the precision for string formatted dates + var stringEquivalentDate: Date { stringValue.dateValue } +} + +extension String { + /// Date converted from a string using the format 2018-08-13T19:06:38.123Z + var dateValue: Date { DateFormatter.ldDateFormatter.date(from: self) ?? Date() } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift deleted file mode 100644 index e952edfb..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation - -protocol DeprecatedCache { - var cachedDataKey: String { get } - var keyedValueCache: KeyedValueCaching { get } - - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan: Date) -> [UserKey] - func removeData(olderThan expirationDate: Date) // provided for testing, to allow the mock to override the protocol extension -} - -extension DeprecatedCache { - func removeData(olderThan expirationDate: Date) { - guard let cachedUserData = keyedValueCache.dictionary(forKey: cachedDataKey) as? [UserKey: [String: Any]], !cachedUserData.isEmpty - else { return } // no cached data - let expiredUserKeys = userKeys(from: cachedUserData, olderThan: expirationDate) - guard !expiredUserKeys.isEmpty - else { return } // no expired user cached data, leave the cache alone - guard expiredUserKeys.count != cachedUserData.count - else { - keyedValueCache.removeObject(forKey: cachedDataKey) // all user cached data is expired, remove the cache key & values - return - } - let unexpiredUserData: [UserKey: [String: Any]] = cachedUserData.filter { userKey, _ in - !expiredUserKeys.contains(userKey) - } - keyedValueCache.set(unexpiredUserData, forKey: cachedDataKey) - } -} - -enum DeprecatedCacheModel: String, CaseIterable { - case version5 // earlier versions are not supported -} - -// updatedAt in cached data was used as the LDUser.lastUpdated, which is deprecated in the Swift SDK -private extension LDUser.CodingKeys { - static let lastUpdated = "updatedAt" // Can't use the CodingKey protocol here, this keeps the usage similar -} - -extension Dictionary where Key == String, Value == Any { - var lastUpdated: Date? { - (self[LDUser.CodingKeys.lastUpdated] as? String)?.dateValue - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift deleted file mode 100644 index f81ceeb6..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCacheModelV5.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Foundation - -// Cache model in use from 2.14.0 up to 4.0.0 -/* Cache model v5 schema -[: [ - “userKey”: , //LDUserEnvironment dictionary - “environments”: [ - : [ - “key: , //LDUserModel dictionary - “ip”: , - “country”: , - “email”: , - “name”: , - “firstName”: , - “lastName”: , - “avatar”: , - “custom”: [ - “device”: , - “os”: , - ...], - “anonymous”: , - “updatedAt: , - ”config”: [ - : [ //LDFlagConfigModel dictionary - “version”: , //LDFlagConfigValue dictionary - “flagVersion”: , - “variation”: , - “value”: , - “trackEvents”: , - “debugEventsUntilDate”: - ] - ], - “privateAttrs”: - ] - ] - ] -] -*/ -final class DeprecatedCacheModelV5: DeprecatedCache { - - struct CacheKeys { - static let userEnvironments = "com.launchdarkly.dataManager.userEnvironments" - static let environments = "environments" - } - - let keyedValueCache: KeyedValueCaching - let cachedDataKey = CacheKeys.userEnvironments - - init(keyedValueCache: KeyedValueCaching) { - self.keyedValueCache = keyedValueCache - } - - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) { - guard let cachedUserEnvironmentsCollection = keyedValueCache.dictionary(forKey: cachedDataKey), !cachedUserEnvironmentsCollection.isEmpty, - let cachedUserEnvironments = cachedUserEnvironmentsCollection[userKey] as? [String: Any], !cachedUserEnvironments.isEmpty, - let cachedEnvironments = cachedUserEnvironments[CacheKeys.environments] as? [MobileKey: [String: Any]], !cachedEnvironments.isEmpty, - let cachedUserDictionary = cachedEnvironments[mobileKey], !cachedUserDictionary.isEmpty, - let featureFlagDictionaries = cachedUserDictionary[LDUser.CodingKeys.config.rawValue] as? [LDFlagKey: [String: Any]] - else { - return (nil, nil) - } - let featureFlags = Dictionary(uniqueKeysWithValues: featureFlagDictionaries.compactMap { flagKey, featureFlagDictionary in - return (flagKey, FeatureFlag(flagKey: flagKey, - value: featureFlagDictionary.value, - variation: featureFlagDictionary.variation, - version: featureFlagDictionary.version, - flagVersion: featureFlagDictionary.flagVersion, - trackEvents: featureFlagDictionary.trackEvents ?? false, - debugEventsUntilDate: Date(millisSince1970: featureFlagDictionary.debugEventsUntilDate))) - }) - return (featureFlags, cachedUserDictionary.lastUpdated) - } - - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan expirationDate: Date) -> [UserKey] { - cachedUserData.compactMap { userKey, userDictionary in - let envsDictionary = userDictionary[CacheKeys.environments] as? [MobileKey: [String: Any]] - let lastUpdated = envsDictionary?.compactMap { $1.lastUpdated }.max() ?? Date.distantFuture - return lastUpdated.isExpired(expirationDate: expirationDate) ? userKey : nil - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift new file mode 100644 index 00000000..4f120db5 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -0,0 +1,56 @@ +import Foundation + +// sourcery: autoMockable +protocol FeatureFlagCaching { + // sourcery: defaultMockValue = KeyedValueCachingMock() + var keyedValueCache: KeyedValueCaching { get } + func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? + func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) +} + +final class FeatureFlagCache: FeatureFlagCaching { + let keyedValueCache: KeyedValueCaching + let maxCachedUsers: Int + + init(serviceFactory: ClientServiceCreating, mobileKey: MobileKey, maxCachedUsers: Int) { + let cacheKey: String + if let bundleId = Bundle.main.bundleIdentifier { + cacheKey = "\(Util.sha256base64(bundleId)).\(Util.sha256base64(mobileKey))" + } else { + cacheKey = Util.sha256base64(mobileKey) + } + self.keyedValueCache = serviceFactory.makeKeyedValueCache(cacheKey: "com.launchdarkly.client.\(cacheKey)") + self.maxCachedUsers = maxCachedUsers + } + + func retrieveFeatureFlags(userKey: String) -> [LDFlagKey: FeatureFlag]? { + guard let cachedData = keyedValueCache.data(forKey: "flags-\(Util.sha256base64(userKey))"), + let cachedFlags = try? JSONDecoder().decode(FeatureFlagCollection.self, from: cachedData) + else { return nil } + return cachedFlags.flags + } + + func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, lastUpdated: Date) { + guard self.maxCachedUsers != 0, let encoded = try? JSONEncoder().encode(featureFlags) + else { return } + + let userSha = Util.sha256base64(userKey) + self.keyedValueCache.set(encoded, forKey: "flags-\(userSha)") + + var cachedUsers: [String: Int64] = [:] + if let cacheMetadata = self.keyedValueCache.data(forKey: "cached-users") { + cachedUsers = (try? JSONDecoder().decode([String: Int64].self, from: cacheMetadata)) ?? [:] + } + cachedUsers[userSha] = lastUpdated.millisSince1970 + if cachedUsers.count > self.maxCachedUsers && self.maxCachedUsers > 0 { + let sorted = cachedUsers.sorted { $0.value < $1.value } + sorted.prefix(cachedUsers.count - self.maxCachedUsers).forEach { sha, _ in + cachedUsers.removeValue(forKey: sha) + self.keyedValueCache.removeObject(forKey: "flags-\(sha)") + } + } + if let encoded = try? JSONEncoder().encode(cachedUsers) { + self.keyedValueCache.set(encoded, forKey: "cached-users") + } + } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift index 5598594f..bb6a1de5 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift @@ -2,10 +2,19 @@ import Foundation // sourcery: autoMockable protocol KeyedValueCaching { - func set(_ value: Any?, forKey: String) - // sourcery: DefaultReturnValue = nil + func set(_ value: Data, forKey: String) + func data(forKey: String) -> Data? func dictionary(forKey: String) -> [String: Any]? func removeObject(forKey: String) + func removeAll() } -extension UserDefaults: KeyedValueCaching { } +extension UserDefaults: KeyedValueCaching { + func set(_ value: Data, forKey: String) { + set(value as Any?, forKey: forKey) + } + + func removeAll() { + dictionaryRepresentation().keys.forEach { removeObject(forKey: $0) } + } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift deleted file mode 100644 index 81db8dde..00000000 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/UserEnvironmentFlagCache.swift +++ /dev/null @@ -1,100 +0,0 @@ -import Foundation - -enum FlagCachingStoreMode: CaseIterable { - case async, sync -} - -// sourcery: autoMockable -protocol FeatureFlagCaching { - // sourcery: defaultMockValue = 5 - var maxCachedUsers: Int { get set } - - func retrieveFeatureFlags(forUserWithKey userKey: String, andMobileKey mobileKey: String) -> [LDFlagKey: FeatureFlag]? - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode) -} - -final class UserEnvironmentFlagCache: FeatureFlagCaching { - - struct Constants { - static let cacheStoreOperationQueueLabel = "com.launchDarkly.FeatureFlagCaching.cacheStoreOperationQueue" - } - - struct CacheKeys { - static let cachedUserEnvironmentFlags = "com.launchDarkly.cachedUserEnvironmentFlags" - } - - private(set) var keyedValueCache: KeyedValueCaching - var maxCachedUsers: Int - - private static let cacheStoreOperationQueue = DispatchQueue(label: Constants.cacheStoreOperationQueueLabel, qos: .background) - - init(withKeyedValueCache keyedValueCache: KeyedValueCaching, maxCachedUsers: Int) { - self.keyedValueCache = keyedValueCache - self.maxCachedUsers = maxCachedUsers - } - - func retrieveFeatureFlags(forUserWithKey userKey: String, andMobileKey mobileKey: String) -> [LDFlagKey: FeatureFlag]? { - let cacheableUserEnvironmentsCollection = retrieveCacheableUserEnvironmentsCollection() - return cacheableUserEnvironmentsCollection[userKey]?.environmentFlags[mobileKey]?.featureFlags - } - - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode) { - storeFeatureFlags(featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated, storeMode: storeMode, completion: nil) - } - - func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], - userKey: String, - mobileKey: String, - lastUpdated: Date, - storeMode: FlagCachingStoreMode = .async, - completion: (() -> Void)?) { - if storeMode == .async { - UserEnvironmentFlagCache.cacheStoreOperationQueue.async { - self.storeFlags(featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated) - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - } - } else { - UserEnvironmentFlagCache.cacheStoreOperationQueue.sync { - self.storeFlags(featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated) - } - } - } - - private func storeFlags(_ featureFlags: [LDFlagKey: FeatureFlag], userKey: String, mobileKey: String, lastUpdated: Date) { - var cacheableUserEnvironmentsCollection = self.retrieveCacheableUserEnvironmentsCollection() - let selectedCacheableUserEnvironments = cacheableUserEnvironmentsCollection[userKey] ?? CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: [:], lastUpdated: Date()) - var environmentFlags = selectedCacheableUserEnvironments.environmentFlags - environmentFlags[mobileKey] = CacheableEnvironmentFlags(userKey: userKey, mobileKey: mobileKey, featureFlags: featureFlags) - cacheableUserEnvironmentsCollection[userKey] = CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: environmentFlags, lastUpdated: lastUpdated) - self.store(cacheableUserEnvironmentsCollection: cacheableUserEnvironmentsCollection) - } - - // MARK: - CacheableUserEnvironmentsCollection - private func store(cacheableUserEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags]) { - let userEnvironmentsCollection = removeOldestUsersIfNeeded(from: cacheableUserEnvironmentsCollection) - keyedValueCache.set(userEnvironmentsCollection.compactMapValues { $0.dictionaryValue }, forKey: CacheKeys.cachedUserEnvironmentFlags) - } - - private func removeOldestUsersIfNeeded(from cacheableUserEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags]) -> [UserKey: CacheableUserEnvironmentFlags] { - guard cacheableUserEnvironmentsCollection.count > maxCachedUsers && maxCachedUsers >= 0 - else { - return cacheableUserEnvironmentsCollection - } - // sort collection into key-value pairs in descending order...youngest to oldest - var userEnvironmentsCollection = cacheableUserEnvironmentsCollection.sorted { - $1.value.lastUpdated < $0.value.lastUpdated - } - while userEnvironmentsCollection.count > maxCachedUsers && maxCachedUsers >= 0 { - userEnvironmentsCollection.removeLast() - } - return [UserKey: CacheableUserEnvironmentFlags](userEnvironmentsCollection, uniquingKeysWith: { value1, _ in - value1 - }) - } - - private func retrieveCacheableUserEnvironmentsCollection() -> [UserKey: CacheableUserEnvironmentFlags] { - keyedValueCache.dictionary(forKey: CacheKeys.cachedUserEnvironmentFlags)?.compactMapValues { CacheableUserEnvironmentFlags(object: $0) } ?? [:] - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index 51112d0e..aa804f8a 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -2,10 +2,9 @@ import Foundation import LDSwiftEventSource protocol ClientServiceCreating { - func makeKeyedValueCache() -> KeyedValueCaching - func makeFeatureFlagCache(maxCachedUsers: Int) -> FeatureFlagCaching - func makeCacheConverter(maxCachedUsers: Int) -> CacheConverting - func makeDeprecatedCacheModel(_ model: DeprecatedCacheModel) -> DeprecatedCache + func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching + func makeFeatureFlagCache(mobileKey: String, maxCachedUsers: Int) -> FeatureFlagCaching + func makeCacheConverter() -> CacheConverting func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, useReport: Bool, service: DarklyServiceProvider) -> LDFlagSynchronizing func makeFlagSynchronizer(streamingMode: LDStreamingMode, @@ -26,22 +25,16 @@ protocol ClientServiceCreating { } final class ClientServiceFactory: ClientServiceCreating { - func makeKeyedValueCache() -> KeyedValueCaching { - UserDefaults.standard + func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching { + UserDefaults(suiteName: cacheKey)! } - func makeFeatureFlagCache(maxCachedUsers: Int) -> FeatureFlagCaching { - UserEnvironmentFlagCache(withKeyedValueCache: makeKeyedValueCache(), maxCachedUsers: maxCachedUsers) + func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedUsers: Int) -> FeatureFlagCaching { + FeatureFlagCache(serviceFactory: self, mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) } - func makeCacheConverter(maxCachedUsers: Int) -> CacheConverting { - CacheConverter(serviceFactory: self, maxCachedUsers: maxCachedUsers) - } - - func makeDeprecatedCacheModel(_ model: DeprecatedCacheModel) -> DeprecatedCache { - switch model { - case .version5: return DeprecatedCacheModelV5(keyedValueCache: makeKeyedValueCache()) - } + func makeCacheConverter() -> CacheConverting { + CacheConverter() } func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift index 296c5a00..9938eb90 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift @@ -90,7 +90,7 @@ final class FlagChangeNotifier: FlagChangeNotifying { } let changedFlags = [LDFlagKey: LDChangedFlag](uniqueKeysWithValues: changedFlagKeys.map { - ($0, LDChangedFlag(key: $0, oldValue: LDValue.fromAny(oldFlags[$0]?.value), newValue: LDValue.fromAny(newFlags[$0]?.value))) + ($0, LDChangedFlag(key: $0, oldValue: oldFlags[$0]?.value ?? .null, newValue: newFlags[$0]?.value ?? .null)) }) Log.debug(typeName(and: #function) + "notifying observers for changes to flags: \(changedFlags.keys.joined(separator: ", ")).") selectedObservers.forEach { observer in @@ -113,12 +113,15 @@ final class FlagChangeNotifier: FlagChangeNotifying { } private func findChangedFlagKeys(oldFlags: [LDFlagKey: FeatureFlag], newFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey] { - oldFlags.symmetricDifference(newFlags) // symmetricDifference tests for equality, which includes version. Exclude version here. - .filter { - guard let old = oldFlags[$0], let new = newFlags[$0] - else { return true } - return !(old.variation == new.variation && AnyComparer.isEqual(old.value, to: new.value)) + let oldKeys = Set(oldFlags.keys) + let newKeys = Set(newFlags.keys) + let newOrDeletedKeys = oldKeys.symmetricDifference(newKeys) + let updatedKeys = oldKeys.intersection(newKeys).filter { possibleUpdatedKey in + guard let old = oldFlags[possibleUpdatedKey], let new = newFlags[possibleUpdatedKey] + else { return true } + return old.variation != new.variation || old.value != new.value } + return newOrDeletedKeys.union(updatedKeys).sorted() } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift index ac9ed9e2..f613ba97 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -3,9 +3,9 @@ import Foundation protocol FlagMaintaining { var featureFlags: [LDFlagKey: FeatureFlag] { get } - func replaceStore(newFlags: [LDFlagKey: Any], completion: CompletionClosure?) - func updateStore(updateDictionary: [LDFlagKey: Any], completion: CompletionClosure?) - func deleteFlag(deleteDictionary: [LDFlagKey: Any], completion: CompletionClosure?) + func replaceStore(newFlags: FeatureFlagCollection) + func updateStore(updatedFlag: FeatureFlag) + func deleteFlag(deleteResponse: DeleteResponse) func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? } @@ -14,10 +14,6 @@ final class FlagStore: FlagMaintaining { fileprivate static let flagQueueLabel = "com.launchdarkly.flagStore.flagQueue" } - struct Keys { - static let flagKey = "key" - } - var featureFlags: [LDFlagKey: FeatureFlag] { flagQueue.sync { _featureFlags } } private var _featureFlags: [LDFlagKey: FeatureFlag] = [:] @@ -26,87 +22,44 @@ final class FlagStore: FlagMaintaining { init() { } - init(featureFlags: [LDFlagKey: FeatureFlag]?) { + init(featureFlags: [LDFlagKey: FeatureFlag]) { Log.debug(typeName(and: #function) + "featureFlags: \(String(describing: featureFlags))") - self._featureFlags = featureFlags ?? [:] - } - - convenience init(featureFlagDictionary: [LDFlagKey: Any]?) { - self.init(featureFlags: featureFlagDictionary?.flagCollection) + self._featureFlags = featureFlags } - /// Replaces all feature flags with new flags. Pass nil to reset to an empty flag store - func replaceStore(newFlags: [LDFlagKey: Any], completion: CompletionClosure?) { + func replaceStore(newFlags: FeatureFlagCollection) { Log.debug(typeName(and: #function) + "newFlags: \(String(describing: newFlags))") - flagQueue.async(flags: .barrier) { - self._featureFlags = newFlags.flagCollection ?? [:] - if let completion = completion { - DispatchQueue.main.async { - completion() - } - } + flagQueue.sync(flags: .barrier) { + self._featureFlags = newFlags.flags } } - // An update dictionary is the same as a flag dictionary. The version will be validated and if it's newer than the - // stored flag, the store will replace the flag with the updated flag. - func updateStore(updateDictionary: [LDFlagKey: Any], completion: CompletionClosure?) { - flagQueue.async(flags: .barrier) { - defer { - if let completion = completion { - DispatchQueue.main.async { - completion() - } - } - } - guard let flagKey = updateDictionary[Keys.flagKey] as? String, - let newFlag = FeatureFlag(dictionary: updateDictionary) - else { - Log.debug(self.typeName(and: #function) + "aborted. Malformed update dictionary. updateDictionary: \(String(describing: updateDictionary))") - return - } - guard self.isValidVersion(for: flagKey, newVersion: newFlag.version) + func updateStore(updatedFlag: FeatureFlag) { + flagQueue.sync(flags: .barrier) { + guard self.isValidVersion(for: updatedFlag.flagKey, newVersion: updatedFlag.version) else { - Log.debug(self.typeName(and: #function) + "aborted. Invalid version. updateDictionary: \(String(describing: updateDictionary)) " - + "existing flag: \(String(describing: self._featureFlags[flagKey]))") + Log.debug(self.typeName(and: #function) + "aborted. Invalid version. updateDictionary: \(updatedFlag) " + + "existing flag: \(String(describing: self._featureFlags[updatedFlag.flagKey]))") return } - Log.debug(self.typeName(and: #function) + "succeeded. new flag: \(newFlag), " + "prior flag: \(String(describing: self._featureFlags[flagKey]))") - self._featureFlags.updateValue(newFlag, forKey: flagKey) + Log.debug(self.typeName(and: #function) + "succeeded. new flag: \(updatedFlag), " + + "prior flag: \(String(describing: self._featureFlags[updatedFlag.flagKey]))") + self._featureFlags.updateValue(updatedFlag, forKey: updatedFlag.flagKey) } } - /* deleteDictionary should have the form: - { - "key": , - "version": - } - */ - func deleteFlag(deleteDictionary: [LDFlagKey: Any], completion: CompletionClosure?) { - flagQueue.async(flags: .barrier) { - defer { - if let completion = completion { - DispatchQueue.main.async { - completion() - } - } - } - guard let flagKey = deleteDictionary[Keys.flagKey] as? String, - let newVersion = deleteDictionary[FeatureFlag.CodingKeys.version.rawValue] as? Int - else { - Log.debug(self.typeName(and: #function) + "aborted. Malformed delete dictionary. deleteDictionary: \(String(describing: deleteDictionary))") - return - } - guard self.isValidVersion(for: flagKey, newVersion: newVersion) + func deleteFlag(deleteResponse: DeleteResponse) { + flagQueue.sync(flags: .barrier) { + guard self.isValidVersion(for: deleteResponse.key, newVersion: deleteResponse.version) else { - Log.debug(self.typeName(and: #function) + "aborted. Invalid version. deleteDictionary: \(String(describing: deleteDictionary)) " - + "existing flag: \(String(describing: self._featureFlags[flagKey]))") + Log.debug(self.typeName(and: #function) + "aborted. Invalid version. deleteResponse: \(deleteResponse) " + + "existing flag: \(String(describing: self._featureFlags[deleteResponse.key]))") return } - Log.debug(self.typeName(and: #function) + "deleted flag with key: " + flagKey) - self._featureFlags.removeValue(forKey: flagKey) + Log.debug(self.typeName(and: #function) + "deleted flag with key: " + deleteResponse.key) + self._featureFlags.removeValue(forKey: deleteResponse.key) } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift index 741358c2..6789c7c6 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift @@ -35,18 +35,21 @@ enum SynchronizingError: Error { } enum FlagSyncResult { - case success([String: Any], FlagUpdateType?) + case flagCollection(FeatureFlagCollection) + case patch(FeatureFlag) + case delete(DeleteResponse) case upToDate case error(SynchronizingError) } +struct DeleteResponse: Decodable { + let key: String + let version: Int? +} + typealias CompletionClosure = (() -> Void) typealias FlagSyncCompleteClosure = ((FlagSyncResult) -> Void) -enum FlagUpdateType: String { - case ping, put, patch, delete -} - class FlagSynchronizer: LDFlagSynchronizing, EventHandler { struct Constants { fileprivate static let queueName = "LaunchDarkly.FlagSynchronizer.syncQueue" @@ -76,8 +79,6 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { let pollingInterval: TimeInterval let useReport: Bool - var streamingActive: Bool { eventSource != nil } - var pollingActive: Bool { flagRequestTimer != nil } private var syncQueue = DispatchQueue(label: Constants.queueName, qos: .utility) private var eventSourceStarted: Date? @@ -99,10 +100,10 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { switch streamingMode { case .streaming: stopPolling() - startEventSource(isOnline: isOnline) + startEventSource() case .polling: stopEventSource() - startPolling(isOnline: isOnline) + startPolling() } } else { stopEventSource() @@ -112,24 +113,9 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { // MARK: Streaming - private func startEventSource(isOnline: Bool) { - guard isOnline, - streamingMode == .streaming, - !streamingActive - else { - var reason = "" - if !isOnline { - reason = "Flag Synchronizer is offline." - } - if reason.isEmpty && streamingMode != .streaming { - reason = "Flag synchronizer is not set for streaming." - } - if reason.isEmpty && streamingActive { - reason = "Clientstream already connected." - } - Log.debug(typeName(and: #function) + "aborted. " + reason) - return - } + private func startEventSource() { + guard eventSource == nil + else { return Log.debug(typeName(and: #function) + "aborted. Clientstream already connected.") } Log.debug(typeName(and: #function)) eventSourceStarted = Date() @@ -141,10 +127,9 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { } private func stopEventSource() { - guard streamingActive else { - Log.debug(typeName(and: #function) + "aborted. Clientstream is not connected.") - return - } + guard eventSource != nil + else { return Log.debug(typeName(and: #function) + "aborted. Clientstream is not connected.") } + Log.debug(typeName(and: #function)) eventSource?.stop() eventSource = nil @@ -152,32 +137,19 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { // MARK: Polling - private func startPolling(isOnline: Bool) { - guard isOnline, - streamingMode == .polling, - !pollingActive - else { - var reason = "" - if !isOnline { reason = "Flag Synchronizer is offline." } - if reason.isEmpty && streamingMode != .polling { - reason = "Flag synchronizer is not set for polling." - } - if reason.isEmpty && pollingActive { - reason = "Polling already active." - } - Log.debug(typeName(and: #function) + "aborted. " + reason) - return - } + private func startPolling() { + guard flagRequestTimer == nil + else { return Log.debug(typeName(and: #function) + "aborted. Polling already active.") } + Log.debug(typeName(and: #function)) flagRequestTimer = LDTimer(withTimeInterval: pollingInterval, fireQueue: syncQueue, execute: processTimer) - makeFlagRequest(isOnline: isOnline) + makeFlagRequest(isOnline: true) } private func stopPolling() { - guard pollingActive else { - Log.debug(typeName(and: #function) + "aborted. Polling already inactive.") - return - } + guard flagRequestTimer != nil + else { return Log.debug(typeName(and: #function) + "aborted. Polling already inactive.") } + Log.debug(typeName(and: #function)) flagRequestTimer?.cancel() flagRequestTimer = nil @@ -234,17 +206,12 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { return } guard let data = serviceResponse.data, - let flags = try? JSONSerialization.jsonDictionary(with: data, options: .allowFragments) + let flagCollection = try? JSONDecoder().decode(FeatureFlagCollection.self, from: data) else { reportDataError(serviceResponse.data) return } - reportSuccess(flagDictionary: flags, eventType: streamingActive ? .ping : nil) - } - - private func reportSuccess(flagDictionary: [String: Any], eventType: FlagUpdateType?) { - Log.debug(typeName(and: #function) + "flagDictionary: \(flagDictionary)" + (eventType == nil ? "" : ", eventType: \(String(describing: eventType))")) - reportSyncComplete(.success(flagDictionary, streamingActive ? eventType : nil)) + reportSyncComplete(.flagCollection(flagCollection)) } private func reportDataError(_ data: Data?) { @@ -301,7 +268,7 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { reportSyncComplete(.error(.streamEventWhilePolling)) return true } - if !streamingActive { + if eventSource == nil { // Since eventSource.close() is async, this prevents responding to events after .close() is called, but before it's actually closed Log.debug(typeName(and: #function) + "aborted. " + "Clientstream is not active.") reportSyncComplete(.error(.isOffline)) @@ -329,18 +296,33 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { guard !shouldAbortStreamUpdate() else { return } - let updateType: FlagUpdateType? = FlagUpdateType(rawValue: eventType) - switch updateType { - case .ping: makeFlagRequest(isOnline: isOnline) - case .put, .patch, .delete: + switch eventType { + case "ping": makeFlagRequest(isOnline: isOnline) + case "put": + guard let data = messageEvent.data.data(using: .utf8), + let flagCollection = try? JSONDecoder().decode(FeatureFlagCollection.self, from: data) + else { + reportDataError(messageEvent.data.data(using: .utf8)) + return + } + reportSyncComplete(.flagCollection(flagCollection)) + case "patch": + guard let data = messageEvent.data.data(using: .utf8), + let flag = try? JSONDecoder().decode(FeatureFlag.self, from: data) + else { + reportDataError(messageEvent.data.data(using: .utf8)) + return + } + reportSyncComplete(.patch(flag)) + case "delete": guard let data = messageEvent.data.data(using: .utf8), - let flagDictionary = try? JSONSerialization.jsonDictionary(with: data) + let deleteResponse = try? JSONDecoder().decode(DeleteResponse.self, from: data) else { reportDataError(messageEvent.data.data(using: .utf8)) return } - reportSuccess(flagDictionary: flagDictionary, eventType: updateType) - case nil: + reportSyncComplete(.delete(deleteResponse)) + default: Log.debug(typeName(and: #function) + "aborted. Unknown event type.") reportSyncComplete(.error(.unknownEventType(eventType))) return @@ -372,21 +354,9 @@ extension FlagSynchronizer { makeFlagRequest(isOnline: isOnline) } - func testStreamOnOpened() { - onOpened() - } - - func testStreamOnClosed() { - onClosed() - } - func testStreamOnMessage(event: String, messageEvent: MessageEvent) { onMessage(eventType: event, messageEvent: messageEvent) } - - func testStreamOnError(error: Error) { - onError(error: error) - } } #endif diff --git a/LaunchDarkly/LaunchDarkly/Util.swift b/LaunchDarkly/LaunchDarkly/Util.swift new file mode 100644 index 00000000..7ecf2a2b --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Util.swift @@ -0,0 +1,13 @@ +import CommonCrypto +import Foundation + +class Util { + class func sha256base64(_ str: String) -> String { + let data = Data(str.utf8) + var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &digest) + } + return Data(digest).base64EncodedString() + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift b/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift deleted file mode 100644 index c53ed123..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/DictionarySpec.swift +++ /dev/null @@ -1,171 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DictionarySpec: QuickSpec { - public override func spec() { - symmetricDifferenceSpec() - withNullValuesRemovedSpec() - } - - private func symmetricDifferenceSpec() { - describe("symmetric difference") { - var dictionary: [String: Any]! - var otherDictionary: [String: Any]! - beforeEach { - dictionary = [String: Any].stub() - otherDictionary = [String: Any].stub() - } - context("when dictionaries are equal") { - it("returns an empty array") { - expect(dictionary.symmetricDifference(otherDictionary)) == [] - } - } - context("when other is empty") { - it("returns all keys in subject") { - otherDictionary = [:] - expect(dictionary.symmetricDifference(otherDictionary)) == dictionary.keys.sorted() - } - } - context("when subject is empty") { - it("returns all keys in other") { - dictionary = [:] - expect(dictionary.symmetricDifference(otherDictionary)) == otherDictionary.keys.sorted() - } - } - context("when subject has an added key") { - it("returns the different key") { - let addedKey = "addedKey" - dictionary[addedKey] = true - expect(dictionary.symmetricDifference(otherDictionary)) == [addedKey] - } - } - context("when other has an added key") { - it("returns the different key") { - let addedKey = "addedKey" - otherDictionary[addedKey] = true - expect(dictionary.symmetricDifference(otherDictionary)) == [addedKey] - } - } - context("when other has a different key") { - it("returns the different keys") { - let addedKeyA = "addedKeyA" - let addedKeyB = "addedKeyB" - otherDictionary[addedKeyA] = true - dictionary[addedKeyB] = true - expect(dictionary.symmetricDifference(otherDictionary)) == [addedKeyA, addedKeyB] - } - } - context("when other has a different bool value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.bool - otherDictionary[differingKey] = !DarklyServiceMock.FlagValues.bool - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different int value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.int - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.int + 1 - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different double value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.double - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.double - 1.0 - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different string value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.string - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.string + " some new text" - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different array value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.array - otherDictionary[differingKey] = DarklyServiceMock.FlagValues.array + [4] - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - context("when other has a different dictionary value") { - it("returns the different key") { - let differingKey = DarklyServiceMock.FlagKeys.dictionary - var differingDictionary = DarklyServiceMock.FlagValues.dictionary - differingDictionary["sub-flag-a"] = !(differingDictionary["sub-flag-a"] as! Bool) - otherDictionary[differingKey] = differingDictionary - expect(dictionary.symmetricDifference(otherDictionary)) == [differingKey] - } - } - } - } - - private func withNullValuesRemovedSpec() { - describe("withNullValuesRemoved") { - it("when no null values exist") { - let dictionary = Dictionary.stub() - let resultingDictionary = dictionary.withNullValuesRemoved - expect(dictionary.keys) == resultingDictionary.keys - } - context("when null values exist") { - it("in the top level") { - var dictionary = Dictionary.stub() - dictionary["null-key"] = NSNull() - let resultingDictionary = dictionary.withNullValuesRemoved - expect(resultingDictionary.keys) == Dictionary.stub().keys - } - it("in the second level") { - var dictionary = Dictionary.stub() - var subDict = Dictionary.Values.dictionary - subDict["null-key"] = NSNull() - dictionary[Dictionary.Keys.dictionary] = subDict - let resultingDictionary = dictionary.withNullValuesRemoved - expect((resultingDictionary[Dictionary.Keys.dictionary] as! [String: Any]).keys) == Dictionary.Values.dictionary.keys - } - } - } - } -} - -fileprivate extension Dictionary where Key == String, Value == Any { - struct Keys { - static let bool: String = "bool-key" - static let int: String = "int-key" - static let double: String = "double-key" - static let string: String = "string-key" - static let array: String = "array-key" - static let dictionary: String = "dictionary-key" - static let null: String = "null-key" - } - - struct Values { - static let bool: Bool = true - static let int: Int = 7 - static let double: Double = 3.14159 - static let string: String = "string value" - static let array: [Int] = [1, 2, 3] - static let dictionary: [String: Any] = ["sub-flag-a": false, "sub-flag-b": 3, "sub-flag-c": 2.71828] - static let null: NSNull = NSNull() - } - - static func stub() -> [String: Any] { - [Keys.bool: Values.bool, - Keys.int: Values.int, - Keys.double: Values.double, - Keys.string: Values.string, - Keys.array: Values.array, - Keys.dictionary: Values.dictionary] - } -} - -extension Dictionary where Key == String, Value == Any { - func appendNull() -> [String: Any] { - var dictWithNull = self - dictWithNull[Keys.null] = Values.null - return dictWithNull - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 5c41b400..5cdcf111 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -9,9 +9,6 @@ final class LDClientSpec: QuickSpec { fileprivate static let alternateMockUrl = URL(string: "https://dummy.alternate.com")! fileprivate static let alternateMockMobileKey = "alternateMockMobileKey" - fileprivate static let newFlagKey = "LDClientSpec.newFlagKey" - fileprivate static let newFlagValue = "LDClientSpec.newFlagValue" - fileprivate static let updateThreshold: TimeInterval = 0.05 } @@ -20,8 +17,8 @@ final class LDClientSpec: QuickSpec { static let int = 5 static let double = 2.71828 static let string = "default string value" - static let array = [-1, -2] - static let dictionary: [String: Any] = ["sub-flag-x": true, "sub-flag-y": 1, "sub-flag-z": 42.42] + static let array: LDValue = [-1, -2] + static let dictionary: LDValue = ["sub-flag-x": true, "sub-flag-y": 1, "sub-flag-z": 42.42] } class TestContext { @@ -36,9 +33,6 @@ final class LDClientSpec: QuickSpec { var featureFlagCachingMock: FeatureFlagCachingMock! { subject.flagCache as? FeatureFlagCachingMock } - var cacheConvertingMock: CacheConvertingMock! { - subject.cacheConverter as? CacheConvertingMock - } var flagStoreMock: FlagMaintainingMock! { subject.flagStore as? FlagMaintainingMock } @@ -87,10 +81,13 @@ final class LDClientSpec: QuickSpec { } serviceFactoryMock.makeFlagChangeNotifierReturnValue = FlagChangeNotifier() - let flagCache = serviceFactoryMock.makeFeatureFlagCacheReturnValue - flagCache.retrieveFeatureFlagsCallback = { - let received = flagCache.retrieveFeatureFlagsReceivedArguments! - flagCache.retrieveFeatureFlagsReturnValue = self.cachedFlags[received.mobileKey]?[received.userKey] + serviceFactoryMock.makeFeatureFlagCacheCallback = { + let mobileKey = self.serviceFactoryMock.makeFeatureFlagCacheReceivedParameters!.mobileKey + let mockCache = FeatureFlagCachingMock() + mockCache.retrieveFeatureFlagsCallback = { + mockCache.retrieveFeatureFlagsReturnValue = self.cachedFlags[mobileKey]?[mockCache.retrieveFeatureFlagsReceivedUserKey!] + } + self.serviceFactoryMock.makeFeatureFlagCacheReturnValue = mockCache } config = newConfig ?? LDConfig.stub(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: serviceFactoryMock.makeEnvironmentReporterReturnValue) @@ -236,17 +233,16 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } it("starts in foreground") { expect(testContext.subject.runMode) == .foreground @@ -280,17 +276,16 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } it("starts in foreground") { expect(testContext.subject.runMode) == .foreground @@ -323,17 +318,16 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 // called on init and subsequent identify - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 2 // both start and internalIdentify expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 // Both start and internalIdentify - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } context("without setting user") { @@ -357,47 +351,45 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.subject.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.subject.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.subject.user } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.subject.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } } it("when called with cached flags for the user and environment") { - let testContext = TestContext().withCached(flags: FlagMaintainingMock.stubFlags()) + let cachedFlags = ["test-flag": FeatureFlag(flagKey: "test-flag")] + let testContext = TestContext().withCached(flags: cachedFlags) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags.flagCollection) == FlagMaintainingMock.stubFlags() + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == cachedFlags - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } it("when called without cached flags for the user") { let testContext = TestContext() withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key expect(testContext.flagStoreMock.replaceStoreCallCount) == 0 - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } @@ -467,30 +459,26 @@ final class LDClientSpec: QuickSpec { // Test that already timed out completion is not called when sync completes completed = false - testContext.onSyncComplete?(.success([:], nil)) - testContext.onSyncComplete?(.success([:], .ping)) - testContext.onSyncComplete?(.success([:], .put)) + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection([:]))) Thread.sleep(forTimeInterval: 1.0) expect(completed) == false } } } } - for eventType in [nil, FlagUpdateType.ping, FlagUpdateType.put] { - context("after receiving flags as " + (eventType?.rawValue ?? "poll")) { - it("does complete without timeout") { - testContext.start(completion: startCompletion) - testContext.onSyncComplete?(.success([:], eventType)) - expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) - } - it("does complete with timeout") { - waitUntil(timeout: .seconds(3)) { done in - testContext.start(timeOut: 5.0, timeOutCompletion: startTimeoutCompletion(done)) - testContext.onSyncComplete?(.success([:], eventType)) - } - expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) - expect(didTimeOut) == false + context("after receiving flags") { + it("does complete without timeout") { + testContext.start(completion: startCompletion) + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection([:]))) + expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + } + it("does complete with timeout") { + waitUntil(timeout: .seconds(3)) { done in + testContext.start(timeOut: 5.0, timeOutCompletion: startTimeoutCompletion(done)) + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection([:]))) } + expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + expect(didTimeOut) == false } } } @@ -513,7 +501,6 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline } it("saves the config") { - expect(testContext.subject.config) == testContext.config expect(testContext.subject.service.config) == testContext.config expect(testContext.makeFlagSynchronizerStreamingMode) == os.backgroundStreamingMode expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) @@ -530,18 +517,12 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } } } } @@ -559,7 +540,6 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.eventReporter.isOnline) == true } it("saves the config") { - expect(testContext.subject.config) == testContext.config expect(testContext.subject.service.config) == testContext.config expect(testContext.makeFlagSynchronizerStreamingMode) == LDStreamingMode.polling expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) @@ -576,18 +556,12 @@ final class LDClientSpec: QuickSpec { } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } } } } @@ -600,8 +574,7 @@ final class LDClientSpec: QuickSpec { let testContext = TestContext(startOnline: true) testContext.start() testContext.featureFlagCachingMock.reset() - testContext.cacheConvertingMock.reset() - + let newUser = LDUser.stub() testContext.subject.internalIdentify(newUser: newUser) @@ -615,20 +588,14 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.flagSynchronizer.isOnline) == true expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == newUser.key expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) - - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } it("when the client is offline") { let testContext = TestContext() testContext.start() testContext.featureFlagCachingMock.reset() - testContext.cacheConvertingMock.reset() let newUser = LDUser.stub() testContext.subject.internalIdentify(newUser: newUser) @@ -643,14 +610,9 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.flagSynchronizer.isOnline) == false expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == newUser.key expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) - - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } it("when the new user has cached feature flags") { let stubFlags = FlagMaintainingMock.stubFlags() @@ -658,17 +620,12 @@ final class LDClientSpec: QuickSpec { let testContext = TestContext().withCached(userKey: newUser.key, flags: stubFlags) testContext.start() testContext.featureFlagCachingMock.reset() - testContext.cacheConvertingMock.reset() testContext.subject.internalIdentify(newUser: newUser) expect(testContext.subject.user) == newUser expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags.flagCollection) == stubFlags - - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == stubFlags } } } @@ -807,22 +764,19 @@ final class LDClientSpec: QuickSpec { } context("flag store contains the requested value") { beforeEach { - waitUntil { done in - testContext.flagStoreMock.replaceStore(newFlags: FlagMaintainingMock.stubFlags(), completion: done) - } + testContext.flagStoreMock.replaceStore(newFlags: FeatureFlagCollection(FlagMaintainingMock.stubFlags())) } context("non-Optional default value") { it("returns the flag value") { - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DarklyServiceMock.FlagValues.bool - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DarklyServiceMock.FlagValues.int - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DarklyServiceMock.FlagValues.double - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DarklyServiceMock.FlagValues.string - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array) == DarklyServiceMock.FlagValues.array).to(beTrue()) - expect(AnyComparer.isEqual(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary), - to: DarklyServiceMock.FlagValues.dictionary)).to(beTrue()) + expect(testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DarklyServiceMock.FlagValues.bool + expect(testContext.subject.intVariation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DarklyServiceMock.FlagValues.int + expect(testContext.subject.doubleVariation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DarklyServiceMock.FlagValues.double + expect(testContext.subject.stringVariation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DarklyServiceMock.FlagValues.string + expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array)) == LDValue.fromAny(DarklyServiceMock.FlagValues.array) + expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary)) == LDValue.fromAny(DarklyServiceMock.FlagValues.dictionary) } it("records a flag evaluation event") { - _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) + _ = testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == LDValue.fromAny(DarklyServiceMock.FlagValues.bool) @@ -835,16 +789,15 @@ final class LDClientSpec: QuickSpec { context("flag store does not contain the requested value") { context("non-Optional default value") { it("returns the default value") { - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DefaultFlagValues.bool - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DefaultFlagValues.int - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DefaultFlagValues.double - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DefaultFlagValues.string - expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array) == DefaultFlagValues.array).to(beTrue()) - expect(AnyComparer.isEqual(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary), - to: DefaultFlagValues.dictionary)).to(beTrue()) + expect(testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool)) == DefaultFlagValues.bool + expect(testContext.subject.intVariation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int)) == DefaultFlagValues.int + expect(testContext.subject.doubleVariation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double)) == DefaultFlagValues.double + expect(testContext.subject.stringVariation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string)) == DefaultFlagValues.string + expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array)) == DefaultFlagValues.array + expect(testContext.subject.jsonVariation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary)) == DefaultFlagValues.dictionary } it("records a flag evaluation event") { - _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) + _ = testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == LDValue.fromAny(DefaultFlagValues.bool) @@ -930,14 +883,8 @@ final class LDClientSpec: QuickSpec { } private func onSyncCompleteSuccessSpec() { - it("polling") { - self.onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .polling) - } - it("streaming ping") { - self.onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .streaming, eventType: .ping) - } - it("streaming put") { - self.onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: .streaming, eventType: .put) + it("flag collection") { + self.onSyncCompleteSuccessReplacingFlagsSpec() } it("streaming patch") { self.onSyncCompleteStreamingPatchSpec() @@ -947,34 +894,30 @@ final class LDClientSpec: QuickSpec { } } - private func onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: LDStreamingMode, eventType: FlagUpdateType? = nil) { + private func onSyncCompleteSuccessReplacingFlagsSpec() { let testContext = TestContext(startOnline: true) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - var newFlags = FlagMaintainingMock.stubFlags() - newFlags[Constants.newFlagKey] = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.string, useAlternateValue: true) - + let newFlags = ["flag1": FeatureFlag(flagKey: "flag1")] var updateDate: Date! waitUntil { done in testContext.changeNotifierMock.notifyObserversCallback = done updateDate = Date() - testContext.onSyncComplete?(.success(newFlags, eventType)) + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection(newFlags))) } expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(AnyComparer.isEqual(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags, to: newFlags)).to(beTrue()) + expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags?.flags) == newFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags - expect(AnyComparer.isEqual(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags, to: testContext.cachedFlags)).to(beTrue()) + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags) == [:] } func onSyncCompleteStreamingPatchSpec() { @@ -982,27 +925,22 @@ final class LDClientSpec: QuickSpec { let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - let flagUpdateDictionary = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, - value: DarklyServiceMock.FlagValues.int + 1, - variation: DarklyServiceMock.Constants.variation + 1, - version: DarklyServiceMock.Constants.version + 1) + let updateFlag = FeatureFlag(flagKey: "abc") var updateDate: Date! waitUntil { done in testContext.changeNotifierMock.notifyObserversCallback = done updateDate = Date() - testContext.onSyncComplete?(.success(flagUpdateDictionary, .patch)) + testContext.onSyncComplete?(.patch(updateFlag)) } expect(testContext.flagStoreMock.updateStoreCallCount) == 1 - expect(AnyComparer.isEqual(testContext.flagStoreMock.updateStoreReceivedArguments?.updateDictionary, to: flagUpdateDictionary)).to(beTrue()) + expect(testContext.flagStoreMock.updateStoreReceivedUpdatedFlag) == updateFlag expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags @@ -1014,24 +952,22 @@ final class LDClientSpec: QuickSpec { let testContext = TestContext(startOnline: true).withCached(flags: stubFlags) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - let flagUpdateDictionary = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) + let deleteResponse = DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) var updateDate: Date! waitUntil { done in testContext.changeNotifierMock.notifyObserversCallback = done updateDate = Date() - testContext.onSyncComplete?(.success(flagUpdateDictionary, .delete)) + testContext.onSyncComplete?(.delete(deleteResponse)) } expect(testContext.flagStoreMock.deleteFlagCallCount) == 1 - expect(AnyComparer.isEqual(testContext.flagStoreMock.deleteFlagReceivedArguments?.deleteDictionary, to: flagUpdateDictionary)).to(beTrue()) + expect(testContext.flagStoreMock.deleteFlagReceivedDeleteResponse) == deleteResponse expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.newFlags) == testContext.flagStoreMock.featureFlags @@ -1365,7 +1301,7 @@ final class LDClientSpec: QuickSpec { it("returns all non-null flag values from store") { let testContext = TestContext().withCached(flags: stubFlags) testContext.start() - expect(AnyComparer.isEqual(testContext.subject.allFlags, to: stubFlags.compactMapValues { $0.value })).to(beTrue()) + expect(testContext.subject.allFlags) == stubFlags.compactMapValues { $0.value } } it("returns nil when client is closed") { let testContext = TestContext().withCached(flags: stubFlags) @@ -1403,8 +1339,8 @@ final class LDClientSpec: QuickSpec { it("when flag doesn't exist") { let testContext = TestContext() testContext.start() - let detail = testContext.subject.variationDetail(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool).reason - if let errorKind = detail?["errorKind"] as? String { + let detail = testContext.subject.boolVariationDetail(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool).reason + if let errorKind = detail?["errorKind"] { expect(errorKind) == "FLAG_NOT_FOUND" } } @@ -1431,17 +1367,15 @@ final class LDClientSpec: QuickSpec { testContext.subject.close() expect(testContext.subject.isInitialized) == false } - for eventType in [nil, FlagUpdateType.ping, FlagUpdateType.put] { - it("when client was started and after receiving flags as " + (eventType?.rawValue ?? "poll")) { - let testContext = TestContext(startOnline: true) - testContext.start() - testContext.onSyncComplete?(.success([:], eventType)) + it("when client was started and after receiving flags") { + let testContext = TestContext(startOnline: true) + testContext.start() + testContext.onSyncComplete?(.flagCollection(FeatureFlagCollection([:]))) - expect(testContext.subject.isInitialized).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + expect(testContext.subject.isInitialized).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) - testContext.subject.close() - expect(testContext.subject.isInitialized) == false - } + testContext.subject.close() + expect(testContext.subject.isInitialized) == false } } } @@ -1450,7 +1384,7 @@ final class LDClientSpec: QuickSpec { extension FeatureFlagCachingMock { func reset() { retrieveFeatureFlagsCallCount = 0 - retrieveFeatureFlagsReceivedArguments = nil + retrieveFeatureFlagsReceivedUserKey = nil retrieveFeatureFlagsReturnValue = nil storeFeatureFlagsCallCount = 0 storeFeatureFlagsReceivedArguments = nil @@ -1464,10 +1398,3 @@ extension OperatingSystem { } private class ErrorMock: Error { } - -extension CacheConvertingMock { - func reset() { - convertCacheDataCallCount = 0 - convertCacheDataReceivedArguments = nil - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index 405efe0d..e41a117a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -3,35 +3,29 @@ import LDSwiftEventSource @testable import LaunchDarkly final class ClientServiceMockFactory: ClientServiceCreating { - func makeKeyedValueCache() -> KeyedValueCaching { - KeyedValueCachingMock() + var makeKeyedValueCacheReturnValue = KeyedValueCachingMock() + var makeKeyedValueCacheCallCount = 0 + var makeKeyedValueCacheReceivedCacheKey: String? = nil + func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching { + makeKeyedValueCacheCallCount += 1 + makeKeyedValueCacheReceivedCacheKey = cacheKey + return makeKeyedValueCacheReturnValue } var makeFeatureFlagCacheReturnValue = FeatureFlagCachingMock() + var makeFeatureFlagCacheCallback: (() -> Void)? var makeFeatureFlagCacheCallCount = 0 - func makeFeatureFlagCache(maxCachedUsers: Int = 5) -> FeatureFlagCaching { + var makeFeatureFlagCacheReceivedParameters: (mobileKey: MobileKey, maxCachedUsers: Int)? = nil + func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedUsers: Int = 5) -> FeatureFlagCaching { makeFeatureFlagCacheCallCount += 1 + makeFeatureFlagCacheReceivedParameters = (mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) + makeFeatureFlagCacheCallback?() return makeFeatureFlagCacheReturnValue } - func makeCacheConverter(maxCachedUsers: Int = 5) -> CacheConverting { - CacheConvertingMock() - } - - var makeDeprecatedCacheModelReturnValue: DeprecatedCacheMock? - var makeDeprecatedCacheModelReturnedValues = [DeprecatedCacheModel: DeprecatedCacheMock]() - var makeDeprecatedCacheModelCallCount = 0 - var makeDeprecatedCacheModelReceivedModels = [DeprecatedCacheModel]() - func makeDeprecatedCacheModel(_ model: DeprecatedCacheModel) -> DeprecatedCache { - makeDeprecatedCacheModelCallCount += 1 - makeDeprecatedCacheModelReceivedModels.append(model) - var returnedCacheMock = makeDeprecatedCacheModelReturnValue - if returnedCacheMock == nil { - returnedCacheMock = DeprecatedCacheMock() - returnedCacheMock?.model = model - } - makeDeprecatedCacheModelReturnedValues[model] = returnedCacheMock! - return returnedCacheMock! + var makeCacheConverterReturnValue = CacheConvertingMock() + func makeCacheConverter() -> CacheConverting { + return makeCacheConverterReturnValue } func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider { diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 38d5ebb0..1c320d72 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -17,12 +17,9 @@ final class DarklyServiceMock: DarklyServiceProvider { static let null = "null-flag" static let unknown = "unknown-flag" - static var knownFlags: [LDFlagKey] { // known means the SDK has the feature flag value + static var knownFlags: [LDFlagKey] { [bool, int, double, string, array, dictionary, null] } - static var flagsWithAnAlternateValue: [LDFlagKey] { - [bool, int, double, string, array, dictionary] - } } struct FlagValues { @@ -46,26 +43,6 @@ final class DarklyServiceMock: DarklyServiceProvider { default: return nil } } - - static func alternateValue(from flagKey: LDFlagKey) -> Any? { - alternate(value(from: flagKey)) - } - - static func alternate(_ value: T) -> T { - switch value { - case let value as Bool: return !value as! T - case let value as Int: return value + 1 as! T - case let value as Double: return value + 1.0 as! T - case let value as String: return value + "-alternate" as! T - case var value as [Any]: - value.append(4) - return value as! T // Not sure why, but this crashes if you combine append the value into the return - case var value as [String: Any]: - value["new-flag"] = "new-value" - return value as! T - default: return value - } - } } struct Constants { @@ -80,101 +57,39 @@ final class DarklyServiceMock: DarklyServiceProvider { static let mockEventsUrl = URL(string: "https://dummy.events.com")! static let mockStreamUrl = URL(string: "https://dummy.stream.com")! - static let stubNameFlag = "Flag Request Stub" - static let stubNameStream = "Stream Connect Stub" - static let stubNameEvent = "Event Report Stub" - static let stubNameDiagnostic = "Diagnostic Report Stub" - static let variation = 2 static let version = 4 static let flagVersion = 3 static let trackEvents = true static let debugEventsUntilDate = Date().addingTimeInterval(30.0) - static let reason = Optional(["kind": "OFF"]) + static let reason: [String: LDValue] = ["kind": "OFF"] - static func stubFeatureFlags(includeNullValue: Bool = true, - includeVariations: Bool = true, - includeVersions: Bool = true, - includeFlagVersions: Bool = true, - alternateVariationNumber: Bool = true, - bumpFlagVersions: Bool = false, - alternateValuesForKeys alternateValueKeys: [LDFlagKey] = [], - trackEvents: Bool = true, - debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0)) -> [LDFlagKey: FeatureFlag] { - - let flagKeys = includeNullValue ? FlagKeys.knownFlags : FlagKeys.flagsWithAnAlternateValue + static func stubFeatureFlags(debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0)) -> [LDFlagKey: FeatureFlag] { + let flagKeys = FlagKeys.knownFlags let featureFlagTuples = flagKeys.map { flagKey in - (flagKey, stubFeatureFlag(for: flagKey, - includeVariation: includeVariations, - includeVersion: includeVersions, - includeFlagVersion: includeFlagVersions, - useAlternateValue: useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), - useAlternateVersion: bumpFlagVersions && useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), - useAlternateFlagVersion: bumpFlagVersions && useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), - useAlternateVariationNumber: alternateVariationNumber, - trackEvents: trackEvents, - debugEventsUntilDate: debugEventsUntilDate)) + (flagKey, stubFeatureFlag(for: flagKey, debugEventsUntilDate: debugEventsUntilDate)) } return Dictionary(uniqueKeysWithValues: featureFlagTuples) } - private static func useAlternateValue(for flagKey: LDFlagKey, alternateValueKeys: [LDFlagKey]) -> Bool { - alternateValueKeys.contains(flagKey) - } - - private static func value(for flagKey: LDFlagKey, useAlternateValue: Bool) -> Any? { - useAlternateValue ? FlagValues.alternateValue(from: flagKey) : FlagValues.value(from: flagKey) - } - - private static func variation(for flagKey: LDFlagKey, includeVariation: Bool, useAlternateValue: Bool) -> Int? { - guard includeVariation - else { return nil } - return useAlternateValue ? variation + 1 : variation - } - - private static func variation(for flagKey: LDFlagKey, includeVariation: Bool) -> Int? { - guard includeVariation - else { return nil } - return variation - } - - private static func version(for flagKey: LDFlagKey, includeVersion: Bool, useAlternateVersion: Bool) -> Int? { - guard includeVersion - else { return nil } + private static func version(for flagKey: LDFlagKey, useAlternateVersion: Bool) -> Int? { return useAlternateVersion ? version + 1 : version } - private static func flagVersion(for flagKey: LDFlagKey, includeFlagVersion: Bool, useAlternateFlagVersion: Bool) -> Int? { - guard includeFlagVersion - else { return nil } - return useAlternateFlagVersion ? flagVersion + 1 : flagVersion - } - private static func reason(includeEvaluationReason: Bool) -> [String: Any]? { - includeEvaluationReason ? reason : nil - } - static func stubFeatureFlag(for flagKey: LDFlagKey, - includeVariation: Bool = true, - includeVersion: Bool = true, - includeFlagVersion: Bool = true, - useAlternateValue: Bool = false, useAlternateVersion: Bool = false, - useAlternateFlagVersion: Bool = false, - useAlternateVariationNumber: Bool = true, trackEvents: Bool = true, - debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0), - includeEvaluationReason: Bool = false, - includeTrackReason: Bool = false) -> FeatureFlag { + debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0)) -> FeatureFlag { FeatureFlag(flagKey: flagKey, - value: value(for: flagKey, useAlternateValue: useAlternateValue), - variation: useAlternateVariationNumber ? variation(for: flagKey, includeVariation: includeVariation, useAlternateValue: useAlternateValue) : variation(for: flagKey, includeVariation: includeVariation), - version: version(for: flagKey, includeVersion: includeVersion, useAlternateVersion: useAlternateValue || useAlternateVersion), - flagVersion: flagVersion(for: flagKey, includeFlagVersion: includeFlagVersion, useAlternateFlagVersion: useAlternateValue || useAlternateFlagVersion), - trackEvents: trackEvents, - debugEventsUntilDate: debugEventsUntilDate, - reason: reason(includeEvaluationReason: includeEvaluationReason), - trackReason: includeTrackReason) + value: LDValue.fromAny(FlagValues.value(from: flagKey)), + variation: variation, + version: version(for: flagKey, useAlternateVersion: useAlternateVersion), + flagVersion: flagVersion, + trackEvents: trackEvents, + debugEventsUntilDate: debugEventsUntilDate, + reason: nil, + trackReason: false) } } @@ -263,7 +178,7 @@ extension DarklyServiceMock { flagResponseEtag: String? = nil, onActivation activate: ((URLRequest) -> Void)? = nil) { let stubbedFeatureFlags = featureFlags ?? Constants.stubFeatureFlags() - let responseData = statusCode == HTTPURLResponse.StatusCodes.ok ? stubbedFeatureFlags.dictionaryValue.jsonData! : Data() + let responseData = statusCode == HTTPURLResponse.StatusCodes.ok ? try! JSONEncoder().encode(stubbedFeatureFlags) : Data() let stubResponse: HTTPStubsResponseBlock = { _ in var headers: [String: String] = [:] if let flagResponseEtag = flagResponseEtag { @@ -283,8 +198,7 @@ extension DarklyServiceMock { func stubFlagResponse(statusCode: Int, badData: Bool = false, responseOnly: Bool = false, errorOnly: Bool = false, responseDate: Date? = nil) { let response = HTTPURLResponse(url: config.baseUrl, statusCode: statusCode, httpVersion: Constants.httpVersion, headerFields: HTTPURLResponse.dateHeader(from: responseDate)) if statusCode == HTTPURLResponse.StatusCodes.ok { - let flagData = try? JSONSerialization.data(withJSONObject: Constants.stubFeatureFlags(includeNullValue: false).dictionaryValue, - options: []) + let flagData = try? JSONEncoder().encode(Constants.stubFeatureFlags()) stubbedFlagResponse = (flagData, response, nil) if badData { stubbedFlagResponse = (Constants.errorData, response, nil) @@ -302,7 +216,7 @@ extension DarklyServiceMock { } func flagStubName(statusCode: Int, useReport: Bool) -> String { - "\(Constants.stubNameFlag) using method \(useReport ? URLRequest.HTTPMethods.report : URLRequest.HTTPMethods.get) with response status code \(statusCode)" + "Flag request stub using method \(useReport ? URLRequest.HTTPMethods.report : URLRequest.HTTPMethods.get) with response status code \(statusCode)" } // MARK: Publish Event @@ -321,7 +235,7 @@ extension DarklyServiceMock { } : { _ in HTTPStubsResponse(error: Constants.error) } - stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: Constants.stubNameEvent) { request, _, _ in + stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: "Event report stub") { request, _, _ in activate?(request) } } @@ -358,7 +272,7 @@ extension DarklyServiceMock { } : { _ in HTTPStubsResponse(error: Constants.error) } - stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: Constants.stubNameDiagnostic, onActivation: activate) + stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: "Diagnostic report stub", onActivation: activate) } // MARK: Stub diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift deleted file mode 100644 index 80934705..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DeprecatedCacheMock.swift +++ /dev/null @@ -1,71 +0,0 @@ -import Foundation -@testable import LaunchDarkly - -// MARK: - DeprecatedCacheMock -final class DeprecatedCacheMock: DeprecatedCache { - - // MARK: model - var modelSetCount = 0 - var setModelCallback: (() -> Void)? - // This may need to be updated when new cache versions are introduced - var model: DeprecatedCacheModel = .version5 { - didSet { - modelSetCount += 1 - setModelCallback?() - } - } - - // MARK: cachedDataKey - var cachedDataKeySetCount = 0 - var setCachedDataKeyCallback: (() -> Void)? - var cachedDataKey: String = CacheConverter.CacheKeys.cachedDataKeyStub { - didSet { - cachedDataKeySetCount += 1 - setCachedDataKeyCallback?() - } - } - - // MARK: keyedValueCache - var keyedValueCacheSetCount = 0 - var setKeyedValueCacheCallback: (() -> Void)? - var keyedValueCache: KeyedValueCaching = KeyedValueCachingMock() { - didSet { - keyedValueCacheSetCount += 1 - setKeyedValueCacheCallback?() - } - } - - // MARK: retrieveFlags - var retrieveFlagsCallCount = 0 - var retrieveFlagsCallback: (() -> Void)? - var retrieveFlagsReceivedArguments: (userKey: UserKey, mobileKey: MobileKey)? - var retrieveFlagsReturnValue: (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?)! - func retrieveFlags(for userKey: UserKey, and mobileKey: MobileKey) -> (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?) { - retrieveFlagsCallCount += 1 - retrieveFlagsReceivedArguments = (userKey: userKey, mobileKey: mobileKey) - retrieveFlagsCallback?() - return retrieveFlagsReturnValue - } - - // MARK: userKeys - var userKeysCallCount = 0 - var userKeysCallback: (() -> Void)? - var userKeysReceivedArguments: (cachedUserData: [UserKey: [String: Any]], olderThan: Date)? - var userKeysReturnValue: [UserKey]! - func userKeys(from cachedUserData: [UserKey: [String: Any]], olderThan: Date) -> [UserKey] { - userKeysCallCount += 1 - userKeysReceivedArguments = (cachedUserData: cachedUserData, olderThan: olderThan) - userKeysCallback?() - return userKeysReturnValue - } - - // MARK: removeData - var removeDataCallCount = 0 - var removeDataCallback: (() -> Void)? - var removeDataReceivedExpirationDate: Date? - func removeData(olderThan expirationDate: Date) { - removeDataCallCount += 1 - removeDataReceivedExpirationDate = expirationDate - removeDataCallback?() - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift index d9b40800..9db37b2b 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift @@ -2,11 +2,6 @@ import Foundation @testable import LaunchDarkly final class FlagMaintainingMock: FlagMaintaining { - struct Constants { - static let updateDictionaryExtraKey = "FlagMaintainingMock.UpdateDictionary.extraKey" - static let updateDictionaryExtraValue = "FlagMaintainingMock.UpdateDictionary.extraValue" - } - let innerStore: FlagStore init() { @@ -22,70 +17,39 @@ final class FlagMaintainingMock: FlagMaintaining { } var replaceStoreCallCount = 0 - var replaceStoreReceivedArguments: (newFlags: [LDFlagKey: Any], completion: CompletionClosure?)? - func replaceStore(newFlags: [LDFlagKey: Any], completion: CompletionClosure?) { + var replaceStoreReceivedNewFlags: FeatureFlagCollection? + func replaceStore(newFlags: FeatureFlagCollection) { replaceStoreCallCount += 1 - replaceStoreReceivedArguments = (newFlags: newFlags, completion: completion) - innerStore.replaceStore(newFlags: newFlags, completion: completion) + replaceStoreReceivedNewFlags = newFlags + innerStore.replaceStore(newFlags: newFlags) } var updateStoreCallCount = 0 - var updateStoreReceivedArguments: (updateDictionary: [String: Any], completion: CompletionClosure?)? - func updateStore(updateDictionary: [String: Any], completion: CompletionClosure?) { + var updateStoreReceivedUpdatedFlag: FeatureFlag? + func updateStore(updatedFlag: FeatureFlag) { updateStoreCallCount += 1 - updateStoreReceivedArguments = (updateDictionary: updateDictionary, completion: completion) - innerStore.updateStore(updateDictionary: updateDictionary, completion: completion) + updateStoreReceivedUpdatedFlag = updatedFlag + innerStore.updateStore(updatedFlag: updatedFlag) } var deleteFlagCallCount = 0 - var deleteFlagReceivedArguments: (deleteDictionary: [String: Any], completion: CompletionClosure?)? - func deleteFlag(deleteDictionary: [String: Any], completion: CompletionClosure?) { + var deleteFlagReceivedDeleteResponse: DeleteResponse? + func deleteFlag(deleteResponse: DeleteResponse) { deleteFlagCallCount += 1 - deleteFlagReceivedArguments = (deleteDictionary: deleteDictionary, completion: completion) - innerStore.deleteFlag(deleteDictionary: deleteDictionary, completion: completion) + deleteFlagReceivedDeleteResponse = deleteResponse + innerStore.deleteFlag(deleteResponse: deleteResponse) } func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? { innerStore.featureFlag(for: flagKey) } - static func stubPatchDictionary(key: LDFlagKey?, value: Any?, variation: Int?, version: Int?, includeExtraKey: Bool = false) -> [String: Any] { - var updateDictionary = [String: Any]() - if let key = key { - updateDictionary[FlagStore.Keys.flagKey] = key - } - if let value = value { - updateDictionary[FeatureFlag.CodingKeys.value.rawValue] = value - } - if let variation = variation { - updateDictionary[FeatureFlag.CodingKeys.variation.rawValue] = variation - } - if let version = version { - updateDictionary[FeatureFlag.CodingKeys.version.rawValue] = version - } - if includeExtraKey { - updateDictionary[Constants.updateDictionaryExtraKey] = Constants.updateDictionaryExtraValue - } - return updateDictionary - } - - static func stubDeleteDictionary(key: LDFlagKey?, version: Int?) -> [String: Any] { - var deleteDictionary = [String: Any]() - if let key = key { - deleteDictionary[FlagStore.Keys.flagKey] = key - } - if let version = version { - deleteDictionary[FeatureFlag.CodingKeys.version.rawValue] = version - } - return deleteDictionary - } - - static func stubFlags(includeNullValue: Bool = true, includeVersions: Bool = true) -> [String: FeatureFlag] { - var flags = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: includeNullValue, includeVersions: includeVersions) + static func stubFlags() -> [LDFlagKey: FeatureFlag] { + var flags = DarklyServiceMock.Constants.stubFeatureFlags() flags["userKey"] = FeatureFlag(flagKey: "userKey", - value: UUID().uuidString, + value: .string(UUID().uuidString), variation: DarklyServiceMock.Constants.variation, - version: includeVersions ? DarklyServiceMock.Constants.version : nil, + version: DarklyServiceMock.Constants.version, flagVersion: DarklyServiceMock.Constants.flagVersion, trackEvents: true, debugEventsUntilDate: Date().addingTimeInterval(30.0), diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift index 8cd5a488..0f08112a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift @@ -3,36 +3,12 @@ import LDSwiftEventSource @testable import LaunchDarkly extension EventHandler { - func send(event: FlagUpdateType, dict: [String: Any]) { - send(event: event, string: dict.jsonString!) - } - - func send(event: FlagUpdateType, string: String) { - onMessage(eventType: event.rawValue, messageEvent: MessageEvent(data: string)) + func send(event: String, string: String) { + onMessage(eventType: event, messageEvent: MessageEvent(data: string)) } func sendPing() { - onMessage(eventType: FlagUpdateType.ping.rawValue, messageEvent: MessageEvent(data: "")) - } - - func sendPut() { - let data = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, includeVariations: true, includeVersions: true) - .dictionaryValue - send(event: .put, dict: data) - } - - func sendPatch() { - let data = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, - value: DarklyServiceMock.FlagValues.int + 1, - variation: DarklyServiceMock.Constants.variation + 1, - version: DarklyServiceMock.Constants.version + 1) - send(event: .patch, dict: data) - } - - func sendDelete() { - let data = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, - version: DarklyServiceMock.Constants.version + 1) - send(event: .delete, dict: data) + onMessage(eventType: "ping", messageEvent: MessageEvent(data: "")) } func sendUnauthorizedError() { diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift deleted file mode 100644 index 134b6464..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableEnvironmentFlagsSpec.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class CacheableEnvironmentFlagsSpec: QuickSpec { - - private struct TestValues { - static let userKey = UUID().uuidString - static let mobKey = UUID().uuidString - static let flags = FlagMaintainingMock.stubFlags() - - static func defaultEnvironment(withFlags: [String: FeatureFlag] = flags) -> CacheableEnvironmentFlags { - CacheableEnvironmentFlags(userKey: userKey, mobileKey: mobKey, featureFlags: withFlags) - } - } - - override func spec() { - initWithElementsSpec() - initWithDictionarySpec() - dictionaryValueSpec() - } - - private func initWithElementsSpec() { - describe("initWithElements") { - it("creates a CacheableEnvironmentFlags with the elements") { - let environmentFlags = TestValues.defaultEnvironment() - expect(environmentFlags.userKey) == TestValues.userKey - expect(environmentFlags.mobileKey) == TestValues.mobKey - expect(environmentFlags.featureFlags) == TestValues.flags - } - } - } - - private func initWithDictionarySpec() { - let defaultDictionary = TestValues.defaultEnvironment().dictionaryValue - describe("initWithDictionary") { - context("creates a new CacheableEnvironmentFlags") { - it("with all elements") { - let other = CacheableEnvironmentFlags(dictionary: defaultDictionary) - expect(other?.userKey) == TestValues.userKey - expect(other?.mobileKey) == TestValues.mobKey - expect(other?.featureFlags) == TestValues.flags - } - it("with extra elements") { - var testDictionary = defaultDictionary - testDictionary["extraKey"] = "abc" - let other = CacheableEnvironmentFlags(dictionary: testDictionary) - expect(other?.userKey) == TestValues.userKey - expect(other?.mobileKey) == TestValues.mobKey - expect(other?.featureFlags) == TestValues.flags - } - } - for key in CacheableEnvironmentFlags.CodingKeys.allCases { - it("returns nil when \(key.rawValue) missing or invalid") { - var testDictionary = defaultDictionary - testDictionary[key.rawValue] = 3 // Invalid value for all fields - expect(CacheableEnvironmentFlags(dictionary: testDictionary)).to(beNil()) - testDictionary.removeValue(forKey: key.rawValue) - expect(CacheableEnvironmentFlags(dictionary: testDictionary)).to(beNil()) - } - } - } - } - - private func dictionaryValueSpec() { - describe("dictionaryValue") { - context("creates a dictionary with the elements") { - it("with null feature flag value") { - let cacheDictionary = TestValues.defaultEnvironment().dictionaryValue - expect(cacheDictionary["userKey"] as? String) == TestValues.userKey - expect(cacheDictionary["mobileKey"] as? String) == TestValues.mobKey - expect((cacheDictionary["featureFlags"] as? [LDFlagKey: Any])?.flagCollection) == TestValues.flags - } - it("without feature flags") { - let cacheDictionary = TestValues.defaultEnvironment(withFlags: [:]).dictionaryValue - expect(cacheDictionary["userKey"] as? String) == TestValues.userKey - expect(cacheDictionary["mobileKey"] as? String) == TestValues.mobKey - expect(AnyComparer.isEqual(cacheDictionary["featureFlags"], to: [:])) == true - } - // Ultimately, this is not desired behavior, but currently we are unable to store internal nil/null values - // inside of the `KeyedValueCache`. When we update our cache format, we can encode all data to get around this. - it("removes internal nulls") { - let flags = ["flag1": FeatureFlag(flagKey: "flag1", value: ["abc": [1, nil, 3]]), - "flag2": FeatureFlag(flagKey: "flag2", value: [1, ["abc": nil], 3])] - let cacheable = CacheableEnvironmentFlags(userKey: "user", mobileKey: "mobile", featureFlags: flags) - let dictionaryFlags = cacheable.dictionaryValue["featureFlags"] as! [String: [String: Any]] - let flag1 = FeatureFlag(dictionary: dictionaryFlags["flag1"]) - let flag2 = FeatureFlag(dictionary: dictionaryFlags["flag2"]) - // Manually comparing fields, `==` on `FeatureFlag` does not compare values. - expect(flag1?.flagKey) == "flag1" - expect(AnyComparer.isEqual(flag1?.value, to: ["abc": [1, 3]])).to(beTrue()) - expect(flag2?.flagKey) == "flag2" - expect(AnyComparer.isEqual(flag2?.value, to: [1, [:], 3])).to(beTrue()) - } - } - } - } -} - -extension CacheableEnvironmentFlags: Equatable { - public static func == (lhs: CacheableEnvironmentFlags, rhs: CacheableEnvironmentFlags) -> Bool { - lhs.userKey == rhs.userKey - && lhs.mobileKey == rhs.mobileKey - && lhs.featureFlags == rhs.featureFlags - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift deleted file mode 100644 index 066035b8..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Models/Cache/CacheableUserEnvironmentFlagsSpec.swift +++ /dev/null @@ -1,177 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class CacheableUserEnvironmentFlagsSpec: QuickSpec { - - private struct TestValues { - static let userKey = UUID().uuidString - static let environments = CacheableEnvironmentFlags.stubCollection(userKey: TestValues.userKey, environmentCount: 3) - static let updated = Date().stringEquivalentDate - - static func defaultEnvironment(withEnvironments: [String: CacheableEnvironmentFlags] = environments) -> CacheableUserEnvironmentFlags { - CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: withEnvironments, lastUpdated: updated) - } - } - - override func spec() { - initWithElementsSpec() - initWithDictionarySpec() - initWithObjectSpec() - dictionaryValueSpec() - } - - private func initWithElementsSpec() { - describe("init") { - it("with no environments") { - let userEnvironmentFlags = TestValues.defaultEnvironment(withEnvironments: [:]) - expect(userEnvironmentFlags.userKey) == TestValues.userKey - expect(userEnvironmentFlags.environmentFlags) == [:] - expect(userEnvironmentFlags.lastUpdated) == TestValues.updated - } - it("with environments") { - let userEnvironmentFlags = TestValues.defaultEnvironment() - expect(userEnvironmentFlags.userKey) == TestValues.userKey - expect(userEnvironmentFlags.environmentFlags) == TestValues.environments - expect(userEnvironmentFlags.lastUpdated) == TestValues.updated - } - } - } - - private func initWithDictionarySpec() { - let defaultDictionary = TestValues.defaultEnvironment().dictionaryValue - describe("initWithDictionary") { - context("creates a matching cacheableUserEnvironments") { - it("with all elements") { - let userEnv = CacheableUserEnvironmentFlags(dictionary: defaultDictionary) - expect(userEnv?.userKey) == TestValues.userKey - expect(userEnv?.environmentFlags) == TestValues.environments - expect(userEnv?.lastUpdated) == TestValues.updated - } - it("with extra dictionary items") { - var testDictionary = defaultDictionary - testDictionary["extraKey"] = "abc" - let userEnv = CacheableUserEnvironmentFlags(dictionary: testDictionary) - expect(userEnv?.userKey) == TestValues.userKey - expect(userEnv?.environmentFlags) == TestValues.environments - expect(userEnv?.lastUpdated) == TestValues.updated - } - } - for key in CacheableUserEnvironmentFlags.CodingKeys.allCases { - it("returns nil when \(key.rawValue) missing or invalid") { - var testDictionary = defaultDictionary - testDictionary[key.rawValue] = 3 // Invalid value for all fields - expect(CacheableUserEnvironmentFlags(dictionary: testDictionary)).to(beNil()) - testDictionary.removeValue(forKey: key.rawValue) - expect(CacheableUserEnvironmentFlags(dictionary: testDictionary)).to(beNil()) - } - } - } - } - - private func initWithObjectSpec() { - describe("initWithObject") { - it("inits when object is a valid dictionary") { - let userEnv = CacheableUserEnvironmentFlags(object: TestValues.defaultEnvironment().dictionaryValue) - expect(userEnv?.userKey) == TestValues.userKey - expect(userEnv?.environmentFlags) == TestValues.environments - expect(userEnv?.lastUpdated) == TestValues.updated - } - it("return nil when object is not a valid dictionary") { - expect(CacheableUserEnvironmentFlags(object: 12 as Any)).to(beNil()) - } - } - } - - private func dictionaryValueSpec() { - describe("dictionaryValue") { - it("creates a dictionary with matching elements") { - let dict = TestValues.defaultEnvironment().dictionaryValue - expect(dict["userKey"] as? String) == TestValues.userKey - let dictEnvs = dict["environmentFlags"] as? [String: [String: Any]] - expect(dictEnvs?.compactMapValues { CacheableEnvironmentFlags(dictionary: $0)}) == TestValues.environments - expect(dict["lastUpdated"] as? String) == TestValues.updated.stringValue - } - it("creates a dictionary without environments") { - let dict = TestValues.defaultEnvironment(withEnvironments: [:]).dictionaryValue - expect(dict["userKey"] as? String) == TestValues.userKey - expect((dict["environmentFlags"] as? [String: Any])?.isEmpty) == true - expect(dict["lastUpdated"] as? String) == TestValues.updated.stringValue - } - } - } -} - -extension FeatureFlag { - struct StubConstants { - static let mobileKey = "mobileKey" - } - - static func stubFlagCollection(userKey: String, mobileKey: String) -> [LDFlagKey: FeatureFlag] { - var flagCollection = DarklyServiceMock.Constants.stubFeatureFlags() - flagCollection[LDUser.StubConstants.userKey] = FeatureFlag(flagKey: LDUser.StubConstants.userKey, - value: userKey, - variation: DarklyServiceMock.Constants.variation, - version: DarklyServiceMock.Constants.version, - flagVersion: DarklyServiceMock.Constants.flagVersion, - trackEvents: true, - debugEventsUntilDate: Date().addingTimeInterval(30.0), - reason: DarklyServiceMock.Constants.reason, - trackReason: false) - flagCollection[StubConstants.mobileKey] = FeatureFlag(flagKey: StubConstants.mobileKey, - value: mobileKey, - variation: DarklyServiceMock.Constants.variation, - version: DarklyServiceMock.Constants.version, - flagVersion: DarklyServiceMock.Constants.flagVersion, - trackEvents: true, - debugEventsUntilDate: Date().addingTimeInterval(30.0), - reason: DarklyServiceMock.Constants.reason, - trackReason: false) - return flagCollection - } -} - -extension Date { - static let stubString = "2018-02-21T18:10:40.823Z" - static let stubDate = stubString.dateValue -} - -extension CacheableEnvironmentFlags { - static func stubCollection(userKey: String, environmentCount: Int) -> [MobileKey: CacheableEnvironmentFlags] { - (0.. (users: [LDUser], collection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) { - var pastSeconds = 0.0 - - let users = (0.. Bool { - if value == nil && other is NSNull { - return considerNilAndNullEqual - } - if value is NSNull && other == nil { - return considerNilAndNullEqual - } - return isEqual(value, to: other) + func testVersionForEvents() { + XCTAssertNil(FeatureFlag(flagKey: "t").versionForEvents) + XCTAssertEqual(FeatureFlag(flagKey: "t", version: 4).versionForEvents, 4) + XCTAssertEqual(FeatureFlag(flagKey: "t", flagVersion: 3).versionForEvents, 3) + XCTAssertEqual(FeatureFlag(flagKey: "t", version: 2, flagVersion: 3).versionForEvents, 3) } } -extension FeatureFlag { - func allPropertiesMatch(_ otherFlag: FeatureFlag) -> Bool { - AnyComparer.isEqual(self.value, to: otherFlag.value, considerNilAndNullEqual: true) - && variation == otherFlag.variation - && version == otherFlag.version - && flagVersion == otherFlag.flagVersion - } - - init(copying featureFlag: FeatureFlag, value: Any? = nil, variation: Int? = nil, version: Int? = nil, flagVersion: Int? = nil, trackEvents: Bool? = nil, debugEventsUntilDate: Date? = nil, reason: [String: Any]? = nil, trackReason: Bool? = nil) { - self.init(flagKey: featureFlag.flagKey, - value: value ?? featureFlag.value, - variation: variation ?? featureFlag.variation, - version: version ?? featureFlag.version, - flagVersion: flagVersion ?? featureFlag.flagVersion, - trackEvents: trackEvents ?? featureFlag.trackEvents, - debugEventsUntilDate: debugEventsUntilDate ?? featureFlag.debugEventsUntilDate, - reason: reason ?? featureFlag.reason, - trackReason: trackReason ?? featureFlag.trackReason) +extension FeatureFlag: Equatable { + public static func == (lhs: FeatureFlag, rhs: FeatureFlag) -> Bool { + lhs.flagKey == rhs.flagKey && + lhs.value == rhs.value && + lhs.variation == rhs.variation && + lhs.version == rhs.version && + lhs.flagVersion == rhs.flagVersion && + lhs.trackEvents == rhs.trackEvents && +// lhs.debugEventsUntilDate == rhs.debugEventsUntilDate && + lhs.reason == rhs.reason && + lhs.trackReason == rhs.trackReason } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift index f37372c7..cb09469e 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift @@ -184,7 +184,7 @@ extension FlagCounter { let flagCounter = FlagCounter() var featureFlag: FeatureFlag? = nil if flagKey.isKnown { - featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey, includeVersion: true, includeFlagVersion: true) + featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey) for _ in 0.. DeprecatedCacheMock { - cacheConverter.deprecatedCaches[version] as! DeprecatedCacheMock - } + override class func setUp() { + upToDateData = try! JSONEncoder().encode(["version": 7]) } - override func spec() { - initSpec() - convertCacheDataSpec() + override func setUp() { + serviceFactory = ClientServiceMockFactory() } - private func initSpec() { - var testContext: TestContext! - describe("init") { - it("creates a cache converter") { - testContext = TestContext() - expect(testContext.clientServiceFactoryMock.makeFeatureFlagCacheCallCount) == 1 - expect(testContext.cacheConverter.currentCache) === testContext.clientServiceFactoryMock.makeFeatureFlagCacheReturnValue - DeprecatedCacheModel.allCases.forEach { deprecatedCacheModel in - expect(testContext.cacheConverter.deprecatedCaches[deprecatedCacheModel]).toNot(beNil()) - expect(testContext.clientServiceFactoryMock.makeDeprecatedCacheModelReceivedModels.contains(deprecatedCacheModel)) == true - } - } - } + func testNoKeysGiven() { + CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: [], maxCachedUsers: 0) + XCTAssertEqual(serviceFactory.makeKeyedValueCacheCallCount, 0) + XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 0) } - private func convertCacheDataSpec() { - let cacheCases: [DeprecatedCacheModel?] = [.version5, nil] // Nil for no deprecated cache - var testContext: TestContext! - describe("convertCacheData") { - afterEach { - // The CacheConverter should always remove all expired data - DeprecatedCacheModel.allCases.forEach { model in - expect(testContext.deprecatedCacheMock(for: model).removeDataCallCount) == 1 - expect(testContext.deprecatedCacheMock(for: model).removeDataReceivedExpirationDate) - .to(beCloseTo(testContext.expiredCacheThreshold, within: 0.5)) - } - } - for deprecatedData in cacheCases { - context("current cache and \(deprecatedData?.rawValue ?? "no") deprecated cache data exists") { - it("does not load from deprecated caches") { - testContext = TestContext(createCacheData: true, deprecatedCacheData: deprecatedData) - testContext.cacheConverter.convertCacheData(for: testContext.user, and: testContext.config) - DeprecatedCacheModel.allCases.forEach { - expect(testContext.deprecatedCacheMock(for: $0).retrieveFlagsCallCount) == 0 - } - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 - } - } - context("no current cache data and \(deprecatedData?.rawValue ?? "no") deprecated cache data exists") { - beforeEach { - testContext = TestContext(createCacheData: false, deprecatedCacheData: deprecatedData) - testContext.cacheConverter.convertCacheData(for: testContext.user, and: testContext.config) - } - it("looks in the deprecated caches for data") { - let searchUpTo = cacheCases.firstIndex(of: deprecatedData)! - DeprecatedCacheModel.allCases.forEach { - expect(testContext.deprecatedCacheMock(for: $0).retrieveFlagsCallCount) == (cacheCases.firstIndex(of: $0)! <= searchUpTo ? 1 : 0) - } - } - if let deprecatedData = deprecatedData { - it("creates current cache data from the deprecated cache data") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == - testContext.deprecatedCacheMock(for: deprecatedData).retrieveFlagsReturnValue?.featureFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated) == - testContext.deprecatedCacheMock(for: deprecatedData).retrieveFlagsReturnValue?.lastUpdated - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .sync - } - } else { - it("leaves the current cache data unchanged") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 - } - } - } - } - } + func testUpToDate() { + let v7valueCacheMock = KeyedValueCachingMock() + serviceFactory.makeFeatureFlagCacheReturnValue.keyedValueCache = v7valueCacheMock + v7valueCacheMock.dataReturnValue = CacheConverterSpec.upToDateData + CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: ["key1", "key2"], maxCachedUsers: 0) + XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 2) + XCTAssertEqual(v7valueCacheMock.dataCallCount, 2) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift deleted file mode 100644 index 9e6df446..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelSpec.swift +++ /dev/null @@ -1,157 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -protocol CacheModelTestInterface { - var cacheKey: String { get } - func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache - func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? - func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] -} - -class DeprecatedCacheModelSpec { - - let cacheModelInterface: CacheModelTestInterface - - struct Constants { - static let offsetInterval: TimeInterval = 0.1 - } - - struct TestContext { - let cacheModel: CacheModelTestInterface - var keyedValueCacheMock = KeyedValueCachingMock() - var deprecatedCache: DeprecatedCache - var users: [LDUser] - var userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags] - var mobileKeys: [MobileKey] - var sortedLastUpdatedDates: [(userKey: UserKey, lastUpdated: Date)] { - userEnvironmentsCollection.map { ($0, $1.lastUpdated) }.sorted { - $0.lastUpdated < $1.lastUpdated - } - } - var userKeys: [UserKey] { users.map { $0.key } } - - init(_ cacheModel: CacheModelTestInterface, userCount: Int = 0) { - self.cacheModel = cacheModel - deprecatedCache = cacheModel.createDeprecatedCache(keyedValueCache: keyedValueCacheMock) - (users, userEnvironmentsCollection, mobileKeys) = CacheableUserEnvironmentFlags.stubCollection(userCount: userCount) - keyedValueCacheMock.dictionaryReturnValue = cacheModel.modelDictionary(for: users, and: userEnvironmentsCollection, mobileKeys: mobileKeys) - } - - func featureFlags(for userKey: UserKey, and mobileKey: MobileKey) -> [LDFlagKey: FeatureFlag]? { - guard let originalFlags = userEnvironmentsCollection[userKey]?.environmentFlags[mobileKey]?.featureFlags - else { return nil } - return cacheModel.expectedFeatureFlags(originalFlags: originalFlags) - } - - func expiredUserKeys(for expirationDate: Date) -> [UserKey] { - sortedLastUpdatedDates.compactMap { - $0.lastUpdated < expirationDate ? $0.userKey : nil - } - } - } - - init(cacheModelInterface: CacheModelTestInterface) { - self.cacheModelInterface = cacheModelInterface - } - - func spec() { - initSpec() - retrieveFlagsSpec() - removeDataSpec() - } - - private func initSpec() { - var testContext: TestContext! - describe("init") { - it("creates cache with the keyed value cache") { - testContext = TestContext(self.cacheModelInterface) - expect(testContext.deprecatedCache.keyedValueCache) === testContext.keyedValueCacheMock - } - } - } - - private func retrieveFlagsSpec() { - var testContext: TestContext! - var cachedData: (featureFlags: [LDFlagKey: FeatureFlag]?, lastUpdated: Date?)! - describe("retrieveFlags") { - it("returns nil when no cached data exists") { - testContext = TestContext(self.cacheModelInterface) - cachedData = testContext.deprecatedCache.retrieveFlags(for: UUID().uuidString, and: UUID().uuidString) - expect(cachedData.featureFlags).to(beNil()) - expect(cachedData.lastUpdated).to(beNil()) - } - context("when cached data exists") { - it("retrieves cached user") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - testContext.users.forEach { user in - let expectedLastUpdated = testContext.userEnvironmentsCollection[user.key]?.lastUpdated.stringEquivalentDate - testContext.mobileKeys.forEach { mobileKey in - let expectedFlags = testContext.featureFlags(for: user.key, and: mobileKey) - cachedData = testContext.deprecatedCache.retrieveFlags(for: user.key, and: mobileKey) - expect(cachedData.featureFlags) == expectedFlags - expect(cachedData.lastUpdated) == expectedLastUpdated - } - } - } - it("returns nil for uncached environment") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - cachedData = testContext.deprecatedCache.retrieveFlags(for: testContext.users.first!.key, and: UUID().uuidString) - expect(cachedData.featureFlags).to(beNil()) - expect(cachedData.lastUpdated).to(beNil()) - } - it("returns nil for uncached user") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - cachedData = testContext.deprecatedCache.retrieveFlags(for: UUID().uuidString, and: testContext.mobileKeys.first!) - expect(cachedData.featureFlags).to(beNil()) - expect(cachedData.lastUpdated).to(beNil()) - } - } - } - } - - private func removeDataSpec() { - var testContext: TestContext! - var expirationDate: Date! - describe("removeData") { - it("no cached data expired") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - let oldestLastUpdatedDate = testContext.sortedLastUpdatedDates.first! - expirationDate = oldestLastUpdatedDate.lastUpdated.addingTimeInterval(-Constants.offsetInterval) - - testContext.deprecatedCache.removeData(olderThan: expirationDate) - expect(testContext.keyedValueCacheMock.setCallCount) == 0 - } - it("some cached data expired") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - let selectedLastUpdatedDate = testContext.sortedLastUpdatedDates[testContext.users.count / 2] - expirationDate = selectedLastUpdatedDate.lastUpdated.addingTimeInterval(-Constants.offsetInterval) - - testContext.deprecatedCache.removeData(olderThan: expirationDate) - expect(testContext.keyedValueCacheMock.setCallCount) == 1 - expect(testContext.keyedValueCacheMock.setReceivedArguments?.forKey) == self.cacheModelInterface.cacheKey - let recachedData = testContext.keyedValueCacheMock.setReceivedArguments?.value as? [String: Any] - let expiredUserKeys = testContext.expiredUserKeys(for: expirationDate) - testContext.userKeys.forEach { userKey in - expect(recachedData?.keys.contains(userKey)) == !expiredUserKeys.contains(userKey) - } - } - it("all cached data expired") { - testContext = TestContext(self.cacheModelInterface, userCount: LDConfig.Defaults.maxCachedUsers) - let newestLastUpdatedDate = testContext.sortedLastUpdatedDates.last! - expirationDate = newestLastUpdatedDate.lastUpdated.addingTimeInterval(Constants.offsetInterval) - - testContext.deprecatedCache.removeData(olderThan: expirationDate) - expect(testContext.keyedValueCacheMock.removeObjectCallCount) == 1 - expect(testContext.keyedValueCacheMock.removeObjectReceivedForKey) == self.cacheModelInterface.cacheKey - } - it("no cached data") { - let testContext = TestContext(self.cacheModelInterface) - testContext.keyedValueCacheMock.dictionaryReturnValue = nil - testContext.deprecatedCache.removeData(olderThan: Date()) - expect(testContext.keyedValueCacheMock.setCallCount) == 0 - } - } - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift deleted file mode 100644 index 9a75b969..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class DeprecatedCacheModelV5Spec: QuickSpec, CacheModelTestInterface { - let cacheKey = DeprecatedCacheModelV5.CacheKeys.userEnvironments - - func createDeprecatedCache(keyedValueCache: KeyedValueCaching) -> DeprecatedCache { - DeprecatedCacheModelV5(keyedValueCache: keyedValueCache) - } - - func modelDictionary(for users: [LDUser], and userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags], mobileKeys: [MobileKey]) -> [UserKey: Any]? { - guard !users.isEmpty - else { return nil } - - var cacheDictionary = [UserKey: [String: Any]]() - users.forEach { user in - guard let userEnvironment = userEnvironmentsCollection[user.key] - else { return } - var environmentsDictionary = [MobileKey: Any]() - let lastUpdated = userEnvironmentsCollection[user.key]?.lastUpdated - mobileKeys.forEach { mobileKey in - guard let featureFlags = userEnvironment.environmentFlags[mobileKey]?.featureFlags - else { return } - environmentsDictionary[mobileKey] = user.modelV5DictionaryValue(including: featureFlags, using: lastUpdated) - } - cacheDictionary[user.key] = [CacheableEnvironmentFlags.CodingKeys.userKey.rawValue: user.key, - DeprecatedCacheModelV5.CacheKeys.environments: environmentsDictionary] - } - return cacheDictionary - } - - func expectedFeatureFlags(originalFlags: [LDFlagKey: FeatureFlag]) -> [LDFlagKey: FeatureFlag] { - originalFlags.filter { $0.value.value != nil }.compactMapValues { orig in - FeatureFlag(flagKey: orig.flagKey, - value: orig.value, - variation: orig.variation, - version: orig.version, - flagVersion: orig.flagVersion, - trackEvents: orig.trackEvents, - debugEventsUntilDate: orig.debugEventsUntilDate) - } - } - - override func spec() { - DeprecatedCacheModelSpec(cacheModelInterface: self).spec() - } -} - -// MARK: Dictionary value to cache - -extension LDUser { - func modelV5DictionaryValue(including featureFlags: [LDFlagKey: FeatureFlag], using lastUpdated: Date?) -> [String: Any] { - var userDictionary = encodeToLDValue(self, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true])?.toAny() as! [String: Any] - userDictionary[CodingKeys.privateAttributes.rawValue] = privateAttributes - userDictionary["updatedAt"] = lastUpdated?.stringValue - userDictionary[LDUser.CodingKeys.config.rawValue] = featureFlags.compactMapValues { $0.modelV5dictionaryValue } - return userDictionary - } -} - -extension FeatureFlag { -/* - [“version”: , - “flagVersion”: , - “variation”: , - “value”: , - “trackEvents”: , - “debugEventsUntilDate”: ] -*/ - var modelV5dictionaryValue: [String: Any]? { - guard value != nil - else { return nil } - var flagDictionary = dictionaryValue - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.flagKey.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.reason.rawValue) - flagDictionary.removeValue(forKey: FeatureFlag.CodingKeys.trackReason.rawValue) - return flagDictionary - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift new file mode 100644 index 00000000..3989e155 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift @@ -0,0 +1,137 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class FeatureFlagCacheSpec: XCTestCase { + + let testFlagCollection = FeatureFlagCollection(["flag1": FeatureFlag(flagKey: "flag1", variation: 1, flagVersion: 2)]) + + private var serviceFactory: ClientServiceMockFactory! + private var mockValueCache: KeyedValueCachingMock { serviceFactory.makeKeyedValueCacheReturnValue } + + override func setUp() { + serviceFactory = ClientServiceMockFactory() + } + + func testInit() { + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) + XCTAssertEqual(flagCache.maxCachedUsers, 2) + XCTAssertEqual(serviceFactory.makeKeyedValueCacheCallCount, 1) + let bundleHashed = Util.sha256base64(Bundle.main.bundleIdentifier!) + let keyHashed = Util.sha256base64("abc") + let expectedCacheKey = "com.launchdarkly.client.\(bundleHashed).\(keyHashed)" + XCTAssertEqual(serviceFactory.makeKeyedValueCacheReceivedCacheKey, expectedCacheKey) + XCTAssertTrue(flagCache.keyedValueCache as? KeyedValueCachingMock === mockValueCache) + } + + func testRetrieveNoData() { + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) + XCTAssertNil(flagCache.retrieveFeatureFlags(userKey: "user1")) + XCTAssertEqual(mockValueCache.dataCallCount, 1) + XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-\(Util.sha256base64("user1"))") + } + + func testRetrieveInvalidData() { + mockValueCache.dataReturnValue = Data("invalid".utf8) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) + XCTAssertNil(flagCache.retrieveFeatureFlags(userKey: "user1")) + } + + func testRetrieveEmptyData() throws { + mockValueCache.dataReturnValue = try JSONEncoder().encode(FeatureFlagCollection([:])) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) + XCTAssertEqual(flagCache.retrieveFeatureFlags(userKey: "user1")?.count, 0) + } + + func testRetrieveValidData() throws { + mockValueCache.dataReturnValue = try JSONEncoder().encode(testFlagCollection) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) + let retrieved = flagCache.retrieveFeatureFlags(userKey: "user1") + XCTAssertEqual(retrieved, testFlagCollection.flags) + XCTAssertEqual(mockValueCache.dataCallCount, 1) + XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-\(Util.sha256base64("user1"))") + } + + func testStoreCacheDisabled() { + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) + flagCache.storeFeatureFlags([:], userKey: "user1", lastUpdated: Date()) + XCTAssertEqual(mockValueCache.setCallCount, 0) + XCTAssertEqual(mockValueCache.dataCallCount, 0) + XCTAssertEqual(mockValueCache.removeObjectCallCount, 0) + } + + func testStoreEmptyData() throws { + let now = Date() + let hashedUserKey = Util.sha256base64("user1") + var count = 0 + mockValueCache.setCallback = { + if self.mockValueCache.setReceivedArguments?.forKey == "cached-users" { + let setData = self.mockValueCache.setReceivedArguments!.value + XCTAssertEqual(setData, try JSONEncoder().encode([hashedUserKey: now.millisSince1970])) + count += 1 + } else if let received = self.mockValueCache.setReceivedArguments { + XCTAssertEqual(received.forKey, "flags-\(hashedUserKey)") + XCTAssertEqual(received.value, try JSONEncoder().encode(FeatureFlagCollection([:]))) + count += 2 + } + } + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: -1) + flagCache.storeFeatureFlags([:], userKey: "user1", lastUpdated: now) + XCTAssertEqual(count, 3) + } + + func testStoreValidData() throws { + mockValueCache.setCallback = { + if let received = self.mockValueCache.setReceivedArguments, received.forKey.starts(with: "flags-") { + XCTAssertEqual(received.value, try JSONEncoder().encode(self.testFlagCollection)) + } + } + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: Date()) + XCTAssertEqual(mockValueCache.setCallCount, 2) + } + + func testStoreMaxCachedUsersStored() throws { + let hashedUserKey = Util.sha256base64("user1") + let now = Date() + let earlier = now.addingTimeInterval(-30.0) + mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": earlier.millisSince1970]) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: now) + XCTAssertEqual(mockValueCache.removeObjectCallCount, 1) + XCTAssertEqual(mockValueCache.removeObjectReceivedForKey, "flags-key1") + let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) + XCTAssertEqual(setMetadata, [hashedUserKey: now.millisSince1970]) + } + + func testStoreAboveMaxCachedUsersStored() throws { + let hashedUserKey = Util.sha256base64("user1") + let now = Date() + let earlier = now.addingTimeInterval(-30.0) + let later = now.addingTimeInterval(30.0) + mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": now.millisSince1970, + "key2": earlier.millisSince1970, + "key3": later.millisSince1970]) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) + var removedObjects: [String] = [] + mockValueCache.removeObjectCallback = { removedObjects.append(self.mockValueCache.removeObjectReceivedForKey!) } + flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: later) + XCTAssertEqual(mockValueCache.removeObjectCallCount, 2) + XCTAssertTrue(removedObjects.contains("flags-key1")) + XCTAssertTrue(removedObjects.contains("flags-key2")) + let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) + XCTAssertEqual(setMetadata, [hashedUserKey: later.millisSince1970, "key3": later.millisSince1970]) + } + + func testStoreInvalidMetadataStored() throws { + let hashedUserKey = Util.sha256base64("user1") + let now = Date() + mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": "123"]) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: now) + XCTAssertEqual(mockValueCache.removeObjectCallCount, 0) + let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) + XCTAssertEqual(setMetadata, [hashedUserKey: now.millisSince1970]) + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift deleted file mode 100644 index 3b59c37d..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/KeyedValueCacheSpec.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import XCTest - -@testable import LaunchDarkly - -final class KeyedValueCacheSpec: XCTestCase { - private let cacheKey = UserEnvironmentFlagCache.CacheKeys.cachedUserEnvironmentFlags - - override func setUp() { - UserDefaults.standard.removeObject(forKey: cacheKey) - } - - func testKeyValueCache() { - let testDictionary = CacheableUserEnvironmentFlags.stubCollection().collection - let cache: KeyedValueCaching = UserDefaults.standard - // Returns nil when nothing stored - XCTAssertNil(cache.dictionary(forKey: cacheKey)) - // Can store flags collection - cache.set(testDictionary.compactMapValues { $0.dictionaryValue }, forKey: cacheKey) - XCTAssertEqual(cache.dictionary(forKey: cacheKey)?.compactMapValues { CacheableUserEnvironmentFlags(object: $0) }, testDictionary) - // Set nil should remove value - cache.set(nil, forKey: cacheKey) - XCTAssertNil(cache.dictionary(forKey: cacheKey)) - // Remove should also remove value - cache.set(testDictionary.compactMapValues { $0.dictionaryValue }, forKey: cacheKey) - cache.removeObject(forKey: cacheKey) - XCTAssertNil(cache.dictionary(forKey: cacheKey)) - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift deleted file mode 100644 index 10145d3e..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift +++ /dev/null @@ -1,270 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class UserEnvironmentFlagCacheSpec: QuickSpec { - - private struct TestValues { - static let replacementFlags = ["newFlagKey": FeatureFlag.stub(flagKey: "newFlagKey", flagValue: "newFlagValue")] - static let newUserEnv = CacheableEnvironmentFlags(userKey: UUID().uuidString, - mobileKey: UUID().uuidString, - featureFlags: TestValues.replacementFlags) - static let lastUpdated = Date().addingTimeInterval(60.0).stringEquivalentDate - } - - struct TestContext { - var keyedValueCacheMock = KeyedValueCachingMock() - let storeMode: FlagCachingStoreMode - var subject: UserEnvironmentFlagCache - var userEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags]! - var selectedUser: String { - userEnvironmentsCollection.randomElement()!.key - } - var selectedMobileKey: String { - userEnvironmentsCollection[selectedUser]!.environmentFlags.randomElement()!.key - } - var oldestUser: String { - userEnvironmentsCollection.compactMapValues { $0.lastUpdated } - .max { $1.value < $0.value }! - .key - } - var setUserEnvironments: [UserKey: CacheableUserEnvironmentFlags]? { - (keyedValueCacheMock.setReceivedArguments?.value as? [UserKey: Any])?.compactMapValues { CacheableUserEnvironmentFlags(object: $0) } - } - - init(maxUsers: Int = 5, storeMode: FlagCachingStoreMode = .async) { - self.storeMode = storeMode - subject = UserEnvironmentFlagCache(withKeyedValueCache: keyedValueCacheMock, maxCachedUsers: maxUsers) - } - - mutating func withCached(userCount: Int = 1) { - userEnvironmentsCollection = CacheableUserEnvironmentFlags.stubCollection(userCount: userCount).collection - keyedValueCacheMock.dictionaryReturnValue = userEnvironmentsCollection.compactMapValues { $0.dictionaryValue } - } - - func storeNewUser() -> CacheableUserEnvironmentFlags { - let env = storeNewUserEnv(userKey: UUID().uuidString) - return CacheableUserEnvironmentFlags(userKey: env.userKey, - environmentFlags: [env.mobileKey: env], - lastUpdated: TestValues.lastUpdated) - } - - func storeNewUserEnv(userKey: String) -> CacheableEnvironmentFlags { - storeUserEnvUpdate(userKey: userKey, mobileKey: UUID().uuidString) - } - - func storeUserEnvUpdate(userKey: String, mobileKey: String) -> CacheableEnvironmentFlags { - storeFlags(TestValues.replacementFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: TestValues.lastUpdated) - return CacheableEnvironmentFlags(userKey: userKey, mobileKey: mobileKey, featureFlags: TestValues.replacementFlags) - } - - func storeFlags(_ featureFlags: [LDFlagKey: FeatureFlag], - userKey: String, - mobileKey: String, - lastUpdated: Date) { - waitUntil { done in - self.subject.storeFeatureFlags(featureFlags, userKey: userKey, mobileKey: mobileKey, lastUpdated: lastUpdated, storeMode: self.storeMode, completion: done) - if self.storeMode == .sync { done() } - } - expect(self.keyedValueCacheMock.setReceivedArguments?.forKey) == UserEnvironmentFlagCache.CacheKeys.cachedUserEnvironmentFlags - } - } - - override func spec() { - initSpec() - retrieveFeatureFlagsSpec() - storeFeatureFlagsSpec(maxUsers: LDConfig.Defaults.maxCachedUsers) - storeFeatureFlagsSpec(maxUsers: 3) - storeUnlimitedUsersSpec() - } - - private func initSpec() { - describe("init") { - it("creates a UserEnvironmentFlagCache") { - let testContext = TestContext(maxUsers: 5) - expect(testContext.subject.keyedValueCache) === testContext.keyedValueCacheMock - expect(testContext.subject.maxCachedUsers) == 5 - } - } - } - - private func retrieveFeatureFlagsSpec() { - var testContext: TestContext! - describe("retrieveFeatureFlags") { - beforeEach { - testContext = TestContext() - } - context("returns nil") { - it("when no flags are stored") { - expect(testContext.subject.retrieveFeatureFlags(forUserWithKey: "unknown", andMobileKey: "unknown")).to(beNil()) - } - it("when no flags are stored for user") { - testContext.withCached(userCount: LDConfig.Defaults.maxCachedUsers) - expect(testContext.subject.retrieveFeatureFlags(forUserWithKey: "unknown", andMobileKey: testContext.selectedMobileKey)).to(beNil()) - } - it("when no flags are stored for environment") { - testContext.withCached(userCount: LDConfig.Defaults.maxCachedUsers) - expect(testContext.subject.retrieveFeatureFlags(forUserWithKey: testContext.selectedUser, andMobileKey: "unknown")).to(beNil()) - } - } - it("returns the flags for user and environment") { - testContext.withCached(userCount: LDConfig.Defaults.maxCachedUsers) - let toRetrieve = testContext.userEnvironmentsCollection.randomElement()!.value.environmentFlags.randomElement()!.value - expect(testContext.subject.retrieveFeatureFlags(forUserWithKey: toRetrieve.userKey, andMobileKey: toRetrieve.mobileKey)) == toRetrieve.featureFlags - } - } - } - - private func storeUnlimitedUsersSpec() { - describe("storeFeatureFlags with no cached limit") { - FlagCachingStoreMode.allCases.forEach { storeMode in - it("and a new users flags are stored") { - var testContext = TestContext(maxUsers: -1, storeMode: storeMode) - testContext.withCached(userCount: LDConfig.Defaults.maxCachedUsers) - let expectedEnv = testContext.storeNewUser() - - expect(testContext.setUserEnvironments?.count) == LDConfig.Defaults.maxCachedUsers + 1 - expect(testContext.setUserEnvironments?[expectedEnv.userKey]) == expectedEnv - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - expect(testContext.setUserEnvironments?[userKey]) == userEnv - } - } - } - } - } - - private func storeFeatureFlagsSpec(maxUsers: Int) { - FlagCachingStoreMode.allCases.forEach { storeMode in - storeFeatureFlagsSpec(maxUsers: maxUsers, storeMode: storeMode) - } - } - - private func storeFeatureFlagsSpec(maxUsers: Int, storeMode: FlagCachingStoreMode) { - var testContext: TestContext! - describe(storeMode == .async ? "storeFeatureFlagsAsync" : "storeFeatureFlagsSync") { - beforeEach { - testContext = TestContext(maxUsers: maxUsers, storeMode: storeMode) - } - it("when store is empty") { - let expectedEnv = testContext.storeNewUser() - - expect(testContext.setUserEnvironments?.count) == 1 - expect(testContext.setUserEnvironments?[expectedEnv.userKey]) == expectedEnv - } - context("when less than the max number of users flags are stored") { - it("and an existing users flags are changed") { - testContext.withCached(userCount: maxUsers - 1) - let expectedEnv = testContext.storeUserEnvUpdate(userKey: testContext.selectedUser, mobileKey: testContext.selectedMobileKey) - - expect(testContext.setUserEnvironments?.count) == maxUsers - 1 - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - if userKey != expectedEnv.userKey { - expect(testContext.setUserEnvironments?[userKey]) == userEnv - return - } - - var userFlags = userEnv.environmentFlags - userFlags[expectedEnv.mobileKey] = expectedEnv - expect(testContext.setUserEnvironments?[userKey]) == CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: userFlags, lastUpdated: TestValues.lastUpdated) - } - } - it("and an existing user adds a new environment") { - testContext.withCached(userCount: maxUsers - 1) - let expectedEnv = testContext.storeNewUserEnv(userKey: testContext.selectedUser) - - expect(testContext.setUserEnvironments?.count) == maxUsers - 1 - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - if userKey != expectedEnv.userKey { - expect(testContext.setUserEnvironments?[userKey]) == userEnv - return - } - - var userFlags = userEnv.environmentFlags - userFlags[expectedEnv.mobileKey] = expectedEnv - expect(testContext.setUserEnvironments?[userKey]) == CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: userFlags, lastUpdated: TestValues.lastUpdated) - } - } - it("and a new users flags are stored") { - testContext.withCached(userCount: maxUsers - 1) - let expectedEnv = testContext.storeNewUser() - - expect(testContext.setUserEnvironments?.count) == maxUsers - expect(testContext.setUserEnvironments?[expectedEnv.userKey]) == expectedEnv - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - expect(testContext.setUserEnvironments?[userKey]) == userEnv - } - } - } - context("when max number of users flags are stored") { - it("and an existing users flags are changed") { - testContext.withCached(userCount: maxUsers) - let expectedEnv = testContext.storeUserEnvUpdate(userKey: testContext.selectedUser, mobileKey: testContext.selectedMobileKey) - - expect(testContext.setUserEnvironments?.count) == maxUsers - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - if userKey != expectedEnv.userKey { - expect(testContext.setUserEnvironments?[userKey]) == userEnv - return - } - - var userFlags = userEnv.environmentFlags - userFlags[expectedEnv.mobileKey] = expectedEnv - expect(testContext.setUserEnvironments?[userKey]) == CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: userFlags, lastUpdated: TestValues.lastUpdated) - } - } - it("and an existing user adds a new environment") { - testContext.withCached(userCount: maxUsers) - let expectedEnv = testContext.storeNewUserEnv(userKey: testContext.selectedUser) - - expect(testContext.setUserEnvironments?.count) == maxUsers - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - if userKey != expectedEnv.userKey { - expect(testContext.setUserEnvironments?[userKey]) == userEnv - return - } - - var userFlags = userEnv.environmentFlags - userFlags[expectedEnv.mobileKey] = expectedEnv - expect(testContext.setUserEnvironments?[userKey]) == CacheableUserEnvironmentFlags(userKey: userKey, environmentFlags: userFlags, lastUpdated: TestValues.lastUpdated) - } - } - it("and a new users flags are stored overwrites oldest user") { - testContext.withCached(userCount: maxUsers) - let expectedEnv = testContext.storeNewUser() - - expect(testContext.setUserEnvironments?.count) == maxUsers - expect(testContext.setUserEnvironments?.keys.contains(testContext.oldestUser)) == false - expect(testContext.setUserEnvironments?[expectedEnv.userKey]) == expectedEnv - testContext.userEnvironmentsCollection.forEach { userKey, userEnv in - guard userKey != testContext.oldestUser - else { return } - expect(testContext.setUserEnvironments?[userKey]) == userEnv - } - } - } - } - } -} - -extension CacheableUserEnvironmentFlags: Equatable { - public static func == (lhs: CacheableUserEnvironmentFlags, rhs: CacheableUserEnvironmentFlags) -> Bool { - lhs.userKey == rhs.userKey && - lhs.lastUpdated == rhs.lastUpdated && - lhs.environmentFlags == rhs.environmentFlags - } -} - -private extension FeatureFlag { - static func stub(flagKey: LDFlagKey, flagValue: Any?) -> FeatureFlag { - FeatureFlag(flagKey: flagKey, - value: flagValue, - variation: DarklyServiceMock.Constants.variation, - version: DarklyServiceMock.Constants.version, - flagVersion: DarklyServiceMock.Constants.flagVersion, - trackEvents: true, - debugEventsUntilDate: Date().addingTimeInterval(30.0), - reason: DarklyServiceMock.Constants.reason, - trackReason: false) - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index caa3f72a..1e0d177e 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -1,7 +1,6 @@ import Foundation import Quick import Nimble -import XCTest @testable import LaunchDarkly final class EventReporterSpec: QuickSpec { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift index 12cf793d..591f7cad 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift @@ -1,227 +1,85 @@ import Foundation -import Quick -import Nimble -@testable import LaunchDarkly +import XCTest -final class FlagStoreSpec: QuickSpec { - struct DefaultValues { - static let bool = false - static let int = 3 - static let double = 2.71828 - static let string = "defaultValue string" - static let array = [0] - static let dictionary: [String: Any] = [DarklyServiceMock.FlagKeys.string: DarklyServiceMock.FlagValues.string] - } +@testable import LaunchDarkly +final class FlagStoreSpec: XCTestCase { let stubFlags = DarklyServiceMock.Constants.stubFeatureFlags() - override func spec() { - initSpec() - replaceStoreSpec() - updateStoreSpec() - deleteFlagSpec() - featureFlagSpec() + func testInit() { + XCTAssertEqual(FlagStore().featureFlags, [:]) + XCTAssertEqual(FlagStore(featureFlags: self.stubFlags).featureFlags, self.stubFlags) } - func initSpec() { - describe("init") { - it("without an initial flag store is empty") { - expect(FlagStore().featureFlags.isEmpty) == true - } - it("with an initial flag store") { - expect(FlagStore(featureFlags: self.stubFlags).featureFlags) == self.stubFlags - } - it("with an initial flag store without elements") { - let featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false) - expect(FlagStore(featureFlags: featureFlags).featureFlags) == featureFlags - } - it("with an initial flag dictionary") { - expect(FlagStore(featureFlagDictionary: self.stubFlags.dictionaryValue).featureFlags) == self.stubFlags - } - } + func testReplaceStore() { + let featureFlags: [LDFlagKey: FeatureFlag] = DarklyServiceMock.Constants.stubFeatureFlags() + let flagStore = FlagStore() + flagStore.replaceStore(newFlags: FeatureFlagCollection(featureFlags)) + XCTAssertEqual(flagStore.featureFlags, featureFlags) } - func replaceStoreSpec() { - let featureFlags: [LDFlagKey: FeatureFlag] = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false) - describe("replaceStore") { - it("with new flag values replaces flag values") { - let flagStore = FlagStore() - waitUntil { done in - flagStore.replaceStore(newFlags: featureFlags, completion: done) - } - expect(flagStore.featureFlags) == featureFlags - } - it("with flags dictionary replaces flag values") { - let flagStore = FlagStore() - waitUntil { done in - flagStore.replaceStore(newFlags: featureFlags.dictionaryValue, completion: done) - } - expect(flagStore.featureFlags) == featureFlags - } - it("with invalid dictionary empties the flag values") { - let flagStore = FlagStore(featureFlags: featureFlags) - waitUntil { done in - flagStore.replaceStore(newFlags: ["fakeKey": "Not a flag dict"], completion: done) - } - expect(flagStore.featureFlags.isEmpty).to(beTrue()) - } - } + func testUpdateStoreNewFlag() { + let flagStore = FlagStore(featureFlags: stubFlags) + let flagUpdate = FeatureFlag(flagKey: "new-int-flag", value: "abc", version: 0) + flagStore.updateStore(updatedFlag: flagUpdate) + XCTAssertEqual(flagStore.featureFlags.count, stubFlags.count + 1) + XCTAssertEqual(flagStore.featureFlags["new-int-flag"], flagUpdate) } - func updateStoreSpec() { - var subject: FlagStore! - var updateDictionary: [String: Any]! + func testUpdateStoreNewerVersion() { + let flagStore = FlagStore(featureFlags: stubFlags) + let flagUpdate = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.int, useAlternateVersion: true) + flagStore.updateStore(updatedFlag: flagUpdate) + XCTAssertEqual(flagStore.featureFlags.count, stubFlags.count) + XCTAssertEqual(flagStore.featureFlags[DarklyServiceMock.FlagKeys.int], flagUpdate) + } - func updateFlag(key: String? = DarklyServiceMock.FlagKeys.int, - value: Any? = DarklyServiceMock.FlagValues.alternate(DarklyServiceMock.FlagValues.int), - variation: Int? = DarklyServiceMock.Constants.variation + 1, - version: Int? = DarklyServiceMock.Constants.version + 1, - includeExtraKey: Bool = false) { - waitUntil { done in - updateDictionary = FlagMaintainingMock.stubPatchDictionary(key: key, value: value, variation: variation, version: version, includeExtraKey: includeExtraKey) - subject.updateStore(updateDictionary: updateDictionary, completion: done) - } - } + func testUpdateStoreNoVersion() { + let flagStore = FlagStore(featureFlags: stubFlags) + let flagUpdate = FeatureFlag(flagKey: DarklyServiceMock.FlagKeys.int, value: "abc", version: nil) + flagStore.updateStore(updatedFlag: flagUpdate) + XCTAssertEqual(flagStore.featureFlags.count, stubFlags.count) + XCTAssertEqual(flagStore.featureFlags[DarklyServiceMock.FlagKeys.int], flagUpdate) + } - describe("updateStore") { - beforeEach { - subject = FlagStore(featureFlags: DarklyServiceMock.Constants.stubFeatureFlags()) - } - context("makes no changes") { - it("when the update version == existing version") { - updateFlag(variation: DarklyServiceMock.Constants.variation, version: DarklyServiceMock.Constants.version) - expect(subject.featureFlags) == self.stubFlags - } - it("when the update version < existing version") { - updateFlag(variation: DarklyServiceMock.Constants.variation - 1, version: DarklyServiceMock.Constants.version - 1) - expect(subject.featureFlags) == self.stubFlags - } - it("when the update dictionary is missing the flagKey") { - updateFlag(key: nil) - expect(subject.featureFlags) == self.stubFlags - } - } - context("updates the feature flag") { - it("when the update version > existing version") { - updateFlag() - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(AnyComparer.isEqual(featureFlag?.value, to: updateDictionary?.value)).to(beTrue()) - expect(featureFlag?.variation) == updateDictionary?.variation - expect(featureFlag?.version) == updateDictionary?.version - } - it("when the new value is null") { - updateFlag(value: NSNull()) - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(featureFlag?.value).to(beNil()) - expect(featureFlag?.variation) == updateDictionary.variation - expect(featureFlag?.version) == updateDictionary.version - } - it("when the update dictionary is missing the value") { - updateFlag(value: nil) - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(featureFlag?.value).to(beNil()) - expect(featureFlag?.variation) == updateDictionary.variation - expect(featureFlag?.version) == updateDictionary.version - } - it("when the update dictionary is missing the variation") { - updateFlag(variation: nil) - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(AnyComparer.isEqual(featureFlag?.value, to: updateDictionary.value)).to(beTrue()) - expect(featureFlag?.variation).to(beNil()) - expect(featureFlag?.version) == updateDictionary.version - } - it("when the update dictionary is missing the version") { - updateFlag(version: nil) - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(AnyComparer.isEqual(featureFlag?.value, to: updateDictionary.value)).to(beTrue()) - expect(featureFlag?.variation) == updateDictionary.variation - expect(featureFlag?.version).to(beNil()) - } - it("when the update dictionary has more keys than needed") { - updateFlag(includeExtraKey: true) - let featureFlag = subject.featureFlags[DarklyServiceMock.FlagKeys.int] - expect(AnyComparer.isEqual(featureFlag?.value, to: updateDictionary.value)).to(beTrue()) - expect(featureFlag?.variation) == updateDictionary.variation - expect(featureFlag?.version) == updateDictionary.version - } - } - it("adds new feature flag to the store") { - updateFlag(key: "new-int-flag") - let featureFlag = subject.featureFlags["new-int-flag"] - expect(AnyComparer.isEqual(featureFlag?.value, to: updateDictionary?.value)).to(beTrue()) - expect(featureFlag?.variation) == updateDictionary?.variation - expect(featureFlag?.version) == updateDictionary?.version - } - } + func testUpdateStoreEarlierOrSameVersion() { + let testFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.int) + let initialVersion = testFlag.version! + let flagStore = FlagStore(featureFlags: stubFlags) + let flagUpdateSameVersion = FeatureFlag(flagKey: DarklyServiceMock.FlagKeys.int, value: "abc", version: initialVersion) + let flagUpdateOlderVersion = FeatureFlag(flagKey: DarklyServiceMock.FlagKeys.int, value: "abc", version: initialVersion - 1) + flagStore.updateStore(updatedFlag: flagUpdateSameVersion) + flagStore.updateStore(updatedFlag: flagUpdateOlderVersion) + XCTAssertEqual(flagStore.featureFlags, self.stubFlags) } - func deleteFlagSpec() { - var subject: FlagStore! + func testDeleteFlagNewerVersion() { + let flagStore = FlagStore(featureFlags: stubFlags) + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1)) + XCTAssertEqual(flagStore.featureFlags.count, self.stubFlags.count - 1) + XCTAssertNil(flagStore.featureFlags[DarklyServiceMock.FlagKeys.int]) + } - func deleteFlag(_ deleteDictionary: [String: Any]) { - waitUntil { done in - subject.deleteFlag(deleteDictionary: deleteDictionary, completion: done) - } - } + func testDeleteFlagMissingVersion() { + let flagStore = FlagStore(featureFlags: stubFlags) + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: nil)) + XCTAssertEqual(flagStore.featureFlags.count, self.stubFlags.count - 1) + XCTAssertNil(flagStore.featureFlags[DarklyServiceMock.FlagKeys.int]) + } - describe("deleteFlag") { - beforeEach { - subject = FlagStore(featureFlags: DarklyServiceMock.Constants.stubFeatureFlags()) - } - context("removes flag") { - it("with exact dictionary") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1)) - expect(subject.featureFlags.count) == self.stubFlags.count - 1 - expect(subject.featureFlags[DarklyServiceMock.FlagKeys.int]).to(beNil()) - } - it("with extra fields on dictionary") { - var deleteDictionary = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) - deleteDictionary["new-field"] = 10 - deleteFlag(deleteDictionary) - expect(subject.featureFlags.count) == self.stubFlags.count - 1 - expect(subject.featureFlags[DarklyServiceMock.FlagKeys.int]).to(beNil()) - } - } - context("makes no changes to the flag store") { - it("when the version is the same") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version)) - expect(subject.featureFlags) == self.stubFlags - } - it("when the new version < existing version") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version - 1)) - expect(subject.featureFlags) == self.stubFlags - } - it("when the flag doesn't exist") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: "new-int-flag", version: DarklyServiceMock.Constants.version + 1)) - expect(subject.featureFlags) == self.stubFlags - } - it("when delete dictionary is missing the key") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: nil, version: DarklyServiceMock.Constants.version + 1)) - expect(subject.featureFlags) == self.stubFlags - } - it("when delete dictionary is missing the version") { - deleteFlag(FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: nil)) - expect(subject.featureFlags) == self.stubFlags - } - } - } + func testDeleteOlderOrNonExistent() { + let flagStore = FlagStore(featureFlags: stubFlags) + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version)) + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version - 1)) + flagStore.deleteFlag(deleteResponse: DeleteResponse(key: "new-int-flag", version: DarklyServiceMock.Constants.version + 1)) + XCTAssertEqual(flagStore.featureFlags, self.stubFlags) } - func featureFlagSpec() { - var flagStore: FlagStore! - describe("featureFlag") { - beforeEach { - flagStore = FlagStore(featureFlags: DarklyServiceMock.Constants.stubFeatureFlags()) - } - it("returns existing feature flag") { - flagStore.featureFlags.forEach { flagKey, featureFlag in - expect(flagStore.featureFlag(for: flagKey)?.allPropertiesMatch(featureFlag)).to(beTrue()) - } - } - it("returns nil when flag doesn't exist") { - let featureFlag = flagStore.featureFlag(for: DarklyServiceMock.FlagKeys.unknown) - expect(featureFlag).to(beNil()) - } + func testFeatureFlag() { + let flagStore = FlagStore(featureFlags: stubFlags) + flagStore.featureFlags.forEach { flagKey, featureFlag in + XCTAssertEqual(flagStore.featureFlag(for: flagKey), featureFlag) } + XCTAssertNil(flagStore.featureFlag(for: DarklyServiceMock.FlagKeys.unknown)) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift index 2b781380..722916cd 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift @@ -7,16 +7,10 @@ import LDSwiftEventSource final class FlagSynchronizerSpec: QuickSpec { struct Constants { fileprivate static let pollingInterval: TimeInterval = 1 - fileprivate static let waitMillis: Int = 500 } struct TestContext { - var config: LDConfig! - var user: LDUser! var serviceMock: DarklyServiceMock! - var eventSourceMock: DarklyStreamingProviderMock? { - serviceMock.createdEventSource - } var providedEventHandler: EventHandler? { serviceMock.createEventSourceReceivedHandler } @@ -28,8 +22,6 @@ final class FlagSynchronizerSpec: QuickSpec { var diagnosticCacheMock: DiagnosticCachingMock init(streamingMode: LDStreamingMode, useReport: Bool, onSyncComplete: FlagSyncCompleteClosure? = nil) { - config = LDConfig.stub - user = LDUser.stub() serviceMock = DarklyServiceMock() diagnosticCacheMock = DiagnosticCachingMock() serviceMock.diagnosticCache = diagnosticCacheMock @@ -39,69 +31,6 @@ final class FlagSynchronizerSpec: QuickSpec { service: serviceMock, onSyncComplete: onSyncComplete) } - - private func isStreamingActive(online: Bool, streamingMode: LDStreamingMode) -> Bool { - online && (streamingMode == .streaming) - } - private func isPollingActive(online: Bool, streamingMode: LDStreamingMode) -> Bool { - online && (streamingMode == .polling) - } - - fileprivate func synchronizerState(synchronizerOnline isOnline: Bool, - streamingMode: LDStreamingMode, - flagRequests: Int, - streamCreated: Bool, - streamOpened: Bool? = nil, - streamClosed: Bool? = nil) -> ToMatchResult { - var messages = [String]() - - // synchronizer state - if flagSynchronizer.isOnline != isOnline { - messages.append("isOnline equals \(flagSynchronizer.isOnline)") - } - if flagSynchronizer.streamingMode != streamingMode { - messages.append("streamingMode equals \(flagSynchronizer.streamingMode)") - } - if flagSynchronizer.streamingActive != isStreamingActive(online: isOnline, streamingMode: streamingMode) { - messages.append("streamingActive equals \(flagSynchronizer.streamingActive)") - } - if flagSynchronizer.pollingActive != isPollingActive(online: isOnline, streamingMode: streamingMode) { - messages.append("pollingActive equals \(flagSynchronizer.pollingActive)") - } - - // flag requests - if serviceMock.getFeatureFlagsCallCount != flagRequests { - messages.append("flag requests equals \(serviceMock.getFeatureFlagsCallCount)") - } - - messages.append(contentsOf: eventSourceStateVerificationMessages(streamCreated: streamCreated, streamOpened: streamOpened, streamClosed: streamClosed)) - - return messages.isEmpty ? .matched : .failed(reason: messages.joined(separator: ", ")) - } - - private func eventSourceStateVerificationMessages(streamCreated: Bool, streamOpened: Bool? = nil, streamClosed: Bool? = nil) -> [String] { - var messages = [String]() - - let expectedStreamCreate = streamCreated ? 1 : 0 - if serviceMock.createEventSourceCallCount != expectedStreamCreate { - messages.append("stream create call count equals \(serviceMock.createEventSourceCallCount), expected \(expectedStreamCreate)") - } - - if let streamOpened = streamOpened { - let expectedStreamOpened = streamOpened ? 1 : 0 - if eventSourceMock?.startCallCount != expectedStreamOpened { - messages.append("stream start call count equals \(String(describing: eventSourceMock?.startCallCount)), expected \(expectedStreamOpened)") - } - } - - if let streamClosed = streamClosed { - if eventSourceMock?.stopCallCount != (streamClosed ? 1 : 0) { - messages.append("stream closed call count equals \(eventSourceMock?.stopCallCount ?? 0), expected \(streamClosed ? 1 : 0)") - } - } - - return messages - } } override func spec() { @@ -114,54 +43,49 @@ final class FlagSynchronizerSpec: QuickSpec { func initSpec() { describe("init") { - var testContext: TestContext! + it("starts up streaming offline using get flag requests") { + let testContext = TestContext(streamingMode: .streaming, useReport: false) - beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: false) + expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval + expect(testContext.flagSynchronizer.useReport) == false + + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } - context("streaming mode") { - context("get flag requests") { - it("starts up streaming offline using get flag requests") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .streaming, flagRequests: 0, streamCreated: false) }).to(match()) - expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval - expect(testContext.flagSynchronizer.useReport) == false - } - } - context("report flag requests") { - beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: true) - } - it("starts up streaming offline using report flag requests") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .streaming, flagRequests: 0, streamCreated: false) }).to(match()) - expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval - expect(testContext.flagSynchronizer.useReport) == true - } - } + it("starts up streaming offline using report flag requests") { + let testContext = TestContext(streamingMode: .streaming, useReport: true) + + expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval + expect(testContext.flagSynchronizer.useReport) == true + + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } - context("polling mode") { - afterEach { - testContext.flagSynchronizer.isOnline = false - } - context("get flag requests") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: false) - } - it("starts up polling offline using get flag requests") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .polling, flagRequests: 0, streamCreated: false) }).to(match()) - expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval - expect(testContext.flagSynchronizer.useReport) == false - } - } - context("report flag requests") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: true) - } - it("starts up polling offline using report flag requests") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .polling, flagRequests: 0, streamCreated: false) }).to(match()) - expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval - expect(testContext.flagSynchronizer.useReport) == true - } - } + it("starts up polling offline using get flag requests") { + let testContext = TestContext(streamingMode: .polling, useReport: false) + + expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval + expect(testContext.flagSynchronizer.useReport) == false + + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 + } + it("starts up polling offline using report flag requests") { + let testContext = TestContext(streamingMode: .polling, useReport: true) + + expect(testContext.flagSynchronizer.pollingInterval) == Constants.pollingInterval + expect(testContext.flagSynchronizer.useReport) == true + + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } } } @@ -174,117 +98,95 @@ final class FlagSynchronizerSpec: QuickSpec { testContext = TestContext(streamingMode: .streaming, useReport: false) } context("online to offline") { - context("streaming") { - beforeEach { - testContext.flagSynchronizer.isOnline = true + it("stops streaming") { + testContext.flagSynchronizer.isOnline = true + testContext.flagSynchronizer.isOnline = false - testContext.flagSynchronizer.isOnline = false - } - it("stops streaming") { - expect({ testContext.synchronizerState(synchronizerOnline: false, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: true) }).to(match()) - } + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 1 } - context("polling") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: false) - testContext.flagSynchronizer.isOnline = true + it("stops polling") { + testContext = TestContext(streamingMode: .polling, useReport: false) + testContext.flagSynchronizer.isOnline = true + testContext.flagSynchronizer.isOnline = false - testContext.flagSynchronizer.isOnline = false - } - it("stops polling") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .polling, flagRequests: 1, streamCreated: false) }).to(match()) - } + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } } context("offline to online") { - context("streaming") { - beforeEach { - testContext.flagSynchronizer.isOnline = true - } - it("starts streaming") { - // streaming expects a ping on successful connection that triggers a flag request. No ping means no flag requests - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - expect(testContext.serviceMock.createEventSourceCallCount) == 1 - expect(testContext.eventSourceMock!.startCallCount) == 1 - } + it("starts streaming") { + testContext.flagSynchronizer.isOnline = true + + // streaming expects a ping on successful connection that triggers a flag request. No ping means no flag requests + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 } - context("polling") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: false) + it("starts polling") { + testContext = TestContext(streamingMode: .polling, useReport: false) + testContext.flagSynchronizer.isOnline = true - testContext.flagSynchronizer.isOnline = true - } - afterEach { - testContext.flagSynchronizer.isOnline = false - } - it("starts polling") { - // polling starts by requesting flags - expect({ testContext.synchronizerState(synchronizerOnline: true, streamingMode: .polling, flagRequests: 1, streamCreated: false) }).to(match()) - } + // polling starts by requesting flags + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 + + testContext.flagSynchronizer.isOnline = false } } context("online to online") { - context("streaming") { - beforeEach { - testContext.flagSynchronizer.isOnline = true + it("does not stop streaming") { + testContext.flagSynchronizer.isOnline = true + testContext.flagSynchronizer.isOnline = true - testContext.flagSynchronizer.isOnline = true - } - it("does not stop streaming") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - expect(testContext.serviceMock.createEventSourceCallCount) == 1 - expect(testContext.eventSourceMock!.startCallCount) == 1 - } + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 } - context("polling") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: false) - testContext.flagSynchronizer.isOnline = true + it("does not stop polling") { + testContext = TestContext(streamingMode: .polling, useReport: false) + testContext.flagSynchronizer.isOnline = true + testContext.flagSynchronizer.isOnline = true - testContext.flagSynchronizer.isOnline = true - } - afterEach { - testContext.flagSynchronizer.isOnline = false - } - it("does not stop polling") { - // setting the same value shouldn't make another flag request - expect({ testContext.synchronizerState(synchronizerOnline: true, streamingMode: .polling, flagRequests: 1, streamCreated: false) }).to(match()) - } + // setting the same value shouldn't make another flag request + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 + + testContext.flagSynchronizer.isOnline = false } } context("offline to offline") { - context("streaming") { - beforeEach { - testContext.flagSynchronizer.isOnline = false - } - it("does not start streaming") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .streaming, flagRequests: 0, streamCreated: false) }).to(match()) - } + it("does not start streaming") { + testContext.flagSynchronizer.isOnline = false + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } - context("polling") { - beforeEach { - testContext = TestContext(streamingMode: .polling, useReport: false) + it("does not start polling") { + testContext = TestContext(streamingMode: .polling, useReport: false) + testContext.flagSynchronizer.isOnline = false - testContext.flagSynchronizer.isOnline = false - } - it("does not start polling") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .polling, flagRequests: 0, streamCreated: false) }).to(match()) - } + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .polling + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 } } } @@ -303,92 +205,84 @@ final class FlagSynchronizerSpec: QuickSpec { func streamingPingEventSpec() { var testContext: TestContext! - - beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: false) - } - context("ping") { context("success") { - var newFlags: [String: FeatureFlag]? - var streamingEvent: FlagUpdateType? - beforeEach { + var syncResult: FlagSyncResult? + it("requests flags and calls onSyncComplete with the new flags and streaming event") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in - if case .success(let flags, let streamEvent) = result { - (newFlags, streamingEvent) = (flags.flagCollection, streamEvent) - } + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in + syncResult = result done() - }) + } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags and calls onSyncComplete with the new flags and streaming event") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 1, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - expect(newFlags) == DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false) - expect(streamingEvent) == .ping + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + + guard case let .flagCollection(flagCollection) = syncResult + else { return fail("Expected flag collection sync result") } + expect(flagCollection.flags) == DarklyServiceMock.Constants.stubFeatureFlags() } } context("bad data") { var synchronizingError: SynchronizingError? - beforeEach { + it("requests flags and calls onSyncComplete with a data error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case let .error(syncError) = result { synchronizingError = syncError } done() - }) + } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok, badData: true) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags and calls onSyncComplete with a data error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 1, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + guard case .data(DarklyServiceMock.Constants.errorData) = synchronizingError else { - fail("Unexpected error for bad data: \(String(describing: synchronizingError))") - return + return fail("Unexpected error for bad data: \(String(describing: synchronizingError))") } } } context("failure response") { var urlResponse: URLResponse? - beforeEach { + it("requests flags and calls onSyncComplete with a response error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case let .error(syncError) = result, case .response(let syncErrorResponse) = syncError { urlResponse = syncErrorResponse } done() - }) + } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.internalServerError, responseOnly: true) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags and calls onSyncComplete with a response error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 1, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(urlResponse).toNot(beNil()) if let urlResponse = urlResponse { expect(urlResponse.httpStatusCode) == HTTPURLResponse.StatusCodes.internalServerError @@ -397,32 +291,31 @@ final class FlagSynchronizerSpec: QuickSpec { } context("failure error") { var synchronizingError: SynchronizingError? - beforeEach { + it("requests flags and calls onSyncComplete with a request error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case let .error(syncError) = result { synchronizingError = syncError } done() - }) + } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.internalServerError, errorOnly: true) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags and calls onSyncComplete with a request error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 1, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + guard case let .request(error) = synchronizingError, DarklyServiceMock.Constants.error == error as NSError else { - fail("Unexpected error for failure: \(String(describing: synchronizingError))") - return + return fail("Unexpected error for failure: \(String(describing: synchronizingError))") } } } @@ -431,65 +324,54 @@ final class FlagSynchronizerSpec: QuickSpec { func streamingPutEventSpec() { var testContext: TestContext! - - beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: false) - } - + var syncResult: FlagSyncResult? context("put") { context("success") { - var newFlags: [String: FeatureFlag]? - var streamingEvent: FlagUpdateType? - beforeEach { + it("does not request flags and calls onSyncComplete with new flags and put event type") { + let putData = "{\"flagKey\": {\"value\": 123}}" waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in - if case .success(let flags, let streamEvent) = result { - (newFlags, streamingEvent) = (flags.flagCollection, streamEvent) - } + testContext = TestContext(streamingMode: .streaming, useReport: false) { + syncResult = $0 done() - }) + } testContext.flagSynchronizer.isOnline = true - - testContext.providedEventHandler!.sendPut() + testContext.providedEventHandler!.send(event: "put", string: putData) } - } - it("does not request flags and calls onSyncComplete with new flags and put event type") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - expect(newFlags) == DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false) - expect(streamingEvent) == .put + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + + guard case let .flagCollection(flagCollection) = syncResult + else { return fail("Expected flag collection sync result") } + expect(flagCollection.flags.count) == 1 + expect(flagCollection.flags["flagKey"]) == FeatureFlag(flagKey: "flagKey", value: 123) } } context("bad data") { - var syncError: SynchronizingError? - beforeEach { + it("does not request flags and calls onSyncComplete with a data error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in - if case .error(let error) = result { - syncError = error - } + testContext = TestContext(streamingMode: .streaming, useReport: false) { + syncResult = $0 done() - }) + } testContext.flagSynchronizer.isOnline = true - - testContext.providedEventHandler!.send(event: .put, string: DarklyServiceMock.Constants.jsonErrorString) + testContext.providedEventHandler!.send(event: "put", string: DarklyServiceMock.Constants.jsonErrorString) } - } - it("does not request flags and calls onSyncComplete with a data error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - guard case .data(DarklyServiceMock.Constants.errorData) = syncError + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + + guard case .error(.data(DarklyServiceMock.Constants.errorData)) = syncResult else { - fail("Unexpected error for bad data: \(String(describing: syncError))") - return + return fail("Unexpected error for bad data: \(String(describing: syncResult))") } } } @@ -498,69 +380,56 @@ final class FlagSynchronizerSpec: QuickSpec { func streamingPatchEventSpec() { var testContext: TestContext! - - beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: false) - } - + var syncResult: FlagSyncResult? context("patch") { context("success") { - var flagDictionary: [String: Any]? - var streamingEvent: FlagUpdateType? - beforeEach { + it("does not request flags and calls onSyncComplete with new flags and put event type") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in - if case .success(let patch, let streamEvent) = result { - (flagDictionary, streamingEvent) = (patch, streamEvent) - } + testContext = TestContext(streamingMode: .streaming, useReport: false) { + syncResult = $0 done() - }) + } testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.sendPatch() + testContext.providedEventHandler!.send(event: "patch", string: "{\"key\": \"abc\"}") } - } - it("does not request flags and calls onSyncComplete with new flags and put event type") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - let stubPatch = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, - value: DarklyServiceMock.FlagValues.int + 1, - variation: DarklyServiceMock.Constants.variation + 1, - version: DarklyServiceMock.Constants.version + 1) - expect(AnyComparer.isEqual(flagDictionary, to: stubPatch)).to(beTrue()) - expect(streamingEvent) == .patch + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + + guard case let .patch(flag) = syncResult + else { return fail("Expected patch sync result") } + expect(flag.flagKey) == "abc" } } context("bad data") { var syncError: SynchronizingError? - beforeEach { + it("does not request flags and calls onSyncComplete with a data error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case .error(let error) = result { syncError = error } done() - }) + } testContext.flagSynchronizer.isOnline = true - - testContext.providedEventHandler!.send(event: .patch, string: DarklyServiceMock.Constants.jsonErrorString) + testContext.providedEventHandler!.send(event: "patch", string: DarklyServiceMock.Constants.jsonErrorString) } - } - it("does not request flags and calls onSyncComplete with a data error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + guard case .data(DarklyServiceMock.Constants.errorData) = syncError else { - fail("Unexpected error for bad data: \(String(describing: syncError))") - return + return fail("Unexpected error for bad data: \(String(describing: syncError))") } } } @@ -576,59 +445,55 @@ final class FlagSynchronizerSpec: QuickSpec { context("delete") { context("success") { - var flagDictionary: [String: Any]? - var streamingEvent: FlagUpdateType? - beforeEach { + var syncResult: FlagSyncResult? + it("does not request flags and calls onSyncComplete with new flags and put event type") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in - if case .success(let delete, let streamEvent) = result { - (flagDictionary, streamingEvent) = (delete, streamEvent) - } + testContext = TestContext(streamingMode: .streaming, useReport: false) { + syncResult = $0 done() - }) + } testContext.flagSynchronizer.isOnline = true - - testContext.providedEventHandler!.sendDelete() + let deleteData = "{\"key\": \"\(DarklyServiceMock.FlagKeys.int)\", \"version\": \(DarklyServiceMock.Constants.version + 1)}" + testContext.providedEventHandler!.send(event: "delete", string: deleteData) } - } - it("does not request flags and calls onSyncComplete with new flags and put event type") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) - let stubDelete = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) - expect(AnyComparer.isEqual(flagDictionary, to: stubDelete)).to(beTrue()) - expect(streamingEvent) == .delete + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + + guard case let .delete(deleteResponse) = syncResult + else { return fail("expected delete dictionary sync result") } + expect(deleteResponse.key) == DarklyServiceMock.FlagKeys.int + expect(deleteResponse.version) == DarklyServiceMock.Constants.version + 1 } } context("bad data") { var syncError: SynchronizingError? - beforeEach { + it("does not request flags and calls onSyncComplete with a data error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case .error(let error) = result { syncError = error } done() - }) + } testContext.flagSynchronizer.isOnline = true - - testContext.providedEventHandler!.send(event: .delete, string: DarklyServiceMock.Constants.jsonErrorString) + testContext.providedEventHandler!.send(event: "delete", string: DarklyServiceMock.Constants.jsonErrorString) } - } - it("does not request flags and calls onSyncComplete with a data error") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + guard case .data(DarklyServiceMock.Constants.errorData) = syncError else { - fail("Unexpected error for bad data: \(String(describing: syncError))") - return + return fail("Unexpected error for bad data: \(String(describing: syncError))") } } } @@ -642,37 +507,36 @@ final class FlagSynchronizerSpec: QuickSpec { var testContext: TestContext! beforeEach { - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { _ in + testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in testContext.onSyncCompleteCallCount += 1 - }) + } } context("error event") { beforeEach { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { syncResult in + testContext = TestContext(streamingMode: .streaming, useReport: false) { syncResult in if case .error(let errorResult) = syncResult { syncError = errorResult } done() - }) + } testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.sendServerError() } } it("does not request flags & reports the error via onSyncComplete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(syncError).toNot(beNil()) expect(syncError?.isClientUnauthorized).to(beFalse()) guard case .streamError = syncError else { - fail("Expected stream error") - return + return fail("Expected stream error") } } it("does not record stream init diagnostic") { @@ -684,29 +548,29 @@ final class FlagSynchronizerSpec: QuickSpec { var returnedAction: ConnectionErrorAction! beforeEach { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { syncResult in + testContext = TestContext(streamingMode: .streaming, useReport: false) { syncResult in if case .error(let errorResult) = syncResult { syncError = errorResult } done() - }) + } testContext.flagSynchronizer.isOnline = true returnedAction = testContext.providedErrorHandler!(UnsuccessfulResponseError(responseCode: 418)) } } it("does not request flags & reports the error via onSyncComplete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(syncError).toNot(beNil()) expect(syncError?.isClientUnauthorized).to(beFalse()) guard case .streamError = syncError else { - fail("Expected stream error") - return + return fail("Expected stream error") } } it("does not record stream init diagnostic") { @@ -726,30 +590,29 @@ final class FlagSynchronizerSpec: QuickSpec { context("unauthorized error event") { beforeEach { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { syncResult in + testContext = TestContext(streamingMode: .streaming, useReport: false) { syncResult in if case .error(let errorResult) = syncResult { syncError = errorResult } done() - }) + } testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.sendUnauthorizedError() } } it("does not request flags & reports the error via onSyncComplete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(syncError).toNot(beNil()) expect(syncError?.isClientUnauthorized).to(beTrue()) guard case .streamError = syncError else { - fail("Expected stream error") - return + return fail("Expected stream error") } } it("does not record stream init diagnostic") { @@ -760,16 +623,16 @@ final class FlagSynchronizerSpec: QuickSpec { context("heartbeat") { beforeEach { testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.onComment(comment: "") } it("does not request flags or report sync complete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(testContext.onSyncCompleteCallCount) == 0 } it("does not record stream init diagnostic") { @@ -780,16 +643,16 @@ final class FlagSynchronizerSpec: QuickSpec { context("comment") { beforeEach { testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.onComment(comment: "foo") } it("does not request flags or report sync complete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(testContext.onSyncCompleteCallCount) == 0 } it("does not record stream init diagnostic") { @@ -800,16 +663,16 @@ final class FlagSynchronizerSpec: QuickSpec { context("open event") { beforeEach { testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.onOpened() } it("does not request flags or report sync complete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(testContext.onSyncCompleteCallCount) == 0 } it("records stream init diagnostic") { @@ -828,16 +691,16 @@ final class FlagSynchronizerSpec: QuickSpec { context("closed event") { beforeEach { testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.onClosed() } it("does not request flags") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(testContext.onSyncCompleteCallCount) == 0 } it("does not record stream init diagnostic") { @@ -849,30 +712,29 @@ final class FlagSynchronizerSpec: QuickSpec { var syncErrorEvent: SynchronizingError? beforeEach { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { syncResult in + testContext = TestContext(streamingMode: .streaming, useReport: false) { syncResult in if case .error(let errorResult) = syncResult { syncErrorEvent = errorResult } done() - }) + } testContext.flagSynchronizer.isOnline = true - testContext.providedEventHandler!.sendNonResponseError() } } it("does not request flags & reports the error via onSyncComplete") { - expect({ testContext.synchronizerState(synchronizerOnline: true, - streamingMode: .streaming, - flagRequests: 0, - streamCreated: true, - streamOpened: true, - streamClosed: false) }).to(match()) + expect(testContext.flagSynchronizer.isOnline) == true + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.startCallCount) == 1 + expect(testContext.serviceMock.createdEventSource?.stopCallCount) == 0 + expect(syncErrorEvent).toNot(beNil()) expect(syncErrorEvent?.isClientUnauthorized).to(beFalse()) guard case .streamError = syncErrorEvent else { - fail("Expected stream error") - return + return fail("Expected stream error") } } it("does not record stream init diagnostic") { @@ -887,44 +749,30 @@ final class FlagSynchronizerSpec: QuickSpec { var testContext: TestContext! var syncError: SynchronizingError? + let data = "{\"flag1\": {}}" + context("event reported while offline") { - beforeEach { - let data = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, - includeVariations: true, - includeVersions: true) - .dictionaryValue - .jsonString! + it("reports offline") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case .error(let errorResult) = result { syncError = errorResult } done() - }) - - testContext.flagSynchronizer.testStreamOnMessage(event: FlagUpdateType.put.rawValue, - messageEvent: MessageEvent(data: data)) + } + testContext.flagSynchronizer.testStreamOnMessage(event: "put", messageEvent: MessageEvent(data: data)) } - } - it("reports offline") { + guard case .isOffline = syncError else { - fail("Expected syncError to be .isOffline, was: \(String(describing: syncError))") - return + return fail("Expected syncError to be .isOffline, was: \(String(describing: syncError))") } } } context("event reported while polling") { - beforeEach { - let data = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, - includeVariations: true, - includeVersions: true) - .dictionaryValue - .jsonString! + it("reports an event error") { waitUntil { done in - testContext = TestContext(streamingMode: .polling, useReport: false, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .polling, useReport: false) { _ in done() } testContext.flagSynchronizer.isOnline = true } waitUntil { done in @@ -935,57 +783,44 @@ final class FlagSynchronizerSpec: QuickSpec { done() } - testContext.flagSynchronizer.testStreamOnMessage(event: FlagUpdateType.put.rawValue, - messageEvent: MessageEvent(data: data)) + testContext.flagSynchronizer.testStreamOnMessage(event: "put", messageEvent: MessageEvent(data: data)) } - } - afterEach { - testContext.flagSynchronizer.isOnline = false - } - it("reports an event error") { + guard case .streamEventWhilePolling = syncError else { - fail("Expected syncError to be .streamEventWhilePolling, was: \(String(describing: syncError))") - return + return fail("Expected syncError to be .streamEventWhilePolling, was: \(String(describing: syncError))") } + + testContext.flagSynchronizer.isOnline = false } } context("event reported while streaming inactive") { - beforeEach { - let data = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, - includeVariations: true, - includeVersions: true) - .dictionaryValue - .jsonString! + it("reports offline") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case .error(let errorResult) = result { syncError = errorResult } done() - }) + } testContext.flagSynchronizer.isOnline = true testContext.flagSynchronizer.testEventSource = nil - testContext.flagSynchronizer.testStreamOnMessage(event: FlagUpdateType.put.rawValue, - messageEvent: MessageEvent(data: data)) + testContext.flagSynchronizer.testStreamOnMessage(event: "put", messageEvent: MessageEvent(data: data)) } - } - it("reports offline") { + guard case .isOffline = syncError else { - fail("Expected syncError to be .isOffline, was: \(String(describing: syncError))") - return + return fail("Expected syncError to be .isOffline, was: \(String(describing: syncError))") } } } } func pollingTimerFiresSpec() { + var syncResult: FlagSyncResult? describe("polling timer fires") { context("one second interval") { var testContext: TestContext! - var newFlags: [String: FeatureFlag]? - var streamingEvent: FlagUpdateType? beforeEach { testContext = TestContext(streamingMode: .polling, useReport: false) testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) @@ -994,9 +829,7 @@ final class FlagSynchronizerSpec: QuickSpec { waitUntil(timeout: .seconds(2)) { done in // In polling mode, the flagSynchronizer makes a flag request when set online right away. To verify the timer this test waits the polling interval (1s) for a second flag request testContext.flagSynchronizer.onSyncComplete = { result in - if case .success(let flags, let streamEvent) = result { - (newFlags, streamingEvent) = (flags.flagCollection, streamEvent) - } + syncResult = result done() } } @@ -1006,8 +839,9 @@ final class FlagSynchronizerSpec: QuickSpec { } it("makes a flag request and calls onSyncComplete with no streaming event") { expect(testContext.serviceMock.getFeatureFlagsCallCount) == 2 - expect(newFlags == DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false, includeVariations: true, includeVersions: true)).to(beTrue()) - expect(streamingEvent).to(beNil()) + guard case let .flagCollection(flagCollection) = syncResult + else { return fail("Expected flag collection sync result") } + expect(flagCollection.flags) == DarklyServiceMock.Constants.stubFeatureFlags() } // This particular test causes a retain cycle between the FlagSynchronizer and something else. By removing onSyncComplete, the closure is no longer called after the test is complete. afterEach { @@ -1022,18 +856,15 @@ final class FlagSynchronizerSpec: QuickSpec { var testContext: TestContext! context("using get method") { context("success") { - beforeEach { + it("requests flags using a get request exactly one time") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags using a get request exactly one time") { + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 expect(testContext.serviceMock.getFeatureFlagsUseReportCalledValue.first) == false } @@ -1043,9 +874,7 @@ final class FlagSynchronizerSpec: QuickSpec { it("requests flags using a get request exactly one time") { for statusCode in HTTPURLResponse.StatusCodes.nonRetry { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: statusCode) testContext.flagSynchronizer.isOnline = true @@ -1061,9 +890,7 @@ final class FlagSynchronizerSpec: QuickSpec { it("requests flags using a get request exactly one time") { for statusCode in HTTPURLResponse.StatusCodes.retry { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: statusCode) testContext.flagSynchronizer.isOnline = true @@ -1079,18 +906,15 @@ final class FlagSynchronizerSpec: QuickSpec { } context("using report method") { context("success") { - beforeEach { + it("requests flags using a get request exactly one time") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: true, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: true) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } - } - it("requests flags using a get request exactly one time") { + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1 expect(testContext.serviceMock.getFeatureFlagsUseReportCalledValue.first) == true } @@ -1100,9 +924,7 @@ final class FlagSynchronizerSpec: QuickSpec { it("requests flags using a get request exactly one time") { for statusCode in HTTPURLResponse.StatusCodes.nonRetry { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: true, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: true) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: statusCode) testContext.flagSynchronizer.isOnline = true @@ -1118,9 +940,7 @@ final class FlagSynchronizerSpec: QuickSpec { it("requests flags using a report request exactly one time, followed by a get request exactly one time") { for statusCode in HTTPURLResponse.StatusCodes.retry { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: true, onSyncComplete: { _ in - done() - }) + testContext = TestContext(streamingMode: .streaming, useReport: true) { _ in done() } testContext.serviceMock.stubFlagResponse(statusCode: statusCode) testContext.flagSynchronizer.isOnline = true @@ -1141,28 +961,36 @@ final class FlagSynchronizerSpec: QuickSpec { // This test completes the test suite on makeFlagRequest by validating the method bails out if it's called and the synchronizer is offline. While that shouldn't happen, there are 2 code paths that don't directly verify the SDK is online before calling the method, so it seems a wise precaution to validate that the method does bailout. Other tests exercise the rest of the method. context("offline") { var synchronizingError: SynchronizingError? - beforeEach { + it("does not request flags and calls onSyncComplete with an isOffline error") { waitUntil { done in - testContext = TestContext(streamingMode: .streaming, useReport: false, onSyncComplete: { result in + testContext = TestContext(streamingMode: .streaming, useReport: false) { result in if case .error(let syncError) = result { synchronizingError = syncError } done() - }) + } testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) testContext.flagSynchronizer.testMakeFlagRequest() } - } - it("does not request flags and calls onSyncComplete with an isOffline error") { - expect({ testContext.synchronizerState(synchronizerOnline: false, streamingMode: .streaming, flagRequests: 0, streamCreated: false) }).to(match()) + + expect(testContext.flagSynchronizer.isOnline) == false + expect(testContext.flagSynchronizer.streamingMode) == .streaming + expect(testContext.serviceMock.getFeatureFlagsCallCount) == 0 + expect(testContext.serviceMock.createEventSourceCallCount) == 0 + guard case .isOffline = synchronizingError else { - fail("Expected syncError to be .isOffline, was: \(String(describing: synchronizingError))") - return + return fail("Expected syncError to be .isOffline, was: \(String(describing: synchronizingError))") } } } } } } + +extension DeleteResponse: Equatable { + public static func == (lhs: DeleteResponse, rhs: DeleteResponse) -> Bool { + lhs.key == rhs.key && lhs.version == rhs.version + } +} diff --git a/SourceryTemplates/mocks.stencil b/SourceryTemplates/mocks.stencil index 4ba5443f..b03faefb 100644 --- a/SourceryTemplates/mocks.stencil +++ b/SourceryTemplates/mocks.stencil @@ -12,18 +12,18 @@ final class {{ type.name }}Mock: {{ type.name }} { {% for variable in type.allVariables|!annotated:"noMock" %} var {{ variable.name }}SetCount = 0 - var set{{ variable.name|upperFirstLetter }}Callback: (() -> Void)? + var set{{ variable.name|upperFirstLetter }}Callback: (() throws -> Void)? var {{ variable.name }}: {{ variable.typeName }}{% if variable|annotated:"defaultMockValue" %} = {{ variable.annotations.defaultMockValue }}{% elif variable.isOptional %} = nil{% elif variable.isArray %} = []{% elif variable.isDictionary %} = [:]{% else %} // You must annotate mocked variables that are not optional, arrays, or dictionaries, using a comment: //sourcery: defaultMockValue = {% endif %} { didSet { {{ variable.name }}SetCount += 1 - set{{ variable.name|upperFirstLetter }}Callback?() + try! set{{ variable.name|upperFirstLetter }}Callback?() } } {% endfor %} {% for method in type.allMethods|!annotated:"noMock" %} var {{ method.callName }}CallCount = 0 - var {{ method.callName }}Callback: (() -> Void)? + var {{ method.callName }}Callback: (() throws -> Void)? {% if method.throws %} var {{ method.callName }}ShouldThrow: Error?{% endif %} {% if method.parameters.count == 1 %} var {{ method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {{ param.typeName.unwrappedTypeName }}?{% endfor %} {% else %}{% if not method.parameters.count == 0 %} var {{ method.callName }}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% if param.typeAttributes.escaping %}{{ param.unwrappedTypeName }}{% else %}{{ param.typeName }}{% endif %}{% if not forloop.last %}, {% endif %}{% endfor %})?{% endif %} @@ -33,7 +33,7 @@ final class {{ type.name }}Mock: {{ type.name }} { {{ method.callName }}CallCount += 1 {%if method.parameters.count == 1 %} {{ method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %}{% else %}{% if not method.parameters.count == 0 %} {{ method.callName }}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}){% endif %}{% if not method.returnTypeName.isVoid %}{% endif %}{% endif %} {% if method.throws %} if let {{ method.callName }}ShouldThrow = {{ method.callName }}ShouldThrow { throw {{ method.callName }}ShouldThrow }{% endif %} - {{ method.callName }}Callback?() + try! {{ method.callName }}Callback?() {% if not method.returnTypeName.isVoid %} return {{ method.callName }}ReturnValue{% endif %} }