I'm experimenting with some variations of validation pattern. I've created Validator class:
public class Validator<O> {
private O toValidate;
private ValidationResult error;
private Validator(O object) {
this(object, null);
}
private Validator(O object, ValidationResult existingError) {
toValidate = object;
error = existingError;
}
public static <O> Validator<O> toFirstError(O object) {
return new Validator<>(object);
}
private static <O> Validator<O> toFirstError(O object, ValidationResult existingError) {
return new Validator<>(object, existingError);
}
public Validator<O> must(Function<O, ValidationResult> predicate) {
if (error != null) {
return this;
}
var validationResult = predicate.apply(toValidate);
validationResult.ifInvalid(e -> error = e);
return this;
}
public Validator<O> ifValid(Consumer<O> consumer) {
if (error == null) {
consumer.accept(toValidate);
}
return this;
}
public <N> Validator<N> mapIfValid(Function<O, N> mapFun) {
if (error == null) {
var newObject = mapFun.apply(toValidate);
return toFirstError(newObject, error);
}
return toFirstError(null, error);
}
public ValidationResult getFirstError() {
if (error == null) {
return ValidationResult.valid();
}
return error;
}
public CommandResult<O> toCommandResult() {
if (error == null) {
return CommandResult.success(toValidate);
}
return CommandResult.error(error.getErrorCode());
}
}
ValidationResult
is pretty simple
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidationResult {
@Getter
private final ErrorCode errorCode;
public static ValidationResult valid() {
return new ValidationResult(null);
}
public static ValidationResult error(ErrorCode errorCode) {
return new ValidationResult(errorCode);
}
public boolean isInvalid() {
return errorCode != null;
}
public boolean isValid() {
return errorCode == null;
}
public void ifInvalid(Consumer<ValidationResult> consumer) {
if (isInvalid()) {
consumer.accept(this);
}
}
@Override
public String toString() {
if (isValid()) {
return "VALID";
} else {
return "INVALID[errorCode=%s]".formatted(errorCode);
}
}
}
Next, I have some command handler. And this is the example of how to use the validator:
@Override
public CommandResult<Profile> handle(CreateProfileCommand command) {
return Validator.toFirstError(command)
.must(this::userDoNotHaveProfile)
.mapIfValid(this::buildProfile)
.must(Profile::validate)
.ifValid(repository::save)
.toCommandResult();
}
private ValidationResult userDoNotHaveProfile(CreateProfileCommand command) {
var existForUser = repository.existByOwnerId(command.owner());
if (existForUser) {
return ValidationResult.error(USER_ALREADY_HAVE_PROFILE);
}
return ValidationResult.valid();
}
private Profile buildProfile(CreateProfileCommand command) {
var slug = createUniqueSlug(command.name());
return new Profile(
ProfileId.random(),
command.owner(),
command.name(),
slug,
Description.empty(),
clock.instant()
);
}
And some domain object with different usage of the validator:
public record Profile(
ProfileId id,
UserAccountId owner,
Name name,
ProfileSlug slug,
Description description,
Instant created
) {
public ValidationResult validate() {
return Validator.toFirstError(this)
.must(p -> p.name().validate())
.getFirstError();
}
}
public record Name(
String asString
) {
private static final int MIN_LEN = 2;
private static final int MAX_LEN = 200;
public static Name of(String name) {
return new Name(name);
}
public ProfileSlug createSlug() {
return ProfileSlug.of(this);
}
public ValidationResult validate() {
if (asString == null
|| asString.length() < MIN_LEN
|| asString.length() > MAX_LEN) {
return ValidationResult.error(PROFILE_NAME_INVALID_NAME_LENGTH);
}
return ValidationResult.valid();
}
}
This variation in general looks good for me, but the whole logic is inside validation class, and this look a little stupid for me. Maybe I'm reinventing the wheel and this method have its name, or maybe exist lib that implements this pattern?