generated from micronaut-projects/micronaut-project-template
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from micronaut-projects/generics-validation
Generics validation
- Loading branch information
Showing
33 changed files
with
2,518 additions
and
1,801 deletions.
There are no files selected for viewing
6 changes: 6 additions & 0 deletions
6
buildSrc/src/main/groovy/io.micronaut.build.internal.validation-base.gradle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,7 @@ | ||
// If you don't need any common settings/dependencies/... for everything, remove this convention plugin and the reference to it in `io.micronaut.build.internal.project-template-module.gradle` file | ||
repositories { | ||
repositories { | ||
mavenCentral() | ||
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
plugins { | ||
id "io.micronaut.build.internal.validation-module" | ||
} | ||
|
||
dependencies { | ||
api mn.micronaut.core.processor | ||
|
||
testImplementation mn.micronaut.core.reactive | ||
testImplementation libs.managed.validation | ||
testCompileOnly mn.micronaut.http | ||
|
||
testImplementation libs.managed.spotbugs | ||
testCompileOnly mn.micronaut.inject.groovy | ||
testImplementation mn.micronaut.inject | ||
testImplementation libs.managed.validation | ||
|
||
testImplementation mn.micronaut.http.client | ||
testImplementation mn.micronaut.inject.java.test | ||
|
||
if (!JavaVersion.current().isJava9Compatible()) { | ||
testImplementation files(org.gradle.internal.jvm.Jvm.current().toolsJar) | ||
} | ||
} | ||
|
||
micronautBuild { | ||
binaryCompatibility { | ||
enabled = false | ||
} | ||
} |
139 changes: 139 additions & 0 deletions
139
validation-processor/src/main/java/io/micronaut/validation/visitor/ValidationVisitor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/* | ||
* Copyright 2017-2020 original 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 io.micronaut.validation.visitor; | ||
|
||
import io.micronaut.core.annotation.Internal; | ||
import io.micronaut.core.annotation.NonNull; | ||
import io.micronaut.inject.ast.ClassElement; | ||
import io.micronaut.inject.ast.ConstructorElement; | ||
import io.micronaut.inject.ast.FieldElement; | ||
import io.micronaut.inject.ast.MethodElement; | ||
import io.micronaut.inject.ast.TypedElement; | ||
import io.micronaut.inject.processing.ProcessingException; | ||
import io.micronaut.inject.validation.RequiresValidation; | ||
import io.micronaut.inject.visitor.TypeElementVisitor; | ||
import io.micronaut.inject.visitor.VisitorContext; | ||
|
||
import java.util.Arrays; | ||
import java.util.Set; | ||
|
||
/** | ||
* The visitor creates annotations utilized by the Validator. | ||
* | ||
* It adds @RequiresValidation annotation to fields if they require validation, and to methods | ||
* if one of the parameters or return value require validation. | ||
* | ||
* @author Denis Stepanov | ||
* @since 4.0.0 | ||
*/ | ||
@Internal | ||
public class ValidationVisitor implements TypeElementVisitor<Object, Object> { | ||
|
||
private static final String ANN_CONSTRAINT = "javax.validation.Constraint"; | ||
private static final String ANN_VALID = "javax.validation.Valid"; | ||
|
||
private ClassElement classElement; | ||
|
||
@Override | ||
public Set<String> getSupportedAnnotationNames() { | ||
return Set.of(ANN_CONSTRAINT, ANN_VALID); | ||
} | ||
|
||
@Override | ||
public int getOrder() { | ||
return 10; // Should run before ConfigurationReaderVisitor | ||
} | ||
|
||
@NonNull | ||
@Override | ||
public VisitorKind getVisitorKind() { | ||
return VisitorKind.ISOLATING; | ||
} | ||
|
||
@Override | ||
public void visitClass(ClassElement element, VisitorContext context) { | ||
classElement = element; | ||
} | ||
|
||
@Override | ||
public void visitConstructor(ConstructorElement element, VisitorContext context) { | ||
if (classElement == null) { | ||
return; | ||
} | ||
if ( | ||
requiresValidation(element.getReturnType(), true) || | ||
parametersRequireValidation(element, true) | ||
) { | ||
element.annotate(RequiresValidation.class); | ||
classElement.annotate(RequiresValidation.class); | ||
} | ||
} | ||
|
||
@Override | ||
public void visitMethod(MethodElement element, VisitorContext context) { | ||
if (classElement == null) { | ||
return; | ||
} | ||
boolean isPrivate = element.isPrivate(); | ||
boolean isAbstract = element.getOwningType().isInterface() || element.getOwningType().isAbstract(); | ||
boolean requireOnConstraint = isAbstract || !isPrivate; | ||
|
||
if ( | ||
requiresValidation(element.getReturnType(), requireOnConstraint) || | ||
parametersRequireValidation(element, requireOnConstraint) | ||
) { | ||
if (isPrivate) { | ||
throw new ProcessingException(element, "Method annotated for validation but is declared private. Change the method to be non-private in order for AOP advice to be applied."); | ||
} | ||
element.annotate(RequiresValidation.class); | ||
classElement.annotate(RequiresValidation.class); | ||
} | ||
} | ||
|
||
@Override | ||
public void visitField(FieldElement element, VisitorContext context) { | ||
if (classElement == null) { | ||
return; | ||
} | ||
if (requiresValidation(element, true)) { | ||
element.annotate(RequiresValidation.class); | ||
classElement.annotate(RequiresValidation.class); | ||
} | ||
} | ||
|
||
private boolean parametersRequireValidation(MethodElement element, boolean requireOnConstraint) { | ||
return Arrays.stream(element.getParameters()) | ||
.anyMatch(param -> requiresValidation(param, requireOnConstraint)); | ||
} | ||
|
||
private boolean requiresValidation(TypedElement e, boolean requireOnConstraint) { | ||
if (e.hasStereotype(ANN_VALID)) { | ||
// Annotate the element with same annotation that we annotate classes with. | ||
// This will ensure the correct behavior of io.micronaut.inject.ast.utils.AstBeanPropertiesUtils | ||
// in certain cases, as it relies on the fact that usages of types inherit | ||
// annotations from the type itself | ||
e.annotate(RequiresValidation.class); | ||
} | ||
return (requireOnConstraint && e.hasStereotype(ANN_CONSTRAINT)) || | ||
e.hasStereotype(ANN_VALID) || | ||
typeArgumentsRequireValidation(e, requireOnConstraint); | ||
} | ||
|
||
private boolean typeArgumentsRequireValidation(TypedElement e, boolean requireOnConstraint) { | ||
return e.getGenericType().getTypeArguments().values().stream() | ||
.anyMatch(classElement -> requiresValidation(classElement, requireOnConstraint)); | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
...essor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
io.micronaut.validation.visitor.ValidationVisitor |
176 changes: 176 additions & 0 deletions
176
...ation-processor/src/test/groovy/io/micronaut/validation/visitor/ValidatedParseSpec.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
package io.micronaut.validation | ||
|
||
import io.micronaut.annotation.processing.test.AbstractTypeElementSpec | ||
import io.micronaut.inject.writer.BeanDefinitionVisitor | ||
import java.time.LocalDate | ||
|
||
class ValidatedParseSpec extends AbstractTypeElementSpec { | ||
final static String VALIDATED_ANN = "io.micronaut.validation.Validated"; | ||
|
||
void "test constraints on beans make them @Validated"() { | ||
given: | ||
def definition = buildBeanDefinition('test.Test',''' | ||
package test; | ||
@jakarta.inject.Singleton | ||
class Test { | ||
@io.micronaut.context.annotation.Executable | ||
public void setName(@javax.validation.constraints.NotBlank String name) { | ||
} | ||
@io.micronaut.context.annotation.Executable | ||
public void setName2(@javax.validation.Valid String name) { | ||
} | ||
} | ||
''') | ||
|
||
expect: | ||
definition != null | ||
definition.findMethod("setName", String).get().hasStereotype(VALIDATED_ANN) | ||
} | ||
|
||
void "test constraints on a declarative client makes it @Validated"() { | ||
given: | ||
def definition = buildBeanDefinition('test.ExchangeRates' + BeanDefinitionVisitor.PROXY_SUFFIX,''' | ||
package test; | ||
import io.micronaut.http.annotation.Get; | ||
import io.micronaut.http.client.annotation.Client; | ||
import javax.validation.constraints.PastOrPresent; | ||
import java.time.LocalDate; | ||
@Client("https://exchangeratesapi.io") | ||
interface ExchangeRates { | ||
@Get("{date}") | ||
String rate(@PastOrPresent LocalDate date); | ||
} | ||
''') | ||
|
||
expect: | ||
definition.findMethod("rate", LocalDate).get().hasStereotype(VALIDATED_ANN) | ||
} | ||
|
||
void "test constraints on generic parameters make method @Validated"() { | ||
given: | ||
def definition = buildBeanDefinition('test.Test',''' | ||
package test; | ||
import java.util.List; | ||
import javax.validation.constraints.NotBlank; | ||
@jakarta.inject.Singleton | ||
class Test { | ||
@io.micronaut.context.annotation.Executable | ||
public void setList(List<@NotBlank String> list) { | ||
} | ||
} | ||
''') | ||
when: | ||
def method = definition.findMethod("setList", List<String>); | ||
|
||
then: | ||
method.isPresent() | ||
method.get().hasStereotype(VALIDATED_ANN) | ||
method.get().getArguments().size() == 1 | ||
} | ||
|
||
void "test constraints on a controller operation make method @Validated"() { | ||
given: | ||
def definition = buildBeanDefinition('test.Test', ''' | ||
package test; | ||
import java.util.List; | ||
import javax.validation.Valid; | ||
import io.micronaut.core.annotation.Introspected; | ||
import io.micronaut.http.annotation.Body; | ||
import io.micronaut.http.annotation.Controller; | ||
import io.micronaut.http.annotation.Post; | ||
@Controller() | ||
class Test { | ||
@Post("/pojos") | ||
public List<Pojo> pojos(@Body List<@Valid Pojo> pojos) { | ||
return pojos; | ||
} | ||
@Introspected | ||
public record Pojo() {} | ||
} | ||
''') | ||
var method = definition.findPossibleMethods("pojos").findFirst() | ||
|
||
expect: | ||
method.isPresent() | ||
method.get().hasStereotype(VALIDATED_ANN) | ||
} | ||
|
||
void "test constraints on return value generic parameters make method @Validated"() { | ||
given: | ||
def definition = buildBeanDefinition('test.Test',''' | ||
package test; | ||
import java.util.List; | ||
import javax.validation.constraints.Min; | ||
import javax.validation.constraints.NotNull; | ||
import io.micronaut.context.annotation.Executable; | ||
@jakarta.inject.Singleton | ||
class Test { | ||
@Executable | ||
public @Min(value=10) Integer getValue() { | ||
return 1; | ||
} | ||
@Executable | ||
public List<@NotNull String> getStrings() { | ||
return null; | ||
} | ||
} | ||
''') | ||
var method = definition.findMethod("getValue") | ||
|
||
expect: | ||
method.isPresent() | ||
method.get().hasStereotype(VALIDATED_ANN) | ||
|
||
when: | ||
var method2 = definition.findMethod("getStrings") | ||
|
||
then: | ||
method2.isPresent() | ||
method2.get().hasStereotype(VALIDATED_ANN) | ||
} | ||
|
||
void "test constraints on reactive return value make method @Validated"() { | ||
given: | ||
def definition = buildBeanDefinition('test.Test',''' | ||
package test; | ||
import javax.validation.constraints.NotBlank; | ||
import io.micronaut.context.annotation.Executable; | ||
import reactor.core.publisher.Mono; | ||
@jakarta.inject.Singleton | ||
class Test { | ||
@Executable | ||
public Mono<@NotBlank String> getMono() { | ||
return Mono.fromCallable(() -> ""); | ||
} | ||
} | ||
''') | ||
var method = definition.findMethod("getMono") | ||
|
||
expect: | ||
method.isPresent() | ||
method.get().hasStereotype(VALIDATED_ANN) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<configuration> | ||
|
||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | ||
<!-- encoders are assigned the type | ||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> | ||
<encoder> | ||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | ||
</encoder> | ||
</appender> | ||
|
||
<root level="info"> | ||
<appender-ref ref="STDOUT" /> | ||
</root> | ||
</configuration> |
Oops, something went wrong.