Skip to content
This repository was archived by the owner on Feb 28, 2025. It is now read-only.

Commit 661f078

Browse files
committed
PHPLIB-1348 Add tests on Custom Aggregation Expression Operators
Automatically convert string into Javascript BSON
1 parent 2660e6d commit 661f078

File tree

14 files changed

+323
-30
lines changed

14 files changed

+323
-30
lines changed

generator/config/accumulator/accumulator.yaml

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,29 @@ tests:
6464
avgCopies:
6565
$accumulator:
6666
init:
67-
$code: 'function () { return { count: 0, sum: 0 } }'
67+
$code: |-
68+
function() {
69+
return { count: 0, sum: 0 }
70+
}
6871
accumulate:
69-
$code: 'function (state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }'
72+
$code: |-
73+
function(state, numCopies) {
74+
return { count: state.count + 1, sum: state.sum + numCopies }
75+
}
7076
accumulateArgs: [ "$copies" ],
7177
merge:
72-
$code: 'function (state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }'
78+
$code: |-
79+
function(state1, state2) {
80+
return {
81+
count: state1.count + state2.count,
82+
sum: state1.sum + state2.sum
83+
}
84+
}
7385
finalize:
74-
$code: 'function (state) { return (state.sum / state.count) }'
86+
$code: |-
87+
function(state) {
88+
return (state.sum / state.count)
89+
}
7590
lang: 'js'
7691

7792
-
@@ -85,16 +100,34 @@ tests:
85100
restaurants:
86101
$accumulator:
87102
init:
88-
$code: 'function (city, userProfileCity) { return { max: city === userProfileCity ? 3 : 1, restaurants: [] } }'
103+
$code: |-
104+
function(city, userProfileCity) {
105+
return { max: city === userProfileCity ? 3 : 1, restaurants: [] }
106+
}
89107
initArgs:
90108
- '$city'
91109
- 'Bettles'
92110
accumulate:
93-
$code: 'function (state, restaurantName) { if (state.restaurants.length < state.max) { state.restaurants.push(restaurantName); } return state; }'
111+
$code: |-
112+
function(state, restaurantName) {
113+
if (state.restaurants.length < state.max) {
114+
state.restaurants.push(restaurantName);
115+
}
116+
return state;
117+
}
94118
accumulateArgs:
95119
- '$name'
96120
merge:
97-
$code: 'function (state1, state2) { return { max: state1.max, restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) } }'
121+
$code: |-
122+
function(state1, state2) {
123+
return {
124+
max: state1.max,
125+
restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max)
126+
}
127+
}
98128
finalize:
99-
$code: 'function (state) { return state.restaurants }'
129+
$code: |-
130+
function(state) {
131+
return state.restaurants
132+
}
100133
lang: 'js'

generator/config/expression/function.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,54 @@ arguments:
2121
- array
2222
description: |
2323
Arguments passed to the function body. If the body function does not take an argument, you can specify an empty array [ ].
24+
default: []
2425
-
2526
name: lang
2627
type:
2728
- string
2829
default: js
30+
tests:
31+
-
32+
name: 'Usage Example'
33+
link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/function/#example-1--usage-example'
34+
pipeline:
35+
-
36+
$addFields:
37+
isFound:
38+
$function:
39+
body:
40+
$code: |-
41+
function(name) {
42+
return hex_md5(name) == "15b0a220baa16331e8d80e15367677ad"
43+
}
44+
args:
45+
- '$name'
46+
lang: 'js'
47+
message:
48+
$function:
49+
body:
50+
$code: |-
51+
function(name, scores) {
52+
let total = Array.sum(scores);
53+
return `Hello ${name}. Your total score is ${total}.`
54+
}
55+
args:
56+
- '$name'
57+
- '$scores'
58+
lang: 'js'
59+
-
60+
name: 'Alternative to $where'
61+
link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/function/#example-2--alternative-to--where'
62+
pipeline:
63+
-
64+
$match:
65+
$expr:
66+
$function:
67+
body:
68+
$code: |-
69+
function(name) {
70+
return hex_md5(name) == "15b0a220baa16331e8d80e15367677ad";
71+
}
72+
args:
73+
- '$name'
74+
lang: 'js'

generator/config/schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@
166166
},
167167
"default": {
168168
"$comment": "The default value for the argument.",
169-
"type": ["string", "number", "boolean"]
169+
"type": ["array", "boolean", "number", "string"]
170170
}
171171
},
172172
"required": [

generator/src/OperatorClassGenerator.php

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

55
namespace MongoDB\CodeGenerator;
66

7+
use MongoDB\BSON\Javascript;
78
use MongoDB\Builder\Type\Encode;
89
use MongoDB\Builder\Type\OperatorInterface;
910
use MongoDB\Builder\Type\QueryObject;
@@ -151,6 +152,16 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition
151152
152153
PHP);
153154
}
155+
156+
if ($type->javascript) {
157+
$namespace->addUse(Javascript::class);
158+
$constuctor->addBody(<<<PHP
159+
if (is_string(\${$argument->name})) {
160+
\${$argument->name} = new Javascript(\${$argument->name});
161+
}
162+
163+
PHP);
164+
}
154165
}
155166

156167
// Set property from constructor argument

