Skip to content

Commit 9e55395

Browse files
authored
(U2C #7) support contextKind in clauses (#108)
1 parent cb7e69a commit 9e55395

File tree

7 files changed

+196
-95
lines changed

7 files changed

+196
-95
lines changed

Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ TEST_HARNESS_PARAMS := $(TEST_HARNESS_PARAMS) \
2020
-skip 'evaluation/bucketing/selection of context' \
2121
-skip 'evaluation/parameterized/attribute references' \
2222
-skip 'evaluation/parameterized/bad attribute reference errors' \
23-
-skip 'evaluation/parameterized/clause kind matching' \
2423
-skip 'evaluation/parameterized/prerequisites' \
2524
-skip 'evaluation/parameterized/segment match/included list is specific to user kind' \
2625
-skip 'evaluation/parameterized/segment match/includedContexts' \

src/LaunchDarkly/Impl/Evaluation/EvaluatorHelpers.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,14 @@ public static function matchClauseWithoutSegments(Clause $clause, LDContext $con
7373
if ($attr === null) {
7474
return false;
7575
}
76-
$contextValue = $context->get($attr);
76+
if ($attr === 'kind') {
77+
return self::maybeNegate($clause, self::matchClauseByKind($clause, $context));
78+
}
79+
$actualContext = $context->getIndividualContext($clause->getContextKind() ?? LDContext::DEFAULT_KIND);
80+
if ($actualContext === null) {
81+
return false;
82+
}
83+
$contextValue = $actualContext->get($attr);
7784
if ($contextValue === null) {
7885
return false;
7986
}
@@ -89,6 +96,20 @@ public static function matchClauseWithoutSegments(Clause $clause, LDContext $con
8996
}
9097
}
9198

99+
private static function matchClauseByKind(Clause $clause, LDContext $context): bool
100+
{
101+
// If attribute is "kind", then we treat operator and values as a match expression against a list
102+
// of all individual kinds in the context. That is, for a multi-kind context with kinds of "org"
103+
// and "user", it is a match if either of those strings is a match with Operator and Values.
104+
for ($i = 0; $i < $context->getIndividualContextCount(); $i++) {
105+
$c = $context->getIndividualContext($i);
106+
if ($c !== null && self::matchAnyClauseValue($clause, $c->getKind())) {
107+
return true;
108+
}
109+
}
110+
return false;
111+
}
112+
92113
private static function matchAnyClauseValue(Clause $clause, mixed $contextValue): bool
93114
{
94115
$op = $clause->getOp();

src/LaunchDarkly/Impl/Model/Clause.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
*/
1515
class Clause
1616
{
17+
private ?string $_contextKind = null;
1718
private ?string $_attribute = null;
1819
private ?string $_op = null;
1920
private array $_values = [];
2021
private bool $_negate = false;
2122

22-
public function __construct(?string $attribute, ?string $op, array $values, bool $negate)
23+
public function __construct(?string $contextKind, ?string $attribute, ?string $op, array $values, bool $negate)
2324
{
25+
$this->_contextKind = $contextKind;
2426
$this->_attribute = $attribute;
2527
$this->_op = $op;
2628
$this->_values = $values;
@@ -32,14 +34,19 @@ public function __construct(?string $attribute, ?string $op, array $values, bool
3234
*/
3335
public static function getDecoder(): \Closure
3436
{
35-
return fn ($v) => new Clause($v['attribute'], $v['op'], $v['values'], $v['negate']);
37+
return fn ($v) => new Clause($v['contextKind'] ?? null, $v['attribute'], $v['op'], $v['values'], $v['negate']);
3638
}
3739

3840
public function getAttribute(): ?string
3941
{
4042
return $this->_attribute;
4143
}
4244

45+
public function getContextKind(): ?string
46+
{
47+
return $this->_contextKind;
48+
}
49+
4350
public function getOp(): ?string
4451
{
4552
return $this->_op;
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
namespace LaunchDarkly\Tests\Impl\Evaluation;
4+
5+
use LaunchDarkly\Impl\Evaluation\Evaluator;
6+
use LaunchDarkly\Impl\Model\Clause;
7+
use LaunchDarkly\Impl\Model\FeatureFlag;
8+
use LaunchDarkly\LDContext;
9+
use LaunchDarkly\Tests\MockFeatureRequester;
10+
use LaunchDarkly\Tests\ModelBuilders;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class EvaluatorClauseTest extends TestCase
14+
{
15+
private static Evaluator $basicEvaluator;
16+
17+
public static function setUpBeforeClass(): void
18+
{
19+
static::$basicEvaluator = EvaluatorTestUtil::basicEvaluator();
20+
}
21+
22+
private function assertMatch(Evaluator $eval, FeatureFlag $flag, LDContext $context, bool $expectMatch)
23+
{
24+
$result = $eval->evaluate($flag, $context, EvaluatorTestUtil::expectNoPrerequisiteEvals());
25+
self::assertEquals($expectMatch, $result->getDetail()->getValue());
26+
}
27+
28+
private function assertMatchClause(Evaluator $eval, Clause $clause, LDContext $context, bool $expectMatch)
29+
{
30+
self::assertMatch($eval, ModelBuilders::booleanFlagWithClauses($clause), $context, $expectMatch);
31+
}
32+
33+
public function testClauseCanMatchBuiltInAttribute()
34+
{
35+
$clause = ModelBuilders::clause(null, 'name', 'in', 'Bob');
36+
$context = LDContext::builder('key')->name('Bob')->build();
37+
38+
self::assertMatchClause(static::$basicEvaluator, $clause, $context, true);
39+
}
40+
41+
public function testClauseCanMatchCustomAttribute()
42+
{
43+
$clause = ModelBuilders::clause(null, 'legs', 'in', 4);
44+
$context = LDContext::builder('key')->set('legs', 4)->build();
45+
46+
self::assertMatchClause(static::$basicEvaluator, $clause, $context, true);
47+
}
48+
49+
public function testClauseReturnsFalseForMissingAttribute()
50+
{
51+
$clause = ModelBuilders::clause(null, 'legs', 'in', 4);
52+
$context = LDContext::create('key');
53+
54+
self::assertMatchClause(static::$basicEvaluator, $clause, $context, false);
55+
}
56+
57+
public function testClauseMatchesContextValueToAnyOfMultipleValues()
58+
{
59+
$clause = ModelBuilders::clause(null, 'name', 'in', 'Bob', 'Carol');
60+
$context = LDContext::builder('key')->name('Carol')->build();
61+
62+
self::assertMatchClause(static::$basicEvaluator, $clause, $context, true);
63+
}
64+
65+
public function testClauseMatchesArrayOfContextValuesToClauseValue()
66+
{
67+
$clause = ModelBuilders::clause(null, 'alias', 'in', 'Maurice');
68+
$context = LDContext::builder('key')->set('alias', ['Space Cowboy', 'Maurice'])->build();
69+
70+
self::assertMatchClause(static::$basicEvaluator, $clause, $context, true);
71+
}
72+
73+
public function testClauseFindsNoMatchInArrayOfContextValues()
74+
{
75+
$clause = ModelBuilders::clause(null, 'alias', 'in', 'Ma');
76+
$context = LDContext::builder('key')->set('alias', ['Mary', 'May'])->build();
77+
78+
self::assertMatchClause(static::$basicEvaluator, $clause, $context, false);
79+
}
80+
81+
public function testClauseCanBeNegatedToReturnFalse()
82+
{
83+
$clause = ModelBuilders::negate(ModelBuilders::clause(null, 'name', 'in', 'Bob'));
84+
$context = LDContext::builder('key')->name('Bob')->build();
85+
86+
self::assertMatchClause(static::$basicEvaluator, $clause, $context, false);
87+
}
88+
89+
public function testClauseCanBeNegatedToReturnTrue()
90+
{
91+
$clause = ModelBuilders::negate(ModelBuilders::clause(null, 'name', 'in', 'Rob'));
92+
$context = LDContext::builder('key')->name('Bob')->build();
93+
94+
self::assertMatchClause(static::$basicEvaluator, $clause, $context, true);
95+
}
96+
97+
public function testClauseWithUnknownOperatorDoesNotMatch()
98+
{
99+
$clause = ModelBuilders::clause(null, 'name', 'doesSomethingUnsupported', 'Bob');
100+
$context = LDContext::builder('key')->name('Bob')->build();
101+
102+
self::assertMatchClause(static::$basicEvaluator, $clause, $context, false);
103+
}
104+
105+
public function testClauseMatchUsesContextKind()
106+
{
107+
$clause = ModelBuilders::clause('company', 'name', 'in', 'Catco');
108+
$context1 = LDContext::builder('cc')->kind('company')->name('Catco')->build();
109+
$context2 = LDContext::builder('l')->name('Lucy')->build();
110+
$context3 = LDContext::createMulti($context1, $context2);
111+
112+
self::assertMatchClause(static::$basicEvaluator, $clause, $context1, true);
113+
self::assertMatchClause(static::$basicEvaluator, $clause, $context2, false);
114+
self::assertMatchClause(static::$basicEvaluator, $clause, $context3, true);
115+
}
116+
117+
public function testClauseMatchByKindAttribute()
118+
{
119+
$clause = ModelBuilders::clause(null, 'kind', 'startsWith', 'a');
120+
$context1 = LDContext::create('key');
121+
$context2 = LDContext::create('key', 'ab');
122+
$context3 = LDContext::createMulti(
123+
LDContext::create('key', 'cd'),
124+
LDContext::create('key', 'ab')
125+
);
126+
127+
self::assertMatchClause(static::$basicEvaluator, $clause, $context1, false);
128+
self::assertMatchClause(static::$basicEvaluator, $clause, $context2, true);
129+
self::assertMatchClause(static::$basicEvaluator, $clause, $context3, true);
130+
}
131+
132+
public function testSegmentMatchClauseRetrievesSegmentFromStore()
133+
{
134+
$context = LDContext::create('key');
135+
$segment = ModelBuilders::segmentBuilder('segkey')->included([$context->getKey()])->build();
136+
$requester = new MockFeatureRequester();
137+
$requester->addSegment($segment);
138+
$evaluator = new Evaluator($requester);
139+
140+
$clause = ModelBuilders::clauseMatchingSegment($segment);
141+
142+
self::assertMatchClause($evaluator, $clause, $context, true);
143+
}
144+
145+
public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound()
146+
{
147+
$context = LDContext::create('key');
148+
$requester = new MockFeatureRequester();
149+
$requester->expectQueryForUnknownSegment('segkey');
150+
$evaluator = new Evaluator($requester);
151+
152+
$clause = ModelBuilders::clause(null, '', 'segmentMatch', 'segkey');
153+
154+
self::assertMatchClause($evaluator, $clause, $context, false);
155+
}
156+
}

tests/Impl/Evaluation/EvaluatorFlagTest.php

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -335,86 +335,4 @@ public function testRolloutCalculationCanBucketBySpecificAttribute()
335335
$detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID));
336336
self::assertEquals($detail, $result->getDetail());
337337
}
338-
339-
public function testClauseCanMatchBuiltInAttribute()
340-
{
341-
$clause = ModelBuilders::clause('name', 'in', 'Bob');
342-
$flag = ModelBuilders::booleanFlagWithClauses($clause);
343-
$context = LDContext::builder('userkey')->name('Bob')->build();
344-
345-
$result = static::$basicEvaluator->evaluate($flag, $context, EvaluatorTestUtil::expectNoPrerequisiteEvals());
346-
self::assertEquals(true, $result->getDetail()->getValue());
347-
}
348-
349-
public function testClauseCanMatchCustomAttribute()
350-
{
351-
$clause = ModelBuilders::clause('legs', 'in', 4);
352-
$flag = ModelBuilders::booleanFlagWithClauses($clause);
353-
$context = LDContext::builder('userkey')->set('legs', 4)->build();
354-
355-
$result = static::$basicEvaluator->evaluate($flag, $context, EvaluatorTestUtil::expectNoPrerequisiteEvals());
356-
self::assertEquals(true, $result->getDetail()->getValue());
357-
}
358-
359-
public function testClauseReturnsFalseForMissingAttribute()
360-
{
361-
$clause = ModelBuilders::clause('legs', 'in', 4);
362-
$flag = ModelBuilders::booleanFlagWithClauses($clause);
363-
$context = LDContext::create('userkey');
364-
365-
$result = static::$basicEvaluator->evaluate($flag, $context, EvaluatorTestUtil::expectNoPrerequisiteEvals());
366-
self::assertEquals(false, $result->getDetail()->getValue());
367-
}
368-
369-
public function testClauseCanBeNegated()
370-
{
371-
$clause = ModelBuilders::negate(ModelBuilders::clause('name', 'in', 'Bob'));
372-
$flag = ModelBuilders::booleanFlagWithClauses($clause);
373-
$context = LDContext::builder('userkey')->name('Bob')->build();
374-
375-
$result = static::$basicEvaluator->evaluate($flag, $context, EvaluatorTestUtil::expectNoPrerequisiteEvals());
376-
self::assertEquals(false, $result->getDetail()->getValue());
377-
}
378-
379-
public function testClauseWithUnknownOperatorDoesNotMatch()
380-
{
381-
$clause = ModelBuilders::clause('name', 'doesSomethingUnsupported', 'Bob');
382-
$flag = ModelBuilders::booleanFlagWithClauses($clause);
383-
$context = LDContext::builder('userkey')->name('Bob')->build();
384-
385-
$result = static::$basicEvaluator->evaluate($flag, $context, EvaluatorTestUtil::expectNoPrerequisiteEvals());
386-
self::assertEquals(false, $result->getDetail()->getValue());
387-
}
388-
389-
public function testSegmentMatchClauseRetrievesSegmentFromStore()
390-
{
391-
global $defaultContext;
392-
$segment = ModelBuilders::segmentBuilder('segkey')->included([$defaultContext->getKey()])->build();
393-
394-
$requester = new MockFeatureRequester();
395-
$requester->addSegment($segment);
396-
$evaluator = new Evaluator($requester);
397-
398-
$feature = ModelBuilders::booleanFlagWithClauses(ModelBuilders::clauseMatchingSegment($segment));
399-
400-
$result = $evaluator->evaluate($feature, $defaultContext, EvaluatorTestUtil::expectNoPrerequisiteEvals());
401-
402-
self::assertTrue($result->getDetail()->getValue());
403-
}
404-
405-
public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound()
406-
{
407-
global $defaultContext;
408-
$requester = new MockFeatureRequester();
409-
$requester->expectQueryForUnknownSegment('segkey');
410-
$evaluator = new Evaluator($requester);
411-
412-
$feature = ModelBuilders::booleanFlagWithClauses(
413-
ModelBuilders::clause('', 'segmentMatch', 'segkey')
414-
);
415-
416-
$result = $evaluator->evaluate($feature, $defaultContext, EvaluatorTestUtil::expectNoPrerequisiteEvals());
417-
418-
self::assertFalse($result->getDetail()->getValue());
419-
}
420338
}

tests/Impl/Evaluation/EvaluatorSegmentTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ public function testMatchingRuleWithMultipleClauses()
110110
$segment = ModelBuilders::segmentBuilder('test')
111111
->rule(
112112
ModelBuilders::segmentRuleBuilder()
113-
->clause(ModelBuilders::clause('email', 'in', '[email protected]'))
114-
->clause(ModelBuilders::clause('name', 'in', 'bob'))
113+
->clause(ModelBuilders::clause(null, 'email', 'in', '[email protected]'))
114+
->clause(ModelBuilders::clause(null, 'name', 'in', 'bob'))
115115
->build()
116116
)
117117
->build();
@@ -124,8 +124,8 @@ public function testNonMatchingRuleWithMultipleClauses()
124124
$segment = ModelBuilders::segmentBuilder('test')
125125
->rule(
126126
ModelBuilders::segmentRuleBuilder()
127-
->clause(ModelBuilders::clause('email', 'in', '[email protected]'))
128-
->clause(ModelBuilders::clause('name', 'in', 'bill'))
127+
->clause(ModelBuilders::clause(null, 'email', 'in', '[email protected]'))
128+
->clause(ModelBuilders::clause(null, 'name', 'in', 'bill'))
129129
->build()
130130
)
131131
->build();

tests/ModelBuilders.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,19 @@ public static function booleanFlagWithClauses(Clause ...$clauses): FeatureFlag
4444
return self::booleanFlagWithRules(self::flagRuleBuilder()->variation(1)->clauses($clauses)->build());
4545
}
4646

47-
public static function clause(string $attribute, string $op, ...$values): Clause
47+
public static function clause(?string $contextKind, string $attribute, string $op, ...$values): Clause
4848
{
49-
return new Clause($attribute, $op, $values, false);
49+
return new Clause($contextKind, $attribute, $op, $values, false);
5050
}
5151

5252
public static function clauseMatchingContext($context): Clause
5353
{
54-
return new Clause('key', 'in', [$context->getKey()], false);
54+
return new Clause($context->getKind(), 'key', 'in', [$context->getKey()], false);
5555
}
5656

5757
public static function clauseMatchingSegment($segment): Clause
5858
{
59-
return new Clause('', 'segmentMatch', [$segment->getKey()], false);
59+
return new Clause(null, '', 'segmentMatch', [$segment->getKey()], false);
6060
}
6161

6262
public static function flagRuleMatchingContext(int $variation, LDContext $context): Rule
@@ -71,7 +71,7 @@ public static function flagRuleWithClauses(int $variation, Clause ...$clauses):
7171

7272
public static function negate(Clause $clause): Clause
7373
{
74-
return new Clause($clause->getAttribute(), $clause->getOp(), $clause->getValues(), true);
74+
return new Clause($clause->getContextKind(), $clause->getAttribute(), $clause->getOp(), $clause->getValues(), true);
7575
}
7676

7777
public static function rolloutWithVariations(WeightedVariation ...$variations)

0 commit comments

Comments
 (0)