From bf368df1fba8b545b22f16a901d1ebd7b5d6fde9 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Tue, 18 Jun 2024 10:20:44 +0200 Subject: [PATCH] feat: CreateConfidence from Provider for Metadata (#141) * feat: SDK ID for native and OF * feat: Force correct init for Provider --- Sources/Confidence/Confidence.swift | 45 +++++++++++----- .../RemoteResolveConfidenceClient.swift | 5 +- .../ConfidenceFeatureProvider.swift | 53 ++++++++++++++++--- .../ConfidenceProviderTest.swift | 20 ++++++- Tests/ConfidenceTests/ConfidenceTest.swift | 7 +++ 5 files changed, 107 insertions(+), 23 deletions(-) diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 09cc2599..067d69e5 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -3,21 +3,25 @@ import Combine import os public class Confidence: ConfidenceEventSender { - public let clientSecret: String - public var region: ConfidenceRegion + private let clientSecret: String + private var region: ConfidenceRegion private let parent: ConfidenceContextProvider? private let eventSenderEngine: EventSenderEngine private let contextSubject = CurrentValueSubject([:]) private var removedContextKeys: Set = Set() private let confidenceQueue = DispatchQueue(label: "com.confidence.queue") - private let remoteFlagResolver: ConfidenceResolveClient private let flagApplier: FlagApplier private var cache = FlagResolution.EMPTY private var storage: Storage - internal let contextReconciliatedChanges = PassthroughSubject() private var cancellables = Set() private var currentFetchTask: Task<(), Never>? + // Internal for testing + internal let remoteFlagResolver: ConfidenceResolveClient + internal let contextReconciliatedChanges = PassthroughSubject() + + public static let sdkId: String = "SDK_ID_SWIFT_CONFIDENCE" + required init( clientSecret: String, region: ConfidenceRegion, @@ -255,15 +259,20 @@ public class Confidence: ConfidenceEventSender { extension Confidence { public class Builder { - let clientSecret: String + // Must be configured or configured automatically + internal let clientSecret: String + internal let eventStorage: EventStorage + internal let visitorId = VisitorUtil().getId() + + // Can be configured + internal var region: ConfidenceRegion = .global + internal var metadata: ConfidenceMetadata? + internal var initialContext: ConfidenceStruct = [:] + + // Injectable for testing internal var flagApplier: FlagApplier? internal var storage: Storage? - internal let eventStorage: EventStorage internal var flagResolver: ConfidenceResolveClient? - var region: ConfidenceRegion = .global - - var visitorId = VisitorUtil().getId() - var initialContext: ConfidenceStruct = [:] /** Initializes the builder with the given credentails. @@ -293,6 +302,9 @@ extension Confidence { return self } + /** + Sets the initial Context. + */ public func withContext(initialContext: ConfidenceStruct) -> Builder { self.initialContext = initialContext return self @@ -307,12 +319,21 @@ extension Confidence { return self } + /** + Overrides the Metadata for the Confidence instance. In normal production scenarios, Metadata is + handled automatically by the SDK and this overrides should not be applied. + */ + public func withMetadata(metadata: ConfidenceMetadata) -> Builder { + self.metadata = metadata + return self + } + public func build() -> Confidence { let options = ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: clientSecret), region: region) - let metadata = ConfidenceMetadata( - name: "SDK_ID_SWIFT_CONFIDENCE", + let metadata = metadata ?? ConfidenceMetadata( + name: Confidence.sdkId, version: "0.1.4") // x-release-please-version let uploader = RemoteConfidenceClient( options: options, diff --git a/Sources/Confidence/RemoteResolveConfidenceClient.swift b/Sources/Confidence/RemoteResolveConfidenceClient.swift index 7521f75a..8c725771 100644 --- a/Sources/Confidence/RemoteResolveConfidenceClient.swift +++ b/Sources/Confidence/RemoteResolveConfidenceClient.swift @@ -3,11 +3,12 @@ import Foundation public class RemoteConfidenceResolveClient: ConfidenceResolveClient { private let targetingKey = "targeting_key" private var options: ConfidenceClientOptions - private let metadata: ConfidenceMetadata - private var httpClient: HttpClient private var applyOnResolve: Bool + // Internal for testing + internal let metadata: ConfidenceMetadata + init( options: ConfidenceClientOptions, session: URLSession? = nil, diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index b611c7f1..4b6d88c0 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -5,12 +5,14 @@ import OpenFeature import os struct Metadata: ProviderMetadata { - var name: String? + var name: String? = ConfidenceFeatureProvider.providerId } /// The implementation of the Confidence Feature Provider. This implementation allows to pre-cache evaluations. public class ConfidenceFeatureProvider: FeatureProvider { - public var metadata: ProviderMetadata + public static let providerId: String = "SDK_ID_SWIFT_PROVIDER" + + public var metadata: ProviderMetadata = Metadata() public var hooks: [any Hook] = [] private let lock = UnfairLock() private let initializationStrategy: InitializationStrategy @@ -20,8 +22,47 @@ public class ConfidenceFeatureProvider: FeatureProvider { private var currentResolveTask: Task? private let confidenceFeatureProviderQueue = DispatchQueue(label: "com.provider.queue") - /// Initialize the Provider via a `Confidence` object. - public convenience init(confidence: Confidence, initializationStrategy: InitializationStrategy = .fetchAndActivate) { + /** + Creates the `Confidence` object to be used as init parameter for this Provider. + */ + public static func createConfidence(clientSecret: String) -> ConfidenceForOpenFeature { + return ConfidenceForOpenFeature.init(confidence: Confidence.Builder.init(clientSecret: clientSecret) + .withRegion(region: .global) + .withMetadata(metadata: ConfidenceMetadata.init( + name: providerId, + version: "0.2.1") // x-release-please-version + ) + .build()) + } + + /** + Proxy holder to ensure correct Confidence configuration passed into the Provider's init. + Do not instantiate directly. + */ + public class ConfidenceForOpenFeature { + internal init(confidence: Confidence) { + self.confidence = confidence + } + let confidence: Confidence + } + + /** + Initialize the Provider via a `Confidence` object. + The `Confidence` object must be creted via the `createConfidence` function available from this same class, + rather then be instantiated directly via `Confidence.init(...)` as you would if not using the OpenFeature integration. + */ + public convenience init( + confidenceForOF: ConfidenceForOpenFeature, + initializationStrategy: InitializationStrategy = .fetchAndActivate + ) { + self.init(confidence: confidenceForOF.confidence, session: nil) + } + + // Allows to pass a confidence object with injected configurations for testing + internal convenience init( + confidence: Confidence, + initializationStrategy: InitializationStrategy = .fetchAndActivate + ) { self.init(confidence: confidence, session: nil) } @@ -30,10 +71,6 @@ public class ConfidenceFeatureProvider: FeatureProvider { initializationStrategy: InitializationStrategy = .fetchAndActivate, session: URLSession? ) { - let metadata = ConfidenceMetadata( - name: "SDK_ID_SWIFT_PROVIDER", - version: "0.2.1") // x-release-please-version - self.metadata = Metadata(name: metadata.name) self.initializationStrategy = initializationStrategy self.confidence = confidence } diff --git a/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift b/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift index edf4945a..f5f580c8 100644 --- a/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift +++ b/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift @@ -1,10 +1,10 @@ import Foundation -import ConfidenceProvider import Combine import OpenFeature import XCTest @testable import Confidence +@testable import ConfidenceProvider class ConfidenceProviderTest: XCTestCase { private var readyExpectation = XCTestExpectation(description: "Ready") @@ -74,4 +74,22 @@ class ConfidenceProviderTest: XCTestCase { await fulfillment(of: [errorExpectation], timeout: 5.0) cancellable.cancel() } + + func testMetadata() async throws { + class FakeClient: ConfidenceResolveClient { + func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + throw ConfidenceError.internalError(message: "test") + } + } + + let confidenceForOpenFeature = ConfidenceFeatureProvider.createConfidence(clientSecret: "testSecret") + let provider = ConfidenceFeatureProvider( + confidenceForOF: confidenceForOpenFeature, + initializationStrategy: .activateAndFetchAsync + ) + XCTAssertEqual("SDK_ID_SWIFT_PROVIDER", provider.metadata.name) + let remoteClient = confidenceForOpenFeature.confidence.remoteFlagResolver as? RemoteConfidenceResolveClient + XCTAssertEqual("SDK_ID_SWIFT_PROVIDER", remoteClient?.metadata.name) + XCTAssertNotEqual("", remoteClient?.metadata.version) + } } diff --git a/Tests/ConfidenceTests/ConfidenceTest.swift b/Tests/ConfidenceTests/ConfidenceTest.swift index 25543438..bb97b5b5 100644 --- a/Tests/ConfidenceTests/ConfidenceTest.swift +++ b/Tests/ConfidenceTests/ConfidenceTest.swift @@ -581,6 +581,13 @@ class ConfidenceTest: XCTestCase { XCTAssertEqual(error as? ConfidenceError, ConfidenceError.invalidContextInMessage) } } + + func testConfidenceMetadata() { + let confidence = Confidence.Builder(clientSecret: "").build() + let remoteClient = confidence.remoteFlagResolver as? RemoteConfidenceResolveClient + XCTAssertEqual("SDK_ID_SWIFT_CONFIDENCE", remoteClient?.metadata.name) + XCTAssertNotEqual("", remoteClient?.metadata.version) + } } final class DispatchQueueFake: DispatchQueueType {