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: #4242pull/4486/head
7 changed files with 295 additions and 28 deletions
@ -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 @@ |
|||||||
|
/* |
||||||
|
* 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