Skip to content

Commit 49e266d

Browse files
authored
PHPLIB-1271 Add tests from the documentation (mongodb#6)
1 parent e5ab490 commit 49e266d

27 files changed

+884
-41
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ psalm-baseline.xml export-ignore
1414
/src/Builder/Query/*.php linguist-generated=true
1515
/src/Builder/Projection/*.php linguist-generated=true
1616
/src/Builder/Stage/*.php linguist-generated=true
17+
/tests/Builder/*/Pipelines.php linguist-generated=true

generator/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
"require": {
1616
"php": ">=8.1",
1717
"ext-mongodb": "^1.16.0",
18-
"mongodb/mongodb": "^1.17.0@dev",
1918
"mongodb/builder": "@dev",
19+
"mongodb/mongodb": "^1.17.0@dev",
2020
"nette/php-generator": "^4",
21+
"nikic/php-parser": "^4.17",
2122
"symfony/console": "^6.3|^7.0",
2223
"symfony/finder": "^6.3|^7.0",
2324
"symfony/yaml": "^6.3|^7.0"

generator/config/accumulator/accumulator.yaml

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ arguments:
1111
-
1212
name: init
1313
type:
14-
- string
14+
- javascript
1515
description: |
1616
Function used to initialize the state. The init function receives its arguments from the initArgs array expression. You can specify the function definition as either BSON type Code or String.
1717
-
@@ -24,7 +24,7 @@ arguments:
2424
-
2525
name: accumulate
2626
type:
27-
- string
27+
- javascript
2828
description: |
2929
Function used to accumulate documents. The accumulate function receives its arguments from the current state and accumulateArgs array expression. The result of the accumulate function becomes the new state. You can specify the function definition as either BSON type Code or String.
3030
-
@@ -36,13 +36,13 @@ arguments:
3636
-
3737
name: merge
3838
type:
39-
- string
39+
- javascript
4040
description: |
4141
Function used to merge two internal states. merge must be either a String or Code BSON type. merge returns the combined result of the two merged states. For information on when the merge function is called, see Merge Two States with $merge.
4242
-
4343
name: finalize
4444
type:
45-
- string
45+
- javascript
4646
optional: true
4747
description: |
4848
Function used to update the result of the accumulation.
@@ -52,3 +52,49 @@ arguments:
5252
- string
5353
description: |
5454
The language used in the $accumulator code.
55+
56+
tests:
57+
-
58+
name: 'Use $accumulator to Implement the $avg Operator'
59+
link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use--accumulator-to-implement-the--avg-operator'
60+
pipeline:
61+
-
62+
$group:
63+
_id: '$author'
64+
avgCopies:
65+
$accumulator:
66+
init:
67+
$code: 'function () { return { count: 0, sum: 0 } }'
68+
accumulate:
69+
$code: 'function (state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }'
70+
accumulateArgs: [ "$copies" ],
71+
merge:
72+
$code: 'function (state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }'
73+
finalize:
74+
$code: 'function (state) { return (state.sum / state.count) }'
75+
lang: 'js'
76+
77+
-
78+
name: 'Use initArgs to Vary the Initial State by Group'
79+
link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use-initargs-to-vary-the-initial-state-by-group'
80+
pipeline:
81+
-
82+
$group:
83+
_id:
84+
city: '$city'
85+
restaurants:
86+
$accumulator:
87+
init:
88+
$code: 'function (city, userProfileCity) { return { max: city === userProfileCity ? 3 : 1, restaurants: [] } }'
89+
initArgs:
90+
- '$city'
91+
- 'Bettles'
92+
accumulate:
93+
$code: 'function (state, restaurantName) { if (state.restaurants.length < state.max) { state.restaurants.push(restaurantName); } return state; }'
94+
accumulateArgs:
95+
- '$name'
96+
merge:
97+
$code: 'function (state1, state2) { return { max: state1.max, restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) } }'
98+
finalize:
99+
$code: 'function (state) { return state.restaurants }'
100+
lang: 'js'

generator/config/accumulator/addToSet.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,35 @@ arguments:
1313
name: expression
1414
type:
1515
- expression
16+
17+
tests:
18+
-
19+
name: 'Use in $group Stage'
20+
link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addToSet/#use-in--group-stage'
21+
pipeline:
22+
- $group:
23+
_id:
24+
day:
25+
$dayOfYear:
26+
date: '$date'
27+
year:
28+
$year:
29+
date: '$date'
30+
itemsSold:
31+
$addToSet: '$item'
32+
-
33+
name: 'Use in $setWindowFields Stage'
34+
link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addToSet/#use-in--setwindowfields-stage'
35+
pipeline:
36+
-
37+
$setWindowFields:
38+
partitionBy: '$state'
39+
sortBy:
40+
orderDate: 1
41+
output:
42+
cakeTypesForState:
43+
$addToSet: '$type'
44+
window:
45+
documents:
46+
- 'unbounded'
47+
- 'current'

generator/config/definitions.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use MongoDB\CodeGenerator\OperatorClassGenerator;
88
use MongoDB\CodeGenerator\OperatorFactoryGenerator;
9+
use MongoDB\CodeGenerator\OperatorTestGenerator;
910

1011
return [
1112
// Aggregation Pipeline Stages
@@ -16,6 +17,7 @@
1617
'generators' => [
1718
OperatorClassGenerator::class,
1819
OperatorFactoryGenerator::class,
20+
OperatorTestGenerator::class,
1921
],
2022
],
2123

@@ -27,6 +29,7 @@
2729
'generators' => [
2830
OperatorClassGenerator::class,
2931
OperatorFactoryGenerator::class,
32+
OperatorTestGenerator::class,
3033
],
3134
],
3235

@@ -38,6 +41,7 @@
3841
'generators' => [
3942
OperatorClassGenerator::class,
4043
OperatorFactoryGenerator::class,
44+
OperatorTestGenerator::class,
4145
],
4246
],
4347

