Skip to content

Commit d64e141

Browse files
committed
Implement map expressions
1 parent 301d70a commit d64e141

File tree

6 files changed

+326
-2
lines changed

6 files changed

+326
-2
lines changed

driver-core/src/main/com/mongodb/client/model/expressions/ArrayExpression.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public interface ArrayExpression<T extends Expression> extends Expression {
7373

7474
<R extends Expression> ArrayExpression<R> union(Function<? super T, ? extends ArrayExpression<? extends R>> mapper);
7575

76+
<R extends Expression> MapExpression<R> buildMap(Function<T, EntryExpression<R>> o);
77+
7678
/**
7779
* user asserts that i is in bounds for the array
7880
*
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.client.model.expressions;
18+
19+
import static com.mongodb.client.model.expressions.Expressions.of;
20+
21+
public interface EntryExpression<T extends Expression> extends Expression {
22+
StringExpression getKey();
23+
24+
T getValue();
25+
26+
EntryExpression<T> setValue(T val);
27+
28+
EntryExpression<T> setKey(StringExpression key);
29+
default EntryExpression<T> setKey(final String key) {
30+
return setKey(of(key));
31+
}
32+
}

driver-core/src/main/com/mongodb/client/model/expressions/Expressions.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.List;
3737

3838
import static com.mongodb.client.model.expressions.MqlExpression.AstPlaceholder;
39+
import static com.mongodb.client.model.expressions.MqlExpression.extractBsonValue;
3940

4041
/**
4142
* Convenience methods related to {@link Expression}.
@@ -184,6 +185,35 @@ public static <T extends Expression> ArrayExpression<T> ofArray(final T... array
184185
});
185186
}
186187

188+
public static <T extends Expression> EntryExpression<T> ofEntry(final String k, final T v) {
189+
Assertions.notNull("k", k);
190+
Assertions.notNull("v", v);
191+
return new MqlExpression<>((cr) -> {
192+
BsonDocument document = new BsonDocument();
193+
document.put("k", new BsonString(k));
194+
document.put("v", extractBsonValue(cr, v));
195+
return new AstPlaceholder(new BsonDocument("$literal",
196+
document.toBsonDocument(BsonDocument.class, cr)));
197+
});
198+
}
199+
200+
public static <T extends Expression> MapExpression<T> ofEmptyMap() {
201+
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonDocument("$literal", new BsonDocument())));
202+
}
203+
204+
/**
205+
* user asserts type of values is T
206+
*
207+
* @param map
208+
* @return
209+
* @param <T>
210+
*/
211+
public static <T extends Expression> MapExpression<T> ofMap(final Bson map) {
212+
Assertions.notNull("map", map);
213+
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonDocument("$literal",
214+
map.toBsonDocument(BsonDocument.class, cr))));
215+
}
216+
187217
public static DocumentExpression of(final Bson document) {
188218
Assertions.notNull("document", document);
189219
// All documents are wrapped in a $literal. If we don't wrap, we need to
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.client.model.expressions;
18+
19+
import static com.mongodb.client.model.expressions.Expressions.of;
20+
21+
public interface MapExpression<T extends Expression> extends Expression {
22+
23+
T get(StringExpression key);
24+
25+
default T get(final String key) {
26+
return get(of(key));
27+
}
28+
29+
T get(StringExpression key, T orElse);
30+
31+
default T get(final String key, final T orElse) {
32+
return get(of(key), orElse);
33+
}
34+
35+
MapExpression<T> set(StringExpression key, T value);
36+
37+
default MapExpression<T> set(final String key, final T value) {
38+
return set(of(key), value);
39+
}
40+
41+
MapExpression<T> unset(StringExpression key);
42+
43+
default MapExpression<T> unset(final String key) {
44+
return unset(of(key));
45+
}
46+
47+
MapExpression<T> mergee(MapExpression<T> map);
48+
49+
ArrayExpression<EntryExpression<T>> entrySet();
50+
}

driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
final class MqlExpression<T extends Expression>
3434
implements Expression, BooleanExpression, IntegerExpression, NumberExpression,
35-
StringExpression, DateExpression, DocumentExpression, ArrayExpression<T> {
35+
StringExpression, DateExpression, DocumentExpression, ArrayExpression<T>, MapExpression<T>, EntryExpression<T> {
3636

3737
private final Function<CodecRegistry, AstPlaceholder> fn;
3838

@@ -53,6 +53,32 @@ private AstPlaceholder astDoc(final String name, final BsonDocument value) {
5353
return new AstPlaceholder(new BsonDocument(name, value));
5454
}
5555

56+
@Override
57+
public StringExpression getKey() {
58+
return new MqlExpression<>(getFieldInternal("k"));
59+
}
60+
61+
@Override
62+
public T getValue() {
63+
return newMqlExpression(getFieldInternal("v"));
64+
}
65+
66+
@Override
67+
public EntryExpression<T> setValue(final T val) {
68+
return newMqlExpression((cr) -> astDoc("$setField", new BsonDocument()
69+
.append("field", new BsonString("v"))
70+
.append("input", this.toBsonValue(cr))
71+
.append("value", extractBsonValue(cr, val))));
72+
}
73+
74+
@Override
75+
public EntryExpression<T> setKey(final StringExpression key) {
76+
return newMqlExpression((cr) -> astDoc("$setField", new BsonDocument()
77+
.append("field", new BsonString("k"))
78+
.append("input", this.toBsonValue(cr))
79+
.append("value", extractBsonValue(cr, key))));
80+
}
81+
5682
static final class AstPlaceholder {
5783
private final BsonValue bsonValue;
5884

@@ -95,7 +121,7 @@ private Function<CodecRegistry, AstPlaceholder> ast(final String name, final Exp
95121
* the only implementation of Expression and all subclasses, so this will
96122
* not mis-cast an expression as anything else.
97123
*/
98-
private static BsonValue extractBsonValue(final CodecRegistry cr, final Expression expression) {
124+
static BsonValue extractBsonValue(final CodecRegistry cr, final Expression expression) {
99125
return ((MqlExpression<?>) expression).toBsonValue(cr);
100126
}
101127

@@ -327,6 +353,11 @@ public BooleanExpression isArray() {
327353
return new MqlExpression<>(astWrapped("$isArray"));
328354
}
329355

356+
public Expression ifNull(final Expression ifNull) {
357+
return new MqlExpression<>(ast("$ifNull", ifNull, Expressions.ofNull()))
358+
.assertImplementsAllExpressions();
359+
}
360+
330361
/**
331362
* checks if array (but cannot check type)
332363
* user asserts array is of type R
@@ -731,4 +762,51 @@ public StringExpression substr(final IntegerExpression start, final IntegerExpre
731762
public StringExpression substrBytes(final IntegerExpression start, final IntegerExpression length) {
732763
return new MqlExpression<>(ast("$substrBytes", start, length));
733764
}
765+
766+
/** @see MapExpression
767+
* @see EntryExpression */
768+
769+
@Override
770+
public T get(final StringExpression key) {
771+
return newMqlExpression((cr) -> astDoc("$getField", new BsonDocument()
772+
.append("input", this.fn.apply(cr).bsonValue)
773+
.append("field", extractBsonValue(cr, key))));
774+
}
775+
776+
@SuppressWarnings("unchecked")
777+
@Override
778+
public T get(final StringExpression key, final T orElse) {
779+
return (T) ((MqlExpression<?>) get(key)).ifNull(orElse); // TODO unchecked
780+
}
781+
782+
@Override
783+
public MapExpression<T> set(final StringExpression key, final T value) {
784+
return newMqlExpression((cr) -> astDoc("$setField", new BsonDocument()
785+
.append("field", extractBsonValue(cr, key))
786+
.append("input", this.toBsonValue(cr))
787+
.append("value", extractBsonValue(cr, value))));
788+
}
789+
790+
@Override
791+
public MapExpression<T> unset(final StringExpression key) {
792+
return newMqlExpression((cr) -> astDoc("$unsetField", new BsonDocument()
793+
.append("field", extractBsonValue(cr, key))
794+
.append("input", this.toBsonValue(cr))));
795+
}
796+
797+
@Override
798+
public MapExpression<T> mergee(final MapExpression<T> map) {
799+
return new MqlExpression<>(ast("$mergeObjects", map));
800+
}
801+
802+
@Override
803+
public ArrayExpression<EntryExpression<T>> entrySet() {
804+
return newMqlExpression(ast("$objectToArray"));
805+
}
806+
807+
@Override
808+
public <R extends Expression> MapExpression<R> buildMap(final Function<T, EntryExpression<R>> o) {
809+
return newMqlExpression(astWrapped("$arrayToObject"));
810+
}
811+
734812
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.client.model.expressions;
18+
19+
import org.bson.BsonDocument;
20+
import org.bson.Document;
21+
import org.junit.jupiter.api.Test;
22+
23+
import java.util.Arrays;
24+
25+
import static com.mongodb.client.model.expressions.Expressions.of;
26+
import static com.mongodb.client.model.expressions.Expressions.ofArray;
27+
import static com.mongodb.client.model.expressions.Expressions.ofEntry;
28+
import static com.mongodb.client.model.expressions.Expressions.ofMap;
29+
30+
class MapExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest {
31+
32+
private final MapExpression<IntegerExpression> mapKey123 = Expressions.<IntegerExpression>ofEmptyMap()
33+
.set("key", of(123));
34+
35+
private final MapExpression<IntegerExpression> mapA1B2 = ofMap(Document.parse("{keyA: 1, keyB: 2}"));
36+
37+
@Test
38+
public void literalsTest() {
39+
// map
40+
assertExpression(
41+
Document.parse("{key: 123}"),
42+
mapKey123,
43+
"{'$setField': {'field': 'key', 'input': {'$literal': {}}, 'value': 123}}");
44+
assertExpression(
45+
Document.parse("{keyA: 1, keyB: 2}"),
46+
ofMap(Document.parse("{keyA: 1, keyB: 2}")),
47+
"{'$literal': {'keyA': 1, 'keyB': 2}}");
48+
// entry
49+
assertExpression(
50+
Document.parse("{k: 'keyA', v: 1}"),
51+
ofEntry("keyA", of(1)));
52+
}
53+
54+
@Test
55+
public void getSetMapTest() {
56+
// get
57+
assertExpression(
58+
123,
59+
mapKey123.get("key"));
60+
assertExpression(
61+
1,
62+
mapKey123.get("missing", of(1)));
63+
// set (map.put)
64+
assertExpression(
65+
BsonDocument.parse("{key: 123, b: 1}"),
66+
mapKey123.set("b", of(1)));
67+
// unset (delete)
68+
assertExpression(
69+
BsonDocument.parse("{}"),
70+
mapKey123.unset("key"));
71+
}
72+
73+
@Test
74+
public void getSetEntryTest() {
75+
EntryExpression<IntegerExpression> entryA1 = ofEntry("keyA", of(1));
76+
assertExpression(
77+
Document.parse("{k: 'keyA', 'v': 33}"),
78+
entryA1.setValue(of(33)));
79+
assertExpression(
80+
Document.parse("{k: 'keyB', 'v': 1}"),
81+
entryA1.setKey(of("keyB")));
82+
assertExpression(
83+
Document.parse("{k: 'keyB', 'v': 1}"),
84+
entryA1.setKey("keyB"));
85+
}
86+
87+
@Test
88+
public void buildMapTest() {
89+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/arrayToObject/ (48)
90+
assertExpression(
91+
Document.parse("{'keyA': 1}"),
92+
ofArray(ofEntry("keyA", of(1))).buildMap(v -> v),
93+
"{'$arrayToObject': [[{'$literal': {'k': 'keyA', 'v': 1}}]]}");
94+
}
95+
96+
@Test
97+
public void entrySetTest() {
98+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/objectToArray/ (23)
99+
assertExpression(
100+
Arrays.asList(Document.parse("{'k': 'k1', 'v': 1}")),
101+
Expressions.<IntegerExpression>ofEmptyMap().set("k1", of(1)).entrySet(),
102+
"{'$objectToArray': {'$setField': "
103+
+ "{'field': 'k1', 'input': {'$literal': {}}, 'value': 1}}}");
104+
105+
// key/value usage
106+
assertExpression(
107+
"keyA|keyB|",
108+
mapA1B2.entrySet().map(v -> v.getKey().concat(of("|"))).join(v -> v));
109+
assertExpression(
110+
23,
111+
mapA1B2.entrySet().map(v -> v.getValue().add(10)).sum(v -> v));
112+
113+
// combined entrySet-buildMap usage
114+
assertExpression(
115+
Document.parse("{'keyA': 2, 'keyB': 3}"),
116+
mapA1B2
117+
.entrySet()
118+
.map(v -> v.setValue(v.getValue().add(1)))
119+
.buildMap(v -> v));
120+
}
121+
122+
@Test
123+
public void mergeTest() {
124+
assertExpression(
125+
Document.parse("{'keyA': 9, 'keyB': 2, 'keyC': 3}"),
126+
ofMap(Document.parse("{keyA: 1, keyB: 2}"))
127+
.mergee(ofMap(Document.parse("{keyA: 9, keyC: 3}"))),
128+
"{'$mergeObjects': [{'$literal': {'keyA': 1, 'keyB': 2}}, "
129+
+ "{'$literal': {'keyA': 9, 'keyC': 3}}]}");
130+
}
131+
132+
}

0 commit comments

Comments
 (0)