* Introduced the new `FeatureFlagStore` enum in YMFFProtocols used to configure the resolver * Updated `FeatureFlagResolverConfiguration` * Updated `FeatureFlagResolver` * Removed the old `FeatureFlagStore` from the YMFF implementation target * Extended `TransparentFeatureFlagStore` with a `FeatureFlagStoreProtocol` conformance * Updated tests
This commit is contained in:
parent
ab41672699
commit
8769b41da0
|
@ -13,15 +13,10 @@ import YMFFProtocols
|
|||
/// A YMFF-supplied object used to provide the feature flag resolver with its configuration.
|
||||
public struct FeatureFlagResolverConfiguration {
|
||||
|
||||
public let persistentStores: [FeatureFlagStoreProtocol]
|
||||
public let runtimeStore: MutableFeatureFlagStoreProtocol
|
||||
public let stores: [FeatureFlagStore]
|
||||
|
||||
public init(
|
||||
persistentStores: [FeatureFlagStore],
|
||||
runtimeStore: MutableFeatureFlagStoreProtocol = RuntimeOverridesStore()
|
||||
) {
|
||||
self.persistentStores = persistentStores
|
||||
self.runtimeStore = runtimeStore
|
||||
public init(stores: [FeatureFlagStore]) {
|
||||
self.stores = stores
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ final public class FeatureFlagResolver {
|
|||
extension FeatureFlagResolver: FeatureFlagResolverProtocol {
|
||||
|
||||
public func value<Value>(for key: FeatureFlagKey) throws -> Value {
|
||||
let retrievedValue: Value = try retrieveValue(forKey: key)
|
||||
let retrievedValue: Value = try retrieveFirstValueFoundInStores(byKey: key)
|
||||
try validateValue(retrievedValue)
|
||||
|
||||
return retrievedValue
|
||||
|
@ -38,11 +38,14 @@ extension FeatureFlagResolver: FeatureFlagResolverProtocol {
|
|||
|
||||
public func overrideInRuntime<Value>(_ key: FeatureFlagKey, with newValue: Value) throws {
|
||||
try validateOverrideValue(newValue, forKey: key)
|
||||
configuration.runtimeStore.setValue(newValue, forKey: key)
|
||||
|
||||
let mutableStore = try findFirstMutableStore()
|
||||
mutableStore.setValue(newValue, forKey: key)
|
||||
}
|
||||
|
||||
public func removeRuntimeOverride(for key: FeatureFlagKey) {
|
||||
configuration.runtimeStore.removeValue(forKey: key)
|
||||
let mutableStores = try? findMutableStores()
|
||||
mutableStores?.forEach({ $0.removeValue(forKey: key) })
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,24 +54,14 @@ extension FeatureFlagResolver: FeatureFlagResolverProtocol {
|
|||
|
||||
extension FeatureFlagResolver {
|
||||
|
||||
private func retrieveValue<Value>(forKey key: String) throws -> Value {
|
||||
if let runtimeValue: Value = configuration.runtimeStore.value(forKey: key) {
|
||||
return runtimeValue
|
||||
func retrieveFirstValueFoundInStores<Value>(byKey key: String) throws -> Value {
|
||||
guard !configuration.stores.isEmpty else {
|
||||
throw FeatureFlagResolverError.noStoreAvailable
|
||||
}
|
||||
|
||||
return try retrieveFirstValueFoundInPersistentStores(byKey: key)
|
||||
}
|
||||
|
||||
func retrieveFirstValueFoundInPersistentStores<Value>(byKey key: String) throws -> Value {
|
||||
let stores = configuration.persistentStores
|
||||
|
||||
guard !stores.isEmpty else {
|
||||
throw FeatureFlagResolverError.noPersistentStoreAvailable
|
||||
}
|
||||
|
||||
for store in stores {
|
||||
if store.containsValue(forKey: key) {
|
||||
guard let value: Value = store.value(forKey: key)
|
||||
for store in configuration.stores {
|
||||
if store.asImmutable.containsValue(forKey: key) {
|
||||
guard let value: Value = store.asImmutable.value(forKey: key)
|
||||
else { throw FeatureFlagResolverError.typeMismatch }
|
||||
|
||||
return value
|
||||
|
@ -90,7 +83,7 @@ extension FeatureFlagResolver {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - Runtime Overriding
|
||||
// MARK: - Overriding
|
||||
|
||||
extension FeatureFlagResolver {
|
||||
|
||||
|
@ -98,7 +91,7 @@ extension FeatureFlagResolver {
|
|||
try validateValue(value)
|
||||
|
||||
do {
|
||||
let _: Value = try retrieveFirstValueFoundInPersistentStores(byKey: key)
|
||||
let _: Value = try retrieveFirstValueFoundInStores(byKey: key)
|
||||
} catch FeatureFlagResolverError.valueNotFoundInPersistentStores {
|
||||
// If none of the persistent stores contains a value for the key, then the client is attempting
|
||||
// to set a new value (instead of overriding an existing one). That’s an acceptable use case.
|
||||
|
@ -107,4 +100,25 @@ extension FeatureFlagResolver {
|
|||
}
|
||||
}
|
||||
|
||||
private func findFirstMutableStore() throws -> MutableFeatureFlagStoreProtocol {
|
||||
let mutableStores = try findMutableStores()
|
||||
return mutableStores[0]
|
||||
}
|
||||
|
||||
private func findMutableStores() throws -> [MutableFeatureFlagStoreProtocol] {
|
||||
var stores = [MutableFeatureFlagStoreProtocol]()
|
||||
|
||||
for store in configuration.stores {
|
||||
if case .mutable(let mutableStore) = store {
|
||||
stores.append(mutableStore)
|
||||
}
|
||||
}
|
||||
|
||||
if stores.isEmpty {
|
||||
throw FeatureFlagResolverError.noMutableStoreAvailable
|
||||
}
|
||||
|
||||
return stores
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
|
||||
/// Errors returned by `FeatureFlagResolver`.
|
||||
public enum FeatureFlagResolverError: Error {
|
||||
case noPersistentStoreAvailable
|
||||
case noMutableStoreAvailable
|
||||
case noStoreAvailable
|
||||
case optionalValuesNotAllowed
|
||||
case typeMismatch
|
||||
case valueNotFoundInPersistentStores(key: String)
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
//
|
||||
// FeatureFlagStore.swift
|
||||
// YMFF
|
||||
//
|
||||
// Created by Yakov Manshin on 9/20/20.
|
||||
// Copyright © 2020 Yakov Manshin. See the LICENSE file for license info.
|
||||
//
|
||||
|
||||
#if canImport(Foundation)
|
||||
import Foundation
|
||||
#endif
|
||||
|
||||
import YMFFProtocols
|
||||
|
||||
// MARK: - FeatureFlagStore
|
||||
|
||||
/// An object that provides a number of ways to supply the feature flag store.
|
||||
public enum FeatureFlagStore {
|
||||
case opaque(FeatureFlagStoreProtocol)
|
||||
case transparent(TransparentFeatureFlagStore)
|
||||
|
||||
#if canImport(Foundation)
|
||||
case userDefaults(UserDefaults)
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - FeatureFlagStoreProtocol
|
||||
|
||||
extension FeatureFlagStore: FeatureFlagStoreProtocol {
|
||||
|
||||
public func containsValue(forKey key: String) -> Bool {
|
||||
switch self {
|
||||
case .opaque(let store):
|
||||
return store.containsValue(forKey: key)
|
||||
case .transparent(let store):
|
||||
return store[key] != nil
|
||||
#if canImport(Foundation)
|
||||
case .userDefaults(let userDefaults):
|
||||
return userDefaults.object(forKey: key) != nil
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public func value<Value>(forKey key: String) -> Value? {
|
||||
switch self {
|
||||
case .opaque(let store):
|
||||
return store.value(forKey: key)
|
||||
case .transparent(let store):
|
||||
return store[key] as? Value
|
||||
#if canImport(Foundation)
|
||||
case .userDefaults(let userDefaults):
|
||||
return userDefaults.object(forKey: key) as? Value
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -6,5 +6,23 @@
|
|||
// Copyright © 2020 Yakov Manshin. See the LICENSE file for license info.
|
||||
//
|
||||
|
||||
import YMFFProtocols
|
||||
|
||||
// MARK: - TransparentFeatureFlagStore
|
||||
|
||||
/// A simple dictionary used to store and retrieve feature flag values.
|
||||
public typealias TransparentFeatureFlagStore = [String : Any]
|
||||
|
||||
// MARK: - FeatureFlagStoreProtocol
|
||||
|
||||
extension TransparentFeatureFlagStore: FeatureFlagStoreProtocol {
|
||||
|
||||
public func containsValue(forKey key: String) -> Bool {
|
||||
self[key] != nil
|
||||
}
|
||||
|
||||
public func value<Value>(forKey key: String) -> Value? {
|
||||
self[key] as? Value
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,11 +37,11 @@ extension UserDefaultsStore: MutableFeatureFlagStoreProtocol {
|
|||
}
|
||||
|
||||
public func value<Value>(forKey key: String) -> Value? {
|
||||
userDefaults.value(forKey: key) as? Value
|
||||
userDefaults.object(forKey: key) as? Value
|
||||
}
|
||||
|
||||
public func setValue<Value>(_ value: Value, forKey key: String) {
|
||||
userDefaults.setValue(value, forKey: key)
|
||||
userDefaults.set(value, forKey: key)
|
||||
}
|
||||
|
||||
public func removeValue(forKey key: String) {
|
||||
|
|
|
@ -11,10 +11,8 @@ public protocol FeatureFlagResolverConfigurationProtocol {
|
|||
|
||||
/// An array of stores which may contain feature flag values.
|
||||
///
|
||||
/// + The array may include both mutable and immutable stores.
|
||||
/// + The stores are examined in order. The first value found for a key will be used.
|
||||
var persistentStores: [FeatureFlagStoreProtocol] { get }
|
||||
|
||||
/// An object that provides feature flag values used in the runtime, within a single app session.
|
||||
var runtimeStore: MutableFeatureFlagStoreProtocol { get }
|
||||
var stores: [FeatureFlagStore] { get }
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// FeatureFlagStore.swift
|
||||
// YMFFProtocols
|
||||
//
|
||||
// Created by Yakov Manshin on 4/10/21.
|
||||
// Copyright © 2021 Yakov Manshin. See the LICENSE file for license info.
|
||||
//
|
||||
|
||||
// MARK: - FeatureFlagStore
|
||||
|
||||
/// The enum used to configure the feature flag resolver.
|
||||
public enum FeatureFlagStore {
|
||||
case immutable(FeatureFlagStoreProtocol)
|
||||
case mutable(MutableFeatureFlagStoreProtocol)
|
||||
}
|
||||
|
||||
public extension FeatureFlagStore {
|
||||
|
||||
var asImmutable: FeatureFlagStoreProtocol {
|
||||
switch self {
|
||||
case .immutable(let store):
|
||||
return store
|
||||
case .mutable(let store):
|
||||
return store
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -62,9 +62,9 @@ extension FeatureFlagResolverTests {
|
|||
} catch FeatureFlagResolverError.valueNotFoundInPersistentStores { } catch { XCTFail() }
|
||||
}
|
||||
|
||||
// MARK: Runtime Override
|
||||
// MARK: Overriding
|
||||
|
||||
func testRuntimeOverrideSuccess() {
|
||||
func testOverrideSuccess() {
|
||||
let key = SharedAssets.intKey
|
||||
let originalValue = 123
|
||||
let overrideValue = 789
|
||||
|
@ -80,7 +80,23 @@ extension FeatureFlagResolverTests {
|
|||
XCTAssertEqual(try resolver.value(for: key), originalValue)
|
||||
}
|
||||
|
||||
func testRuntimeOverrideFailure() {
|
||||
func testOverrideFailureNoMutableStores() {
|
||||
resolver = FeatureFlagResolver(configuration: SharedAssets.configurationWithNoMutableStores)
|
||||
let key = SharedAssets.intKey
|
||||
let originalValue = 123
|
||||
let overrideValue = 789
|
||||
|
||||
XCTAssertEqual(try resolver.value(for: key), originalValue)
|
||||
|
||||
do {
|
||||
_ = try resolver.overrideInRuntime(key, with: overrideValue)
|
||||
XCTFail()
|
||||
} catch FeatureFlagResolverError.noMutableStoreAvailable { } catch { XCTFail() }
|
||||
|
||||
XCTAssertEqual(try resolver.value(for: key), originalValue)
|
||||
}
|
||||
|
||||
func testOverrideFailureTypeMismatch() {
|
||||
let key = SharedAssets.stringKey
|
||||
let overrideValue = 789
|
||||
|
||||
|
@ -94,7 +110,7 @@ extension FeatureFlagResolverTests {
|
|||
XCTAssertEqual(try resolver.value(for: key), "STRING_VALUE_REMOTE")
|
||||
}
|
||||
|
||||
func testRuntimeOverrideForNewKeys() {
|
||||
func testOverrideForNewKeys() {
|
||||
let key = FeatureFlagKey("NEW_KEY")
|
||||
let overrideValue = 789
|
||||
|
||||
|
@ -119,17 +135,17 @@ extension FeatureFlagResolverTests {
|
|||
func testValueRetrieval() {
|
||||
let key = "int"
|
||||
|
||||
XCTAssertNoThrow(try resolver.retrieveFirstValueFoundInPersistentStores(byKey: key) as Int)
|
||||
XCTAssertNoThrow(try resolver.retrieveFirstValueFoundInStores(byKey: key) as Int)
|
||||
}
|
||||
|
||||
func testValueRetrievalFromEmptyPersistentStoresArray() {
|
||||
resolver = FeatureFlagResolver(configuration: SharedAssets.configurationWithNoPersistentStores)
|
||||
func testValueRetrievalFromEmptyStoresArray() {
|
||||
resolver = FeatureFlagResolver(configuration: SharedAssets.configurationWithNoStores)
|
||||
|
||||
let key = "int"
|
||||
|
||||
do {
|
||||
let _: Int = try resolver.retrieveFirstValueFoundInPersistentStores(byKey: key)
|
||||
} catch FeatureFlagResolverError.noPersistentStoreAvailable { } catch { XCTFail() }
|
||||
let _: Int = try resolver.retrieveFirstValueFoundInStores(byKey: key)
|
||||
} catch FeatureFlagResolverError.noStoreAvailable { } catch { XCTFail() }
|
||||
}
|
||||
|
||||
// MARK: Value Validation
|
||||
|
|
|
@ -14,14 +14,22 @@ import YMFFProtocols
|
|||
enum SharedAssets {
|
||||
|
||||
static var configuration: FeatureFlagResolverConfiguration {
|
||||
.init(persistentStores: [
|
||||
.opaque(OpaqueStoreWithLimitedTypeSupport(store: remoteStore)),
|
||||
.transparent(localStore)
|
||||
.init(stores: [
|
||||
.mutable(RuntimeOverridesStore()),
|
||||
.immutable(OpaqueStoreWithLimitedTypeSupport(store: remoteStore)),
|
||||
.immutable(localStore),
|
||||
])
|
||||
}
|
||||
|
||||
static var configurationWithNoPersistentStores: FeatureFlagResolverConfiguration {
|
||||
.init(persistentStores: [])
|
||||
static var configurationWithNoMutableStores: FeatureFlagResolverConfiguration {
|
||||
.init(stores: [
|
||||
.immutable(OpaqueStoreWithLimitedTypeSupport(store: remoteStore)),
|
||||
.immutable(localStore),
|
||||
])
|
||||
}
|
||||
|
||||
static var configurationWithNoStores: FeatureFlagResolverConfiguration {
|
||||
.init(stores: [])
|
||||
}
|
||||
|
||||
private static var localStore: [String : Any] { [
|
||||
|
@ -66,11 +74,12 @@ private struct OpaqueStoreWithLimitedTypeSupport: FeatureFlagStoreProtocol {
|
|||
let expectedValueType = Value.self
|
||||
|
||||
switch expectedValueType {
|
||||
case is Bool.Type:
|
||||
return store[key] as? Value
|
||||
case is Int.Type:
|
||||
return store[key] as? Value
|
||||
case is String.Type:
|
||||
case is Bool.Type,
|
||||
is Int.Type,
|
||||
is String.Type,
|
||||
is Optional<Bool>.Type,
|
||||
is Optional<Int>.Type,
|
||||
is Optional<String>.Type:
|
||||
return store[key] as? Value
|
||||
default:
|
||||
assertionFailure("The expected feature flag value type (\(expectedValueType)) is not supported")
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
// Copyright © 2021 Yakov Manshin. See the LICENSE file for license info.
|
||||
//
|
||||
|
||||
#if canImport(Foundation)
|
||||
|
||||
import XCTest
|
||||
@testable import YMFF
|
||||
|
||||
|
@ -18,10 +20,9 @@ final class UserDefaultsStoreTests: XCTestCase {
|
|||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
resolver = FeatureFlagResolver(configuration: .init(
|
||||
persistentStores: [.userDefaults(userDefaults)],
|
||||
runtimeStore: UserDefaultsStore(userDefaults: userDefaults)
|
||||
))
|
||||
resolver = FeatureFlagResolver(configuration: .init(stores: [
|
||||
.mutable(UserDefaultsStore(userDefaults: userDefaults))
|
||||
]))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -32,7 +33,7 @@ extension UserDefaultsStoreTests {
|
|||
let key = "TEST_UserDefaults_key_123"
|
||||
let value = 123
|
||||
|
||||
userDefaults.setValue(value, forKey: key)
|
||||
userDefaults.set(value, forKey: key)
|
||||
|
||||
// FIXME: [#40] Can't use `retrievedValue: Int?` here
|
||||
let retrievedValue = try? resolver.value(for: key) as Int
|
||||
|
@ -46,7 +47,7 @@ extension UserDefaultsStoreTests {
|
|||
|
||||
try? resolver.overrideInRuntime(key, with: value)
|
||||
|
||||
let retrievedValue = userDefaults.value(forKey: key) as? Int
|
||||
let retrievedValue = userDefaults.object(forKey: key) as? Int
|
||||
|
||||
XCTAssertEqual(retrievedValue, value)
|
||||
}
|
||||
|
@ -63,4 +64,19 @@ extension UserDefaultsStoreTests {
|
|||
XCTAssertEqual(retrievedValue, value)
|
||||
}
|
||||
|
||||
func testRemoveValueWithResolver() {
|
||||
let key = "TEST_UserDefaults_key_012"
|
||||
let value = 012
|
||||
|
||||
userDefaults.set(value, forKey: key)
|
||||
|
||||
XCTAssertEqual(userDefaults.object(forKey: key) as? Int, value)
|
||||
|
||||
resolver.removeRuntimeOverride(for: key)
|
||||
|
||||
XCTAssertNil(userDefaults.object(forKey: key))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -10,15 +10,16 @@ extension FeatureFlagResolverTests {
|
|||
("testIntValueResolution", testIntValueResolution),
|
||||
("testNonexistentValueResolution", testNonexistentValueResolution),
|
||||
("testOptionalIntValueResolution", testOptionalIntValueResolution),
|
||||
("testOverrideFailureNoMutableStores", testOverrideFailureNoMutableStores),
|
||||
("testOverrideFailureTypeMismatch", testOverrideFailureTypeMismatch),
|
||||
("testOverrideForNewKeys", testOverrideForNewKeys),
|
||||
("testOverrideSuccess", testOverrideSuccess),
|
||||
("testOverrideValueValidationFailureOptional", testOverrideValueValidationFailureOptional),
|
||||
("testOverrideValueValidationFailureTypeMismatch", testOverrideValueValidationFailureTypeMismatch),
|
||||
("testOverrideValueValidationSuccess", testOverrideValueValidationSuccess),
|
||||
("testRuntimeOverrideFailure", testRuntimeOverrideFailure),
|
||||
("testRuntimeOverrideForNewKeys", testRuntimeOverrideForNewKeys),
|
||||
("testRuntimeOverrideSuccess", testRuntimeOverrideSuccess),
|
||||
("testStringValueResolution", testStringValueResolution),
|
||||
("testValueRetrieval", testValueRetrieval),
|
||||
("testValueRetrievalFromEmptyPersistentStoresArray", testValueRetrievalFromEmptyPersistentStoresArray),
|
||||
("testValueRetrievalFromEmptyStoresArray", testValueRetrievalFromEmptyStoresArray),
|
||||
("testValueValidation", testValueValidation),
|
||||
]
|
||||
}
|
||||
|
@ -59,6 +60,7 @@ extension UserDefaultsStoreTests {
|
|||
// to regenerate.
|
||||
static let __allTests__UserDefaultsStoreTests = [
|
||||
("testReadValueWithResolver", testReadValueWithResolver),
|
||||
("testRemoveValueWithResolver", testRemoveValueWithResolver),
|
||||
("testWriteAndReadValueWithResolver", testWriteAndReadValueWithResolver),
|
||||
("testWriteValueWithResolver", testWriteValueWithResolver),
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue