Skip to content

[FME-2963] Prerequisites implementation #670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 60 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
1fab9ca
Merge branch 'SDKS-9373_baseline' into FME-2963-prerequisites-baseline
gthea May 21, 2025
c5daeae
Changing branch. Much stuff needs correction
MartinCardozo-SDK May 21, 2025
1ae502c
Tests added
MartinCardozo-SDK May 21, 2025
65e84e7
Clean up
MartinCardozo-SDK May 21, 2025
9d6f6ef
Merging branches
MartinCardozo-SDK May 21, 2025
45c0706
Merge branch 'FME-2963-prerequisites_baseline' into FME-2963-prerequi…
MartinCardozo-SDK May 21, 2025
d6035ef
Fix?
gthea May 22, 2025
ebb5827
SplitView fields
MartinCardozo-SDK May 22, 2025
9fc9320
Solving conflicts
MartinCardozo-SDK May 22, 2025
e56b759
Cleanup
MartinCardozo-SDK May 22, 2025
6973f4f
Tests added
MartinCardozo-SDK May 22, 2025
a85f547
Test added
MartinCardozo-SDK May 22, 2025
c2fcd97
Improved tests
MartinCardozo-SDK May 23, 2025
5b98670
Improved tests
MartinCardozo-SDK May 23, 2025
ef582d3
Split tests
MartinCardozo-SDK May 23, 2025
ebf2772
Cleanup
MartinCardozo-SDK May 23, 2025
9d7e0cb
Fixed test
MartinCardozo-SDK May 23, 2025
57a5bf8
New Matcher
MartinCardozo-SDK May 23, 2025
8be3882
Merge pull request #661 from splitio/FME-2963-prerequisites-DTO
MartinCardozo-SDK May 23, 2025
af86270
Spliting branch
MartinCardozo-SDK May 26, 2025
de62351
Unit tests added
MartinCardozo-SDK May 26, 2025
7b94819
Cleanup
MartinCardozo-SDK May 26, 2025
1912467
New PrerequisitesMatcher. New result label and new step in Evaluator
MartinCardozo-SDK May 26, 2025
0857b48
Improved
MartinCardozo-SDK May 27, 2025
f1e668e
Pull request comments fixed. Tests added
MartinCardozo-SDK May 27, 2025
40239d6
Fixed test
MartinCardozo-SDK May 27, 2025
ded5eae
Merge conflicts
MartinCardozo-SDK May 28, 2025
76830ec
New coding keys
MartinCardozo-SDK May 28, 2025
f039797
Coding keys necessary
MartinCardozo-SDK May 28, 2025
90a8be0
Tests
MartinCardozo-SDK May 28, 2025
5d9ec70
Merge branch 'FME-2963-prerequisites_matcher' into FME-2963-prerequis…
MartinCardozo-SDK May 28, 2025
efeb85b
Fixes on paramters properties
MartinCardozo-SDK May 28, 2025
00d0309
Merge branch 'FME-2963-prerequisites_matcher' into FME-2963-prerequis…
MartinCardozo-SDK May 28, 2025
5065a23
Prerequisites parameter name not needed now.
MartinCardozo-SDK May 28, 2025
34d9028
Improved Implementation
MartinCardozo-SDK May 28, 2025
9010471
Cleanup
MartinCardozo-SDK May 28, 2025
31c1fe3
Cleanup
MartinCardozo-SDK May 28, 2025
708ce6e
Solving branch problem
MartinCardozo-SDK May 28, 2025
9defa10
Solving problem of branches
MartinCardozo-SDK May 28, 2025
3e4e7b8
Same
MartinCardozo-SDK May 28, 2025
7cd1203
Same
MartinCardozo-SDK May 28, 2025
393e41c
Same
MartinCardozo-SDK May 28, 2025
d4d3c5d
Cleanup
MartinCardozo-SDK May 28, 2025
470216d
Cleanup
MartinCardozo-SDK May 28, 2025
2597ce9
Merge branch 'FME-2963-prerequisites_matcher' into FME-2963-prerequis…
MartinCardozo-SDK May 28, 2025
85e8919
Deleting Comment
MartinCardozo-SDK May 28, 2025
61a9031
Merge pull request #667 from splitio/FME-2963-prerequisites_matcher
MartinCardozo-SDK May 28, 2025
58b4da7
Sharing branch
MartinCardozo-SDK May 30, 2025
ac821b5
Return full TargetingRules in test mock
gthea May 30, 2025
b10ec71
Tests added
MartinCardozo-SDK May 30, 2025
3b0812b
Merge pull request #665 from splitio/FME-2963-prerequisites-evaluator
MartinCardozo-SDK May 30, 2025
5e74920
Deleting unnecessary file
MartinCardozo-SDK May 30, 2025
fd5c5e1
Adapting to GitHub actions
MartinCardozo-SDK May 30, 2025
e72416e
Merge branch 'FME-2963-prerequisites_baseline' into FME-2963-prerequi…
MartinCardozo-SDK May 30, 2025
425cf53
Solving conflict
MartinCardozo-SDK May 30, 2025
4b979ff
Wrong label corrected. Test adapted
MartinCardozo-SDK May 30, 2025
302abed
Adding Matcher to WatchOS target
MartinCardozo-SDK May 30, 2025
3159187
Test added
MartinCardozo-SDK May 30, 2025
71fa7b1
Marks
MartinCardozo-SDK May 30, 2025
9f5a69c
Merge pull request #669 from splitio/FME-2963-prerequisites-E2E
MartinCardozo-SDK May 30, 2025
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
50 changes: 34 additions & 16 deletions Split.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Split/Api/DefaultSplitManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import Foundation
splitView.trafficType = split.trafficTypeName
splitView.defaultTreatment = split.defaultTreatment
splitView.killed = split.killed
splitView.prerequisites = split.prerequisites ?? []
splitView.sets = Array(split.sets ?? [])
splitView.configs = split.configurations ?? [String: String]()
splitView.impressionsDisabled = split.impressionsDisabled ?? false
Expand Down
1 change: 1 addition & 0 deletions Split/Api/SplitView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class SplitView: NSObject, Codable {
return changeNumber as NSNumber?
}
@objc public var configs: [String: String]?
@objc public var prerequisites: [Prerequisite]?

