Browse Source
This commit introduces a new AggregationVariable type that is intended to better identify variables within a pipeline to avoid mapping failures caused by invalid field names. Closes #4070 Original pull request: #42424.1.x
7 changed files with 295 additions and 28 deletions
@ -0,0 +1,130 @@
@@ -0,0 +1,130 @@
|
||||
/* |
||||
* Copyright 2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.mongodb.core.aggregation; |
||||
|
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ObjectUtils; |
||||
|
||||
/** |
||||
* A special field that points to a variable {@code $$} expression. |
||||
* |
||||
* @author Christoph Strobl |
||||
* @since 4.1 |
||||
*/ |
||||
public interface AggregationVariable extends Field { |
||||
|
||||
String PREFIX = "$$"; |
||||
|
||||
/** |
||||
* @return {@literal true} if the fields {@link #getName() name} does not match the defined {@link #getTarget() |
||||
* target}. |
||||
*/ |
||||
default boolean isAliased() { |
||||
return !ObjectUtils.nullSafeEquals(getName(), getTarget()); |
||||
} |
||||
|
||||
@Override |
||||
default String getName() { |
||||
return getTarget(); |
||||
} |
||||
|
||||
default boolean isInternal() { |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@link AggregationVariable} for the given name. |
||||
* <p> |
||||
* Variables start with {@code $$}. If not, the given value gets prefixed with {@code $$}. |
||||
* |
||||
* @param value must not be {@literal null}. |
||||
* @return new instance of {@link AggregationVariable}. |
||||
* @throws IllegalArgumentException if given value is {@literal null}. |
||||
*/ |
||||
static AggregationVariable variable(String value) { |
||||
|
||||
Assert.notNull(value, "Value must not be null"); |
||||
return new AggregationVariable() { |
||||
|
||||
private final String val = AggregationVariable.prefixVariable(value); |
||||
|
||||
@Override |
||||
public String getTarget() { |
||||
return val; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@link #isInternal() local} {@link AggregationVariable} for the given name. |
||||
* <p> |
||||
* Variables start with {@code $$}. If not, the given value gets prefixed with {@code $$}. |
||||
* |
||||
* @param value must not be {@literal null}. |
||||
* @return new instance of {@link AggregationVariable}. |
||||
* @throws IllegalArgumentException if given value is {@literal null}. |
||||
*/ |
||||
static AggregationVariable localVariable(String value) { |
||||
|
||||
Assert.notNull(value, "Value must not be null"); |
||||
return new AggregationVariable() { |
||||
|
||||
private final String val = AggregationVariable.prefixVariable(value); |
||||
|
||||
@Override |
||||
public String getTarget() { |
||||
return val; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isInternal() { |
||||
return true; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Check if the given field name reference may be variable. |
||||
* |
||||
* @param fieldRef can be {@literal null}. |
||||
* @return true if given value matches the variable identification pattern. |
||||
*/ |
||||
static boolean isVariable(@Nullable String fieldRef) { |
||||
return fieldRef != null && fieldRef.stripLeading().matches("^\\$\\$\\w.*"); |
||||
} |
||||
|
||||
/** |
||||
* Check if the given field may be variable. |
||||
* |
||||
* @param field can be {@literal null}. |
||||
* @return true if given {@link Field field} is an {@link AggregationVariable} or if its value is a |
||||
* {@link #isVariable(String) variable}. |
||||
*/ |
||||
static boolean isVariable(Field field) { |
||||
|
||||
if (field instanceof AggregationVariable) { |
||||
return true; |
||||
} |
||||
return isVariable(field.getTarget()); |
||||
} |
||||
|
||||
private static String prefixVariable(String variable) { |
||||
|
||||
var trimmed = variable.stripLeading(); |
||||
return trimmed.startsWith(PREFIX) ? trimmed : (PREFIX + trimmed); |
||||
} |
||||
} |
||||
@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
|
||||
/* |
||||
* Copyright 2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.mongodb.core.aggregation; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.Mockito; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
*/ |
||||
class AggregationVariableUnitTests { |
||||
|
||||
@Test // GH-4070
|
||||
void variableErrorsOnNullValue() { |
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> AggregationVariable.variable(null)); |
||||
} |
||||
|
||||
@Test // GH-4070
|
||||
void createsVariable() { |
||||
|
||||
var variable = AggregationVariable.variable("$$now"); |
||||
|
||||
assertThat(variable.getTarget()).isEqualTo("$$now"); |
||||
assertThat(variable.isInternal()).isFalse(); |
||||
} |
||||
|
||||
@Test // GH-4070
|
||||
void prefixesVariableIfNeeded() { |
||||
|
||||
var variable = AggregationVariable.variable("this"); |
||||
|
||||
assertThat(variable.getTarget()).isEqualTo("$$this"); |
||||
} |
||||
|
||||
@Test // GH-4070
|
||||
void localVariableErrorsOnNullValue() { |
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> AggregationVariable.localVariable(null)); |
||||
} |
||||
|
||||
@Test // GH-4070
|
||||
void localVariable() { |
||||
|
||||
var variable = AggregationVariable.localVariable("$$this"); |
||||
|
||||
assertThat(variable.getTarget()).isEqualTo("$$this"); |
||||
assertThat(variable.isInternal()).isTrue(); |
||||
} |
||||
|
||||
@Test // GH-4070
|
||||
void prefixesLocalVariableIfNeeded() { |
||||
|
||||
var variable = AggregationVariable.localVariable("this"); |
||||
|
||||
assertThat(variable.getTarget()).isEqualTo("$$this"); |
||||
} |
||||
|
||||
@Test // GH-4070
|
||||
void isVariableReturnsTrueForAggregationVariableTypes() { |
||||
|
||||
var variable = Mockito.mock(AggregationVariable.class); |
||||
|
||||
assertThat(AggregationVariable.isVariable(variable)).isTrue(); |
||||
} |
||||
|
||||
@Test // GH-4070
|
||||
void isVariableReturnsTrueForFieldThatTargetsVariable() { |
||||
|
||||
var variable = Fields.field("value", "$$this"); |
||||
|
||||
assertThat(AggregationVariable.isVariable(variable)).isTrue(); |
||||
} |
||||
|
||||
@Test // GH-4070
|
||||
void isVariableReturnsFalseForFieldThatDontTargetsVariable() { |
||||
|
||||
var variable = Fields.field("value", "$this"); |
||||
|
||||
assertThat(AggregationVariable.isVariable(variable)).isFalse(); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue