Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/SplitProvider.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SplitProvider"
BuildableName = "SplitProvider"
BlueprintName = "SplitProvider"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SplitProviderTests"
BuildableName = "SplitProviderTests"
BlueprintName = "SplitProviderTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SplitProvider"
BuildableName = "SplitProvider"
BlueprintName = "SplitProvider"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
36 changes: 36 additions & 0 deletions Sources/SplitProvider/Evaluator.swift
Original file line number Diff line number Diff line change
@@ -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<T>(_ value: String, as type: T.Type) -> T? {
switch type {
case is Bool.Type:
return (value.lowercased() == "true") as? T
Comment thread
MartinCardozo-SDK marked this conversation as resolved.
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
Comment thread
MartinCardozo-SDK marked this conversation as resolved.
default:
return nil
}
}

internal func evaluate<T>(key: String, defaultValue: T, context: (any EvaluationContext)?) -> ProviderEvaluation<T> {
let treatment = splitClient?.getTreatment(key) ?? Constants.CONTROL.rawValue
let value = parseValue(treatment, as: T.self) ?? defaultValue
return ProviderEvaluation(value: value)
}
}
36 changes: 19 additions & 17 deletions Sources/SplitProvider/SplitProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<Void, Never>) in
var didResume = false

Expand All @@ -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
}
}

Expand All @@ -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<Bool> {
ProviderEvaluation(value: false)
public func getBooleanEvaluation(key: String, defaultValue: Bool, context: (any EvaluationContext)?) throws -> ProviderEvaluation<Bool> {
evaluator.evaluate(key: key, defaultValue: defaultValue, context: context) ?? ProviderEvaluation(value: defaultValue)
Comment thread
MartinCardozo-SDK marked this conversation as resolved.
}

public func getStringEvaluation(key: String, defaultValue: String, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation<String> {
ProviderEvaluation(value: splitClient?.getTreatment(key) ?? Constants.CONTROL.rawValue)
public func getStringEvaluation(key: String, defaultValue: String, context: (any EvaluationContext)?) throws -> ProviderEvaluation<String> {
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<Int64> {
ProviderEvaluation(value: 1)
public func getIntegerEvaluation(key: String, defaultValue: Int64, context: (any EvaluationContext)?) throws -> ProviderEvaluation<Int64> {
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<Double> {
ProviderEvaluation(value: 1.0)
public func getDoubleEvaluation(key: String, defaultValue: Double, context: (any EvaluationContext)?) throws -> ProviderEvaluation<Double> {
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<OpenFeature.Value> {
throw SplitError.notImplemented
public func getObjectEvaluation(key: String, defaultValue: Value, context: (any EvaluationContext)?) throws -> ProviderEvaluation<Value> {
evaluator.evaluate(key: key, defaultValue: defaultValue, context: context) ?? ProviderEvaluation(value: defaultValue)
}

public func observe() -> AnyPublisher<OpenFeature.ProviderEvent?, Never> {
eventHandler.publisher.eraseToAnyPublisher()
eventHandler.publisher.eraseToAnyPublisher()
}
}

// MARK: Open Feature
struct SplitProviderMetadata: ProviderMetadata {
let name: String? = Constants.PROVIDER_NAME.rawValue
}

62 changes: 62 additions & 0 deletions Tests/SplitProviderTests/EvaluatorTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
105 changes: 91 additions & 14 deletions Tests/SplitProviderTests/SplitProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -36,7 +36,7 @@ final class SplitProviderTests: XCTestCase {
switch event {
case .ready:
readyExp.fulfill()
case .error(let errorCode, _):
case .error:
nonErrorExp.fulfill()
default:
break
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
}