@@ -49,6 +53,7 @@
4953
'generators' => [
5054
OperatorClassGenerator::class,
5155
OperatorFactoryGenerator::class,
56+
OperatorTestGenerator::class,
5257
],
5358
],
5459

@@ -60,6 +65,7 @@
6065
'generators' => [
6166
OperatorClassGenerator::class,
6267
OperatorFactoryGenerator::class,
68+
OperatorTestGenerator::class,
6369
],
6470
],
6571
];

generator/config/query/regex.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,27 @@ arguments:
1111
name: regex
1212
type:
1313
- regex
14+
15+
tests:
16+
-
17+
name: 'Perform a LIKE Match'
18+
link: 'https://www.mongodb.com/docs/manual/reference/operator/query/regex/#perform-a-like-match'
19+
pipeline:
20+
-
21+
$match:
22+
sku:
23+
# Should be nested in $regex
24+
$regularExpression:
25+
pattern: '789$'
26+
options: ''
27+
-
28+
name: 'Perform Case-Insensitive Regular Expression Match'
29+
link: 'https://www.mongodb.com/docs/manual/reference/operator/query/regex/#perform-case-insensitive-regular-expression-match'
30+
pipeline:
31+
-
32+
$match:
33+
sku:
34+
# Should be nested in $regex
35+
$regularExpression:
36+
pattern: '^ABC'
37+
options: 'i'

generator/config/schema.json

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
"$comment": "The link to the operator's documentation on MongoDB's website.",
1616
"type": "string",
1717
"format": "uri",
18-
"qt-uri-protocols": [
19-
"https"
20-
]
18+
"pattern": "^https://"
2119
},
2220
"type": {
2321
"type": "array",
@@ -76,6 +74,13 @@
7674
"items": {
7775
"$ref": "#/definitions/Argument"
7876
}
77+
},
78+
"tests": {
79+
"$comment": "An optional list of examples for the operator.",
80+
"type": "array",
81+
"items": {
82+
"$ref": "#/definitions/Test"
83+
}
7984
}
8085
},
8186
"required": [
@@ -168,6 +173,26 @@
168173
"type"
169174
],
170175
"title": "Argument"
176+
},
177+
"Test": {
178+
"type": "object",
179+
"additionalProperties": false,
180+
"properties": {
181+
"name": {
182+
"type": "string"
183+
},
184+
"link": {
185+
"type": "string",
186+
"format": "uri",
187+
"pattern": "^https://"
188+
},
189+
"pipeline": {
190+
"type": "array",
191+
"items": {
192+
"type": "object"
193+
}
194+
}
195+
}
171196
}
172197
}
173198
}

