feat: 添加二元组属性校验支持

- 新增 `PairPropertyValidator` 类,用于校验二元组属性
- 在 `BaseValidator` 和 `MapValidator` 中添加对二元组校验器的支持
- 添加相关单元测试
This commit is contained in:
zhouxy108 2025-06-01 08:46:13 +08:00
parent 8be8be8f17
commit 3ef2ebac2f
5 changed files with 250 additions and 4 deletions

View File

@ -19,6 +19,7 @@ package xyz.zhouxy.plusone.validator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
@ -246,6 +247,19 @@ public abstract class BaseValidator<T> implements IValidator<T> {
return validator;
}
/**
* 添加一个针对二元组的校验器
* @param <V1> 第一个元素的类型
* @param <V2> 第二个元素的类型
* @param getter 获取属性值的函数
* @return 二元组校验器
*/
protected final <V1, V2> PairPropertyValidator<T, V1, V2> ruleForPair(Function<T, Entry<V1, V2>> getter) {
PairPropertyValidator<T, V1, V2> validator = new PairPropertyValidator<>(getter);
this.rules.add(validator::validate);
return validator;
}
/** {@inheritDoc} */
@Override
public void validate(T obj) {

View File

@ -16,9 +16,11 @@
package xyz.zhouxy.plusone.validator;
import java.util.AbstractMap.SimpleEntry;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -107,6 +109,7 @@ public abstract class MapValidator<K, V> extends BaseValidator<Map<K, V>> {
/**
* 添加一个属性校验器对指定 key 对应的 value 进行校验
*
* @param <T> 属性类型
* @param key key
* @return 属性校验器
*/
@ -191,6 +194,21 @@ public abstract class MapValidator<K, V> extends BaseValidator<Map<K, V>> {
return ruleForCollection(getter);
}
/**
* 添加一个属性校验器对指定的两个 key 对应的 value 进行校验
* @param <V1> 第一个属性的类型
* @param <V2> 第二个属性的类型
* @param k1 第一个 key
* @param k2 第二个 key
* @return 属性校验器
*/
protected final <V1 extends V, V2 extends V>
PairPropertyValidator<Map<K, V>, V1, V2> ruleForPair(K k1, K k2) {
@SuppressWarnings("unchecked")
Function<Map<K, V>, Entry<V1, V2>> getter = m -> new SimpleEntry<V1, V2>((V1) m.get(k1), (V2) m.get(k2));
return ruleForPair(getter);
}
// ================================
// #endregion - ruleFor
// ================================

View File

@ -0,0 +1,86 @@
/*
* Copyright 2025 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 xyz.zhouxy.plusone.validator;
import java.util.Map.Entry;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* 针对二元组的属性校验器
*
* @author ZhouXY
*/
public class PairPropertyValidator<T, V1, V2>
extends BasePropertyValidator<T, Entry<V1, V2>, PairPropertyValidator<T, V1, V2>> {
protected PairPropertyValidator(Function<T, ? extends Entry<V1, V2>> getter) {
super(getter);
}
/**
* 添加一条校验属性的规则校验二元组是否满足给定的条件
*
* @param condition 校验规则
* @return 属性校验器
*/
public final PairPropertyValidator<T, V1, V2> must(BiPredicate<V1, V2> condition) {
return must(pair -> condition.test(pair.getKey(), pair.getValue()));
}
/**
* 添加一条校验属性的规则校验二元组是否满足给定的条件
*
* @param condition 校验规则
* @param errMsg 错误信息
* @return 属性校验器
*/
public final PairPropertyValidator<T, V1, V2> must(BiPredicate<V1, V2> condition, String errMsg) {
return must(pair -> condition.test(pair.getKey(), pair.getValue()), errMsg);
}
/**
* 添加一条校验属性的规则校验二元组是否满足给定的条件
*
* @param condition 校验规则
* @param e 自定义异常
* @return 属性校验器
*/
public final <E extends RuntimeException> PairPropertyValidator<T, V1, V2> must(
BiPredicate<V1, V2> condition, Supplier<E> e) {
return must(pair -> condition.test(pair.getKey(), pair.getValue()), e);
}
/**
* 添加一条校验属性的规则校验二元组是否满足给定的条件
*
* @param condition 校验规则
* @param e 自定义异常
* @return 属性校验器
*/
public final <E extends RuntimeException> PairPropertyValidator<T, V1, V2> must(
BiPredicate<V1, V2> condition, BiFunction<V1, V2, E> e) {
return must(pair -> condition.test(pair.getKey(), pair.getValue()),
pair -> e.apply(pair.getKey(), pair.getValue()));
}
@Override
protected PairPropertyValidator<T, V1, V2> thisObject() {
return this;
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright 2025 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 xyz.zhouxy.plusone.example.validator;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Objects;
import java.util.AbstractMap.SimpleEntry;
import org.junit.jupiter.api.Test;
import xyz.zhouxy.plusone.ExampleException;
import xyz.zhouxy.plusone.example.ExampleCommand;
import xyz.zhouxy.plusone.validator.BaseValidator;
import xyz.zhouxy.plusone.validator.IValidator;
import xyz.zhouxy.plusone.validator.ValidationException;
public class PairPropertyValidatorTests {
static final String MESSAGE = "Validation failed.";
// ================================
// #region - must
// ================================
@Test
void must_validInput() {
IValidator<ExampleCommand> validator = new BaseValidator<ExampleCommand>() {
{
ruleForPair((ExampleCommand command) -> new SimpleEntry<String,Integer>(command.getStringProperty(), command.getIntProperty()))
.must((str, intValue) -> Objects.equals(str, intValue.toString()))
.must((str, intValue) -> Objects.equals(str, intValue.toString()), MESSAGE)
.must((str, intValue) -> Objects.equals(str, intValue.toString()), () -> ExampleException.withMessage(MESSAGE))
.must((str, intValue) -> Objects.equals(str, intValue.toString()),
(str, intValue) -> ExampleException.withMessage("Validation failed: ('%s', %d).", str, intValue));
}
};
ExampleCommand command = exampleCommandWithIntAndStringListProperty(100, "100");
assertDoesNotThrow(() -> validator.validate(command));
}
@Test
void must_default_invalidInput() {
IValidator<ExampleCommand> validator = new BaseValidator<ExampleCommand>() {
{
ruleForPair((ExampleCommand command) -> new SimpleEntry<String,Integer>(command.getStringProperty(), command.getIntProperty()))
.must((str, intValue) -> Objects.equals(str, intValue.toString()));
}
};
ExampleCommand command = exampleCommandWithIntAndStringListProperty(100, "");
ValidationException e = assertThrows(ValidationException.class, () -> validator.validate(command));
assertEquals("The specified condition was not met for the input.", e.getMessage());
}
@Test
void must_message_invalidInput() {
IValidator<ExampleCommand> validator = new BaseValidator<ExampleCommand>() {
{
ruleForPair((ExampleCommand command) -> new SimpleEntry<String,Integer>(command.getStringProperty(), command.getIntProperty()))
.must((str, intValue) -> Objects.equals(str, intValue.toString()), MESSAGE);
}
};
ExampleCommand command = exampleCommandWithIntAndStringListProperty(100, "");
ValidationException e = assertThrows(ValidationException.class, () -> validator.validate(command));
assertEquals(MESSAGE, e.getMessage());
}
@Test
void must_exceptionSupplier_invalidInput() {
IValidator<ExampleCommand> validator = new BaseValidator<ExampleCommand>() {
{
ruleForPair((ExampleCommand command) -> new SimpleEntry<String,Integer>(command.getStringProperty(), command.getIntProperty()))
.must((str, intValue) -> Objects.equals(str, intValue.toString()), () -> ExampleException.withMessage(MESSAGE));
}
};
ExampleCommand command = exampleCommandWithIntAndStringListProperty(100, "");
ExampleException e = assertThrows(ExampleException.class, () -> validator.validate(command));
assertEquals(MESSAGE, e.getMessage());
}
@Test
void must_exceptionFunction_invalidInput() {
IValidator<ExampleCommand> validator = new BaseValidator<ExampleCommand>() {
{
ruleForPair((ExampleCommand command) -> new SimpleEntry<String,Integer>(command.getStringProperty(), command.getIntProperty()))
.must((str, intValue) -> Objects.equals(str, intValue.toString()),
(str, intValue) -> ExampleException.withMessage("Validation failed: ('%s', %d).", str, intValue));
}
};
ExampleCommand command = exampleCommandWithIntAndStringListProperty(100, "");
ExampleException e = assertThrows(ExampleException.class, () -> validator.validate(command));
assertEquals("Validation failed: ('', 100).", e.getMessage());
}
// ================================
// #endregion - must
// ================================
static ExampleCommand exampleCommandWithIntAndStringListProperty(Integer intValue, String str) {
ExampleCommand exampleCommand = new ExampleCommand();
exampleCommand.setIntProperty(intValue);
exampleCommand.setStringProperty(str);
return exampleCommand;
}
}

View File

@ -22,7 +22,6 @@ import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.junit.jupiter.api.Test;
@ -126,9 +125,9 @@ class ParamsValidator extends MapValidator<String, Object> {
ruleForCollection(STRING_LIST_PROPERTY)
.notNull(d -> ExampleException.withMessage("The stringListProperty cannot be null, but it was %s", d));
// 校验到多个属性只能针对 map 本身进行校验
withRule(m -> Objects.equals(m.get(STRING_PROPERTY), m.get(STRING_PROPERTY2)),
"'stringProperty' must be equal to 'stringProperty2'.");
ruleForPair(STRING_PROPERTY, STRING_PROPERTY2)
.must((str1, str2) -> str1 != null && str1.equals(str2),
"'stringProperty' must be equal to 'stringProperty2'.");
}
public static Set<String> keySet() {