Skip to content

Commit

Permalink
v1.0.0 (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
yakovmanshin authored Dec 9, 2020
2 parents 89c2322 + 9a567c6 commit 0efbede
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
/.build
/docs/docsets/
/.swiftpm
/Packages
/*.xcodeproj
Expand Down
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# YMFF: Feature management made easy

Every company I worked at needed a way to manage feature availability in shipped apps, without changing code. 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.

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 Swift’s [property wrappers](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617).

YMFF provides a complete implementation of the mechanism: you get everything you need to start in just a few minutes. But since YMFF is protocol-based, you can replace nearly any component of the system with your own, customized implementation.

## Installation
To add YMFF to your project, use Xcode’s built-in support for Swift packages. Click File → Swift Packages → Add Package Dependency, and paste the following URL into the search field:

```
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:

```swift
.package(url: "https://github.com/yakovmanshin/YMFF", .upToNextMajor(from: "1.0.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 a single required method, `value(forKey:)`.

### 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.

```swift
import FirebaseRemoteConfig
import YMFF

extension RemoteConfig: FeatureFlagStoreProtocol {

public func value<Value>(forKey key: String) -> Value? {
// Remote Config returns a default value if the requested key doesn't exist,
// so you need to check the key for existence explicitly.
guard self.allKeys(from: .remote).contains(key) else { return nil }

let remoteConfigValue = self[key]

// You need to use different RemoteConfigValue methods, depending on the return type.
switch Value.self {
case is Bool.Type:
return remoteConfigValue.boolValue as? Value
case is Data.Type:
return remoteConfigValue.dataValue as? Value
case is Double.Type:
return remoteConfigValue.numberValue.doubleValue as? Value
case is Int.Type:
return remoteConfigValue.numberValue.intValue as? Value
case is String.Type:
return remoteConfigValue.stringValue as? Value
default:
return nil
}
}

}
```

Now `RemoteConfig` is a valid feature flag store.

## Usage
Here’s the most basic way to use YMFF.

```swift
// For convenience, use an enum to create a namespace.
enum FeatureFlags {

// `resolver` references one or more feature flag stores.
// `MyFeatureFlagStore.shared` conforms to `FeatureFlagStoreProtocol`.
private static var resolver: FeatureFlagResolverProtocol = {
FeatureFlagResolver(configuration: .init(persistentStores: [
.opaque(MyFeatureFlagStore.shared)
]))
}

// Feature flags are initialized with three pieces of data:
// a key string, the default value (used as fallback
// when all feature flag stores fail to provide one), and the resolver.
@FeatureFlag("promo_enabled", default: false, resolver: resolver)
static var promoEnabled

// Feature flags aren't limited to booleans. You can use any type of value.
@FeatureFlag("number_of_banners", default: 3, resolver: resolver)
static var numberOfBanners

}
```

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

You can browse the source files to learn more about the options available to you. An extended documentation is coming later.

## Contributing
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).
2 changes: 1 addition & 1 deletion Sources/YMFF/FeatureFlag/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public struct FeatureFlag<Value> {
/// - defaultValue: *Required.* The value returned in case both the local and remote stores failed to provide values by the key.
/// - resolver: *Required.* The resolver object used to retrieve values from the stores.
public init(
_ key: String,
_ key: FeatureFlagKey,
default defaultValue: Value,
resolver: FeatureFlagResolverProtocol
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Copyright © 2020 Yakov Manshin. See the LICENSE file for license info.
//

/// A service that resolves feature flag values by their keys.
/// A service that resolves feature flag values with their keys.
public protocol FeatureFlagResolverProtocol {

/// The object used to configure the resolver.
Expand All @@ -17,7 +17,7 @@ public protocol FeatureFlagResolverProtocol {
/// - Parameter key: *Required.* The feature flag key.
func value<Value>(for key: FeatureFlagKey) throws -> Value

/// Sets a new feature flag value that's available in the runtime, within a single app session, and overrides values from other stores.
/// Sets a new feature flag value that's available in runtime, within a single app session, and takes precedence over values from other stores.
///
/// - Parameters:
/// - key: *Required.* The feature flag key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
// Copyright © 2020 Yakov Manshin. See the LICENSE file for license info.
//

/// An object that stores feature flag values.
/// An object that stores feature flag values, and provides them at the resolver's request.
public protocol FeatureFlagStoreProtocol {

/// Retrieves feature flag value by its key.
/// Retrieves a feature flag value by its key.
///
/// - Parameter key: *Required.* The key that points to a feature flag value in the store.
func value<Value>(forKey key: String) -> Value?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Copyright © 2020 Yakov Manshin. See the LICENSE file for license info.
//

/// An object that stores feature flag values set in the runtime.
/// An object that stores feature flag values that can be added and removed in runtime.
public protocol MutableFeatureFlagStoreProtocol: AnyObject, FeatureFlagStoreProtocol {

/// Adds the value to the store so it can be retrieved with the key later.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ extension FeatureFlagResolver: FeatureFlagResolverProtocol {

extension FeatureFlagResolver {

func retrieveValue<Value>(forKey key: String) throws -> Value {
private func retrieveValue<Value>(forKey key: String) throws -> Value {
if let runtimeValue: Value = configuration.runtimeStore.value(forKey: key) {
return runtimeValue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

// MARK: - RuntimeOverridesStore

/// A YMFF-supplied implementation of the object that stores feature flag values used in the runtime.
/// A YMFF-supplied implementation of the object that stores feature flag values used in runtime.
final public class RuntimeOverridesStore {

private var store: TransparentFeatureFlagStore
Expand Down

0 comments on commit 0efbede

Please sign in to comment.