From c829387c8260936276e46b791e135468546c9c77 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 9 Mar 2020 14:08:28 +0100 Subject: [PATCH] DATAMONGO-2363 - Polishing. Introduce support for SpEL aggregation expressions for AddFields and Set operations. Rearrange methods. Make fields final where possible. Original pull request: #801. --- .../core/aggregation/AddFieldsOperation.java | 17 +++- .../mongodb/core/aggregation/Aggregation.java | 82 ++++++++++--------- .../core/aggregation/AggregationUpdate.java | 2 +- .../DocumentEnhancingOperation.java | 47 ++++++++++- .../core/aggregation/MergeOperation.java | 65 +++++++++------ .../core/aggregation/SetOperation.java | 19 ++++- .../AddFieldsOperationUnitTests.java | 17 +++- .../aggregation/MergeOperationUnitTests.java | 4 +- .../aggregation/SetOperationUnitTests.java | 32 +++++--- 9 files changed, 202 insertions(+), 83 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java index 984cfdc8c..8ebc744ad 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java @@ -151,6 +151,13 @@ public class AddFieldsOperation extends DocumentEnhancingOperation { valueMap.put(field, value instanceof String ? Fields.fields((String) value) : value); return AddFieldsOperationBuilder.this; } + + @Override + public AddFieldsOperationBuilder withValueOfExpression(String operation, Object... values) { + + valueMap.put(field, new ExpressionProjection(operation, values)); + return AddFieldsOperationBuilder.this; + } }; } @@ -179,7 +186,15 @@ public class AddFieldsOperation extends DocumentEnhancingOperation { * @return new instance of {@link AddFieldsOperation}. */ AddFieldsOperationBuilder withValueOf(Object value); - } + /** + * Adds a generic projection for the current field. + * + * @param operation the operation key, e.g. {@code $add}. + * @param values the values to be set for the projection operation. + * @return new instance of {@link AddFieldsOperation}. + */ + AddFieldsOperationBuilder withValueOfExpression(String operation, Object... values); + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java index 44376d6e8..efcb26f24 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java @@ -119,7 +119,7 @@ public class Aggregation { /** * Creates a new {@link AggregationUpdate} from the given {@link AggregationOperation}s. - * + * * @param operations can be {@literal empty} but must not be {@literal null}. * @return new instance of {@link AggregationUpdate}. * @since 3.0 @@ -202,11 +202,16 @@ public class Aggregation { Assert.notNull(aggregationOperations, "AggregationOperations must not be null!"); Assert.notNull(options, "AggregationOptions must not be null!"); - // check $out is the last operation if it exists + // check $out/$merge is the last operation if it exists for (AggregationOperation aggregationOperation : aggregationOperations) { + if (aggregationOperation instanceof OutOperation && !isLast(aggregationOperation, aggregationOperations)) { throw new IllegalArgumentException("The $out operator must be the last stage in the pipeline."); } + + if (aggregationOperation instanceof MergeOperation && !isLast(aggregationOperation, aggregationOperations)) { + throw new IllegalArgumentException("The $merge operator must be the last stage in the pipeline."); + } } this.operations = aggregationOperations; @@ -236,6 +241,20 @@ public class Aggregation { return "_id"; } + /** + * Obtain an {@link AddFieldsOperationBuilder builder} instance to create a new {@link AddFieldsOperation}. + *