generator/src/OperatorFactoryGenerator.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ private function addMethod(GeneratorDefinition $definition, OperatorDefinition $
8080
} else {
8181
if ($argument->optional) {
8282
$parameter->setDefaultValue(new Literal('Optional::Undefined'));
83+
} elseif ($argument->default !== null) {
84+
$parameter->setDefaultValue($argument->default);
8385
}
8486

8587
$method->addComment('@param ' . $type->doc . ' $' . $argument->name . rtrim(' ' . $argument->description));

generator/src/OperatorGenerator.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ final protected function getType(string $type): ExpressionDefinition
7171
* Expression types can contain class names, interface, native types or "list".
7272
* PHPDoc types are more precise than native types, so we use them systematically even if redundant.
7373
*
74-
* @return object{native:string,doc:string,use:list<class-string>,list:bool,query:bool}
74+
* @return object{native:string,doc:string,use:list<class-string>,list:bool,query:bool,javascript:bool}
7575
*/
7676
final protected function getAcceptedTypes(ArgumentDefinition $arg): stdClass
7777
{
@@ -117,6 +117,9 @@ final protected function getAcceptedTypes(ArgumentDefinition $arg): stdClass
117117
// If the argument is a query, we need to convert it to a QueryObject
118118
$isQuery = in_array('query', $arg->type, true);
119119

120+
// If the argument is code, we need to convert it to a Javascript object
121+
$isJavascript = in_array('javascript', $arg->type, true);
122+
120123
// mixed can only be used as a standalone type
121124
if (in_array('mixed', $nativeTypes, true)) {
122125
$nativeTypes = ['mixed'];
@@ -132,6 +135,7 @@ final protected function getAcceptedTypes(ArgumentDefinition $arg): stdClass
132135
'use' => array_unique($use),
133136
'list' => $listCheck,
134137
'query' => $isQuery,
138+
'javascript' => $isJavascript,
135139
];
136140
}
137141

src/Builder/Accumulator/AccumulatorAccumulator.php

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Builder/Expression/FactoryTrait.php

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Builder/Expression/FunctionOperator.php

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Builder/Query/WhereOperator.php

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/Builder/Accumulator/AccumulatorAccumulatorTest.php

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace MongoDB\Tests\Builder\Accumulator;
66

7-
use MongoDB\BSON\Javascript;
87
use MongoDB\Builder\Accumulator;
98
use MongoDB\Builder\Expression;
109
use MongoDB\Builder\Pipeline;
@@ -24,11 +23,30 @@ public function testUseAccumulatorToImplementTheAvgOperator(): void
2423
Stage::group(
2524
_id: Expression::fieldPath('author'),
2625
avgCopies: Accumulator::accumulator(
27-
init: new Javascript('function () { return { count: 0, sum: 0 } }'),
28-
accumulate: new Javascript('function (state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }'),
26+
init: <<<'JS'
27+
function() {
28+
return { count: 0, sum: 0 }
29+
}
30+
JS,
31+
accumulate: <<<'JS'
32+
function(state, numCopies) {
33+
return { count: state.count + 1, sum: state.sum + numCopies }
34+
}
35+
JS,
2936
accumulateArgs: [Expression::fieldPath('copies')],
30-
merge: new Javascript('function (state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }'),
31-
finalize: new Javascript('function (state) { return (state.sum / state.count) }'),
37+
merge: <<<'JS'
38+
function(state1, state2) {
39+
return {
40+
count: state1.count + state2.count,
41+
sum: state1.sum + state2.sum
42+
}
43+
}
44+
JS,
45+
finalize: <<<'JS'
46+
function(state) {
47+
return (state.sum / state.count)
48+
}
49+
JS,
3250
lang: 'js',
3351
),
3452
),
@@ -43,16 +61,38 @@ public function testUseInitArgsToVaryTheInitialStateByGroup(): void
4361
Stage::group(
4462
_id: object(city: Expression::fieldPath('city')),
4563
restaurants: Accumulator::accumulator(
46-
init: new Javascript('function (city, userProfileCity) { return { max: city === userProfileCity ? 3 : 1, restaurants: [] } }'),
47-
accumulate: new Javascript('function (state, restaurantName) { if (state.restaurants.length < state.max) { state.restaurants.push(restaurantName); } return state; }'),
64+
init: <<<'JS'
65+
function(city, userProfileCity) {
66+
return { max: city === userProfileCity ? 3 : 1, restaurants: [] }
67+
}
68+
JS,
69+
accumulate: <<<'JS'
70+
function(state, restaurantName) {
71+
if (state.restaurants.length < state.max) {
72+
state.restaurants.push(restaurantName);
73+
}
74+
return state;
75+
}
76+
JS,
4877
accumulateArgs: [Expression::fieldPath('name')],
49-
merge: new Javascript('function (state1, state2) { return { max: state1.max, restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) } }'),
78+
merge: <<<'JS'
79+
function(state1, state2) {
80+
return {
81+
max: state1.max,
82+
restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max)
83+
}
84+
}
85+
JS,
5086
lang: 'js',
5187
initArgs: [
5288
Expression::fieldPath('city'),
5389
'Bettles',
5490
],
55-
finalize: new Javascript('function (state) { return state.restaurants }'),
91+
finalize: <<<'JS'
92+
function(state) {
93+
return state.restaurants
94+
}
95+
JS,
5696
),
5797
),
5898
);

0 commit comments

Comments
 (0)