[#41] New Stores Model (#52)

* 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:
Yakov Manshin 2021-04-12 19:58:13 +03:00 committed by GitHub
parent ab41672699
commit 8769b41da0
12 changed files with 162 additions and 122 deletions

View File

@ -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
}
}

View File

@ -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). Thats 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
}
}

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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 }
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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),
]