Compare commits

..

3 Commits

Author SHA1 Message Date
c27525a637 test: 修改测试用例以覆盖 BaseValidator#ruleFor 2025-06-01 08:48:23 +08:00
3ef2ebac2f feat: 添加二元组属性校验支持
- 新增 `PairPropertyValidator` 类,用于校验二元组属性
- 在 `BaseValidator` 和 `MapValidator` 中添加对二元组校验器的支持
- 添加相关单元测试
2025-06-01 08:46:13 +08:00
8be8be8f17 chore: 更新 README 中的示例 2025-06-01 06:48:30 +08:00
7 changed files with 267 additions and 20 deletions

View File

@@ -57,11 +57,12 @@ class CustomerMapValidator extends MapValidator<String, Object> {
private static final CustomerMapValidator INSTANCE = new CustomerMapValidator();
private static final String[] FIELD_NAMES = {
private static final String[] FIELD_NAMES = {
"name", "emailAddress", "vipLevel", "customerId", "birthday", "address"
};
private CustomerMapValidator() {
// validateAndCopy() 时默认保留的 key
super(FIELD_NAMES);
ruleForString("name").notBlank("姓名不能为空");
@@ -92,7 +93,7 @@ class CustomerMapValidator extends MapValidator<String, Object> {
使用:
```java
public void foo(Map<String, Object> customer) {
CustomerMapValidator.getInstance().validate(customer);
Map<String, Object> validatedCustomer = CustomerMapValidator.getInstance().validateAndCopy(customer);
// ...
}
```

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

@@ -46,21 +46,21 @@ public class ObjectPropertyValidatorTests {
void notNull_validInput() {
IValidator<ExampleCommand> validator = new BaseValidator<ExampleCommand>() {
{
ruleForBool(ExampleCommand::getBoolProperty)
ruleFor(ExampleCommand::getBoolProperty)
.notNull();
ruleForInt(ExampleCommand::getIntProperty)
ruleFor(ExampleCommand::getIntProperty)
.notNull("The intProperty cannot be null");
ruleForLong(ExampleCommand::getLongProperty)
ruleFor(ExampleCommand::getLongProperty)
.notNull(() -> ExampleException.withMessage("The longProperty cannot be null"));
ruleForDouble(ExampleCommand::getDoubleProperty)
ruleFor(ExampleCommand::getDoubleProperty)
.notNull(d -> ExampleException.withMessage("The doubleProperty cannot be null, but it was %s", d));
ruleForString(ExampleCommand::getStringProperty)
ruleFor(ExampleCommand::getStringProperty)
.notNull();
ruleForComparable(ExampleCommand::getDateTimeProperty)
ruleFor(ExampleCommand::getDateTimeProperty)
.notNull("The dateTimeProperty cannot be null");
ruleFor(ExampleCommand::getObjectProperty)
.notNull(() -> ExampleException.withMessage("The objectProperty cannot be null"));
ruleForCollection(ExampleCommand::getStringListProperty)
ruleFor(ExampleCommand::getStringListProperty)
.notNull(d -> ExampleException.withMessage("The stringListProperty cannot be null, but it was %s", d));
}
};
@@ -134,21 +134,21 @@ public class ObjectPropertyValidatorTests {
void isNull_validInput() {
IValidator<ExampleCommand> validator = new BaseValidator<ExampleCommand>() {
{
ruleForBool(ExampleCommand::getBoolProperty)
ruleFor(ExampleCommand::getBoolProperty)
.isNull();
ruleForInt(ExampleCommand::getIntProperty)
ruleFor(ExampleCommand::getIntProperty)
.isNull("The intProperty should be null");
ruleForLong(ExampleCommand::getLongProperty)
ruleFor(ExampleCommand::getLongProperty)
.isNull(() -> ExampleException.withMessage("The longProperty should be null"));
ruleForDouble(ExampleCommand::getDoubleProperty)
ruleFor(ExampleCommand::getDoubleProperty)
.isNull(d -> ExampleException.withMessage("The doubleProperty should be null, but it was %s", d));
ruleForString(ExampleCommand::getStringProperty)
ruleFor(ExampleCommand::getStringProperty)
.isNull();
ruleForComparable(ExampleCommand::getDateTimeProperty)
ruleFor(ExampleCommand::getDateTimeProperty)
.isNull("The dateTimeProperty should be null");
ruleFor(ExampleCommand::getObjectProperty)
.isNull(() -> ExampleException.withMessage("The objectProperty should be null"));
ruleForCollection(ExampleCommand::getStringListProperty)
ruleFor(ExampleCommand::getStringListProperty)
.isNull(d -> ExampleException.withMessage("The stringListProperty should be null, but it was %s", d));
}
};

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() {