diff --git a/ConfigCat.podspec b/ConfigCat.podspec index 27bddd3..25d9569 100755 --- a/ConfigCat.podspec +++ b/ConfigCat.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = "ConfigCat" - spec.version = "9.4.0" + spec.version = "10.0.0" spec.summary = "ConfigCat Swift SDK" spec.swift_version = "4.2" diff --git a/ConfigCat.xcconfig b/ConfigCat.xcconfig index 270fea8..8371559 100644 --- a/ConfigCat.xcconfig +++ b/ConfigCat.xcconfig @@ -47,4 +47,4 @@ SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator watchos watchsimulator app SWIFT_VERSION = 4.2 // ConfigCat SDK version -MARKETING_VERSION = 9.4.0 +MARKETING_VERSION = 10.0.0 diff --git a/ConfigCat.xcodeproj/project.pbxproj b/ConfigCat.xcodeproj/project.pbxproj index a13e8c5..77f7f95 100755 --- a/ConfigCat.xcodeproj/project.pbxproj +++ b/ConfigCat.xcodeproj/project.pbxproj @@ -56,8 +56,13 @@ C4FA1B3E278D919A00BFA8C3 /* OverrideDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FA1B3C278D919900BFA8C3 /* OverrideDataSource.swift */; }; C4FA1B40278D953300BFA8C3 /* OverrideBehaviour.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FA1B3F278D953300BFA8C3 /* OverrideBehaviour.swift */; }; C4FA1B41278D953300BFA8C3 /* OverrideBehaviour.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FA1B3F278D953300BFA8C3 /* OverrideBehaviour.swift */; }; + F10AC6D02A93B02B006FA496 /* CacheTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6CF2A93B02B006FA496 /* CacheTest.swift */; }; + F10AC6D22A950197006FA496 /* FlagEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6D12A950197006FA496 /* FlagEvaluator.swift */; }; + F10AC6D32A9507C4006FA496 /* FlagEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6D12A950197006FA496 /* FlagEvaluator.swift */; }; + F10AC6D52A9516F5006FA496 /* ConfigCatSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6D42A9516F5006FA496 /* ConfigCatSnapshot.swift */; }; + F10AC6D62A9516F5006FA496 /* ConfigCatSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6D42A9516F5006FA496 /* ConfigCatSnapshot.swift */; }; F11F76BC288AD6CA0097939F /* AsyncAwaitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BB288AD6CA0097939F /* AsyncAwaitTests.swift */; }; - F11F76BE288AE7540097939F /* SyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BD288AE7540097939F /* SyncTests.swift */; }; + F11F76BE288AE7540097939F /* SnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BD288AE7540097939F /* SnapshotTests.swift */; }; F11F76C0288AE7650097939F /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BF288AE7640097939F /* Extensions.swift */; }; F11F76C1288AE7970097939F /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BF288AE7640097939F /* Extensions.swift */; }; F14961CF28FF71400095A72A /* EvaluationDetailsExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F14961CE28FF71400095A72A /* EvaluationDetailsExtensionTests.swift */; }; @@ -120,9 +125,12 @@ C4D34D3A249B6F2900908D76 /* testmatrix_variationId.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_variationId.csv; path = Resources/testmatrix_variationId.csv; sourceTree = ""; }; C4FA1B3C278D919900BFA8C3 /* OverrideDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverrideDataSource.swift; sourceTree = ""; }; C4FA1B3F278D953300BFA8C3 /* OverrideBehaviour.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverrideBehaviour.swift; sourceTree = ""; }; + F10AC6CF2A93B02B006FA496 /* CacheTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheTest.swift; sourceTree = ""; }; + F10AC6D12A950197006FA496 /* FlagEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagEvaluator.swift; sourceTree = ""; }; + F10AC6D42A9516F5006FA496 /* ConfigCatSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigCatSnapshot.swift; sourceTree = ""; }; F10F787D2528950D0021F468 /* DataGovernanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGovernanceTests.swift; sourceTree = ""; }; F11F76BB288AD6CA0097939F /* AsyncAwaitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncAwaitTests.swift; sourceTree = ""; }; - F11F76BD288AE7540097939F /* SyncTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncTests.swift; sourceTree = ""; }; + F11F76BD288AE7540097939F /* SnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotTests.swift; sourceTree = ""; }; F11F76BF288AE7640097939F /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; F14961CE28FF71400095A72A /* EvaluationDetailsExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvaluationDetailsExtensionTests.swift; sourceTree = ""; }; F15F9AF62169176A00F490CD /* ConfigCatUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigCatUser.swift; sourceTree = ""; }; @@ -222,6 +230,8 @@ 3F1F2C6523E10BF300AFA7D2 /* PollingModes.swift */, F1E1180A257532D700DA245A /* Log.swift */, 3F880A41207BE91300087A6B /* Resources */, + F10AC6D12A950197006FA496 /* FlagEvaluator.swift */, + F10AC6D42A9516F5006FA496 /* ConfigCatSnapshot.swift */, ); name = Sources; path = Sources/ConfigCat; @@ -232,7 +242,7 @@ children = ( F14961CE28FF71400095A72A /* EvaluationDetailsExtensionTests.swift */, F1BC414B28E1D6C900F2230A /* Helpers.swift */, - F11F76BD288AE7540097939F /* SyncTests.swift */, + F11F76BD288AE7540097939F /* SnapshotTests.swift */, F11F76BB288AD6CA0097939F /* AsyncAwaitTests.swift */, F17DEE22288876AE009C3E48 /* LazyLoadingTests.swift */, C40CF51127B5533800D9F88A /* LocalTests.swift */, @@ -252,6 +262,7 @@ 3F8EDF9F2084194700906339 /* ConfigCatClientTests.swift */, F15F9B16216973B000F490CD /* RolloutIntegrationTests.swift */, F10F787D2528950D0021F468 /* DataGovernanceTests.swift */, + F10AC6CF2A93B02B006FA496 /* CacheTest.swift */, ); name = Tests; path = Tests/ConfigCatTests; @@ -380,8 +391,10 @@ F17DEE28288876F7009C3E48 /* MutableQueue.swift in Sources */, F1BC414728E1D54800F2230A /* EvaluationDetails.swift in Sources */, C4FA1B3D278D919A00BFA8C3 /* OverrideDataSource.swift in Sources */, + F10AC6D52A9516F5006FA496 /* ConfigCatSnapshot.swift in Sources */, B4BD2AB6258CA6FF007371E2 /* Log.swift in Sources */, B4BD2AB8258CA6FF007371E2 /* KeyValue.swift in Sources */, + F10AC6D32A9507C4006FA496 /* FlagEvaluator.swift in Sources */, B4BD2AB9258CA6FF007371E2 /* Config.swift in Sources */, B4BD2ABA258CA6FF007371E2 /* RolloutEvaluator.swift in Sources */, B4BD2ABE258CA6FF007371E2 /* Synced.swift in Sources */, @@ -404,6 +417,8 @@ F1BC414828E1D54800F2230A /* EvaluationDetails.swift in Sources */, C40CF51527B557EE00D9F88A /* LocalDictionaryDataSource.swift in Sources */, F1B1D8B628FF2C830034165E /* ConfigCatOptions.swift in Sources */, + F10AC6D62A9516F5006FA496 /* ConfigCatSnapshot.swift in Sources */, + F10AC6D22A950197006FA496 /* FlagEvaluator.swift in Sources */, B4BD2AE6258CA7DF007371E2 /* ConfigFetcher.swift in Sources */, B4BD2AE7258CA7DF007371E2 /* RolloutIntegrationTests.swift in Sources */, B4BD2AE8258CA7DF007371E2 /* Config.swift in Sources */, @@ -411,6 +426,7 @@ B4BD2AEC258CA7DF007371E2 /* ConfigFetcherTests.swift in Sources */, B4BD2AED258CA7DF007371E2 /* DataGovernanceTests.swift in Sources */, B4BD2AEE258CA7DF007371E2 /* PollingMode.swift in Sources */, + F10AC6D02A93B02B006FA496 /* CacheTest.swift in Sources */, F17DEE2F288876F7009C3E48 /* Utils.swift in Sources */, F11F76BC288AD6CA0097939F /* AsyncAwaitTests.swift in Sources */, C4FA1B41278D953300BFA8C3 /* OverrideBehaviour.swift in Sources */, @@ -432,7 +448,7 @@ B4BD2AFC258CA7DF007371E2 /* AutoPollingTests.swift in Sources */, B4BD2AFF258CA7DF007371E2 /* VariationIdTests.swift in Sources */, B4BD2B00258CA7DF007371E2 /* Mock.swift in Sources */, - F11F76BE288AE7540097939F /* SyncTests.swift in Sources */, + F11F76BE288AE7540097939F /* SnapshotTests.swift in Sources */, C40CF51227B5533800D9F88A /* LocalTests.swift in Sources */, B4BD2B02258CA7DF007371E2 /* ManualPollingTests.swift in Sources */, F17DEE29288876F7009C3E48 /* MutableQueue.swift in Sources */, diff --git a/README.md b/README.md index 8e0efbb..a7fb83b 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ If you want to use ConfigCat in a [SwiftPM](https://swift.org/package-manager/) ``` swift dependencies: [ - .package(url: "https://github.com/configcat/swift-sdk", from: "9.4.0") + .package(url: "https://github.com/configcat/swift-sdk", from: "10.0.0") ] ``` diff --git a/Sources/ConfigCat/Config.swift b/Sources/ConfigCat/Config.swift index e04fe2d..b47fc5d 100644 --- a/Sources/ConfigCat/Config.swift +++ b/Sources/ConfigCat/Config.swift @@ -4,44 +4,68 @@ protocol JsonSerializable { func toJsonMap() -> [String: Any] } -class ConfigEntry: Equatable, JsonSerializable { +class ConfigEntry: Equatable { static func ==(lhs: ConfigEntry, rhs: ConfigEntry) -> Bool { lhs.eTag == rhs.eTag } let config: Config + let configJson: String let eTag: String let fetchTime: Date - init(config: Config = Config.empty, eTag: String = "", fetchTime: Date = .distantPast) { + init(config: Config = Config.empty, configJson: String = "", eTag: String = "", fetchTime: Date = .distantPast) { self.config = config self.eTag = eTag self.fetchTime = fetchTime + self.configJson = configJson } func withFetchTime(time: Date) -> ConfigEntry { - ConfigEntry(config: config, eTag: eTag, fetchTime: time) + ConfigEntry(config: config, configJson: configJson, eTag: eTag, fetchTime: time) + } + + func isExpired(seconds: Int) -> Bool { + return Date().subtract(seconds: seconds)! > fetchTime; } - static func fromJson(json: [String: Any]) -> ConfigEntry { - let eTag = json["eTag"] as? String ?? "" - var config: Config = .empty - var fetchTime: Date = .distantPast - if let configFromMap = json["config"] as? [String: Any] { - config = Config.fromJson(json: configFromMap) + static func fromConfigJson(json: String, eTag: String, fetchTime: Date) -> Result { + do { + guard let data = json.data(using: .utf8) else { + return .failure(ParseError(message: "Decode to utf8 data failed.")) + } + guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + return .failure(ParseError(message: "Convert to [String: Any] map failed.")) + } + + return .success(ConfigEntry(config: Config.fromJson(json: jsonObject), configJson: json, eTag: eTag, fetchTime: fetchTime)) + } catch { + return .failure(error) + } + } + + static func fromCached(cached: String) -> Result { + guard let timeIndex = cached.firstIndex(of: "\n") else { + return .failure(ParseError(message: "Number of values is fewer than expected.")) + } + let withoutTime = String(cached.suffix(from: cached.index(timeIndex, offsetBy: 1))) + guard let eTagIndex = withoutTime.firstIndex(of: "\n") else { + return .failure(ParseError(message: "Number of values is fewer than expected.")) } - if let fetchIntervalSince1970 = json["fetchTime"] as? Double { - fetchTime = Date(timeIntervalSince1970: fetchIntervalSince1970) + + let timeString = String(cached[.. [String: Any] { - [ - "eTag": eTag, - "fetchTime": fetchTime.timeIntervalSince1970, - "config": config.toJsonMap() - ] + func serialize() -> String { + String(format: "%.0f", floor(fetchTime.timeIntervalSince1970 * 1000)) + "\n" + eTag + "\n" + configJson } var isEmpty: Bool { diff --git a/Sources/ConfigCat/ConfigCatClient.swift b/Sources/ConfigCat/ConfigCatClient.swift index fe2fda9..35bc9ff 100755 --- a/Sources/ConfigCat/ConfigCatClient.swift +++ b/Sources/ConfigCat/ConfigCatClient.swift @@ -1,10 +1,6 @@ import Foundation import os.log -extension ConfigCatClient { - public typealias ConfigChangedHandler = () -> () -} - /// Describes the location of your feature flag and setting data within the ConfigCat CDN. @objc public enum DataGovernance: Int { /// Select this if your feature flags are published to all global CDN nodes. @@ -16,7 +12,7 @@ extension ConfigCatClient { /// A client for handling configurations provided by ConfigCat. public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { private let log: Logger - private let evaluator: RolloutEvaluator + private let flagEvaluator: FlagEvaluator private let configService: ConfigService? private let sdkKey: String private let overrideDataSource: OverrideDataSource? @@ -26,33 +22,6 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { private static let mutex = Mutex() private static var instances: [String: Weak] = [:] - /** - Initializes a new `ConfigCatClient`. - - - Parameter sdkKey: the SDK Key for to communicate with the ConfigCat services. - - Parameter dataGovernance: default: Global. Set this parameter to be in sync with the Data Governance preference on the Dashboard: - https://app.configcat.com/organization/data-governance - - Parameter configCache: a cache implementation, see `ConfigCache`. - - Parameter refreshMode: the polling mode, `autoPoll`, `lazyLoad` or `manualPoll`. - - Parameter sessionConfiguration: the url session configuration. - - Parameter baseUrl: use this if you want to use a proxy server between your application and ConfigCat. - - Parameter flagOverrides: An OverrideDataSource implementation used to override feature flags & settings. - - Parameter logLevel: default: warning. Internal log level. - - Returns: A new `ConfigCatClient`. - */ - @available(*, deprecated, message: "Use `ConfigCatClient.get()` instead") - @objc public convenience init(sdkKey: String, - dataGovernance: DataGovernance = DataGovernance.global, - configCache: ConfigCache? = nil, - refreshMode: PollingMode = PollingModes.autoPoll(), - sessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default, - baseUrl: String = "", - flagOverrides: OverrideDataSource? = nil, - logLevel: LogLevel = .warning) { - self.init(sdkKey: sdkKey, pollingMode: refreshMode, httpEngine: URLSessionEngine(session: URLSession(configuration: sessionConfiguration)), - configCache: configCache ?? UserDefaultsCache(), baseUrl: baseUrl, dataGovernance: dataGovernance, flagOverrides: flagOverrides, logLevel: logLevel) - } - init(sdkKey: String, pollingMode: PollingMode, httpEngine: HttpEngine?, @@ -64,20 +33,20 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { defaultUser: ConfigCatUser? = nil, logLevel: LogLevel = .warning, offline: Bool = false) { - if sdkKey.isEmpty { - assert(false, "sdkKey cannot be empty") - } + + assert(!sdkKey.isEmpty, "sdkKey cannot be empty") self.sdkKey = sdkKey self.hooks = hooks ?? Hooks() self.defaultUser = defaultUser log = Logger(level: logLevel, hooks: self.hooks) overrideDataSource = flagOverrides - evaluator = RolloutEvaluator(logger: log) + flagEvaluator = FlagEvaluator(log: log, evaluator: RolloutEvaluator(logger: log), hooks: self.hooks) if let overrideDataSource = overrideDataSource, overrideDataSource.behaviour == .localOnly { // configService is not needed in localOnly mode configService = nil + hooks?.invokeOnReady(state: .hasLocalOverrideFlagDataOnly) } else { let fetcher = ConfigFetcher(httpEngine: httpEngine ?? URLSessionEngine(session: URLSession(configuration: URLSessionConfiguration.default)), logger: log, @@ -195,58 +164,17 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { - Parameter completion: the function which will be called when the feature flag or setting is evaluated. */ public func getValue(for key: String, defaultValue: Value, user: ConfigCatUser? = nil, completion: @escaping (Value) -> ()) { - if key.isEmpty { - assert(false, "key cannot be empty") - } + assert(!key.isEmpty, "key cannot be empty") let evalUser = user ?? defaultUser - if Value.self != String.self && - Value.self != String?.self && - Value.self != Int.self && - Value.self != Int?.self && - Value.self != Double.self && - Value.self != Double?.self && - Value.self != Bool.self && - Value.self != Bool?.self && - Value.self != Any.self && - Value.self != Any?.self { - let message = "Only String, Integer, Double, Bool or Any types are supported." - log.error(eventId: 2022, message: message) - hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, - value: defaultValue, - error: message, - user: evalUser)) + + if let _ = flagEvaluator.validateFlagType(of: Value.self, key: key, defaultValue: defaultValue, user: evalUser) { completion(defaultValue) return } + getSettings { result in - if result.isEmpty { - let message = String(format: "Config JSON is not present when evaluating setting '%@'. Returning the `defaultValue` parameter that you specified in your application: '%@'.", - key, "\(defaultValue)") - self.log.error(eventId: 1000, message: message) - self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, - value: defaultValue, - error: message, - user: evalUser)) - completion(defaultValue) - return - } - guard let setting = result.settings[key] else { - let message = String(format: "Failed to evaluate setting '%@' (the key was not found in config JSON). " - + "Returning the `defaultValue` parameter that you specified in your application: '%@'. Available keys: [%@].", - key, "\(defaultValue)", result.settings.keys.map { key in - return "'"+key+"'" - }.joined(separator: ", ")) - self.log.error(eventId: 1001, message: message) - self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, - value: defaultValue, - error: message, - user: evalUser)) - completion(defaultValue) - return - } - - let evalDetails = self.evaluate(setting: setting, key: key, user: evalUser, fetchTime: result.fetchTime) - completion(evalDetails.value as? Value ?? defaultValue) + let evalDetails = self.flagEvaluator.evaluateFlag(result: result, key: key, defaultValue: defaultValue, user: evalUser) + completion(evalDetails.value) } } @@ -259,70 +187,17 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { - Parameter completion: the function which will be called when the feature flag or setting is evaluated. */ public func getValueDetails(for key: String, defaultValue: Value, user: ConfigCatUser? = nil, completion: @escaping (TypedEvaluationDetails) -> ()) { - if key.isEmpty { - assert(false, "key cannot be empty") - } + assert(!key.isEmpty, "key cannot be empty") let evalUser = user ?? defaultUser - if Value.self != String.self && - Value.self != String?.self && - Value.self != Int.self && - Value.self != Int?.self && - Value.self != Double.self && - Value.self != Double?.self && - Value.self != Bool.self && - Value.self != Bool?.self && - Value.self != Any.self && - Value.self != Any?.self { - let message = "Only String, Integer, Double, Bool or Any types are supported." - log.error(eventId: 2022, message: message) - hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: evalUser)) - completion(TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: evalUser)) + + if let error = flagEvaluator.validateFlagType(of: Value.self, key: key, defaultValue: defaultValue, user: evalUser) { + completion(TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: error, user: evalUser)) return } + getSettings { result in - if result.isEmpty { - let message = String(format: "Config JSON is not present when evaluating setting '%@'. Returning the `defaultValue` parameter that you specified in your application: '%@'.", - key, "\(defaultValue)") - self.log.error(eventId: 1000, message: message) - self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: evalUser)) - completion(TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: evalUser)) - return - } - guard let setting = result.settings[key] else { - let message = String(format: "Failed to evaluate setting '%@' (the key was not found in config JSON). " - + "Returning the `defaultValue` parameter that you specified in your application: '%@'. Available keys: [%@].", - key, "\(defaultValue)", result.settings.keys.map { key in - return "'"+key+"'" - }.joined(separator: ", ")) - self.log.error(eventId: 1001, message: message) - self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, - value: defaultValue, - error: message, - user: evalUser)) - completion(TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: evalUser)) - return - } - - let details = self.evaluate(setting: setting, key: key, user: evalUser, fetchTime: result.fetchTime) - guard let typedValue = details.value as? Value else { - let message = String(format: "Failed to evaluate setting '%@' (the value '%@' cannot be converted to the requested type). " - + "Returning the `defaultValue` parameter that you specified in your application: '%@'.", - key, "\(details.value)", "\(defaultValue)") - self.log.error(eventId: 2002, message: message) - self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: evalUser)) - completion(TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: evalUser)) - return - } - - self.hooks.invokeOnFlagEvaluated(details: details) - completion(TypedEvaluationDetails(key: key, - value: typedValue, - variationId: details.variationId ?? "", - fetchTime: result.fetchTime, - user: user, - matchedEvaluationRule: details.matchedEvaluationRule, - matchedEvaluationPercentageRule: details.matchedEvaluationPercentageRule)) - + let evalDetails = self.flagEvaluator.evaluateFlag(result: result, key: key, defaultValue: defaultValue, user: evalUser) + completion(evalDetails) } } @@ -344,7 +219,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { guard let setting = result.settings[key] else { continue } - let details = self.evaluate(setting: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime) + let details = self.flagEvaluator.evaluateRules(for: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime) detailsResult.append(details) } completion(detailsResult) @@ -363,57 +238,6 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { } } - /// Gets the Variation ID (analytics) of a feature flag or setting based on it's key asynchronously. - @objc public func getVariationId(for key: String, defaultVariationId: String?, user: ConfigCatUser? = nil, completion: @escaping (String?) -> ()) { - if key.isEmpty { - assert(false, "key cannot be empty") - } - getSettings { result in - if result.isEmpty { - self.log.error(eventId: 1000, message: String(format: "Config JSON is not present when evaluating setting '%@'. Returning the `defaultVariationId` parameter that you specified in your application: '%@'.", - key, "\(defaultVariationId ?? "")")) - completion(defaultVariationId) - return - } - guard let setting = result.settings[key] else { - self.log.error(eventId: 1001, message: String(format: "Failed to evaluate setting '%@' (the key was not found in config JSON). " - + "Returning the `defaultVariationId` parameter that you specified in your application: '%@'. Available keys: [%@].", - key, "\(defaultVariationId ?? "")", result.settings.keys.map { key in - return "'"+key+"'" - }.joined(separator: ", "))) - completion(defaultVariationId) - return - } - - let details = self.evaluate(setting: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime) - completion(details.variationId ?? defaultVariationId) - } - } - - /// Gets the Variation IDs (analytics) of all feature flags or settings asynchronously. - @objc public func getAllVariationIds(user: ConfigCatUser? = nil, completion: @escaping ([String]) -> ()) { - getSettings { result in - if result.isEmpty { - self.log.error(eventId: 1000, message: "Config JSON is not present. Returning empty array.") - completion([]) - return - } - var variationIds = [String]() - for key in result.settings.keys { - guard let setting = result.settings[key] else { - continue - } - let details = self.evaluate(setting: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime) - if let variationId = details.variationId { - variationIds.append(variationId) - } else { - self.log.error(eventId: 2012, message: String(format: "Failed to evaluate the variation ID for the key '%@'.", key)) - } - } - completion(variationIds) - } - } - /// Gets the key of a setting and it's value identified by the given Variation ID (analytics) @objc public func getKeyAndValue(for variationId: String, completion: @escaping (KeyValue?) -> ()) { getSettings { result in @@ -459,30 +283,13 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { guard let setting = result.settings[key] else { continue } - let details = self.evaluate(setting: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime) + let details = self.flagEvaluator.evaluateRules(for: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime) allValues[key] = details.value } completion(allValues) } } - /** - Initiates a force refresh asynchronously on the cached configuration. - - - Parameter completion: the function which will be called when refresh completed successfully. - */ - @available(*, deprecated, message: "Use `forceRefresh()` instead") - @objc public func refresh(completion: @escaping () -> ()) { - if let configService = configService { - configService.refresh { _ in - completion() - } - } else { - log.warning(eventId: 3202, message: "Client is configured to use local-only mode, thus `.refresh()` has no effect.") - completion() - } - } - /** Initiates a force refresh asynchronously on the cached configuration. @@ -497,6 +304,26 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { completion(RefreshResult(success: false, error: message)) } } + + @objc public func snapshot() -> ConfigCatSnapshot { + return ConfigCatSnapshot(flagEvaluator: flagEvaluator, settingsSnapshot: getInMemorySettings(), defaultUser: defaultUser, log: log) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + @discardableResult + public func waitForReady() async -> ClientReadyState { + await withCheckedContinuation { continuation in + guard let configService = self.configService else { + continuation.resume(returning: .hasLocalOverrideFlagDataOnly) + return + } + configService.onReady { state in + continuation.resume(returning: state) + } + } + } + #endif func getSettings(completion: @escaping (SettingResult) -> Void) { if let overrideDataSource = overrideDataSource, overrideDataSource.behaviour == .localOnly { @@ -529,6 +356,32 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { completion(settings) } } + + func getInMemorySettings() -> SettingResult { + if let overrideDataSource = overrideDataSource, overrideDataSource.behaviour == .localOnly { + return SettingResult(settings: overrideDataSource.getOverrides(), fetchTime: .distantPast) + } + guard let configService = configService else { + return SettingResult.empty + } + + let inMemory = configService.inMemory + + if let overrideDataSource = overrideDataSource { + if overrideDataSource.behaviour == .localOverRemote { + return SettingResult(settings: inMemory.config.entries.merging(overrideDataSource.getOverrides()) { (_, new) in + new + }, fetchTime: inMemory.fetchTime) + } + if overrideDataSource.behaviour == .remoteOverLocal { + return SettingResult(settings: inMemory.config.entries.merging(overrideDataSource.getOverrides()) { (current, _) in + current + }, fetchTime: inMemory.fetchTime) + } + } + + return SettingResult(settings: inMemory.config.entries, fetchTime: inMemory.fetchTime) + } /// Sets the default user. @objc public func setDefaultUser(user: ConfigCatUser) { @@ -556,20 +409,4 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { configService?.isOffline ?? true } } - - func evaluate(setting: Setting, key: String, user: ConfigCatUser?, fetchTime: Date) -> EvaluationDetails { - let (value, variationId, evaluateLog, rolloutRule, percentageRule): (Any, String?, String?, RolloutRule?, PercentageRule?) = evaluator.evaluate(setting: setting, key: key, user: user) - if let evaluateLog = evaluateLog { - log.info(eventId: 5000, message: evaluateLog) - } - let details = EvaluationDetails(key: key, - value: value, - variationId: variationId, - fetchTime: fetchTime, - user: user, - matchedEvaluationRule: rolloutRule, - matchedEvaluationPercentageRule: percentageRule) - hooks.invokeOnFlagEvaluated(details: details) - return details - } } diff --git a/Sources/ConfigCat/ConfigCatClientProtocol.swift b/Sources/ConfigCat/ConfigCatClientProtocol.swift index d0f0acc..6287a94 100755 --- a/Sources/ConfigCat/ConfigCatClientProtocol.swift +++ b/Sources/ConfigCat/ConfigCatClientProtocol.swift @@ -33,14 +33,6 @@ public protocol ConfigCatClientProtocol { /// Gets all the setting keys asynchronously. func getAllKeys(completion: @escaping ([String]) -> ()) - /// Gets the Variation ID (analytics) of a feature flag or setting based on it's key asynchronously. - @available(*, deprecated, message: "This method is obsolete and will be removed in a future major version. Please use getValueDetails() instead.") - func getVariationId(for key: String, defaultVariationId: String?, user: ConfigCatUser?, completion: @escaping (String?) -> ()) - - /// Gets the Variation IDs (analytics) of all feature flags or settings asynchronously. - @available(*, deprecated, message: "This method is obsolete and will be removed in a future major version. Please use getAllValueDetails() instead.") - func getAllVariationIds(user: ConfigCatUser?, completion: @escaping ([String]) -> ()) - /// Gets the key of a setting and it's value identified by the given Variation ID (analytics) func getKeyAndValue(for variationId: String, completion: @escaping (KeyValue?) -> ()) @@ -62,20 +54,16 @@ public protocol ConfigCatClientProtocol { /// True when the SDK is configured not to initiate HTTP requests, otherwise false. var isOffline: Bool { get } - /** - Initiates a force refresh asynchronously on the cached configuration. - - - Parameter completion: the function which will be called when refresh completed successfully. - */ - @available(*, deprecated, message: "Use `forceRefresh()` instead") - func refresh(completion: @escaping () -> ()) - /** Initiates a force refresh asynchronously on the cached configuration. - Parameter completion: the function which will be called when refresh completed. */ func forceRefresh(completion: @escaping (RefreshResult) -> ()) + + /// Returns a snapshot of the current state of the feature flag data within the SDK. + /// The snapshot allows synchronous feature flag evaluation on the captured feature flag data. + func snapshot() -> ConfigCatSnapshot /// Async/await interface #if compiler(>=5.5) && canImport(_Concurrency) @@ -111,16 +99,6 @@ public protocol ConfigCatClientProtocol { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func getAllKeys() async -> [String] - /// Gets the Variation ID (analytics) of a feature flag or setting based on it's key asynchronously. - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - @available(*, deprecated, message: "This method is obsolete and will be removed in a future major version. Please use getValueDetails() instead.") - func getVariationId(for key: String, defaultVariationId: String?, user: ConfigCatUser?) async -> String? - - /// Gets the Variation IDs (analytics) of all feature flags or settings asynchronously. - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - @available(*, deprecated, message: "This method is obsolete and will be removed in a future major version. Please use getAllValueDetails() instead.") - func getAllVariationIds(user: ConfigCatUser?) async -> [String] - /// Gets the key of a setting and it's value identified by the given Variation ID (analytics) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func getKeyAndValue(for variationId: String) async -> KeyValue? @@ -129,14 +107,13 @@ public protocol ConfigCatClientProtocol { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func getAllValues(user: ConfigCatUser?) async -> [String: Any] - /// Initiates a force refresh asynchronously on the cached configuration. - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - @available(*, deprecated, message: "Use `forceRefresh()` instead") - func refresh() async - /// Initiates a force refresh asynchronously on the cached configuration. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func forceRefresh() async -> RefreshResult + + /// Awaits for SDK initialization. + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func waitForReady() async -> ClientReadyState #endif /// Objective-C interface diff --git a/Sources/ConfigCat/ConfigCatOptions.swift b/Sources/ConfigCat/ConfigCatOptions.swift index c49f6f0..5a04eb4 100644 --- a/Sources/ConfigCat/ConfigCatOptions.swift +++ b/Sources/ConfigCat/ConfigCatOptions.swift @@ -44,10 +44,23 @@ public final class ConfigCatOptions: NSObject { } } +/// Describes the initialization state of the `ConfigCatClient`. +@objc public enum ClientReadyState: Int { + /// The SDK has no feature flag data neither from the cache nor from the ConfigCat CDN. + case noFlagData + /// The SDK runs with local only feature flag data. + case hasLocalOverrideFlagDataOnly + /// The SDK has feature flag data to work with only from the cache. + case hasCachedFlagDataOnly + /// The SDK works with the latest feature flag data received from the ConfigCat CDN. + case hasUpToDateFlagData +} + /// Hooks for events sent by `ConfigCatClient`. public final class Hooks: NSObject { private let mutex: Mutex = Mutex(recursive: true); - private var onReady: [() -> ()] = [] + private var readyState: ClientReadyState? + private var onReady: [(ClientReadyState) -> ()] = [] private var onFlagEvaluated: [(EvaluationDetails) -> ()] = [] private var onConfigChanged: [([String: Setting]) -> ()] = [] private var onError: [(String) -> ()] = [] @@ -56,10 +69,14 @@ public final class Hooks: NSObject { Subscribes a handler to the `onReady` hook. - Parameter handler: the handler to subscribe. */ - @objc public func addOnReady(handler: @escaping () -> ()) { + @objc public func addOnReady(handler: @escaping (ClientReadyState) -> ()) { mutex.lock() defer { mutex.unlock() } - onReady.append(handler) + if let readyState = self.readyState { + handler(readyState) + } else { + onReady.append(handler) + } } /** @@ -92,11 +109,12 @@ public final class Hooks: NSObject { onError.append(handler) } - func invokeOnReady() { + func invokeOnReady(state: ClientReadyState) { mutex.lock() defer { mutex.unlock() } + readyState = state for item in onReady { - item(); + item(state); } } diff --git a/Sources/ConfigCat/ConfigCatSnapshot.swift b/Sources/ConfigCat/ConfigCatSnapshot.swift new file mode 100644 index 0000000..018c9ae --- /dev/null +++ b/Sources/ConfigCat/ConfigCatSnapshot.swift @@ -0,0 +1,64 @@ +import Foundation + +public final class ConfigCatSnapshot: NSObject { + private let flagEvaluator: FlagEvaluator + private let settingsSnapshot: SettingResult + private let defaultUser: ConfigCatUser? + private let log: Logger + + init(flagEvaluator: FlagEvaluator, settingsSnapshot: SettingResult, defaultUser: ConfigCatUser?, log: Logger) { + self.flagEvaluator = flagEvaluator + self.settingsSnapshot = settingsSnapshot + self.defaultUser = defaultUser + self.log = log + } + + /** + Gets the value of a feature flag or setting identified by the given `key`. + + - Parameter key: the identifier of the feature flag or setting. + - Parameter defaultValue: in case of any failure, this value will be returned. + - Parameter user: the user object to identify the caller. + - Returns: The evaluated feature flag value. + */ + public func getValue(for key: String, defaultValue: Value, user: ConfigCatUser? = nil) -> Value { + assert(!key.isEmpty, "key cannot be empty") + let evalUser = user ?? defaultUser + + if let _ = flagEvaluator.validateFlagType(of: Value.self, key: key, defaultValue: defaultValue, user: evalUser) { + return defaultValue + } + + let evalDetails = self.flagEvaluator.evaluateFlag(result: settingsSnapshot, key: key, defaultValue: defaultValue, user: evalUser) + return evalDetails.value + } + + /** + Gets the value and evaluation details of a feature flag or setting identified by the given `key`. + + - Parameter key: the identifier of the feature flag or setting. + - Parameter defaultValue: in case of any failure, this value will be returned. + - Parameter user: the user object to identify the caller. + - Returns: The evaluation details. + */ + public func getValueDetails(for key: String, defaultValue: Value, user: ConfigCatUser? = nil) -> TypedEvaluationDetails { + assert(!key.isEmpty, "key cannot be empty") + let evalUser = user ?? defaultUser + + if let error = flagEvaluator.validateFlagType(of: Value.self, key: key, defaultValue: defaultValue, user: evalUser) { + return TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: error, user: evalUser) + } + + let evalDetails = self.flagEvaluator.evaluateFlag(result: settingsSnapshot, key: key, defaultValue: defaultValue, user: evalUser) + return evalDetails + } + + /// Gets all the setting keys within the snapshot. + @objc public func getAllKeys() -> [String] { + if settingsSnapshot.isEmpty { + log.error(eventId: 1000, message: "Config JSON is not present. Returning empty array.") + return [] + } + return [String](settingsSnapshot.settings.keys) + } +} diff --git a/Sources/ConfigCat/ConfigFetcher.swift b/Sources/ConfigCat/ConfigFetcher.swift index c5f402d..9ca8272 100755 --- a/Sources/ConfigCat/ConfigFetcher.swift +++ b/Sources/ConfigCat/ConfigFetcher.swift @@ -137,10 +137,10 @@ class ConfigFetcher: NSObject { self.log.debug(message: "Fetch was successful: new config fetched") let etag = response.allHeaderFields["Etag"] as? String ?? "" let jsonString = String(data: data, encoding: .utf8) ?? "" - let configResult = self.parseConfigFromJson(json: jsonString) - switch configResult { - case .success(let config): - completion(.fetched(ConfigEntry(config: config, eTag: etag, fetchTime: Date()))) + let result = ConfigEntry.fromConfigJson(json: jsonString, eTag: etag, fetchTime: Date()) + switch result { + case .success(let entry): + completion(.fetched(entry)) case .failure(let error): let message = String(format: "Fetching config JSON was successful but the HTTP response content was invalid. " + "JSON parsing failed. %@", error.localizedDescription) @@ -167,7 +167,7 @@ class ConfigFetcher: NSObject { } private func getRequest(url: String, eTag: String) -> URLRequest { - var request = URLRequest(url: URL(string: url + "/configuration-files/" + sdkKey + "/" + Constants.configJsonName + ".json")!) + var request = URLRequest(url: URL(string: url + "/configuration-files/" + sdkKey + "/" + Constants.configJsonName)!) request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData request.addValue("ConfigCat-Swift/" + mode + "-" + Constants.version, forHTTPHeaderField: "X-ConfigCat-UserAgent") if !eTag.isEmpty { @@ -175,18 +175,4 @@ class ConfigFetcher: NSObject { } return request } - - private func parseConfigFromJson(json: String) -> Result { - do { - guard let data = json.data(using: .utf8) else { - return .failure(ParseError(message: "Decode to utf8 data failed.")) - } - guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - return .failure(ParseError(message: "Convert to [String: Any] map failed.")) - } - return .success(Config.fromJson(json: jsonObject)) - } catch { - return .failure(error) - } - } } diff --git a/Sources/ConfigCat/ConfigService.swift b/Sources/ConfigCat/ConfigService.swift index 115f124..a4452a1 100644 --- a/Sources/ConfigCat/ConfigService.swift +++ b/Sources/ConfigCat/ConfigService.swift @@ -56,8 +56,7 @@ class ConfigService { self.pollingMode = pollingMode self.hooks = hooks self.offline = offline - let keyToHash = "swift_" + Constants.configJsonName + "_" + sdkKey - cacheKey = String(keyToHash.sha1hex ?? keyToHash) + cacheKey = Utils.generateCacheKey(sdkKey: sdkKey) if let autoPoll = pollingMode as? AutoPollingMode, !offline { @@ -78,13 +77,15 @@ class ConfigService { if !this.initialized { this.log.warning(eventId: 4200, message: String(format: "`maxInitWaitTimeInSeconds` for the very first fetch reached (%ds). Returning cached config.", autoPoll.maxInitWaitTimeInSeconds)) this.initialized = true - hooks.invokeOnReady() + hooks.invokeOnReady(state: this.determineReadyState()) this.callCompletions(result: .success(this.cachedEntry)) this.completions = nil } }) initTimer?.resume() } else { + // Sync up with cache before reporting ready state + cachedEntry = readCache() setInitialized() } } @@ -100,8 +101,8 @@ class ConfigService { func settings(completion: @escaping (SettingResult) -> Void) { switch pollingMode { - case let lazy as LazyLoadingMode: - fetchIfOlder(time: Date().subtract(seconds: lazy.cacheRefreshIntervalInSeconds)!) { result in + case let lazyMode as LazyLoadingMode: + fetchIfOlder(time: Date().subtract(seconds: lazyMode.cacheRefreshIntervalInSeconds)!) { result in switch result { case .success(let entry): completion(!entry.isEmpty ? SettingResult(settings: entry.config.entries, fetchTime: entry.fetchTime) @@ -169,6 +170,24 @@ class ConfigService { return offline } } + + func onReady(completion: @escaping (ClientReadyState) -> Void) { + mutex.lock() + defer { mutex.unlock() } + if initialized { + completion(determineReadyState()) + } else { + hooks.addOnReady(handler: completion) + } + } + + var inMemory: ConfigEntry { + get { + mutex.lock() + defer { mutex.unlock() } + return cachedEntry + } + } private func fetchIfOlder(time: Date, preferCache: Bool = false, completion: @escaping (FetchResult) -> Void) { mutex.lock() @@ -216,14 +235,10 @@ class ConfigService { mutex.lock() defer { mutex.unlock() } - setInitialized() switch response { case .fetched(let entry): cachedEntry = entry writeCache(entry: entry) - if let auto = pollingMode as? AutoPollingMode { - auto.onConfigChanged?() - } hooks.invokeOnConfigChanged(settings: entry.config.entries) callCompletions(result: .success(entry)) case .notModified: @@ -238,12 +253,13 @@ class ConfigService { callCompletions(result: .failure(error, cachedEntry)) } completions = nil + setInitialized() } private func setInitialized() { if !initialized { initialized = true - hooks.invokeOnReady() + hooks.invokeOnReady(state: determineReadyState()) } } @@ -279,12 +295,7 @@ class ConfigService { return } do { - let jsonMap = entry.toJsonMap() - let json = try JSONSerialization.data(withJSONObject: jsonMap, options: []) - guard let jsonString = String(data: json, encoding: .utf8) else { - log.error(eventId: 2201, message: "Error occurred while writing the cache. Could not convert the JSON object to string.") - return - } + let jsonString = entry.serialize() cachedJsonString = jsonString try cache.write(for: cacheKey, value: jsonString) } catch { @@ -301,19 +312,39 @@ class ConfigService { if json.isEmpty || json == cachedJsonString { return .empty } - guard let data = json.data(using: .utf8) else { - log.error(eventId: 2200, message: "Error occurred while reading the cache. Decode to utf8 data failed.") + let cached = ConfigEntry.fromCached(cached: json) + switch cached { + case .success(let entry): + cachedJsonString = json + return entry + case .failure(let error): + log.error(eventId: 2200, message: String(format: "Error occurred while reading the cache. %@", error.localizedDescription)) return .empty } - guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - log.error(eventId: 2200, message: "Error occurred while reading the cache. Convert to [String: Any] map failed.") - return .empty - } - cachedJsonString = json - return .fromJson(json: jsonObject) } catch { log.error(eventId: 2200, message: String(format: "Error occurred while reading the cache. %@", error.localizedDescription)) return .empty } } + + private func determineReadyState() -> ClientReadyState { + if cachedEntry.isEmpty { + return .noFlagData + } + + switch pollingMode { + case let lazyMode as LazyLoadingMode: + if cachedEntry.isExpired(seconds: lazyMode.cacheRefreshIntervalInSeconds) { + return .hasCachedFlagDataOnly + } + case let autoMode as AutoPollingMode: + if cachedEntry.isExpired(seconds: autoMode.autoPollIntervalInSeconds) { + return .hasCachedFlagDataOnly + } + default: // manual polling + return .hasCachedFlagDataOnly + } + + return .hasUpToDateFlagData + } } diff --git a/Sources/ConfigCat/Extensions.swift b/Sources/ConfigCat/Extensions.swift index 3d1279e..15775c9 100644 --- a/Sources/ConfigCat/Extensions.swift +++ b/Sources/ConfigCat/Extensions.swift @@ -96,26 +96,6 @@ extension ConfigCatClient { } } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - @available(*, deprecated, message: "This method is obsolete and will be removed in a future major version. Please use getValueDetails() instead.") - public func getVariationId(for key: String, defaultVariationId: String?, user: ConfigCatUser? = nil) async -> String? { - await withCheckedContinuation { continuation in - getVariationId(for: key, defaultVariationId: defaultVariationId, user: user) { variationId in - continuation.resume(returning: variationId) - } - } - } - - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - @available(*, deprecated, message: "This method is obsolete and will be removed in a future major version. Please use getAllValueDetails() instead.") - public func getAllVariationIds(user: ConfigCatUser? = nil) async -> [String] { - await withCheckedContinuation { continuation in - getAllVariationIds(user: user) { variationIds in - continuation.resume(returning: variationIds) - } - } - } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public func getKeyAndValue(for variationId: String) async -> KeyValue? { await withCheckedContinuation { continuation in @@ -134,16 +114,6 @@ extension ConfigCatClient { } } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - @available(*, deprecated, message: "Use `forceRefresh()` instead") - public func refresh() async { - await withCheckedContinuation { continuation in - forceRefresh { _ in - continuation.resume() - } - } - } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @discardableResult public func forceRefresh() async -> RefreshResult { @@ -154,169 +124,55 @@ extension ConfigCatClient { } } #endif +} - // Synchronous extensions - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getValue() instead.") - public func getValueSync(for key: String, defaultValue: Value, user: ConfigCatUser? = nil) -> Value { - let semaphore = DispatchSemaphore(value: 0) - var result: Value? - getValue(for: key, defaultValue: defaultValue, user: user) { value in - result = value - semaphore.signal() - } - semaphore.wait() - return result ?? defaultValue - } - - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getValueDetails() instead.") - public func getValueDetailsSync(for key: String, defaultValue: Value, user: ConfigCatUser? = nil) -> TypedEvaluationDetails { - let semaphore = DispatchSemaphore(value: 0) - var result: TypedEvaluationDetails? - getValueDetails(for: key, defaultValue: defaultValue, user: user) { value in - result = value - semaphore.signal() - } - semaphore.wait() - return result ?? TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: String(format: "Could not get the evaluation details for '%@'.", key), user: user) - } - - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getAllValueDetails() instead.") - @objc public func getAllValueDetailsSync(user: ConfigCatUser? = nil) -> [EvaluationDetails] { - let semaphore = DispatchSemaphore(value: 0) - var result = [EvaluationDetails]() - getAllValueDetails(user: user) { details in - result = details - semaphore.signal() - } - semaphore.wait() - return result - } - - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getAllKeys() instead.") - @objc public func getAllKeysSync() -> [String] { - let semaphore = DispatchSemaphore(value: 0) - var result = [String]() - getAllKeys { keys in - result = keys - semaphore.signal() - } - semaphore.wait() - return result - } - - @available(*, deprecated, message: "This method is obsolete and will be removed in a future major version. Please use getValueDetailsSync() instead.") - @objc public func getVariationIdSync(for key: String, defaultVariationId: String?, user: ConfigCatUser? = nil) -> String? { - let semaphore = DispatchSemaphore(value: 0) - var result: String? - getVariationId(for: key, defaultVariationId: defaultVariationId, user: user) { variationId in - result = variationId - semaphore.signal() - } - semaphore.wait() - return result ?? defaultVariationId - } - - @available(*, deprecated, message: "This method is obsolete and will be removed in a future major version. Please use getAllValueDetailsSync() instead.") - @objc public func getAllVariationIdsSync(user: ConfigCatUser? = nil) -> [String] { - let semaphore = DispatchSemaphore(value: 0) - var result = [String]() - getAllVariationIds(user: user) { variationIds in - result = variationIds - semaphore.signal() - } - semaphore.wait() - return result - } - - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getKeyAndValue() instead.") - @objc public func getKeyAndValueSync(for variationId: String) -> KeyValue? { - let semaphore = DispatchSemaphore(value: 0) - var result: KeyValue? - getKeyAndValue(for: variationId) { value in - result = value - semaphore.signal() - } - semaphore.wait() - return result - } - - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getAllValues() instead.") - @objc public func getAllValuesSync(user: ConfigCatUser? = nil) -> [String: Any] { - let semaphore = DispatchSemaphore(value: 0) - var result = [String: Any]() - getAllValues(user: user) { values in - result = values - semaphore.signal() - } - semaphore.wait() - return result - } - - @available(*, deprecated, message: "Use `forceRefreshSync()` instead") - @objc public func refreshSync() { - let semaphore = DispatchSemaphore(value: 0) - forceRefresh { _ in - semaphore.signal() - } - semaphore.wait() - } - - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous forceRefresh() instead.") - @discardableResult - @objc public func forceRefreshSync() -> RefreshResult { - let semaphore = DispatchSemaphore(value: 0) - var refreshResult: RefreshResult? - forceRefresh { result in - refreshResult = result - semaphore.signal() - } - semaphore.wait() - return refreshResult ?? RefreshResult(success: false) +extension ConfigCatSnapshot { + @objc public func getStringValue(for key: String, defaultValue: String, user: ConfigCatUser?) -> String { + return getValue(for: key, defaultValue: defaultValue, user: user) } - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getStringValue() instead.") - @objc public func getStringValueSync(for key: String, defaultValue: String, user: ConfigCatUser?) -> String { - getValueSync(for: key, defaultValue: defaultValue, user: user) + @objc public func getIntValue(for key: String, defaultValue: Int, user: ConfigCatUser?) -> Int { + return getValue(for: key, defaultValue: defaultValue, user: user) } - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getIntValue() instead.") - @objc public func getIntValueSync(for key: String, defaultValue: Int, user: ConfigCatUser?) -> Int { - getValueSync(for: key, defaultValue: defaultValue, user: user) + @objc public func getDoubleValue(for key: String, defaultValue: Double, user: ConfigCatUser?) -> Double { + return getValue(for: key, defaultValue: defaultValue, user: user) } - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getDoubleValue() instead.") - @objc public func getDoubleValueSync(for key: String, defaultValue: Double, user: ConfigCatUser?) -> Double { - getValueSync(for: key, defaultValue: defaultValue, user: user) + @objc public func getBoolValue(for key: String, defaultValue: Bool, user: ConfigCatUser?) -> Bool { + return getValue(for: key, defaultValue: defaultValue, user: user) } - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getBoolValue() instead.") - @objc public func getBoolValueSync(for key: String, defaultValue: Bool, user: ConfigCatUser?) -> Bool { - getValueSync(for: key, defaultValue: defaultValue, user: user) + @objc public func getAnyValue(for key: String, defaultValue: Any, user: ConfigCatUser?) -> Any { + return getValue(for: key, defaultValue: defaultValue, user: user) } - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getAnyValue() instead.") - @objc public func getAnyValueSync(for key: String, defaultValue: Any, user: ConfigCatUser?) -> Any { - getValueSync(for: key, defaultValue: defaultValue, user: user) + @objc public func getAnyValueDetails(for key: String, defaultValue: Any, user: ConfigCatUser?) -> EvaluationDetails { + let details = getValueDetails(for: key, defaultValue: defaultValue, user: user) + return EvaluationDetails(key: details.key, + value: details.value, + variationId: details.variationId, + fetchTime: details.fetchTime, + user: user, + isDefaultValue: details.isDefaultValue, + error: details.error, + matchedEvaluationRule: details.matchedEvaluationRule, + matchedEvaluationPercentageRule: details.matchedEvaluationPercentageRule) } - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getStringValueDetails() instead.") - @objc public func getStringValueDetailsSync(for key: String, defaultValue: String, user: ConfigCatUser?) -> StringEvaluationDetails { - getValueDetailsSync(for: key, defaultValue: defaultValue, user: user).toStringDetails() + @objc public func getStringValueDetails(for key: String, defaultValue: String, user: ConfigCatUser?) -> StringEvaluationDetails { + return getValueDetails(for: key, defaultValue: defaultValue, user: user).toStringDetails() } - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getIntValueDetails() instead.") - @objc public func getIntValueDetailsSync(for key: String, defaultValue: Int, user: ConfigCatUser?) -> IntEvaluationDetails { - getValueDetailsSync(for: key, defaultValue: defaultValue, user: user).toIntDetails() + @objc public func getBoolValueDetails(for key: String, defaultValue: Bool, user: ConfigCatUser?) -> BoolEvaluationDetails { + return getValueDetails(for: key, defaultValue: defaultValue, user: user).toBoolDetails() } - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getDoubleValueDetails() instead.") - @objc public func getDoubleValueDetailsSync(for key: String, defaultValue: Double, user: ConfigCatUser?) -> DoubleEvaluationDetails { - getValueDetailsSync(for: key, defaultValue: defaultValue, user: user).toDoubleDetails() + @objc public func getIntValueDetails(for key: String, defaultValue: Int, user: ConfigCatUser?) -> IntEvaluationDetails { + return getValueDetails(for: key, defaultValue: defaultValue, user: user).toIntDetails() } - @available(*, deprecated, message: "This method can produce thread priority inversion due to the usage of DispatchSemaphore for call synchronization. It will be removed in a future major version. Please use the asynchronous getBoolValueDetails() instead.") - @objc public func getBoolValueDetailsSync(for key: String, defaultValue: Bool, user: ConfigCatUser?) -> BoolEvaluationDetails { - getValueDetailsSync(for: key, defaultValue: defaultValue, user: user).toBoolDetails() + @objc public func getDoubleValueDetails(for key: String, defaultValue: Double, user: ConfigCatUser?) -> DoubleEvaluationDetails { + return getValueDetails(for: key, defaultValue: defaultValue, user: user).toDoubleDetails() } } - diff --git a/Sources/ConfigCat/FlagEvaluator.swift b/Sources/ConfigCat/FlagEvaluator.swift new file mode 100644 index 0000000..ef4b6b2 --- /dev/null +++ b/Sources/ConfigCat/FlagEvaluator.swift @@ -0,0 +1,99 @@ +import Foundation + +class FlagEvaluator { + private let log: Logger + private let evaluator: RolloutEvaluator + private let hooks: Hooks + + init(log: Logger, evaluator: RolloutEvaluator, hooks: Hooks) { + self.log = log + self.evaluator = evaluator + self.hooks = hooks + } + + func validateFlagType(of: Value.Type, key: String, defaultValue: Any, user: ConfigCatUser?) -> String? { + if of != String.self && + of != String?.self && + of != Int.self && + of != Int?.self && + of != Double.self && + of != Double?.self && + of != Bool.self && + of != Bool?.self && + of != Any.self && + of != Any?.self { + let message = "Only String, Integer, Double, Bool or Any types are supported." + log.error(eventId: 2022, message: message) + hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, + value: defaultValue, + error: message, + user: user)) + return message + } + + return nil + } + + func evaluateFlag(result: SettingResult, key: String, defaultValue: Value, user: ConfigCatUser?) -> TypedEvaluationDetails { + if result.settings.isEmpty { + let message = String(format: "Config JSON is not present when evaluating setting '%@'. Returning the `defaultValue` parameter that you specified in your application: '%@'.", + key, "\(defaultValue)") + self.log.error(eventId: 1000, message: message) + self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, + value: defaultValue, + error: message, + user: user)) + return TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: user) + } + guard let setting = result.settings[key] else { + let message = String(format: "Failed to evaluate setting '%@' (the key was not found in config JSON). " + + "Returning the `defaultValue` parameter that you specified in your application: '%@'. Available keys: [%@].", key, "\(defaultValue)", result.settings.keys.map { key in + return "'"+key+"'" + }.joined(separator: ", ")) + self.log.error(eventId: 1001, message: message) + self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, + value: defaultValue, + error: message, + user: user)) + return TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: user) + } + + let evaluationResult = self.evaluateRules(for: setting, key: key, user: user, fetchTime: result.fetchTime) + + guard let typedValue = evaluationResult.value as? Value else { + let message = String(format: "Failed to evaluate setting '%@' (the value '%@' cannot be converted to the requested type). " + + "Returning the `defaultValue` parameter that you specified in your application: '%@'.", + key, "\(evaluationResult.value)", "\(defaultValue)") + self.log.error(eventId: 2002, message: message) + self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, + value: defaultValue, + error: message, + user: user)) + return TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: user) + } + + return TypedEvaluationDetails(key: key, + value: typedValue, + variationId: evaluationResult.variationId ?? "", + fetchTime: result.fetchTime, + user: user, + matchedEvaluationRule: evaluationResult.matchedEvaluationRule, + matchedEvaluationPercentageRule: evaluationResult.matchedEvaluationPercentageRule) + } + + func evaluateRules(for setting: Setting, key: String, user: ConfigCatUser?, fetchTime: Date) -> EvaluationDetails { + let (value, variationId, evaluateLog, rolloutRule, percentageRule): (Any, String?, String?, RolloutRule?, PercentageRule?) = evaluator.evaluate(setting: setting, key: key, user: user) + if let evaluateLog = evaluateLog { + log.info(eventId: 5000, message: evaluateLog) + } + let details = EvaluationDetails(key: key, + value: value, + variationId: variationId, + fetchTime: fetchTime, + user: user, + matchedEvaluationRule: rolloutRule, + matchedEvaluationPercentageRule: percentageRule) + hooks.invokeOnFlagEvaluated(details: details) + return details + } +} diff --git a/Sources/ConfigCat/PollingMode.swift b/Sources/ConfigCat/PollingMode.swift index 885c83e..3a86b47 100644 --- a/Sources/ConfigCat/PollingMode.swift +++ b/Sources/ConfigCat/PollingMode.swift @@ -8,16 +8,14 @@ import Foundation class AutoPollingMode: PollingMode { let autoPollIntervalInSeconds: Int let maxInitWaitTimeInSeconds: Int - let onConfigChanged: ConfigCatClient.ConfigChangedHandler? - init(autoPollIntervalInSeconds: Int, maxInitWaitTimeInSeconds: Int, onConfigChanged: ConfigCatClient.ConfigChangedHandler? = nil) { + init(autoPollIntervalInSeconds: Int, maxInitWaitTimeInSeconds: Int) { self.autoPollIntervalInSeconds = autoPollIntervalInSeconds < 1 ? 1 : autoPollIntervalInSeconds self.maxInitWaitTimeInSeconds = maxInitWaitTimeInSeconds < 1 ? 1 : maxInitWaitTimeInSeconds - self.onConfigChanged = onConfigChanged } var identifier: String { diff --git a/Sources/ConfigCat/PollingModes.swift b/Sources/ConfigCat/PollingModes.swift index 91d97b0..a8b2604 100644 --- a/Sources/ConfigCat/PollingModes.swift +++ b/Sources/ConfigCat/PollingModes.swift @@ -4,19 +4,6 @@ import Foundation public final class PollingModes: NSObject { /** Creates a new `AutoPollingMode`. - - - Parameter autoPollIntervalInSeconds: the poll interval in seconds. - - Parameter maxInitWaitTimeInSeconds: maximum waiting time between initialization and the first config acquisition in seconds. - - Parameter onConfigChanged: the configuration changed event handler. - - Returns: A new `AutoPollingMode`. - */ - @available(*, deprecated, message: "For subscribing the `onConfigChanged` event, use the `hooks` property of `ConfigCatClient`") - @objc public static func autoPoll(autoPollIntervalInSeconds: Int = 60, maxInitWaitTimeInSeconds: Int = 5, onConfigChanged: ConfigCatClient.ConfigChangedHandler? = nil) -> PollingMode { - AutoPollingMode(autoPollIntervalInSeconds: autoPollIntervalInSeconds, maxInitWaitTimeInSeconds: maxInitWaitTimeInSeconds, onConfigChanged: onConfigChanged) - } - - /** - Creates a new `AutoPollingMode`. - Parameter autoPollIntervalInSeconds: the poll interval in seconds. - Parameter maxInitWaitTimeInSeconds: maximum waiting time between initialization and the first config acquisition in seconds. diff --git a/Sources/ConfigCat/Utils.swift b/Sources/ConfigCat/Utils.swift index 4ae2f59..f0c6725 100644 --- a/Sources/ConfigCat/Utils.swift +++ b/Sources/ConfigCat/Utils.swift @@ -38,8 +38,16 @@ extension Date { } class Constants { - static let version: String = "9.4.0" - static let configJsonName: String = "config_v5" + static let version: String = "10.0.0" + static let configJsonName: String = "config_v5.json" + static let configJsonCacheVersion: String = "v2" static let globalBaseUrl: String = "https://cdn-global.configcat.com" static let euOnlyBaseUrl: String = "https://cdn-eu.configcat.com" } + +class Utils { + public static func generateCacheKey(sdkKey: String) -> String { + let keyToHash = sdkKey + "_" + Constants.configJsonName + "_" + Constants.configJsonCacheVersion + return String(keyToHash.sha1hex ?? keyToHash) + } +} diff --git a/Tests/ConfigCatTests/AsyncAwaitTests.swift b/Tests/ConfigCatTests/AsyncAwaitTests.swift index c4af34a..9c839c5 100644 --- a/Tests/ConfigCatTests/AsyncAwaitTests.swift +++ b/Tests/ConfigCatTests/AsyncAwaitTests.swift @@ -24,10 +24,10 @@ class AsyncAwaitTests: XCTestCase { engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) - let id = await client.getVariationId(for: "key1", defaultVariationId: "") - XCTAssertEqual("fakeId1", id) - let id2 = await client.getVariationId(for: "key2", defaultVariationId: "", user: user) - XCTAssertEqual("9f21c24c", id2) + let details = await client.getValueDetails(for: "key1", defaultValue: false) + XCTAssertEqual("fakeId1", details.variationId) + let details2 = await client.getValueDetails(for: "key2", defaultValue: false, user: user) + XCTAssertEqual("9f21c24c", details2.variationId) } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -107,5 +107,95 @@ class AsyncAwaitTests: XCTestCase { XCTAssertFalse(details.value) XCTAssertEqual(1, engine.requests.count) } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testReadyCache() async { + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) + + let initValue = testJsonMultiple.asEntryString() + let cache = SingleValueCache(initValue: initValue) + + let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine, configCache: cache) + + let state = await client.waitForReady() + + XCTAssertEqual(ClientReadyState.hasUpToDateFlagData, state) + XCTAssertEqual(0, engine.requests.count) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testReadyExpiredCacheDownload() async { + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) + + let initValue = testJsonMultiple.asEntryString(date: Date().subtract(minutes: 5)!) + let cache = SingleValueCache(initValue: initValue) + + let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine, configCache: cache) + + let state = await client.waitForReady() + + XCTAssertEqual(ClientReadyState.hasUpToDateFlagData, state) + XCTAssertEqual(1, engine.requests.count) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testReadyExpiredCacheFailedDownload() async { + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 400)) + + let initValue = testJsonMultiple.asEntryString(date: Date().subtract(minutes: 5)!) + let cache = SingleValueCache(initValue: initValue) + + let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine, configCache: cache) + + let state = await client.waitForReady() + + XCTAssertEqual(ClientReadyState.hasCachedFlagDataOnly, state) + XCTAssertEqual(1, engine.requests.count) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testReadyNoCacheFailedDownload() async { + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 400)) + + let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + + let state = await client.waitForReady() + + XCTAssertEqual(ClientReadyState.noFlagData, state) + XCTAssertEqual(1, engine.requests.count) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testReadyManualNoCacheFailedDownload() async { + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 400)) + + let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) + + let state = await client.waitForReady() + + XCTAssertEqual(ClientReadyState.noFlagData, state) + XCTAssertEqual(0, engine.requests.count) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testReadyManualCached() async { + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 400)) + + let initValue = testJsonMultiple.asEntryString() + let cache = SingleValueCache(initValue: initValue) + + let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine, configCache: cache) + + let state = await client.waitForReady() + + XCTAssertEqual(ClientReadyState.hasCachedFlagDataOnly, state) + XCTAssertEqual(0, engine.requests.count) + } #endif } diff --git a/Tests/ConfigCatTests/AutoPollingTests.swift b/Tests/ConfigCatTests/AutoPollingTests.swift index 3eb5e21..186a958 100755 --- a/Tests/ConfigCatTests/AutoPollingTests.swift +++ b/Tests/ConfigCatTests/AutoPollingTests.swift @@ -286,8 +286,9 @@ class AutoPollingTests: XCTestCase { var ready = false let hooks = Hooks() - hooks.addOnReady { + hooks.addOnReady { state in ready = true + XCTAssertEqual(ClientReadyState.hasUpToDateFlagData, state) } let initValue = String(format: testJsonFormat, "test").asEntryString() let cache = SingleValueCache(initValue: initValue) diff --git a/Tests/ConfigCatTests/CacheTest.swift b/Tests/ConfigCatTests/CacheTest.swift new file mode 100644 index 0000000..a007e1b --- /dev/null +++ b/Tests/ConfigCatTests/CacheTest.swift @@ -0,0 +1,28 @@ +import XCTest +@testable import ConfigCat + +class CacheTests: XCTestCase { + func testCacheKeys() { + XCTAssertEqual("147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6", Utils.generateCacheKey(sdkKey: "test1")) + XCTAssertEqual("c09513b1756de9e4bc48815ec7a142b2441ed4d5", Utils.generateCacheKey(sdkKey: "test2")) + } + + func testPayloads() { + let testJson = "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0},\"f\":{\"testKey\":{\"v\":\"testValue\",\"t\":1,\"p\":[],\"r\":[]}}}" + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + let time = formatter.date(from: "2023-06-14T15:27:15.8440000Z")! + + let expectedPayload = "1686756435844\ntest-etag\n" + testJson + + let entry = try! ConfigEntry.fromConfigJson(json: testJson, eTag: "test-etag", fetchTime: time).get() + + XCTAssertEqual(expectedPayload, entry.serialize()) + + let fromCache = try! ConfigEntry.fromCached(cached: expectedPayload).get() + + XCTAssertEqual(time, fromCache.fetchTime) + XCTAssertEqual(testJson, fromCache.configJson) + XCTAssertEqual("test-etag", fromCache.eTag) + } +} diff --git a/Tests/ConfigCatTests/ConfigCatClientTests.swift b/Tests/ConfigCatTests/ConfigCatClientTests.swift index 56f0b3f..540d5cf 100755 --- a/Tests/ConfigCatTests/ConfigCatClientTests.swift +++ b/Tests/ConfigCatTests/ConfigCatClientTests.swift @@ -233,9 +233,8 @@ class ConfigCatClientTests: XCTestCase { let engine = MockEngine() let cache = InMemoryConfigCache() let sdkKey = "test" - let keyToHash = "swift_" + Constants.configJsonName + "_" + sdkKey - let cacheKey = String(keyToHash.sha1hex ?? keyToHash) - try cache.write(for: cacheKey, value: String(format: testJsonFormat, "\"fake\"").toEntryFromConfigString().toJsonString()) + let cacheKey = Utils.generateCacheKey(sdkKey: sdkKey) + try cache.write(for: cacheKey, value: String(format: testJsonFormat, "\"fake\"").toEntryFromConfigString().serialize()) engine.enqueueResponse(response: Response(body: "", statusCode: 500)) let client = ConfigCatClient(sdkKey: sdkKey, pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), httpEngine: engine, configCache: cache) @@ -251,9 +250,8 @@ class ConfigCatClientTests: XCTestCase { let engine = MockEngine() let cache = InMemoryConfigCache() let sdkKey = "test" - let keyToHash = "swift_" + Constants.configJsonName + "_" + sdkKey - let cacheKey = String(keyToHash.sha1hex ?? keyToHash) - try cache.write(for: cacheKey, value: String(format: testJsonFormat, "\"fake\"").toEntryFromConfigString().toJsonString()) + let cacheKey = Utils.generateCacheKey(sdkKey: sdkKey) + try cache.write(for: cacheKey, value: String(format: testJsonFormat, "\"fake\"").toEntryFromConfigString().serialize()) engine.enqueueResponse(response: Response(body: "", statusCode: 500)) let client = ConfigCatClient(sdkKey: sdkKey, pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), httpEngine: engine, configCache: cache) @@ -440,9 +438,11 @@ class ConfigCatClientTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) var ready = false + var state = ClientReadyState.hasUpToDateFlagData let hooks = Hooks() - hooks.addOnReady { + hooks.addOnReady { st in ready = true + state = st } let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine, hooks: hooks, offline: true) let expectation = self.expectation(description: "wait for response") @@ -453,6 +453,7 @@ class ConfigCatClientTests: XCTestCase { XCTAssertEqual(0, engine.requests.count) XCTAssertTrue(ready) + XCTAssertEqual(ClientReadyState.noFlagData, state) } func testDefaultUser() { @@ -505,8 +506,9 @@ class ConfigCatClientTests: XCTestCase { hooks.addOnError { e in error = e } - hooks.addOnReady { + hooks.addOnReady { state in ready = true + XCTAssertEqual(ClientReadyState.noFlagData, state) } hooks.addOnConfigChanged { _ in changed = true @@ -528,6 +530,19 @@ class ConfigCatClientTests: XCTestCase { waitFor { changed && ready && error.starts(with: "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey.") && error.contains("404") } + + let expectation3 = self.expectation(description: "wait for ready second time") + client.hooks.addOnReady { state in + XCTAssertEqual(ClientReadyState.noFlagData, state) + expectation3.fulfill() + } + let expectation4 = self.expectation(description: "wait for ready third time") + client.hooks.addOnReady { state in + XCTAssertEqual(ClientReadyState.noFlagData, state) + expectation4.fulfill() + } + + wait(for: [expectation3, expectation4], timeout: 5) } func testHooksSub() { @@ -563,11 +578,11 @@ class ConfigCatClientTests: XCTestCase { } } - func testDefaultCache() { + func testLazyCache() throws { let engine = MockEngine() - let cache = UserDefaultsCache() + let cache = InMemoryConfigCache() engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) - let client = ConfigCatClient(sdkKey: "testDefaultCache", pollingMode: PollingModes.lazyLoad(), httpEngine: engine, configCache: cache) + let client = ConfigCatClient(sdkKey: "test1", pollingMode: PollingModes.lazyLoad(), httpEngine: engine, configCache: cache) let expectation = self.expectation(description: "wait for response") client.getValue(for: "fakeKey", defaultValue: "") { r in @@ -584,7 +599,7 @@ class ConfigCatClientTests: XCTestCase { wait(for: [expectation2], timeout: 5) XCTAssertEqual(1, engine.requests.count) - try XCTAssertFalse(cache.read(for: "ca67405a97c0f10ec01fdc65276fc6f4f009bc48").isEmpty) + try XCTAssertFalse(cache.read(for: "147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6").isEmpty) } func testOnFlagEvaluationError() { diff --git a/Tests/ConfigCatTests/EvaluationDetailsExtensionTests.swift b/Tests/ConfigCatTests/EvaluationDetailsExtensionTests.swift index bc43b28..c6d387f 100644 --- a/Tests/ConfigCatTests/EvaluationDetailsExtensionTests.swift +++ b/Tests/ConfigCatTests/EvaluationDetailsExtensionTests.swift @@ -12,7 +12,11 @@ class EvaluationDetailsExtensionTests: XCTestCase { engine.enqueueResponse(response: Response(body: testBoolJson, statusCode: 200)) let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) - client.forceRefreshSync() + let refreshExpectation = expectation(description: "wait for refresh") + client.forceRefresh { RefreshResult in + refreshExpectation.fulfill() + } + wait(for: [refreshExpectation], timeout: 5) let expectation = expectation(description: "wait for result") client.getBoolValueDetails(for: "key", defaultValue: true, user: nil) { details in @@ -29,7 +33,11 @@ class EvaluationDetailsExtensionTests: XCTestCase { engine.enqueueResponse(response: Response(body: testIntJson, statusCode: 200)) let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) - client.forceRefreshSync() + let refreshExpectation = expectation(description: "wait for refresh") + client.forceRefresh { RefreshResult in + refreshExpectation.fulfill() + } + wait(for: [refreshExpectation], timeout: 5) let expectation = expectation(description: "wait for result") client.getIntValueDetails(for: "key", defaultValue: 0, user: nil) { details in @@ -46,7 +54,11 @@ class EvaluationDetailsExtensionTests: XCTestCase { engine.enqueueResponse(response: Response(body: testDoubleJson, statusCode: 200)) let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) - client.forceRefreshSync() + let refreshExpectation = expectation(description: "wait for refresh") + client.forceRefresh { RefreshResult in + refreshExpectation.fulfill() + } + wait(for: [refreshExpectation], timeout: 5) let expectation = expectation(description: "wait for result") client.getDoubleValueDetails(for: "key", defaultValue: 0, user: nil) { details in @@ -63,7 +75,11 @@ class EvaluationDetailsExtensionTests: XCTestCase { engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) - client.forceRefreshSync() + let refreshExpectation = expectation(description: "wait for refresh") + client.forceRefresh { RefreshResult in + refreshExpectation.fulfill() + } + wait(for: [refreshExpectation], timeout: 5) let expectation = expectation(description: "wait for result") client.getStringValueDetails(for: "key", defaultValue: "", user: nil) { details in @@ -74,53 +90,5 @@ class EvaluationDetailsExtensionTests: XCTestCase { wait(for: [expectation], timeout: 5) XCTAssertEqual(1, engine.requests.count) } - - func testBoolDetailsSync() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testBoolJson, statusCode: 200)) - - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) - client.forceRefreshSync() - let details = client.getBoolValueDetailsSync(for: "key", defaultValue: true, user: nil) - XCTAssertFalse(details.isDefaultValue) - XCTAssertTrue(details.value) - XCTAssertEqual(1, engine.requests.count) - } - - func testIntDetailsSync() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testIntJson, statusCode: 200)) - - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) - client.forceRefreshSync() - let details = client.getIntValueDetailsSync(for: "key", defaultValue: 0, user: nil) - XCTAssertFalse(details.isDefaultValue) - XCTAssertEqual(42, details.value) - XCTAssertEqual(1, engine.requests.count) - } - - func testDoubleDetailsSync() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testDoubleJson, statusCode: 200)) - - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) - client.forceRefreshSync() - let details = client.getDoubleValueDetailsSync(for: "key", defaultValue: 0, user: nil) - XCTAssertFalse(details.isDefaultValue) - XCTAssertEqual(3.14, details.value) - XCTAssertEqual(1, engine.requests.count) - } - - func testStringDetailsSync() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) - - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) - client.forceRefreshSync() - let details = client.getStringValueDetailsSync(for: "key", defaultValue: "", user: nil) - XCTAssertFalse(details.isDefaultValue) - XCTAssertEqual("fake", details.value) - XCTAssertEqual(1, engine.requests.count) - } } diff --git a/Tests/ConfigCatTests/Helpers.swift b/Tests/ConfigCatTests/Helpers.swift index c4e3c0f..c71e0c9 100644 --- a/Tests/ConfigCatTests/Helpers.swift +++ b/Tests/ConfigCatTests/Helpers.swift @@ -4,14 +4,11 @@ import XCTest extension String { func toEntryFromConfigString() -> ConfigEntry { - let data = data(using: .utf8)! - let jsonObject = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] - let config = Config.fromJson(json: jsonObject) - return ConfigEntry(config: config) + return try! ConfigEntry.fromConfigJson(json: self, eTag: "", fetchTime: .distantPast).get() } func asEntryString(date: Date = Date()) -> String { - toEntryFromConfigString().withFetchTime(time: date).toJsonString() + toEntryFromConfigString().withFetchTime(time: date).serialize() } } @@ -42,4 +39,4 @@ extension XCTestCase { } } } -} \ No newline at end of file +} diff --git a/Tests/ConfigCatTests/RolloutIntegrationTests.swift b/Tests/ConfigCatTests/RolloutIntegrationTests.swift index d1fd23f..c005211 100644 --- a/Tests/ConfigCatTests/RolloutIntegrationTests.swift +++ b/Tests/ConfigCatTests/RolloutIntegrationTests.swift @@ -168,10 +168,10 @@ class RolloutIntegrationTests: XCTestCase { } } } else { - client.getVariationId(for: settingKey, defaultVariationId: "", user: user) { stringValue in + client.getValueDetails(for: settingKey, defaultValue: nil, user: user) { (details: TypedEvaluationDetails) in let expectedValue = testObjects[i + 4] - if stringValue != expectedValue { - errors.append(String(format: "Identifier: %@, Key: %@. Expected: %@, Result: %@", testObjects[0], settingKey, expectedValue, stringValue ?? "")) + if details.variationId != expectedValue { + errors.append(String(format: "Identifier: %@, Key: %@. Expected: %@, Result: %@", testObjects[0], settingKey, expectedValue, details.variationId ?? "")) } expectation.fulfill() } diff --git a/Tests/ConfigCatTests/SnapshotTests.swift b/Tests/ConfigCatTests/SnapshotTests.swift new file mode 100644 index 0000000..5d7f165 --- /dev/null +++ b/Tests/ConfigCatTests/SnapshotTests.swift @@ -0,0 +1,78 @@ +import XCTest +@testable import ConfigCat + +class SnapshotTests: XCTestCase { + let testJsonMultiple = #"{ "f": { "key1": { "v": true, "i": "fakeId1", "p": [], "r": [] }, "key2": { "v": false, "i": "fakeId2", "p": [], "r": [{"o":1,"a":"Email","t":2,"c":"@example.com","v":true,"i":"9f21c24c"}] } } }"# + let user = ConfigCatUser(identifier: "id", email: "test@example.com") + + func testGetValue() { + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) + + let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let expectation = self.expectation(description: "wait for ready") + client.hooks.addOnReady { _ in + expectation.fulfill() + } + wait(for: [expectation], timeout: 5) + + let snapshot = client.snapshot() + let value = snapshot.getValue(for: "key1", defaultValue: false) + XCTAssertTrue(value) + let value2 = snapshot.getValue(for: "key2", defaultValue: false, user: user) + XCTAssertTrue(value2) + } + + func testGetAllKeys() { + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) + + let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let expectation = self.expectation(description: "wait for ready") + client.hooks.addOnReady { _ in + expectation.fulfill() + } + wait(for: [expectation], timeout: 5) + + let snapshot = client.snapshot() + let keys = snapshot.getAllKeys() + XCTAssertEqual(2, keys.count) + } + + + func testDetails() { + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) + + let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let expectation = self.expectation(description: "wait for ready") + client.hooks.addOnReady { _ in + expectation.fulfill() + } + wait(for: [expectation], timeout: 5) + + let snapshot = client.snapshot() + let details = snapshot.getValueDetails(for: "key2", defaultValue: true) + XCTAssertFalse(details.isDefaultValue) + XCTAssertFalse(details.value) + XCTAssertEqual(1, engine.requests.count) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testGetValueWait() async { + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) + + let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + + await client.waitForReady() + + let snapshot = client.snapshot() + let value = snapshot.getValue(for: "key1", defaultValue: false) + XCTAssertTrue(value) + let value2 = snapshot.getValue(for: "key2", defaultValue: false, user: user) + XCTAssertTrue(value2) + } + #endif +} diff --git a/Tests/ConfigCatTests/SyncTests.swift b/Tests/ConfigCatTests/SyncTests.swift deleted file mode 100644 index 71cfac6..0000000 --- a/Tests/ConfigCatTests/SyncTests.swift +++ /dev/null @@ -1,91 +0,0 @@ -import XCTest -@testable import ConfigCat - -class SyncTests: XCTestCase { - let testJsonMultiple = #"{ "f": { "key1": { "v": true, "i": "fakeId1", "p": [], "r": [] }, "key2": { "v": false, "i": "fakeId2", "p": [], "r": [{"o":1,"a":"Email","t":2,"c":"@example.com","v":true,"i":"9f21c24c"}] } } }"# - let user = ConfigCatUser(identifier: "id", email: "test@example.com") - - func testGetValue() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) - let value = client.getValueSync(for: "key1", defaultValue: false) - XCTAssertTrue(value) - let value2 = client.getValueSync(for: "key2", defaultValue: false, user: user) - XCTAssertTrue(value2) - } - - func testGetVariationId() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) - let id = client.getVariationIdSync(for: "key1", defaultVariationId: "") - XCTAssertEqual("fakeId1", id) - let id2 = client.getVariationIdSync(for: "key2", defaultVariationId: "", user: user) - XCTAssertEqual("9f21c24c", id2) - } - - func testGetKeyValue() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) - let id = client.getKeyAndValueSync(for: "fakeId1") - XCTAssertEqual(true, id?.value as? Bool) - XCTAssertEqual("key1", id?.key) - } - - func testGetAllKeys() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) - let keys = client.getAllKeysSync() - XCTAssertEqual(2, keys.count) - } - - func testGetAllValues() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) - let values = client.getAllValuesSync() - XCTAssertEqual(2, values.count) - } - - func testGetAllValueDetails() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) - let values = client.getAllValuesSync() - XCTAssertEqual(2, values.count) - } - - func testRefresh() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) - let result = client.forceRefreshSync() - let value = client.getValueSync(for: "key2", defaultValue: true) - XCTAssertTrue(result.success) - XCTAssertFalse(value) - XCTAssertEqual(1, engine.requests.count) - } - - func testRefreshWithoutResult() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) - client.forceRefreshSync() - let value = client.getValueSync(for: "key2", defaultValue: true) - XCTAssertFalse(value) - XCTAssertEqual(1, engine.requests.count) - } - - func testDetails() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) - let details = client.getValueDetailsSync(for: "key2", defaultValue: true) - XCTAssertFalse(details.isDefaultValue) - XCTAssertFalse(details.value) - XCTAssertEqual(1, engine.requests.count) - } -} diff --git a/Tests/ConfigCatTests/VariationIdTests.swift b/Tests/ConfigCatTests/VariationIdTests.swift index 0c2005e..1cd3eed 100755 --- a/Tests/ConfigCatTests/VariationIdTests.swift +++ b/Tests/ConfigCatTests/VariationIdTests.swift @@ -52,8 +52,8 @@ class VariationIdTests: XCTestCase { let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in - client.getVariationId(for: "key1", defaultVariationId: nil) { variationId in - XCTAssertEqual("fakeId1", variationId) + client.getValueDetails(for: "key1", defaultValue: false) { details in + XCTAssertEqual("fakeId1", details.variationId) expectation.fulfill() } } @@ -66,39 +66,8 @@ class VariationIdTests: XCTestCase { let client = createClient(httpEngine: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in - client.getVariationId(for: "nonexisting", defaultVariationId: "def") { variationId in - XCTAssertEqual("def", variationId) - expectation.fulfill() - } - } - wait(for: [expectation], timeout: 5) - } - - func testGetAllVariationIds() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: testJson, statusCode: 200)) - let client = createClient(httpEngine: engine) - - let expectation = self.expectation(description: "wait for response") - client.forceRefresh { _ in - client.getAllVariationIds { variationIds in - XCTAssertEqual(2, variationIds.count) - XCTAssertTrue(variationIds.contains("fakeId1")) - XCTAssertTrue(variationIds.contains("fakeId2")) - expectation.fulfill() - } - } - wait(for: [expectation], timeout: 5) - } - - func testGetAllVariationIdsEmpty() { - let engine = MockEngine() - engine.enqueueResponse(response: Response(body: "{}", statusCode: 200)) - let client = createClient(httpEngine: engine) - let expectation = self.expectation(description: "wait for response") - client.forceRefresh { _ in - client.getAllVariationIds { variationIds in - XCTAssertEqual(0, variationIds.count) + client.getValueDetails(for: "nonexisting", defaultValue: false) { details in + XCTAssertEqual("", details.variationId) expectation.fulfill() } }