@objc public var impressionsDisabled: Bool = false
}
11 changes: 11 additions & 0 deletions Split/Common/Extensions/SplitView+StringConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ extension SplitView {
} else {
output+="treatments = nil\n"
}
if let prerequisites = prerequisites {
output+="prerequisites = [\n"
prerequisites.forEach { prerequisite in
output+="""
\(prerequisite.flagName): {\(prerequisite.treatments.joined(separator: ","))}\n
"""
}
output+="]\n"
} else {
output+="prerequisites = nil\n"
}
if let sets = sets {
output+="sets = [\(sets.joined(separator: ","))]\n"
} else {
Expand Down
2 changes: 1 addition & 1 deletion Split/Common/Utils/SplitHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class SplitHelper {
split.trafficAllocation = 100
split.trafficAllocationSeed = 1
split.seed = 1
split.isParsed = true
split.isCompletelyParsed = true
return split
}

Expand Down
47 changes: 32 additions & 15 deletions Split/Engine/Evaluator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ protocol Evaluator {
}

class DefaultEvaluator: Evaluator {
// Internal for testing purposes
var splitter: SplitterProtocol = Splitter.shared

// For testing purposes
internal var splitter: SplitterProtocol = Splitter.shared
private var prerequisitesMatcherFactory: ([Prerequisite]) -> MatcherProtocol = { prerequisites in PrerequisitesMatcher(prerequisites) }

private let splitsStorage: SplitsStorage
private let mySegmentsStorage: MySegmentsStorage
private let myLargeSegmentsStorage: MySegmentsStorage?
Expand All @@ -72,17 +75,29 @@ class DefaultEvaluator: Evaluator {
func evalTreatment(matchingKey: String, bucketingKey: String?,
splitName: String, attributes: [String: Any]?) throws -> EvaluationResult {

guard let split = splitsStorage.get(name: splitName),
split.status != .archived else {
Logger.w("The feature flag definition for '\(splitName)' has not been found")
return EvaluationResult(treatment: SplitConstants.control, label: ImpressionsConstants.splitNotFound)
// 1. Guarantee Split exists & is active
guard let split = splitsStorage.get(name: splitName), split.status != .archived else {
Logger.w("The feature flag definition for '\(splitName)' has not been found")
return EvaluationResult(treatment: SplitConstants.control, label: ImpressionsConstants.splitNotFound)
}


// 2. Guarantee is not killed
let changeNumber = split.changeNumber ?? -1
let defaultTreatment = split.defaultTreatment ?? SplitConstants.control
if let killed = split.killed, killed {
let defaultTreatment = split.defaultTreatment ?? SplitConstants.control
guard let killed = split.killed, !killed else {
return EvaluationResult(treatment: defaultTreatment, label: ImpressionsConstants.killed, changeNumber: changeNumber,
configuration: split.configurations?[defaultTreatment], impressionsDisabled: split.isImpressionsDisabled())
}

// 3. Extract necessary info
let bucketKey = selectBucketKey(matchingKey: matchingKey, bucketingKey: bucketingKey)
let values = EvalValues(matchValue: matchingKey, matchingKey: matchingKey, bucketingKey: bucketKey, attributes: attributes)

// 4. Evaluate Prerequisites
let matcher = prerequisitesMatcherFactory(split.prerequisites ?? [])
if !matcher.evaluate(values: values, context: getContext()) {
return EvaluationResult(treatment: defaultTreatment,
label: ImpressionsConstants.killed,
label: ImpressionsConstants.prerequisitesNotMet,
changeNumber: changeNumber,
configuration: split.configurations?[defaultTreatment],
impressionsDisabled: split.isImpressionsDisabled())
Expand All @@ -95,8 +110,6 @@ class DefaultEvaluator: Evaluator {
splitAlgo = algo
}

let bucketKey = selectBucketKey(matchingKey: matchingKey, bucketingKey: bucketingKey)

guard let conditions: [Condition] = split.conditions,
let trafficAllocationSeed = split.trafficAllocationSeed,
let seed = split.seed else {
Expand Down Expand Up @@ -155,11 +168,15 @@ class DefaultEvaluator: Evaluator {
}

private func selectBucketKey(matchingKey: String, bucketingKey: String?) -> String {
if let key = bucketingKey, !key.isEmpty() {
return key
}
if let key = bucketingKey, !key.isEmpty { return key }
return matchingKey
}

#if DEBUG
internal func overridePrerequisitesMatcher(_ factory: @escaping ([Prerequisite]) -> MatcherProtocol) {
prerequisitesMatcherFactory = factory
}
#endif
}

private extension Split {
Expand Down
1 change: 1 addition & 0 deletions Split/Impressions/ImpressionsConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ struct ImpressionsConstants {
static let exception: String = "exception"
static let notReady: String = "not ready"
static let unsupportedMatcherType: String = "targeting rule type unsupported by sdk"
static let prerequisitesNotMet = "prerequisites not met"
}
38 changes: 38 additions & 0 deletions Split/Matchers/PrerequisitesMatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Created by Martin Cardozo on 22/05/2025.
// Copyright © 2025 Split. All rights reserved.

import Foundation

class PrerequisitesMatcher: BaseMatcher, MatcherProtocol {

private var prerequisites: [Prerequisite]?

init(_ prerequisites: [Prerequisite]? = nil) {
self.prerequisites = prerequisites
}

// This evaluation passes JUST if -all- prerequisite are met
func evaluate(values: EvalValues, context: EvalContext?) -> Bool {

guard let prerequisites = prerequisites, !prerequisites.isEmpty else { return true }

for prerequisite in prerequisites {
guard !prerequisite.treatments.isEmpty else { return true }

do {
guard let treatment = try context?.evaluator?.evalTreatment(matchingKey: values.matchingKey, bucketingKey: values.bucketingKey, splitName: prerequisite.flagName, attributes: nil).treatment else {
continue
}

if !prerequisite.treatments.contains(treatment) {
return false
}
} catch {
Logger.e("Error evaluating condition in PrerequisitesMatcher: \(error)")
return false
}
}

return true
}
}
39 changes: 31 additions & 8 deletions Split/Models/SplitModel/Split.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
//
// Split.swift
// Split
//
// Created by Brian Sztamfater on 28/9/17.
//
//

import Foundation

// The JSON is -partially- parsed at startup to improve SDK ready times (for example "conditions" are left out).
// After parsing just the strictly necesary stuff, it saves the complete JSON to finish parsing later.
// Once .sdkReady is fired, it concurrently finishes parsing the rest (on SplitStorage.get())

typealias Split = SplitDTO

class SplitDTO: NSObject, SplitBase, Codable {
Expand All @@ -25,10 +23,11 @@ class SplitDTO: NSObject, SplitBase, Codable {
var configurations: [String: String]?
var sets: Set<String>?
var impressionsDisabled: Bool?
var prerequisites: [Prerequisite]?

var json: String = ""

var isParsed = true
var isCompletelyParsed = true

init(name: String, trafficType: String, status: Status, sets: Set<String>?, json: String, killed: Bool = false, impressionsDisabled: Bool = false) {
self.name = name
Expand All @@ -37,7 +36,7 @@ class SplitDTO: NSObject, SplitBase, Codable {
self.sets = sets
self.json = json
self.killed = killed
self.isParsed = false
self.isCompletelyParsed = false
self.impressionsDisabled = impressionsDisabled
}

Expand All @@ -56,5 +55,29 @@ class SplitDTO: NSObject, SplitBase, Codable {
case configurations
case sets
case impressionsDisabled
case prerequisites
}
}

@objc public class Prerequisite: NSObject, Codable {
@objc public var flagName: String
@objc public var treatments: [String]

enum CodingKeys: String, CodingKey {
case flagName = "n"
case treatments = "ts"
}

required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.flagName = try container.decodeIfPresent(String.self, forKey: .flagName) ?? ""
self.treatments = try container.decodeIfPresent([String].self, forKey: .treatments) ?? []
}

#if DEBUG
init(flagName: String, treatments: [String]) {
self.flagName = flagName
self.treatments = treatments
}
#endif
}
4 changes: 3 additions & 1 deletion Split/Storage/Splits/SplitsStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class DefaultSplitsStorage: SplitsStorage {
guard let split = inMemorySplits.value(forKey: name.lowercased()) else {
return nil
}
if !split.isParsed {
if !split.isCompletelyParsed {
if let parsed = try? Json.decodeFrom(json: split.json, to: Split.self) {
if isUnsupportedMatcher(split: parsed) {
parsed.conditions = [SplitHelper.createDefaultCondition()]
Expand Down Expand Up @@ -221,3 +221,5 @@ class BackgroundSyncSplitsStorage: SyncSplitsStorage {
persistentStorage.clear()
}
}


Loading
Loading