generator/config/stage/addFields.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,28 @@ arguments:
1414
variadic: object
1515
description: |
1616
Specify the name of each field to add and set its value to an aggregation expression or an empty object.
17+
18+
tests:
19+
-
20+
name: 'Using Two $addFields Stages'
21+
link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#using-two--addfields-stages'
22+
pipeline:
23+
-
24+
$addFields:
25+
totalHomework:
26+
$sum: '$homework'
27+
totalQuiz:
28+
$sum: '$quiz'
29+
-
30+
$addFields:
31+
totalScore:
32+
$add:
33+
- '$totalHomework'
34+
- '$totalQuiz'
35+
- '$extraCredit'
36+
-
37+
name: 'Adding Fields to an Embedded Document'
38+
link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#adding-fields-to-an-embedded-document'
39+
pipeline:
40+
- $addFields:
41+
specs.fuel_type: 'unleaded'

generator/src/AbstractGenerator.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
use function current;
1717
use function dirname;
1818
use function explode;
19+
use function file_get_contents;
1920
use function file_put_contents;
2021
use function implode;
2122
use function is_dir;
23+
use function is_file;
2224
use function ltrim;
2325
use function mkdir;
2426
use function sprintf;
@@ -48,7 +50,7 @@ final protected function splitNamespaceAndClassName(string $fqcn): array
4850
return [implode('\\', $parts), $className];
4951
}
5052

51-
final protected function writeFile(PhpNamespace $namespace): void
53+
final protected function writeFile(PhpNamespace $namespace, bool $autoGeneratedWarning = true): void
5254
{
5355
$classes = $namespace->getClasses();
5456
assert(count($classes) === 1, sprintf('Expected exactly one class in namespace "%s", got %d.', $namespace->getName(), count($classes)));
@@ -62,12 +64,26 @@ final protected function writeFile(PhpNamespace $namespace): void
6264

6365
$file = new PhpFile();
6466
$file->setStrictTypes();
65-
$file->setComment('THIS FILE IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!');
67+
if ($autoGeneratedWarning) {
68+
$file->setComment('THIS FILE IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!');
69+
}
70+
6671
$file->addNamespace($namespace);
6772

6873
file_put_contents($filename, $this->printer->printFile($file));
6974
}
7075

76+
final protected function readFile(string ...$fqcn): PhpFile|null
77+
{
78+
$filename = $this->rootDir . $this->getFileName(...$fqcn);
79+
80+
if (! is_file($filename)) {
81+
return null;
82+
}
83+
84+
return PhpFile::fromCode(file_get_contents($filename));
85+
}
86+
7187
/**
7288
* Thanks to PSR-4, the file name can be determined from the fully qualified class name.
7389
*

generator/src/Definition/OperatorDefinition.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@
77
use MongoDB\Builder\Type\Encode;
88
use UnexpectedValueException;
99

10+
use function array_map;
1011
use function array_merge;
12+
use function array_values;
1113
use function assert;
1214
use function count;
1315
use function sprintf;
1416

1517
final class OperatorDefinition
1618
{
17-
public Encode $encode;
19+
public readonly Encode $encode;
1820

1921
/** @var list<ArgumentDefinition> */
20-
public array $arguments;
22+
public readonly array $arguments;
23+
24+
/** @var list<TestDefinition> */
25+
public readonly array $tests;
2126

2227
public function __construct(
2328
public string $name,
@@ -27,6 +32,7 @@ public function __construct(
2732
public array $type,
2833
public string|null $description = null,
2934
array $arguments = [],
35+
array $tests = [],
3036
) {
3137
$this->encode = match ($encode) {
3238
'single' => Encode::Single,
@@ -55,5 +61,7 @@ public function __construct(
5561
}
5662

5763
$this->arguments = array_merge($requiredArgs, $optionalArgs);
64+
65+
$this->tests = array_map(static fn (array $test): TestDefinition => new TestDefinition(...$test), array_values($tests));
5866
}
5967
}

0 commit comments

Comments
 (0)