Skip to content

Commit 7f16210

Browse files
authored
(U2C #8) support contextTargets (#109)
1 parent 9e55395 commit 7f16210

File tree

8 files changed

+216
-30
lines changed

8 files changed

+216
-30
lines changed

Makefile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ TEST_HARNESS_PARAMS := $(TEST_HARNESS_PARAMS) \
2626
-skip 'evaluation/parameterized/segment match/excluded list is specific to user kind' \
2727
-skip 'evaluation/parameterized/segment match/excludedContexts' \
2828
-skip 'evaluation/parameterized/segment recursion' \
29-
-skip 'evaluation/parameterized/target match/context targets' \
30-
-skip 'evaluation/parameterized/target match/multi-kind' \
3129
-skip 'events'
3230

3331
build-contract-tests:

src/LaunchDarkly/Impl/Evaluation/Evaluator.php

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,9 @@ private function evaluateInternal(
5959
}
6060

6161
// Check to see if targets match
62-
foreach ($flag->getTargets() as $target) {
63-
foreach ($target->getValues() as $value) {
64-
if ($value === $context->getKey()) {
65-
$detail = EvaluatorHelpers::evaluationDetailForVariation(
66-
$flag,
67-
$target->getVariation(),
68-
EvaluationReason::targetMatch()
69-
);
70-
return new EvalResult($detail, false);
71-
}
72-
}
62+
$targetResult = $this->checkTargets($flag, $context);
63+
if ($targetResult) {
64+
return $targetResult;
7365
}
7466

7567
// Now walk through the rules and see if any match
@@ -124,6 +116,51 @@ private function checkPrerequisites(
124116
return null;
125117
}
126118

119+
private function checkTargets(FeatureFlag $flag, LDContext $context): ?EvalResult
120+
{
121+
$userTargets = $flag->getTargets();
122+
$contextTargets = $flag->getContextTargets();
123+
if (count($contextTargets) === 0) {
124+
// old-style data has only targets for users
125+
if (count($userTargets) !== 0) {
126+
$userContext = $context->getIndividualContext(LDContext::DEFAULT_KIND);
127+
if ($userContext === null) {
128+
return null;
129+
}
130+
foreach ($userTargets as $t) {
131+
if (in_array($userContext->getKey(), $t->getValues())) {
132+
return EvaluatorHelpers::targetMatchResult($flag, $t);
133+
}
134+
}
135+
}
136+
return null;
137+
}
138+
139+
foreach ($contextTargets as $t) {
140+
if (($t->getContextKind() ?: LDContext::DEFAULT_KIND) === LDContext::DEFAULT_KIND) {
141+
$userContext = $context->getIndividualContext(LDContext::DEFAULT_KIND);
142+
if ($userContext === null) {
143+
continue;
144+
}
145+
$userKey = $userContext->getKey();
146+
foreach ($userTargets as $ut) {
147+
if ($ut->getVariation() === $t->getVariation()) {
148+
if (in_array($userKey, $ut->getValues())) {
149+
return EvaluatorHelpers::targetMatchResult($flag, $ut);
150+
}
151+
break;
152+
}
153+
}
154+
} else {
155+
if (EvaluatorHelpers::contextKeyIsInTargetList($context, $t->getContextKind(), $t->getValues())) {
156+
return EvaluatorHelpers::targetMatchResult($flag, $t);
157+
}
158+
}
159+
}
160+
161+
return null;
162+
}
163+
127164
private function ruleMatchesContext(Rule $rule, LDContext $context): bool
128165
{
129166
foreach ($rule->getClauses() as $clause) {

src/LaunchDarkly/Impl/Evaluation/EvaluatorHelpers.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use LaunchDarkly\EvaluationReason;
77
use LaunchDarkly\Impl\Model\Clause;
88
use LaunchDarkly\Impl\Model\FeatureFlag;
9+
use LaunchDarkly\Impl\Model\Target;
910
use LaunchDarkly\Impl\Model\VariationOrRollout;
1011
use LaunchDarkly\LDContext;
1112

@@ -19,6 +20,15 @@
1920
*/
2021
class EvaluatorHelpers
2122
{
23+
public static function contextKeyIsInTargetList(LDContext $context, ?string $contextKind, array $keys): bool
24+
{
25+
if (count($keys) === 0) {
26+
return false;
27+
}
28+
$matchContext = $context->getIndividualContext($contextKind ?: LDContext::DEFAULT_KIND);
29+
return $matchContext !== null && in_array($matchContext->getKey(), $keys);
30+
}
31+
2232
public static function evaluationDetailForVariation(
2333
FeatureFlag $flag,
2434
int $index,
@@ -126,4 +136,12 @@ public static function maybeNegate(Clause $clause, bool $b): bool
126136
{
127137
return $clause->isNegate() ? !$b : $b;
128138
}
139+
140+
public static function targetMatchResult(FeatureFlag $flag, Target $t): EvalResult
141+
{
142+
return new EvalResult(
143+
self::evaluationDetailForVariation($flag, $t->getVariation(), EvaluationReason::targetMatch()),
144+
false
145+
);
146+
}
129147
}

src/LaunchDarkly/Impl/Model/FeatureFlag.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class FeatureFlag
2222
protected string $_salt;
2323
/** @var Target[] */
2424
protected array $_targets = [];
25+
/** @var Target[] */
26+
protected array $_contextTargets = [];
2527
/** @var Rule[] */
2628
protected array $_rules = [];
2729
protected VariationOrRollout $_fallthrough;
@@ -44,6 +46,7 @@ public function __construct(
4446
array $prerequisites,
4547
string $salt,
4648
array $targets,
49+
array $contextTargets,
4750
array $rules,
4851
VariationOrRollout $fallthrough,
4952
?int $offVariation,
@@ -60,6 +63,7 @@ public function __construct(
6063
$this->_prerequisites = $prerequisites;
6164
$this->_salt = $salt;
6265
$this->_targets = $targets;
66+
$this->_contextTargets = $contextTargets;
6367
$this->_rules = $rules;
6468
$this->_fallthrough = $fallthrough;
6569
$this->_offVariation = $offVariation;
@@ -86,6 +90,7 @@ public static function getDecoder(): \Closure
8690
array_map(Prerequisite::getDecoder(), $v['prerequisites'] ?: []),
8791
$v['salt'],
8892
array_map(Target::getDecoder(), $v['targets'] ?: []),
93+
array_map(Target::getDecoder(), ($v['contextTargets'] ?? null) ?: []),
8994
array_map(Rule::getDecoder(), $v['rules'] ?: []),
9095
call_user_func(VariationOrRollout::getDecoder(), $v['fallthrough']),
9196
$v['offVariation'],
@@ -109,6 +114,12 @@ public function isClientSide(): bool
109114
return $this->_clientSide;
110115
}
111116

117+
/** @return Target[] */
118+
public function getContextTargets(): array
119+
{
120+
return $this->_contextTargets;
121+
}
122+
112123
public function getDebugEventsUntilDate(): ?int
113124
{
114125
return $this->_debugEventsUntilDate;
@@ -139,11 +150,13 @@ public function isOn(): bool
139150
return $this->_on;
140151
}
141152

153+
/** @return Prerequisite[] */
142154
public function getPrerequisites(): array
143155
{
144156
return $this->_prerequisites;
145157
}
146158

159+
/** @return Rule[] */
147160
public function getRules(): array
148161
{
149162
return $this->_rules;
@@ -154,6 +167,7 @@ public function getSalt(): string
154167
return $this->_salt;
155168
}
156169

170+
/** @return Target[] */
157171
public function getTargets(): array
158172
{
159173
return $this->_targets;

src/LaunchDarkly/Impl/Model/Target.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,26 @@
1414
*/
1515
class Target
1616
{
17+
private ?string $_contextKind;
1718
/** @var string[] */
18-
private array $_values = [];
19+
private array $_values;
1920
private int $_variation;
2021

21-
public function __construct(array $values, int $variation)
22+
public function __construct(?string $contextKind, array $values, int $variation)
2223
{
24+
$this->_contextKind = $contextKind;
2325
$this->_values = $values;
2426
$this->_variation = $variation;
2527
}
2628

2729
public static function getDecoder(): \Closure
2830
{
29-
return fn (array $v) => new Target($v['values'], $v['variation']);
31+
return fn (array $v) => new Target($v['contextKind'] ?? null, $v['values'], $v['variation']);
32+
}
33+
34+
public function getContextKind(): ?string
35+
{
36+
return $this->_contextKind;
3037
}
3138

3239
/**

tests/FlagBuilder.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class FlagBuilder
1919
private string $_salt = '';
2020
/** @var Target[] */
2121
private array $_targets = [];
22+
/** @var Target[] */
23+
private array $_contextTargets = [];
2224
/** @var Rule[] */
2325
private array $_rules = [];
2426
private VariationOrRollout $_fallthrough;
@@ -44,6 +46,7 @@ public function build(): FeatureFlag
4446
$this->_prerequisites,
4547
$this->_salt,
4648
$this->_targets,
49+
$this->_contextTargets,
4750
$this->_rules,
4851
$this->_fallthrough,
4952
$this->_offVariation,
@@ -56,6 +59,12 @@ public function build(): FeatureFlag
5659
);
5760
}
5861

62+
public function contextTarget(string $contextKind, int $variation, string ...$values): FlagBuilder
63+
{
64+
$this->_contextTargets[] = new Target($contextKind, $values, $variation);
65+
return $this;
66+
}
67+
5968
public function fallthroughRollout(Rollout $rollout): FlagBuilder
6069
{
6170
$this->_fallthrough = new VariationOrRollout(null, $rollout);
@@ -112,7 +121,7 @@ public function salt(string $salt): FlagBuilder
112121

113122
public function target(int $variation, string ...$values): FlagBuilder
114123
{
115-
$this->_targets[] = new Target($values, $variation);
124+
$this->_targets[] = new Target(null, $values, $variation);
116125
return $this;
117126
}
118127

tests/Impl/Evaluation/EvaluatorFlagTest.php

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -172,19 +172,6 @@ public function testFlagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAn
172172
self::assertEquals($flag0, $eval->getPrereqOfFlag());
173173
}
174174

175-
public function testFlagMatchesContextFromTargets()
176-
{
177-
$flag = ModelBuilders::flagBuilder('feature')->variations('fall', 'off', 'on')
178-
->on(true)->offVariation(1)->fallthroughVariation(0)
179-
->target(2, 'whoever', 'userkey')
180-
->build();
181-
$context = LDContext::create('userkey');
182-
183-
$result = static::$basicEvaluator->evaluate($flag, $context, EvaluatorTestUtil::expectNoPrerequisiteEvals());
184-
$detail = new EvaluationDetail('on', 2, EvaluationReason::targetMatch());
185-
self::assertEquals($detail, $result->getDetail());
186-
}
187-
188175
public function testFlagMatchesContextFromRules()
189176
{
190177
global $defaultContext;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
namespace LaunchDarkly\Tests\Impl\Evaluation;
4+
5+
use LaunchDarkly\EvaluationReason;
6+
use LaunchDarkly\Impl\Evaluation\Evaluator;
7+
use LaunchDarkly\Impl\Model\FeatureFlag;
8+
use LaunchDarkly\LDContext;
9+
use LaunchDarkly\Tests\FlagBuilder;
10+
use LaunchDarkly\Tests\ModelBuilders;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class EvaluatorTargetTest extends TestCase
14+
{
15+
const FALLTHROUGH_VAR = 0, MATCH_VAR_1 = 1, MATCH_VAR_2 = 2;
16+
const VARIATIONS = ['fallthrough', 'match1', 'match2'];
17+
18+
private static Evaluator $basicEvaluator;
19+
20+
public static function setUpBeforeClass(): void
21+
{
22+
static::$basicEvaluator = EvaluatorTestUtil::basicEvaluator();
23+
}
24+
25+
public function testUserTargetsOnly()
26+
{
27+
$f = self::baseFlagBuilder()
28+
->target(self::MATCH_VAR_1, 'c')
29+
->target(self::MATCH_VAR_2, 'b', 'a')
30+
->build();
31+
32+
self::expectMatch($f, LDContext::create('a'), self::MATCH_VAR_2);
33+
self::expectMatch($f, LDContext::create('b'), self::MATCH_VAR_2);
34+
self::expectMatch($f, LDContext::create('c'), self::MATCH_VAR_1);
35+
self::expectFallthrough($f, LDContext::create('z'));
36+
37+
// in a multi-kind context, these targets match only the key for the user kind
38+
self::expectMatch(
39+
$f,
40+
LDContext::createMulti(LDContext::create('b', 'dog'), LDContext::create('a')),
41+
self::MATCH_VAR_2
42+
);
43+
self::expectMatch(
44+
$f,
45+
LDContext::createMulti(LDContext::create('a', 'dog'), LDContext::create('c')),
46+
self::MATCH_VAR_1
47+
);
48+
self::expectFallthrough(
49+
$f,
50+
LDContext::createMulti(LDContext::create('b', 'dog'), LDContext::create('z'))
51+
);
52+
self::expectFallthrough(
53+
$f,
54+
LDContext::createMulti(LDContext::create('a', 'dog'), LDContext::create('b', 'cat'))
55+
);
56+
}
57+
58+
public function userTargetsAndContextTargets()
59+
{
60+
$f = self::baseFlagBuilder()
61+
->target(self::MATCH_VAR_1, 'c')
62+
->target(self::MATCH_VAR_2, 'b', 'a')
63+
->contextTarget('dog', self::MATCH_VAR_1, 'a', 'b')
64+
->contextTarget('dog', self::MATCH_VAR_2, 'c')
65+
->contextTarget(LDContext::DEFAULT_KIND, self::MATCH_VAR_1)
66+
->contextTarget(LDContext::DEFAULT_KIND, self::MATCH_VAR_2)
67+
->build();
68+
69+
self::expectMatch($f, LDContext::create('a'), self::MATCH_VAR_2);
70+
self::expectMatch($f, LDContext::create('b'), self::MATCH_VAR_2);
71+
self::expectMatch($f, LDContext::create('c'), self::MATCH_VAR_1);
72+
self::expectFallthrough($f, LDContext::create('z'));
73+
74+
self::expectMatch(
75+
$f,
76+
LDContext::createMulti(LDContext::create('b', 'dog'), LDContext::create('a')),
77+
self::MATCH_VAR_1 // the "dog" target takes precedence due to ordering
78+
);
79+
self::expectMatch(
80+
$f,
81+
LDContext::createMulti(LDContext::create('z', 'dog'), LDContext::create('a')),
82+
self::MATCH_VAR_2 // "dog" targets don't match, continue to "user" targets
83+
);
84+
self::expectFallthrough(
85+
$f,
86+
LDContext::createMulti(LDContext::create('x', 'dog'), LDContext::create('z')) // nothing matches
87+
);
88+
self::expectMatch(
89+
$f,
90+
LDContext::createMulti(LDContext::create('a', 'dog'), LDContext::create('b', 'cat')),
91+
self::MATCH_VAR_1
92+
);
93+
}
94+
95+
private static function baseFlagBuilder(): FlagBuilder
96+
{
97+
return ModelBuilders::flagBuilder('feature')->on(true)->variations(...self::VARIATIONS)
98+
->fallthroughVariation(self::FALLTHROUGH_VAR)->offVariation(self::FALLTHROUGH_VAR);
99+
}
100+
101+
private function expectMatch(FeatureFlag $f, LDContext $c, int $v)
102+
{
103+
$result = EvaluatorTestUtil::basicEvaluator()->evaluate($f, $c, EvaluatorTestUtil::expectNoPrerequisiteEvals());
104+
self::assertEquals($v, $result->getDetail()->getVariationIndex());
105+
self::assertEquals(self::VARIATIONS[$v], $result->getDetail()->getValue());
106+
self::assertEquals(EvaluationReason::targetMatch(), $result->getDetail()->getReason());
107+
}
108+
109+
private function expectFallthrough(FeatureFlag $f, LDContext $c)
110+
{
111+
$result = EvaluatorTestUtil::basicEvaluator()->evaluate($f, $c, EvaluatorTestUtil::expectNoPrerequisiteEvals());
112+
self::assertEquals(self::FALLTHROUGH_VAR, $result->getDetail()->getVariationIndex());
113+
self::assertEquals(self::VARIATIONS[self::FALLTHROUGH_VAR], $result->getDetail()->getValue());
114+
self::assertEquals(EvaluationReason::fallthrough(), $result->getDetail()->getReason());
115+
}
116+
}

0 commit comments

Comments
 (0)