From 556a10e7031f8f1ec1145fa212bbc07ea8c476e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 23 Jan 2024 18:37:03 +0100 Subject: [PATCH] PHPLIB-1358 Add tests on Type Expression Operators --- generator/config/expression/convert.yaml | 47 +++ generator/config/expression/isNumber.yaml | 61 ++- generator/config/expression/toBool.yaml | 27 ++ generator/config/expression/toDecimal.yaml | 9 + generator/config/expression/toDouble.yaml | 13 + generator/config/expression/toInt.yaml | 9 + generator/config/expression/toLong.yaml | 12 + generator/config/expression/toObjectId.yaml | 12 + generator/config/expression/type.yaml | 9 + generator/js2yaml.html | 12 + src/Builder/Expression/FactoryTrait.php | 7 +- src/Builder/Expression/IsNumberOperator.php | 20 +- .../Expression/ConvertOperatorTest.php | 75 ++++ .../Expression/IsNumberOperatorTest.php | 96 +++++ tests/Builder/Expression/Pipelines.php | 363 ++++++++++++++++++ .../Builder/Expression/ToBoolOperatorTest.php | 52 +++ .../Expression/ToDecimalOperatorTest.php | 29 ++ .../Expression/ToDoubleOperatorTest.php | 33 ++ .../Builder/Expression/ToIntOperatorTest.php | 29 ++ .../Builder/Expression/ToLongOperatorTest.php | 36 ++ .../Expression/ToObjectIdOperatorTest.php | 36 ++ tests/Builder/Expression/TypeOperatorTest.php | 29 ++ 22 files changed, 995 insertions(+), 21 deletions(-) create mode 100644 tests/Builder/Expression/ConvertOperatorTest.php create mode 100644 tests/Builder/Expression/IsNumberOperatorTest.php create mode 100644 tests/Builder/Expression/ToBoolOperatorTest.php create mode 100644 tests/Builder/Expression/ToDecimalOperatorTest.php create mode 100644 tests/Builder/Expression/ToDoubleOperatorTest.php create mode 100644 tests/Builder/Expression/ToIntOperatorTest.php create mode 100644 tests/Builder/Expression/ToLongOperatorTest.php create mode 100644 tests/Builder/Expression/ToObjectIdOperatorTest.php create mode 100644 tests/Builder/Expression/TypeOperatorTest.php diff --git a/generator/config/expression/convert.yaml b/generator/config/expression/convert.yaml index 36475be..a76311e 100644 --- a/generator/config/expression/convert.yaml +++ b/generator/config/expression/convert.yaml @@ -33,3 +33,50 @@ arguments: description: | The value to return if the input is null or missing. The arguments can be any valid expression. If unspecified, $convert returns null if the input is null or missing. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/convert/#example' + pipeline: + - + $addFields: + convertedPrice: + $convert: + input: '$price' + to: 'decimal' + onError: 'Error' + onNull: !bson_decimal128 '0' + convertedQty: + $convert: + input: '$qty' + to: 'int' + onError: + $concat: + - 'Could not convert ' + - + $toString: '$qty' + - ' to type integer.' + onNull: 0 + - + $project: + totalPrice: + $switch: + branches: + - + case: + $eq: + - + $type: '$convertedPrice' + - 'string' + then: 'NaN' + - + case: + $eq: + - + $type: '$convertedQty' + - 'string' + then: 'NaN' + default: + $multiply: + - '$convertedPrice' + - '$convertedQty' diff --git a/generator/config/expression/isNumber.yaml b/generator/config/expression/isNumber.yaml index 4bce308..3bce99e 100644 --- a/generator/config/expression/isNumber.yaml +++ b/generator/config/expression/isNumber.yaml @@ -13,4 +13,63 @@ arguments: name: expression type: - expression - variadic: array +tests: + - + name: 'Use $isNumber to Check if a Field is Numeric' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/#use--isnumber-to-check-if-a-field-is-numeric' + pipeline: + - + $addFields: + isNumber: + $isNumber: '$reading' + hasType: + $type: '$reading' + - + name: 'Conditionally Modify Fields using $isNumber' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/#conditionally-modify-fields-using--isnumber' + pipeline: + - + $addFields: + points: + $cond: + if: + $isNumber: '$grade' + then: '$grade' + else: + $switch: + branches: + - + case: + $eq: + - '$grade' + - 'A' + then: 4 + - + case: + $eq: + - '$grade' + - 'B' + then: 3 + - + case: + $eq: + - '$grade' + - 'C' + then: 2 + - + case: + $eq: + - '$grade' + - 'D' + then: 1 + - + case: + $eq: + - '$grade' + - 'F' + then: 0 + - + $group: + _id: '$student_id' + GPA: + $avg: '$points' diff --git a/generator/config/expression/toBool.yaml b/generator/config/expression/toBool.yaml index eaf8ca2..7f771ec 100644 --- a/generator/config/expression/toBool.yaml +++ b/generator/config/expression/toBool.yaml @@ -12,3 +12,30 @@ arguments: name: expression type: - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toBool/#example' + pipeline: + - + $addFields: + convertedShippedFlag: + $switch: + branches: + - + case: + $eq: + - '$shipped' + - 'false' + then: false + - + case: + $eq: + - '$shipped' + - '' + then: false + default: + $toBool: '$shipped' + - + $match: + convertedShippedFlag: false diff --git a/generator/config/expression/toDecimal.yaml b/generator/config/expression/toDecimal.yaml index 59db31a..2f35883 100644 --- a/generator/config/expression/toDecimal.yaml +++ b/generator/config/expression/toDecimal.yaml @@ -12,3 +12,12 @@ arguments: name: expression type: - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDecimal/#example' + pipeline: + - + $addFields: + convertedPrice: + $toDecimal: '$price' diff --git a/generator/config/expression/toDouble.yaml b/generator/config/expression/toDouble.yaml index 6fc87f6..f34c36e 100644 --- a/generator/config/expression/toDouble.yaml +++ b/generator/config/expression/toDouble.yaml @@ -12,3 +12,16 @@ arguments: name: expression type: - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDouble/#example' + pipeline: + - + $addFields: + degrees: + $toDouble: + $substrBytes: + - '$temp' + - 0 + - 4 diff --git a/generator/config/expression/toInt.yaml b/generator/config/expression/toInt.yaml index cf34364..2b02399 100644 --- a/generator/config/expression/toInt.yaml +++ b/generator/config/expression/toInt.yaml @@ -12,3 +12,12 @@ arguments: name: expression type: - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/#example' + pipeline: + - + $addFields: + convertedQty: + $toInt: '$qty' diff --git a/generator/config/expression/toLong.yaml b/generator/config/expression/toLong.yaml index 9687e47..3168ad9 100644 --- a/generator/config/expression/toLong.yaml +++ b/generator/config/expression/toLong.yaml @@ -12,3 +12,15 @@ arguments: name: expression type: - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLong/#example' + pipeline: + - + $addFields: + convertedQty: + $toLong: '$qty' + - + $sort: + convertedQty: -1 diff --git a/generator/config/expression/toObjectId.yaml b/generator/config/expression/toObjectId.yaml index e78ee28..803f7ca 100644 --- a/generator/config/expression/toObjectId.yaml +++ b/generator/config/expression/toObjectId.yaml @@ -12,3 +12,15 @@ arguments: name: expression type: - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toObjectId/#example' + pipeline: + - + $addFields: + convertedId: + $toObjectId: '$_id' + - + $sort: + convertedId: -1 diff --git a/generator/config/expression/type.yaml b/generator/config/expression/type.yaml index f9aa73f..c1f63db 100644 --- a/generator/config/expression/type.yaml +++ b/generator/config/expression/type.yaml @@ -11,3 +11,12 @@ arguments: name: expression type: - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/type/#example' + pipeline: + - + $project: + a: + $type: '$a' diff --git a/generator/js2yaml.html b/generator/js2yaml.html index 8e11abb..89e0dd5 100644 --- a/generator/js2yaml.html +++ b/generator/js2yaml.html @@ -74,6 +74,18 @@

