Skip to content

Commit 2bc2c91

Browse files
authored
PHPLIB-1569: Implement $$matchAsDocument and $$matchAsRoot (#1508)
* PHPLIB-1569: Implement $$matchAsDocument and $$matchAsRoot * PHPLIB-1577: Test for liberal numeric comparisons in Matches * Update skip path for $$lte test * Update specs submodule * Skip distinct-hint tests pending PHPLIB-1582
1 parent 21f5e44 commit 2bc2c91

File tree

5 files changed

+79
-3
lines changed

5 files changed

+79
-3
lines changed

tests/UnifiedSpecTests/Constraint/Matches.php

+34
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

+41
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public function testFlexibleNumericComparison(): void
3030
$this->assertResult(true, $c, ['x' => 1.0, 'y' => 1.0], 'Float instead of expected int matches');
3131
$this->assertResult(true, $c, ['x' => 1, 'y' => 1], 'Int instead of expected float matches');
3232
$this->assertResult(false, $c, ['x' => 'foo', 'y' => 1.0], 'Different type does not match');
33+
34+
/* Matches uses PHPUnit's comparators, which follow PHP behavior. This
35+
* is more liberal than the comparison logic called for by the unified
36+
* test format. This test can be removed when PHPLIB-1577 is addressed.
37+
*/
38+
$this->assertResult(true, $c, ['x' => '1.0', 'y' => '1'], 'Numeric strings may match ints and floats');
3339
}
3440

3541
public function testDoNotAllowExtraRootKeys(): void
@@ -171,6 +177,37 @@ public function testOperatorSessionLsid(): void
171177
$this->assertResult(false, $c, ['x' => 1], 'session LSID does not match (embedded)');
172178
}
173179

180+
public function testOperatorMatchAsDocument(): void
181+
{
182+
$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => 1]]]);
183+
$this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches');
184+
$this->assertResult(false, $c, ['json' => '{"x": 2}'], 'JSON document does not match');
185+
$this->assertResult(false, $c, ['json' => '{"x": 1, "y": 2}'], 'JSON document cannot contain extra fields');
186+
187+
$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => 1.0]]]);
188+
$this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches (flexible numeric comparison)');
189+
190+
$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => ['$$exists' => true]]]]);
191+
$this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches (special operators)');
192+
$this->assertResult(false, $c, ['json' => '{"y": 1}'], 'JSON document does not match (special operators)');
193+
194+
$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => ['$$type' => 'objectId']]]]);
195+
$this->assertResult(true, $c, ['json' => '{"x": {"$oid": "57e193d7a9cc81b4027498b5"}}'], 'JSON document matches (extended JSON)');
196+
$this->assertResult(false, $c, ['json' => '{"x": {"$numberDecimal": "1234.5"}}'], 'JSON document does not match (extended JSON)');
197+
}
198+
199+
public function testOperatorMatchAsRoot(): void
200+
{
201+
$c = new Matches(['x' => ['$$matchAsRoot' => ['y' => 2]]]);
202+
$this->assertResult(true, $c, ['x' => ['y' => 2, 'z' => 3]], 'Nested document matches (allow extra fields)');
203+
$this->assertResult(true, $c, ['x' => ['y' => 2.0, 'z' => 3.0]], 'Nested document matches (flexible numeric comparison)');
204+
$this->assertResult(false, $c, ['x' => ['y' => 3, 'z' => 3]], 'Nested document does not match');
205+
206+
$c = new Matches(['x' => ['$$matchAsRoot' => ['y' => ['$$exists' => true]]]]);
207+
$this->assertResult(true, $c, ['x' => ['y' => 2, 'z' => 3]], 'Nested document matches (special operators)');
208+
$this->assertResult(false, $c, ['x' => ['z' => 3]], 'Nested document matches (special operators)');
209+
}
210+
174211
#[DataProvider('errorMessageProvider')]
175212
public function testErrorMessages($expectedMessageRegex, Matches $constraint, $actualValue): void
176213
{
@@ -302,6 +339,10 @@ public static function operatorErrorMessageProvider()
302339
'$$sessionLsid requires string',
303340
new Matches(['x' => ['$$sessionLsid' => 1]], new EntityMap()),
304341
],
342+
'$$matchAsDocument type' => [
343+
'$$matchAsDocument requires a BSON document',
344+
new Matches(['x' => ['$$matchAsDocument' => 'foo']]),
345+
],
305346
];
306347
}
307348

tests/UnifiedSpecTests/UnifiedSpecTest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class UnifiedSpecTest extends FunctionalTestCase
4343
'crud/replaceOne-sort' => 'Sort for replace operations is not supported (PHPLIB-1492)',
4444
'crud/updateOne-sort' => 'Sort for update operations is not supported (PHPLIB-1492)',
4545
'crud/bypassDocumentValidation' => 'bypassDocumentValidation is handled by libmongoc (PHPLIB-1576)',
46+
'crud/distinct-hint' => 'Hint for distinct operations is not supported (PHPLIB-1582)',
4647
];
4748

4849
/** @var array<string, string> */
@@ -60,7 +61,7 @@ class UnifiedSpecTest extends FunctionalTestCase
6061
'valid-pass/expectedEventsForClient-eventType: eventType defaults to command if unset' => 'PHPC does not implement CMAP',
6162
// CSOT is not yet implemented (PHPC-1760)
6263
'valid-pass/collectionData-createOptions: collection is created with the correct options' => 'CSOT is not yet implemented (PHPC-1760)',
63-
'valid-pass/matches-lte-operator: special lte matching operator' => 'CSOT is not yet implemented (PHPC-1760)',
64+
'valid-pass/operator-lte: special lte matching operator' => 'CSOT is not yet implemented (PHPC-1760)',
6465
// libmongoc always adds readConcern to aggregate command
6566
'index-management/search index operations ignore read and write concern: listSearchIndexes ignores read and write concern' => 'libmongoc appends readConcern to aggregate command',
6667
// Uses an invalid object name

tests/UnifiedSpecTests/UnifiedTestRunner.php

+1-1
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';

tests/specifications

Submodule specifications updated 40 files

0 commit comments

Comments
 (0)