diff --git a/.github/workflows/run-tests.yml b/.github/workflows/YMFF.yml similarity index 88% rename from .github/workflows/run-tests.yml rename to .github/workflows/YMFF.yml index aac4fcb..887c14e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/YMFF.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Select Xcode version - run: sudo xcode-select -switch /Applications/Xcode_12.5.app + run: sudo xcode-select -switch /Applications/Xcode_13.2.1.app - name: Run tests run: swift test -v diff --git a/.jazzy.yaml b/.jazzy.yaml index 5b86f45..899f2b7 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -1,11 +1,12 @@ author: Yakov Manshin -author_url: https://yakovmanshin.com/ +author_url: https://me.ym.dev/ clean: true -copyright: © 2020–2021 [Yakov Manshin](https://yakovmanshin.com/). Available under [Apache License v2](https://github.com/yakovmanshin/YMFF/blob/main/LICENSE). +copyright: © 2020–2022 [Yakov Manshin](https://me.ym.dev/). Available under [Apache License v2](https://github.com/yakovmanshin/YMFF/blob/main/LICENSE). custom_categories: - name: YMFF children: - FeatureFlag + - FeatureFlagValueTransformer - FeatureFlagResolver - FeatureFlagResolverError - FeatureFlagResolverConfiguration @@ -25,5 +26,5 @@ disable_search: true github_file_prefix: https://github.com/yakovmanshin/YMFF/tree/main github_url: https://github.com/yakovmanshin/YMFF hide_documentation_coverage: true -title: YMFF v2 Docs +title: YMFF v3 Docs undocumented_text: Documentation Coming Soon… diff --git a/LICENSE b/LICENSE index 8ca5f2a..9c29234 100644 --- a/LICENSE +++ b/LICENSE @@ -175,7 +175,7 @@ END OF TERMS AND CONDITIONS - Copyright © 2020–2021 Yakov Manshin + Copyright © 2020–2022 Yakov Manshin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Package.swift b/Package.swift index f032bf8..6e7a382 100644 --- a/Package.swift +++ b/Package.swift @@ -4,6 +4,10 @@ import PackageDescription let package = Package( name: "YMFF", + platforms: [ + .macOS(.v10_13), + .iOS(.v11), + ], products: [ .library( name: "YMFF", diff --git a/README.md b/README.md index fc87442..f8cd7a3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # YMFF: Feature management made easy -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. +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 the power of Swift’s [property wrappers](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617). +YMFF is a nice little library that makes managing features with feature flags—and managing feature flags themselves—a bliss, thanks mainly 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. The supplied implementation and the protocols are kept 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 get started 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 @@ -19,28 +19,26 @@ https://github.com/yakovmanshin/YMFF You’re then prompted to select the version to install and indicate the desired update policy. I recommend starting with the latest version (it’s selected automatically), and choosing “up to next major” as the preferred update rule. Once you click Next, the package is fetched. Then select the target you’re going to use YMFF in. Click Finish, and you’re ready to go. -If you need to use YMFF in another Swift package, add it as a dependency: +If you need to use YMFF in another Swift package, add it to the `Package.swift` file as a dependency: ```swift -.package(url: "https://github.com/yakovmanshin/YMFF", .upToNextMajor(from: "2.0.0")) +.package(url: "https://github.com/yakovmanshin/YMFF", .upToNextMajor(from: "3.0.0")) ``` ### CocoaPods -YMFF now supports installation via [CocoaPods](https://youtu.be/iEAjvNRdZa0). +YMFF alternatively supports installation via [CocoaPods](https://youtu.be/iEAjvNRdZa0). Add the following to your Podfile: ```ruby -pod 'YMFF', '~> 2.1' +pod 'YMFF', '~> 3.0' ``` ## Setup -All you need to start managing features with YMFF is at least one feature flag *store*—an object which conforms to `FeatureFlagStoreProtocol` and provides values that correspond to feature flag keys. - -`FeatureFlagStoreProtocol` has two required methods: `containsValue(forKey:)` and `value(forKey:)`. +All you need to start managing features with YMFF is at least one *feature flag store*—an object which conforms to `FeatureFlagStoreProtocol` and provides values that correspond to feature flag keys. ### Firebase Remote Config -Firebase’s Remote Config is one of the most popular tools to manage feature flags on the back-end side. Remote Config’s `RemoteConfigValue` requires use of different methods to retrieve values of different types. Integration of YMFF with Remote Config, although doesn’t look very pretty, is quite simple. +Firebase’s Remote Config is one of the most popular tools to manage feature flags on the server side. Remote Config’s `RemoteConfigValue` requires the use of different methods to retrieve values of different types. Integration of YMFF with Remote Config, although doesn’t look very pretty, is quite straightforward. ```swift import FirebaseRemoteConfig @@ -80,9 +78,9 @@ extension RemoteConfig: FeatureFlagStoreProtocol { } ``` -Now `RemoteConfig` is a valid feature flag store. +Now, `RemoteConfig` is a valid *feature flag store*. -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. +Alternatively, you can create a custom wrapper object. That’s what I tend to do in my projects to achieve greater flexibility and avoid tight coupling. ## Usage Here’s the most basic way to use YMFF: @@ -111,6 +109,29 @@ enum FeatureFlags { @FeatureFlag("number_of_banners", default: 3, resolver: resolver) static var numberOfBanners + // Sometimes it may be convenient to transform the raw value—the one you receive from the store— + // to the native value—the one used in your app. + // In the following example, `MyFeatureFlagStore` stores values as strings, but the app uses an enum. + // To switch between the types, you use a `FeatureFlagValueTransformer`. + @FeatureFlag( + "promo_unit_kind", + FeatureFlagValueTransformer { string in + PromoUnitKind(rawValue: string) + } rawValueFromValue: { kind in + kind.rawValue + }, + default: .image, + resolver: resolver + ) + static var promoUnitKind + +} + +// You can create feature flags of any type. +enum PromoUnitKind: String { + case text + case image + case video } ``` @@ -118,7 +139,14 @@ To the code that makes use of a feature flag, the flag acts just like the type o ```swift if FeatureFlags.promoEnabled { - displayPromoBanners(count: FeatureFlags.numberOfBanners) + switch FeatureFlags.promoUnitKind { + case .text: + displayPromoText() + case .image: + displayPromoBanners(count: FeatureFlags.numberOfBanners) + case .video: + playPromoVideo() + } } ``` @@ -161,9 +189,9 @@ Contributions are welcome! Have a look at [issues](https://github.com/yakovmanshin/YMFF/issues) to see the project’s current needs. Don’t hesitate to create new issues, especially if you intend to work on them yourself. -If you’d like to discuss something else regarding YMFF (or not), contact [me](https://github.com/yakovmanshin) via email (the address is in the profile). +If you’d like to discuss something else, contact [me](https://github.com/yakovmanshin) via email (the address is in the profile). ## License and Copyright YMFF is available under the terms of the Apache License, version 2.0. See the [LICENSE file](https://github.com/yakovmanshin/YMFF/blob/main/LICENSE) for details. -© 2020–2021 Yakov Manshin +© 2020–2022 Yakov Manshin diff --git a/Sources/YMFF/FeatureFlag/FeatureFlag.swift b/Sources/YMFF/FeatureFlag/FeatureFlag.swift index 9b94591..0ae721d 100644 --- a/Sources/YMFF/FeatureFlag/FeatureFlag.swift +++ b/Sources/YMFF/FeatureFlag/FeatureFlag.swift @@ -12,12 +12,14 @@ import YMFFProtocols /// An object that facilitates access to feature flag values. @propertyWrapper -final public class FeatureFlag { +final public class FeatureFlag { // MARK: Properties /// The key used to retrieve feature flag values. public let key: FeatureFlagKey + + private let transformer: FeatureFlagValueTransformer /// The fallback value returned when no store is able to provide the real one. public let defaultValue: Value @@ -30,33 +32,55 @@ final public class FeatureFlag { /// /// - Parameters: /// - key: *Required.* The key used to address feature flag values in stores. + /// - transformer: *Required.* The object that transforms raw values into values, and vice versa. /// - defaultValue: *Required.* The value returned in case all stores fail to provide a value. /// - resolver: *Required.* The resolver object used to retrieve values from stores. public init( _ key: FeatureFlagKey, + transformer: FeatureFlagValueTransformer, default defaultValue: Value, resolver: FeatureFlagResolverProtocol ) { self.key = key + self.transformer = transformer self.defaultValue = defaultValue self.resolver = resolver } + /// Creates a new `FeatureFlag` with value and raw value of the same type. + /// + /// - Parameters: + /// - key: *Required.* The key used to address feature flag values in stores. + /// - defaultValue: *Required.* The value returned in case all stores fail to provide a value. + /// - resolver: *Required.* The resolver object used to retrieve values from stores. + public convenience init( + _ key: FeatureFlagKey, + default defaultValue: Value, + resolver: FeatureFlagResolverProtocol + ) where RawValue == Value { + self.init(key, transformer: .identity, default: defaultValue, resolver: resolver) + } + // MARK: Wrapped Value /// The resolved value of the feature flag. public var wrappedValue: Value { get { - (try? (resolver.value(for: key) as Value)) ?? defaultValue + guard + let rawValue = try? (resolver.value(for: key) as RawValue), + let value = transformer.valueFromRawValue(rawValue) + else { return defaultValue } + + return value } set { - try? resolver.setValue(newValue, toMutableStoreUsing: key) + try? resolver.setValue(transformer.rawValueFromValue(newValue), toMutableStoreUsing: key) } } // MARK: Projected Value /// The object returned when referencing the feature flag with a dollar sign (`$`). - public var projectedValue: FeatureFlag { self } + public var projectedValue: FeatureFlag { self } // MARK: Mutable Value Removal diff --git a/Sources/YMFF/FeatureFlag/FeatureFlagValueTransformer.swift b/Sources/YMFF/FeatureFlag/FeatureFlagValueTransformer.swift new file mode 100644 index 0000000..36413b7 --- /dev/null +++ b/Sources/YMFF/FeatureFlag/FeatureFlagValueTransformer.swift @@ -0,0 +1,41 @@ +// +// FeatureFlagValueTransformer.swift +// YMFF +// +// Created by Yakov Manshin on 2/5/22. +// Copyright © 2022 Yakov Manshin. See the LICENSE file for license info. +// + +// MARK: - Transformer + +/// An object used by `FeatureFlag` to transform raw values into native values, and vice versa. +public struct FeatureFlagValueTransformer { + + let valueFromRawValue: (RawValue) -> Value? + let rawValueFromValue: (Value) -> RawValue + + /// Creates a new instance of transformer using the specified transformation closures. + /// + /// - Parameters: + /// - valueFromRawValue: *Required.* The closure which attempts to transform a raw value into a value. + /// - rawValueFromValue: *Required.* The closure which transforms a value into a raw value. + public init( + valueFromRawValue: @escaping (RawValue) -> Value?, + rawValueFromValue: @escaping (Value) -> RawValue + ) { + self.valueFromRawValue = valueFromRawValue + self.rawValueFromValue = rawValueFromValue + } + +} + +// MARK: - Identity + +extension FeatureFlagValueTransformer where RawValue == Value { + + /// The transformer which converts between values of the same type. + static var identity: FeatureFlagValueTransformer { + .init(valueFromRawValue: { $0 }, rawValueFromValue: { $0 }) + } + +} diff --git a/Sources/YMFF/FeatureFlagResolver/Configuration/FeatureFlagResolverConfiguration.swift b/Sources/YMFF/FeatureFlagResolver/Configuration/FeatureFlagResolverConfiguration.swift index a2b9ace..8b6b443 100644 --- a/Sources/YMFF/FeatureFlagResolver/Configuration/FeatureFlagResolverConfiguration.swift +++ b/Sources/YMFF/FeatureFlagResolver/Configuration/FeatureFlagResolverConfiguration.swift @@ -12,11 +12,12 @@ import YMFFProtocols // MARK: - FeatureFlagResolverConfiguration -/// A YMFF-supplied object used to provide the feature flag resolver with its configuration. -public struct FeatureFlagResolverConfiguration { +/// An object used to configure the resolver. +final public class FeatureFlagResolverConfiguration { - public let stores: [FeatureFlagStore] + public var stores: [FeatureFlagStore] + /// Initializes a new configuration with the given array of feature flag stores. public init(stores: [FeatureFlagStore]) { self.stores = stores } diff --git a/Sources/YMFF/FeatureFlagResolver/Configuration/MutableFeatureFlagResolverConfiguration.swift b/Sources/YMFF/FeatureFlagResolver/Configuration/MutableFeatureFlagResolverConfiguration.swift deleted file mode 100644 index 232938a..0000000 --- a/Sources/YMFF/FeatureFlagResolver/Configuration/MutableFeatureFlagResolverConfiguration.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// MutableFeatureFlagResolverConfiguration.swift -// YMFF -// -// Created by Yakov Manshin on 5/17/21. -// Copyright © 2021 Yakov Manshin. See the LICENSE file for license info. -// - -#if !COCOAPODS -import YMFFProtocols -#endif - -// MARK: - MutableFeatureFlagResolverConfiguration - -/// An object used to configure the resolver, which holds feature flag stores that can be changed. -final public class MutableFeatureFlagResolverConfiguration { - - public var stores: [FeatureFlagStore] - - public init(stores: [FeatureFlagStore]) { - self.stores = stores - } - -} - -// MARK: - FeatureFlagResolverConfigurationProtocol - -extension MutableFeatureFlagResolverConfiguration: FeatureFlagResolverConfigurationProtocol { } diff --git a/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver.swift b/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver.swift index 3196a01..6493d9b 100644 --- a/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver.swift +++ b/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver.swift @@ -28,11 +28,6 @@ final public class FeatureFlagResolver { self.configuration = configuration } - @available(*, deprecated, message: "Use init(stores:)") - public convenience init(configuration: FeatureFlagResolverConfiguration) { - self.init(configuration: configuration as FeatureFlagResolverConfigurationProtocol) - } - /// Initializes the resolver with the list of feature flag stores. /// /// + Passing in an empty array will produce the `noStoreAvailable` error on next read attempt. diff --git a/Sources/YMFFProtocols/FeatureFlagResolver/Configuration/FeatureFlagResolverConfigurationProtocol.swift b/Sources/YMFFProtocols/FeatureFlagResolver/Configuration/FeatureFlagResolverConfigurationProtocol.swift index 04c9479..8edf88c 100644 --- a/Sources/YMFFProtocols/FeatureFlagResolver/Configuration/FeatureFlagResolverConfigurationProtocol.swift +++ b/Sources/YMFFProtocols/FeatureFlagResolver/Configuration/FeatureFlagResolverConfigurationProtocol.swift @@ -7,12 +7,12 @@ // /// An object that provides the resources critical to functioning of the resolver. -public protocol FeatureFlagResolverConfigurationProtocol { +public protocol FeatureFlagResolverConfigurationProtocol: AnyObject { /// An array of stores which may contain feature flag values. /// /// + The array may include both mutable and immutable stores. /// + The stores are examined in order. The first value found for a key will be used. - var stores: [FeatureFlagStore] { get } + var stores: [FeatureFlagStore] { get set } } diff --git a/Tests/YMFFTests/FeatureFlagResolverConfigurationTests.swift b/Tests/YMFFTests/FeatureFlagResolverConfigurationTests.swift index 5307ea5..b425040 100644 --- a/Tests/YMFFTests/FeatureFlagResolverConfigurationTests.swift +++ b/Tests/YMFFTests/FeatureFlagResolverConfigurationTests.swift @@ -12,8 +12,8 @@ import XCTest final class FeatureFlagResolverConfigurationTests: XCTestCase { - func testStoreAdditionToMutableConfiguration() { - let configuration = MutableFeatureFlagResolverConfiguration(stores: []) + func testStoreAdditionToConfiguration() { + let configuration = FeatureFlagResolverConfiguration(stores: []) XCTAssertEqual(configuration.stores.count, 0) @@ -22,8 +22,8 @@ final class FeatureFlagResolverConfigurationTests: XCTestCase { XCTAssertEqual(configuration.stores.count, 1) } - func testStoreRemovalFromMutableConfiguration() { - let configuration = MutableFeatureFlagResolverConfiguration(stores: [.immutable(TransparentFeatureFlagStore())]) + func testStoreRemovalFromConfiguration() { + let configuration = FeatureFlagResolverConfiguration(stores: [.immutable(TransparentFeatureFlagStore())]) XCTAssertEqual(configuration.stores.count, 1) diff --git a/Tests/YMFFTests/FeatureFlagTests.swift b/Tests/YMFFTests/FeatureFlagTests.swift index 2e05230..7e0d2d7 100644 --- a/Tests/YMFFTests/FeatureFlagTests.swift +++ b/Tests/YMFFTests/FeatureFlagTests.swift @@ -40,6 +40,22 @@ final class FeatureFlagTests: XCTestCase { @FeatureFlag("NONEXISTENT_OVERRIDE_KEY", default: 999, resolver: resolver) private var nonexistentOverrideFlag + @FeatureFlag( + SharedAssets.stringToBoolKey, + transformer: .init(valueFromRawValue: { $0 == "true" }, rawValueFromValue: { $0 ? "true" : "false" }), + default: false, + resolver: resolver + ) + private var stringToBoolFeatureFlag + + @FeatureFlag( + SharedAssets.stringToAdTypeKey, + transformer: .init(valueFromRawValue: { AdType(rawValue: $0) }, rawValueFromValue: { $0.rawValue }), + default: .none, + resolver: resolver + ) + private var stringToAdTypeFeatureFlag + } // MARK: - Wrapped Value Tests @@ -90,6 +106,37 @@ extension FeatureFlagTests { XCTAssertEqual(nonexistentOverrideFlag, 999) } + func testStringToBoolWrappedValue() { + XCTAssertTrue(stringToBoolFeatureFlag) + } + + func testStringToBoolWrappedValueOverride() { + XCTAssertTrue(stringToBoolFeatureFlag) + + stringToBoolFeatureFlag = false + XCTAssertFalse(stringToBoolFeatureFlag) + + $stringToBoolFeatureFlag.removeValueFromMutableStore() + XCTAssertTrue(stringToBoolFeatureFlag) + } + + func testStringToAdTypeWrappedValue() { + XCTAssertEqual(stringToAdTypeFeatureFlag, .video) + } + + func testStringToAdTypeWrappedValueOverride() { + XCTAssertEqual(stringToAdTypeFeatureFlag, .video) + + stringToAdTypeFeatureFlag = .banner + XCTAssertEqual(stringToAdTypeFeatureFlag, .banner) + + XCTAssertNoThrow(try Self.resolver.setValue("image", toMutableStoreUsing: SharedAssets.stringToAdTypeKey)) + XCTAssertEqual(stringToAdTypeFeatureFlag, .none) + + $stringToAdTypeFeatureFlag.removeValueFromMutableStore() + XCTAssertEqual(stringToAdTypeFeatureFlag, .video) + } + } // MARK: - Projected Value Tests @@ -97,23 +144,31 @@ extension FeatureFlagTests { extension FeatureFlagTests { func testBoolProjectedValue() { - XCTAssertTrue(value($boolFeatureFlag, isOfType: FeatureFlag.self)) + XCTAssertTrue(value($boolFeatureFlag, isOfType: IdentityFeatureFlag.self)) } func testIntProjectedValue() { - XCTAssertTrue(value($intFeatureFlag, isOfType: FeatureFlag.self)) + XCTAssertTrue(value($intFeatureFlag, isOfType: IdentityFeatureFlag.self)) } func testStringProjectedValue() { - XCTAssertTrue(value($stringFeatureFlag, isOfType: FeatureFlag.self)) + XCTAssertTrue(value($stringFeatureFlag, isOfType: IdentityFeatureFlag.self)) } func testOptionalIntProjectedValue() { - XCTAssertTrue(value($optionalIntFeatureFlag, isOfType: FeatureFlag.self)) + XCTAssertTrue(value($optionalIntFeatureFlag, isOfType: IdentityFeatureFlag.self)) } func testNonexistentIntProjectedValue() { - XCTAssertTrue(value($nonexistentIntFeatureFlag, isOfType: FeatureFlag.self)) + XCTAssertTrue(value($nonexistentIntFeatureFlag, isOfType: IdentityFeatureFlag.self)) + } + + func testStringToBoolProjectedValue() { + XCTAssertTrue(value($stringToBoolFeatureFlag, isOfType: FeatureFlag.self)) + } + + func testStringToAdTypeProjectedValue() { + XCTAssertTrue(value($stringToAdTypeFeatureFlag, isOfType: FeatureFlag.self)) } private func value(_ value: Any, isOfType type: T.Type) -> Bool { @@ -121,3 +176,13 @@ extension FeatureFlagTests { } } + +// MARK: - Support Types + +fileprivate typealias IdentityFeatureFlag = FeatureFlag + +fileprivate enum AdType: String { + case none + case banner + case video +} diff --git a/Tests/YMFFTests/FeatureFlagValueTransformerTests.swift b/Tests/YMFFTests/FeatureFlagValueTransformerTests.swift new file mode 100644 index 0000000..7f93f1e --- /dev/null +++ b/Tests/YMFFTests/FeatureFlagValueTransformerTests.swift @@ -0,0 +1,131 @@ +// +// FeatureFlagValueTransformerTests.swift +// YMFFTests +// +// Created by Yakov Manshin on 2/5/22. +// Copyright © 2022 Yakov Manshin. See the LICENSE file for license info. +// + +import XCTest + +@testable import YMFF + +// MARK: - Tests + +final class FeatureFlagValueTransformerTests: XCTestCase { + + func testIdentityTransformer() { + let transformer: FeatureFlagValueTransformer = .identity + + let intValue = 123 + + XCTAssertEqual(transformer.valueFromRawValue(intValue), intValue) + XCTAssertEqual(transformer.rawValueFromValue(intValue), intValue) + } + + func testSameTypeTransformation() { + let transformer = FeatureFlagValueTransformer { string in + String(string.dropFirst(4)) + } rawValueFromValue: { string in + "RAW_\(string)" + } + + let stringValue = "some_value" + let stringRawValue = "RAW_some_value" + + XCTAssertEqual(transformer.valueFromRawValue(stringRawValue), stringValue) + XCTAssertEqual(transformer.rawValueFromValue(stringValue), stringRawValue) + } + + func testStringToBoolTransformation() { + let transformer = FeatureFlagValueTransformer { string in + string == "true" + } rawValueFromValue: { bool in + bool ? "true" : "false" + } + + let stringRawValueTrue = "true" + let stringRawValueFalse = "false" + let stringRawValueOther = "OTHER" + + XCTAssertTrue(transformer.valueFromRawValue(stringRawValueTrue) == true) + XCTAssertTrue(transformer.valueFromRawValue(stringRawValueFalse) == false) + XCTAssertTrue(transformer.valueFromRawValue(stringRawValueOther) == false) + + XCTAssertEqual(transformer.rawValueFromValue(true), stringRawValueTrue) + XCTAssertEqual(transformer.rawValueFromValue(false), stringRawValueFalse) + } + + func testStringToEnumWithRawValueTransformation() { + let transformer = FeatureFlagValueTransformer { string in + AdType(rawValue: string) + } rawValueFromValue: { type in + type.rawValue + } + + let stringRawValueNone = "none" + let stringRawValueBanner = "banner" + let stringRawValueVideo = "video" + let stringRawValueOther = "image" + + XCTAssertEqual(transformer.valueFromRawValue(stringRawValueNone), AdType.none) + XCTAssertEqual(transformer.valueFromRawValue(stringRawValueBanner), .banner) + XCTAssertEqual(transformer.valueFromRawValue(stringRawValueVideo), .video) + XCTAssertEqual(transformer.valueFromRawValue(stringRawValueOther), nil) + + XCTAssertEqual(transformer.rawValueFromValue(.none), stringRawValueNone) + XCTAssertEqual(transformer.rawValueFromValue(.banner), stringRawValueBanner) + XCTAssertEqual(transformer.rawValueFromValue(.video), stringRawValueVideo) + } + + func testIntToEnumWithCustomInitializationTransformation() { + let transformer = FeatureFlagValueTransformer { (age: Int) -> AgeGroup in + switch age { + case ..<13: + return .under13 + case 13...17: + return .between13And17 + case 18...: + return .over17 + default: + return .under13 + } + } rawValueFromValue: { group in + switch group { + case .under13: + return 13 + case .between13And17: + return 17 + case .over17: + return 18 + } + } + + let intRawValue5 = 5 + let intRawValue15 = 15 + let intRawValue20 = 20 + + XCTAssertEqual(transformer.valueFromRawValue(intRawValue5), .under13) + XCTAssertEqual(transformer.valueFromRawValue(intRawValue15), .between13And17) + XCTAssertEqual(transformer.valueFromRawValue(intRawValue20), .over17) + + XCTAssertEqual(transformer.rawValueFromValue(.under13), 13) + XCTAssertEqual(transformer.rawValueFromValue(.between13And17), 17) + XCTAssertEqual(transformer.rawValueFromValue(.over17), 18) + } + +} + +// MARK: - Support Types + +fileprivate enum AdType: String { + case none + case banner + case video +} + +fileprivate enum AgeGroup { + case under13 + case between13And17 + case over17 +} diff --git a/Tests/YMFFTests/SharedAssets/SharedAssets.swift b/Tests/YMFFTests/SharedAssets/SharedAssets.swift index b0b435a..2706d07 100644 --- a/Tests/YMFFTests/SharedAssets/SharedAssets.swift +++ b/Tests/YMFFTests/SharedAssets/SharedAssets.swift @@ -40,6 +40,7 @@ enum SharedAssets { "string": "STRING_VALUE_LOCAL", "optionalInt": Optional.some(123) as Any, "intToOverride": 123, + stringToAdTypeKey: "video", ] } private static var remoteStore: [String : Any] { [ @@ -47,6 +48,7 @@ enum SharedAssets { "string": "STRING_VALUE_REMOTE", "optionalInt": Optional.none as Any, "intToOverride": 456, + stringToBoolKey: "true", ] } static var boolKey: FeatureFlagKey { "bool" } @@ -55,6 +57,8 @@ enum SharedAssets { static var optionalIntKey: FeatureFlagKey { "optionalInt" } static var nonexistentKey: FeatureFlagKey { "nonexistent" } static var intToOverrideKey: FeatureFlagKey { "intToOverride" } + static var stringToBoolKey: FeatureFlagKey { "stringToBool" } + static var stringToAdTypeKey: FeatureFlagKey { "stringToAdType" } } diff --git a/Tests/YMFFTests/Stores/MutableFeatureFlagStore.swift b/Tests/YMFFTests/Stores/MutableFeatureFlagStoreMock.swift similarity index 92% rename from Tests/YMFFTests/Stores/MutableFeatureFlagStore.swift rename to Tests/YMFFTests/Stores/MutableFeatureFlagStoreMock.swift index 287e7f5..ef0022d 100644 --- a/Tests/YMFFTests/Stores/MutableFeatureFlagStore.swift +++ b/Tests/YMFFTests/Stores/MutableFeatureFlagStoreMock.swift @@ -1,5 +1,5 @@ // -// MutableFeatureFlagStore.swift +// MutableFeatureFlagStoreMock.swift // YMFF // // Created by Yakov Manshin on 5/30/21. @@ -11,7 +11,7 @@ import YMFF import YMFFProtocols #endif -final class MutableFeatureFlagStore: MutableFeatureFlagStoreProtocol { +final class MutableFeatureFlagStoreMock: MutableFeatureFlagStoreProtocol { private var store: TransparentFeatureFlagStore private var onSaveChangesClosure: () -> Void diff --git a/Tests/YMFFTests/Stores/MutableStoreTests.swift b/Tests/YMFFTests/Stores/MutableStoreTests.swift index 07c816c..2b0fe1f 100644 --- a/Tests/YMFFTests/Stores/MutableStoreTests.swift +++ b/Tests/YMFFTests/Stores/MutableStoreTests.swift @@ -21,7 +21,7 @@ final class MutableStoreTests: XCTestCase { override func setUp() { super.setUp() - mutableStore = MutableFeatureFlagStore(store: .init()) + mutableStore = MutableFeatureFlagStoreMock(store: .init()) resolver = FeatureFlagResolver(stores: [.mutable(mutableStore)]) } @@ -67,7 +67,7 @@ final class MutableStoreTests: XCTestCase { func testChangeSaving() { var saveChangesCount = 0 - mutableStore = MutableFeatureFlagStore(store: .init()) { + mutableStore = MutableFeatureFlagStoreMock(store: .init()) { saveChangesCount += 1 } resolver = FeatureFlagResolver(stores: [.mutable(mutableStore)]) diff --git a/YMFF.podspec b/YMFF.podspec index 710905f..b14876e 100644 --- a/YMFF.podspec +++ b/YMFF.podspec @@ -1,29 +1,25 @@ Pod::Spec.new do |s| - # Basic Info + # Root s.name = "YMFF" - s.version = "2.3.0" + s.version = "3.0.0" + s.swift_version = "5.3" + s.authors = { "Yakov Manshin" => "git@yakovmanshin.com" } + s.social_media_url = "https://twitter.com/yakovmanshin" + s.license = { :type => "Apache License, version 2.0", :file => "LICENSE" } + s.homepage = "https://github.com/yakovmanshin/YMFF" + s.readme = "https://github.com/yakovmanshin/YMFF/blob/main/README.md" + s.source = { :git => "https://github.com/yakovmanshin/YMFF.git", :tag => "#{s.version}" } s.summary = "Feature management made easy." - s.description = <<-DESC YMFF is a nice little library that makes management of features with feature flags—and management of the feature flags themselves—a bliss. DESC - - s.homepage = "https://github.com/yakovmanshin/YMFF" s.documentation_url = "https://opensource.ym.dev/YMFF/" - s.license = { :type => "Apache License, version 2.0", :file => "LICENSE" } - - s.author = { "Yakov Manshin" => "contact@yakovmanshin.com" } - s.social_media_url = "https://github.com/yakovmanshin" - - # Sources & Build Settings + # Platform - s.source = { :git => "https://github.com/yakovmanshin/YMFF.git", :tag => "#{s.version}" } - - s.swift_version = "5.3" s.osx.deployment_target = "10.13" s.ios.deployment_target = "11.0"