Skip to content

Commit

Permalink
move forced-decision-validation to DecisionService (#443)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaeopt authored Jan 4, 2022
1 parent e2e0deb commit ea35379
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 58 deletions.
10 changes: 9 additions & 1 deletion Sources/Data Model/ProjectConfig.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2019-2021, Optimizely, Inc. and contributors
// Copyright 2019-2022, Optimizely, Inc. and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -393,4 +393,12 @@ extension ProjectConfig {
return true
}

func getFlagVariationByKey(flagKey: String, variationKey: String) -> Variation? {
if let variations = flagVariationsMap[flagKey] {
return variations.filter { $0.key == variationKey }.first
}

return nil
}

}
35 changes: 30 additions & 5 deletions Sources/Implementation/DefaultDecisionService.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2019-2021, Optimizely, Inc. and contributors
// Copyright 2019-2022, Optimizely, Inc. and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -321,8 +321,9 @@ class DefaultDecisionService: OPTDecisionService {

// check forced-decision first

let forcedDecisionResponse = user.findValidatedForcedDecision(context: OptimizelyDecisionContext(flagKey: flagKey, ruleKey: rule.key),
options: options)
let forcedDecisionResponse = findValidatedForcedDecision(config: config,
user: user,
context: OptimizelyDecisionContext(flagKey: flagKey, ruleKey: rule.key))
reasons.merge(forcedDecisionResponse.reasons)

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

let rule = rules[ruleIndex]
let forcedDecisionResponse = user.findValidatedForcedDecision(context: OptimizelyDecisionContext(flagKey: flagKey, ruleKey: rule.key),
options: options)
let forcedDecisionResponse = findValidatedForcedDecision(config: config,
user: user,
context: OptimizelyDecisionContext(flagKey: flagKey, ruleKey: rule.key))
reasons.merge(forcedDecisionResponse.reasons)

if let variation = forcedDecisionResponse.result {
Expand Down Expand Up @@ -425,6 +427,29 @@ class DefaultDecisionService: OPTDecisionService {
return bucketingId
}

func findValidatedForcedDecision(config: ProjectConfig,
user: OptimizelyUserContext,
context: OptimizelyDecisionContext) -> DecisionResponse<Variation> {
let reasons = DecisionReasons()

if let variationKey = user.getForcedDecision(context: context)?.variationKey {
let userId = user.userId

if let variation = config.getFlagVariationByKey(flagKey: context.flagKey, variationKey: variationKey) {
let info = LogMessage.userHasForcedDecision(userId, context.flagKey, context.ruleKey, variationKey)
logger.i(info)
reasons.addInfo(info)
return DecisionResponse(result: variation, reasons: reasons)
} else {
let info = LogMessage.userHasForcedDecisionButInvalid(userId, context.flagKey, context.ruleKey)
logger.i(info)
reasons.addInfo(info)
}
}

return DecisionResponse(result: nil, reasons: reasons)
}

}

// MARK: - UserProfileService Helpers
Expand Down
14 changes: 4 additions & 10 deletions Sources/Optimizely+Decide/OptimizelyClient+Decide.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2021, Optimizely, Inc. and contributors
// Copyright 2021-2022, Optimizely, Inc. and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -60,7 +60,9 @@ extension OptimizelyClient {

// check forced-decisions first

let forcedDecisionResponse = user.findValidatedForcedDecision(context: OptimizelyDecisionContext(flagKey: key), options: allOptions)
let forcedDecisionResponse = decisionService.findValidatedForcedDecision(config: config,
user: user,
context: OptimizelyDecisionContext(flagKey: key))
reasons.merge(forcedDecisionResponse.reasons)

