Skip to content

Commit ea35379

Browse files
authored
move forced-decision-validation to DecisionService (#443)
1 parent e2e0deb commit ea35379

File tree

7 files changed

+115
-58
lines changed

7 files changed

+115
-58
lines changed

Sources/Data Model/ProjectConfig.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2019-2021, Optimizely, Inc. and contributors
2+
// Copyright 2019-2022, Optimizely, Inc. and contributors
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -393,4 +393,12 @@ extension ProjectConfig {
393393
return true
394394
}
395395

396+
func getFlagVariationByKey(flagKey: String, variationKey: String) -> Variation? {
397+
if let variations = flagVariationsMap[flagKey] {
398+
return variations.filter { $0.key == variationKey }.first
399+
}
400+
401+
return nil
402+
}
403+
396404
}

Sources/Implementation/DefaultDecisionService.swift

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2019-2021, Optimizely, Inc. and contributors
2+
// Copyright 2019-2022, Optimizely, Inc. and contributors
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -321,8 +321,9 @@ class DefaultDecisionService: OPTDecisionService {
321321

322322
// check forced-decision first
323323

324-
let forcedDecisionResponse = user.findValidatedForcedDecision(context: OptimizelyDecisionContext(flagKey: flagKey, ruleKey: rule.key),
325-
options: options)
324+
let forcedDecisionResponse = findValidatedForcedDecision(config: config,
325+
user: user,
326+
context: OptimizelyDecisionContext(flagKey: flagKey, ruleKey: rule.key))
326327
reasons.merge(forcedDecisionResponse.reasons)
327328

328329
if let variation = forcedDecisionResponse.result {
@@ -353,8 +354,9 @@ class DefaultDecisionService: OPTDecisionService {
353354
// check forced-decision first
354355

355356
let rule = rules[ruleIndex]
356-
let forcedDecisionResponse = user.findValidatedForcedDecision(context: OptimizelyDecisionContext(flagKey: flagKey, ruleKey: rule.key),
357-
options: options)
357+
let forcedDecisionResponse = findValidatedForcedDecision(config: config,
358+
user: user,
359+
context: OptimizelyDecisionContext(flagKey: flagKey, ruleKey: rule.key))
358360
reasons.merge(forcedDecisionResponse.reasons)
359361

360362
if let variation = forcedDecisionResponse.result {
@@ -425,6 +427,29 @@ class DefaultDecisionService: OPTDecisionService {
425427
return bucketingId
426428
}
427429

430+
func findValidatedForcedDecision(config: ProjectConfig,
431+
user: OptimizelyUserContext,
432+
context: OptimizelyDecisionContext) -> DecisionResponse<Variation> {
433+
let reasons = DecisionReasons()
434+
435+
if let variationKey = user.getForcedDecision(context: context)?.variationKey {
436+
let userId = user.userId
437+
438+
if let variation = config.getFlagVariationByKey(flagKey: context.flagKey, variationKey: variationKey) {
439+
let info = LogMessage.userHasForcedDecision(userId, context.flagKey, context.ruleKey, variationKey)
440+
logger.i(info)
441+
reasons.addInfo(info)
442+
return DecisionResponse(result: variation, reasons: reasons)
443+
} else {
444+
let info = LogMessage.userHasForcedDecisionButInvalid(userId, context.flagKey, context.ruleKey)
445+
logger.i(info)
446+
reasons.addInfo(info)
447+
}
448+
}
449+
450+
return DecisionResponse(result: nil, reasons: reasons)
451+
}
452+
428453
}
429454

430455
// MARK: - UserProfileService Helpers

Sources/Optimizely+Decide/OptimizelyClient+Decide.swift

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2021, Optimizely, Inc. and contributors
2+
// Copyright 2021-2022, Optimizely, Inc. and contributors
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -60,7 +60,9 @@ extension OptimizelyClient {
6060

6161
// check forced-decisions first
6262

63-
let forcedDecisionResponse = user.findValidatedForcedDecision(context: OptimizelyDecisionContext(flagKey: key), options: allOptions)
63+
let forcedDecisionResponse = decisionService.findValidatedForcedDecision(config: config,
64+
user: user,
65+
context: OptimizelyDecisionContext(flagKey: key))
6466
reasons.merge(forcedDecisionResponse.reasons)
6567

6668
if let variation = forcedDecisionResponse.result {
@@ -176,14 +178,6 @@ extension OptimizelyClient {
176178

177179
extension OptimizelyClient {
178180

179-
func getFlagVariationByKey(flagKey: String, variationKey: String) -> Variation? {
180-
if let variations = config?.flagVariationsMap[flagKey] {
181-
return variations.filter { $0.key == variationKey }.first
182-
}
183-
184-
return nil
185-
}
186-
187181
func getDecisionVariableMap(feature: FeatureFlag,
188182
variation: Variation?,
189183
enabled: Bool) -> DecisionResponse<[String: Any]> {

Sources/Optimizely+Decide/OptimizelyUserContext.swift

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2021, Optimizely, Inc. and contributors
2+
// Copyright 2021-2022, Optimizely, Inc. and contributors
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -184,9 +184,9 @@ extension OptimizelyUserContext {
184184
/// - context: A decision context
185185
/// - Returns: A forced decision or nil if forced decisions are not set for the decision context.
186186
public func getForcedDecision(context: OptimizelyDecisionContext) -> OptimizelyForcedDecision? {
187-
guard forcedDecisions != nil else { return nil }
187+
guard let fds = forcedDecisions else { return nil }
188188

189-
return findForcedDecision(context: context)
189+
return fds[context]
190190
}
191191

192192
/// Removes the forced decision for a given decision context.
@@ -196,7 +196,7 @@ extension OptimizelyUserContext {
196196
public func removeForcedDecision(context: OptimizelyDecisionContext) -> Bool {
197197
guard let fds = forcedDecisions else { return false }
198198

199-
if findForcedDecision(context: context) != nil {
199+
if getForcedDecision(context: context) != nil {
200200
fds[context] = nil
201201
return true
202202
}
@@ -214,32 +214,6 @@ extension OptimizelyUserContext {
214214
return true
215215
}
216216

217-
func findForcedDecision(context: OptimizelyDecisionContext) -> OptimizelyForcedDecision? {
218-
guard let fds = forcedDecisions else { return nil }
219-
220-
return fds[context]
221-
}
222-
223-
func findValidatedForcedDecision(context: OptimizelyDecisionContext,
224-
options: [OptimizelyDecideOption]? = nil) -> DecisionResponse<Variation> {
225-
let reasons = DecisionReasons(options: options)
226-
227-
if let variationKey = findForcedDecision(context: context)?.variationKey {
228-
if let variation = optimizely?.getFlagVariationByKey(flagKey: context.flagKey, variationKey: variationKey) {
229-
let info = LogMessage.userHasForcedDecision(userId, context.flagKey, context.ruleKey, variationKey)
230-
logger.d(info)
231-
reasons.addInfo(info)
232-
return DecisionResponse(result: variation, reasons: reasons)
233-
} else {
234-
let info = LogMessage.userHasForcedDecisionButInvalid(userId, context.flagKey, context.ruleKey)
235-
logger.d(info)
236-
reasons.addInfo(info)
237-
}
238-
}
239-
240-
return DecisionResponse(result: nil, reasons: reasons)
241-
}
242-
243217
}
244218

245219
// MARK: - Equatable

Sources/Protocols/OPTDecisionService.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2019-2021, Optimizely, Inc. and contributors
2+
// Copyright 2019-2022, Optimizely, Inc. and contributors
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -33,10 +33,8 @@ protocol OPTDecisionService {
3333

3434
- Parameter config: The project configuration.
3535
- Parameter experiment: The experiment in which to bucket the user.
36-
- Parameter userId: The ID of the user.
37-
- Parameter attributes: User attributes
36+
- Parameter user: The user context.
3837
- Parameter options: An array of decision options
39-
- Parameter reasons: A struct to collect decision reasons
4038
- Returns: The variation assigned to the specified user ID for an experiment.
4139
*/
4240
func getVariation(config: ProjectConfig,
@@ -48,15 +46,24 @@ protocol OPTDecisionService {
4846
Get a variation the user is bucketed into for the given FeatureFlag
4947
- Parameter config: The project configuration.
5048
- Parameter featureFlag: The feature flag the user wants to access.
51-
- Parameter userId: The ID of the user.
52-
- Parameter attributes: User attributes
49+
- Parameter user: The user context.
5350
- Parameter options: An array of decision options
54-
- Parameter reasons: A struct to collect decision reasons
5551
- Returns: The variation assigned to the specified user ID for a feature flag.
5652
*/
5753
func getVariationForFeature(config: ProjectConfig,
5854
featureFlag: FeatureFlag,
5955
user: OptimizelyUserContext,
6056
options: [OptimizelyDecideOption]?) -> DecisionResponse<FeatureDecision>
6157

58+
/**
59+
Get a variation the user is bucketed into for the given FeatureFlag
60+
- Parameter config: The project configuration.
61+
- Parameter user: The user context.
62+
- Parameter context: The decision context.
63+
- Returns: The validated variation wrapped in DecisionResponse with reasons.
64+
*/
65+
func findValidatedForcedDecision(config: ProjectConfig,
66+
user: OptimizelyUserContext,
67+
context: OptimizelyDecisionContext) -> DecisionResponse<Variation>
68+
6269
}

Tests/OptimizelyTests-Common/DecisionServiceTests_Others.swift

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2019-2021, Optimizely, Inc. and contributors
2+
// Copyright 2019-2022, Optimizely, Inc. and contributors
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -41,6 +41,56 @@ class DecisionServiceTests_Others: XCTestCase {
4141
XCTAssert(isValid)
4242
}
4343

44-
// TODO: transfer valid ObjC SDK tests
45-
44+
func testFindValidatedForcedDecision() {
45+
let optimizely = OTUtils.createOptimizely(datafileName: ktypeAudienceDatafileName, clearUserProfileService: true)!
46+
let config = optimizely.config!
47+
48+
let user = optimizely.createUserContext(userId: kUserId)
49+
50+
let flagKey = "feat_with_var"
51+
let ruleKey = "feat_with_var_test"
52+
let variationKeys = [
53+
"variation_2",
54+
"11475708558"
55+
]
56+
57+
var fdContext: OptimizelyDecisionContext
58+
var fdForFlag: String
59+
var fd: DecisionResponse<Variation>
60+
61+
// F-to-D
62+
63+
fdContext = OptimizelyDecisionContext(flagKey: flagKey)
64+
fdForFlag = variationKeys[0]
65+
_ = user.setForcedDecision(context: fdContext, decision: OptimizelyForcedDecision(variationKey: fdForFlag))
66+
fd = optimizely.decisionService.findValidatedForcedDecision(config: config, user: user, context: fdContext)
67+
68+
XCTAssertEqual(fdForFlag, fd.result!.key)
69+
XCTAssertEqual("Variation (\(fdForFlag)) is mapped to flag (\(flagKey)) and user (\(kUserId)) in the forced decision map.", fd.reasons.infos![0].reason)
70+
71+
fdForFlag = "invalid"
72+
_ = user.setForcedDecision(context: fdContext, decision: OptimizelyForcedDecision(variationKey: fdForFlag))
73+
fd = optimizely.decisionService.findValidatedForcedDecision(config: config, user: user, context: fdContext)
74+
75+
XCTAssertNil(fd.result)
76+
XCTAssertEqual("Invalid variation is mapped to flag (\(flagKey)) and user (\(kUserId)) in the forced decision map.", fd.reasons.infos![0].reason)
77+
78+
// E-to-D
79+
80+
fdContext = OptimizelyDecisionContext(flagKey: flagKey, ruleKey: ruleKey)
81+
fdForFlag = variationKeys[1]
82+
_ = user.setForcedDecision(context: fdContext, decision: OptimizelyForcedDecision(variationKey: fdForFlag))
83+
fd = optimizely.decisionService.findValidatedForcedDecision(config: config, user: user, context: fdContext)
84+
85+
XCTAssertEqual(fdForFlag, fd.result!.key)
86+
XCTAssertEqual("Variation (\(fdForFlag)) is mapped to flag (\(flagKey)), rule (\(ruleKey)) and user (\(kUserId)) in the forced decision map.", fd.reasons.infos![0].reason)
87+
88+
fdForFlag = "invalid"
89+
_ = user.setForcedDecision(context: fdContext, decision: OptimizelyForcedDecision(variationKey: fdForFlag))
90+
fd = optimizely.decisionService.findValidatedForcedDecision(config: config, user: user, context: fdContext)
91+
92+
XCTAssertNil(fd.result)
93+
XCTAssertEqual("Invalid variation is mapped to flag (\(flagKey)), rule (\(ruleKey)) and user (\(kUserId)) in the forced decision map.", fd.reasons.infos![0].reason)
94+
95+
}
4696
}

Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ForcedDecisions.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,8 +432,7 @@ class OptimizelyUserContextTests_ForcedDecisions: XCTestCase {
432432
XCTAssertNil(user.getForcedDecision(context: OptimizelyDecisionContext(flagKey: "a")))
433433
XCTAssertFalse(user.removeForcedDecision(context: OptimizelyDecisionContext(flagKey: "a")))
434434
XCTAssertTrue(user.removeAllForcedDecisions()) // removeAll always returns true
435-
XCTAssertNil(user.findForcedDecision(context: OptimizelyDecisionContext(flagKey: "a")))
436-
XCTAssertNil(user.findValidatedForcedDecision(context: OptimizelyDecisionContext(flagKey: "a",ruleKey: "b")).result)
435+
XCTAssertNil(user.getForcedDecision(context: OptimizelyDecisionContext(flagKey: "a")))
437436
}
438437

439438
func testClone() {

0 commit comments

Comments
 (0)