[#43] Change Saving (#53)

* Extended `MutableFeatureFlagStoreProtocol` with an optional method `saveChanges()`
* Implemented `saveChanges()` in `UserDefaultsStore`
* Added change saving to `FeatureFlagResolver`’s `deinit`
* Extended `FeatureFlagStore` with an `asMutable` computed property
* Added `MutableStoreTests`
* Updated Linux tests
This commit is contained in:
Yakov Manshin 2021-04-17 16:07:47 +03:00 committed by GitHub
parent 8769b41da0
commit 4c83b8d58a
6 changed files with 173 additions and 7 deletions

View File

@ -23,6 +23,12 @@ final public class FeatureFlagResolver {
self.configuration = configuration
}
deinit {
configuration.stores
.compactMap({ $0.asMutable })
.forEach({ $0.saveChanges() })
}
}
// MARK: - FeatureFlagResolverProtocol
@ -106,13 +112,7 @@ extension FeatureFlagResolver {
}
private func findMutableStores() throws -> [MutableFeatureFlagStoreProtocol] {
var stores = [MutableFeatureFlagStoreProtocol]()
for store in configuration.stores {
if case .mutable(let mutableStore) = store {
stores.append(mutableStore)
}
}
let stores = configuration.stores.compactMap({ $0.asMutable })
if stores.isEmpty {
throw FeatureFlagResolverError.noMutableStoreAvailable

View File

@ -48,6 +48,10 @@ extension UserDefaultsStore: MutableFeatureFlagStoreProtocol {
userDefaults.removeObject(forKey: key)
}
public func saveChanges() {
userDefaults.synchronize()
}
}
#endif

View File

@ -25,4 +25,13 @@ public extension FeatureFlagStore {
}
}
var asMutable: MutableFeatureFlagStoreProtocol? {
switch self {
case .immutable:
return nil
case .mutable(let store):
return store
}
}
}

View File

@ -21,4 +21,18 @@ public protocol MutableFeatureFlagStoreProtocol: AnyObject, FeatureFlagStoreProt
/// - Parameter key: *Required.* The key used to address the value.
func removeValue(forKey key: String)
/// Immediately saves changed values so theyre not lost.
///
/// + This method can be called when work with the feature flag store is finished.
func saveChanges()
}
// MARK: - Default Implementation
extension MutableFeatureFlagStoreProtocol {
// Not all kinds of feature flag stores need this method, so its optional to implement.
public func saveChanges() { }
}

View File

@ -0,0 +1,127 @@
//
// MutableStoreTests.swift
// YMFF
//
// Created by Yakov Manshin on 4/17/21.
// Copyright © 2021 Yakov Manshin. See the LICENSE file for license info.
//
import XCTest
import YMFFProtocols
@testable import YMFF
// MARK: - MutableStoreTests
final class MutableStoreTests: XCTestCase {
private var mutableStore: MutableFeatureFlagStoreProtocol!
private var resolver: FeatureFlagResolverProtocol!
override func setUp() {
super.setUp()
mutableStore = MutableFeatureFlagStore(store: .init())
resolver = FeatureFlagResolver(configuration: .init(stores: [.mutable(mutableStore)]))
}
func testOverride() {
let overrideKey = "OVERRIDE_KEY"
let overrideValue = 123
let initialValueFromResolver = try? resolver.value(for: overrideKey) as Int
let initialValueFromStore: Int? = mutableStore.value(forKey: overrideKey)
XCTAssertNil(initialValueFromResolver)
XCTAssertNil(initialValueFromStore)
try? resolver.overrideInRuntime(overrideKey, with: overrideValue)
let overrideValueFromResolver = try? resolver.value(for: overrideKey) as Int
let overrideValueFromStore: Int? = mutableStore.value(forKey: overrideKey)
XCTAssertEqual(overrideValueFromResolver, overrideValue)
XCTAssertEqual(overrideValueFromStore, overrideValue)
}
func testOverrideRemoval() {
let overrideKey = "OVERRIDE_KEY"
let overrideValue = 123
mutableStore.setValue(overrideValue, forKey: overrideKey)
let overrideValueFromResolver = try? resolver.value(for: overrideKey) as Int
let overrideValueFromStore: Int? = mutableStore.value(forKey: overrideKey)
XCTAssertEqual(overrideValueFromResolver, overrideValue)
XCTAssertEqual(overrideValueFromStore, overrideValue)
resolver.removeRuntimeOverride(for: overrideKey)
let removedValueFromResolver = try? resolver.value(for: overrideKey) as Int
let removedValueFromStore: Int? = mutableStore.value(forKey: overrideKey)
XCTAssertNil(removedValueFromResolver)
XCTAssertNil(removedValueFromStore)
}
func testChangeSaving() {
var saveChangesCount = 0
mutableStore = MutableFeatureFlagStore(store: .init()) {
saveChangesCount += 1
}
resolver = FeatureFlagResolver(configuration: .init(stores: [.mutable(mutableStore)]))
resolver = nil
XCTAssertEqual(saveChangesCount, 1)
}
}
// MARK: - MutableFeatureFlagStore
final private class MutableFeatureFlagStore: MutableFeatureFlagStoreProtocol {
private var store: TransparentFeatureFlagStore
private var onSaveChangesClosure: () -> Void
init(store: TransparentFeatureFlagStore, onSaveChanges: @escaping () -> Void = { }) {
self.store = store
self.onSaveChangesClosure = onSaveChanges
}
func containsValue(forKey key: String) -> Bool {
store[key] != nil
}
func value<Value>(forKey key: String) -> Value? {
let expectedValueType = Value.self
switch expectedValueType {
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")
return nil
}
}
func setValue<Value>(_ value: Value, forKey key: String) {
store[key] = value
}
func removeValue(forKey key: String) {
store.removeValue(forKey: key)
}
func saveChanges() {
onSaveChangesClosure()
}
}

View File

@ -44,6 +44,17 @@ extension FeatureFlagTests {
]
}
extension MutableStoreTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__MutableStoreTests = [
("testChangeSaving", testChangeSaving),
("testOverride", testOverride),
("testOverrideRemoval", testOverrideRemoval),
]
}
extension RuntimeOverridesStoreTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
@ -70,6 +81,7 @@ public func __allTests() -> [XCTestCaseEntry] {
return [
testCase(FeatureFlagResolverTests.__allTests__FeatureFlagResolverTests),
testCase(FeatureFlagTests.__allTests__FeatureFlagTests),
testCase(MutableStoreTests.__allTests__MutableStoreTests),
testCase(RuntimeOverridesStoreTests.__allTests__RuntimeOverridesStoreTests),
testCase(UserDefaultsStoreTests.__allTests__UserDefaultsStoreTests),
]