if let variation = forcedDecisionResponse.result {
Expand Down Expand Up @@ -176,14 +178,6 @@ extension OptimizelyClient {

extension OptimizelyClient {

func getFlagVariationByKey(flagKey: String, variationKey: String) -> Variation? {
if let variations = config?.flagVariationsMap[flagKey] {
return variations.filter { $0.key == variationKey }.first
}

return nil
}

func getDecisionVariableMap(feature: FeatureFlag,
variation: Variation?,
enabled: Bool) -> DecisionResponse<[String: Any]> {
Expand Down
34 changes: 4 additions & 30 deletions Sources/Optimizely+Decide/OptimizelyUserContext.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2021, Optimizely, Inc. and contributors
// Copyright 2021-2022, Optimizely, Inc. and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -184,9 +184,9 @@ extension OptimizelyUserContext {
/// - context: A decision context
/// - Returns: A forced decision or nil if forced decisions are not set for the decision context.
public func getForcedDecision(context: OptimizelyDecisionContext) -> OptimizelyForcedDecision? {
guard forcedDecisions != nil else { return nil }
guard let fds = forcedDecisions else { return nil }

return findForcedDecision(context: context)
return fds[context]
}

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

if findForcedDecision(context: context) != nil {
if getForcedDecision(context: context) != nil {
fds[context] = nil
return true
}
Expand All @@ -214,32 +214,6 @@ extension OptimizelyUserContext {
return true
}

func findForcedDecision(context: OptimizelyDecisionContext) -> OptimizelyForcedDecision? {
guard let fds = forcedDecisions else { return nil }

return fds[context]
}

func findValidatedForcedDecision(context: OptimizelyDecisionContext,
options: [OptimizelyDecideOption]? = nil) -> DecisionResponse<Variation> {
let reasons = DecisionReasons(options: options)

if let variationKey = findForcedDecision(context: context)?.variationKey {
if let variation = optimizely?.getFlagVariationByKey(flagKey: context.flagKey, variationKey: variationKey) {
let info = LogMessage.userHasForcedDecision(userId, context.flagKey, context.ruleKey, variationKey)
logger.d(info)
reasons.addInfo(info)
return DecisionResponse(result: variation, reasons: reasons)
} else {
let info = LogMessage.userHasForcedDecisionButInvalid(userId, context.flagKey, context.ruleKey)
logger.d(info)
reasons.addInfo(info)
}
}

return DecisionResponse(result: nil, reasons: reasons)
}

}

// MARK: - Equatable
Expand Down
21 changes: 14 additions & 7 deletions Sources/Protocols/OPTDecisionService.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2019-2021, Optimizely, Inc. and contributors
// Copyright 2019-2022, Optimizely, Inc. and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -33,10 +33,8 @@ protocol OPTDecisionService {
- Parameter config: The project configuration.
- Parameter experiment: The experiment in which to bucket the user.
- Parameter userId: The ID of the user.
- Parameter attributes: User attributes
- Parameter user: The user context.
- Parameter options: An array of decision options
- Parameter reasons: A struct to collect decision reasons
- Returns: The variation assigned to the specified user ID for an experiment.
*/
func getVariation(config: ProjectConfig,
Expand All @@ -48,15 +46,24 @@ protocol OPTDecisionService {
Get a variation the user is bucketed into for the given FeatureFlag
- Parameter config: The project configuration.
- Parameter featureFlag: The feature flag the user wants to access.
- Parameter userId: The ID of the user.
- Parameter attributes: User attributes
- Parameter user: The user context.
- Parameter options: An array of decision options
- Parameter reasons: A struct to collect decision reasons
- Returns: The variation assigned to the specified user ID for a feature flag.
*/
func getVariationForFeature(config: ProjectConfig,
featureFlag: FeatureFlag,
user: OptimizelyUserContext,
options: [OptimizelyDecideOption]?) -> DecisionResponse<FeatureDecision>

/**
Get a variation the user is bucketed into for the given FeatureFlag
- Parameter config: The project configuration.
- Parameter user: The user context.
- Parameter context: The decision context.
- Returns: The validated variation wrapped in DecisionResponse with reasons.
*/
func findValidatedForcedDecision(config: ProjectConfig,
user: OptimizelyUserContext,
context: OptimizelyDecisionContext) -> DecisionResponse<Variation>

}
56 changes: 53 additions & 3 deletions Tests/OptimizelyTests-Common/DecisionServiceTests_Others.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2019-2021, Optimizely, Inc. and contributors
// Copyright 2019-2022, Optimizely, Inc. and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -41,6 +41,56 @@ class DecisionServiceTests_Others: XCTestCase {
XCTAssert(isValid)
}

// TODO: transfer valid ObjC SDK tests

func testFindValidatedForcedDecision() {
let optimizely = OTUtils.createOptimizely(datafileName: ktypeAudienceDatafileName, clearUserProfileService: true)!
let config = optimizely.config!

let user = optimizely.createUserContext(userId: kUserId)

let flagKey = "feat_with_var"
let ruleKey = "feat_with_var_test"
let variationKeys = [
"variation_2",
"11475708558"
]

var fdContext: OptimizelyDecisionContext
var fdForFlag: String
var fd: DecisionResponse<Variation>

// F-to-D

fdContext = OptimizelyDecisionContext(flagKey: flagKey)
fdForFlag = variationKeys[0]
_ = user.setForcedDecision(context: fdContext, decision: OptimizelyForcedDecision(variationKey: fdForFlag))
fd = optimizely.decisionService.findValidatedForcedDecision(config: config, user: user, context: fdContext)

XCTAssertEqual(fdForFlag, fd.result!.key)
XCTAssertEqual("Variation (\(fdForFlag)) is mapped to flag (\(flagKey)) and user (\(kUserId)) in the forced decision map.", fd.reasons.infos![0].reason)

fdForFlag = "invalid"
_ = user.setForcedDecision(context: fdContext, decision: OptimizelyForcedDecision(variationKey: fdForFlag))
fd = optimizely.decisionService.findValidatedForcedDecision(config: config, user: user, context: fdContext)

XCTAssertNil(fd.result)
XCTAssertEqual("Invalid variation is mapped to flag (\(flagKey)) and user (\(kUserId)) in the forced decision map.", fd.reasons.infos![0].reason)

// E-to-D

fdContext = OptimizelyDecisionContext(flagKey: flagKey, ruleKey: ruleKey)
fdForFlag = variationKeys[1]
_ = user.setForcedDecision(context: fdContext, decision: OptimizelyForcedDecision(variationKey: fdForFlag))
fd = optimizely.decisionService.findValidatedForcedDecision(config: config, user: user, context: fdContext)

XCTAssertEqual(fdForFlag, fd.result!.key)
XCTAssertEqual("Variation (\(fdForFlag)) is mapped to flag (\(flagKey)), rule (\(ruleKey)) and user (\(kUserId)) in the forced decision map.", fd.reasons.infos![0].reason)

fdForFlag = "invalid"
_ = user.setForcedDecision(context: fdContext, decision: OptimizelyForcedDecision(variationKey: fdForFlag))
fd = optimizely.decisionService.findValidatedForcedDecision(config: config, user: user, context: fdContext)

XCTAssertNil(fd.result)
XCTAssertEqual("Invalid variation is mapped to flag (\(flagKey)), rule (\(ruleKey)) and user (\(kUserId)) in the forced decision map.", fd.reasons.infos![0].reason)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -432,8 +432,7 @@ class OptimizelyUserContextTests_ForcedDecisions: XCTestCase {
XCTAssertNil(user.getForcedDecision(context: OptimizelyDecisionContext(flagKey: "a")))
XCTAssertFalse(user.removeForcedDecision(context: OptimizelyDecisionContext(flagKey: "a")))
XCTAssertTrue(user.removeAllForcedDecisions()) // removeAll always returns true
XCTAssertNil(user.findForcedDecision(context: OptimizelyDecisionContext(flagKey: "a")))
XCTAssertNil(user.findValidatedForcedDecision(context: OptimizelyDecisionContext(flagKey: "a",ruleKey: "b")).result)
XCTAssertNil(user.getForcedDecision(context: OptimizelyDecisionContext(flagKey: "a")))
}

func testClone() {
Expand Down

0 comments on commit ea35379

Please sign in to comment.