Skip to content

Add support for sharing instances of NotNull NotBlank adapters #277

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 2 commits into from
Jan 30, 2025
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 @@ -116,6 +116,7 @@ interface Message {
*/
String lookupkey();
}

/** Request to create a Validation Adapter. */
interface AdapterCreateRequest {

Expand All @@ -132,7 +133,7 @@ interface AdapterCreateRequest {
Map<String, Object> attributes();

/** Return the attribute for the given key. */
<T> T attribute(String key);
<T> T attribute(String key);

/** Return the message to use */
Message message();
Expand All @@ -143,7 +144,19 @@ interface AdapterCreateRequest {
/** Return the target type */
String targetType();

/** Return true if the groups is ONLY the default group */
boolean isDefaultGroupOnly();

/** Clone and return the request with a new value attribute */
AdapterCreateRequest withValue(long value);
}

/** Used to build default ValidationAdapters with the default group and message. */
interface RequestBuilder {

/**
* Build a default AdapterCreateRequest with the appropriate default message.
*/
AdapterCreateRequest defaultRequest(String defaultMessage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

import io.avaje.validation.adapter.ConstraintAdapter;
import io.avaje.validation.groups.Default;
import org.jspecify.annotations.Nullable;

import io.avaje.validation.adapter.ValidationAdapter;
import io.avaje.validation.adapter.ValidationContext;
import io.avaje.validation.core.adapters.BasicAdapters;
import io.avaje.validation.core.adapters.FuturePastAdapterFactory;
import io.avaje.validation.core.adapters.NumberAdapters;
import io.avaje.validation.groups.Default;
import io.avaje.validation.spi.AdapterFactory;
import io.avaje.validation.spi.AnnotationFactory;

Expand All @@ -41,7 +42,10 @@ final class CoreAdapterBuilder {
this.context = context;
this.factories.addAll(userFactories);
this.annotationFactories.addAll(userAnnotationFactories);
this.annotationFactories.add(BasicAdapters.FACTORY);
// bootstrap the builtin factories potentially with default adapters
// that use the default group and default message
var requestBuilder = new RequestBuilder(context);
this.annotationFactories.add(BasicAdapters.factory(requestBuilder));
this.annotationFactories.add(NumberAdapters.FACTORY);
this.annotationFactories.add(new FuturePastAdapterFactory(clockSupplier, temporalTolerance));
}
Expand Down Expand Up @@ -107,7 +111,22 @@ <T> ValidationAdapter<T> buildAnnotation(
return NoOpValidator.INSTANCE;
}

record Request(
private static final class RequestBuilder implements ValidationContext.RequestBuilder {

private final DValidator context;

private RequestBuilder(DValidator context) {
this.context = context;
}

@Override
public ValidationContext.AdapterCreateRequest defaultRequest(String defaultMessage) {
// ConstraintAdapter.class is just a placeholder and not meaningful
return new Request(context, ConstraintAdapter.class, DEFAULT_GROUP, Map.of("message", defaultMessage));
}
}

private record Request(

ValidationContext ctx,
Class<? extends Annotation> annotationType,
Expand All @@ -116,6 +135,11 @@ record Request(

) implements ValidationContext.AdapterCreateRequest {

@Override
public boolean isDefaultGroupOnly() {
return DEFAULT_GROUP.equals(groups);
}

@Override
public String targetType() {
return attribute("_type");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,75 @@
import io.avaje.validation.adapter.ValidationAdapter;
import io.avaje.validation.adapter.ValidationContext;
import io.avaje.validation.adapter.ValidationContext.AdapterCreateRequest;
import io.avaje.validation.adapter.ValidationContext.RequestBuilder;
import io.avaje.validation.adapter.ValidationRequest;
import io.avaje.validation.spi.AnnotationFactory;

public final class BasicAdapters {
private static final String LENGTH_MAX = "{avaje.Length.max.message}";
private static final String NOT_NULL_MESSAGE = "{avaje.NotNull.message}";
private static final String NULL_MESSAGE = "{avaje.Null.message}";
private static final String NOT_BLANK_MESSAGE = "{avaje.NotBlank.message}";

private BasicAdapters() {}

public static final AnnotationFactory FACTORY =
request ->
switch (request.annotationType().getSimpleName()) {
case "Email" -> new EmailAdapter(request);
case "UUID" -> new UuidAdapter(request);
case "URI" -> new UriAdapter(request);
case "Null" -> new NullableAdapter(request, true);
case "NotNull", "NonNull" -> new NullableAdapter(request, false);
case "AssertTrue" -> new AssertBooleanAdapter(request, true);
case "AssertFalse" -> new AssertBooleanAdapter(request, false);
case "NotBlank" -> new NotBlankAdapter(request);
case "NotEmpty" -> new NotEmptyAdapter(request);
case "Pattern" -> new PatternAdapter(request);
case "Size", "Length" -> new SizeAdapter(request);
case "Valid" -> new ValidAdapter(request);
default -> null;
};
public static AnnotationFactory factory(RequestBuilder requestBuilder) {
return new Factory(requestBuilder);
}

private static final class Factory implements AnnotationFactory {

private final NullableAdapter defaultNotNullAdapter;
private final NullableAdapter defaultNullAdapter;
private final NotBlankAdapter defaultNotBlankAdapter;

Factory(RequestBuilder reqBuilder) {
// create default adapters that will be shared instances (when no groups or message customisation)
this.defaultNotNullAdapter = new NullableAdapter(reqBuilder.defaultRequest(NOT_NULL_MESSAGE), false);
this.defaultNullAdapter = new NullableAdapter(reqBuilder.defaultRequest(NULL_MESSAGE), true);
this.defaultNotBlankAdapter = new NotBlankAdapter(reqBuilder.defaultRequest(NOT_BLANK_MESSAGE));
}

@Override
public ValidationAdapter<?> create(AdapterCreateRequest request) {
return switch (request.annotationType().getSimpleName()) {
case "Email" -> new EmailAdapter(request);
case "UUID" -> new UuidAdapter(request);
case "URI" -> new UriAdapter(request);
case "Null" -> nullable(request);
case "NotNull", "NonNull" -> notNull(request);
case "AssertTrue" -> new AssertBooleanAdapter(request, true);
case "AssertFalse" -> new AssertBooleanAdapter(request, false);
case "NotBlank" -> notBlank(request);
case "NotEmpty" -> new NotEmptyAdapter(request);
case "Pattern" -> new PatternAdapter(request);
case "Size", "Length" -> new SizeAdapter(request);
case "Valid" -> new ValidAdapter(request);
default -> null;
};
}

private ValidationAdapter<?> notBlank(AdapterCreateRequest request) {
if (NotBlankAdapter.isDefault(request)) {
return defaultNotBlankAdapter;
}
return new NotBlankAdapter(request);
}

private ValidationAdapter<?> notNull(AdapterCreateRequest request) {
if (request.isDefaultGroupOnly() && NOT_NULL_MESSAGE.equals(request.attribute("message"))) {
return defaultNotNullAdapter;
}
return new NullableAdapter(request, false);
}

private ValidationAdapter<?> nullable(AdapterCreateRequest request) {
if (request.isDefaultGroupOnly() && NULL_MESSAGE.equals(request.attribute("message"))) {
return defaultNullAdapter;
}
return new NullableAdapter(request, true);
}
}

static sealed class PatternAdapter extends AbstractConstraintAdapter<CharSequence>
permits EmailAdapter {
Expand Down Expand Up @@ -153,6 +197,12 @@ private static final class NotBlankAdapter implements ValidationAdapter<CharSequ
}
}

private static boolean isDefault(AdapterCreateRequest request) {
return request.isDefaultGroupOnly()
&& standardMessage(request)
&& maxLength(request) == 0;
}

private static int maxLength(AdapterCreateRequest request) {
final Integer max = request.attribute("max");
return Objects.requireNonNullElse(max, 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ void continueOnInvalid_expect_false() {
assertThat(notBlankMaxAdapter.validate(null, request, "foo")).isFalse();
assertThat(notBlankMaxAdapter.validate("", request, "foo")).isFalse();
}

@Test
void continueOnInvalid_expect_true_when_maxExceeded() {
assertThat(notBlankMaxAdapter.validate("01234", request, "foo")).isTrue();
Expand All @@ -50,4 +51,19 @@ void testBlank() {
assertThat(notBlankAdapter.validate("", request)).isFalse();
assertThat(notBlankAdapter.validate(" ", request)).isFalse();
}

@Test
void defaultInstance() {
var adapter0 = ctx.adapter(NotBlank.class, Map.of("message", "{avaje.NotBlank.message}"));
var adapter1 = ctx.adapter(NotBlank.class, Map.of("message", "{avaje.NotBlank.message}"));
var adapter2 = ctx.adapter(NotBlank.class, Map.of("message", "{avaje.NotBlank.message}", "max", 0));
assertThat(adapter1).isSameAs(adapter0).isSameAs(adapter2);

// these are different instances
var adapterDiff1 = ctx.adapter(NotBlank.class, Map.of("message", "Other message"));
var adapterDiff2 = ctx.adapter(NotBlank.class, Map.of("message", "{avaje.NotBlank.message}", "max", 4));
assertThat(adapter0).isNotSameAs(adapterDiff1);
assertThat(adapter0).isNotSameAs(adapterDiff2);
assertThat(adapterDiff1).isNotSameAs(adapterDiff2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import static org.assertj.core.api.Assertions.assertThat;

import java.util.Map;
import java.util.Set;

import io.avaje.validation.groups.Default;
import org.junit.jupiter.api.Test;

import io.avaje.validation.adapter.ValidationAdapter;
Expand Down Expand Up @@ -54,4 +56,17 @@ void testNotNull() {
assertThat(isValid(notNulladapter, 0)).isTrue();
assertThat(isValid(nonNulladapter, 0)).isTrue();
}

@Test
void defaultInstance() {
var adapter0 = ctx.adapter(NotNull.class, Map.of("message", "{avaje.NotNull.message}"));
var adapter1 = ctx.adapter(NotNull.class, Map.of("message", "{avaje.NotNull.message}"));
var adapter2 = ctx.adapter(NotNull.class, Set.of(Default.class), "{avaje.NotNull.message}", Map.of("message", "{avaje.NotNull.message}"));
assertThat(adapter1).isSameAs(adapter0).isSameAs(adapter2);

// these are different instances
var adapterDiff1 = ctx.adapter(NotNull.class, Map.of("message", "Other message"));
var adapterDiff2 = ctx.adapter(NotNull.class, Set.of(NullableAdapterTest.class), "{avaje.NotNull.message}", Map.of("message", "{avaje.NotNull.message}"));
assertThat(adapter0).isNotSameAs(adapterDiff1).isNotSameAs(adapterDiff2);
}
}