Convert JS examples into Yaml

return new TaggedValue('bson_binary', value); } + function Decimal128(value) { + return new TaggedValue('bson_decimal128', value) + } + + function Int32(value) { + return parseInt(value); + } + + function Int64(value) { + return new TaggedValue('bson_int64', value) + } + function convert(jsString) { try { return toYaml(eval(jsString), 1); diff --git a/src/Builder/Expression/FactoryTrait.php b/src/Builder/Expression/FactoryTrait.php index 2c16a6a..cadcf01 100644 --- a/src/Builder/Expression/FactoryTrait.php +++ b/src/Builder/Expression/FactoryTrait.php @@ -970,14 +970,13 @@ public static function isArray( * New in MongoDB 4.4. * * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/ - * @no-named-arguments - * @param ExpressionInterface|Type|array|bool|float|int|non-empty-string|null|stdClass ...$expression + * @param ExpressionInterface|Type|array|bool|float|int|non-empty-string|null|stdClass $expression */ public static function isNumber( - Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, ): IsNumberOperator { - return new IsNumberOperator(...$expression); + return new IsNumberOperator($expression); } /** diff --git a/src/Builder/Expression/IsNumberOperator.php b/src/Builder/Expression/IsNumberOperator.php index 2eb1e1e..8ccc977 100644 --- a/src/Builder/Expression/IsNumberOperator.php +++ b/src/Builder/Expression/IsNumberOperator.php @@ -12,11 +12,8 @@ use MongoDB\Builder\Type\Encode; use MongoDB\Builder\Type\ExpressionInterface; use MongoDB\Builder\Type\OperatorInterface; -use MongoDB\Exception\InvalidArgumentException; use stdClass; -use function array_is_list; - /** * Returns boolean true if the specified expression resolves to an integer, decimal, double, or long. * Returns boolean false if the expression resolves to any other BSON type, null, or a missing field. @@ -28,23 +25,14 @@ class IsNumberOperator implements ResolvesToBool, OperatorInterface { public const ENCODE = Encode::Single; - /** @var list $expression */ - public readonly array $expression; + /** @var ExpressionInterface|Type|array|bool|float|int|non-empty-string|null|stdClass $expression */ + public readonly Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression; /** - * @param ExpressionInterface|Type|array|bool|float|int|non-empty-string|null|stdClass ...$expression - * @no-named-arguments + * @param ExpressionInterface|Type|array|bool|float|int|non-empty-string|null|stdClass $expression */ - public function __construct(Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression) + public function __construct(Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression) { - if (\count($expression) < 1) { - throw new \InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); - } - - if (! array_is_list($expression)) { - throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); - } - $this->expression = $expression; } diff --git a/tests/Builder/Expression/ConvertOperatorTest.php b/tests/Builder/Expression/ConvertOperatorTest.php new file mode 100644 index 0000000..f284889 --- /dev/null +++ b/tests/Builder/Expression/ConvertOperatorTest.php @@ -0,0 +1,75 @@ +assertSamePipeline(Pipelines::ConvertExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/IsNumberOperatorTest.php b/tests/Builder/Expression/IsNumberOperatorTest.php new file mode 100644 index 0000000..2fb81a1 --- /dev/null +++ b/tests/Builder/Expression/IsNumberOperatorTest.php @@ -0,0 +1,96 @@ +assertSamePipeline(Pipelines::IsNumberConditionallyModifyFieldsUsingIsNumber, $pipeline); + } + + public function testUseIsNumberToCheckIfAFieldIsNumeric(): void + { + $pipeline = new Pipeline( + Stage::addFields( + isNumber: Expression::isNumber( + Expression::fieldPath('reading'), + ), + hasType: Expression::type( + Expression::fieldPath('reading'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::IsNumberUseIsNumberToCheckIfAFieldIsNumeric, $pipeline); + } +} diff --git a/tests/Builder/Expression/Pipelines.php b/tests/Builder/Expression/Pipelines.php index e41135c..37fe784 100644 --- a/tests/Builder/Expression/Pipelines.php +++ b/tests/Builder/Expression/Pipelines.php @@ -745,6 +745,86 @@ enum Pipelines: string ] JSON; + /** + * Example + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/convert/#example + */ + case ConvertExample = <<<'JSON' + [ + { + "$addFields": { + "convertedPrice": { + "$convert": { + "input": "$price", + "to": "decimal", + "onError": "Error", + "onNull": { + "$numberDecimal": "0" + } + } + }, + "convertedQty": { + "$convert": { + "input": "$qty", + "to": "int", + "onError": { + "$concat": [ + "Could not convert ", + { + "$toString": "$qty" + }, + " to type integer." + ] + }, + "onNull": { + "$numberInt": "0" + } + } + } + } + }, + { + "$project": { + "totalPrice": { + "$switch": { + "branches": [ + { + "case": { + "$eq": [ + { + "$type": "$convertedPrice" + }, + "string" + ] + }, + "then": "NaN" + }, + { + "case": { + "$eq": [ + { + "$type": "$convertedQty" + }, + "string" + ] + }, + "then": "NaN" + } + ], + "default": { + "$multiply": [ + "$convertedPrice", + "$convertedQty" + ] + } + } + } + } + } + ] + JSON; + /** * Example * @@ -2254,6 +2334,117 @@ enum Pipelines: string ] JSON; + /** + * Use $isNumber to Check if a Field is Numeric + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/#use--isnumber-to-check-if-a-field-is-numeric + */ + case IsNumberUseIsNumberToCheckIfAFieldIsNumeric = <<<'JSON' + [ + { + "$addFields": { + "isNumber": { + "$isNumber": "$reading" + }, + "hasType": { + "$type": "$reading" + } + } + } + ] + JSON; + + /** + * Conditionally Modify Fields using $isNumber + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/#conditionally-modify-fields-using--isnumber + */ + case IsNumberConditionallyModifyFieldsUsingIsNumber = <<<'JSON' + [ + { + "$addFields": { + "points": { + "$cond": { + "if": { + "$isNumber": "$grade" + }, + "then": "$grade", + "else": { + "$switch": { + "branches": [ + { + "case": { + "$eq": [ + "$grade", + "A" + ] + }, + "then": { + "$numberInt": "4" + } + }, + { + "case": { + "$eq": [ + "$grade", + "B" + ] + }, + "then": { + "$numberInt": "3" + } + }, + { + "case": { + "$eq": [ + "$grade", + "C" + ] + }, + "then": { + "$numberInt": "2" + } + }, + { + "case": { + "$eq": [ + "$grade", + "D" + ] + }, + "then": { + "$numberInt": "1" + } + }, + { + "case": { + "$eq": [ + "$grade", + "F" + ] + }, + "then": { + "$numberInt": "0" + } + } + ] + } + } + } + } + } + }, + { + "$group": { + "_id": "$student_id", + "GPA": { + "$avg": "$points" + } + } + } + ] + JSON; + /** * Example * @@ -4912,6 +5103,52 @@ enum Pipelines: string ] JSON; + /** + * Example + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toBool/#example + */ + case ToBoolExample = <<<'JSON' + [ + { + "$addFields": { + "convertedShippedFlag": { + "$switch": { + "branches": [ + { + "case": { + "$eq": [ + "$shipped", + "false" + ] + }, + "then": false + }, + { + "case": { + "$eq": [ + "$shipped", + "" + ] + }, + "then": false + } + ], + "default": { + "$toBool": "$shipped" + } + } + } + } + }, + { + "$match": { + "convertedShippedFlag": false + } + } + ] + JSON; + /** * Example * @@ -4936,6 +5173,50 @@ enum Pipelines: string ] JSON; + /** + * Example + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDecimal/#example + */ + case ToDecimalExample = <<<'JSON' + [ + { + "$addFields": { + "convertedPrice": { + "$toDecimal": "$price" + } + } + } + ] + JSON; + + /** + * Example + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDouble/#example + */ + case ToDoubleExample = <<<'JSON' + [ + { + "$addFields": { + "degrees": { + "$toDouble": { + "$substrBytes": [ + "$temp", + { + "$numberInt": "0" + }, + { + "$numberInt": "4" + } + ] + } + } + } + } + ] + JSON; + /** * Example * @@ -4960,6 +5241,47 @@ enum Pipelines: string ] JSON; + /** + * Example + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/#example + */ + case ToIntExample = <<<'JSON' + [ + { + "$addFields": { + "convertedQty": { + "$toInt": "$qty" + } + } + } + ] + JSON; + + /** + * Example + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLong/#example + */ + case ToLongExample = <<<'JSON' + [ + { + "$addFields": { + "convertedQty": { + "$toLong": "$qty" + } + } + }, + { + "$sort": { + "convertedQty": { + "$numberInt": "-1" + } + } + } + ] + JSON; + /** * Example * @@ -4980,6 +5302,30 @@ enum Pipelines: string ] JSON; + /** + * Example + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toObjectId/#example + */ + case ToObjectIdExample = <<<'JSON' + [ + { + "$addFields": { + "convertedId": { + "$toObjectId": "$_id" + } + } + }, + { + "$sort": { + "convertedId": { + "$numberInt": "-1" + } + } + } + ] + JSON; + /** * Example * @@ -5140,6 +5486,23 @@ enum Pipelines: string ] JSON; + /** + * Example + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/type/#example + */ + case TypeExample = <<<'JSON' + [ + { + "$project": { + "a": { + "$type": "$a" + } + } + } + ] + JSON; + /** * Remove Fields that Contain Periods * diff --git a/tests/Builder/Expression/ToBoolOperatorTest.php b/tests/Builder/Expression/ToBoolOperatorTest.php new file mode 100644 index 0000000..6d6b855 --- /dev/null +++ b/tests/Builder/Expression/ToBoolOperatorTest.php @@ -0,0 +1,52 @@ +assertSamePipeline(Pipelines::ToBoolExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToDecimalOperatorTest.php b/tests/Builder/Expression/ToDecimalOperatorTest.php new file mode 100644 index 0000000..d12a727 --- /dev/null +++ b/tests/Builder/Expression/ToDecimalOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::ToDecimalExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToDoubleOperatorTest.php b/tests/Builder/Expression/ToDoubleOperatorTest.php new file mode 100644 index 0000000..d0819e7 --- /dev/null +++ b/tests/Builder/Expression/ToDoubleOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::ToDoubleExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToIntOperatorTest.php b/tests/Builder/Expression/ToIntOperatorTest.php new file mode 100644 index 0000000..cc88ca6 --- /dev/null +++ b/tests/Builder/Expression/ToIntOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::ToIntExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToLongOperatorTest.php b/tests/Builder/Expression/ToLongOperatorTest.php new file mode 100644 index 0000000..c21fef6 --- /dev/null +++ b/tests/Builder/Expression/ToLongOperatorTest.php @@ -0,0 +1,36 @@ +assertSamePipeline(Pipelines::ToLongExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToObjectIdOperatorTest.php b/tests/Builder/Expression/ToObjectIdOperatorTest.php new file mode 100644 index 0000000..856c765 --- /dev/null +++ b/tests/Builder/Expression/ToObjectIdOperatorTest.php @@ -0,0 +1,36 @@ +assertSamePipeline(Pipelines::ToObjectIdExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/TypeOperatorTest.php b/tests/Builder/Expression/TypeOperatorTest.php new file mode 100644 index 0000000..797536c --- /dev/null +++ b/tests/Builder/Expression/TypeOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::TypeExample, $pipeline); + } +}