diff --git a/Errors/TweakError.swift b/Errors/TweakError.swift new file mode 100644 index 0000000..a3bb454 --- /dev/null +++ b/Errors/TweakError.swift @@ -0,0 +1,12 @@ +// +// TweakError.swift +// Copyright (c) 2022 Just Eat Holding Ltd. All rights reserved. +// + +import Foundation + +public enum TweakError: String, Error { + case notFound = "Feature or variable is not found" + case notSupported = "Variable type is not supported" + case decryptionClosureNotProvided = "Value is encrypted but there's no decryption closure provided" +} diff --git a/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift b/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift index 413ddfa..16b35f8 100644 --- a/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift +++ b/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift @@ -15,47 +15,47 @@ class GeneratedTweakAccessor { } var canShowGreenView: Bool { - get { tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayGreenView)?.boolValue ?? false } + get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayGreenView))?.boolValue ?? false } set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayGreenView) } } var canShowRedView: Bool { - get { tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView)?.boolValue ?? false } + get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView))?.boolValue ?? false } set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayRedView) } } var canShowYellowView: Bool { - get { tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView)?.boolValue ?? false } + get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView))?.boolValue ?? false } set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayYellowView) } } var definitiveAnswerEncrypted: String { - get { tweakManager.tweakWith(feature: Features.general, variable: Variables.encryptedAnswerToTheUniverse)?.stringValue ?? "" } + get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.encryptedAnswerToTheUniverse))?.stringValue ?? "" } set { tweakManager.set(newValue, feature: Features.general, variable: Variables.encryptedAnswerToTheUniverse) } } var isTapGestureToChangeColorEnabled: Bool { - get { tweakManager.tweakWith(feature: Features.general, variable: Variables.tapToChangeColorEnabled)?.boolValue ?? false } + get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.tapToChangeColorEnabled))?.boolValue ?? false } set { tweakManager.set(newValue, feature: Features.general, variable: Variables.tapToChangeColorEnabled) } } var labelText: String { - get { tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.labelText)?.stringValue ?? "" } + get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.labelText))?.stringValue ?? "" } set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.labelText) } } var meaningOfLife: Int { - get { tweakManager.tweakWith(feature: Features.general, variable: Variables.answerToTheUniverse)?.intValue ?? 0 } + get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.answerToTheUniverse))?.intValue ?? 0 } set { tweakManager.set(newValue, feature: Features.general, variable: Variables.answerToTheUniverse) } } var redViewAlpha: Double { - get { tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.redViewAlphaComponent)?.doubleValue ?? 0.0 } + get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.redViewAlphaComponent))?.doubleValue ?? 0.0 } set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.redViewAlphaComponent) } } var shouldShowAlert: Bool { - get { tweakManager.tweakWith(feature: Features.general, variable: Variables.greetOnAppDidBecomeActive)?.boolValue ?? false } + get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.greetOnAppDidBecomeActive))?.boolValue ?? false } set { tweakManager.set(newValue, feature: Features.general, variable: Variables.greetOnAppDidBecomeActive) } } } \ No newline at end of file diff --git a/Example/JustTweak/Accessors/TweakAccessor.swift b/Example/JustTweak/Accessors/TweakAccessor.swift index 918ea91..f97ec19 100644 --- a/Example/JustTweak/Accessors/TweakAccessor.swift +++ b/Example/JustTweak/Accessors/TweakAccessor.swift @@ -87,18 +87,18 @@ class TweakAccessor { // MARK: - Via TweakManager var canShowYellowView: Bool { - return tweakManager.tweakWith(feature: Features.uiCustomization, - variable: Variables.displayYellowView)?.boolValue ?? false + return (try? tweakManager.tweakWith(feature: Features.uiCustomization, + variable: Variables.displayYellowView))?.boolValue ?? false } - + var redViewAlpha: Float { - return tweakManager.tweakWith(feature: Features.uiCustomization, - variable: Variables.redViewAlpha)?.floatValue ?? 0.0 + return (try? tweakManager.tweakWith(feature: Features.uiCustomization, + variable: Variables.redViewAlpha))?.floatValue ?? 0.0 } - + var isTapGestureToChangeColorEnabled: Bool { - return tweakManager.tweakWith(feature: Features.general, - variable: Variables.tapToChangeViewColor)?.boolValue ?? false + return (try? tweakManager.tweakWith(feature: Features.general, + variable: Variables.tapToChangeViewColor))?.boolValue ?? false } } diff --git a/Example/JustTweak/TweakProviders/FirebaseTweakProvider.swift b/Example/JustTweak/TweakProviders/FirebaseTweakProvider.swift index 34f02ec..ece320f 100644 --- a/Example/JustTweak/TweakProviders/FirebaseTweakProvider.swift +++ b/Example/JustTweak/TweakProviders/FirebaseTweakProvider.swift @@ -55,8 +55,8 @@ public class FirebaseTweakProvider: TweakProvider { return configValue.boolValue } - public func tweakWith(feature: String, variable: String) -> Tweak? { - guard configured else { return nil } + public func tweakWith(feature: String, variable: String) throws -> Tweak { + guard configured else { throw TweakError.notFound } let configValue = remoteConfiguration.configValue(forKey: variable) guard configValue.source != .static else { return nil } guard let stringValue = configValue.stringValue else { return nil } diff --git a/Example/JustTweak/TweakProviders/OptimizelyTweakProvider.swift b/Example/JustTweak/TweakProviders/OptimizelyTweakProvider.swift index 3149274..064d6a2 100644 --- a/Example/JustTweak/TweakProviders/OptimizelyTweakProvider.swift +++ b/Example/JustTweak/TweakProviders/OptimizelyTweakProvider.swift @@ -47,9 +47,12 @@ public class OptimizelyTweakProvider: TweakProvider { return optimizelyClient?.isFeatureEnabled(feature, userId: userId, attributes: attributes) ?? false } - public func tweakWith(feature: String, variable: String) -> Tweak? { - guard let optimizelyClient = optimizelyClient else { return nil } - guard optimizelyClient.isFeatureEnabled(feature, userId: userId, attributes: attributes) == true else { return nil } + public func tweakWith(feature: String, variable: String) throws -> Tweak { + guard let optimizelyClient = optimizelyClient, + optimizelyClient.isFeatureEnabled(feature, userId: userId, attributes: attributes) == true + else { + throw TweakError.notFound + } let tweakValue: TweakValue? = { if let boolValue = optimizelyClient.getFeatureVariableBoolean(feature, variableKey: variable, userId: userId, attributes: attributes)?.boolValue { @@ -66,11 +69,10 @@ public class OptimizelyTweakProvider: TweakProvider { } return nil }() - - if let tweakValue = tweakValue { - return Tweak(feature: feature, variable: variable, value: tweakValue, title: nil, group: nil) + + guard let tweakValue = tweakValue else { + throw TweakError.notFound } - - return nil + return Tweak(feature: feature, variable: variable, value: tweakValue, title: nil, group: nil) } } diff --git a/Example/Tests/Core/LocalTweakProviderTests.swift b/Example/Tests/Core/LocalTweakProviderTests.swift index 919d0bb..f2f34b9 100644 --- a/Example/Tests/Core/LocalTweakProviderTests.swift +++ b/Example/Tests/Core/LocalTweakProviderTests.swift @@ -32,8 +32,8 @@ class LocalTweakProviderTests: XCTestCase { value: true, title: "Display Red View", group: "UI Customization") - XCTAssertEqual(redViewTweak, tweakProvider.tweakWith(feature: Features.uiCustomization, - variable: Variables.displayRedView)) + XCTAssertEqual(redViewTweak, try tweakProvider.tweakWith(feature: Features.uiCustomization, + variable: Variables.displayRedView)) } func testParsesFloatTweak() { @@ -42,8 +42,8 @@ class LocalTweakProviderTests: XCTestCase { value: 1.0, title: "Red View Alpha Component", group: "UI Customization") - XCTAssertEqual(redViewAlphaTweak, tweakProvider.tweakWith(feature: Features.uiCustomization, - variable: Variables.redViewAlpha)) + XCTAssertEqual(redViewAlphaTweak, try tweakProvider.tweakWith(feature: Features.uiCustomization, + variable: Variables.redViewAlpha)) } func testParsesStringTweak() { @@ -51,8 +51,8 @@ class LocalTweakProviderTests: XCTestCase { variable: Variables.labelText, value: "Test value", title: "Label Text", group: "UI Customization") - XCTAssertEqual(buttonLabelTweak, tweakProvider.tweakWith(feature: Features.uiCustomization, - variable: Variables.labelText)) + XCTAssertEqual(buttonLabelTweak, try tweakProvider.tweakWith(feature: Features.uiCustomization, + variable: Variables.labelText)) } func testDecryptionClosure() { diff --git a/Example/Tests/Core/TweakManagerCacheTests.swift b/Example/Tests/Core/TweakManagerCacheTests.swift index ef089b4..f9b7fea 100644 --- a/Example/Tests/Core/TweakManagerCacheTests.swift +++ b/Example/Tests/Core/TweakManagerCacheTests.swift @@ -5,6 +5,7 @@ import XCTest @testable import JustTweak +@testable import JustTweak_Example fileprivate struct Constants { static let featureActiveValue = true @@ -55,28 +56,28 @@ class TweakManagerCacheTests: XCTestCase { // MARK: - tweakWith(feature:variable:) - func testTweakFetch_CacheDisabled() { - tweakFetch(useCache: false) + func testTweakFetch_CacheDisabled() throws { + try tweakFetch(useCache: false) } - func testTweakFetch_CacheEnabled() { - tweakFetch(useCache: true) + func testTweakFetch_CacheEnabled() throws { + try tweakFetch(useCache: true) } - private func tweakFetch(useCache: Bool) { + private func tweakFetch(useCache: Bool) throws { tweakManager.useCache = useCache XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, 0) let value = true tweakManager.set(value, feature: Constants.feature, variable: Constants.variable) - XCTAssertEqual(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)!.value as! Bool, value) + XCTAssertEqual(try XCTUnwrap(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)).value as! Bool, value) XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, 1) - XCTAssertEqual(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)!.value as! Bool, value) + XCTAssertEqual(try XCTUnwrap(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)).value as! Bool, value) XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, useCache ? 1 : 2) tweakManager.set(value, feature: Constants.feature, variable: Constants.variable) - XCTAssertEqual(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)!.value as! Bool, value) + XCTAssertEqual(try XCTUnwrap(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)).value as! Bool, value) XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, useCache ? 2 : 3) tweakManager.resetCache() - XCTAssertEqual(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)!.value as! Bool, value) + XCTAssertEqual(try XCTUnwrap(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)).value as! Bool, value) XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, useCache ? 3 : 4) } } @@ -97,9 +98,12 @@ fileprivate class MockTweakProvider: MutableTweakProvider { return featureBackingStore[feature] ?? false } - func tweakWith(feature: String, variable: String) -> Tweak? { + func tweakWith(feature: String, variable: String) throws -> Tweak { tweakWithFeatureVariableCallsCounter += 1 - return tweakBackingStore[feature]?[variable] + guard let tweak = tweakBackingStore[feature]?[variable] else { + throw TweakError.notFound + } + return tweak } func set(_ value: TweakValue, feature: String, variable: String) { diff --git a/Example/Tests/Core/TweakManagerTests.swift b/Example/Tests/Core/TweakManagerTests.swift index 3b8a1cc..d2753bb 100644 --- a/Example/Tests/Core/TweakManagerTests.swift +++ b/Example/Tests/Core/TweakManagerTests.swift @@ -37,33 +37,33 @@ class TweakManagerTests: XCTestCase { } func testReturnsNil_ForUndefinedTweak() { - XCTAssertNil(tweakManager.tweakWith(feature: Features.uiCustomization, variable: "some_undefined_tweak")) + XCTAssertNil(try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: "some_undefined_tweak")) } - func testReturnsRemoteConfigValue_ForDisplayRedViewTweak() { - XCTAssertTrue(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView)!.boolValue) + func testReturnsRemoteConfigValue_ForDisplayRedViewTweak() throws { + XCTAssertTrue(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView)).boolValue) } - func testReturnsRemoteConfigValue_ForDisplayYellowViewTweak() { - XCTAssertFalse(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView)!.boolValue) + func testReturnsRemoteConfigValue_ForDisplayYellowViewTweak() throws { + XCTAssertFalse(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView)).boolValue) } - func testReturnsRemoteConfigValue_ForDisplayGreenViewTweak() { - XCTAssertFalse(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayGreenView)!.boolValue) + func testReturnsRemoteConfigValue_ForDisplayGreenViewTweak() throws { + XCTAssertFalse(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayGreenView)).boolValue) } - func testReturnsRemoteConfigValue_ForGreetOnAppDidBecomeActiveTweak() { - XCTAssertTrue(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive)!.boolValue) + func testReturnsRemoteConfigValue_ForGreetOnAppDidBecomeActiveTweak() throws { + XCTAssertTrue(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive)).boolValue) } - func testReturnsJSONConfigValue_ForTapToChangeViewColorTweak_AsYetUnkown() { - XCTAssertTrue(tweakManager.tweakWith(feature: Features.general, variable: Variables.tapToChangeViewColor)!.boolValue) + func testReturnsJSONConfigValue_ForTapToChangeViewColorTweak_AsYetUnkown() throws { + XCTAssertTrue(try XCTUnwrap(tweakManager.tweakWith(feature: Features.general, variable: Variables.tapToChangeViewColor)).boolValue) } - func testReturnsUserSetValue_ForGreetOnAppDidBecomeActiveTweak_AfterUpdatingUserDefaultsTweakProvider() { + func testReturnsUserSetValue_ForGreetOnAppDidBecomeActiveTweak_AfterUpdatingUserDefaultsTweakProvider() throws { let mutableTweakProvider = tweakManager.mutableTweakProvider! mutableTweakProvider.set(false, feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive) - XCTAssertFalse(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive)!.boolValue) + XCTAssertFalse(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive)).boolValue) } func testCallsClosureForRegisteredObserverWhenAnyConfigurationChanges() { @@ -100,7 +100,7 @@ class TweakManagerTests: XCTestCase { let feature = "general" let variable = "encrypted_answer_to_the_universe" - let tweak = tweakManager.tweakWith(feature: feature, variable: variable) + let tweak = try? tweakManager.tweakWith(feature: feature, variable: variable) XCTAssertEqual("Definitely not 42", tweak?.stringValue) } @@ -132,8 +132,8 @@ fileprivate class MockTweakProvider: TweakProvider { return false } - func tweakWith(feature: String, variable: String) -> Tweak? { - guard let value = knownValues[variable] else { return nil } + func tweakWith(feature: String, variable: String) throws -> Tweak { + guard let value = knownValues[variable] else { throw TweakError.notFound } return Tweak(feature: feature, variable: variable, value: value["Value"]!, title: nil, group: nil) } } diff --git a/Example/Tests/Core/UserDefaultsTweakProviderTests.swift b/Example/Tests/Core/UserDefaultsTweakProviderTests.swift index 91f6ba7..3bbeeba 100644 --- a/Example/Tests/Core/UserDefaultsTweakProviderTests.swift +++ b/Example/Tests/Core/UserDefaultsTweakProviderTests.swift @@ -24,52 +24,52 @@ class UserDefaultsTweakProviderTests: XCTestCase { super.tearDown() } - func testReturnsCorrectTweaksIdentifiersWhenInitializedAndTweaksHaveBeenSet() { + func testReturnsCorrectTweaksIdentifiersWhenInitializedAndTweaksHaveBeenSet() throws { let anotherConfiguration = UserDefaultsTweakProvider(userDefaults: userDefaults) anotherConfiguration.set("hello", feature: "feature_1", variable: "variable_4") anotherConfiguration.set(true, feature: "feature_1", variable: "variable_3") anotherConfiguration.set(12.34, feature: "feature_1", variable: "variable_2") anotherConfiguration.set(42, feature: "feature_1", variable: "variable_1") - XCTAssertTrue(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_1")!.value == 42) - XCTAssertTrue(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_2")!.value == 12.34) - XCTAssertTrue(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_3")!.value == true) - XCTAssertTrue(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_4")!.value == "hello") + XCTAssertTrue(try XCTUnwrap(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_1")).value == 42) + XCTAssertTrue(try XCTUnwrap(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_2")).value == 12.34) + XCTAssertTrue(try XCTUnwrap(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_3")).value == true) + XCTAssertTrue(try XCTUnwrap(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_4")).value == "hello") } func testReturnsNilForTweaksThatHaveNoUserDefaultValue() { - let tweak = userDefaultsTweakProvider.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView) + let tweak = try? userDefaultsTweakProvider.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView) XCTAssertNil(tweak) } - func testUpdatesValueForTweak_withBool() { + func testUpdatesValueForTweak_withBool() throws { userDefaultsTweakProvider.set(true, feature: "feature_1", variable: "variable_1") - let tweak = userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1") - XCTAssertTrue(tweak!.value == true) + let tweak = try XCTUnwrap(userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1")) + XCTAssertTrue(tweak.value.boolValue) } - func testUpdatesValueForTweak_withInteger() { + func testUpdatesValueForTweak_withInteger() throws { userDefaultsTweakProvider.set(42, feature: "feature_1", variable: "variable_1") - let tweak = userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1") - XCTAssertTrue(tweak!.value == 42) + let tweak = try XCTUnwrap(userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1")) + XCTAssertTrue(tweak.value == 42) } - func testUpdatesValueForTweak_withFloat() { + func testUpdatesValueForTweak_withFloat() throws { userDefaultsTweakProvider.set(Float(12.34), feature: "feature_1", variable: "variable_1") - let tweak = userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1") - XCTAssertTrue(tweak!.value == Float(12.34)) + let tweak = try XCTUnwrap(userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1")) + XCTAssertTrue(tweak.value == Float(12.34)) } - func testUpdatesValueForTweak_withDouble() { + func testUpdatesValueForTweak_withDouble() throws { userDefaultsTweakProvider.set(Double(23.45), feature: "feature_1", variable: "variable_1") - let tweak = userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1") - XCTAssertTrue(tweak!.value == Double(23.45)) + let tweak = try XCTUnwrap(userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1")) + XCTAssertTrue(tweak.value == Double(23.45)) } - func testUpdatesValueForTweak_withString() { + func testUpdatesValueForTweak_withString() throws { userDefaultsTweakProvider.set("Hello", feature: "feature_1", variable: "variable_1") - let tweak = userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1") - XCTAssertTrue(tweak!.value == "Hello") + let tweak = try XCTUnwrap(userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1")) + XCTAssertTrue(tweak.value == "Hello") } func testDecryptionClosure() { diff --git a/Example/Tests/UI/TweakViewControllerTests.swift b/Example/Tests/UI/TweakViewControllerTests.swift index 5416e4e..84abd94 100644 --- a/Example/Tests/UI/TweakViewControllerTests.swift +++ b/Example/Tests/UI/TweakViewControllerTests.swift @@ -123,7 +123,7 @@ class TweakViewControllerTests: XCTestCase { // MARK: Cells Actions - func testUpdatesValueOfTweak_WhenUserTooglesSwitchOnBooleanCell() { + func testUpdatesValueOfTweak_WhenUserTooglesSwitchOnBooleanCell() throws { viewController.beginAppearanceTransition(true, animated: false) viewController.endAppearanceTransition() @@ -131,7 +131,7 @@ class TweakViewControllerTests: XCTestCase { let cell = viewController.tableView.cellForRow(at: indexPath) as! BooleanTweakTableViewCell cell.switchControl.isOn = true cell.switchControl.sendActions(for: .valueChanged) - XCTAssertTrue(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView)!.boolValue) + XCTAssertTrue(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView)).boolValue) } // MARK: Other Actions diff --git a/JustTweak.podspec b/JustTweak.podspec index bf72ec0..93f0b6d 100644 --- a/JustTweak.podspec +++ b/JustTweak.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'JustTweak' - s.version = '9.0.0' + s.version = '10.0.0' s.summary = 'A framework for feature flagging, locally and remotely configure and A/B test iOS apps.' s.description = <<-DESC JustTweak is a framework for feature flagging, locally and remotely configure and A/B test iOS apps. diff --git a/JustTweak/Assets/TweakAccessorGenerator.bundle/TweakAccessorGenerator b/JustTweak/Assets/TweakAccessorGenerator.bundle/TweakAccessorGenerator index 7b86b85..17cb431 100755 Binary files a/JustTweak/Assets/TweakAccessorGenerator.bundle/TweakAccessorGenerator and b/JustTweak/Assets/TweakAccessorGenerator.bundle/TweakAccessorGenerator differ diff --git a/JustTweak/Classes/Protocols/TweakProvider.swift b/JustTweak/Classes/Protocols/TweakProvider.swift index 4385f97..ffbdee8 100644 --- a/JustTweak/Classes/Protocols/TweakProvider.swift +++ b/JustTweak/Classes/Protocols/TweakProvider.swift @@ -14,7 +14,7 @@ public typealias LogClosure = (String, LogLevel) -> Void public protocol TweakProvider { var logClosure: LogClosure? { set get } func isFeatureEnabled(_ feature: String) -> Bool - func tweakWith(feature: String, variable: String) -> Tweak? + func tweakWith(feature: String, variable: String) throws -> Tweak var decryptionClosure: ((Tweak) -> TweakValue)? { get set } } diff --git a/JustTweak/Classes/TweakManager.swift b/JustTweak/Classes/TweakManager.swift index aedf024..494f60e 100644 --- a/JustTweak/Classes/TweakManager.swift +++ b/JustTweak/Classes/TweakManager.swift @@ -59,7 +59,7 @@ final public class TweakManager { } extension TweakManager: MutableTweakProvider { - + public func isFeatureEnabled(_ feature: String) -> Bool { queue.sync { if useCache, let cachedFeature = featureCache[feature] { @@ -81,8 +81,8 @@ extension TweakManager: MutableTweakProvider { } } - public func tweakWith(feature: String, variable: String) -> Tweak? { - queue.sync { + public func tweakWith(feature: String, variable: String) throws -> Tweak { + try queue.sync { if useCache, let cachedTweaks = tweakCache[feature], let cachedTweak = cachedTweaks[variable] { logClosure?("Tweak '\(cachedTweak)' found in cache.)", .verbose) return cachedTweak @@ -90,7 +90,7 @@ extension TweakManager: MutableTweakProvider { var result: Tweak? = nil for (_, tweakProvider) in tweakProviders.enumerated() { - if let tweak = tweakProvider.tweakWith(feature: feature, variable: variable) { + if let tweak = try? tweakProvider.tweakWith(feature: feature, variable: variable) { logClosure?("Tweak '\(tweak)' found in configuration \(tweakProvider))", .verbose) result = Tweak(feature: feature, @@ -100,24 +100,22 @@ extension TweakManager: MutableTweakProvider { group: tweak.group, source: "\(type(of: tweakProvider))") break - } - else { + } else { let logMessage = "Tweak with identifier '\(variable)' in configuration \(tweakProvider)) could NOT be found or has an invalid configuration" logClosure?(logMessage, .verbose) } } - if let result = result { - logClosure?("Tweak with feature '\(feature)' and variable '\(variable)' resolved. Using '\(result)'.", .debug) - if useCache { - if let _ = tweakCache[feature] { - tweakCache[feature]?[variable] = result - } else { - tweakCache[feature] = [variable : result] - } - } - } - else { + guard let result = result else { logClosure?("No Tweak found for identifier '\(variable)'", .verbose) + throw TweakError.notFound + } + logClosure?("Tweak with feature '\(feature)' and variable '\(variable)' resolved. Using '\(result)'.", .debug) + if useCache { + if let _ = tweakCache[feature] { + tweakCache[feature]?[variable] = result + } else { + tweakCache[feature] = [variable : result] + } } return result } diff --git a/JustTweak/Classes/TweakProviders/EphemeralTweakProvider.swift b/JustTweak/Classes/TweakProviders/EphemeralTweakProvider.swift index 856603b..12ab2e5 100644 --- a/JustTweak/Classes/TweakProviders/EphemeralTweakProvider.swift +++ b/JustTweak/Classes/TweakProviders/EphemeralTweakProvider.swift @@ -23,8 +23,8 @@ extension NSDictionary: TweakProvider { self[feature] as? Bool ?? false } - public func tweakWith(feature: String, variable: String) -> Tweak? { - guard let storedValue = self[variable] else { return nil } + public func tweakWith(feature: String, variable: String) throws -> Tweak { + guard let storedValue = self[variable] else { throw TweakError.notFound } var value: TweakValue? = nil if let theValue = storedValue as? String { value = theValue @@ -32,7 +32,7 @@ extension NSDictionary: TweakProvider { else if let theValue = storedValue as? NSNumber { value = theValue.tweakValue } - guard let finalValue = value else { return nil } + guard let finalValue = value else { throw TweakError.notSupported } return Tweak(feature: feature, variable: variable, value: finalValue) } } diff --git a/JustTweak/Classes/TweakProviders/LocalTweakProvider.swift b/JustTweak/Classes/TweakProviders/LocalTweakProvider.swift index e3e631c..58ca735 100644 --- a/JustTweak/Classes/TweakProviders/LocalTweakProvider.swift +++ b/JustTweak/Classes/TweakProviders/LocalTweakProvider.swift @@ -60,8 +60,8 @@ extension LocalTweakProvider: TweakProvider { return configurationFile[feature] != nil } - public func tweakWith(feature: String, variable: String) -> Tweak? { - guard let entry = configurationFile[feature]?[variable] else { return nil } + public func tweakWith(feature: String, variable: String) throws -> Tweak { + guard let entry = configurationFile[feature]?[variable] else { throw TweakError.notFound } let title = entry[EncodingKeys.Title.rawValue] as? String let description = entry[EncodingKeys.Description.rawValue] as? String let group = entry[EncodingKeys.Group.rawValue] as? String @@ -78,8 +78,7 @@ extension LocalTweakProvider: TweakProvider { if isEncrypted { guard let decryptionClosure = decryptionClosure else { // The configuration is not correct, it's encrypted, but there's no way to decrypt - // So return nil to indicate an error. Should be changed to a throwing function in the future - return nil + throw TweakError.decryptionClosureNotProvided } return tweak.mutatedCopy(value: decryptionClosure(tweak)) diff --git a/JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift b/JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift index 85453a3..62054f9 100644 --- a/JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift +++ b/JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift @@ -26,10 +26,10 @@ extension UserDefaultsTweakProvider: TweakProvider { return userDefaults.bool(forKey: userDefaultsKey) } - public func tweakWith(feature: String, variable: String) -> Tweak? { + public func tweakWith(feature: String, variable: String) throws -> Tweak { let userDefaultsKey = keyForTweakWithIdentifier(variable) let userDefaultsValue = userDefaults.object(forKey: userDefaultsKey) as AnyObject? - guard let value = updateUserDefaults(userDefaultsValue) else { return nil } + guard let value = updateUserDefaults(userDefaultsValue) else { throw TweakError.notFound } return Tweak( feature: feature, diff --git a/JustTweak/Classes/UI/TweakManager+Presentation.swift b/JustTweak/Classes/UI/TweakManager+Presentation.swift index c069984..663a04b 100644 --- a/JustTweak/Classes/UI/TweakManager+Presentation.swift +++ b/JustTweak/Classes/UI/TweakManager+Presentation.swift @@ -12,15 +12,14 @@ extension TweakManager { for localTweakProvider in self.localTweakProviders.reversed() { for (feature, variables) in localTweakProvider.features { for variable in variables { - if let tweak = tweakWith(feature: feature, variable: variable), - let jsonTweak = localTweakProvider.tweakWith(feature: feature, variable: variable) { + if let tweak = try? tweakWith(feature: feature, variable: variable), + let jsonTweak = try? localTweakProvider.tweakWith(feature: feature, variable: variable) { let aggregatedTweak = Tweak(feature: feature, variable: variable, value: tweak.value, title: jsonTweak.title, description: jsonTweak.desc, group: jsonTweak.group) - let key = "\(feature)-\(variable)" tweaks[key] = aggregatedTweak } diff --git a/JustTweak/Classes/Utilities/PropertyWrappers.swift b/JustTweak/Classes/Utilities/PropertyWrappers.swift index 919dde8..f021c4c 100644 --- a/JustTweak/Classes/Utilities/PropertyWrappers.swift +++ b/JustTweak/Classes/Utilities/PropertyWrappers.swift @@ -19,8 +19,8 @@ public struct TweakProperty { public var wrappedValue: T { get { - let tweak = tweakManager.tweakWith(feature: feature, variable: variable) - return tweak!.value as! T + let tweak = try? tweakManager.tweakWith(feature: feature, variable: variable) + return tweak?.value as! T } set { tweakManager.set(newValue, feature: feature, variable: variable) @@ -44,7 +44,7 @@ public struct FallbackTweakProperty { public var wrappedValue: T { get { - let tweak = tweakManager.tweakWith(feature: feature, variable: variable) + let tweak = try? tweakManager.tweakWith(feature: feature, variable: variable) return (tweak?.value as? T) ?? fallbackValue } set { @@ -69,7 +69,7 @@ public struct OptionalTweakProperty { public var wrappedValue: T? { get { - let tweak = tweakManager.tweakWith(feature: feature, variable: variable) + let tweak = try? tweakManager.tweakWith(feature: feature, variable: variable) return (tweak?.value as? T) ?? fallbackValue } set { diff --git a/README.md b/README.md index f7b2b5f..c4548b9 100644 --- a/README.md +++ b/README.md @@ -131,19 +131,36 @@ if enabled { } ``` -2. Get the value of a flag for a given feature. `TweakManager` will return the value from the tweak provider with the highest priority and automatically fallback to the others if no set value is found. +2. Get the value of a flag for a given feature. `TweakManager` will return the value from the tweak provider with the highest priority and automatically fallback to the others if no set value is found. It throws exeception when a nil Tweak is found which can be catched and handled as needed. Use either `tweakWith(feature:variable:)` or the provided property wrappers. ```swift // check for a tweak value -let tweak = tweakManager.tweakWith(feature: "some_feature", variable: "some_flag") +let tweak = try? tweakManager.tweakWith(feature: "some_feature", variable: "some_flag") if let tweak = tweak { // tweak was found in some tweak provider, use tweak.value } else { // tweak was not found in any tweak provider } ``` +Or with do-catch +```swift +// check for a tweak value +do { + let tweak = try tweakManager.tweakWith(feature: "some_feature", variable: "some_flag") + // tweak was found in some tweak provider, use tweak.value + return tweak +} catch let error as TweakError { + switch error { + case .notFound: () // "Feature or variable is not found" + case .notSupported: () // "Variable type is not supported" + case .decryptionClosureNotProvided: () // "Value is encrypted but there's no decryption closure provided" + } +} catch let error { // add a default catch to satisfy the compiler + print(error.localizedDescription) +} +``` `@TweakProperty`, `@OptionalTweakProperty` and `@FallbackTweakProperty` property wrappers are available to mark properties representing feature flags. Mind that in order to use these property wrappers, a static instance of `TweakManager` is needed. diff --git a/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..cfb5fa4 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "swift-argument-parser", + "repositoryURL": "git@github.com:apple/swift-argument-parser.git", + "state": { + "branch": null, + "revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca", + "version": "0.5.0" + } + } + ] + }, + "version": 1 +} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakAccessorCodeGenerator.swift b/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakAccessorCodeGenerator.swift index c0b7a3e..8ec743f 100644 --- a/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakAccessorCodeGenerator.swift +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakAccessorCodeGenerator.swift @@ -152,7 +152,7 @@ extension TweakAccessorCodeGenerator { let defaultValue = try! self.defaultValue(for: tweak.valueType) return """ var \(propertyName): \(tweak.valueType) { - get { tweakManager.tweakWith(feature: \(feature), variable: \(variable))?.\(castProperty) ?? \(defaultValue) } + get { (try? tweakManager.tweakWith(feature: \(feature), variable: \(variable)))?.\(castProperty) ?? \(defaultValue) } set { tweakManager.set(newValue, feature: \(feature), variable: \(variable)) } } """