+ * Starting in version 4.2, MongoDB adds a new aggregation pipeline stage {@link AggregationUpdate#set $set} that is + * an alias for {@code $addFields}. + * + * @return new instance of {@link AddFieldsOperationBuilder}. + * @see AddFieldsOperation + * @since 3.0 + */ + public static AddFieldsOperationBuilder addFields() { + return AddFieldsOperation.builder(); + } + /** * Creates a new {@link ProjectionOperation} including the given fields. * @@ -495,6 +514,30 @@ public class Aggregation { return new MatchOperation(criteria); } + /** + * Creates a new {@link GeoNearOperation} instance from the given {@link NearQuery} and the {@code distanceField}. The + * {@code distanceField} defines output field that contains the calculated distance. + * + * @param query must not be {@literal null}. + * @param distanceField must not be {@literal null} or empty. + * @return + * @since 1.7 + */ + public static GeoNearOperation geoNear(NearQuery query, String distanceField) { + return new GeoNearOperation(query, distanceField); + } + + /** + * Obtain a {@link MergeOperationBuilder builder} instance to create a new {@link MergeOperation}. + * + * @return new instance of {@link MergeOperationBuilder}. + * @see MergeOperation + * @since 3.0 + */ + public static MergeOperationBuilder merge() { + return MergeOperation.builder(); + } + /** * Creates a new {@link OutOperation} using the given collection name. This operation must be the last operation in * the pipeline. @@ -636,41 +679,6 @@ public class Aggregation { return Fields.from(field(name, target)); } - /** - * Creates a new {@link GeoNearOperation} instance from the given {@link NearQuery} and the {@code distanceField}. The - * {@code distanceField} defines output field that contains the calculated distance. - * - * @param query must not be {@literal null}. - * @param distanceField must not be {@literal null} or empty. - * @return - * @since 1.7 - */ - public static GeoNearOperation geoNear(NearQuery query, String distanceField) { - return new GeoNearOperation(query, distanceField); - } - - /** - * Obtain a {@link MergeOperationBuilder builder} instance to create a new {@link MergeOperation}. - * - * @return new instance of {@link MergeOperationBuilder}. - * @see MergeOperation - * @since 3.0 - */ - public static MergeOperationBuilder merge() { - return MergeOperation.builder(); - } - - /** - * Obtain an {@link AddFieldsOperationBuilder builder} instance to create a new {@link AddFieldsOperation}. - * - * @return new instance of {@link AddFieldsOperationBuilder}. - * @see AddFieldsOperation - * @since 3.0 - */ - public static AddFieldsOperationBuilder addFields() { - return AddFieldsOperation.builder(); - } - /** * Returns a new {@link AggregationOptions.Builder}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java index 5866f2a1e..c97ca3974 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java @@ -79,7 +79,7 @@ import org.springframework.util.Assert; public class AggregationUpdate extends Aggregation implements UpdateDefinition { private boolean isolated = false; - private Set keysTouched = new HashSet<>(); + private final Set keysTouched = new HashSet<>(); /** * Create new {@link AggregationUpdate}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java index 9c078de59..1562bce70 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java @@ -24,16 +24,18 @@ import java.util.stream.Collectors; import org.bson.Document; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; +import org.springframework.util.Assert; /** - * Base class for common taks required by {@link SetOperation} and {@link AddFieldsOperation}. + * Base class for common tasks required by {@link SetOperation} and {@link AddFieldsOperation}. * * @author Christoph Strobl * @since 3.0 */ abstract class DocumentEnhancingOperation implements InheritsFieldsAggregationOperation { - private Map valueMap; + private final Map valueMap; + private ExposedFields exposedFields = ExposedFields.empty(); protected DocumentEnhancingOperation(Map source) { @@ -112,14 +114,53 @@ abstract class DocumentEnhancingOperation implements InheritsFieldsAggregationOp if (value instanceof Field) { return context.getReference((Field) value).toString(); } + + if (value instanceof ExpressionProjection) { + return ((ExpressionProjection) value).toExpression(context); + } + if (value instanceof AggregationExpression) { return ((AggregationExpression) value).toDocument(context); } + if (value instanceof Collection) { - return ((Collection) value).stream().map(it -> computeValue(it, context)).collect(Collectors.toList()); + return ((Collection) value).stream().map(it -> computeValue(it, context)).collect(Collectors.toList()); } return value; } + /** + * A {@link AggregationExpression} based on a SpEL expression. + * + * @author Mark Paluch + */ + static class ExpressionProjection { + + private static final SpelExpressionTransformer TRANSFORMER = new SpelExpressionTransformer(); + + private final String expression; + private final Object[] params; + + /** + * Creates a new {@link ProjectionOperation.ExpressionProjectionOperationBuilder.ExpressionProjection} for the given + * field, SpEL expression and parameters. + * + * @param expression must not be {@literal null} or empty. + * @param parameters must not be {@literal null}. + */ + ExpressionProjection(String expression, Object[] parameters) { + + Assert.notNull(expression, "Expression must not be null!"); + Assert.notNull(parameters, "Parameters must not be null!"); + + this.expression = expression; + this.params = parameters.clone(); + } + + Object toExpression(AggregationOperationContext context) { + return TRANSFORMER.transform(expression, context, params); + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java index 188f9d72d..cae54d394 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java @@ -33,8 +33,8 @@ import org.springframework.util.StringUtils; /** * Encapsulates the {@code $merge}-operation. *

- * We recommend to use the {@link MergeOperationBuilder builder} via {@link MergeOperation#builder()} instead of creating - * instances of this class directly. + * We recommend to use the {@link MergeOperationBuilder builder} via {@link MergeOperation#builder()} instead of + * creating instances of this class directly. * * @see MongoDB Documentation * @author Christoph Strobl @@ -42,15 +42,15 @@ import org.springframework.util.StringUtils; */ public class MergeOperation implements FieldsExposingAggregationOperation, InheritsFieldsAggregationOperation { - private MergeOperationTarget into; - private UniqueMergeId on; - private @Nullable Let let; - private @Nullable WhenDocumentsMatch whenMatched; - private @Nullable WhenDocumentsDontMatch whenNotMatched; + private final MergeOperationTarget into; + private final UniqueMergeId on; + private final @Nullable Let let; + private final @Nullable WhenDocumentsMatch whenMatched; + private final @Nullable WhenDocumentsDontMatch whenNotMatched; /** * Create new instance of {@link MergeOperation}. - * + * * @param into the target (collection and database) * @param on the unique identifier. Can be {@literal null}. * @param let exposed variables for {@link WhenDocumentsMatch#updateWith(Aggregation)}. Can be {@literal null}. @@ -63,7 +63,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher @Nullable WhenDocumentsMatch whenMatched, @Nullable WhenDocumentsDontMatch whenNotMatched) { Assert.notNull(into, "Into must not be null! Please provide a target collection."); - Assert.notNull(on, "On must not be null! Use Collections.emptySet() instead."); + Assert.notNull(on, "On must not be null! Use UniqueMergeId.id() instead."); this.into = into; this.on = on; @@ -93,7 +93,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher } /* - * (non-Javadoc) + * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.Aggregation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ @Override @@ -109,15 +109,17 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher if (!on.isJustIdField()) { $merge.putAll(on.toDocument(context)); } + if (let != null) { $merge.append("let", let.toDocument(context).get("$let", Document.class).get("vars")); } + if (whenMatched != null) { $merge.putAll(whenMatched.toDocument(context)); } + if (whenNotMatched != null) { $merge.putAll(whenNotMatched.toDocument(context)); - } return new Document("$merge", $merge); @@ -159,13 +161,12 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher * collection. * * @author Christoph Strobl - * @since 2.3 */ public static class UniqueMergeId { private static final UniqueMergeId ID = new UniqueMergeId(Collections.emptyList()); - private Collection uniqueIdentifier; + private final Collection uniqueIdentifier; private UniqueMergeId(Collection uniqueIdentifier) { this.uniqueIdentifier = uniqueIdentifier; @@ -173,6 +174,8 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher public static UniqueMergeId ofIdFields(String... fields) { + Assert.noNullElements(fields, "Fields must not contain null values!"); + if (ObjectUtils.isEmpty(fields)) { return id(); } @@ -182,7 +185,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher /** * Merge Documents by using the MongoDB {@literal _id} field. - * + * * @return */ public static UniqueMergeId id() { @@ -206,19 +209,19 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher * If not stated explicitly via {@link MergeOperationTarget#inDatabase(String)} the {@literal collection} is created * in the very same {@literal database}. In this case {@code into} is just a single String holding the collection * name.
- * + * *

 	 *     into: "target-collection-name"
 	 * 
- * + * * If the collection needs to be in a different database {@code into} will be a {@link Document} like the following - * + * *
 	 * {
 	 * 	into: {}
 	 * }
 	 * 
- * + * * @author Christoph Strobl * @since 2.3 */ @@ -267,7 +270,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher /** * Value Object specifying how to deal with a result document that matches an existing document in the collection * based on the fields of the {@code on} property describing the unique identifier. - * + * * @author Christoph Strobl * @since 2.3 */ @@ -354,7 +357,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher /** * Value Object specifying how to deal with a result document that do not match an existing document in the collection * based on the fields of the {@code on} property describing the unique identifier. - * + * * @author Christoph Strobl * @since 2.3 */ @@ -363,9 +366,18 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher private final String value; private WhenDocumentsDontMatch(String value) { + + Assert.notNull(value, "Value must not be null!"); + this.value = value; } + /** + * Factory method creating {@link WhenDocumentsDontMatch} from a {@code value} literal. + * + * @param value + * @return new instance of {@link WhenDocumentsDontMatch}. + */ public static WhenDocumentsDontMatch whenNotMatchedOf(String value) { return new WhenDocumentsDontMatch(value); } @@ -412,7 +424,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher private String collection; private @Nullable String database; - private @Nullable UniqueMergeId id; + private UniqueMergeId id = UniqueMergeId.id(); private @Nullable Let let; private @Nullable WhenDocumentsMatch whenMatched; private @Nullable WhenDocumentsDontMatch whenNotMatched; @@ -447,7 +459,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher /** * Define the target to store results in. - * + * * @param into must not be {@literal null}. * @return this. */ @@ -484,7 +496,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher /** * Set the identifier that determines if a results document matches an already existing document in the output * collection. - * + * * @param id must not be {@literal null}. * @return this. */ @@ -497,7 +509,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher /** * Expose the variables defined by {@link Let} to the {@link WhenDocumentsMatch#updateWith(Aggregation) update * aggregation}. - * + * * @param let the variable expressions * @return this. */ @@ -520,7 +532,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher /** * The action to take place when documents already exist in the target collection. - * + * * @param whenMatched must not be {@literal null}. * @return this. */ @@ -576,8 +588,7 @@ public class MergeOperation implements FieldsExposingAggregationOperation, Inher * @return new instance of {@link MergeOperation}. */ public MergeOperation build() { - return new MergeOperation(new MergeOperationTarget(database, collection), id != null ? id : UniqueMergeId.id(), - let, whenMatched, whenNotMatched); + return new MergeOperation(new MergeOperationTarget(database, collection), id, let, whenMatched, whenNotMatched); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java index 5516ece3d..b9f43d5d3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java @@ -25,7 +25,7 @@ import org.springframework.lang.Nullable; /** * Adds new fields to documents. {@code $set} outputs documents that contain all existing fields from the input * documents and newly added fields. - * + * *
  * SetOperation.set("totalHomework").toValue("A+").and().set("totalQuiz").toValue("B-")
  * 
@@ -143,6 +143,13 @@ public class SetOperation extends DocumentEnhancingOperation { valueMap.put(field, value instanceof String ? Fields.fields((String) value) : value); return FieldAppender.this.build(); } + + @Override + public SetOperation withValueOfExpression(String operation, Object... values) { + + valueMap.put(field, new ExpressionProjection(operation, values)); + return FieldAppender.this.build(); + } }; } @@ -152,6 +159,7 @@ public class SetOperation extends DocumentEnhancingOperation { /** * @author Christoph Strobl + * @author Mark Paluch * @since 3.0 */ public interface ValueAppender { @@ -171,6 +179,15 @@ public class SetOperation extends DocumentEnhancingOperation { * @return new instance of {@link SetOperation}. */ SetOperation toValueOf(Object value); + + /** + * Adds a generic projection for the current field. + * + * @param operation the operation key, e.g. {@code $add}. + * @param values the values to be set for the projection operation. + * @return new instance of {@link SetOperation}. + */ + SetOperation withValueOfExpression(String operation, Object... values); } } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java index 829a1ba8a..8a6c0b0f0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java @@ -29,7 +29,10 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.lang.Nullable; /** + * Unit tests for {@link AddFieldsOperation}. + * * @author Christoph Strobl + * @author Mark Paluch */ class AddFieldsOperationUnitTests { @@ -92,6 +95,17 @@ class AddFieldsOperationUnitTests { "{\"$addFields\" : {\"scoresWithMappedField.student_name\":\"$scoresWithMappedField.home_work\"}}")); } + @Test // DATAMONGO-2363 + void appliesSpelExpressionCorrectly() { + + AddFieldsOperation operation = AddFieldsOperation.builder().addField("totalHomework") + .withValueOfExpression("sum(homework) * [0]", 2) // + .build(); + + assertThat(operation.toPipelineStages(contextFor(ScoresWrapper.class))).contains( + Document.parse("{\"$addFields\" : {\"totalHomework\": { $multiply : [{ \"$sum\" : [\"$homework\"] }, 2] }}}")); + } + @Test // DATAMONGO-2363 void rendersTargetValueExpressionCorrectly() { @@ -105,10 +119,11 @@ class AddFieldsOperationUnitTests { ExposedFields fields = AddFieldsOperation.builder().addField("totalHomework").withValue("A+") // .addField("totalQuiz").withValue("B-") // - .build().getFields(); + .addField("computed").withValueOfExpression("totalHomework").build().getFields(); assertThat(fields.getField("totalHomework")).isNotNull(); assertThat(fields.getField("totalQuiz")).isNotNull(); + assertThat(fields.getField("computed")).isNotNull(); assertThat(fields.getField("does-not-exist")).isNull(); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java index 509dfc48d..60a52fe7a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java @@ -33,6 +33,8 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.lang.Nullable; /** + * Unit tests for {@link MergeOperation}. + * * @author Christoph Strobl */ class MergeOperationUnitTests { @@ -98,7 +100,7 @@ class MergeOperationUnitTests { } @Test // DATAMONGO-2363 - public void mapsFieldNames() { + void mapsFieldNames() { assertThat(merge().intoCollection("newrestaurants").on("date", "postCode").build() .toDocument(contextFor(Restaurant.class))).isEqualTo( diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java index a3c3203fa..f0162333d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java @@ -32,30 +32,31 @@ import org.springframework.lang.Nullable; * Unit tests for {@link SetOperation}. * * @author Christoph Strobl + * @author Mark Paluch */ -public class SetOperationUnitTests { +class SetOperationUnitTests { @Test // DATAMONGO-2331 - public void raisesErrorOnNullField() { + void raisesErrorOnNullField() { assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new SetOperation(null, "value")); } @Test // DATAMONGO-2331 - public void rendersFieldReferenceCorrectly() { + void rendersFieldReferenceCorrectly() { assertThat(new SetOperation("name", "value").toPipelineStages(contextFor(Scores.class))) .containsExactly(Document.parse("{\"$set\" : {\"name\":\"value\"}}")); } @Test // DATAMONGO-2331 - public void rendersMappedFieldReferenceCorrectly() { + void rendersMappedFieldReferenceCorrectly() { assertThat(new SetOperation("student", "value").toPipelineStages(contextFor(ScoresWithMappedField.class))) .containsExactly(Document.parse("{\"$set\" : {\"student_name\":\"value\"}}")); } @Test // DATAMONGO-2331 - public void rendersNestedMappedFieldReferenceCorrectly() { + void rendersNestedMappedFieldReferenceCorrectly() { assertThat( new SetOperation("scoresWithMappedField.student", "value").toPipelineStages(contextFor(ScoresWrapper.class))) @@ -63,14 +64,14 @@ public class SetOperationUnitTests { } @Test // DATAMONGO-2331 - public void rendersTargetValueFieldReferenceCorrectly() { + void rendersTargetValueFieldReferenceCorrectly() { assertThat(new SetOperation("name", Fields.field("value")).toPipelineStages(contextFor(Scores.class))) .containsExactly(Document.parse("{\"$set\" : {\"name\":\"$value\"}}")); } @Test // DATAMONGO-2331 - public void rendersMappedTargetValueFieldReferenceCorrectly() { + void rendersMappedTargetValueFieldReferenceCorrectly() { assertThat( new SetOperation("student", Fields.field("homework")).toPipelineStages(contextFor(ScoresWithMappedField.class))) @@ -78,7 +79,7 @@ public class SetOperationUnitTests { } @Test // DATAMONGO-2331 - public void rendersNestedMappedTargetValueFieldReferenceCorrectly() { + void rendersNestedMappedTargetValueFieldReferenceCorrectly() { assertThat(new SetOperation("scoresWithMappedField.student", Fields.field("scoresWithMappedField.homework")) .toPipelineStages(contextFor(ScoresWrapper.class))) @@ -86,8 +87,18 @@ public class SetOperationUnitTests { .parse("{\"$set\" : {\"scoresWithMappedField.student_name\":\"$scoresWithMappedField.home_work\"}}")); } + @Test // DATAMONGO-2363 + void appliesSpelExpressionCorrectly() { + + SetOperation operation = SetOperation.builder().set("totalHomework").withValueOfExpression("sum(homework) * [0]", + 2); + + assertThat(operation.toPipelineStages(contextFor(AddFieldsOperationUnitTests.ScoresWrapper.class))).contains( + Document.parse("{\"$set\" : {\"totalHomework\": { $multiply : [{ \"$sum\" : [\"$homework\"] }, 2] }}}")); + } + @Test // DATAMONGO-2331 - public void rendersTargetValueExpressionCorrectly() { + void rendersTargetValueExpressionCorrectly() { assertThat(SetOperation.builder().set("totalHomework").toValueOf(ArithmeticOperators.valueOf("homework").sum()) .toPipelineStages(contextFor(Scores.class))) @@ -95,7 +106,7 @@ public class SetOperationUnitTests { } @Test // DATAMONGO-2331 - public void exposesFieldsCorrectly() { + void exposesFieldsCorrectly() { ExposedFields fields = SetOperation.builder().set("totalHomework").toValue("A+") // .and() // @@ -138,5 +149,4 @@ public class SetOperationUnitTests { Scores scores; ScoresWithMappedField scoresWithMappedField; } - }