From 372a051a703a2661078183a5edb8090c066009f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 18 Oct 2023 22:07:05 +0200 Subject: [PATCH 1/8] PHPLIB-1271 Add tests from the documentation in JS --- generator/composer.json | 3 +- .../config/accumulator/accumulator.tests.js | 76 +++++++++++ generator/config/accumulator/accumulator.yaml | 8 +- .../config/accumulator/addToSet.tests.js | 25 ++++ generator/config/definitions.php | 6 + generator/config/schema.json | 7 + generator/config/stage/addFields.tests.js | 25 ++++ generator/src/AbstractGenerator.php | 20 ++- .../src/Definition/OperatorDefinition.php | 1 + generator/src/Definition/YamlReader.php | 8 ++ generator/src/OperatorTestGenerator.php | 110 ++++++++++++++++ generator/src/TestCase/PipelineConverter.php | 22 ++++ generator/src/TestCase/testsToPhp.js | 69 ++++++++++ phpcs.xml.dist | 1 + .../Accumulator/AccumulatorAccumulator.php | 33 ++--- src/Builder/Accumulator/FactoryTrait.php | 17 +-- .../AccumulatorAccumulatorTest.php | 123 ++++++++++++++++++ .../Accumulator/AddToSetAccumulatorTest.php | 114 ++++++++++++++++ tests/Builder/PipelineTestCase.php | 22 ++++ tests/Builder/Stage/AddFieldsStageTest.php | 96 ++++++++++++++ 20 files changed, 755 insertions(+), 31 deletions(-) create mode 100644 generator/config/accumulator/accumulator.tests.js create mode 100644 generator/config/accumulator/addToSet.tests.js create mode 100644 generator/config/stage/addFields.tests.js create mode 100644 generator/src/OperatorTestGenerator.php create mode 100644 generator/src/TestCase/PipelineConverter.php create mode 100644 generator/src/TestCase/testsToPhp.js create mode 100644 tests/Builder/Accumulator/AccumulatorAccumulatorTest.php create mode 100644 tests/Builder/Accumulator/AddToSetAccumulatorTest.php create mode 100644 tests/Builder/PipelineTestCase.php create mode 100644 tests/Builder/Stage/AddFieldsStageTest.php diff --git a/generator/composer.json b/generator/composer.json index 9295237..d03eb2e 100644 --- a/generator/composer.json +++ b/generator/composer.json @@ -15,9 +15,10 @@ "require": { "php": ">=8.1", "ext-mongodb": "^1.16.0", - "mongodb/mongodb": "^1.17.0@dev", "mongodb/builder": "@dev", + "mongodb/mongodb": "^1.17.0@dev", "nette/php-generator": "^4", + "nikic/php-parser": "^4.17", "symfony/console": "^6.3|^7.0", "symfony/finder": "^6.3|^7.0", "symfony/yaml": "^6.3|^7.0" diff --git a/generator/config/accumulator/accumulator.tests.js b/generator/config/accumulator/accumulator.tests.js new file mode 100644 index 0000000..bdaef6a --- /dev/null +++ b/generator/config/accumulator/accumulator.tests.js @@ -0,0 +1,76 @@ +/** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use--accumulator-to-implement-the--avg-operator */ +module.exports.UseAccumulatorToImplementTheAvgOperator = [ + { + $group: + { + _id: "$author", + avgCopies: + { + $accumulator: + { + init: function () { + return {count: 0, sum: 0} + }, + accumulate: function (state, numCopies) { + return { + count: state.count + 1, + sum: state.sum + numCopies + } + }, + accumulateArgs: ["$copies"], + merge: function (state1, state2) { + return { + count: state1.count + state2.count, + sum: state1.sum + state2.sum + } + }, + finalize: function (state) { + return (state.sum / state.count) + }, + lang: "js" + } + } + } + } +]; + +/** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use-initargs-to-vary-the-initial-state-by-group */ +module.exports.UseInitArgsToVaryTheInitialStateByGroup = [ + { + $group: + { + _id: {city: "$city"}, + restaurants: + { + $accumulator: + { + init: function (city, userProfileCity) { + return { + max: city === userProfileCity ? 3 : 1, + restaurants: [] + } + }, + initArgs: ["$city", "Bettles"], + accumulate: function (state, restaurantName) { + if (state.restaurants.length < state.max) { + state.restaurants.push(restaurantName); + } + return state; + }, + accumulateArgs: ["$name"], + merge: function (state1, state2) { + return { + max: state1.max, + restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) + } + }, + finalize: function (state) { + return state.restaurants + }, + lang: "js", + } + } + } + } +] + diff --git a/generator/config/accumulator/accumulator.yaml b/generator/config/accumulator/accumulator.yaml index ed20178..af72b9d 100644 --- a/generator/config/accumulator/accumulator.yaml +++ b/generator/config/accumulator/accumulator.yaml @@ -11,7 +11,7 @@ arguments: - name: init type: - - string + - javascript description: | 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. - @@ -24,7 +24,7 @@ arguments: - name: accumulate type: - - string + - javascript description: | 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. - @@ -36,13 +36,13 @@ arguments: - name: merge type: - - string + - javascript description: | 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. - name: finalize type: - - string + - javascript optional: true description: | Function used to update the result of the accumulation. diff --git a/generator/config/accumulator/addToSet.tests.js b/generator/config/accumulator/addToSet.tests.js new file mode 100644 index 0000000..7bd4613 --- /dev/null +++ b/generator/config/accumulator/addToSet.tests.js @@ -0,0 +1,25 @@ +module.exports.UseInGroupStage = [ + { + $group: { + _id: {day: {$dayOfYear: {date: "$date"}}, year: {$year: {date: "$date"}}}, + itemsSold: {$addToSet: "$item"} + } + } +] + +module.exports.UseInSetWindowFieldsStage = [ + { + $setWindowFields: { + partitionBy: "$state", + sortBy: {orderDate: 1}, + output: { + cakeTypesForState: { + $addToSet: "$type", + window: { + documents: ["unbounded", "current"] + } + } + } + } + } +] diff --git a/generator/config/definitions.php b/generator/config/definitions.php index 3581acb..baf101e 100644 --- a/generator/config/definitions.php +++ b/generator/config/definitions.php @@ -6,6 +6,7 @@ use MongoDB\CodeGenerator\OperatorClassGenerator; use MongoDB\CodeGenerator\OperatorFactoryGenerator; +use MongoDB\CodeGenerator\OperatorTestGenerator; return [ // Aggregation Pipeline Stages @@ -16,6 +17,7 @@ 'generators' => [ OperatorClassGenerator::class, OperatorFactoryGenerator::class, + OperatorTestGenerator::class, ], ], @@ -27,6 +29,7 @@ 'generators' => [ OperatorClassGenerator::class, OperatorFactoryGenerator::class, + OperatorTestGenerator::class, ], ], @@ -38,6 +41,7 @@ 'generators' => [ OperatorClassGenerator::class, OperatorFactoryGenerator::class, + OperatorTestGenerator::class, ], ], @@ -49,6 +53,7 @@ 'generators' => [ OperatorClassGenerator::class, OperatorFactoryGenerator::class, + OperatorTestGenerator::class, ], ], @@ -60,6 +65,7 @@ 'generators' => [ OperatorClassGenerator::class, OperatorFactoryGenerator::class, + OperatorTestGenerator::class, ], ], ]; diff --git a/generator/config/schema.json b/generator/config/schema.json index e5ea163..f177d02 100644 --- a/generator/config/schema.json +++ b/generator/config/schema.json @@ -76,6 +76,13 @@ "items": { "$ref": "#/definitions/Argument" } + }, + "examples": { + "$comment": "An optional list of examples for the operator.", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "required": [ diff --git a/generator/config/stage/addFields.tests.js b/generator/config/stage/addFields.tests.js new file mode 100644 index 0000000..f0646f6 --- /dev/null +++ b/generator/config/stage/addFields.tests.js @@ -0,0 +1,25 @@ +/** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#using-two--addfields-stages */ +module.exports.UsingTwoAaddFieldsStages = [ + { + $addFields: { + totalHomework: {$sum: "$homework"}, + totalQuiz: {$sum: "$quiz"} + } + }, + { + $addFields: { + totalScore: { + $add: ["$totalHomework", "$totalQuiz", "$extraCredit"] + } + } + } +] + +/** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#adding-fields-to-an-embedded-document */ +module.exports.AddingFieldsToAnEmbeddedDocument = [ + { + $addFields: { + "specs.fuel_type": "unleaded" + } + } +] diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php index 17fb61e..e74647f 100644 --- a/generator/src/AbstractGenerator.php +++ b/generator/src/AbstractGenerator.php @@ -16,9 +16,11 @@ use function current; use function dirname; use function explode; +use function file_get_contents; use function file_put_contents; use function implode; use function is_dir; +use function is_file; use function ltrim; use function mkdir; use function sprintf; @@ -48,7 +50,7 @@ final protected function splitNamespaceAndClassName(string $fqcn): array return [implode('\\', $parts), $className]; } - final protected function writeFile(PhpNamespace $namespace): void + final protected function writeFile(PhpNamespace $namespace, $autoGeneratedWarning = true): void { $classes = $namespace->getClasses(); 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 $file = new PhpFile(); $file->setStrictTypes(); - $file->setComment('THIS FILE IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!'); + if ($autoGeneratedWarning) { + $file->setComment('THIS FILE IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!'); + } + $file->addNamespace($namespace); file_put_contents($filename, $this->printer->printFile($file)); } + final protected function readFile(string ...$fqcn): PhpFile|null + { + $filename = $this->rootDir . $this->getFileName(...$fqcn); + + if (! is_file($filename)) { + return null; + } + + return PhpFile::fromCode(file_get_contents($filename)); + } + /** * Thanks to PSR-4, the file name can be determined from the fully qualified class name. * diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php index acbcc97..14c32e0 100644 --- a/generator/src/Definition/OperatorDefinition.php +++ b/generator/src/Definition/OperatorDefinition.php @@ -27,6 +27,7 @@ public function __construct( public array $type, public string|null $description = null, array $arguments = [], + public string|null $testsFile = null, ) { $this->encode = match ($encode) { 'single' => Encode::Single, diff --git a/generator/src/Definition/YamlReader.php b/generator/src/Definition/YamlReader.php index 7cc2d62..14812f8 100644 --- a/generator/src/Definition/YamlReader.php +++ b/generator/src/Definition/YamlReader.php @@ -9,6 +9,8 @@ use function assert; use function is_array; +use function is_file; +use function preg_replace; final class YamlReader { @@ -22,6 +24,12 @@ public function read(string $dirname): array foreach ($finder as $file) { $operator = Yaml::parseFile($file->getPathname()); assert(is_array($operator)); + + $testsFile = preg_replace('/\.yaml$/', '.tests.js', $file->getPathname()); + if (is_file($testsFile)) { + $operator['testsFile'] = $testsFile; + } + $definitions[] = new OperatorDefinition(...$operator); } diff --git a/generator/src/OperatorTestGenerator.php b/generator/src/OperatorTestGenerator.php new file mode 100644 index 0000000..0c27dd3 --- /dev/null +++ b/generator/src/OperatorTestGenerator.php @@ -0,0 +1,110 @@ +getOperators($definition) as $operator) { + // Skip operators without tests + if ($operator->testsFile === null) { + continue; + } + + try { + $this->writeFile($this->createClass($definition, $operator), false); + } catch (Throwable $e) { + throw new RuntimeException(sprintf('Failed to generate class for operator "%s"', $operator->name), 0, $e); + } + } + } + + public function createClass(GeneratorDefinition $definition, OperatorDefinition $operator): PhpNamespace + { + $tests = (new PipelineConverter())->getTestsAsRawPhp($operator->testsFile); + + $testNamespace = str_replace('MongoDB', 'MongoDB\\Tests', $definition->namespace); + $testClass = $this->getOperatorClassName($definition, $operator) . 'Test'; + + $namespace = $this->readFile($testNamespace, $testClass)?->getNamespaces()[$testNamespace] ?? null; + $namespace ??= new PhpNamespace($testNamespace); + + $class = $namespace->getClasses()[$testClass] ?? null; + $class ??= $namespace->addClass($testClass); + $namespace->addUse(PipelineTestCase::class); + $class->setExtends(PipelineTestCase::class); + $namespace->addUse(Pipeline::class); + + foreach ($tests as $testName => $expected) { + if ($class->hasMethod('test' . $testName)) { + $testMethod = $class->getMethod('test' . $testName); + } else { + $testMethod = $class->addMethod('test' . $testName); + $testMethod->setBody(<<getExpected{$testName}(); + + \$this->assertSamePipeline(\$expected, \$pipeline); + PHP); + } + + $testMethod->setComment('@see getExpected' . $testName); + $testMethod->setPublic(); + $testMethod->setReturnType(Type::Void); + + $expectedMethod = $class->hasMethod('getExpected' . $testName) + ? $class->getMethod('getExpected' . $testName) + : $class->addMethod('getExpected' . $testName); + $expectedMethod->setPublic(); + $expectedMethod->setReturnType(Type::Array); + $expectedMethod->setComment('THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!'); + $expectedMethod->addComment(''); + $expectedMethod->addComment('@see test' . $testName); + $expectedMethod->addComment(''); + $expectedMethod->addComment('@return list>'); + + // Replace namespace BSON classes with use statements + $expected = preg_replace_callback( + '/\\\?MongoDB\\\BSON\\\([A-Z][a-zA-Z0-9]+)/', + function ($matches) use ($namespace): string { + $namespace->addUse($matches[0]); + + return $matches[1]; + }, + $expected, + ); + + $expectedMethod->setBody(<<getMethods(); + ksort($methods); + $class->setMethods($methods); + + return $namespace; + } +} diff --git a/generator/src/TestCase/PipelineConverter.php b/generator/src/TestCase/PipelineConverter.php new file mode 100644 index 0000000..8261f25 --- /dev/null +++ b/generator/src/TestCase/PipelineConverter.php @@ -0,0 +1,22 @@ + */ + public function getTestsAsRawPhp(string $filename): array + { + $output = shell_exec(sprintf('node %s %s', self::SCRIPT, $filename)); + + return json_decode($output, true); + } +} diff --git a/generator/src/TestCase/testsToPhp.js b/generator/src/TestCase/testsToPhp.js new file mode 100644 index 0000000..2c805b5 --- /dev/null +++ b/generator/src/TestCase/testsToPhp.js @@ -0,0 +1,69 @@ +/** + * Node script to convert test pipelines from JS into PHP code. + */ +const fs = require('fs'); + +function toPhp(object, indent = 0) { + const newline = '\n' + (' '.repeat(indent)); + const newlineplus1 = '\n' + (' '.repeat(indent + 1)); + if (object === null) { + return 'null'; + } + + if (Array.isArray(object)) { + if (object.length <= 1) { + return '[' + object.map((item) => toPhp(item, indent)) + ']'; + } + + return '[' + newlineplus1 + object.map((item) => toPhp(item, indent + 1)).join(',' + newlineplus1) + ',' + newline + ']'; + } + + switch (typeof object) { + case 'boolean': + return object ? 'true' : 'false'; + case 'string': + return "'" + object.replace(/'/g, "\\'") + "'"; + case 'number': + return object.toString(); + case 'object': + var dump = []; + for (var key in object) { + dump.push("'" + key.replace(/'/g, "\\'") + "' => " + toPhp(object[key], indent + 1)); + } + + return '(object) [' + newlineplus1 + dump.join(',' + newlineplus1) + ',' + newline + ']'; + case 'function': + return 'new \\MongoDB\\BSON\\Javascript(\'' + + object.toString() + .replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '') + .replace(/\s+/g, ' ') + + '\')'; + default: + return '"Unsupported type: ' + typeof object + '"'; + } +} + +// Get the file path from the command-line arguments +const args = process.argv.slice(2); + +if (args.length !== 1) { + console.error('Usage: node ${path.basename(__filename)} '); + process.exit(1); +} + +const constantsFilePath = args[0]; + +try { + const tests = require(constantsFilePath); + + for (const name in tests) { + if (Object.prototype.hasOwnProperty.call(tests, name)) { + tests[name] = toPhp(tests[name], 0); + } + } + + // Print the transformed constants to the standard output + console.log(JSON.stringify(tests)); +} catch (err) { + console.error('Error reading the constants file:', err.message); +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 74f7c0b..520984b 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -16,6 +16,7 @@ src/Builder/(Accumulator|Expression|Query|Projection|Stage)/*\.php + *\.js diff --git a/src/Builder/Accumulator/AccumulatorAccumulator.php b/src/Builder/Accumulator/AccumulatorAccumulator.php index 49cae3e..0f20886 100644 --- a/src/Builder/Accumulator/AccumulatorAccumulator.php +++ b/src/Builder/Accumulator/AccumulatorAccumulator.php @@ -8,6 +8,7 @@ namespace MongoDB\Builder\Accumulator; +use MongoDB\BSON\Javascript; use MongoDB\BSON\PackedArray; use MongoDB\Builder\Expression\ResolvesToArray; use MongoDB\Builder\Type\AccumulatorInterface; @@ -30,17 +31,17 @@ class AccumulatorAccumulator implements AccumulatorInterface, OperatorInterface { public const ENCODE = Encode::Object; - /** @var non-empty-string $init 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. */ - public readonly string $init; + /** @var Javascript|non-empty-string $init 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. */ + public readonly Javascript|string $init; - /** @var non-empty-string $accumulate 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. */ - public readonly string $accumulate; + /** @var Javascript|non-empty-string $accumulate 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. */ + public readonly Javascript|string $accumulate; /** @var BSONArray|PackedArray|ResolvesToArray|array $accumulateArgs Arguments passed to the accumulate function. You can use accumulateArgs to specify what field value(s) to pass to the accumulate function. */ public readonly PackedArray|ResolvesToArray|BSONArray|array $accumulateArgs; - /** @var non-empty-string $merge 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. */ - public readonly string $merge; + /** @var Javascript|non-empty-string $merge 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. */ + public readonly Javascript|string $merge; /** @var non-empty-string $lang The language used in the $accumulator code. */ public readonly string $lang; @@ -48,26 +49,26 @@ class AccumulatorAccumulator implements AccumulatorInterface, OperatorInterface /** @var Optional|BSONArray|PackedArray|ResolvesToArray|array $initArgs Arguments passed to the init function. */ public readonly Optional|PackedArray|ResolvesToArray|BSONArray|array $initArgs; - /** @var Optional|non-empty-string $finalize Function used to update the result of the accumulation. */ - public readonly Optional|string $finalize; + /** @var Optional|Javascript|non-empty-string $finalize Function used to update the result of the accumulation. */ + public readonly Optional|Javascript|string $finalize; /** - * @param non-empty-string $init 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. - * @param non-empty-string $accumulate 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. + * @param Javascript|non-empty-string $init 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. + * @param Javascript|non-empty-string $accumulate 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. * @param BSONArray|PackedArray|ResolvesToArray|array $accumulateArgs Arguments passed to the accumulate function. You can use accumulateArgs to specify what field value(s) to pass to the accumulate function. - * @param non-empty-string $merge 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. + * @param Javascript|non-empty-string $merge 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. * @param non-empty-string $lang The language used in the $accumulator code. * @param Optional|BSONArray|PackedArray|ResolvesToArray|array $initArgs Arguments passed to the init function. - * @param Optional|non-empty-string $finalize Function used to update the result of the accumulation. + * @param Optional|Javascript|non-empty-string $finalize Function used to update the result of the accumulation. */ public function __construct( - string $init, - string $accumulate, + Javascript|string $init, + Javascript|string $accumulate, PackedArray|ResolvesToArray|BSONArray|array $accumulateArgs, - string $merge, + Javascript|string $merge, string $lang, Optional|PackedArray|ResolvesToArray|BSONArray|array $initArgs = Optional::Undefined, - Optional|string $finalize = Optional::Undefined, + Optional|Javascript|string $finalize = Optional::Undefined, ) { $this->init = $init; $this->accumulate = $accumulate; diff --git a/src/Builder/Accumulator/FactoryTrait.php b/src/Builder/Accumulator/FactoryTrait.php index 09f70c9..c53f323 100644 --- a/src/Builder/Accumulator/FactoryTrait.php +++ b/src/Builder/Accumulator/FactoryTrait.php @@ -11,6 +11,7 @@ use MongoDB\BSON\Decimal128; use MongoDB\BSON\Document; use MongoDB\BSON\Int64; +use MongoDB\BSON\Javascript; use MongoDB\BSON\PackedArray; use MongoDB\BSON\Serializable; use MongoDB\BSON\Type; @@ -35,22 +36,22 @@ trait FactoryTrait * New in MongoDB 4.4. * * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/ - * @param non-empty-string $init 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. - * @param non-empty-string $accumulate 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. + * @param Javascript|non-empty-string $init 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. + * @param Javascript|non-empty-string $accumulate 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. * @param BSONArray|PackedArray|ResolvesToArray|array $accumulateArgs Arguments passed to the accumulate function. You can use accumulateArgs to specify what field value(s) to pass to the accumulate function. - * @param non-empty-string $merge 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. + * @param Javascript|non-empty-string $merge 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. * @param non-empty-string $lang The language used in the $accumulator code. * @param Optional|BSONArray|PackedArray|ResolvesToArray|array $initArgs Arguments passed to the init function. - * @param Optional|non-empty-string $finalize Function used to update the result of the accumulation. + * @param Optional|Javascript|non-empty-string $finalize Function used to update the result of the accumulation. */ public static function accumulator( - string $init, - string $accumulate, + Javascript|string $init, + Javascript|string $accumulate, PackedArray|ResolvesToArray|BSONArray|array $accumulateArgs, - string $merge, + Javascript|string $merge, string $lang, Optional|PackedArray|ResolvesToArray|BSONArray|array $initArgs = Optional::Undefined, - Optional|string $finalize = Optional::Undefined, + Optional|Javascript|string $finalize = Optional::Undefined, ): AccumulatorAccumulator { return new AccumulatorAccumulator($init, $accumulate, $accumulateArgs, $merge, $lang, $initArgs, $finalize); diff --git a/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php b/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php new file mode 100644 index 0000000..f7efad6 --- /dev/null +++ b/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php @@ -0,0 +1,123 @@ +> + */ + public function getExpectedUseAccumulatorToImplementTheAvgOperator(): array + { + return [(object) [ + '$group' => (object) [ + '_id' => '$author', + 'avgCopies' => (object) [ + '$accumulator' => (object) [ + 'init' => new Javascript('function () { return {count: 0, sum: 0} }'), + 'accumulate' => new Javascript('function (state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }'), + 'accumulateArgs' => ['$copies'], + 'merge' => new Javascript('function (state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }'), + 'finalize' => new Javascript('function (state) { return (state.sum / state.count) }'), + 'lang' => 'js', + ], + ], + ], + ], + ]; + } + + /** + * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! + * + * @see testUseInitArgsToVaryTheInitialStateByGroup + * + * @return list> + */ + public function getExpectedUseInitArgsToVaryTheInitialStateByGroup(): array + { + return [(object) [ + '$group' => (object) [ + '_id' => (object) ['city' => '$city'], + 'restaurants' => (object) [ + '$accumulator' => (object) [ + 'init' => new Javascript('function (city, userProfileCity) { return { max: city === userProfileCity ? 3 : 1, restaurants: [] } }'), + 'initArgs' => [ + '$city', + 'Bettles', + ], + 'accumulate' => new Javascript('function (state, restaurantName) { if (state.restaurants.length < state.max) { state.restaurants.push(restaurantName); } return state; }'), + 'accumulateArgs' => ['$name'], + 'merge' => new Javascript('function (state1, state2) { return { max: state1.max, restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) } }'), + 'finalize' => new Javascript('function (state) { return state.restaurants }'), + 'lang' => 'js', + ], + ], + ], + ], + ]; + } + + /** @see getExpectedUseAccumulatorToImplementTheAvgOperator */ + public function testUseAccumulatorToImplementTheAvgOperator(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('author'), + avgCopies: Accumulator::accumulator( + init: new Javascript('function () { return {count: 0, sum: 0} }'), + accumulate: new Javascript('function (state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }'), + accumulateArgs: [Expression::fieldPath('copies')], + merge: new Javascript('function (state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }'), + finalize: new Javascript('function (state) { return (state.sum / state.count) }'), + lang: 'js', + ), + ), + ); + + $expected = $this->getExpectedUseAccumulatorToImplementTheAvgOperator(); + + $this->assertSamePipeline($expected, $pipeline); + } + + /** @see getExpectedUseInitArgsToVaryTheInitialStateByGroup */ + public function testUseInitArgsToVaryTheInitialStateByGroup(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: object(city: Expression::fieldPath('city')), + restaurants: Accumulator::accumulator( + init: new Javascript('function (city, userProfileCity) { return { max: city === userProfileCity ? 3 : 1, restaurants: [] } }'), + initArgs: [ + Expression::fieldPath('city'), + 'Bettles', + ], + accumulate: new Javascript('function (state, restaurantName) { if (state.restaurants.length < state.max) { state.restaurants.push(restaurantName); } return state; }'), + accumulateArgs: [Expression::fieldPath('name')], + merge: new Javascript('function (state1, state2) { return { max: state1.max, restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) } }'), + finalize: new Javascript('function (state) { return state.restaurants }'), + lang: 'js', + ), + ), + ); + + $expected = $this->getExpectedUseInitArgsToVaryTheInitialStateByGroup(); + + $this->assertSamePipeline($expected, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/AddToSetAccumulatorTest.php b/tests/Builder/Accumulator/AddToSetAccumulatorTest.php new file mode 100644 index 0000000..5f39674 --- /dev/null +++ b/tests/Builder/Accumulator/AddToSetAccumulatorTest.php @@ -0,0 +1,114 @@ +> + */ + public function getExpectedUseInGroupStage(): array + { + return [(object) [ + '$group' => (object) [ + '_id' => (object) [ + 'day' => (object) [ + '$dayOfYear' => (object) ['date' => '$date'], + ], + 'year' => (object) [ + '$year' => (object) ['date' => '$date'], + ], + ], + 'itemsSold' => (object) ['$addToSet' => '$item'], + ], + ], + ]; + } + + /** + * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! + * + * @see testUseInSetWindowFieldsStage + * + * @return list> + */ + public function getExpectedUseInSetWindowFieldsStage(): array + { + return [(object) [ + '$setWindowFields' => (object) [ + 'partitionBy' => '$state', + 'sortBy' => (object) ['orderDate' => 1], + 'output' => (object) [ + 'cakeTypesForState' => (object) [ + '$addToSet' => '$type', + 'window' => (object) [ + 'documents' => [ + 'unbounded', + 'current', + ], + ], + ], + ], + ], + ], + ]; + } + + /** @see getExpectedUseInGroupStage */ + public function testUseInGroupStage(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: object( + day: Expression::dayOfYear(Expression::dateFieldPath('date')), + year: Expression::year(Expression::dateFieldPath('date')), + ), + itemsSold: Accumulator::addToSet(Expression::arrayFieldPath('item')), + ), + ); + + $expected = $this->getExpectedUseInGroupStage(); + + $this->assertSamePipeline($expected, $pipeline); + } + + /** @see getExpectedUseInSetWindowFieldsStage */ + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: 1, + ), + output: object( + cakeTypesForState: Accumulator::outputWindow( + Accumulator::addToSet(Expression::fieldPath('type')), + documents: [ + 'unbounded', + 'current', + ], + ), + ), + ), + ); + + $expected = $this->getExpectedUseInSetWindowFieldsStage(); + + $this->assertSamePipeline($expected, $pipeline); + } +} diff --git a/tests/Builder/PipelineTestCase.php b/tests/Builder/PipelineTestCase.php new file mode 100644 index 0000000..1f25950 --- /dev/null +++ b/tests/Builder/PipelineTestCase.php @@ -0,0 +1,22 @@ +encode($pipeline); + + self::assertEquals($expected, $actual, var_export($actual, true)); + } +} diff --git a/tests/Builder/Stage/AddFieldsStageTest.php b/tests/Builder/Stage/AddFieldsStageTest.php new file mode 100644 index 0000000..7641d93 --- /dev/null +++ b/tests/Builder/Stage/AddFieldsStageTest.php @@ -0,0 +1,96 @@ +> + */ + public function getExpectedAddingFieldsToAnEmbeddedDocument(): array + { + return [(object) [ + '$addFields' => (object) ['specs.fuel_type' => 'unleaded'], + ], + ]; + } + + /** + * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! + * + * @see testUsingTwoAaddFieldsStages + * + * @return list> + */ + public function getExpectedUsingTwoAaddFieldsStages(): array + { + return [ + (object) [ + '$addFields' => (object) [ + 'totalHomework' => (object) ['$sum' => '$homework'], + 'totalQuiz' => (object) ['$sum' => '$quiz'], + ], + ], + (object) [ + '$addFields' => (object) [ + 'totalScore' => (object) [ + '$add' => [ + '$totalHomework', + '$totalQuiz', + '$extraCredit', + ], + ], + ], + ], + ]; + } + + /** @see getExpectedAddingFieldsToAnEmbeddedDocument */ + public function testAddingFieldsToAnEmbeddedDocument(): void + { + $pipeline = new Pipeline( + Stage::addFields( + ...['specs.fuel_type' => 'unleaded'], + ), + ); + + $expected = $this->getExpectedAddingFieldsToAnEmbeddedDocument(); + + $this->assertSamePipeline($expected, $pipeline); + } + + /** @see getExpectedUsingTwoAaddFieldsStages */ + public function testUsingTwoAaddFieldsStages(): void + { + $this->markTestSkipped('$sum must accept arrayFieldPath and render it as a single value: https://jira.mongodb.org/browse/PHPLIB-1287'); + + $pipeline = new Pipeline( + Stage::addFields( + totalHomework: Expression::sum(Expression::fieldPath('homework')), + totalQuiz: Expression::sum(Expression::fieldPath('quiz')), + ), + Stage::addFields( + totalScore: Expression::add( + Expression::fieldPath('totalHomework'), + Expression::fieldPath('totalQuiz'), + Expression::fieldPath('extraCredit'), + ), + ), + ); + + $expected = $this->getExpectedUsingTwoAaddFieldsStages(); + + $this->assertSamePipeline($expected, $pipeline); + } +} From 74c8fdcd06f4cb535c3fbeb4c10234b18697167d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 19 Oct 2023 18:38:16 +0200 Subject: [PATCH 2/8] Remove node dependency by putting operator examples in Yaml --- .../config/accumulator/accumulator.tests.js | 76 ------------ generator/config/accumulator/accumulator.yaml | 46 ++++++++ .../config/accumulator/addToSet.tests.js | 25 ---- generator/config/accumulator/addToSet.yaml | 32 +++++ generator/config/schema.json | 30 ++++- generator/config/stage/addFields.tests.js | 25 ---- generator/config/stage/addFields.yaml | 25 ++++ .../src/Definition/OperatorDefinition.php | 12 +- generator/src/Definition/TestDefinition.php | 20 ++++ generator/src/Definition/YamlReader.php | 7 -- generator/src/OperatorTestGenerator.php | 31 +++-- generator/src/TestCase/PipelineConverter.php | 22 ---- generator/src/TestCase/testsToPhp.js | 69 ----------- .../AccumulatorAccumulatorTest.php | 110 +++++++++++------- .../Accumulator/AddToSetAccumulatorTest.php | 86 ++++++++------ tests/Builder/PipelineTestCase.php | 7 +- tests/Builder/Stage/AddFieldsStageTest.php | 71 ++++++----- 17 files changed, 331 insertions(+), 363 deletions(-) delete mode 100644 generator/config/accumulator/accumulator.tests.js delete mode 100644 generator/config/accumulator/addToSet.tests.js delete mode 100644 generator/config/stage/addFields.tests.js create mode 100644 generator/src/Definition/TestDefinition.php delete mode 100644 generator/src/TestCase/PipelineConverter.php delete mode 100644 generator/src/TestCase/testsToPhp.js diff --git a/generator/config/accumulator/accumulator.tests.js b/generator/config/accumulator/accumulator.tests.js deleted file mode 100644 index bdaef6a..0000000 --- a/generator/config/accumulator/accumulator.tests.js +++ /dev/null @@ -1,76 +0,0 @@ -/** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use--accumulator-to-implement-the--avg-operator */ -module.exports.UseAccumulatorToImplementTheAvgOperator = [ - { - $group: - { - _id: "$author", - avgCopies: - { - $accumulator: - { - init: function () { - return {count: 0, sum: 0} - }, - accumulate: function (state, numCopies) { - return { - count: state.count + 1, - sum: state.sum + numCopies - } - }, - accumulateArgs: ["$copies"], - merge: function (state1, state2) { - return { - count: state1.count + state2.count, - sum: state1.sum + state2.sum - } - }, - finalize: function (state) { - return (state.sum / state.count) - }, - lang: "js" - } - } - } - } -]; - -/** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use-initargs-to-vary-the-initial-state-by-group */ -module.exports.UseInitArgsToVaryTheInitialStateByGroup = [ - { - $group: - { - _id: {city: "$city"}, - restaurants: - { - $accumulator: - { - init: function (city, userProfileCity) { - return { - max: city === userProfileCity ? 3 : 1, - restaurants: [] - } - }, - initArgs: ["$city", "Bettles"], - accumulate: function (state, restaurantName) { - if (state.restaurants.length < state.max) { - state.restaurants.push(restaurantName); - } - return state; - }, - accumulateArgs: ["$name"], - merge: function (state1, state2) { - return { - max: state1.max, - restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) - } - }, - finalize: function (state) { - return state.restaurants - }, - lang: "js", - } - } - } - } -] - diff --git a/generator/config/accumulator/accumulator.yaml b/generator/config/accumulator/accumulator.yaml index af72b9d..696a321 100644 --- a/generator/config/accumulator/accumulator.yaml +++ b/generator/config/accumulator/accumulator.yaml @@ -52,3 +52,49 @@ arguments: - string description: | The language used in the $accumulator code. + +tests: + - + name: 'Use $accumulator to Implement the $avg Operator' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use--accumulator-to-implement-the--avg-operator' + pipeline: + - + $group: + _id: '$author' + avgCopies: + $accumulator: + init: + $code: 'function () { return { count: 0, sum: 0 } }' + accumulate: + $code: 'function (state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }' + accumulateArgs: [ "$copies" ], + merge: + $code: 'function (state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }' + finalize: + $code: 'function (state) { return (state.sum / state.count) }' + lang: 'js' + + - + name: 'Use initArgs to Vary the Initial State by Group' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use-initargs-to-vary-the-initial-state-by-group' + pipeline: + - + $group: + _id: + city: '$city' + restaurants: + $accumulator: + init: + $code: 'function (city, userProfileCity) { return { max: city === userProfileCity ? 3 : 1, restaurants: [] } }' + initArgs: + - '$city' + - 'Bettles' + accumulate: + $code: 'function (state, restaurantName) { if (state.restaurants.length < state.max) { state.restaurants.push(restaurantName); } return state; }' + accumulateArgs: + - '$name' + merge: + $code: 'function (state1, state2) { return { max: state1.max, restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) } }' + finalize: + $code: 'function (state) { return state.restaurants }' + lang: 'js' diff --git a/generator/config/accumulator/addToSet.tests.js b/generator/config/accumulator/addToSet.tests.js deleted file mode 100644 index 7bd4613..0000000 --- a/generator/config/accumulator/addToSet.tests.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports.UseInGroupStage = [ - { - $group: { - _id: {day: {$dayOfYear: {date: "$date"}}, year: {$year: {date: "$date"}}}, - itemsSold: {$addToSet: "$item"} - } - } -] - -module.exports.UseInSetWindowFieldsStage = [ - { - $setWindowFields: { - partitionBy: "$state", - sortBy: {orderDate: 1}, - output: { - cakeTypesForState: { - $addToSet: "$type", - window: { - documents: ["unbounded", "current"] - } - } - } - } - } -] diff --git a/generator/config/accumulator/addToSet.yaml b/generator/config/accumulator/addToSet.yaml index 4ea79e7..9566899 100644 --- a/generator/config/accumulator/addToSet.yaml +++ b/generator/config/accumulator/addToSet.yaml @@ -13,3 +13,35 @@ arguments: name: expression type: - expression + +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addToSet/#use-in--group-stage' + pipeline: + - $group: + _id: + day: + $dayOfYear: + date: '$date' + year: + $year: + date: '$date' + itemsSold: + $addToSet: '$item' + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addToSet/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + cakeTypesForState: + $addToSet: '$type' + window: + documents: + - 'unbounded' + - 'current' diff --git a/generator/config/schema.json b/generator/config/schema.json index f177d02..b225f91 100644 --- a/generator/config/schema.json +++ b/generator/config/schema.json @@ -77,11 +77,11 @@ "$ref": "#/definitions/Argument" } }, - "examples": { + "tests": { "$comment": "An optional list of examples for the operator.", - "type": "object", - "additionalProperties": { - "type": "string" + "type": "array", + "items": { + "$ref": "#/definitions/Test" } } }, @@ -175,6 +175,28 @@ "type" ], "title": "Argument" + }, + "Test": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "link": { + "type": "string", + "format": "uri", + "qt-uri-protocols": [ + "https" + ] + }, + "pipeline": { + "type": "array", + "items": { + "type": "object" + } + } + } } } } diff --git a/generator/config/stage/addFields.tests.js b/generator/config/stage/addFields.tests.js deleted file mode 100644 index f0646f6..0000000 --- a/generator/config/stage/addFields.tests.js +++ /dev/null @@ -1,25 +0,0 @@ -/** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#using-two--addfields-stages */ -module.exports.UsingTwoAaddFieldsStages = [ - { - $addFields: { - totalHomework: {$sum: "$homework"}, - totalQuiz: {$sum: "$quiz"} - } - }, - { - $addFields: { - totalScore: { - $add: ["$totalHomework", "$totalQuiz", "$extraCredit"] - } - } - } -] - -/** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#adding-fields-to-an-embedded-document */ -module.exports.AddingFieldsToAnEmbeddedDocument = [ - { - $addFields: { - "specs.fuel_type": "unleaded" - } - } -] diff --git a/generator/config/stage/addFields.yaml b/generator/config/stage/addFields.yaml index f4084f0..343362a 100644 --- a/generator/config/stage/addFields.yaml +++ b/generator/config/stage/addFields.yaml @@ -14,3 +14,28 @@ arguments: variadic: object description: | Specify the name of each field to add and set its value to an aggregation expression or an empty object. + +tests: + - + name: 'Using Two $addFields Stages' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#using-two--addfields-stages' + pipeline: + - + $addFields: + totalHomework: + $sum: '$homework' + totalQuiz: + $sum: '$quiz' + - + $addFields: + totalScore: + $add: + - '$totalHomework' + - '$totalQuiz' + - '$extraCredit' + - + name: 'Adding Fields to an Embedded Document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#adding-fields-to-an-embedded-document' + pipeline: + - $addFields: + specs.fuel_type: 'unleaded' diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php index 14c32e0..e485aab 100644 --- a/generator/src/Definition/OperatorDefinition.php +++ b/generator/src/Definition/OperatorDefinition.php @@ -7,6 +7,7 @@ use MongoDB\Builder\Type\Encode; use UnexpectedValueException; +use function array_map; use function array_merge; use function assert; use function count; @@ -14,10 +15,13 @@ final class OperatorDefinition { - public Encode $encode; + public readonly Encode $encode; /** @var list */ - public array $arguments; + public readonly array $arguments; + + /** @var list */ + public readonly array $tests; public function __construct( public string $name, @@ -27,7 +31,7 @@ public function __construct( public array $type, public string|null $description = null, array $arguments = [], - public string|null $testsFile = null, + array $tests = [], ) { $this->encode = match ($encode) { 'single' => Encode::Single, @@ -56,5 +60,7 @@ public function __construct( } $this->arguments = array_merge($requiredArgs, $optionalArgs); + + $this->tests = array_map(static fn (array $test): TestDefinition => new TestDefinition(...$test), $tests); } } diff --git a/generator/src/Definition/TestDefinition.php b/generator/src/Definition/TestDefinition.php new file mode 100644 index 0000000..f5e77a3 --- /dev/null +++ b/generator/src/Definition/TestDefinition.php @@ -0,0 +1,20 @@ +getPathname()); assert(is_array($operator)); - $testsFile = preg_replace('/\.yaml$/', '.tests.js', $file->getPathname()); - if (is_file($testsFile)) { - $operator['testsFile'] = $testsFile; - } - $definitions[] = new OperatorDefinition(...$operator); } diff --git a/generator/src/OperatorTestGenerator.php b/generator/src/OperatorTestGenerator.php index 0c27dd3..ddfc9c8 100644 --- a/generator/src/OperatorTestGenerator.php +++ b/generator/src/OperatorTestGenerator.php @@ -7,17 +7,19 @@ use MongoDB\Builder\Pipeline; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\Definition\OperatorDefinition; -use MongoDB\CodeGenerator\TestCase\PipelineConverter; use MongoDB\Tests\Builder\PipelineTestCase; use Nette\PhpGenerator\PhpNamespace; use Nette\PhpGenerator\Type; use RuntimeException; use Throwable; +use function json_encode; use function ksort; -use function preg_replace_callback; use function sprintf; use function str_replace; +use function ucwords; + +use const JSON_PRETTY_PRINT; /** * Generates a tests for all operators. @@ -28,7 +30,7 @@ public function generate(GeneratorDefinition $definition): void { foreach ($this->getOperators($definition) as $operator) { // Skip operators without tests - if ($operator->testsFile === null) { + if (! $operator->tests) { continue; } @@ -42,8 +44,6 @@ public function generate(GeneratorDefinition $definition): void public function createClass(GeneratorDefinition $definition, OperatorDefinition $operator): PhpNamespace { - $tests = (new PipelineConverter())->getTestsAsRawPhp($operator->testsFile); - $testNamespace = str_replace('MongoDB', 'MongoDB\\Tests', $definition->namespace); $testClass = $this->getOperatorClassName($definition, $operator) . 'Test'; @@ -56,7 +56,9 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition $class->setExtends(PipelineTestCase::class); $namespace->addUse(Pipeline::class); - foreach ($tests as $testName => $expected) { + foreach ($operator->tests as $test) { + $testName = str_replace(' ', '', ucwords(str_replace('$', '', $test->name))); + if ($class->hasMethod('test' . $testName)) { $testMethod = $class->getMethod('test' . $testName); } else { @@ -78,26 +80,19 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition ? $class->getMethod('getExpected' . $testName) : $class->addMethod('getExpected' . $testName); $expectedMethod->setPublic(); - $expectedMethod->setReturnType(Type::Array); + $expectedMethod->setReturnType(Type::String); $expectedMethod->setComment('THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!'); $expectedMethod->addComment(''); $expectedMethod->addComment('@see test' . $testName); $expectedMethod->addComment(''); - $expectedMethod->addComment('@return list>'); // Replace namespace BSON classes with use statements - $expected = preg_replace_callback( - '/\\\?MongoDB\\\BSON\\\([A-Z][a-zA-Z0-9]+)/', - function ($matches) use ($namespace): string { - $namespace->addUse($matches[0]); - - return $matches[1]; - }, - $expected, - ); + $expected = json_encode($test->pipeline, JSON_PRETTY_PRINT); $expectedMethod->setBody(<< */ - public function getTestsAsRawPhp(string $filename): array - { - $output = shell_exec(sprintf('node %s %s', self::SCRIPT, $filename)); - - return json_decode($output, true); - } -} diff --git a/generator/src/TestCase/testsToPhp.js b/generator/src/TestCase/testsToPhp.js deleted file mode 100644 index 2c805b5..0000000 --- a/generator/src/TestCase/testsToPhp.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Node script to convert test pipelines from JS into PHP code. - */ -const fs = require('fs'); - -function toPhp(object, indent = 0) { - const newline = '\n' + (' '.repeat(indent)); - const newlineplus1 = '\n' + (' '.repeat(indent + 1)); - if (object === null) { - return 'null'; - } - - if (Array.isArray(object)) { - if (object.length <= 1) { - return '[' + object.map((item) => toPhp(item, indent)) + ']'; - } - - return '[' + newlineplus1 + object.map((item) => toPhp(item, indent + 1)).join(',' + newlineplus1) + ',' + newline + ']'; - } - - switch (typeof object) { - case 'boolean': - return object ? 'true' : 'false'; - case 'string': - return "'" + object.replace(/'/g, "\\'") + "'"; - case 'number': - return object.toString(); - case 'object': - var dump = []; - for (var key in object) { - dump.push("'" + key.replace(/'/g, "\\'") + "' => " + toPhp(object[key], indent + 1)); - } - - return '(object) [' + newlineplus1 + dump.join(',' + newlineplus1) + ',' + newline + ']'; - case 'function': - return 'new \\MongoDB\\BSON\\Javascript(\'' - + object.toString() - .replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '') - .replace(/\s+/g, ' ') - + '\')'; - default: - return '"Unsupported type: ' + typeof object + '"'; - } -} - -// Get the file path from the command-line arguments -const args = process.argv.slice(2); - -if (args.length !== 1) { - console.error('Usage: node ${path.basename(__filename)} '); - process.exit(1); -} - -const constantsFilePath = args[0]; - -try { - const tests = require(constantsFilePath); - - for (const name in tests) { - if (Object.prototype.hasOwnProperty.call(tests, name)) { - tests[name] = toPhp(tests[name], 0); - } - } - - // Print the transformed constants to the standard output - console.log(JSON.stringify(tests)); -} catch (err) { - console.error('Error reading the constants file:', err.message); -} diff --git a/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php b/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php index f7efad6..184eea0 100644 --- a/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php +++ b/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php @@ -19,58 +19,82 @@ class AccumulatorAccumulatorTest extends PipelineTestCase * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! * * @see testUseAccumulatorToImplementTheAvgOperator - * - * @return list> */ - public function getExpectedUseAccumulatorToImplementTheAvgOperator(): array + public function getExpectedUseAccumulatorToImplementTheAvgOperator(): string { - return [(object) [ - '$group' => (object) [ - '_id' => '$author', - 'avgCopies' => (object) [ - '$accumulator' => (object) [ - 'init' => new Javascript('function () { return {count: 0, sum: 0} }'), - 'accumulate' => new Javascript('function (state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }'), - 'accumulateArgs' => ['$copies'], - 'merge' => new Javascript('function (state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }'), - 'finalize' => new Javascript('function (state) { return (state.sum / state.count) }'), - 'lang' => 'js', - ], - ], - ], - ], - ]; + return <<<'JSON' + [ + { + "$group": { + "_id": "$author", + "avgCopies": { + "$accumulator": { + "init": { + "$code": "function () { return { count: 0, sum: 0 } }" + }, + "accumulate": { + "$code": "function (state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }" + }, + "accumulateArgs": [ + "$copies" + ], + "merge": { + "$code": "function (state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }" + }, + "finalize": { + "$code": "function (state) { return (state.sum \/ state.count) }" + }, + "lang": "js" + } + } + } + } + ] + JSON; } /** * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! * * @see testUseInitArgsToVaryTheInitialStateByGroup - * - * @return list> */ - public function getExpectedUseInitArgsToVaryTheInitialStateByGroup(): array + public function getExpectedUseInitArgsToVaryTheInitialStateByGroup(): string { - return [(object) [ - '$group' => (object) [ - '_id' => (object) ['city' => '$city'], - 'restaurants' => (object) [ - '$accumulator' => (object) [ - 'init' => new Javascript('function (city, userProfileCity) { return { max: city === userProfileCity ? 3 : 1, restaurants: [] } }'), - 'initArgs' => [ - '$city', - 'Bettles', - ], - 'accumulate' => new Javascript('function (state, restaurantName) { if (state.restaurants.length < state.max) { state.restaurants.push(restaurantName); } return state; }'), - 'accumulateArgs' => ['$name'], - 'merge' => new Javascript('function (state1, state2) { return { max: state1.max, restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) } }'), - 'finalize' => new Javascript('function (state) { return state.restaurants }'), - 'lang' => 'js', - ], - ], - ], - ], - ]; + return <<<'JSON' + [ + { + "$group": { + "_id": { + "city": "$city" + }, + "restaurants": { + "$accumulator": { + "init": { + "$code": "function (city, userProfileCity) { return { max: city === userProfileCity ? 3 : 1, restaurants: [] } }" + }, + "initArgs": [ + "$city", + "Bettles" + ], + "accumulate": { + "$code": "function (state, restaurantName) { if (state.restaurants.length < state.max) { state.restaurants.push(restaurantName); } return state; }" + }, + "accumulateArgs": [ + "$name" + ], + "merge": { + "$code": "function (state1, state2) { return { max: state1.max, restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) } }" + }, + "finalize": { + "$code": "function (state) { return state.restaurants }" + }, + "lang": "js" + } + } + } + } + ] + JSON; } /** @see getExpectedUseAccumulatorToImplementTheAvgOperator */ @@ -80,7 +104,7 @@ public function testUseAccumulatorToImplementTheAvgOperator(): void Stage::group( _id: Expression::fieldPath('author'), avgCopies: Accumulator::accumulator( - init: new Javascript('function () { return {count: 0, sum: 0} }'), + init: new Javascript('function () { return { count: 0, sum: 0 } }'), accumulate: new Javascript('function (state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }'), accumulateArgs: [Expression::fieldPath('copies')], merge: new Javascript('function (state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }'), diff --git a/tests/Builder/Accumulator/AddToSetAccumulatorTest.php b/tests/Builder/Accumulator/AddToSetAccumulatorTest.php index 5f39674..3270e4f 100644 --- a/tests/Builder/Accumulator/AddToSetAccumulatorTest.php +++ b/tests/Builder/Accumulator/AddToSetAccumulatorTest.php @@ -18,54 +18,64 @@ class AddToSetAccumulatorTest extends PipelineTestCase * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! * * @see testUseInGroupStage - * - * @return list> */ - public function getExpectedUseInGroupStage(): array + public function getExpectedUseInGroupStage(): string { - return [(object) [ - '$group' => (object) [ - '_id' => (object) [ - 'day' => (object) [ - '$dayOfYear' => (object) ['date' => '$date'], - ], - 'year' => (object) [ - '$year' => (object) ['date' => '$date'], - ], - ], - 'itemsSold' => (object) ['$addToSet' => '$item'], - ], - ], - ]; + return <<<'JSON' + [ + { + "$group": { + "_id": { + "day": { + "$dayOfYear": { + "date": "$date" + } + }, + "year": { + "$year": { + "date": "$date" + } + } + }, + "itemsSold": { + "$addToSet": "$item" + } + } + } + ] + JSON; } /** * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! * * @see testUseInSetWindowFieldsStage - * - * @return list> */ - public function getExpectedUseInSetWindowFieldsStage(): array + public function getExpectedUseInSetWindowFieldsStage(): string { - return [(object) [ - '$setWindowFields' => (object) [ - 'partitionBy' => '$state', - 'sortBy' => (object) ['orderDate' => 1], - 'output' => (object) [ - 'cakeTypesForState' => (object) [ - '$addToSet' => '$type', - 'window' => (object) [ - 'documents' => [ - 'unbounded', - 'current', - ], - ], - ], - ], - ], - ], - ]; + return <<<'JSON' + [ + { + "$setWindowFields": { + "partitionBy": "$state", + "sortBy": { + "orderDate": 1 + }, + "output": { + "cakeTypesForState": { + "$addToSet": "$type", + "window": { + "documents": [ + "unbounded", + "current" + ] + } + } + } + } + } + ] + JSON; } /** @see getExpectedUseInGroupStage */ diff --git a/tests/Builder/PipelineTestCase.php b/tests/Builder/PipelineTestCase.php index 1f25950..ca76be4 100644 --- a/tests/Builder/PipelineTestCase.php +++ b/tests/Builder/PipelineTestCase.php @@ -8,12 +8,17 @@ use MongoDB\Builder\Pipeline; use PHPUnit\Framework\TestCase; +use function MongoDB\BSON\fromJSON; +use function MongoDB\BSON\toPHP; use function var_export; class PipelineTestCase extends TestCase { - final public static function assertSamePipeline(array $expected, Pipeline $pipeline): void + final public static function assertSamePipeline(string $expected, Pipeline $pipeline): void { + // BSON Documents doesn't support top-level arrays. + $expected = toPHP(fromJSON('{"root":' . $expected . '}'))->root; + $codec = new BuilderEncoder(); $actual = $codec->encode($pipeline); diff --git a/tests/Builder/Stage/AddFieldsStageTest.php b/tests/Builder/Stage/AddFieldsStageTest.php index 7641d93..d05dc5b 100644 --- a/tests/Builder/Stage/AddFieldsStageTest.php +++ b/tests/Builder/Stage/AddFieldsStageTest.php @@ -15,45 +15,52 @@ class AddFieldsStageTest extends PipelineTestCase * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! * * @see testAddingFieldsToAnEmbeddedDocument - * - * @return list> */ - public function getExpectedAddingFieldsToAnEmbeddedDocument(): array + public function getExpectedAddingFieldsToAnEmbeddedDocument(): string { - return [(object) [ - '$addFields' => (object) ['specs.fuel_type' => 'unleaded'], - ], - ]; + return <<<'JSON' + [ + { + "$addFields": { + "specs.fuel_type": "unleaded" + } + } + ] + JSON; } /** * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! * - * @see testUsingTwoAaddFieldsStages - * - * @return list> + * @see testUsingTwoAddFieldsStages */ - public function getExpectedUsingTwoAaddFieldsStages(): array + public function getExpectedUsingTwoAddFieldsStages(): string { - return [ - (object) [ - '$addFields' => (object) [ - 'totalHomework' => (object) ['$sum' => '$homework'], - 'totalQuiz' => (object) ['$sum' => '$quiz'], - ], - ], - (object) [ - '$addFields' => (object) [ - 'totalScore' => (object) [ - '$add' => [ - '$totalHomework', - '$totalQuiz', - '$extraCredit', - ], - ], - ], - ], - ]; + return <<<'JSON' + [ + { + "$addFields": { + "totalHomework": { + "$sum": "$homework" + }, + "totalQuiz": { + "$sum": "$quiz" + } + } + }, + { + "$addFields": { + "totalScore": { + "$add": [ + "$totalHomework", + "$totalQuiz", + "$extraCredit" + ] + } + } + } + ] + JSON; } /** @see getExpectedAddingFieldsToAnEmbeddedDocument */ @@ -70,8 +77,8 @@ public function testAddingFieldsToAnEmbeddedDocument(): void $this->assertSamePipeline($expected, $pipeline); } - /** @see getExpectedUsingTwoAaddFieldsStages */ - public function testUsingTwoAaddFieldsStages(): void + /** @see getExpectedUsingTwoAddFieldsStages */ + public function testUsingTwoAddFieldsStages(): void { $this->markTestSkipped('$sum must accept arrayFieldPath and render it as a single value: https://jira.mongodb.org/browse/PHPLIB-1287'); From b47ad7ea167a8e7edab41de1b93cd739757ca296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 19 Oct 2023 18:43:18 +0200 Subject: [PATCH 3/8] Fix CS --- generator/src/AbstractGenerator.php | 2 +- generator/src/Definition/TestDefinition.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php index e74647f..b0fef69 100644 --- a/generator/src/AbstractGenerator.php +++ b/generator/src/AbstractGenerator.php @@ -50,7 +50,7 @@ final protected function splitNamespaceAndClassName(string $fqcn): array return [implode('\\', $parts), $className]; } - final protected function writeFile(PhpNamespace $namespace, $autoGeneratedWarning = true): void + final protected function writeFile(PhpNamespace $namespace, bool $autoGeneratedWarning = true): void { $classes = $namespace->getClasses(); assert(count($classes) === 1, sprintf('Expected exactly one class in namespace "%s", got %d.', $namespace->getName(), count($classes))); diff --git a/generator/src/Definition/TestDefinition.php b/generator/src/Definition/TestDefinition.php index f5e77a3..11a14f4 100644 --- a/generator/src/Definition/TestDefinition.php +++ b/generator/src/Definition/TestDefinition.php @@ -12,6 +12,7 @@ final class TestDefinition { public function __construct( public string $name, + /** @var list */ public array $pipeline, public string|null $link = null, ) { From c05fc2a546ff7f8a97d6a58afb941e46edd8dacf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 19 Oct 2023 22:15:50 +0200 Subject: [PATCH 4/8] Cleanup --- generator/config/schema.json | 8 ++------ generator/src/Definition/OperatorDefinition.php | 3 ++- generator/src/Definition/YamlReader.php | 1 - phpcs.xml.dist | 1 - 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/generator/config/schema.json b/generator/config/schema.json index b225f91..0209108 100644 --- a/generator/config/schema.json +++ b/generator/config/schema.json @@ -15,9 +15,7 @@ "$comment": "The link to the operator's documentation on MongoDB's website.", "type": "string", "format": "uri", - "qt-uri-protocols": [ - "https" - ] + "pattern": "^https://" }, "type": { "type": "array", @@ -186,9 +184,7 @@ "link": { "type": "string", "format": "uri", - "qt-uri-protocols": [ - "https" - ] + "pattern": "^https://" }, "pipeline": { "type": "array", diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php index e485aab..f97c522 100644 --- a/generator/src/Definition/OperatorDefinition.php +++ b/generator/src/Definition/OperatorDefinition.php @@ -9,6 +9,7 @@ use function array_map; use function array_merge; +use function array_values; use function assert; use function count; use function sprintf; @@ -61,6 +62,6 @@ public function __construct( $this->arguments = array_merge($requiredArgs, $optionalArgs); - $this->tests = array_map(static fn (array $test): TestDefinition => new TestDefinition(...$test), $tests); + $this->tests = array_map(static fn (array $test): TestDefinition => new TestDefinition(...$test), array_values($tests)); } } diff --git a/generator/src/Definition/YamlReader.php b/generator/src/Definition/YamlReader.php index cc7355a..7cc2d62 100644 --- a/generator/src/Definition/YamlReader.php +++ b/generator/src/Definition/YamlReader.php @@ -22,7 +22,6 @@ public function read(string $dirname): array foreach ($finder as $file) { $operator = Yaml::parseFile($file->getPathname()); assert(is_array($operator)); - $definitions[] = new OperatorDefinition(...$operator); } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 520984b..74f7c0b 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -16,7 +16,6 @@ src/Builder/(Accumulator|Expression|Query|Projection|Stage)/*\.php - *\.js From a3086a5b2f4005cd7dcb25210e2e040efa1fee00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 19 Oct 2023 23:00:54 +0200 Subject: [PATCH 5/8] Move fixture pipelines to class const --- .gitattributes | 1 + generator/src/OperatorGenerator.php | 2 +- generator/src/OperatorTestGenerator.php | 58 ++++---- .../AccumulatorAccumulatorTest.php | 95 +------------ .../Accumulator/AddToSetAccumulatorTest.php | 77 +---------- tests/Builder/Accumulator/Pipelines.php | 130 ++++++++++++++++++ tests/Builder/Expression/Pipelines.php | 13 ++ tests/Builder/PipelineTestCase.php | 4 +- tests/Builder/Projection/Pipelines.php | 13 ++ tests/Builder/Query/Pipelines.php | 13 ++ tests/Builder/Stage/AddFieldsStageTest.php | 65 +-------- tests/Builder/Stage/Pipelines.php | 50 +++++++ 12 files changed, 271 insertions(+), 250 deletions(-) create mode 100644 tests/Builder/Accumulator/Pipelines.php create mode 100644 tests/Builder/Expression/Pipelines.php create mode 100644 tests/Builder/Projection/Pipelines.php create mode 100644 tests/Builder/Query/Pipelines.php create mode 100644 tests/Builder/Stage/Pipelines.php diff --git a/.gitattributes b/.gitattributes index 9071b99..0f861c4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,3 +14,4 @@ psalm-baseline.xml export-ignore /src/Builder/Query/*.php linguist-generated=true /src/Builder/Projection/*.php linguist-generated=true /src/Builder/Stage/*.php linguist-generated=true +/tests/Builder/*/Pipelines.php linguist-generated=true diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php index fbab602..b194c97 100644 --- a/generator/src/OperatorGenerator.php +++ b/generator/src/OperatorGenerator.php @@ -45,7 +45,7 @@ final public function __construct( abstract public function generate(GeneratorDefinition $definition): void; - /** @return list> */ + /** @return list */ final protected function getOperators(GeneratorDefinition $definition): array { // Remove unsupported operators diff --git a/generator/src/OperatorTestGenerator.php b/generator/src/OperatorTestGenerator.php index ddfc9c8..2d1efde 100644 --- a/generator/src/OperatorTestGenerator.php +++ b/generator/src/OperatorTestGenerator.php @@ -8,15 +8,19 @@ use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\Definition\OperatorDefinition; use MongoDB\Tests\Builder\PipelineTestCase; +use Nette\PhpGenerator\ClassType; +use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\PhpNamespace; use Nette\PhpGenerator\Type; use RuntimeException; use Throwable; +use function basename; use function json_encode; use function ksort; use function sprintf; use function str_replace; +use function strtoupper; use function ucwords; use const JSON_PRETTY_PRINT; @@ -26,8 +30,12 @@ */ class OperatorTestGenerator extends OperatorGenerator { + private const DATA_CLASS = 'Pipelines'; + public function generate(GeneratorDefinition $definition): void { + $dataNamespace = $this->createExpectedClass($definition); + foreach ($this->getOperators($definition) as $operator) { // Skip operators without tests if (! $operator->tests) { @@ -35,14 +43,27 @@ public function generate(GeneratorDefinition $definition): void } try { - $this->writeFile($this->createClass($definition, $operator), false); + $this->writeFile($this->createClass($definition, $operator, $dataNamespace->getClasses()[self::DATA_CLASS]), false); } catch (Throwable $e) { throw new RuntimeException(sprintf('Failed to generate class for operator "%s"', $operator->name), 0, $e); } } + + $this->writeFile($dataNamespace); + } + + public function createExpectedClass(GeneratorDefinition $definition): PhpNamespace + { + $dataNamespace = str_replace('MongoDB', 'MongoDB\\Tests', $definition->namespace); + + $namespace = new PhpNamespace($dataNamespace); + $class = $namespace->addClass(self::DATA_CLASS); + $class->setFinal(); + + return $namespace; } - public function createClass(GeneratorDefinition $definition, OperatorDefinition $operator): PhpNamespace + public function createClass(GeneratorDefinition $definition, OperatorDefinition $operator, ClassType $dataClass): PhpNamespace { $testNamespace = str_replace('MongoDB', 'MongoDB\\Tests', $definition->namespace); $testClass = $this->getOperatorClassName($definition, $operator) . 'Test'; @@ -55,9 +76,18 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition $namespace->addUse(PipelineTestCase::class); $class->setExtends(PipelineTestCase::class); $namespace->addUse(Pipeline::class); + $class->setComment('Test ' . $operator->name . ' ' . basename($definition->configFiles)); foreach ($operator->tests as $test) { $testName = str_replace(' ', '', ucwords(str_replace('$', '', $test->name))); + $constName = str_replace(' ', '_', strtoupper(str_replace('$', '', $operator->name . ' ' . $test->name))); + + $constant = $dataClass->addConstant($constName, new Literal('<<<\'JSON\'' . "\n" . json_encode($test->pipeline, JSON_PRETTY_PRINT) . "\n" . 'JSON')); + if ($test->link) { + $constant->setComment('@see ' . $test->link); + } + + $constName = self::DATA_CLASS . '::' . $constName; if ($class->hasMethod('test' . $testName)) { $testMethod = $class->getMethod('test' . $testName); @@ -66,34 +96,12 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition $testMethod->setBody(<<getExpected{$testName}(); - - \$this->assertSamePipeline(\$expected, \$pipeline); + \$this->assertSamePipeline({$constName}, \$pipeline); PHP); } - $testMethod->setComment('@see getExpected' . $testName); $testMethod->setPublic(); $testMethod->setReturnType(Type::Void); - - $expectedMethod = $class->hasMethod('getExpected' . $testName) - ? $class->getMethod('getExpected' . $testName) - : $class->addMethod('getExpected' . $testName); - $expectedMethod->setPublic(); - $expectedMethod->setReturnType(Type::String); - $expectedMethod->setComment('THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!'); - $expectedMethod->addComment(''); - $expectedMethod->addComment('@see test' . $testName); - $expectedMethod->addComment(''); - - // Replace namespace BSON classes with use statements - $expected = json_encode($test->pipeline, JSON_PRETTY_PRINT); - - $expectedMethod->setBody(<<getMethods(); diff --git a/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php b/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php index 184eea0..e17c9ac 100644 --- a/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php +++ b/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php @@ -13,91 +13,11 @@ use function MongoDB\object; +/** + * Test $accumulator accumulator + */ class AccumulatorAccumulatorTest extends PipelineTestCase { - /** - * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! - * - * @see testUseAccumulatorToImplementTheAvgOperator - */ - public function getExpectedUseAccumulatorToImplementTheAvgOperator(): string - { - return <<<'JSON' - [ - { - "$group": { - "_id": "$author", - "avgCopies": { - "$accumulator": { - "init": { - "$code": "function () { return { count: 0, sum: 0 } }" - }, - "accumulate": { - "$code": "function (state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }" - }, - "accumulateArgs": [ - "$copies" - ], - "merge": { - "$code": "function (state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }" - }, - "finalize": { - "$code": "function (state) { return (state.sum \/ state.count) }" - }, - "lang": "js" - } - } - } - } - ] - JSON; - } - - /** - * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! - * - * @see testUseInitArgsToVaryTheInitialStateByGroup - */ - public function getExpectedUseInitArgsToVaryTheInitialStateByGroup(): string - { - return <<<'JSON' - [ - { - "$group": { - "_id": { - "city": "$city" - }, - "restaurants": { - "$accumulator": { - "init": { - "$code": "function (city, userProfileCity) { return { max: city === userProfileCity ? 3 : 1, restaurants: [] } }" - }, - "initArgs": [ - "$city", - "Bettles" - ], - "accumulate": { - "$code": "function (state, restaurantName) { if (state.restaurants.length < state.max) { state.restaurants.push(restaurantName); } return state; }" - }, - "accumulateArgs": [ - "$name" - ], - "merge": { - "$code": "function (state1, state2) { return { max: state1.max, restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) } }" - }, - "finalize": { - "$code": "function (state) { return state.restaurants }" - }, - "lang": "js" - } - } - } - } - ] - JSON; - } - - /** @see getExpectedUseAccumulatorToImplementTheAvgOperator */ public function testUseAccumulatorToImplementTheAvgOperator(): void { $pipeline = new Pipeline( @@ -114,12 +34,9 @@ public function testUseAccumulatorToImplementTheAvgOperator(): void ), ); - $expected = $this->getExpectedUseAccumulatorToImplementTheAvgOperator(); - - $this->assertSamePipeline($expected, $pipeline); + $this->assertSamePipeline(Pipelines::ACCUMULATOR_USE_ACCUMULATOR_TO_IMPLEMENT_THE_AVG_OPERATOR, $pipeline); } - /** @see getExpectedUseInitArgsToVaryTheInitialStateByGroup */ public function testUseInitArgsToVaryTheInitialStateByGroup(): void { $pipeline = new Pipeline( @@ -140,8 +57,6 @@ public function testUseInitArgsToVaryTheInitialStateByGroup(): void ), ); - $expected = $this->getExpectedUseInitArgsToVaryTheInitialStateByGroup(); - - $this->assertSamePipeline($expected, $pipeline); + $this->assertSamePipeline(Pipelines::ACCUMULATOR_USE_INITARGS_TO_VARY_THE_INITIAL_STATE_BY_GROUP, $pipeline); } } diff --git a/tests/Builder/Accumulator/AddToSetAccumulatorTest.php b/tests/Builder/Accumulator/AddToSetAccumulatorTest.php index 3270e4f..8ff9e49 100644 --- a/tests/Builder/Accumulator/AddToSetAccumulatorTest.php +++ b/tests/Builder/Accumulator/AddToSetAccumulatorTest.php @@ -12,73 +12,11 @@ use function MongoDB\object; +/** + * Test $addToSet accumulator + */ class AddToSetAccumulatorTest extends PipelineTestCase { - /** - * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! - * - * @see testUseInGroupStage - */ - public function getExpectedUseInGroupStage(): string - { - return <<<'JSON' - [ - { - "$group": { - "_id": { - "day": { - "$dayOfYear": { - "date": "$date" - } - }, - "year": { - "$year": { - "date": "$date" - } - } - }, - "itemsSold": { - "$addToSet": "$item" - } - } - } - ] - JSON; - } - - /** - * THIS METHOD IS AUTO-GENERATED. ANY CHANGES WILL BE LOST! - * - * @see testUseInSetWindowFieldsStage - */ - public function getExpectedUseInSetWindowFieldsStage(): string - { - return <<<'JSON' - [ - { - "$setWindowFields": { - "partitionBy": "$state", - "sortBy": { - "orderDate": 1 - }, - "output": { - "cakeTypesForState": { - "$addToSet": "$type", - "window": { - "documents": [ - "unbounded", - "current" - ] - } - } - } - } - } - ] - JSON; - } - - /** @see getExpectedUseInGroupStage */ public function testUseInGroupStage(): void { $pipeline = new Pipeline( @@ -91,12 +29,9 @@ public function testUseInGroupStage(): void ), ); - $expected = $this->getExpectedUseInGroupStage(); - - $this->assertSamePipeline($expected, $pipeline); + $this->assertSamePipeline(Pipelines::ADDTOSET_USE_IN_GROUP_STAGE, $pipeline); } - /** @see getExpectedUseInSetWindowFieldsStage */ public function testUseInSetWindowFieldsStage(): void { $pipeline = new Pipeline( @@ -117,8 +52,6 @@ public function testUseInSetWindowFieldsStage(): void ), ); - $expected = $this->getExpectedUseInSetWindowFieldsStage(); - - $this->assertSamePipeline($expected, $pipeline); + $this->assertSamePipeline(Pipelines::ADDTOSET_USE_IN_SETWINDOWFIELDS_STAGE, $pipeline); } } diff --git a/tests/Builder/Accumulator/Pipelines.php b/tests/Builder/Accumulator/Pipelines.php new file mode 100644 index 0000000..f2b06ee --- /dev/null +++ b/tests/Builder/Accumulator/Pipelines.php @@ -0,0 +1,130 @@ +root; + $expected = toPHP(fromJSON('{"root":' . $expectedJson . '}'))->root; $codec = new BuilderEncoder(); $actual = $codec->encode($pipeline); diff --git a/tests/Builder/Projection/Pipelines.php b/tests/Builder/Projection/Pipelines.php new file mode 100644 index 0000000..5ddbfca --- /dev/null +++ b/tests/Builder/Projection/Pipelines.php @@ -0,0 +1,13 @@ +getExpectedAddingFieldsToAnEmbeddedDocument(); - - $this->assertSamePipeline($expected, $pipeline); + $this->assertSamePipeline(Pipelines::ADDFIELDS_ADDING_FIELDS_TO_AN_EMBEDDED_DOCUMENT, $pipeline); } - /** @see getExpectedUsingTwoAddFieldsStages */ public function testUsingTwoAddFieldsStages(): void { $this->markTestSkipped('$sum must accept arrayFieldPath and render it as a single value: https://jira.mongodb.org/browse/PHPLIB-1287'); @@ -96,8 +43,6 @@ public function testUsingTwoAddFieldsStages(): void ), ); - $expected = $this->getExpectedUsingTwoAaddFieldsStages(); - - $this->assertSamePipeline($expected, $pipeline); + $this->assertSamePipeline(Pipelines::ADDFIELDS_USING_TWO_ADDFIELDS_STAGES, $pipeline); } } diff --git a/tests/Builder/Stage/Pipelines.php b/tests/Builder/Stage/Pipelines.php new file mode 100644 index 0000000..ced52e4 --- /dev/null +++ b/tests/Builder/Stage/Pipelines.php @@ -0,0 +1,50 @@ + Date: Thu, 19 Oct 2023 23:11:45 +0200 Subject: [PATCH 6/8] Use an enum for expected JSON --- generator/src/OperatorTestGenerator.php | 19 ++++++++++--------- .../AccumulatorAccumulatorTest.php | 4 ++-- .../Accumulator/AddToSetAccumulatorTest.php | 4 ++-- tests/Builder/Accumulator/Pipelines.php | 10 +++++----- tests/Builder/Expression/Pipelines.php | 2 +- tests/Builder/PipelineTestCase.php | 7 ++++++- tests/Builder/Projection/Pipelines.php | 2 +- tests/Builder/Query/Pipelines.php | 2 +- tests/Builder/Stage/AddFieldsStageTest.php | 4 ++-- tests/Builder/Stage/Pipelines.php | 6 +++--- 10 files changed, 33 insertions(+), 27 deletions(-) diff --git a/generator/src/OperatorTestGenerator.php b/generator/src/OperatorTestGenerator.php index 2d1efde..7bfb19a 100644 --- a/generator/src/OperatorTestGenerator.php +++ b/generator/src/OperatorTestGenerator.php @@ -9,6 +9,7 @@ use MongoDB\CodeGenerator\Definition\OperatorDefinition; use MongoDB\Tests\Builder\PipelineTestCase; use Nette\PhpGenerator\ClassType; +use Nette\PhpGenerator\EnumType; use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\PhpNamespace; use Nette\PhpGenerator\Type; @@ -30,7 +31,7 @@ */ class OperatorTestGenerator extends OperatorGenerator { - private const DATA_CLASS = 'Pipelines'; + private const DATA_ENUM = 'Pipelines'; public function generate(GeneratorDefinition $definition): void { @@ -43,7 +44,7 @@ public function generate(GeneratorDefinition $definition): void } try { - $this->writeFile($this->createClass($definition, $operator, $dataNamespace->getClasses()[self::DATA_CLASS]), false); + $this->writeFile($this->createClass($definition, $operator, $dataNamespace->getClasses()[self::DATA_ENUM]), false); } catch (Throwable $e) { throw new RuntimeException(sprintf('Failed to generate class for operator "%s"', $operator->name), 0, $e); } @@ -57,13 +58,13 @@ public function createExpectedClass(GeneratorDefinition $definition): PhpNamespa $dataNamespace = str_replace('MongoDB', 'MongoDB\\Tests', $definition->namespace); $namespace = new PhpNamespace($dataNamespace); - $class = $namespace->addClass(self::DATA_CLASS); - $class->setFinal(); + $enum = $namespace->addEnum(self::DATA_ENUM); + $enum->setType('string'); return $namespace; } - public function createClass(GeneratorDefinition $definition, OperatorDefinition $operator, ClassType $dataClass): PhpNamespace + public function createClass(GeneratorDefinition $definition, OperatorDefinition $operator, EnumType $dataEnum): PhpNamespace { $testNamespace = str_replace('MongoDB', 'MongoDB\\Tests', $definition->namespace); $testClass = $this->getOperatorClassName($definition, $operator) . 'Test'; @@ -80,14 +81,14 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition foreach ($operator->tests as $test) { $testName = str_replace(' ', '', ucwords(str_replace('$', '', $test->name))); - $constName = str_replace(' ', '_', strtoupper(str_replace('$', '', $operator->name . ' ' . $test->name))); + $caseName = str_replace(' ', '', ucwords(str_replace('$', '', $operator->name . ' ' . $test->name))); - $constant = $dataClass->addConstant($constName, new Literal('<<<\'JSON\'' . "\n" . json_encode($test->pipeline, JSON_PRETTY_PRINT) . "\n" . 'JSON')); + $constant = $dataEnum->addCase($caseName, new Literal('<<<\'JSON\'' . "\n" . json_encode($test->pipeline, JSON_PRETTY_PRINT) . "\n" . 'JSON')); if ($test->link) { $constant->setComment('@see ' . $test->link); } - $constName = self::DATA_CLASS . '::' . $constName; + $caseName = self::DATA_ENUM . '::' . $caseName; if ($class->hasMethod('test' . $testName)) { $testMethod = $class->getMethod('test' . $testName); @@ -96,7 +97,7 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition $testMethod->setBody(<<assertSamePipeline({$constName}, \$pipeline); + \$this->assertSamePipeline({$caseName}, \$pipeline); PHP); } diff --git a/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php b/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php index e17c9ac..8a392f0 100644 --- a/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php +++ b/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php @@ -34,7 +34,7 @@ public function testUseAccumulatorToImplementTheAvgOperator(): void ), ); - $this->assertSamePipeline(Pipelines::ACCUMULATOR_USE_ACCUMULATOR_TO_IMPLEMENT_THE_AVG_OPERATOR, $pipeline); + $this->assertSamePipeline(Pipelines::AccumulatorUseAccumulatorToImplementTheAvgOperator, $pipeline); } public function testUseInitArgsToVaryTheInitialStateByGroup(): void @@ -57,6 +57,6 @@ public function testUseInitArgsToVaryTheInitialStateByGroup(): void ), ); - $this->assertSamePipeline(Pipelines::ACCUMULATOR_USE_INITARGS_TO_VARY_THE_INITIAL_STATE_BY_GROUP, $pipeline); + $this->assertSamePipeline(Pipelines::AccumulatorUseInitArgsToVaryTheInitialStateByGroup, $pipeline); } } diff --git a/tests/Builder/Accumulator/AddToSetAccumulatorTest.php b/tests/Builder/Accumulator/AddToSetAccumulatorTest.php index 8ff9e49..a63b327 100644 --- a/tests/Builder/Accumulator/AddToSetAccumulatorTest.php +++ b/tests/Builder/Accumulator/AddToSetAccumulatorTest.php @@ -29,7 +29,7 @@ public function testUseInGroupStage(): void ), ); - $this->assertSamePipeline(Pipelines::ADDTOSET_USE_IN_GROUP_STAGE, $pipeline); + $this->assertSamePipeline(Pipelines::AddToSetUseInGroupStage, $pipeline); } public function testUseInSetWindowFieldsStage(): void @@ -52,6 +52,6 @@ public function testUseInSetWindowFieldsStage(): void ), ); - $this->assertSamePipeline(Pipelines::ADDTOSET_USE_IN_SETWINDOWFIELDS_STAGE, $pipeline); + $this->assertSamePipeline(Pipelines::AddToSetUseInSetWindowFieldsStage, $pipeline); } } diff --git a/tests/Builder/Accumulator/Pipelines.php b/tests/Builder/Accumulator/Pipelines.php index f2b06ee..9d207ef 100644 --- a/tests/Builder/Accumulator/Pipelines.php +++ b/tests/Builder/Accumulator/Pipelines.php @@ -8,10 +8,10 @@ namespace MongoDB\Tests\Builder\Accumulator; -final class Pipelines +enum Pipelines: string { /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use--accumulator-to-implement-the--avg-operator */ - public const ACCUMULATOR_USE_ACCUMULATOR_TO_IMPLEMENT_THE_AVG_OPERATOR = <<<'JSON' + case AccumulatorUseAccumulatorToImplementTheAvgOperator = <<<'JSON' [ { "$group": { @@ -42,7 +42,7 @@ final class Pipelines JSON; /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use-initargs-to-vary-the-initial-state-by-group */ - public const ACCUMULATOR_USE_INITARGS_TO_VARY_THE_INITIAL_STATE_BY_GROUP = <<<'JSON' + case AccumulatorUseInitArgsToVaryTheInitialStateByGroup = <<<'JSON' [ { "$group": { @@ -79,7 +79,7 @@ final class Pipelines JSON; /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addToSet/#use-in--group-stage */ - public const ADDTOSET_USE_IN_GROUP_STAGE = <<<'JSON' + case AddToSetUseInGroupStage = <<<'JSON' [ { "$group": { @@ -104,7 +104,7 @@ final class Pipelines JSON; /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addToSet/#use-in--setwindowfields-stage */ - public const ADDTOSET_USE_IN_SETWINDOWFIELDS_STAGE = <<<'JSON' + case AddToSetUseInSetWindowFieldsStage = <<<'JSON' [ { "$setWindowFields": { diff --git a/tests/Builder/Expression/Pipelines.php b/tests/Builder/Expression/Pipelines.php index cda4683..9c54017 100644 --- a/tests/Builder/Expression/Pipelines.php +++ b/tests/Builder/Expression/Pipelines.php @@ -8,6 +8,6 @@ namespace MongoDB\Tests\Builder\Expression; -final class Pipelines +enum Pipelines: string { } diff --git a/tests/Builder/PipelineTestCase.php b/tests/Builder/PipelineTestCase.php index dd8a5af..c220bc5 100644 --- a/tests/Builder/PipelineTestCase.php +++ b/tests/Builder/PipelineTestCase.php @@ -4,6 +4,7 @@ namespace MongoDB\Tests\Builder; +use BackedEnum; use MongoDB\Builder\BuilderEncoder; use MongoDB\Builder\Pipeline; use PHPUnit\Framework\TestCase; @@ -14,8 +15,12 @@ class PipelineTestCase extends TestCase { - final public static function assertSamePipeline(string $expectedJson, Pipeline $pipeline): void + final public static function assertSamePipeline(string|BackedEnum $expectedJson, Pipeline $pipeline): void { + if ($expectedJson instanceof BackedEnum) { + $expectedJson = $expectedJson->value; + } + // BSON Documents doesn't support top-level arrays. $expected = toPHP(fromJSON('{"root":' . $expectedJson . '}'))->root; diff --git a/tests/Builder/Projection/Pipelines.php b/tests/Builder/Projection/Pipelines.php index 5ddbfca..251d9e6 100644 --- a/tests/Builder/Projection/Pipelines.php +++ b/tests/Builder/Projection/Pipelines.php @@ -8,6 +8,6 @@ namespace MongoDB\Tests\Builder\Projection; -final class Pipelines +enum Pipelines: string { } diff --git a/tests/Builder/Query/Pipelines.php b/tests/Builder/Query/Pipelines.php index 4edf05a..29b9742 100644 --- a/tests/Builder/Query/Pipelines.php +++ b/tests/Builder/Query/Pipelines.php @@ -8,6 +8,6 @@ namespace MongoDB\Tests\Builder\Query; -final class Pipelines +enum Pipelines: string { } diff --git a/tests/Builder/Stage/AddFieldsStageTest.php b/tests/Builder/Stage/AddFieldsStageTest.php index e26b7a3..9e88631 100644 --- a/tests/Builder/Stage/AddFieldsStageTest.php +++ b/tests/Builder/Stage/AddFieldsStageTest.php @@ -22,7 +22,7 @@ public function testAddingFieldsToAnEmbeddedDocument(): void ), ); - $this->assertSamePipeline(Pipelines::ADDFIELDS_ADDING_FIELDS_TO_AN_EMBEDDED_DOCUMENT, $pipeline); + $this->assertSamePipeline(Pipelines::AddFieldsAddingFieldsToAnEmbeddedDocument, $pipeline); } public function testUsingTwoAddFieldsStages(): void @@ -43,6 +43,6 @@ public function testUsingTwoAddFieldsStages(): void ), ); - $this->assertSamePipeline(Pipelines::ADDFIELDS_USING_TWO_ADDFIELDS_STAGES, $pipeline); + $this->assertSamePipeline(Pipelines::AddFieldsUsingTwoAddFieldsStages, $pipeline); } } diff --git a/tests/Builder/Stage/Pipelines.php b/tests/Builder/Stage/Pipelines.php index ced52e4..f90b517 100644 --- a/tests/Builder/Stage/Pipelines.php +++ b/tests/Builder/Stage/Pipelines.php @@ -8,10 +8,10 @@ namespace MongoDB\Tests\Builder\Stage; -final class Pipelines +enum Pipelines: string { /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#using-two--addfields-stages */ - public const ADDFIELDS_USING_TWO_ADDFIELDS_STAGES = <<<'JSON' + case AddFieldsUsingTwoAddFieldsStages = <<<'JSON' [ { "$addFields": { @@ -38,7 +38,7 @@ final class Pipelines JSON; /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#adding-fields-to-an-embedded-document */ - public const ADDFIELDS_ADDING_FIELDS_TO_AN_EMBEDDED_DOCUMENT = <<<'JSON' + case AddFieldsAddingFieldsToAnEmbeddedDocument = <<<'JSON' [ { "$addFields": { From b75e59d73de0206b7be7eb00c3d72fa863ddea91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 20 Oct 2023 00:05:52 +0200 Subject: [PATCH 7/8] Add tests on $regex --- generator/config/query/regex.yaml | 24 ++++++++++++++ generator/src/OperatorTestGenerator.php | 18 +++++----- tests/Builder/Accumulator/Pipelines.php | 24 +++++++++++--- tests/Builder/Query/Pipelines.php | 39 ++++++++++++++++++++++ tests/Builder/Query/RegexOperatorTest.php | 40 +++++++++++++++++++++++ tests/Builder/Stage/Pipelines.php | 12 +++++-- 6 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 tests/Builder/Query/RegexOperatorTest.php diff --git a/generator/config/query/regex.yaml b/generator/config/query/regex.yaml index 0b0b8f1..7aee0c8 100644 --- a/generator/config/query/regex.yaml +++ b/generator/config/query/regex.yaml @@ -11,3 +11,27 @@ arguments: name: regex type: - regex + +tests: + - + name: 'Perform a LIKE Match' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/regex/#perform-a-like-match' + pipeline: + - + $match: + sku: + # Should be nested in $regex + $regularExpression: + pattern: '789$' + options: '' + - + name: 'Perform Case-Insensitive Regular Expression Match' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/regex/#perform-case-insensitive-regular-expression-match' + pipeline: + - + $match: + sku: + # Should be nested in $regex + $regularExpression: + pattern: '^ABC' + options: 'i' diff --git a/generator/src/OperatorTestGenerator.php b/generator/src/OperatorTestGenerator.php index 7bfb19a..18273bf 100644 --- a/generator/src/OperatorTestGenerator.php +++ b/generator/src/OperatorTestGenerator.php @@ -8,7 +8,6 @@ use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\Definition\OperatorDefinition; use MongoDB\Tests\Builder\PipelineTestCase; -use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\EnumType; use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\PhpNamespace; @@ -21,7 +20,6 @@ use function ksort; use function sprintf; use function str_replace; -use function strtoupper; use function ucwords; use const JSON_PRETTY_PRINT; @@ -80,20 +78,22 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition $class->setComment('Test ' . $operator->name . ' ' . basename($definition->configFiles)); foreach ($operator->tests as $test) { - $testName = str_replace(' ', '', ucwords(str_replace('$', '', $test->name))); - $caseName = str_replace(' ', '', ucwords(str_replace('$', '', $operator->name . ' ' . $test->name))); + $testName = 'test' . str_replace([' ', '-'], '', ucwords(str_replace('$', '', $test->name))); + $caseName = str_replace([' ', '-'], '', ucwords(str_replace('$', '', $operator->name . ' ' . $test->name))); - $constant = $dataEnum->addCase($caseName, new Literal('<<<\'JSON\'' . "\n" . json_encode($test->pipeline, JSON_PRETTY_PRINT) . "\n" . 'JSON')); + $case = $dataEnum->addCase($caseName, new Literal('<<<\'JSON\'' . "\n" . json_encode($test->pipeline, JSON_PRETTY_PRINT) . "\n" . 'JSON')); + $case->setComment($test->name); if ($test->link) { - $constant->setComment('@see ' . $test->link); + $case->addComment(''); + $case->addComment('@see ' . $test->link); } $caseName = self::DATA_ENUM . '::' . $caseName; - if ($class->hasMethod('test' . $testName)) { - $testMethod = $class->getMethod('test' . $testName); + if ($class->hasMethod($testName)) { + $testMethod = $class->getMethod($testName); } else { - $testMethod = $class->addMethod('test' . $testName); + $testMethod = $class->addMethod($testName); $testMethod->setBody(<<assertSamePipeline(Pipelines::RegexPerformALIKEMatch, $pipeline); + } + + public function testPerformCaseInsensitiveRegularExpressionMatch(): void + { + $pipeline = new Pipeline( + Stage::match( + // sku: \MongoDB\Builder\Query::regex('^ABC', 'i'), + sku: new Regex('^ABC', 'i'), + ), + ); + + $this->assertSamePipeline(Pipelines::RegexPerformCaseInsensitiveRegularExpressionMatch, $pipeline); + } +} diff --git a/tests/Builder/Stage/Pipelines.php b/tests/Builder/Stage/Pipelines.php index f90b517..c671f1c 100644 --- a/tests/Builder/Stage/Pipelines.php +++ b/tests/Builder/Stage/Pipelines.php @@ -10,7 +10,11 @@ enum Pipelines: string { - /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#using-two--addfields-stages */ + /** + * Using Two $addFields Stages + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#using-two--addfields-stages + */ case AddFieldsUsingTwoAddFieldsStages = <<<'JSON' [ { @@ -37,7 +41,11 @@ enum Pipelines: string ] JSON; - /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#adding-fields-to-an-embedded-document */ + /** + * Adding Fields to an Embedded Document + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#adding-fields-to-an-embedded-document + */ case AddFieldsAddingFieldsToAnEmbeddedDocument = <<<'JSON' [ { From 36cb967fd395737351ad7e4a7894eb82fd34eb31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 31 Oct 2023 18:16:54 +0100 Subject: [PATCH 8/8] Fix types on Query --- src/Builder/Query.php | 5 +++-- src/Builder/Type/QueryObject.php | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Builder/Query.php b/src/Builder/Query.php index a7f4ccc..bb74d4c 100644 --- a/src/Builder/Query.php +++ b/src/Builder/Query.php @@ -4,8 +4,9 @@ namespace MongoDB\Builder; +use MongoDB\BSON\Decimal128; +use MongoDB\BSON\Int64; use MongoDB\BSON\Regex; -use MongoDB\BSON\Serializable; use MongoDB\Builder\Query\RegexOperator; use MongoDB\Builder\Type\FieldQueryInterface; use MongoDB\Builder\Type\QueryInterface; @@ -40,7 +41,7 @@ public static function regex(Regex|string $regex, string|null $flags = null): Re return self::generatedRegex($regex); } - public static function query(FieldQueryInterface|QueryInterface|Serializable|array|bool|float|int|stdClass|string|null ...$query): QueryInterface + public static function query(QueryInterface|FieldQueryInterface|Decimal128|Int64|Regex|stdClass|array|bool|float|int|string|null ...$query): QueryInterface { return QueryObject::create($query); } diff --git a/src/Builder/Type/QueryObject.php b/src/Builder/Type/QueryObject.php index 2fbf34c..af6e437 100644 --- a/src/Builder/Type/QueryObject.php +++ b/src/Builder/Type/QueryObject.php @@ -27,7 +27,7 @@ final class QueryObject implements QueryInterface { public readonly array $queries; - /** @param array $queries */ + /** @param array $queries */ public static function create(array $queries): QueryInterface { // We don't wrap a single query in a QueryObject @@ -38,7 +38,7 @@ public static function create(array $queries): QueryInterface return new self($queries); } - /** @param array $queriesOrArrayOfQueries */ + /** @param array $queriesOrArrayOfQueries */ private function __construct(array $queriesOrArrayOfQueries) { // If the first element is an array and not an operator, we assume variadic arguments were not used