From 2e49e2370d29d63450cc094894743fae92914df5 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Tue, 2 Apr 2024 15:20:56 +0200 Subject: [PATCH] refactor: Add Confidence Library scaffolding (#83) * chore: Update ignore * Remove xcscheme file from repo * Fix test script used in CI * feat: First Confidence Scaffolding * Basic EvaluationContext wiring in events * More Builder and constructor refactoring * Update README * Move enum to standalone files * Remove temporary code --- .gitignore | 4 +- .../xcschemes/ConfidenceDemoApp.xcscheme | 100 ---------------- .../ConfidenceDemoApp/ConfidenceDemoApp.swift | 17 ++- Package.swift | 13 ++- README.md | 4 +- Sources/Confidence/Confidence.swift | 82 +++++++++++++ .../Confidence/ConfidenceEventSender.swift | 7 ++ Sources/Confidence/ConfidenceRegion.swift | 9 ++ Sources/Confidence/Contextual.swift | 15 +++ .../InitializationStrategy.swift | 1 + .../ConfidenceClientOptions.swift | 32 ++++++ .../RemoteConfidenceClient.swift | 33 +----- .../ConfidenceFeatureProvider.swift | 108 +++++++++++------- .../Http/NetworkClient.swift | 1 + scripts/run_tests.sh | 3 +- 15 files changed, 241 insertions(+), 188 deletions(-) delete mode 100644 ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/xcshareddata/xcschemes/ConfidenceDemoApp.xcscheme create mode 100644 Sources/Confidence/Confidence.swift create mode 100644 Sources/Confidence/ConfidenceEventSender.swift create mode 100644 Sources/Confidence/ConfidenceRegion.swift create mode 100644 Sources/Confidence/Contextual.swift rename Sources/{ConfidenceProvider => Confidence}/InitializationStrategy.swift (59%) create mode 100644 Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClientOptions.swift diff --git a/.gitignore b/.gitignore index a6cc5249..ad8ce721 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .DS_Store /.build /Packages -/*.xcodeproj +/**/*.xcodeproj xcuserdata/ DerivedData/ .swiftpm/config/registries.json @@ -10,4 +10,4 @@ DerivedData/ .build .mockingbird project.json -.swiftpm +.swiftpm \ No newline at end of file diff --git a/ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/xcshareddata/xcschemes/ConfidenceDemoApp.xcscheme b/ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/xcshareddata/xcschemes/ConfidenceDemoApp.xcscheme deleted file mode 100644 index 1514facf..00000000 --- a/ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/xcshareddata/xcschemes/ConfidenceDemoApp.xcscheme +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift index 3bb4737f..e84a5d2b 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift @@ -1,4 +1,5 @@ import ConfidenceProvider +import Confidence import OpenFeature import SwiftUI @@ -21,17 +22,21 @@ extension ConfidenceDemoApp { } // If we have no cache, then do a fetch first. - var initializationStratgey: InitializationStrategy = .activateAndFetchAsync + var initializationStrategy: InitializationStrategy = .activateAndFetchAsync if ConfidenceFeatureProvider.isStorageEmpty() { - initializationStratgey = .fetchAndActivate + initializationStrategy = .fetchAndActivate } - let provider = ConfidenceFeatureProvider - .Builder(credentials: .clientSecret(secret: secret)) - .with(initializationStrategy: initializationStratgey) + let confidence = Confidence.Builder(clientSecret: secret) + .withInitializationstrategy(initializationStrategy: initializationStrategy) .build() + let provider = ConfidenceFeatureProvider(confidence: confidence) + // NOTE: Using a random UUID for each app start is not advised and can result in getting stale values. let ctx = MutableContext(targetingKey: UUID.init().uuidString, structure: MutableStructure()) - OpenFeatureAPI.shared.setProvider(provider: provider, initialContext: ctx) + Task { + await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: ctx) + confidence.send(eventName: "my_event") + } } } diff --git a/Package.swift b/Package.swift index d4a35f41..74afe395 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( - name: "ConfidenceProvider", + name: "Confidence", platforms: [ .iOS(.v14), .macOS(.v12) @@ -12,16 +12,25 @@ let package = Package( products: [ .library( name: "ConfidenceProvider", - targets: ["ConfidenceProvider"]) + targets: ["ConfidenceProvider"]), + .library( + name: "Confidence", + targets: ["Confidence"]) ], dependencies: [ .package(url: "git@github.com:open-feature/swift-sdk.git", from: "0.1.0"), ], targets: [ + .target( + name: "Confidence", + dependencies: [], + plugins: [] + ), .target( name: "ConfidenceProvider", dependencies: [ .product(name: "OpenFeature", package: "swift-sdk"), + "Confidence" ], plugins: [] ), diff --git a/README.md b/README.md index 969ba75e..aaa25f5d 100644 --- a/README.md +++ b/README.md @@ -37,16 +37,18 @@ In the dependencies section of Package.swift add: and in the target dependencies section add: ```swift .product(name: "ConfidenceProvider", package: "confidence-openfeature-provider-swift"), +.product(name: "Confidence", package: "confidence-openfeature-provider-swift"), ``` ## Usage ### Import Modules -Import the `ConfidenceProvider` and `OpenFeature` modules +Import the `ConfidenceProvider`, the `Confidence` and the `OpenFeature` modules ```swift import ConfidenceProvider +import Confidence import OpenFeature ``` diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift new file mode 100644 index 00000000..99d1e282 --- /dev/null +++ b/Sources/Confidence/Confidence.swift @@ -0,0 +1,82 @@ +import Foundation + +public class Confidence: ConfidenceEventSender { + public var context: [String: String] + public let clientSecret: String + public var timeout: TimeInterval + public var region: ConfidenceRegion + public var initializationStrategy: InitializationStrategy + + init( + clientSecret: String, + timeout: TimeInterval, + region: ConfidenceRegion, + initializationStrategy: InitializationStrategy + ) { + self.context = [:] + self.clientSecret = clientSecret + self.timeout = timeout + self.region = region + self.initializationStrategy = initializationStrategy + } + + // TODO: Implement actual event uploading to the backend + public func send(eventName: String) { + print("Sending \(eventName) - Targeting key: \(context["targeting_key"] ?? "UNKNOWN")") + } + + public func updateContextEntry(key: String, value: String) { + context[key] = value + } + + public func removeContextEntry(key: String) { + context.removeValue(forKey: key) + } + + public func clearContext() { + context = [:] + } + + // TODO: Implement creation of child instances + public func withContext(_ context: [String: String]) -> Self { + return self + } +} + +extension Confidence { + public class Builder { + let clientSecret: String + var timeout: TimeInterval = 10.0 + var region: ConfidenceRegion = .global + var initializationStrategy: InitializationStrategy = .fetchAndActivate + + public init(clientSecret: String) { + self.clientSecret = clientSecret + } + + public func withTimeout(timeout: TimeInterval) -> Builder { + self.timeout = timeout + return self + } + + + public func withRegion(region: ConfidenceRegion) -> Builder { + self.region = region + return self + } + + public func withInitializationstrategy(initializationStrategy: InitializationStrategy) -> Builder { + self.initializationStrategy = initializationStrategy + return self + } + + public func build() -> Confidence { + return Confidence( + clientSecret: clientSecret, + timeout: timeout, + region: region, + initializationStrategy: initializationStrategy + ) + } + } +} diff --git a/Sources/Confidence/ConfidenceEventSender.swift b/Sources/Confidence/ConfidenceEventSender.swift new file mode 100644 index 00000000..4747a543 --- /dev/null +++ b/Sources/Confidence/ConfidenceEventSender.swift @@ -0,0 +1,7 @@ +import Foundation + +/// Sends events to Confidence. Contextual data is appended to each event +// TODO: Add functions for sending events with payload +public protocol ConfidenceEventSender: Contextual { + func send(eventName: String) +} diff --git a/Sources/Confidence/ConfidenceRegion.swift b/Sources/Confidence/ConfidenceRegion.swift new file mode 100644 index 00000000..231b90a2 --- /dev/null +++ b/Sources/Confidence/ConfidenceRegion.swift @@ -0,0 +1,9 @@ +import Foundation + +/// Sets the region for the network request to the Confidence backend. +/// This is applied for both sending events as well as fetching flag's data. +public enum ConfidenceRegion { + case global + case europe + case usa +} diff --git a/Sources/Confidence/Contextual.swift b/Sources/Confidence/Contextual.swift new file mode 100644 index 00000000..9b6859ce --- /dev/null +++ b/Sources/Confidence/Contextual.swift @@ -0,0 +1,15 @@ +import Foundation + +/// A Contextual implementer maintains context data and can create child instances +/// that can still access their parent's data +public protocol Contextual { + // TODO: Add complex type to the context Dictionary + var context: [String: String] { get set } + + func updateContextEntry(key: String, value: String) + func removeContextEntry(key: String) + func clearContext() + /// Creates a child Contextual instance that still has access + /// to its parent context + func withContext(_ context: [String: String]) -> Self +} diff --git a/Sources/ConfidenceProvider/InitializationStrategy.swift b/Sources/Confidence/InitializationStrategy.swift similarity index 59% rename from Sources/ConfidenceProvider/InitializationStrategy.swift rename to Sources/Confidence/InitializationStrategy.swift index 0669c528..8dfbc0c0 100644 --- a/Sources/ConfidenceProvider/InitializationStrategy.swift +++ b/Sources/Confidence/InitializationStrategy.swift @@ -1,5 +1,6 @@ import Foundation +/// Flag resolve configuration related to how to refresh flags at startup public enum InitializationStrategy { case fetchAndActivate, activateAndFetchAsync } diff --git a/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClientOptions.swift b/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClientOptions.swift new file mode 100644 index 00000000..4c8de133 --- /dev/null +++ b/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClientOptions.swift @@ -0,0 +1,32 @@ +import Foundation +import Confidence + +public struct ConfidenceClientOptions { + public var credentials: ConfidenceClientCredentials + public var timeout: TimeInterval + public var region: ConfidenceRegion + public var initializationStrategy: InitializationStrategy + + public init( + credentials: ConfidenceClientCredentials, + timeout: TimeInterval? = nil, + region: ConfidenceRegion? = nil, + initializationStrategy: InitializationStrategy = .fetchAndActivate + ) { + self.credentials = credentials + self.timeout = timeout ?? 10.0 + self.region = region ?? .global + self.initializationStrategy = initializationStrategy + } +} + +public enum ConfidenceClientCredentials { + case clientSecret(secret: String) + + public func getSecret() -> String { + switch self { + case .clientSecret(let secret): + return secret + } + } +} diff --git a/Sources/ConfidenceProvider/ConfidenceClient/RemoteConfidenceClient.swift b/Sources/ConfidenceProvider/ConfidenceClient/RemoteConfidenceClient.swift index 3ceba804..71b93fa8 100644 --- a/Sources/ConfidenceProvider/ConfidenceClient/RemoteConfidenceClient.swift +++ b/Sources/ConfidenceProvider/ConfidenceClient/RemoteConfidenceClient.swift @@ -1,6 +1,8 @@ import Foundation +import Confidence import OpenFeature + public class RemoteConfidenceClient: ConfidenceClient { private let targetingKey = "targeting_key" private let flagApplier: FlagApplier @@ -161,37 +163,6 @@ struct ApplyFlagsRequest: Codable { struct ApplyFlagsResponse: Codable { } -public struct ConfidenceClientOptions { - public var credentials: ConfidenceClientCredentials - public var timeout: TimeInterval - public var region: ConfidenceRegion - - public init( - credentials: ConfidenceClientCredentials, timeout: TimeInterval? = nil, region: ConfidenceRegion? = nil - ) { - self.credentials = credentials - self.timeout = timeout ?? 10.0 - self.region = region ?? .global - } -} - -public enum ConfidenceClientCredentials { - case clientSecret(secret: String) - - public func getSecret() -> String { - switch self { - case .clientSecret(let secret): - return secret - } - } -} - -public enum ConfidenceRegion { - case global - case europe - case usa -} - struct Sdk: Codable { init(id: String?, version: String?) { self.id = id ?? "SDK_ID_SWIFT_PROVIDER" diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 8b41fe16..e224e623 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -1,4 +1,5 @@ import Foundation +import Confidence import OpenFeature import Combine import os @@ -21,8 +22,9 @@ public class ConfidenceFeatureProvider: FeatureProvider { private let initializationStrategy: InitializationStrategy private let storage: Storage private let eventHandler = EventHandler(ProviderEvent.notReady) + private let confidence: Confidence? - /// Should not be called externally, use `ConfidenceFeatureProvider.Builder` instead. + /// Should not be called externally, use `ConfidenceFeatureProvider.Builder`or init with `Confidence` instead. init( metadata: ProviderMetadata, client: RemoteConfidenceClient, @@ -30,8 +32,8 @@ public class ConfidenceFeatureProvider: FeatureProvider { storage: Storage, overrides: [String: LocalOverride] = [:], flagApplier: FlagApplier, - applyStorage: Storage, - initializationStrategy: InitializationStrategy + initializationStrategy: InitializationStrategy, + confidence: Confidence? ) { self.client = client self.metadata = metadata @@ -40,8 +42,34 @@ public class ConfidenceFeatureProvider: FeatureProvider { self.flagApplier = flagApplier self.initializationStrategy = initializationStrategy self.storage = storage + self.confidence = confidence + self.resolver = LocalStorageResolver(cache: cache) + } - resolver = LocalStorageResolver(cache: cache) + /// Initialize the Provider via a `Confidence` object. + public init(confidence: Confidence) { + let metadata = ConfidenceMetadata(version: "0.1.4") // x-release-please-version + let options = ConfidenceClientOptions( + credentials: ConfidenceClientCredentials.clientSecret(secret: confidence.clientSecret), + timeout: confidence.timeout, + region: confidence.region) + self.metadata = metadata + self.cache = InMemoryProviderCache.from(storage: DefaultStorage.resolverFlagsCache()) + self.storage = DefaultStorage.resolverFlagsCache() + self.resolver = LocalStorageResolver(cache: cache) + self.flagApplier = FlagApplierWithRetries( + httpClient: NetworkClient(region: options.region), + storage: DefaultStorage.applierFlagsCache(), + options: options, + metadata: metadata) + self.client = RemoteConfidenceClient( + options: options, + applyOnResolve: false, + flagApplier: flagApplier, + metadata: metadata) + self.initializationStrategy = confidence.initializationStrategy + self.overrides = [:] + self.confidence = confidence } public func initialize(initialContext: OpenFeature.EvaluationContext?) { @@ -115,7 +143,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws - -> OpenFeature.ProviderEvaluation + -> OpenFeature.ProviderEvaluation { return try errorWrappedResolveFlag( flag: key, @@ -125,7 +153,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } public func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws - -> OpenFeature.ProviderEvaluation + -> OpenFeature.ProviderEvaluation { return try errorWrappedResolveFlag( flag: key, @@ -135,7 +163,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } public func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws - -> OpenFeature.ProviderEvaluation + -> OpenFeature.ProviderEvaluation { return try errorWrappedResolveFlag( flag: key, @@ -145,7 +173,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } public func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws - -> OpenFeature.ProviderEvaluation + -> OpenFeature.ProviderEvaluation { return try errorWrappedResolveFlag( flag: key, @@ -155,7 +183,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } public func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value, context: EvaluationContext?) - throws -> OpenFeature.ProviderEvaluation + throws -> OpenFeature.ProviderEvaluation { return try errorWrappedResolveFlag( flag: key, @@ -194,7 +222,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } public func errorWrappedResolveFlag(flag: String, defaultValue: T, ctx: EvaluationContext?, errorPrefix: String) - throws -> ProviderEvaluation + throws -> ProviderEvaluation { do { let path = try FlagPath.getPath(for: flag) @@ -251,18 +279,18 @@ public class ConfidenceFeatureProvider: FeatureProvider { ) return evaluationResult } catch ConfidenceError.cachedValueExpired { - return ProviderEvaluation(value: defaultValue, - variant: nil, - reason: Reason.error.rawValue, - errorCode: ErrorCode.providerNotReady - )} - catch { + return ProviderEvaluation( + value: defaultValue, + variant: nil, + reason: Reason.error.rawValue, + errorCode: ErrorCode.providerNotReady) + } catch { throw error } } private func resolveFlagNoValue(defaultValue: T, resolverResult: ResolveResult, ctx: EvaluationContext) - -> ProviderEvaluation + -> ProviderEvaluation { switch resolverResult.resolvedValue.resolveReason { case .noMatch: @@ -409,8 +437,9 @@ extension ConfidenceFeatureProvider { var cache: ProviderCache? var flagApplier: (any FlagApplier)? var initializationStrategy: InitializationStrategy = .fetchAndActivate - var applyStorage: Storage = DefaultStorage.resolverApplyCache() + var confidence: Confidence? + /// DEPRECATED /// Initializes the builder with the given credentails. /// /// OpenFeatureAPI.shared.setProvider(provider: @@ -427,8 +456,7 @@ extension ConfidenceFeatureProvider { flagApplier: FlagApplier?, storage: Storage, cache: ProviderCache?, - initializationStrategy: InitializationStrategy, - applyStorage: Storage + initializationStrategy: InitializationStrategy ) { self.options = options self.session = session @@ -437,7 +465,6 @@ extension ConfidenceFeatureProvider { self.storage = storage self.cache = cache self.initializationStrategy = initializationStrategy - self.applyStorage = applyStorage } /// Allows the `ConfidenceClient` to be configured with a custom URLSession, useful for @@ -453,8 +480,7 @@ extension ConfidenceFeatureProvider { flagApplier: flagApplier, storage: storage, cache: cache, - initializationStrategy: initializationStrategy, - applyStorage: applyStorage + initializationStrategy: initializationStrategy ) } @@ -470,8 +496,7 @@ extension ConfidenceFeatureProvider { flagApplier: flagApplier, storage: storage, cache: cache, - initializationStrategy: initializationStrategy, - applyStorage: applyStorage + initializationStrategy: initializationStrategy ) } @@ -487,8 +512,7 @@ extension ConfidenceFeatureProvider { flagApplier: flagApplier, storage: storage, cache: cache, - initializationStrategy: initializationStrategy, - applyStorage: applyStorage + initializationStrategy: initializationStrategy ) } @@ -504,8 +528,7 @@ extension ConfidenceFeatureProvider { flagApplier: flagApplier, storage: storage, cache: cache, - initializationStrategy: initializationStrategy, - applyStorage: applyStorage + initializationStrategy: initializationStrategy ) } @@ -521,8 +544,7 @@ extension ConfidenceFeatureProvider { flagApplier: flagApplier, storage: storage, cache: cache, - initializationStrategy: initializationStrategy, - applyStorage: applyStorage + initializationStrategy: initializationStrategy ) } @@ -538,8 +560,7 @@ extension ConfidenceFeatureProvider { flagApplier: flagApplier, storage: storage, cache: cache, - initializationStrategy: initializationStrategy, - applyStorage: applyStorage + initializationStrategy: initializationStrategy ) } @@ -572,21 +593,20 @@ extension ConfidenceFeatureProvider { flagApplier: flagApplier, storage: storage, cache: cache, - initializationStrategy: initializationStrategy, - applyStorage: applyStorage + initializationStrategy: initializationStrategy ) } /// Creates the `ConfidenceFeatureProvider` according to the settings specified in the builder. public func build() -> ConfidenceFeatureProvider { let flagApplier = - flagApplier - ?? FlagApplierWithRetries( - httpClient: NetworkClient(region: options.region), - storage: DefaultStorage.applierFlagsCache(), - options: options, - metadata: metadata - ) + flagApplier + ?? FlagApplierWithRetries( + httpClient: NetworkClient(region: options.region), + storage: DefaultStorage.applierFlagsCache(), + options: options, + metadata: metadata + ) let cache = cache ?? InMemoryProviderCache.from(storage: storage) @@ -605,8 +625,8 @@ extension ConfidenceFeatureProvider { storage: storage, overrides: localOverrides, flagApplier: flagApplier, - applyStorage: applyStorage, - initializationStrategy: initializationStrategy + initializationStrategy: initializationStrategy, + confidence: confidence ) } } diff --git a/Sources/ConfidenceProvider/Http/NetworkClient.swift b/Sources/ConfidenceProvider/Http/NetworkClient.swift index d6a4e47f..ff43a61f 100644 --- a/Sources/ConfidenceProvider/Http/NetworkClient.swift +++ b/Sources/ConfidenceProvider/Http/NetworkClient.swift @@ -1,4 +1,5 @@ import Foundation +import Confidence final class NetworkClient: HttpClient { private let headers: [String: String] diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 72fc485b..04385d5c 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -16,7 +16,6 @@ fi (cd $root_dir && TEST_RUNNER_CLIENT_TOKEN=$test_runner_client_token TEST_RUNNER_TEST_FLAG_NAME=$2 xcodebuild \ -quiet \ - -scheme ConfidenceProvider \ - -sdk "iphonesimulator" \ + -scheme Confidence-Package \ -destination 'platform=iOS Simulator,name=iPhone 14 Pro,OS=16.2' \ test)