Merge branch 'dev' into v3-base
This commit is contained in:
commit
d6d5a6c71b
|
@ -25,4 +25,4 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Lint Podspec
|
- name: Lint Podspec
|
||||||
run: pod lib lint --allow-warnings --verbose
|
run: pod lib lint --verbose
|
||||||
|
|
16
README.md
16
README.md
|
@ -1,12 +1,12 @@
|
||||||
# YMFF: Feature management made easy
|
# 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.
|
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 Swift’s [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 Swift’s [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
|
## Installation
|
||||||
|
|
||||||
|
@ -82,22 +82,22 @@ extension RemoteConfig: FeatureFlagStoreProtocol {
|
||||||
|
|
||||||
Now `RemoteConfig` is a valid feature flag store.
|
Now `RemoteConfig` is a valid feature flag store.
|
||||||
|
|
||||||
Alternatively, instead of extending `RemoteConfig`, you can create a custom wrapper object. That’s what I prefer to do in my projects.
|
Alternatively, you can create a custom wrapper object instead of extending `RemoteConfig`. That’s what I prefer to do in my projects for better flexibility.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
Here’s the most basic way to use YMFF.
|
Here’s the most basic way to use YMFF:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
import YMFF
|
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 {
|
enum FeatureFlags {
|
||||||
|
|
||||||
// `resolver` references one or more feature flag stores.
|
// `resolver` references one or more feature flag stores.
|
||||||
// `MyFeatureFlagStore.shared` conforms to `FeatureFlagStoreProtocol`.
|
|
||||||
private static var resolver = FeatureFlagResolver(configuration: .init(stores: [
|
private static var resolver = FeatureFlagResolver(configuration: .init(stores: [
|
||||||
// If you want to change feature flag values from within your app, you’ll need at least one mutable store.
|
// If you want to change feature flag values from within your app, you’ll need at least one mutable store.
|
||||||
.mutable(RuntimeOverridesStore()),
|
.mutable(RuntimeOverridesStore()),
|
||||||
|
// `MyFeatureFlagStore.shared` conforms to `FeatureFlagStoreProtocol`.
|
||||||
.immutable(MyFeatureFlagStore.shared),
|
.immutable(MyFeatureFlagStore.shared),
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ FeatureFlags.$promoEnabled.removeValueFromMutableStore()
|
||||||
|
|
||||||
### `UserDefaults`
|
### `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
|
```swift
|
||||||
import YMFF
|
import YMFF
|
||||||
|
|
|
@ -39,7 +39,8 @@ final public class FeatureFlagResolver {
|
||||||
///
|
///
|
||||||
/// - Parameter stores: *Required.* The array of feature flag stores.
|
/// - Parameter stores: *Required.* The array of feature flag stores.
|
||||||
public convenience init(stores: [FeatureFlagStore]) {
|
public convenience init(stores: [FeatureFlagStore]) {
|
||||||
self.init(configuration: FeatureFlagResolverConfiguration(stores: stores))
|
let configuration: FeatureFlagResolverConfigurationProtocol = FeatureFlagResolverConfiguration(stores: stores)
|
||||||
|
self.init(configuration: configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
|
|
@ -15,23 +15,23 @@ import YMFFProtocols
|
||||||
|
|
||||||
enum SharedAssets {
|
enum SharedAssets {
|
||||||
|
|
||||||
static var configuration: FeatureFlagResolverConfiguration {
|
static var configuration: FeatureFlagResolverConfigurationProtocol {
|
||||||
.init(stores: [
|
FeatureFlagResolverConfiguration(stores: [
|
||||||
.mutable(RuntimeOverridesStore()),
|
.mutable(RuntimeOverridesStore()),
|
||||||
.immutable(OpaqueStoreWithLimitedTypeSupport(store: remoteStore)),
|
.immutable(OpaqueStoreWithLimitedTypeSupport(store: remoteStore)),
|
||||||
.immutable(localStore),
|
.immutable(localStore),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
static var configurationWithNoMutableStores: FeatureFlagResolverConfiguration {
|
static var configurationWithNoMutableStores: FeatureFlagResolverConfigurationProtocol {
|
||||||
.init(stores: [
|
FeatureFlagResolverConfiguration(stores: [
|
||||||
.immutable(OpaqueStoreWithLimitedTypeSupport(store: remoteStore)),
|
.immutable(OpaqueStoreWithLimitedTypeSupport(store: remoteStore)),
|
||||||
.immutable(localStore),
|
.immutable(localStore),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
static var configurationWithNoStores: FeatureFlagResolverConfiguration {
|
static var configurationWithNoStores: FeatureFlagResolverConfigurationProtocol {
|
||||||
.init(stores: [])
|
FeatureFlagResolverConfiguration(stores: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var localStore: [String : Any] { [
|
private static var localStore: [String : Any] { [
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -13,8 +13,6 @@ import YMFFProtocols
|
||||||
|
|
||||||
@testable import YMFF
|
@testable import YMFF
|
||||||
|
|
||||||
// MARK: - MutableStoreTests
|
|
||||||
|
|
||||||
final class MutableStoreTests: XCTestCase {
|
final class MutableStoreTests: XCTestCase {
|
||||||
|
|
||||||
private var mutableStore: MutableFeatureFlagStoreProtocol!
|
private var mutableStore: MutableFeatureFlagStoreProtocol!
|
||||||
|
@ -24,7 +22,7 @@ final class MutableStoreTests: XCTestCase {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
|
|
||||||
mutableStore = MutableFeatureFlagStore(store: .init())
|
mutableStore = MutableFeatureFlagStore(store: .init())
|
||||||
resolver = FeatureFlagResolver(configuration: .init(stores: [.mutable(mutableStore)]))
|
resolver = FeatureFlagResolver(stores: [.mutable(mutableStore)])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOverride() {
|
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in New Issue