Skip to content

Commit

Permalink
v1.2.0 (#47)
Browse files Browse the repository at this point in the history
* [#38, #44] Added support for feature flag values in `UserDefaults` (#42, #46)
* [#37] Updated Copyright Year in `LICENSE`
* [#33, #35] Updated CI configuration (#34, #36)
  • Loading branch information
yakovmanshin authored Mar 28, 2021
2 parents ae71331 + c35bef7 commit 6537e4d
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/spm-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Value>(forKey key: String) -> Value? {
userDefaults.value(forKey: key) as? Value
}

public func setValue<Value>(_ value: Value, forKey key: String) {
userDefaults.setValue(value, forKey: key)
}

public func removeValue(forKey key: String) {
userDefaults.removeObject(forKey: key)
}

}

#endif
66 changes: 66 additions & 0 deletions Tests/YMFFTests/UserDefaultsStoreTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}

}
12 changes: 12 additions & 0 deletions Tests/YMFFTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 6537e4d

Please sign in to comment.