Skip to content

Commit

Permalink
Merge pull request #17 from immobiliare/1.2.1
Browse files Browse the repository at this point in the history
Release 1.2.1
  • Loading branch information
malcommac authored Jan 26, 2022
2 parents ba2bb61 + 384d77e commit ee182a0
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 13 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ The following documentation describe detailed usage of the library.
- 1.5 - [Load a Feature Flag Collection in a `FlagLoader`](./documentation/introduction.md#15-load-a-feature-flag-collection-in-a-flagloader)
- 1.6 - [Configure Key Evaluation for `FlagsLoader`'s `@Flag`](./documentation/introduction.md#16-configure-key-evaluation-for-flagsloaders-flag)
- 1.7 - [Query a specific data provider](./documentation/introduction.md#17-query-a-specific-data-provider)
- 1.8 - [Set Flag defaultValue at runtime](./documentation/introduction.md#18-set-flag-defaultvalue-at-runtime)
- 1.9 - [Reset Flag values](./documentation/introduction.md#19-reset-flag-values)
- 1.10 - [Reset LocalProvider values](./documentation/introduction.md#110-reset-localprovider-values)
- 2.0 - [Organize Feature Flags](./documentation/organize_feature_flags.md)
- 2.1 - [The `@FlagCollection` annotation](./documentation/organize_feature_flags.md#21-the-flagcollection-annotation)
- 2.2 - [Nested Structures](./documentation/organize_feature_flags.md#22-nested-structures)
Expand Down
2 changes: 1 addition & 1 deletion RealFlags.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "RealFlags"
s.version = "1.2.0"
s.version = "1.2.1"
s.summary = "Feature flagging framework for Swift"
s.homepage = "https://github.com/immobiliare/RealFlags.git"
s.license = { :type => "MIT", :file => "LICENSE" }
Expand Down
32 changes: 28 additions & 4 deletions RealFlags/Sources/RealFlags/Classes/Flag/Flag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,20 @@ public struct Flag<Value: FlagProtocol>: FeatureFlagConfigurableProtocol, Identi

public typealias ComputedFlagClosure = (() -> Value?)

private class DefaultValueBox<Value> {
var value: Value?
}

// MARK: - Public Properties

/// Unique identifier of the feature flag.
public var id = UUID()

/// The default value for this flag; this value is used when no provider can obtain the
/// value you are requesting. Consider it as a fallback.
public var defaultValue: Value
public var defaultValue: Value {
defaultValueBox.value!
}

/// The value associated with flag; if specified it will be get by reading the value of the provider, otherwise
/// the `defaultValue` is used instead.
Expand All @@ -39,7 +45,7 @@ public struct Flag<Value: FlagProtocol>: FeatureFlagConfigurableProtocol, Identi
public var projectedValue: Flag<Value> {
self
}

/// If specified you can attach a dynamic closure which may help you to compute the value of of the
/// flag. This can be useful when your flags depend from other static or runtime-based values.
/// This value is computed before any provider; if returned value is `nil` the library continue
Expand Down Expand Up @@ -98,6 +104,9 @@ public struct Flag<Value: FlagProtocol>: FeatureFlagConfigurableProtocol, Identi

/// You can force a fixed key for a property instead of using auto-evaluation.
private var fixedKey: String?

/// This is necessary in order to avoid mutable box.
private var defaultValueBox = DefaultValueBox<Value>()

// MARK: - Initialization

Expand All @@ -123,7 +132,7 @@ public struct Flag<Value: FlagProtocol>: FeatureFlagConfigurableProtocol, Identi
computedValue: ComputedFlagClosure? = nil,
description: FlagMetadata) {

self.defaultValue = defaultValue
self.defaultValueBox.value = defaultValue
self.excludedProviders = excludedProviders
self.fixedKey = key
self.computedValue = computedValue
Expand Down Expand Up @@ -163,6 +172,21 @@ public struct Flag<Value: FlagProtocol>: FeatureFlagConfigurableProtocol, Identi
return (nil, nil)
}

/// Change the default fallback value manually.
///
/// - Parameter value: value.
public func setDefault(_ value: Value) {
defaultValueBox.value = value
}

/// Reset value stored in any writable provider assigned to this flag.
/// Non writable provider are ignored.
public func resetValue() throws {
for provider in providers where provider.isWritable {
try provider.resetValueForFlag(key: self.keyPath)
}
}

/// Allows to change the value of feature flag by overwriting it to all or certain types
/// of providers.
///
Expand All @@ -173,7 +197,7 @@ public struct Flag<Value: FlagProtocol>: FeatureFlagConfigurableProtocol, Identi
/// providers assigned to the parent's `FlagLoader`.
/// - Returns: Return the list of provider which accepted the change.
@discardableResult
public func setValue(_ value: Value?, providers: [FlagsProvider.Type]?) -> [FlagsProvider] {
public func setValue(_ value: Value?, providers: [FlagsProvider.Type]? = nil) -> [FlagsProvider] {
var alteredProviders = [FlagsProvider]()
for provider in providersWithTypes(providers) {
do {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ import Foundation

public enum BentoDict {

/// Remove a set key from dictionary.
internal static func removeValue(_ dict: inout [String: Any], forKeyPath keyPath: FlagKeyPath) {
switch keyPath.count {
case 1:
dict.removeValue(forKey: keyPath.first!)
case (2..<Int.max):
let key = keyPath.first!
var subDict = (dict[key] as? [String: Any]) ?? [:]
removeValue(&subDict, forKeyPath: keyPath.dropFirst())
dict[key] = subDict
default:
return
}
}

/// Set the value into a dictionary for a keypath.
///
/// - Parameters:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ public protocol FlagsProvider {
/// Store a new value for a flag value.
///
/// - Parameters:
/// - value: value to set; `nil` value is set to clear any previously set value for given key.
/// - value: value to set.
/// - key: keypath to set.
@discardableResult
func setValue<Value>(_ value: Value?, forFlag key: FlagKeyPath) throws -> Bool where Value: FlagProtocol


/// 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)
// 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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public protocol DelegateProviderProtocol: AnyObject {
/// - key: key.
func setValue<Value>(_ value: Value?, forFlag key: FlagKeyPath) throws -> Bool where Value: FlagProtocol

/// Reset the value for a specified key.
func resetValueForFlag(_ key: FlagKeyPath) throws

}

// MARK: - DelegateProvider
Expand Down Expand Up @@ -67,5 +70,9 @@ public class DelegateProvider: FlagsProvider, Identifiable {

return try delegate?.setValue(value, forFlag: key) ?? false
}

public func resetValueForFlag(key: FlagKeyPath) throws {
try delegate?.resetValueForFlag(key)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ public class LocalProvider: FlagsProvider, Identifiable {
return true
}

/// Reset the value for a flag key inside the local provider.
///
/// - Parameters:
/// - key: key to remove.
/// - save: `true` to save the provider's data snapshot to disk.
public func resetValueForFlag(key: FlagKeyPath) throws {
BentoDict.removeValue(&storage, forKeyPath: key)

try saveToDisk()
}

// MARK: - Persistent Management

/// Force saving of the data locally (only if `localURL` has been set).
Expand All @@ -98,4 +109,14 @@ public class LocalProvider: FlagsProvider, Identifiable {
try data.write(to: localURL)
}

/// The following method reset all the data of the local provider saved
/// to disk restoring and empty dictionary of data.
public func resetAllData() throws {
if let localURL = self.localURL,
FileManager.default.fileExists(atPath: localURL.path) {
try FileManager.default.removeItem(at: localURL)
}
storage.removeAll()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ extension UserDefaults: FlagsProvider {
return true
}

public func resetValueForFlag(key: FlagKeyPath) throws {
removeObject(forKey: key.fullPath)
}

}
20 changes: 16 additions & 4 deletions RealFlags/Sources/RealFlags/Classes/Type-Erased/AnyFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,28 @@ public protocol AnyFlag: AnyFlagOrCollection {
/// - Parameter provider: provider to use.
func setValueToProvider(_ provider: FlagsProvider) throws

/// Change the default fallback value.
/// Value must be of the same type of the Flag inner implementation.
func setDefaultValue(_ value: Any) throws

}

// MARK: - AnyFlag (Flag Conformance)

extension Flag: AnyFlag {

public func setDefaultValue(_ value: Any) throws {
guard let value = value as? Value else {
fatalError("Error is not of the same type as expected: \(String(describing: value))")
}

setDefault(value)
}

public var defaultFallbackValue: Any? {
defaultValue
}

public var hasWritableProvider: Bool {
guard !isUILocked else { return false }

Expand Down Expand Up @@ -97,10 +113,6 @@ extension Flag: AnyFlag {
return type(of: wrappedValue.self)
}
}

public var defaultFallbackValue: Any? {
defaultValue
}

public var providers: [FlagsProvider] {
loader.instance?.providers ?? []
Expand Down
2 changes: 1 addition & 1 deletion RealFlagsFirebase.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "RealFlagsFirebase"
s.version = "1.2.0"
s.version = "1.2.1"
s.summary = "Feature flagging framework for Swift: FirebaseRemoteConfig Data Provider"
s.homepage = "https://github.com/immobiliare/RealFlags.git"
s.license = { :type => "MIT", :file => "LICENSE" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,8 @@ public class FirebaseRemoteProvider: FlagsProvider {
false
}

public func resetValueForFlag(key: FlagKeyPath) throws {
// Not supported for firebase
}

}
17 changes: 17 additions & 0 deletions Tests/RealFlagsTests/FlagProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ final class FlagProviderTests: XCTestCase {
XCTAssert(loader.topLevelFlag1 == "A_VALUE")
}

func testDefaultValueChangeOnWritableProviders() throws {
try [localProviderOne, localProviderTwo].forEach {
try $0.resetAllData()
}

loader.$topLevelFlag.setDefault(1000)
XCTAssert(loader.topLevelFlag == 1000)
}

func testResetAllOnProviders() throws {
resetValuesOnProviders()

XCTAssert(loader.topLevelFlag == 5)
try loader.$topLevelFlag.resetValue()
XCTAssert(loader.topLevelFlag == 0)
}

func testValueDataType() {
XCTAssert(loader.$topLevelFlag.dataType == Int.self)
XCTAssert(loader.$topLevelFlag1.dataType == String.self)
Expand Down
101 changes: 101 additions & 0 deletions Tests/RealFlagsTests/LocalProviderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// RealFlags
// Easily manage Feature Flags in Swift
//
// Created by the Mobile Team @ ImmobiliareLabs
// Email: [email protected]
// Web: http://labs.immobiliare.it
//
// Copyright ©2021 Immobiliare.it SpA. All rights reserved.
// Licensed under MIT License.
//

import Foundation
import XCTest
@testable import RealFlags

class LocalProviderTests: XCTestCase {

fileprivate var loader: FlagsLoader<LPFlagsCollection>!

fileprivate lazy var localProviderFileURL: URL = {
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsDirectory.appendingPathComponent("LocalProviderTest.xml")
return fileURL
}()

fileprivate lazy var localProvider = LocalProvider(localURL: localProviderFileURL)

override func setUp() {
super.setUp()
loader = FlagsLoader<LPFlagsCollection>(LPFlagsCollection.self, providers: [localProvider])
}

override func tearDown() {
super.tearDown()
}

func testResetKeyPathValue() throws {
for _ in 0..<2 {
// Reset any stored data on local provider (only for second pass)
try localProvider.resetAllData()
// Test if file was removed
XCTAssertFalse(FileManager.default.fileExists(atPath: localProviderFileURL.path))

// Returned value should be the default one set
print(loader.nested.flagInt)
XCTAssertEqual(loader.nested.flagInt, 2)

// Chaning the default value should reflect on query
loader.nested.$flagInt.setDefault(100)
XCTAssertEqual(loader.nested.flagInt, 100)

// Changing the value should be reflected on query
loader.nested.$flagInt.setValue(200)
XCTAssertEqual(loader.nested.flagInt, 200)
// Check if file is written correctly
XCTAssertTrue(FileManager.default.fileExists(atPath: localProviderFileURL.path))

let dataString = try String(contentsOf: localProviderFileURL)
let hasValidValue = dataString.contains("<key>flag_int</key>\n\t\t<integer>200</integer>")
XCTAssertTrue(hasValidValue)

// Change root key
loader.$flagBool.setValue(false)

// Reset should return to the last default one set
try? loader.nested.$flagInt.resetValue()
XCTAssertEqual(loader.nested.flagInt, 100)

// Validate other key after reset
XCTAssertFalse(loader.flagBool)
let dataStringAfterReset = try String(contentsOf: localProviderFileURL)
let hasValidValueAfterReset = dataStringAfterReset.contains("<key>flag_bool</key>\n\t<false/>")
XCTAssertTrue(hasValidValueAfterReset)

// Restore initial state
loader.nested.$flagInt.setDefault(2)
}
}
}


fileprivate struct LPFlagsCollection: FlagCollectionProtocol {

@Flag(default: true, description: "")
var flagBool: Bool

@FlagCollection(description: "")
var nested: LPNestedFlagsCollection

}

fileprivate struct LPNestedFlagsCollection: FlagCollectionProtocol {

@Flag(default: 2, description: "")
var flagInt: Int

@Flag(default: "fallback_string", description: "")
var flagString: String

}
Loading

0 comments on commit ee182a0

Please sign in to comment.