Skip to content

Commit

Permalink
Use UserDefaults as default cache (#32)
Browse files Browse the repository at this point in the history
* Use UserDefaults as default cache

* Update ConfigCatClientTests.swift

* Update ConfigCache.swift

* Add getAllValueDetails() (#33)
  • Loading branch information
z4kn4fein authored Dec 20, 2022
1 parent ea73226 commit 1d080a1
Show file tree
Hide file tree
Showing 18 changed files with 184 additions and 39 deletions.
2 changes: 1 addition & 1 deletion ConfigCat.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |spec|

spec.name = "ConfigCat"
spec.version = "9.2.4"
spec.version = "9.3.0"
spec.summary = "ConfigCat Swift SDK"
spec.swift_version = "4.2"

Expand Down
2 changes: 1 addition & 1 deletion ConfigCat.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator watchos watchsimulator app
SWIFT_VERSION = 4.2

// ConfigCat SDK version
MARKETING_VERSION = 9.2.4
MARKETING_VERSION = 9.3.0
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.4")
.package(url: "https://github.com/configcat/swift-sdk", from: "9.3.0")
]
```

Expand Down
12 changes: 12 additions & 0 deletions Sources/ConfigCat/ConfigCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,15 @@ import Foundation
*/
func write(for key: String, value: String) throws
}

class UserDefaultsCache: ConfigCache {
private let prefix = "com.configcat-"

func read(for key: String) throws -> String {
UserDefaults.standard.string(forKey: prefix + key) ?? ""
}

func write(for key: String, value: String) throws {
UserDefaults.standard.set(value, forKey: prefix + key)
}
}
54 changes: 40 additions & 14 deletions Sources/ConfigCat/ConfigCatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
flagOverrides: OverrideDataSource? = nil,
logLevel: LogLevel = .warning) {
self.init(sdkKey: sdkKey, pollingMode: refreshMode, httpEngine: URLSessionEngine(session: URLSession(configuration: sessionConfiguration)),
configCache: configCache, baseUrl: baseUrl, dataGovernance: dataGovernance, flagOverrides: flagOverrides, logLevel: logLevel)
configCache: configCache ?? UserDefaultsCache(), baseUrl: baseUrl, dataGovernance: dataGovernance, flagOverrides: flagOverrides, logLevel: logLevel)
}

init(sdkKey: String,
Expand Down Expand Up @@ -197,6 +197,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
if key.isEmpty {
assert(false, "key cannot be empty")
}
let evalUser = user ?? defaultUser
if Value.self != String.self &&
Value.self != String?.self &&
Value.self != Int.self &&
Expand All @@ -211,7 +212,8 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
log.error(message: message)
hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key,
value: defaultValue,
error: message))
error: message,
user: evalUser))
completion(defaultValue)
return
}
Expand All @@ -221,7 +223,8 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
self.log.error(message: message)
self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key,
value: defaultValue,
error: message))
error: message,
user: evalUser))
completion(defaultValue)
return
}
Expand All @@ -230,12 +233,13 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
self.log.error(message: message)
self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key,
value: defaultValue,
error: message))
error: message,
user: evalUser))
completion(defaultValue)
return
}

let evalDetails = self.evaluate(setting: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime)
let evalDetails = self.evaluate(setting: setting, key: key, user: evalUser, fetchTime: result.fetchTime)
completion(evalDetails.value as? Value ?? defaultValue)
}
}
Expand All @@ -252,6 +256,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
if key.isEmpty {
assert(false, "key cannot be empty")
}
let evalUser = user ?? defaultUser
if Value.self != String.self &&
Value.self != String?.self &&
Value.self != Int.self &&
Expand All @@ -264,38 +269,39 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
Value.self != Any?.self {
let message = "Only String, Integer, Double, Bool or Any types are supported."
log.error(message: message)
hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, error: message))
completion(TypedEvaluationDetails<Value>.fromError(key: key, value: defaultValue, error: message))
hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: evalUser))
completion(TypedEvaluationDetails<Value>.fromError(key: key, value: defaultValue, error: message, user: evalUser))
return
}
getSettings { result in
if result.settings.isEmpty {
let message = String(format: "Config is not present. Returning defaultValue: [%@].", "\(defaultValue)")
self.log.error(message: message)
self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, error: message))
completion(TypedEvaluationDetails<Value>.fromError(key: key, value: defaultValue, error: message))
self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: evalUser))
completion(TypedEvaluationDetails<Value>.fromError(key: key, value: defaultValue, error: message, user: evalUser))
return
}
guard let setting = result.settings[key] else {
let message = String(format: "Value not found for key '%@'. Here are the available keys: %@", key, [String](result.settings.keys))
self.log.error(message: message)
self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key,
value: defaultValue,
error: message))
completion(TypedEvaluationDetails<Value>.fromError(key: key, value: defaultValue, error: message))
error: message,
user: evalUser))
completion(TypedEvaluationDetails<Value>.fromError(key: key, value: defaultValue, error: message, user: evalUser))
return
}

let details = self.evaluate(setting: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime)
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: """
The value '%@' cannot be converted to the requested type.
Returning defaultValue: [%@].
Here are the available keys: %@
""", "\(details.value)", "\(defaultValue)", [String](result.settings.keys))
self.log.error(message: message)
self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, error: message))
completion(TypedEvaluationDetails<Value>.fromError(key: key, value: defaultValue, error: message))
self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: evalUser))
completion(TypedEvaluationDetails<Value>.fromError(key: key, value: defaultValue, error: message, user: evalUser))
return
}

Expand All @@ -311,6 +317,26 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
}
}

/**
Gets the values along with evaluation details of all feature flags and settings.

- Parameter user: the user object to identify the caller.
- Parameter completion: the function which will be called when the feature flag or setting is evaluated.
*/
@objc public func getAllValueDetails(user: ConfigCatUser? = nil, completion: @escaping ([EvaluationDetails]) -> ()) {
getSettings { result in
var detailsResult = [EvaluationDetails]()
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)
detailsResult.append(details)
}
completion(detailsResult)
}
}

/// Gets all the setting keys asynchronously.
@objc public func getAllKeys(completion: @escaping ([String]) -> ()) {
getSettings { result in
Expand Down
21 changes: 20 additions & 1 deletion Sources/ConfigCat/ConfigCatClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,23 @@ public protocol ConfigCatClientProtocol {
*/
func getValueDetails<Value>(for key: String, defaultValue: Value, user: ConfigCatUser?, completion: @escaping (TypedEvaluationDetails<Value>) -> ())

/**
Gets the values along with evaluation details of all feature flags and settings.

- Parameter user: the user object to identify the caller.
- Parameter completion: the function which will be called when the feature flag or setting is evaluated.
*/
func getAllValueDetails(user: ConfigCatUser?, completion: @escaping ([EvaluationDetails]) -> ())

/// 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)
Expand Down Expand Up @@ -85,21 +95,30 @@ public protocol ConfigCatClientProtocol {
- 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.
- Parameter completion: the function which will be called when the feature flag or setting is evaluated.
*/
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func getValueDetails<Value>(for key: String, defaultValue: Value, user: ConfigCatUser?) async -> TypedEvaluationDetails<Value>

/**
Gets the values along with evaluation details of all feature flags and settings.

- Parameter user: the user object to identify the caller.
*/
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func getAllValueDetails(user: ConfigCatUser?) async -> [EvaluationDetails]

/// Gets all the setting keys asynchronously.
@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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/ConfigCat/ConfigCatOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public final class ConfigCatOptions: NSObject {
@objc public var dataGovernance: DataGovernance = .global

/// The cache implementation used to cache the downloaded config.json.
@objc public var configCache: ConfigCache? = nil
@objc public var configCache: ConfigCache? = UserDefaultsCache()

/// The polling mode.
@objc public var pollingMode: PollingMode = PollingModes.autoPoll()
Expand Down
18 changes: 12 additions & 6 deletions Sources/ConfigCat/ConfigFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ enum RedirectMode: Int {
enum FetchResponse: Equatable {
case fetched(ConfigEntry)
case notModified
case failure(String)
case failure(message: String, isTransient: Bool)

public var entry: ConfigEntry? {
switch self {
Expand All @@ -25,7 +25,7 @@ func ==(lhs: FetchResponse, rhs: FetchResponse) -> Bool {
switch (lhs, rhs) {
case (.fetched(_), .fetched(_)),
(.notModified, .notModified),
(.failure(_), .failure(_)):
(.failure(_, _), .failure(_, _)):
return true
default:
return false
Expand Down Expand Up @@ -131,7 +131,7 @@ class ConfigFetcher: NSObject {
}
let message = String(format: "An error occurred during the config fetch: %@%@", error.localizedDescription, extraInfo)
self.log.error(message: message)
completion(.failure(message))
completion(.failure(message: message, isTransient: true))
} else {
let response = resp as! HTTPURLResponse
if response.statusCode >= 200 && response.statusCode < 300, let data = data {
Expand All @@ -145,17 +145,23 @@ class ConfigFetcher: NSObject {
case .failure(let error):
let message = String(format: "An error occurred during JSON deserialization. %@", error.localizedDescription)
self.log.error(message: message)
completion(.failure(message))
completion(.failure(message: message, isTransient: true))
}
} else if response.statusCode == 304 {
self.log.debug(message: "Fetch was successful: not modified")
completion(.notModified)
} else if response.statusCode == 404 || response.statusCode == 403 {
let message = String(format: """
Double-check your SDK Key at https://app.configcat.com/sdkkey. Status code: %@
""", String(response.statusCode))
self.log.error(message: message)
completion(.failure(message: message, isTransient: false))
} else {
let message = String(format: """
Double-check your SDK Key at https://app.configcat.com/sdkkey. Non success status code: %@
Unexpected HTTP response was received: %@
""", String(response.statusCode))
self.log.error(message: message)
completion(.failure(message))
completion(.failure(message: message, isTransient: true))
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion Sources/ConfigCat/ConfigService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,11 @@ class ConfigService {
cachedEntry = cachedEntry.withFetchTime(time: Date())
writeCache(entry: cachedEntry)
callCompletions(result: .success(cachedEntry))
case .failure(let error):
case .failure(let error, let isTransient):
if !isTransient && !cachedEntry.isEmpty {
cachedEntry = cachedEntry.withFetchTime(time: Date())
writeCache(entry: cachedEntry)
}
callCompletions(result: .failure(error, cachedEntry))
}
completions = nil
Expand Down
8 changes: 4 additions & 4 deletions Sources/ConfigCat/EvaluationDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ public final class EvaluationDetails: EvaluationDetailsBase {
super.init(key: key, variationId: variationId, fetchTime: fetchTime, user: user, isDefaultValue: isDefaultValue, error: error, matchedEvaluationRule: matchedEvaluationRule, matchedEvaluationPercentageRule: matchedEvaluationPercentageRule)
}

static func fromError(key: String, value: Any, error: String) -> EvaluationDetails {
EvaluationDetails(key: key, value: value, variationId: "", isDefaultValue: true, error: error)
static func fromError(key: String, value: Any, error: String, user: ConfigCatUser?) -> EvaluationDetails {
EvaluationDetails(key: key, value: value, variationId: "", user: user, isDefaultValue: true, error: error)
}
}

Expand Down Expand Up @@ -106,8 +106,8 @@ public final class TypedEvaluationDetails<Value>: EvaluationDetailsBase {
super.init(key: key, variationId: variationId, fetchTime: fetchTime, user: user, isDefaultValue: isDefaultValue, error: error, matchedEvaluationRule: matchedEvaluationRule, matchedEvaluationPercentageRule: matchedEvaluationPercentageRule)
}

static func fromError<Value>(key: String, value: Value, error: String) -> TypedEvaluationDetails<Value> {
TypedEvaluationDetails<Value>(key: key, value: value, variationId: "", isDefaultValue: true, error: error)
static func fromError<Value>(key: String, value: Value, error: String, user: ConfigCatUser?) -> TypedEvaluationDetails<Value> {
TypedEvaluationDetails<Value>(key: key, value: value, variationId: "", user: user, isDefaultValue: true, error: error)
}

func toStringDetails() -> StringEvaluationDetails {
Expand Down
Loading

0 comments on commit 1d080a1

Please sign in to comment.