diff --git a/.github/workflows/spm-ci.yml b/.github/workflows/spm-ci.yml index ff829a4..9b2f212 100644 --- a/.github/workflows/spm-ci.yml +++ b/.github/workflows/spm-ci.yml @@ -10,6 +10,6 @@ jobs: steps: - uses: actions/checkout@v2 - name: Select Xcode version - run: sudo xcode-select -switch /Applications/Xcode_12.2.app + run: sudo xcode-select -switch /Applications/Xcode_12.5.app - name: Run tests run: swift test -v diff --git a/LICENSE b/LICENSE index 14d318b..2f8b816 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2020 Yakov Manshin + Copyright © 2020–2021 Yakov Manshin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index c67b3e1..7a705d4 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,9 @@ enum FeatureFlags { // `resolver` references one or more feature flag stores. // `MyFeatureFlagStore.shared` conforms to `FeatureFlagStoreProtocol`. - private static var resolver: FeatureFlagResolverProtocol = { - FeatureFlagResolver(configuration: .init(persistentStores: [ - .opaque(MyFeatureFlagStore.shared) - ])) - } + private static var resolver = FeatureFlagResolver(configuration: .init( + persistentStores: [.opaque(MyFeatureFlagStore.shared)] + )) // Feature flags are initialized with three pieces of data: // a key string, the default value (used as fallback @@ -118,6 +116,25 @@ To remove the override and revert to using values from persistent stores, you ca FeatureFlags.$promoEnabled.removeRuntimeOverride() ``` +### `UserDefaults` + +Since v1.2.0, you can use `UserDefaults` to read and write feature flag values. Just pass an instance of `UserDefaultsStore` as `runtimeStore` in `FeatureFlagResolverConfiguration`. + +For backward-compatibility reasons, **you can’t use `UserDefaultsStore` and the in-memory `RuntimeOverridesStore` at the same time**. But [it’ll get better in v2](https://github.com/yakovmanshin/YMFF/issues/41). + +```swift +import Foundation + +// The `UserDefaultsStore` store must be both added in `persistentStores` +// and, most importantly, set as the `runtimeStore`. +private static var resolver = FeatureFlagResolver(configuration: .init( + persistentStores: [.userDefaults(UserDefaults.standard)], + runtimeStore: UserDefaultsStore() +)) +``` + +### More + You can browse the source files to learn more about the options available to you. An extended documentation is coming later. ## Contributing diff --git a/Sources/YMFF/FeatureFlagResolverImplementation/Store/FeatureFlagStore.swift b/Sources/YMFF/FeatureFlagResolverImplementation/Store/FeatureFlagStore.swift index 0f659b5..3041c69 100644 --- a/Sources/YMFF/FeatureFlagResolverImplementation/Store/FeatureFlagStore.swift +++ b/Sources/YMFF/FeatureFlagResolverImplementation/Store/FeatureFlagStore.swift @@ -6,12 +6,20 @@ // Copyright © 2020 Yakov Manshin. See the LICENSE file for license info. // +#if canImport(Foundation) +import Foundation +#endif + // 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 @@ -24,6 +32,10 @@ extension FeatureFlagStore: FeatureFlagStoreProtocol { 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 } } diff --git a/Sources/YMFF/FeatureFlagResolverImplementation/Store/UserDefaultsStore.swift b/Sources/YMFF/FeatureFlagResolverImplementation/Store/UserDefaultsStore.swift new file mode 100644 index 0000000..c8ed65b --- /dev/null +++ b/Sources/YMFF/FeatureFlagResolverImplementation/Store/UserDefaultsStore.swift @@ -0,0 +1,48 @@ +// +// UserDefaultsStore.swift +// YMFF +// +// Created by Yakov Manshin on 3/25/21. +// Copyright © 2021 Yakov Manshin. See the LICENSE file for license info. +// + +#if canImport(Foundation) + +import Foundation + +// MARK: - UserDefaultsStore + +/// An object that provides read and write access to feature flag values store in `UserDefaults`. +final public class UserDefaultsStore { + + private let userDefaults: UserDefaults + + /// Initializes a new `UserDefaultsStore`. + /// + /// - Parameter userDefaults: *Optional.* The `UserDefaults` object used to read and write values. + /// `UserDefaults.standard` is used by default. + public init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + +} + +// MARK: - MutableFeatureFlagStoreProtocol + +extension UserDefaultsStore: MutableFeatureFlagStoreProtocol { + + public func value(forKey key: String) -> Value? { + userDefaults.value(forKey: key) as? Value + } + + public func setValue(_ value: Value, forKey key: String) { + userDefaults.setValue(value, forKey: key) + } + + public func removeValue(forKey key: String) { + userDefaults.removeObject(forKey: key) + } + +} + +#endif diff --git a/Tests/YMFFTests/UserDefaultsStoreTests.swift b/Tests/YMFFTests/UserDefaultsStoreTests.swift new file mode 100644 index 0000000..f7585c3 --- /dev/null +++ b/Tests/YMFFTests/UserDefaultsStoreTests.swift @@ -0,0 +1,66 @@ +// +// UserDefaultsStoreTests.swift +// YMFF +// +// Created by Yakov Manshin on 3/21/21. +// Copyright © 2021 Yakov Manshin. See the LICENSE file for license info. +// + +import XCTest +@testable import YMFF + +final class UserDefaultsStoreTests: XCTestCase { + + private var resolver: FeatureFlagResolver! + + private lazy var userDefaults = UserDefaults() + + override func setUp() { + super.setUp() + + resolver = FeatureFlagResolver(configuration: .init( + persistentStores: [.userDefaults(userDefaults)], + runtimeStore: UserDefaultsStore(userDefaults: userDefaults) + )) + } + +} + +extension UserDefaultsStoreTests { + + func testReadValueWithResolver() { + let key = "TEST_UserDefaults_key_123" + let value = 123 + + userDefaults.setValue(value, forKey: key) + + // FIXME: [#40] Can't use `retrievedValue: Int?` here + let retrievedValue = try? resolver.value(for: key) as Int + + XCTAssertEqual(retrievedValue, value) + } + + func testWriteValueWithResolver() { + let key = "TEST_UserDefaults_key_456" + let value = 456 + + try? resolver.overrideInRuntime(key, with: value) + + let retrievedValue = userDefaults.value(forKey: key) as? Int + + XCTAssertEqual(retrievedValue, value) + } + + func testWriteAndReadValueWithResolver() { + let key = "TEST_UserDefaults_key_789" + let value = 789 + + try? resolver.overrideInRuntime(key, with: value) + + // FIXME: [#40] Can't use `retrievedValue: Int?` here + let retrievedValue = try? resolver.value(for: key) as Int + + XCTAssertEqual(retrievedValue, value) + } + +} diff --git a/Tests/YMFFTests/XCTestManifests.swift b/Tests/YMFFTests/XCTestManifests.swift index c6a0e00..8120cee 100644 --- a/Tests/YMFFTests/XCTestManifests.swift +++ b/Tests/YMFFTests/XCTestManifests.swift @@ -53,11 +53,23 @@ extension RuntimeOverridesStoreTests { ] } +extension UserDefaultsStoreTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__UserDefaultsStoreTests = [ + ("testReadValueWithResolver", testReadValueWithResolver), + ("testWriteAndReadValueWithResolver", testWriteAndReadValueWithResolver), + ("testWriteValueWithResolver", testWriteValueWithResolver), + ] +} + public func __allTests() -> [XCTestCaseEntry] { return [ testCase(FeatureFlagResolverTests.__allTests__FeatureFlagResolverTests), testCase(FeatureFlagTests.__allTests__FeatureFlagTests), testCase(RuntimeOverridesStoreTests.__allTests__RuntimeOverridesStoreTests), + testCase(UserDefaultsStoreTests.__allTests__UserDefaultsStoreTests), ] } #endif