Skip to content

Commit

Permalink
feat: add LocalProvider value updates publisher
Browse files Browse the repository at this point in the history
  • Loading branch information
NicFontana committed Jul 12, 2023
1 parent 5c574e4 commit 338b358
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 10 deletions.
2 changes: 1 addition & 1 deletion RealFlags/Sources/RealFlags/Classes/Flag/Flag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// Licensed under MIT License.
//

import UIKit
import Foundation

/// This a wrapper which represent a single Feature Flag.
/// The type that you wrap with `@Flag` must conform to `FlagProtocol`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public enum EncodedFlagValue: Equatable {
}
}

/// Trnsform boxed data in a valid `NSObject` you can store.
/// Transform boxed data in a valid `NSObject` you can store.
///
/// - Returns: NSObject
internal func nsObject() -> NSObject {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// Licensed under MIT License.
//

#if !os(Linux)
#if canImport(Combine)
import Combine
#endif

Expand Down Expand Up @@ -47,28 +47,26 @@ public protocol FlagsProvider {
/// Reset the value for keypath; it will remove the value from the record of the flag provider.
func resetValueForFlag(key: FlagKeyPath) throws

#if !os(Linux)
#if canImport(Combine)
// Apple platform also support Combine framework to provide realtime notification of new events to any subscriber.
// By default it does nothing (take a look to the default implementation in extension below).

/// You can use this value to receive updates when keys did updated in sources.
///
/// - Parameter keys: updated keys; may be `nil` if your provider does not support this kind of granularity.
func didUpdateValuesForKeys(_ keys: Set<String>?) -> AnyPublisher<Set<String>, Never>?
/// You can use this value to receive updates when a flag's values changes inside the current `FlagsProvider`.
func didUpdateValueForKey() -> AnyPublisher<(FlagKeyPath, (any FlagProtocol)?), Never>?

#endif

}

// MARK: - FlagsProvider (Default Publisher Behaviour)

#if !os(Linux)
#if canImport(Combine)

/// Make support for real-time flag updates optional by providing a default nil implementation
///
public extension FlagsProvider {

func didUpdateValuesForKeys(_ keys: Set<String>?) -> AnyPublisher<Set<String>, Never>? {
func didUpdateValueForKey() -> AnyPublisher<(FlagKeyPath, (any FlagProtocol)?), Never>? {
nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
//

import Foundation
#if canImport(Combine)
import Combine
#endif

/// LocalProvider is a local source for feature flags. You can use this object for persistent local data
/// or ephemeral storage.
Expand Down Expand Up @@ -39,6 +42,11 @@ public class LocalProvider: FlagsProvider, Identifiable {
/// Storage data.
internal var storage: [String: Any]

#if canImport(Combine)
/// `PassthroughSubject` that emits when a flag's values changes inside this `LocalProvider`
private let didUpdateValueForKeyPublisher: PassthroughSubject<(FlagKeyPath, (any FlagProtocol)?), Never> = .init()
#endif

// MARK: - Initialization

/// Initialize a new ephemeral storage which will be never saved locally.
Expand Down Expand Up @@ -82,6 +90,11 @@ public class LocalProvider: FlagsProvider, Identifiable {
BentoDict.setValueForDictionary(&storage, value: encodedValue, keyPath: key)

try saveToDisk()

#if canImport(Combine)
didUpdateValueForKeyPublisher.send((key, value))
#endif

return true
}

Expand Down Expand Up @@ -119,4 +132,10 @@ public class LocalProvider: FlagsProvider, Identifiable {
storage.removeAll()
}

#if canImport(Combine)
public func didUpdateValueForKey() -> AnyPublisher<(FlagKeyPath, (any FlagProtocol)?), Never>? {
didUpdateValueForKeyPublisher
.eraseToAnyPublisher()
}
#endif
}
43 changes: 43 additions & 0 deletions Tests/RealFlagsTests/LocalProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@

import Foundation
import XCTest
import Combine
@testable import RealFlags

class LocalProviderTests: XCTestCase {

fileprivate var loader: FlagsLoader<LPFlagsCollection>!
private var cancellables: Set<AnyCancellable>!

fileprivate lazy var localProviderFileURL: URL = {
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
Expand All @@ -29,6 +31,7 @@ class LocalProviderTests: XCTestCase {
override func setUp() {
super.setUp()
loader = FlagsLoader<LPFlagsCollection>(LPFlagsCollection.self, providers: [localProvider])
cancellables = []
}

override func tearDown() {
Expand Down Expand Up @@ -77,6 +80,46 @@ class LocalProviderTests: XCTestCase {
loader.nested.$flagInt.setDefault(2)
}
}

func testDidUpdateValueForKeyPublisher() throws {
// Setup subscriber
let expectation = expectation(description: "didUpdateValueForKeyPublisher")
var didUpdateValueForKeyReceivedValues: [(FlagKeyPath, (any FlagProtocol)?)] = []

localProvider.didUpdateValueForKey()?
.collect(5)
.sink(receiveValue: { values in
didUpdateValueForKeyReceivedValues = values
expectation.fulfill()
})
.store(in: &cancellables)

// Change values
let _ = try localProvider.setValue(true, forFlag: loader.$flagBool.keyPath)
let _ = try localProvider.setValue(false, forFlag: loader.$flagBool.keyPath)
let _ = try localProvider.setValue(10, forFlag: loader.nested.$flagInt.keyPath)
let _ = try localProvider.setValue("Test", forFlag: loader.nested.$flagString.keyPath)
let _ = try localProvider.setValue(Optional<String>.none, forFlag: loader.nested.$flagString.keyPath)

wait(for: [expectation], timeout: 10)

// Assertions on received values
XCTAssertEqual(5, didUpdateValueForKeyReceivedValues.count)
let keyPaths: [FlagKeyPath] = didUpdateValueForKeyReceivedValues.map { $0.0 }
let flags: [(any FlagProtocol)?] = didUpdateValueForKeyReceivedValues.map { $0.1 }

XCTAssertEqual(loader.$flagBool.keyPath, keyPaths[0])
XCTAssertEqual(loader.$flagBool.keyPath, keyPaths[1])
XCTAssertEqual(loader.nested.$flagInt.keyPath, keyPaths[2])
XCTAssertEqual(loader.nested.$flagString.keyPath, keyPaths[3])
XCTAssertEqual(loader.nested.$flagString.keyPath, keyPaths[4])

XCTAssertEqual(true.encoded(), flags[0]?.encoded())
XCTAssertEqual(false.encoded(), flags[1]?.encoded())
XCTAssertEqual(10.encoded(), flags[2]?.encoded())
XCTAssertEqual("Test".encoded(), flags[3]?.encoded())
XCTAssertEqual(nil, flags[4]?.encoded())
}
}


Expand Down

0 comments on commit 338b358

Please sign in to comment.