Skip to content

Commit c08b966

Browse files
committed
PHPLIB-1569: Implement $$matchAsDocument and $$matchAsRoot
1 parent 0dcd511 commit c08b966

File tree

3 files changed

+70
-1
lines changed

3 files changed

+70
-1
lines changed

tests/UnifiedSpecTests/Constraint/Matches.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace MongoDB\Tests\UnifiedSpecTests\Constraint;
44

55
use LogicException;
6+
use MongoDB\BSON\Document;
67
use MongoDB\BSON\Serializable;
78
use MongoDB\BSON\Type;
89
use MongoDB\Model\BSONArray;
@@ -25,10 +26,13 @@
2526
use function is_int;
2627
use function is_object;
2728
use function ltrim;
29+
use function PHPUnit\Framework\assertInstanceOf;
2830
use function PHPUnit\Framework\assertIsBool;
2931
use function PHPUnit\Framework\assertIsString;
32+
use function PHPUnit\Framework\assertJson;
3033
use function PHPUnit\Framework\assertMatchesRegularExpression;
3134
use function PHPUnit\Framework\assertNotNull;
35+
use function PHPUnit\Framework\assertStringStartsWith;
3236
use function PHPUnit\Framework\assertThat;
3337
use function PHPUnit\Framework\containsOnly;
3438
use function PHPUnit\Framework\isInstanceOf;
@@ -39,6 +43,7 @@
3943
use function sprintf;
4044
use function str_starts_with;
4145
use function strrchr;
46+
use function trim;
4247

4348
/**
4449
* Constraint that checks if one value matches another.
@@ -263,6 +268,35 @@ private function assertMatchesOperator(BSONDocument $operator, $actual, string $
263268
return;
264269
}
265270

271+
if ($name === '$$matchAsDocument') {
272+
assertInstanceOf(BSONDocument::class, $operator['$$matchAsDocument'], '$$matchAsDocument requires a BSON document');
273+
assertIsString($actual, '$$matchAsDocument requires actual value to be a JSON string');
274+
assertJson($actual, '$$matchAsDocument requires actual value to be a JSON string');
275+
276+
/* Note: assertJson() accepts array and scalar values, but the spec
277+
* assumes that the JSON string will yield a document. */
278+
assertStringStartsWith('{', trim($actual), '$$matchAsDocument requires actual value to be a JSON string denoting an object');
279+
280+
$actualDocument = Document::fromJSON($actual)->toPHP();
281+
$constraint = new Matches($operator['$$matchAsDocument'], $this->entityMap, allowExtraRootKeys: false);
282+
283+
if (! $constraint->evaluate($actualDocument, '', true)) {
284+
self::failAt(sprintf('%s did not match: %s', (new Exporter())->shortenedExport($actual), $constraint->additionalFailureDescription(null)), $keyPath);
285+
}
286+
287+
return;
288+
}
289+
290+
if ($name === '$$matchAsRoot') {
291+
$constraint = new Matches($operator['$$matchAsRoot'], $this->entityMap, allowExtraRootKeys: true);
292+
293+
if (! $constraint->evaluate($actual, '', true)) {
294+
self::failAt(sprintf('$actual did not match as root-level document: %s', $constraint->additionalFailureDescription(null)), $keyPath);
295+
}
296+
297+
return;
298+
}
299+
266300
if ($name === '$$matchesEntity') {
267301
assertNotNull($this->entityMap, '$$matchesEntity requires EntityMap');
268302
assertIsString($operator['$$matchesEntity'], '$$matchesEntity requires string');

tests/UnifiedSpecTests/Constraint/MatchesTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,37 @@ public function testOperatorSessionLsid(): void
171171
$this->assertResult(false, $c, ['x' => 1], 'session LSID does not match (embedded)');
172172
}
173173

174+
public function testOperatorMatchAsDocument(): void
175+
{
176+
$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => 1]]]);
177+
$this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches');
178+
$this->assertResult(false, $c, ['json' => '{"x": 2}'], 'JSON document does not match');
179+
$this->assertResult(false, $c, ['json' => '{"x": 1, "y": 2}'], 'JSON document cannot contain extra fields');
180+
181+
$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => 1.0]]]);
182+
$this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches (flexible numeric comparison)');
183+
184+
$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => ['$$exists' => true]]]]);
185+
$this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches (special operators)');
186+
$this->assertResult(false, $c, ['json' => '{"y": 1}'], 'JSON document does not match (special operators)');
187+
188+
$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => ['$$type' => 'objectId']]]]);
189+
$this->assertResult(true, $c, ['json' => '{"x": {"$oid": "57e193d7a9cc81b4027498b5"}}'], 'JSON document matches (extended JSON)');
190+
$this->assertResult(false, $c, ['json' => '{"x": {"$numberDecimal": "1234.5"}}'], 'JSON document does not match (extended JSON)');
191+
}
192+
193+
public function testOperatorMatchAsRoot(): void
194+
{
195+
$c = new Matches(['x' => ['$$matchAsRoot' => ['y' => 2]]]);
196+
$this->assertResult(true, $c, ['x' => ['y' => 2, 'z' => 3]], 'Nested document matches (allow extra fields)');
197+
$this->assertResult(true, $c, ['x' => ['y' => 2.0, 'z' => 3.0]], 'Nested document matches (flexible numeric comparison)');
198+
$this->assertResult(false, $c, ['x' => ['y' => 3, 'z' => 3]], 'Nested document does not match');
199+
200+
$c = new Matches(['x' => ['$$matchAsRoot' => ['y' => ['$$exists' => true]]]]);
201+
$this->assertResult(true, $c, ['x' => ['y' => 2, 'z' => 3]], 'Nested document matches (special operators)');
202+
$this->assertResult(false, $c, ['x' => ['z' => 3]], 'Nested document matches (special operators)');
203+
}
204+
174205
#[DataProvider('errorMessageProvider')]
175206
public function testErrorMessages($expectedMessageRegex, Matches $constraint, $actualValue): void
176207
{
@@ -302,6 +333,10 @@ public static function operatorErrorMessageProvider()
302333
'$$sessionLsid requires string',
303334
new Matches(['x' => ['$$sessionLsid' => 1]], new EntityMap()),
304335
],
336+
'$$matchAsDocument type' => [
337+
'$$matchAsDocument requires a BSON document',
338+
new Matches(['x' => ['$$matchAsDocument' => 'foo']]),
339+
],
305340
];
306341
}
307342

tests/UnifiedSpecTests/UnifiedTestRunner.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ final class UnifiedTestRunner
6161
* - 1.9: Only createEntities operation is implemented
6262
* - 1.10: Not implemented
6363
* - 1.11: Not implemented, but CMAP is not applicable
64-
* - 1.13: Not implemented
64+
* - 1.13: Only $$matchAsDocument and $$matchAsRoot is implemented
6565
* - 1.14: Not implemented
6666
*/
6767
public const MAX_SCHEMA_VERSION = '1.15';

0 commit comments

Comments
 (0)