diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SplitProvider.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SplitProvider.xcscheme new file mode 100644 index 0000000..fb2cde7 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/SplitProvider.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/SplitProvider/Evaluator.swift b/Sources/SplitProvider/Evaluator.swift new file mode 100644 index 0000000..aa50769 --- /dev/null +++ b/Sources/SplitProvider/Evaluator.swift @@ -0,0 +1,36 @@ +// Created by Martin Cardozo on 24/10/2025. + +import Split +import OpenFeature + +final class Evaluator { + + private let splitClient: SplitClient? + + init(splitClient: SplitClient?) { + self.splitClient = splitClient + } + + private func parseValue(_ value: String, as type: T.Type) -> T? { + switch type { + case is Bool.Type: + return (value.lowercased() == "true") as? T + case is Int64.Type: + return Int64(value) as? T + case is Double.Type: + return Double(value) as? T + case is String.Type: + return value as? T + case is OpenFeature.Value.Type: + return OpenFeature.Value.string(value) as? T + default: + return nil + } + } + + internal func evaluate(key: String, defaultValue: T, context: (any EvaluationContext)?) -> ProviderEvaluation { + let treatment = splitClient?.getTreatment(key) ?? Constants.CONTROL.rawValue + let value = parseValue(treatment, as: T.self) ?? defaultValue + return ProviderEvaluation(value: value) + } +} diff --git a/Sources/SplitProvider/SplitProvider.swift b/Sources/SplitProvider/SplitProvider.swift index 354d714..ec9e1a0 100644 --- a/Sources/SplitProvider/SplitProvider.swift +++ b/Sources/SplitProvider/SplitProvider.swift @@ -17,6 +17,7 @@ public class SplitProvider: FeatureProvider { public var metadata: any OpenFeature.ProviderMetadata = SplitProviderMetadata() private let eventHandler = EventHandler() private var splitContext: InitContext? + internal var evaluator: Evaluator! // MARK: Custom Initialization public init(_ config: SplitClientConfig? = nil) { @@ -45,12 +46,11 @@ public class SplitProvider: FeatureProvider { // 2. Client setup splitContext = InitContext(apiKey: API_KEY, userKey: USER_KEY) let key: Key = Key(matchingKey: USER_KEY) - if factory == nil { - factory = DefaultSplitFactoryBuilder().setApiKey(API_KEY).setKey(key).setConfig(splitClientConfig ?? SplitClientConfig()).build() - } + if factory == nil { factory = DefaultSplitFactoryBuilder().setApiKey(API_KEY).setKey(key).setConfig(splitClientConfig ?? SplitClientConfig()).build() } splitClient = factory?.client + if evaluator == nil { evaluator = Evaluator(splitClient: splitClient) } - // 3. Wait for SDK + // 3. Subscribe to events and wait for SDK await withCheckedContinuation { (continuation: CheckedContinuation) in var didResume = false @@ -62,7 +62,7 @@ public class SplitProvider: FeatureProvider { if error { eventHandler.send(.error(errorCode: .general, message: "Provider timed out")) } else { - continuation.resume() + continuation.resume() // Pass control to openFeature again } } @@ -80,32 +80,34 @@ public class SplitProvider: FeatureProvider { // MARK: Evaluation Methods extension SplitProvider { - - public func getBooleanEvaluation(key: String, defaultValue: Bool, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation { - ProviderEvaluation(value: false) + + public func getBooleanEvaluation(key: String, defaultValue: Bool, context: (any EvaluationContext)?) throws -> ProviderEvaluation { + evaluator.evaluate(key: key, defaultValue: defaultValue, context: context) ?? ProviderEvaluation(value: defaultValue) } - public func getStringEvaluation(key: String, defaultValue: String, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation { - ProviderEvaluation(value: splitClient?.getTreatment(key) ?? Constants.CONTROL.rawValue) + public func getStringEvaluation(key: String, defaultValue: String, context: (any EvaluationContext)?) throws -> ProviderEvaluation { + evaluator.evaluate(key: key, defaultValue: defaultValue, context: context) ?? ProviderEvaluation(value: defaultValue) } - public func getIntegerEvaluation(key: String, defaultValue: Int64, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation { - ProviderEvaluation(value: 1) + public func getIntegerEvaluation(key: String, defaultValue: Int64, context: (any EvaluationContext)?) throws -> ProviderEvaluation { + evaluator.evaluate(key: key, defaultValue: defaultValue, context: context) ?? ProviderEvaluation(value: defaultValue) } - public func getDoubleEvaluation(key: String, defaultValue: Double, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation { - ProviderEvaluation(value: 1.0) + public func getDoubleEvaluation(key: String, defaultValue: Double, context: (any EvaluationContext)?) throws -> ProviderEvaluation { + evaluator.evaluate(key: key, defaultValue: defaultValue, context: context) ?? ProviderEvaluation(value: defaultValue) } - public func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation { - throw SplitError.notImplemented + public func getObjectEvaluation(key: String, defaultValue: Value, context: (any EvaluationContext)?) throws -> ProviderEvaluation { + evaluator.evaluate(key: key, defaultValue: defaultValue, context: context) ?? ProviderEvaluation(value: defaultValue) } public func observe() -> AnyPublisher { - eventHandler.publisher.eraseToAnyPublisher() + eventHandler.publisher.eraseToAnyPublisher() } } +// MARK: Open Feature struct SplitProviderMetadata: ProviderMetadata { let name: String? = Constants.PROVIDER_NAME.rawValue } + diff --git a/Tests/SplitProviderTests/EvaluatorTests.swift b/Tests/SplitProviderTests/EvaluatorTests.swift new file mode 100644 index 0000000..ff0bac6 --- /dev/null +++ b/Tests/SplitProviderTests/EvaluatorTests.swift @@ -0,0 +1,62 @@ +// Created by Martin Cardozo on 24/10/2025. + +import XCTest +import OpenFeature +@testable import SplitProvider + +final class EvaluatorTests: XCTestCase { + + private func makeEvaluator(with treatment: String) -> Evaluator { + let client = ClientMock() + client.treatment = treatment + return Evaluator(splitClient: client) + } + + func testBoolTrue() { + let SUT = makeEvaluator(with: "true") + let result = SUT.evaluate(key: "flag", defaultValue: false, context: nil) + XCTAssertEqual(result.value, true) + } + + func testBoolFalse() { + let SUT = makeEvaluator(with: "FA LSE") + let result = SUT.evaluate(key: "flag", defaultValue: true, context: nil) + XCTAssertEqual(result.value, false) + } + + func testInt64() { + let SUT = makeEvaluator(with: "123") + let result = SUT.evaluate(key: "flag", defaultValue: Int64(0), context: nil) + XCTAssertEqual(result.value, 123) + } + + func testDouble() { + let SUT = makeEvaluator(with: "3.14") + let result = SUT.evaluate(key: "flag", defaultValue: 0.0, context: nil) + XCTAssertEqual(result.value, 3.14, accuracy: 0.0001) + } + + func testString() { + let SUT = makeEvaluator(with: "banana") + let result = SUT.evaluate(key: "flag", defaultValue: "default", context: nil) + XCTAssertEqual(result.value, "banana") + } + + func testValue() { + let SUT = makeEvaluator(with: "json_string") + let result = SUT.evaluate(key: "flag", defaultValue: OpenFeature.Value.string("default"), context: nil) + XCTAssertEqual(result.value, .string("json_string")) + } + + func testInvalidTypeFallsBackToDefault() { + let SUT = makeEvaluator(with: "notAnInt") + let result = SUT.evaluate(key: "flag", defaultValue: Int64(42), context: nil) + XCTAssertEqual(result.value, 42, "Should return default when conversion fails") + } + + func testWhenSplitClientIsNilReturnsControl() { + let SUT = Evaluator(splitClient: nil) + let result = SUT.evaluate(key: "flag", defaultValue: "default", context: nil) + XCTAssertEqual(result.value, Constants.CONTROL.rawValue) + } +} diff --git a/Tests/SplitProviderTests/SplitProviderTests.swift b/Tests/SplitProviderTests/SplitProviderTests.swift index 1008e4f..dac2145 100644 --- a/Tests/SplitProviderTests/SplitProviderTests.swift +++ b/Tests/SplitProviderTests/SplitProviderTests.swift @@ -19,11 +19,11 @@ final class SplitProviderTests: XCTestCase { override func tearDown() { providerCancellable?.cancel() } +} - func testNameIsCorrect() { - XCTAssertTrue(SplitProvider().metadata.name == Constants.PROVIDER_NAME.rawValue) - } - +// MARK: Setup Tests +extension SplitProviderTests { + func testCorrectInitialization() { let readyExp = expectation(description: "SDK Ready") @@ -36,7 +36,7 @@ final class SplitProviderTests: XCTestCase { switch event { case .ready: readyExp.fulfill() - case .error(let errorCode, _): + case .error: nonErrorExp.fulfill() default: break @@ -130,10 +130,10 @@ final class SplitProviderTests: XCTestCase { provider = SplitProvider() // Setup events observer - providerCancellable = OpenFeatureAPI.shared.observe().sink { [weak self] event in + providerCancellable = OpenFeatureAPI.shared.observe().sink { event in switch event { case .ready: - self?.eval("mauro-test-flag") + break case .error(let errorCode, _): if errorCode == .invalidContext { errorFired = true @@ -220,13 +220,90 @@ final class SplitProviderTests: XCTestCase { wait(for: [errorExp], timeout: 4) } + + func testNameIsCorrect() { + XCTAssertTrue(SplitProvider().metadata.name == Constants.PROVIDER_NAME.rawValue) + } +} - fileprivate func eval(_ flag: String) { - do { - let eval = try provider.getStringEvaluation(key: flag, defaultValue: "", context: nil) - print("Flag value:", eval.value) - } catch { - print("Provider error:", error) - } +// MARK: Evaluation Tests +extension SplitProviderTests { + + func testBooleanEvaluationTrue() throws { + let client = ClientMock() + let evaluator = Evaluator(splitClient: client) + client.treatment = "true" + + let provider = SplitProvider() + provider.splitClient = client + provider.evaluator = evaluator + + let result = try provider.getBooleanEvaluation(key: "flag", defaultValue: false, context: nil) + XCTAssertEqual(result.value, true) + } + + func testBooleanEvaluationWrong() throws { + let client = ClientMock() + let evaluator = Evaluator(splitClient: client) + client.treatment = "tru" + + let provider = SplitProvider() + provider.splitClient = client + provider.evaluator = evaluator + + let result = try provider.getBooleanEvaluation(key: "flag", defaultValue: false, context: nil) + XCTAssertEqual(result.value, false, "Treatment should be the default value") + } + + func testBooleanEvaluationFalse() throws { + let client = ClientMock() + let evaluator = Evaluator(splitClient: client) + client.treatment = "false" + + let provider = SplitProvider() + provider.splitClient = client + provider.evaluator = evaluator + + let result = try provider.getBooleanEvaluation(key: "flag", defaultValue: false, context: nil) + XCTAssertEqual(result.value, false) + } + + func testIntegerEvaluation() throws { + let client = ClientMock() + let evaluator = Evaluator(splitClient: client) + client.treatment = "123" + + let provider = SplitProvider() + provider.splitClient = client + provider.evaluator = evaluator + + let result = try provider.getIntegerEvaluation(key: "flag", defaultValue: 0, context: nil) + XCTAssertEqual(result.value, 123) + } + + func testDoubleEvaluation() throws { + let client = ClientMock() + let evaluator = Evaluator(splitClient: client) + client.treatment = "3.14" + + let provider = SplitProvider() + provider.splitClient = client + provider.evaluator = evaluator + + let result = try provider.getDoubleEvaluation(key: "flag", defaultValue: 0.0, context: nil) + XCTAssertEqual(result.value, 3.14, accuracy: 0.0001) + } + + func testStringEvaluation() throws { + let client = ClientMock() + let evaluator = Evaluator(splitClient: client) + client.treatment = "hello" + + let provider = SplitProvider() + provider.splitClient = client + provider.evaluator = evaluator + + let result = try provider.getStringEvaluation(key: "flag", defaultValue: "default", context: nil) + XCTAssertEqual(result.value, "hello") } }