From bc911f0a1900fdf540d6809cb99f3c1db47476d5 Mon Sep 17 00:00:00 2001 From: Yakov Manshin Date: Sat, 26 Dec 2020 00:24:13 +0300 Subject: [PATCH 1/5] [#33] Updated CI Config with Xcode 12.3 (#34) --- .github/workflows/spm-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spm-ci.yml b/.github/workflows/spm-ci.yml index ff829a4..1cf2b27 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.3.app - name: Run tests run: swift test -v From 718d93360c3f2374900c10097e12709c73c62418 Mon Sep 17 00:00:00 2001 From: Yakov Manshin Date: Sat, 20 Mar 2021 13:37:48 +0300 Subject: [PATCH 2/5] [#35] Updated CI to Xcode 12.5 (#36) --- .github/workflows/spm-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spm-ci.yml b/.github/workflows/spm-ci.yml index 1cf2b27..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.3.app + run: sudo xcode-select -switch /Applications/Xcode_12.5.app - name: Run tests run: swift test -v From 9dbc903bbc8e805ae27030a33448b5f998ea8f83 Mon Sep 17 00:00:00 2001 From: Yakov Manshin Date: Thu, 25 Mar 2021 22:41:08 +0300 Subject: [PATCH 3/5] [#37] Updated Copyright Year in LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 093ee8f6f9487da400cfa2c8eb5b291b537eef3d Mon Sep 17 00:00:00 2001 From: Yakov Manshin Date: Sun, 28 Mar 2021 14:09:03 +0300 Subject: [PATCH 4/5] [#38] UserDefaults Store (#42) * Introduced `UserDefaultsStore` which can be used as both read-only and mutable store * Extended `FeatureFlagStore` with the new `userDefaults(_:)` case --- .../Store/FeatureFlagStore.swift | 12 ++++ .../Store/UserDefaultsStore.swift | 48 ++++++++++++++ Tests/YMFFTests/UserDefaultsStoreTests.swift | 66 +++++++++++++++++++ Tests/YMFFTests/XCTestManifests.swift | 12 ++++ 4 files changed, 138 insertions(+) create mode 100644 Sources/YMFF/FeatureFlagResolverImplementation/Store/UserDefaultsStore.swift create mode 100644 Tests/YMFFTests/UserDefaultsStoreTests.swift 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 From c35bef79957d1286ceb827f4375f6912bbfe1fbf Mon Sep 17 00:00:00 2001 From: Yakov Manshin Date: Sun, 28 Mar 2021 22:46:07 +0300 Subject: [PATCH 5/5] [#44] Updated README for UserDefaultsStore (#46) * Described how `UserDefaults` can be used to read and write feature flag values * Updated the code example in Usage --- README.md | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) 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