Skip to content
This repository has been archived by the owner on Sep 13, 2023. It is now read-only.

Commit

Permalink
Merge pull request #5 from spotify/static-context
Browse files Browse the repository at this point in the history
Move to "Static Context" design
  • Loading branch information
fabriziodemaria authored Mar 16, 2023
2 parents c16d927 + f6b6163 commit cbd1a0d
Show file tree
Hide file tree
Showing 25 changed files with 228 additions and 447 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.swiftpm/xcode/xcshareddata/xcschemes/OpenFeature.xcscheme
.netrc
.build
.mockingbird
Expand Down
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ excluded:
- ${PWD}/Pods
- ${PWD}/DerivedData
- ${PWD}/.build
- ${PWD}/Tools/.build
- ${PWD}/Tools/*/.build
- ${PWD}/Tests/OpenFeatureTests/MockingbirdMocks/
- ${PWD}/Sources/OpenFeature/FlagResolver

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ To enable the provider and start resolving flags add the following:
import OpenFeature

// Change this to your actual provider
OpenFeatureAPI.shared.provider = NoOpProvider()
OpenFeatureAPI.shared.setProvider(provider: NoOpProvider())

let client = OpenFeatureAPI.shared.getClient()
let value = client.getBooleanValue(key: "flag", defaultValue: false)
Expand Down
3 changes: 0 additions & 3 deletions Sources/OpenFeature/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ import Foundation
public protocol Client: Features {
var metadata: Metadata { get }

/// Return an optional client-level evaluation context.
var evaluationContext: EvaluationContext? { get set }

/// The hooks associated to this client.
var hooks: [AnyHook] { get }

Expand Down
2 changes: 0 additions & 2 deletions Sources/OpenFeature/EvaluationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,4 @@ public protocol EvaluationContext: Structure {
func getTargetingKey() -> String

func setTargetingKey(targetingKey: String)

func merge(overridingContext: EvaluationContext) -> EvaluationContext
}
16 changes: 11 additions & 5 deletions Sources/OpenFeature/FeatureProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ public protocol FeatureProvider {
var hooks: [AnyHook] { get }
var metadata: Metadata { get }

func getBooleanEvaluation(key: String, defaultValue: Bool, ctx: EvaluationContext) throws -> ProviderEvaluation<
// Called by OpenFeatureAPI whenever the new Provider is registered
func initialize(initialContext: EvaluationContext) async

// Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application
func onContextSet(oldContext: EvaluationContext, newContext: EvaluationContext) async

func getBooleanEvaluation(key: String, defaultValue: Bool) throws -> ProviderEvaluation<
Bool
>
func getStringEvaluation(key: String, defaultValue: String, ctx: EvaluationContext) throws -> ProviderEvaluation<
func getStringEvaluation(key: String, defaultValue: String) throws -> ProviderEvaluation<
String
>
func getIntegerEvaluation(key: String, defaultValue: Int64, ctx: EvaluationContext) throws -> ProviderEvaluation<
func getIntegerEvaluation(key: String, defaultValue: Int64) throws -> ProviderEvaluation<
Int64
>
func getDoubleEvaluation(key: String, defaultValue: Double, ctx: EvaluationContext) throws -> ProviderEvaluation<
func getDoubleEvaluation(key: String, defaultValue: Double) throws -> ProviderEvaluation<
Double
>
func getObjectEvaluation(key: String, defaultValue: Value, ctx: EvaluationContext) throws -> ProviderEvaluation<
func getObjectEvaluation(key: String, defaultValue: Value) throws -> ProviderEvaluation<
Value
>
}
40 changes: 10 additions & 30 deletions Sources/OpenFeature/Features.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,75 +4,55 @@ public protocol Features {
// MARK: Bool
func getBooleanValue(key: String, defaultValue: Bool) -> Bool

func getBooleanValue(key: String, defaultValue: Bool, ctx: EvaluationContext?) -> Bool

func getBooleanValue(key: String, defaultValue: Bool, ctx: EvaluationContext?, options: FlagEvaluationOptions)
func getBooleanValue(key: String, defaultValue: Bool, options: FlagEvaluationOptions)
-> Bool

func getBooleanDetails(key: String, defaultValue: Bool) -> FlagEvaluationDetails<Bool>

func getBooleanDetails(key: String, defaultValue: Bool, ctx: EvaluationContext?) -> FlagEvaluationDetails<Bool>

func getBooleanDetails(key: String, defaultValue: Bool, ctx: EvaluationContext?, options: FlagEvaluationOptions)
func getBooleanDetails(key: String, defaultValue: Bool, options: FlagEvaluationOptions)
-> FlagEvaluationDetails<Bool>

// MARK: String
func getStringValue(key: String, defaultValue: String) -> String

func getStringValue(key: String, defaultValue: String, ctx: EvaluationContext?) -> String

func getStringValue(key: String, defaultValue: String, ctx: EvaluationContext?, options: FlagEvaluationOptions)
func getStringValue(key: String, defaultValue: String, options: FlagEvaluationOptions)
-> String

func getStringDetails(key: String, defaultValue: String) -> FlagEvaluationDetails<String>

func getStringDetails(key: String, defaultValue: String, ctx: EvaluationContext?) -> FlagEvaluationDetails<String>

func getStringDetails(key: String, defaultValue: String, ctx: EvaluationContext?, options: FlagEvaluationOptions)
func getStringDetails(key: String, defaultValue: String, options: FlagEvaluationOptions)
-> FlagEvaluationDetails<String>

// MARK: Int
func getIntegerValue(key: String, defaultValue: Int64) -> Int64

func getIntegerValue(key: String, defaultValue: Int64, ctx: EvaluationContext?) -> Int64

func getIntegerValue(key: String, defaultValue: Int64, ctx: EvaluationContext?, options: FlagEvaluationOptions)
func getIntegerValue(key: String, defaultValue: Int64, options: FlagEvaluationOptions)
-> Int64

func getIntegerDetails(key: String, defaultValue: Int64) -> FlagEvaluationDetails<Int64>

func getIntegerDetails(key: String, defaultValue: Int64, ctx: EvaluationContext?) -> FlagEvaluationDetails<Int64>

func getIntegerDetails(key: String, defaultValue: Int64, ctx: EvaluationContext?, options: FlagEvaluationOptions)
func getIntegerDetails(key: String, defaultValue: Int64, options: FlagEvaluationOptions)
-> FlagEvaluationDetails<Int64>

// MARK: Double
func getDoubleValue(key: String, defaultValue: Double) -> Double

func getDoubleValue(key: String, defaultValue: Double, ctx: EvaluationContext?) -> Double

func getDoubleValue(key: String, defaultValue: Double, ctx: EvaluationContext?, options: FlagEvaluationOptions)
func getDoubleValue(key: String, defaultValue: Double, options: FlagEvaluationOptions)
-> Double

func getDoubleDetails(key: String, defaultValue: Double) -> FlagEvaluationDetails<Double>

func getDoubleDetails(key: String, defaultValue: Double, ctx: EvaluationContext?) -> FlagEvaluationDetails<Double>

func getDoubleDetails(key: String, defaultValue: Double, ctx: EvaluationContext?, options: FlagEvaluationOptions)
func getDoubleDetails(key: String, defaultValue: Double, options: FlagEvaluationOptions)
-> FlagEvaluationDetails<Double>

// MARK: Object
func getObjectValue(key: String, defaultValue: Value) -> Value

func getObjectValue(key: String, defaultValue: Value, ctx: EvaluationContext?) -> Value

func getObjectValue(key: String, defaultValue: Value, ctx: EvaluationContext?, options: FlagEvaluationOptions)
func getObjectValue(key: String, defaultValue: Value, options: FlagEvaluationOptions)
-> Value

func getObjectDetails(key: String, defaultValue: Value) -> FlagEvaluationDetails<Value>

func getObjectDetails(key: String, defaultValue: Value, ctx: EvaluationContext?) -> FlagEvaluationDetails<Value>

func getObjectDetails(key: String, defaultValue: Value, ctx: EvaluationContext?, options: FlagEvaluationOptions)
func getObjectDetails(key: String, defaultValue: Value, options: FlagEvaluationOptions)
-> FlagEvaluationDetails<Value>
}
5 changes: 2 additions & 3 deletions Sources/OpenFeature/Hook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
public protocol Hook {
associatedtype HookValue: Equatable

func before(ctx: HookContext<HookValue>, hints: [String: Any]) -> EvaluationContext?
func before(ctx: HookContext<HookValue>, hints: [String: Any])

func after(ctx: HookContext<HookValue>, details: FlagEvaluationDetails<HookValue>, hints: [String: Any])

Expand All @@ -15,8 +15,7 @@ public protocol Hook {
}

extension Hook {
func before(ctx: HookContext<HookValue>, hints: [String: Any]) -> EvaluationContext? {
return nil
func before(ctx: HookContext<HookValue>, hints: [String: Any]) {
}

func after(ctx: HookContext<HookValue>, details: FlagEvaluationDetails<HookValue>, hints: [String: Any]) {
Expand Down
26 changes: 8 additions & 18 deletions Sources/OpenFeature/HookSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,43 +109,33 @@ class HookSupport {
}
}

func beforeHooks<T>(flagValueType: FlagValueType, hookCtx: HookContext<T>, hooks: [AnyHook], hints: [String: Any])
-> EvaluationContext
{
let result =
hooks
func beforeHooks<T>(flagValueType: FlagValueType, hookCtx: HookContext<T>, hooks: [AnyHook], hints: [String: Any]) {
hooks
.reversed()
.filter { hook in hook.supportsFlagValueType(flagValueType: flagValueType) }
.compactMap { hook in
.forEach { hook in
switch hook {
case .boolean(let booleanHook):
if let booleanCtx = hookCtx as? HookContext<Bool> {
return booleanHook.before(ctx: booleanCtx, hints: hints)
booleanHook.before(ctx: booleanCtx, hints: hints)
}
case .integer(let integerHook):
if let integerCtx = hookCtx as? HookContext<Int64> {
return integerHook.before(ctx: integerCtx, hints: hints)
integerHook.before(ctx: integerCtx, hints: hints)
}
case .double(let doubleHook):
if let doubleCtx = hookCtx as? HookContext<Double> {
return doubleHook.before(ctx: doubleCtx, hints: hints)
doubleHook.before(ctx: doubleCtx, hints: hints)
}
case .string(let stringHook):
if let stringCtx = hookCtx as? HookContext<String> {
return stringHook.before(ctx: stringCtx, hints: hints)
stringHook.before(ctx: stringCtx, hints: hints)
}
case .object(let objectHook):
if let objectCtx = hookCtx as? HookContext<Value> {
return objectHook.before(ctx: objectCtx, hints: hints)
objectHook.before(ctx: objectCtx, hints: hints)
}
}

return nil
}

return hookCtx.ctx.merge(
overridingContext: result.reduce(hookCtx.ctx) { acc, cur in
acc.merge(overridingContext: cur)
})
}
}
14 changes: 0 additions & 14 deletions Sources/OpenFeature/MutableContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,6 @@ public class MutableContext: EvaluationContext {
self.targetingKey = targetingKey
}

public func merge(overridingContext: EvaluationContext) -> EvaluationContext {
let merged = self.asMap().merging(overridingContext.asMap()) { _, new in new }
let mergedContext = MutableContext(attributes: merged)

if !self.targetingKey.isEmpty {
mergedContext.setTargetingKey(targetingKey: self.targetingKey)
}
if !overridingContext.getTargetingKey().trimmingCharacters(in: .whitespaces).isEmpty {
mergedContext.setTargetingKey(targetingKey: overridingContext.getTargetingKey())
}

return mergedContext
}

public func keySet() -> Set<String> {
return structure.keySet()
}
Expand Down
18 changes: 13 additions & 5 deletions Sources/OpenFeature/NoOpProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,43 @@ class NoOpProvider: FeatureProvider {
var metadata: Metadata = NoOpMetadata(name: "No-op provider")
var hooks: [AnyHook] = []

func getBooleanEvaluation(key: String, defaultValue: Bool, ctx: EvaluationContext) throws -> ProviderEvaluation<
func onContextSet(oldContext: EvaluationContext, newContext: EvaluationContext) {
// no-op
}

func initialize(initialContext: EvaluationContext) {
// no-op
}

func getBooleanEvaluation(key: String, defaultValue: Bool) throws -> ProviderEvaluation<
Bool
> {
return ProviderEvaluation(
value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue)
}

func getStringEvaluation(key: String, defaultValue: String, ctx: EvaluationContext) throws -> ProviderEvaluation<
func getStringEvaluation(key: String, defaultValue: String) throws -> ProviderEvaluation<
String
> {
return ProviderEvaluation(
value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue)
}

func getIntegerEvaluation(key: String, defaultValue: Int64, ctx: EvaluationContext) throws -> ProviderEvaluation<
func getIntegerEvaluation(key: String, defaultValue: Int64) throws -> ProviderEvaluation<
Int64
> {
return ProviderEvaluation(
value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue)
}

func getDoubleEvaluation(key: String, defaultValue: Double, ctx: EvaluationContext) throws -> ProviderEvaluation<
func getDoubleEvaluation(key: String, defaultValue: Double) throws -> ProviderEvaluation<
Double
> {
return ProviderEvaluation(
value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue)
}

func getObjectEvaluation(key: String, defaultValue: Value, ctx: EvaluationContext) throws -> ProviderEvaluation<
func getObjectEvaluation(key: String, defaultValue: Value) throws -> ProviderEvaluation<
Value
> {
return ProviderEvaluation(
Expand Down
71 changes: 37 additions & 34 deletions Sources/OpenFeature/OpenFeatureAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,52 @@ import Foundation
/// A global singleton which holds base configuration for the OpenFeature library.
/// Configuration here will be shared across all ``Client``s.
public class OpenFeatureAPI {
// TODO: We use DispatchQueue here instead of being an actor to not lock into new versions of Swift
private let contextQueue = DispatchQueue(label: "dev.openfeature.api.context")
private let providerQueue = DispatchQueue(label: "dev.openfeature.api.provider")
private let hookQueue = DispatchQueue(label: "dev.openfeature.api.hook")

private var _provider: FeatureProvider?
public var provider: FeatureProvider? {
get {
return self._provider
}
set {
self.providerQueue.sync {
self._provider = newValue
}
}
private var _evaluationContext: EvaluationContext = MutableContext()
private(set) var hooks: [AnyHook] = []

/// The ``OpenFeatureAPI`` singleton
static public let shared = OpenFeatureAPI()

public init() {
}

private var _evaluationContext: EvaluationContext?
public var evaluationContext: EvaluationContext? {
get {
return self._evaluationContext
}
set {
self.contextQueue.sync {
self._evaluationContext = newValue
}
public func setProvider(provider: FeatureProvider) async {
await self.setProvider(provider: provider, initialContext: nil)
}

public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) async {
await provider.initialize(initialContext: initialContext ?? self._evaluationContext)
self._provider = provider
guard let newEvaluationContext = initialContext else {
return
}
self._evaluationContext = newEvaluationContext
}

private(set) var hooks: [AnyHook] = []
public func getProvider() -> FeatureProvider? {
return self._provider
}

/// The ``OpenFeatureAPI`` singleton
static public let shared = OpenFeatureAPI()
public func clearProvider() {
self._provider = nil
}

public init() {
public func setEvaluationContext(evaluationContext: EvaluationContext) async {
await getProvider()?.onContextSet(oldContext: self._evaluationContext, newContext: evaluationContext)
// A provider evaluation reading the global ctx at this point would fail due to stale cache.
// To prevent this, the provider should internally manage the ctx to use on each evaluation, and
// make sure it's aligned with the values in the cache at all times. If no guarantees are offered by
// the provider, the application can expect STALE resolves while setting a new global ctx
self._evaluationContext = evaluationContext
}

public func getEvaluationContext() -> EvaluationContext? {
return self._evaluationContext
}

public func getProviderMetadata() -> Metadata? {
return self.provider?.metadata
return self.getProvider()?.metadata
}

public func getClient() -> Client {
Expand All @@ -53,14 +60,10 @@ public class OpenFeatureAPI {
}

public func addHooks(hooks: AnyHook...) {
hookQueue.sync {
self.hooks.append(contentsOf: hooks)
}
self.hooks.append(contentsOf: hooks)
}

public func clearHooks() {
hookQueue.sync {
self.hooks.removeAll()
}
self.hooks.removeAll()
}
}
Loading

0 comments on commit cbd1a0d

Please sign in to comment.