Skip to content

Add @ValidSubTypes #280

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 7 commits into from
Feb 17, 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
@@ -0,0 +1,8 @@
package example.avaje.subtypes;

import java.util.List;
import java.util.UUID;

import io.avaje.validation.constraints.NotEmpty;

public final record ByIdSelector(@NotEmpty List<UUID> ids) implements EntitySelector {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package example.avaje.subtypes;

import io.avaje.validation.constraints.NotBlank;

public final record ByQuerySelector(@NotBlank String query) implements EntitySelector {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package example.avaje.subtypes;

import io.avaje.validation.ValidSubTypes;

@ValidSubTypes({ByQuerySelector.class, ByIdSelector.class})
public interface EntitySelector {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package example.avaje.subtypes;

import jakarta.validation.Valid;

@Valid
public record SubtypeEntity(@Valid EntitySelector selector) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package example.avaje.subtypes.sealed;

import java.util.List;
import java.util.UUID;

import io.avaje.validation.constraints.NotEmpty;

public final class ByIdSelectorSealed implements SealedEntitySelector {

@NotEmpty private final List<UUID> ids;

public ByIdSelectorSealed(@NotEmpty List<UUID> ids) {
this.ids = ids;
}

public List<UUID> getIds() {
return ids;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package example.avaje.subtypes.sealed;

import io.avaje.validation.constraints.NotBlank;

public final record ByQuerySelectorSealed(@NotBlank String query) implements SealedEntitySelector {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package example.avaje.subtypes.sealed;

import jakarta.validation.Valid;

@Valid
public record SealedEntity(@Valid SealedEntitySelector selector) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package example.avaje.subtypes.sealed;

import java.util.List;
import java.util.UUID;

import example.avaje.subtypes.sealed.SealedEntitySelector.NestedSealed;
import io.avaje.validation.ValidSubTypes;
import io.avaje.validation.constraints.NotEmpty;

@ValidSubTypes
public sealed interface SealedEntitySelector
permits ByQuerySelectorSealed, ByIdSelectorSealed, NestedSealed {

public final record NestedSealed(@NotEmpty List<UUID> ids) implements SealedEntitySelector {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package example.avaje.subtypes;

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

import java.util.List;

import org.junit.jupiter.api.Test;

import io.avaje.validation.Validator;

class EntitySelectorTest {

Validator validator = Validator.builder().build();

@Test
void validByIdSelector() {
var entity = new SubtypeEntity(new ByIdSelector(List.of()));

assertThat(validator.check(entity).iterator().next().message()).isEqualTo("must not be empty");
}

@Test
void validByIdQuery() {
var entity = new SubtypeEntity(new ByQuerySelector(""));
assertThat(validator.check(entity).iterator().next().message()).isEqualTo("must not be blank");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package example.avaje.subtypes.sealed;

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

import java.util.List;

import org.junit.jupiter.api.Test;

import io.avaje.validation.Validator;

class SealedEntitySelectorTest {

Validator validator = Validator.builder().build();

@Test
void validByIdSelector() {
var entity = new SealedEntity(new ByIdSelectorSealed(List.of()));

assertThat(validator.check(entity).iterator().next().message()).isEqualTo("must not be empty");
}

@Test
void validByIdQuery() {
var entity = new SealedEntity(new ByQuerySelectorSealed(""));
assertThat(validator.check(entity).iterator().next().message()).isEqualTo("must not be blank");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@

import io.avaje.http.api.ValidationException;
import io.avaje.http.api.Validator;
import io.avaje.inject.spi.AvajeModule;
import io.avaje.inject.spi.Builder;
import io.avaje.inject.spi.Module;
import io.avaje.inject.test.InjectTest;
import jakarta.inject.Inject;

@InjectTest
class DefaultValidatorProviderTest {
Module mod =
new Module() {
AvajeModule mod =
new AvajeModule() {

@Override
public Class<?>[] classes() {
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<nexus.staging.autoReleaseAfterClose>true</nexus.staging.autoReleaseAfterClose>
<maven.compiler.release>17</maven.compiler.release>
<inject.version>10.3</inject.version>
<spi.version>2.5</spi.version>
<spi.version>2.9</spi.version>
<project.build.outputTimestamp>2025-02-10T08:50:36Z</project.build.outputTimestamp>
</properties>

Expand Down
17 changes: 17 additions & 0 deletions validator-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<description>annotation processor generating validation adapters</description>
<properties>
<avaje.prisms.version>1.38</avaje.prisms.version>
<io.jstach.version>1.3.6</io.jstach.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -66,6 +67,22 @@
<optional>true</optional>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.jstach</groupId>
<artifactId>jstachio-annotation</artifactId>
<version>${io.jstach.version}</version>
<optional>true</optional>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jstach</groupId>
<artifactId>jstachio-apt</artifactId>
<version>${io.jstach.version}</version>
<optional>true</optional>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package io.avaje.validation.generator;

import static io.avaje.validation.generator.APContext.createSourceFile;
import static io.avaje.validation.generator.APContext.logError;

import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.JavaFileObject;

import io.jstach.jstache.JStache;

public class SubTypeWriter {

TypeElement element;
List<String> subtypeStrings;
private final Set<String> importTypes = new TreeSet<>();
private final String shortName;
private final String adapterPackage;
private final String adapterFullName;
private final String shortType;

public SubTypeWriter(TypeElement element, List<TypeMirror> subtypes) {
this.element = element;
this.subtypeStrings =
subtypes.stream().map(TypeMirror::toString).map(ProcessorUtils::shortType).toList();
final AdapterName adapterName = new AdapterName(element);
this.shortName = adapterName.shortName();
this.adapterPackage = adapterName.adapterPackage();
this.adapterFullName = adapterName.fullName();
this.shortType = element.getQualifiedName().toString().transform(ProcessorUtils::shortType);

importTypes.add("io.avaje.validation.adapter.ValidationAdapter");
importTypes.add("io.avaje.validation.adapter.ValidationContext");
importTypes.add("io.avaje.validation.adapter.ValidationRequest");
importTypes.add("io.avaje.validation.spi.Generated");
subtypes.stream().map(TypeMirror::toString).forEach(importTypes::add);
}

private Writer createFileWriter() throws IOException {
final JavaFileObject jfo = createSourceFile(adapterFullName);
return jfo.openWriter();
}

void write() {
Append writer;
try {
writer = new Append(createFileWriter());

var template =
new SubTemplate(
adapterPackage,
importTypes,
shortName,
shortType,
subtypeStrings,
APContext.jdkVersion() > 17,
element.getModifiers().contains(Modifier.SEALED))
.render();

writer.append(template).close();
} catch (IOException e) {

logError("Error writing ValidationAdapter for %s %s", element, e);
}
}

String fullName() {
return adapterFullName;
}

@JStache(
template =
"""
package {{packageName}};

{{#imports}}
import {{.}};
{{/imports}}

@Generated("avaje-validation-generator")
public class {{shortName}}ValidationAdapter implements ValidationAdapter<{{shortType}}> {

{{#subtypes}}
private final ValidationAdapter<{{.}}> subAdapter{{@index}};
{{/subtypes}}

public {{shortName}}ValidationAdapter(ValidationContext ctx) {
{{#subtypes}}
this.subAdapter{{@index}} = ctx.adapter({{.}}.class);
{{/subtypes}}
}

@Override
public boolean validate({{shortType}} value, ValidationRequest request, String field) {
{{#switchValid}}
return switch(value) {
case null -> true;
{{#subtypes}}
case {{.}} val -> subAdapter{{@index}}.validate(val, request, field);
{{/subtypes}}
{{^sealed}}
default -> true;
{{/sealed}}
};
{{/switchValid}}
{{^switchValid}}
{{#subtypes}}
if (value instanceof {{.}} val) {
return subAdapter{{@index}}.validate(val, request, field);
}
{{/subtypes}}
return true;
{{/switchValid}}
}
}
""")
public record SubTemplate(
String packageName,
Set<String> imports,
String shortName,
String shortType,
List<String> subtypes,
boolean switchValid,
boolean sealed) {
String render() {
return SubTemplateRenderer.of().execute(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
import static java.util.stream.Collectors.joining;

import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashSet;
Expand All @@ -28,7 +25,6 @@
import io.avaje.prism.GenerateAPContext;
import io.avaje.prism.GenerateModuleInfoReader;
import io.avaje.prism.GenerateUtils;

import static io.avaje.validation.generator.APContext.*;

@GenerateUtils
Expand All @@ -46,6 +42,7 @@
JavaxConstraintPrism.PRISM_TYPE,
CrossParamConstraintPrism.PRISM_TYPE,
ValidMethodPrism.PRISM_TYPE,
ValidSubTypesPrism.PRISM_TYPE,
"io.avaje.spi.ServiceProvider"
})
public final class ValidationProcessor extends AbstractProcessor {
Expand Down Expand Up @@ -121,6 +118,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
getElements(round, MixInPrism.PRISM_TYPE).ifPresent(this::writeAdaptersForMixInTypes);
getElements(round, ImportValidPojoPrism.PRISM_TYPE).ifPresent(this::writeAdaptersForImported);
getElements(round, "io.avaje.spi.ServiceProvider").ifPresent(this::registerSPI);
getElements(round, ValidSubTypesPrism.PRISM_TYPE).ifPresent(this::writeSubTypeAdaptersForImported);

initialiseComponent();
cascadeTypes();
Expand Down Expand Up @@ -213,6 +211,29 @@ private boolean ignoreType(String type) {
|| sourceTypes.contains(type);
}

/** Elements that have a {@code @ValidSubTypes} annotation. */
private void writeSubTypeAdaptersForImported(Set<? extends Element> subtypeElements) {
for (final var element : ElementFilter.typesIn(subtypeElements)) {
var prism = ValidSubTypesPrism.getInstanceOn(element);
var subtypes = new ArrayList<>(prism.value());
subtypes.addAll(element.getPermittedSubclasses());

var seen = new HashSet<>();
subtypes.removeIf(s -> !seen.add(s.toString()));
var writer = new SubTypeWriter(element, subtypes);
writer.write();
metaData.add(writer.fullName());
// cascade types
for (final TypeMirror importType : subtypes) {
// if imported by mixin annotation skip
if (mixInImports.contains(importType.toString())) {
continue;
}
writeAdapterForType(asTypeElement(importType));
}
}
}

/** Elements that have a {@code @Valid.Import} annotation. */
private void writeAdaptersForImported(Set<? extends Element> importedElements) {
for (final var importedElement : ElementFilter.typesIn(importedElements)) {
Expand Down
Loading