Skip to content

Commit

Permalink
v3.0.0 (#93)
Browse files Browse the repository at this point in the history
* #71 via #75
* #82 via #83
* #84 via #86
* #85 via #87
* #89 via #90
* #91 via #92
* Removed deprecated initializer from `FeatureFlagResolver`
* Updated tests
* Updated Jazzy config
  • Loading branch information
yakovmanshin authored Feb 20, 2022
2 parents 1de38bd + effb747 commit 879f73d
Show file tree
Hide file tree
Showing 18 changed files with 353 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 4 additions & 3 deletions .jazzy.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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…
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import PackageDescription

let package = Package(
name: "YMFF",
platforms: [
.macOS(.v10_13),
.iOS(.v11),
],
products: [
.library(
name: "YMFF",
Expand Down
62 changes: 45 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -111,14 +109,44 @@ 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
}
```

To the code that makes use of a feature flag, the flag acts just like the type of its value:

```swift
if FeatureFlags.promoEnabled {
displayPromoBanners(count: FeatureFlags.numberOfBanners)
switch FeatureFlags.promoUnitKind {
case .text:
displayPromoText()
case .image:
displayPromoBanners(count: FeatureFlags.numberOfBanners)
case .video:
playPromoVideo()
}
}
```

Expand Down Expand Up @@ -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
32 changes: 28 additions & 4 deletions Sources/YMFF/FeatureFlag/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import YMFFProtocols

/// An object that facilitates access to feature flag values.
@propertyWrapper
final public class FeatureFlag<Value> {
final public class FeatureFlag<RawValue, Value> {

// MARK: Properties

/// The key used to retrieve feature flag values.
public let key: FeatureFlagKey

private let transformer: FeatureFlagValueTransformer<RawValue, Value>

/// The fallback value returned when no store is able to provide the real one.
public let defaultValue: Value
Expand All @@ -30,33 +32,55 @@ final public class FeatureFlag<Value> {
///
/// - 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<RawValue, Value>,
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<Value> { self }
public var projectedValue: FeatureFlag<RawValue, Value> { self }

// MARK: Mutable Value Removal

Expand Down
41 changes: 41 additions & 0 deletions Sources/YMFF/FeatureFlag/FeatureFlagValueTransformer.swift
Original file line number Diff line number Diff line change
@@ -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<RawValue, Value> {

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 })
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

This file was deleted.

5 changes: 0 additions & 5 deletions Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

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

Expand All @@ -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)

Expand Down
Loading

0 comments on commit 879f73d

Please sign in to comment.