Compare commits
11 Commits
main
...
96-async-s
Author | SHA1 | Date |
---|---|---|
![]() |
aca7cdaf6d | |
![]() |
470177543a | |
![]() |
a8e34b17d0 | |
![]() |
4ca114eab4 | |
![]() |
d13bbb2c58 | |
![]() |
c708f79588 | |
![]() |
a368a02ee7 | |
![]() |
fbe154665f | |
![]() |
dbf6cd9d1a | |
![]() |
9b2454e783 | |
![]() |
df6a034bc5 |
|
@ -4,26 +4,28 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- "main"
|
||||
workflow_call:
|
||||
inputs:
|
||||
force_all_checks:
|
||||
type: boolean
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
env:
|
||||
XCODE_PATH: "/Applications/Xcode_14.0.1.app"
|
||||
XCODE_PATH: "/Applications/Xcode_14.1.app"
|
||||
jobs:
|
||||
spm_tests_macos:
|
||||
name: SPM Tests (Swift ${{ matrix.SWIFT_VERSION }} on macOS)
|
||||
runs-on: ${{ matrix.OS }}
|
||||
strategy:
|
||||
matrix:
|
||||
SWIFT_VERSION: ["5.7", "5.5", "5.3"]
|
||||
SWIFT_VERSION: ["5.7", "5.5"]
|
||||
include:
|
||||
- SWIFT_VERSION: "5.7"
|
||||
OS: macOS-12
|
||||
XCODE_APP_NAME: "Xcode_14.0.1"
|
||||
XCODE_APP_NAME: "Xcode_14.1"
|
||||
- SWIFT_VERSION: "5.5"
|
||||
OS: macOS-12
|
||||
XCODE_APP_NAME: "Xcode_13.2.1"
|
||||
- SWIFT_VERSION: "5.3"
|
||||
OS: macOS-11
|
||||
XCODE_APP_NAME: "Xcode_12.4"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Select Xcode Version
|
||||
|
@ -44,7 +46,7 @@ jobs:
|
|||
cocoapods_podspec_lint:
|
||||
name: CocoaPods Podspec Linting
|
||||
runs-on: macOS-12
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
if: ${{ inputs.force_all_checks || github.event_name == 'pull_request' }}
|
||||
needs: [spm_tests_macos, spm_tests_linux]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
name: Release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
env:
|
||||
XCODE_PATH: "/Applications/Xcode_14.1.app"
|
||||
jobs:
|
||||
run_common_checks:
|
||||
uses: ./.github/workflows/Common.yml
|
||||
with:
|
||||
force_all_checks: true
|
||||
cocoapods_trunk_push:
|
||||
name: Push to CocoaPods Trunk
|
||||
runs-on: macOS-12
|
||||
needs: run_common_checks
|
||||
env:
|
||||
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Select Xcode Version
|
||||
run: sudo xcode-select -switch ${{ env.XCODE_PATH }}
|
||||
- name: Push
|
||||
run: |
|
||||
pod trunk push YMFF.podspec --verbose
|
|
@ -1,12 +1,12 @@
|
|||
// swift-tools-version:5.3
|
||||
// swift-tools-version: 5.5
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "YMFF",
|
||||
platforms: [
|
||||
.iOS(.v11),
|
||||
.macOS(.v10_13),
|
||||
.iOS(.v13),
|
||||
.macOS(.v10_15),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
27
README.md
27
README.md
|
@ -1,12 +1,17 @@
|
|||
# YMFF: Feature management made easy
|
||||
|
||||
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).
|
||||
|
||||
<details>
|
||||
<summary>Why & How</summary>
|
||||
|
||||
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.**
|
||||
|
||||
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-to-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).
|
||||
|
||||
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).
|
||||
</details>
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -22,7 +27,7 @@ You’re then prompted to select the version to install and indicate the desired
|
|||
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: "3.0.0"))
|
||||
.package(url: "https://github.com/yakovmanshin/YMFF", .upToNextMajor(from: "3.1.0"))
|
||||
```
|
||||
|
||||
### CocoaPods
|
||||
|
@ -31,7 +36,7 @@ YMFF alternatively supports installation via [CocoaPods](https://youtu.be/iEAjvN
|
|||
Add the following to your Podfile:
|
||||
|
||||
```ruby
|
||||
pod 'YMFF', '~> 3.0'
|
||||
pod 'YMFF', '~> 3.1'
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
@ -40,6 +45,9 @@ All you need to start managing features with YMFF is at least one *feature flag
|
|||
### Firebase Remote Config
|
||||
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.
|
||||
|
||||
<details>
|
||||
<summary>Typical Setup</summary>
|
||||
|
||||
```swift
|
||||
import FirebaseRemoteConfig
|
||||
import YMFFProtocols
|
||||
|
@ -82,6 +90,8 @@ Now, `RemoteConfig` is a valid *feature flag store*.
|
|||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
## Usage
|
||||
Here’s the most basic way to use YMFF:
|
||||
|
||||
|
@ -184,6 +194,13 @@ That’s it!
|
|||
|
||||
You can browse the source files to learn more about the options available to you. An autogenerated documentation is available at [opensource.ym.dev](https://opensource.ym.dev/YMFF/).
|
||||
|
||||
## v4 Roadmap
|
||||
* [[#96](https://github.com/yakovmanshin/YMFF/issues/96)] Support for asynchronous feature-flag stores
|
||||
* [[#104](https://github.com/yakovmanshin/YMFF/issues/104)] Minimum compiler version: Swift 5.5 (Xcode 13)
|
||||
* [[#106](https://github.com/yakovmanshin/YMFF/issues/106)] Minimum deployment target: iOS 13, macOS 10.15
|
||||
|
||||
YMFF v4 is expected to be released in late 2022.
|
||||
|
||||
## Contributing
|
||||
Contributions are welcome!
|
||||
|
||||
|
@ -192,6 +209,6 @@ Have a look at [issues](https://github.com/yakovmanshin/YMFF/issues) to see the
|
|||
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.
|
||||
YMFF is licensed under the Apache License. See the [LICENSE file](https://github.com/yakovmanshin/YMFF/blob/main/LICENSE) for details.
|
||||
|
||||
© 2020–2022 Yakov Manshin
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// AsyncFeatureFlagStoreProtocol.swift
|
||||
// YMFFProtocols
|
||||
//
|
||||
// Created by Yakov Manshin on 9/29/22.
|
||||
// Copyright © 2022 Yakov Manshin. See the LICENSE file for license info.
|
||||
//
|
||||
|
||||
#if swift(>=5.5)
|
||||
|
||||
@available(iOS 13, *)
|
||||
@available(macOS 10.15, *)
|
||||
public protocol AsyncFeatureFlagStoreProtocol {
|
||||
|
||||
func value<Value>(forKey key: String) async throws -> Value
|
||||
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// AsyncMutableFeatureFlagStoreProtocol.swift
|
||||
// YMFFProtocols
|
||||
//
|
||||
// Created by Yakov Manshin on 9/29/22.
|
||||
// Copyright © 2022 Yakov Manshin. See the LICENSE file for license info.
|
||||
//
|
||||
|
||||
#if swift(>=5.5)
|
||||
|
||||
@available(iOS 13, *)
|
||||
@available(macOS 10.15, *)
|
||||
public protocol AsyncMutableFeatureFlagStoreProtocol: AnyObject, AsyncFeatureFlagStoreProtocol {
|
||||
|
||||
func setValue<Value>(_ value: Value, forKey key: String) async throws
|
||||
|
||||
func removeValue(forKey key: String) async throws
|
||||
|
||||
func saveChanges() async throws
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Default Implementation
|
||||
|
||||
@available(iOS 13, *)
|
||||
@available(macOS 10.15, *)
|
||||
extension AsyncMutableFeatureFlagStoreProtocol {
|
||||
|
||||
public func saveChanges() async throws { }
|
||||
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// AsyncMutableFeatureFlagStoreMock.swift
|
||||
// YMFFTests
|
||||
//
|
||||
// Created by Yakov Manshin on 9/29/22.
|
||||
// Copyright © 2022 Yakov Manshin. See the LICENSE file for license info.
|
||||
//
|
||||
|
||||
#if swift(>=5.5)
|
||||
|
||||
import YMFF
|
||||
#if !COCOAPODS
|
||||
import YMFFProtocols
|
||||
#endif
|
||||
|
||||
// MARK: - Mock
|
||||
|
||||
final class AsyncMutableFeatureFlagStoreMock: AsyncMutableFeatureFlagStoreProtocol {
|
||||
|
||||
private var store: TransparentFeatureFlagStore
|
||||
private var onSaveChangesClosure: () -> Void
|
||||
|
||||
init(store: TransparentFeatureFlagStore, onSaveChanges: @escaping () -> Void = { }) {
|
||||
self.store = store
|
||||
self.onSaveChangesClosure = onSaveChanges
|
||||
}
|
||||
|
||||
func value<Value>(forKey key: String) async throws -> Value {
|
||||
// Wait for 0.1 second
|
||||
try await Task.sleep(nanoseconds: 1_000_000)
|
||||
|
||||
guard store[key] != nil else {
|
||||
throw AsyncMutableFeatureFlagStoreMockError.valueNotFoundInStore
|
||||
}
|
||||
|
||||
guard let value = store[key] as? Value else {
|
||||
throw AsyncMutableFeatureFlagStoreMockError.valueTypeMismatch
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func setValue<Value>(_ value: Value, forKey key: String) async throws {
|
||||
// Wait for 0.1 second
|
||||
try await Task.sleep(nanoseconds: 1_000_000)
|
||||
|
||||
if let existingValue = store[key], type(of: existingValue) != type(of: value) {
|
||||
throw AsyncMutableFeatureFlagStoreMockError.valueTypeMismatch
|
||||
}
|
||||
|
||||
store[key] = value
|
||||
}
|
||||
|
||||
func removeValue(forKey key: String) async throws {
|
||||
// Wait for 0.1 second
|
||||
try await Task.sleep(nanoseconds: 1_000_000)
|
||||
|
||||
guard store[key] != nil else {
|
||||
throw AsyncMutableFeatureFlagStoreMockError.valueNotFoundInStore
|
||||
}
|
||||
|
||||
store[key] = nil
|
||||
}
|
||||
|
||||
func saveChanges() async throws {
|
||||
onSaveChangesClosure()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Error
|
||||
|
||||
fileprivate enum AsyncMutableFeatureFlagStoreMockError: Error {
|
||||
case valueNotFoundInStore
|
||||
case valueTypeMismatch
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// AsyncMutableStoreTests.swift
|
||||
// YMFFTests
|
||||
//
|
||||
// Created by Yakov Manshin on 9/30/22.
|
||||
// Copyright © 2022 Yakov Manshin. See the LICENSE file for license info.
|
||||
//
|
||||
|
||||
#if swift(>=5.5)
|
||||
|
||||
@testable import YMFF
|
||||
|
||||
import XCTest
|
||||
#if !COCOAPODS
|
||||
import YMFFProtocols
|
||||
#endif
|
||||
|
||||
final class AsyncMutableStoreTests: XCTestCase {
|
||||
|
||||
private var asyncMutableStore: AsyncMutableFeatureFlagStoreProtocol!
|
||||
private var resolver: FeatureFlagResolverProtocol!
|
||||
|
||||
override func setUp() async throws {
|
||||
try await super.setUp()
|
||||
|
||||
asyncMutableStore = AsyncMutableFeatureFlagStoreMock(store: .init())
|
||||
resolver = FeatureFlagResolver(stores: [])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
|
@ -3,8 +3,8 @@ Pod::Spec.new do |s|
|
|||
# Root
|
||||
|
||||
s.name = "YMFF"
|
||||
s.version = "3.0.0"
|
||||
s.swift_version = "5.3"
|
||||
s.version = "3.1.0"
|
||||
s.swift_version = "5.5"
|
||||
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" }
|
||||
|
@ -20,8 +20,8 @@ Pod::Spec.new do |s|
|
|||
|
||||
# Platform
|
||||
|
||||
s.osx.deployment_target = "10.13"
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.ios.deployment_target = "13.0"
|
||||
s.osx.deployment_target = "10.15"
|
||||
|
||||
# Subspecs
|
||||
|
||||
|
|
Loading…
Reference in New Issue