Merge branch 'dev' into v3-base

This commit is contained in:
Yakov Manshin 2021-06-05 13:36:16 +03:00
commit d6d5a6c71b
8 changed files with 76 additions and 66 deletions

View File

@ -25,4 +25,4 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Lint Podspec
run: pod lib lint --allow-warnings --verbose
run: pod lib lint --verbose

View File

@ -1,12 +1,12 @@
# YMFF: Feature management made easy
Every company I worked at needed a way to manage availability of features in the apps already shipped to customers. Surprisingly enough, [feature flags](https://en.wikipedia.org/wiki/Feature_toggle) (a.k.a. feature toggles a.k.a. feature switches) tend to cause a lot of struggle.
Every company I worked for needed a way to manage availability of features in the apps already shipped to users. Surprisingly enough, [feature flags](https://en.wikipedia.org/wiki/Feature_toggle) (a.k.a. feature toggles a.k.a. feature switches) tend to cause a lot of struggle.
I aspire to change that.
YMFF is a nice little library that makes management of features with feature flags—and management of the feature flags themselves—a bliss, thanks to Swifts [property wrappers](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617).
YMFF is a nice little library that makes management of features with feature flags—and management of the feature flags themselves—a bliss, thanks to the power of Swifts [property wrappers](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617).
YMFF ships completely ready for use, right out of the box: you get everything you need to start in just a few minutes. But you can also replace nearly any component of the system with your own, customized implementation. Since version 2.0, the implementation and the protocols are in two separate targets (YMFF and YMFFProtocols, respectively).
YMFF ships completely ready for use, right out of the box: you get everything you need to start in just a few minutes. But you can also replace nearly any component of the system with your own, customized implementation. The supplied implementation and the protocols are kept in two separate targets (YMFF and YMFFProtocols, respectively).
## Installation
@ -82,22 +82,22 @@ extension RemoteConfig: FeatureFlagStoreProtocol {
Now `RemoteConfig` is a valid feature flag store.
Alternatively, instead of extending `RemoteConfig`, you can create a custom wrapper object. Thats what I prefer to do in my projects.
Alternatively, you can create a custom wrapper object instead of extending `RemoteConfig`. Thats what I prefer to do in my projects for better flexibility.
## Usage
Heres the most basic way to use YMFF.
Heres the most basic way to use YMFF:
```swift
import YMFF
// For convenience, use an enum to create a namespace.
// For convenience, organize feature flags in a separate namespace using an enum.
enum FeatureFlags {
// `resolver` references one or more feature flag stores.
// `MyFeatureFlagStore.shared` conforms to `FeatureFlagStoreProtocol`.
private static var resolver = FeatureFlagResolver(configuration: .init(stores: [
// If you want to change feature flag values from within your app, youll need at least one mutable store.
.mutable(RuntimeOverridesStore()),
// `MyFeatureFlagStore.shared` conforms to `FeatureFlagStoreProtocol`.
.immutable(MyFeatureFlagStore.shared),
]))
@ -142,7 +142,7 @@ FeatureFlags.$promoEnabled.removeValueFromMutableStore()
### `UserDefaults`
Since v1.2.0, you can use `UserDefaults` to read and write feature flag values. Your changes will persist when the app is restarted.
You can use `UserDefaults` to read and write feature flag values. Your changes will persist when the app is restarted.
```swift
import YMFF

View File

@ -39,7 +39,8 @@ final public class FeatureFlagResolver {
///
/// - Parameter stores: *Required.* The array of feature flag stores.
public convenience init(stores: [FeatureFlagStore]) {
self.init(configuration: FeatureFlagResolverConfiguration(stores: stores))
let configuration: FeatureFlagResolverConfigurationProtocol = FeatureFlagResolverConfiguration(stores: stores)
self.init(configuration: configuration)
}
deinit {

View File

@ -15,23 +15,23 @@ import YMFFProtocols
enum SharedAssets {
static var configuration: FeatureFlagResolverConfiguration {
.init(stores: [
static var configuration: FeatureFlagResolverConfigurationProtocol {
FeatureFlagResolverConfiguration(stores: [
.mutable(RuntimeOverridesStore()),
.immutable(OpaqueStoreWithLimitedTypeSupport(store: remoteStore)),
.immutable(localStore),
])
}
static var configurationWithNoMutableStores: FeatureFlagResolverConfiguration {
.init(stores: [
static var configurationWithNoMutableStores: FeatureFlagResolverConfigurationProtocol {
FeatureFlagResolverConfiguration(stores: [
.immutable(OpaqueStoreWithLimitedTypeSupport(store: remoteStore)),
.immutable(localStore),
])
}
static var configurationWithNoStores: FeatureFlagResolverConfiguration {
.init(stores: [])
static var configurationWithNoStores: FeatureFlagResolverConfigurationProtocol {
FeatureFlagResolverConfiguration(stores: [])
}
private static var localStore: [String : Any] { [

View File

@ -0,0 +1,58 @@
//
// MutableFeatureFlagStore.swift
// YMFF
//
// Created by Yakov Manshin on 5/30/21.
// Copyright © 2021 Yakov Manshin. See the LICENSE file for license info.
//
import YMFF
#if !COCOAPODS
import YMFFProtocols
#endif
final 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

@ -13,8 +13,6 @@ import YMFFProtocols
@testable import YMFF
// MARK: - MutableStoreTests
final class MutableStoreTests: XCTestCase {
private var mutableStore: MutableFeatureFlagStoreProtocol!
@ -24,7 +22,7 @@ final class MutableStoreTests: XCTestCase {
super.setUp()
mutableStore = MutableFeatureFlagStore(store: .init())
resolver = FeatureFlagResolver(configuration: .init(stores: [.mutable(mutableStore)]))
resolver = FeatureFlagResolver(stores: [.mutable(mutableStore)])
}
func testOverride() {
@ -80,50 +78,3 @@ final class MutableStoreTests: XCTestCase {
}
}
// 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()
}
}