Skip to content

Build a decent top level text message for ConstraintViolationException #116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
import io.avaje.validation.constraints.Valid;

@Valid
public record ACrew (@NotBlank(max = 4) String name, String role) {
public record ACrew (@NotBlank(max = 4) String name) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@Valid
public record DShip(
@NotBlank String name,
@NotBlank String rating,
@NotEmpty String[] crew // cascade validation
) {
}
116 changes: 116 additions & 0 deletions blackbox-test/src/test/java/example/avaje/ExceptionMessageTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package example.avaje;

import example.avaje.cascade.ACrew;
import example.avaje.cascade.BShip;
import example.avaje.cascade.DShip;
import io.avaje.validation.ConstraintViolationException;
import io.avaje.validation.Validator;
import org.junit.jupiter.api.Test;

import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;

class ExceptionMessageTest {

Validator validator = Validator.builder().addLocales(Locale.GERMAN).build();

@Test
void oneError() {
var ship = new DShip("", "ok", new String[]{"ok"});
try {
validator.validate(ship);
fail("don't get here");
} catch (ConstraintViolationException e) {
assertThat(e.violations()).hasSize(1);
assertThat(e).hasMessage("1 constraint violation(s) occurred.\n" +
" name: must not be blank");
}
}

@Test
void twoErrors() {
var ship = new DShip("", "", new String[]{"ok"});
try {
validator.validate(ship);
fail("don't get here");
} catch (ConstraintViolationException e) {
assertThat(e.violations()).hasSize(2);
assertThat(e).hasMessage("2 constraint violation(s) occurred.\n" +
" name: must not be blank\n" +
" rating: must not be blank");
}
}

@Test
void threeErrors() {
var ship = new DShip("", "", null);
try {
validator.validate(ship);
fail("don't get here");
} catch (ConstraintViolationException e) {
assertThat(e.violations()).hasSize(3);
assertThat(e).hasMessage("3 constraint violation(s) occurred.\n" +
" name: must not be blank\n" +
" rating: must not be blank\n" +
" crew: must not be empty");
}
}

@Test
void tenErrors() {
var ship = new BShip("", createBadCrew(9));
try {
validator.validate(ship);
fail("don't get here");
} catch (ConstraintViolationException e) {
assertThat(e.violations()).hasSize(10);
assertThat(e).hasMessage("10 constraint violation(s) occurred.\n" +
" name: must not be blank\n" +
" crew[0].name: maximum length 4 exceeded\n" +
" crew[1].name: maximum length 4 exceeded\n" +
" crew[2].name: maximum length 4 exceeded\n" +
" crew[3].name: maximum length 4 exceeded\n" +
" crew[4].name: maximum length 4 exceeded\n" +
" crew[5].name: maximum length 4 exceeded\n" +
" crew[6].name: maximum length 4 exceeded\n" +
" crew[7].name: maximum length 4 exceeded\n" +
" crew[8].name: maximum length 4 exceeded");
}
}

@Test
void elevenErrors() {
var ship = new BShip("", createBadCrew(10));
try {
validator.validate(ship);
fail("don't get here");
} catch (ConstraintViolationException e) {
assertThat(e.violations()).hasSize(11);
assertThat(e).hasMessage("11 constraint violation(s) occurred.\n" +
" name: must not be blank\n" +
" crew[0].name: maximum length 4 exceeded\n" +
" crew[1].name: maximum length 4 exceeded\n" +
" crew[2].name: maximum length 4 exceeded\n" +
" crew[3].name: maximum length 4 exceeded\n" +
" crew[4].name: maximum length 4 exceeded\n" +
" crew[5].name: maximum length 4 exceeded\n" +
" crew[6].name: maximum length 4 exceeded\n" +
" crew[7].name: maximum length 4 exceeded\n" +
" crew[8].name: maximum length 4 exceeded\n" +
" and 1 other error(s)");
}
}

private Set<ACrew> createBadCrew(int count) {
Set<ACrew> set = new LinkedHashSet<>();
for (int i = 0; i < count; i++) {
set.add(new ACrew("abcd" + i));
}
return set;
}

}
24 changes: 12 additions & 12 deletions blackbox-test/src/test/java/example/avaje/cascade/AShipTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,31 @@ class AShipTest {

@Test
void valid() {
var ship = new AShip("lollyPop", List.of(new ACrew("ok", null)));
var ship = new AShip("lollyPop", List.of(new ACrew("ok")));
validator.validate(ship);
}

@Test
void valid_usingSet() {
var ship = new BShip("lollyPop", Set.of(new ACrew("ok", null)));
var ship = new BShip("lollyPop", Set.of(new ACrew("ok")));
validator.validate(ship);
}

@Test
void valid_usingArray() {
var ship = new CShip("lollyPop", new ACrew[]{new ACrew("ok", null)});
var ship = new CShip("lollyPop", new ACrew[]{new ACrew("ok")});
validator.validate(ship);
}

@Test
void valid_usingArray3() {
var ship = new CShip3("lollyPop", new ACrew[]{new ACrew("ok", null)});
var ship = new CShip3("lollyPop", new ACrew[]{new ACrew("ok")});
validator.validate(ship);
}

@Test
void valid_usingScalarArray() {
var ship = new DShip("lollyPop", new String[]{"bob"});
var ship = new DShip("lollyPop", "ok", new String[]{"bob"});
validator.validate(ship);
}

Expand All @@ -61,19 +61,19 @@ void validAllowNullCollection() {

@Test
void valid2() {
var ship = new AShip2("lollyPop", List.of(new ACrew("ok", null)));
var ship = new AShip2("lollyPop", List.of(new ACrew("ok")));
validator.validate(ship);
}

@Test
void valid3_expect_noCascadeValidationToCrew() {
var ship = new AShip3("lollyPop", List.of(new ACrew("NotValid", null)));
var ship = new AShip3("lollyPop", List.of(new ACrew("NotValid")));
validator.validate(ship);
}

@Test
void invalid() {
var ship = new AShip("", List.of(new ACrew("NotValid", null)));
var ship = new AShip("", List.of(new ACrew("NotValid")));
List<ConstraintViolation> violations = violations(ship);

assertThat(violations).hasSize(2);
Expand Down Expand Up @@ -109,7 +109,7 @@ void invalidShip2_when_nullCollection_expect_InvalidEmptyCollection() {

@Test
void arrayCascade() {
var ship = new CShip("", new ACrew[]{new ACrew("NotValid", null)});
var ship = new CShip("", new ACrew[]{new ACrew("NotValid")});
List<ConstraintViolation> violations = violations(ship);

assertThat(violations).hasSize(2);
Expand All @@ -121,7 +121,7 @@ void arrayCascade() {

@Test
void arrayNotCascade() {
var ship = new CShip3("", new ACrew[]{new ACrew("NotValid", null)});
var ship = new CShip3("", new ACrew[]{new ACrew("NotValid")});
List<ConstraintViolation> violations = violations(ship);

assertThat(violations).hasSize(1);
Expand Down Expand Up @@ -153,7 +153,7 @@ void arrayNotEmpty_when_null() {

@Test
void arrayNotEmpty_when_scalarArrayEmpty() {
var ship = new DShip("", new String[]{});
var ship = new DShip("", "ok", new String[]{});
List<ConstraintViolation> violations = violations(ship);

assertThat(violations).hasSize(2);
Expand All @@ -165,7 +165,7 @@ void arrayNotEmpty_when_scalarArrayEmpty() {

@Test
void arrayNotEmpty_when_scalarNull() {
var ship = new DShip("ok", null);
var ship = new DShip("ok", "ok", null);
List<ConstraintViolation> violations = violations(ship);

assertThat(violations).hasSize(1);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package io.avaje.validation;

import java.io.Serial;
import java.util.List;
import java.util.Set;

/** Exception holding a set of constraint violations. */
public final class ConstraintViolationException extends RuntimeException {

@Serial
private static final long serialVersionUID = 1L;
private final transient Set<ConstraintViolation> violations;
private final transient List<Class<?>> groups;

/** Create with the given constraint violations */
public ConstraintViolationException(Set<ConstraintViolation> violations, List<Class<?>> groups) {
public ConstraintViolationException(String message, Set<ConstraintViolation> violations, List<Class<?>> groups) {
super(message);
this.violations = violations;
this.groups = groups;
}
Expand Down
15 changes: 14 additions & 1 deletion validator/src/main/java/io/avaje/validation/core/DRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,23 @@ public void popPath() {
@Override
public void throwWithViolations() {
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations, groups);
throw new ConstraintViolationException(message(), violations, groups);
}
}

private String message() {
final var msg = new StringBuilder(100);
msg.append(violations.size()).append(" constraint violation(s) occurred.");
violations.stream().limit(10).forEach(cv -> {
msg.append("\n ").append(cv.path()).append(": ").append(cv.message());
});
final int others = violations.size() - 10;
if (others > 0) {
msg.append("\n and ").append(others).append(" other error(s)");
}
return msg.toString();
}

@Override
public Set<ConstraintViolation> violations() {
return violations;
Expand Down