Skip to content

Commit

Permalink
Merge pull request #18 from micronaut-projects/validation-core-updates
Browse files Browse the repository at this point in the history
Merge update from micronaut-core
  • Loading branch information
dstepanov authored Jan 16, 2023
2 parents 60b8023 + 9dade34 commit b5f6bd7
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 9 deletions.
1 change: 1 addition & 0 deletions validation-processor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {

testImplementation mn.micronaut.http.client
testImplementation mn.micronaut.inject.java.test
testImplementation mn.micronaut.inject.groovy.test

if (!JavaVersion.current().isJava9Compatible()) {
testImplementation files(org.gradle.internal.jvm.Jvm.current().toolsJar)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.micronaut.validation.visitor

import io.micronaut.aop.Around
import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec
import io.micronaut.core.beans.BeanIntrospection
import io.micronaut.inject.ProxyBeanDefinition
import io.micronaut.inject.writer.BeanDefinitionVisitor
import io.micronaut.inject.writer.BeanDefinitionWriter
import io.micronaut.validation.ValidatedParseSpec

import java.time.LocalDate

class ValidatedParseSpecGroovy extends AbstractBeanDefinitionSpec {
void "test constraints on beans make them @Validated"() {
given:
def definition = buildBeanDefinition('validateparse1.Test','''
package validateparse1
import io.micronaut.context.annotation.Executable
import javax.validation.Valid
import javax.validation.constraints.NotBlank
@jakarta.inject.Singleton
class Test {
@Executable
void setName(@NotBlank String name) {}
@Executable
void setName2(@Valid String name) {}
}
''')

expect:
definition.findMethod("setName", String).get().hasStereotype(ValidatedParseSpec.VALIDATED_ANN)
definition.findMethod("setName2", String).get().hasStereotype(ValidatedParseSpec.VALIDATED_ANN)
}

void "test annotation default values on a groovy property"() {
given:
BeanIntrospection beanIntrospection = buildBeanIntrospection('validateparse2.Test','''
package validateparse2;
import io.micronaut.core.annotation.Introspected
import javax.validation.Constraint
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Introspected
class Test {
@ValidURLs
List<String> webs
}
@Constraint(validatedBy = [])
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface ValidURLs {
String message() default "invalid url"
}
''')

expect:
beanIntrospection.getProperty("webs").isPresent()
beanIntrospection.getRequiredProperty("webs", List).annotationMetadata.getDefaultValue("validateparse2.ValidURLs", "message", String).get() == "invalid url"
}

void "test constraints on a declarative client makes it @Validated"() {
given:
def definition = buildBeanDefinition('validateparse3.ExchangeRates' + BeanDefinitionVisitor.PROXY_SUFFIX,'''
package validateparse3
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(ValidatedParseSpec.VALIDATED_ANN)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.micronaut.aop.MethodInterceptor;
import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.validation.validator.ExecutableMethodValidator;
import io.micronaut.validation.validator.ReactiveValidator;
Expand Down Expand Up @@ -48,15 +49,19 @@ public class ValidatingInterceptor implements MethodInterceptor<Object, Object>

private final @Nullable ExecutableValidator executableValidator;
private final @Nullable ExecutableMethodValidator micronautValidator;
private final ConversionService conversionService;

/**
* Creates ValidatingInterceptor from the validatorFactory.
*
* @param micronautValidator The micronaut validator use if no factory is available
* @param validatorFactory Factory returning initialized {@code Validator} instances
* @param conversionService The conversion service
*/
public ValidatingInterceptor(@Nullable Validator micronautValidator,
@Nullable ValidatorFactory validatorFactory) {
@Nullable ValidatorFactory validatorFactory,
ConversionService conversionService) {
this.conversionService = conversionService;

if (validatorFactory != null) {
javax.validation.Validator validator = validatorFactory.getValidator();
Expand Down Expand Up @@ -112,7 +117,7 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
}
}
if (micronautValidator instanceof ReactiveValidator) {
InterceptedMethod interceptedMethod = InterceptedMethod.of(context);
InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService);
try {
switch (interceptedMethod.resultType()) {
case PUBLISHER:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.inject.InjectionPoint;
import io.micronaut.inject.MethodReference;
import io.micronaut.inject.ProxyBeanDefinition;
import io.micronaut.inject.annotation.AnnotatedElementValidator;
import io.micronaut.inject.validation.BeanDefinitionValidator;
import io.micronaut.validation.validator.constraints.ConstraintValidator;
Expand Down Expand Up @@ -616,17 +617,21 @@ public <T> void validateBean(
@NonNull BeanDefinition<T> definition,
@NonNull T bean
) throws BeanInstantiationException {
final BeanIntrospection<T> introspection = (BeanIntrospection<T>) getBeanIntrospection(bean);
Class<T> beanType;
if (definition instanceof ProxyBeanDefinition<?> proxyBeanDefinition) {
beanType = (Class<T>) proxyBeanDefinition.getTargetType();
} else {
beanType = definition.getBeanType();
}
final BeanIntrospection<T> introspection = (BeanIntrospection<T>) getBeanIntrospection(bean, beanType);
if (introspection != null) {
Set<ConstraintViolation<T>> errors = validate(introspection, bean);
final Class<?> beanType = bean.getClass();
failOnError(resolutionContext, errors, beanType);
} else if (bean instanceof Intercepted && definition.hasStereotype(ConfigurationReader.class)) {
final Collection<ExecutableMethod<T, ?>> executableMethods = definition.getExecutableMethods();
if (CollectionUtils.isNotEmpty(executableMethods)) {
Set<ConstraintViolation<T>> violations = new HashSet<>();
final DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(bean);
final Class<T> beanType = definition.getBeanType();
final Class<?>[] interfaces = beanType.getInterfaces();
if (ArrayUtils.isNotEmpty(interfaces)) {
context.addConstructorNode(interfaces[0].getSimpleName());
Expand All @@ -651,6 +656,8 @@ public <T> void validateBean(

failOnError(resolutionContext, violations, beanType);
}
} else {
throw new BeanInstantiationException(resolutionContext, "Cannot validate bean [" + beanType.getName() + "]. No bean introspection present. Please add @Introspected.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.context.exceptions.BeanInstantiationException
import io.micronaut.core.annotation.Nullable
import io.micronaut.core.beans.BeanIntrospection
import io.micronaut.core.convert.ConversionService
import io.micronaut.core.order.OrderUtil
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
Expand Down Expand Up @@ -71,7 +71,7 @@ class ValidatedSpec extends Specification {
Object intercept(InvocationContext context) {
return null
}
}, new ValidatingInterceptor(null, null)]
}, new ValidatingInterceptor(null, null, ConversionService.SHARED)]
OrderUtil.sort(list)

expect:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package io.micronaut.validation.validator
import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.Executable
import io.micronaut.context.annotation.Prototype
import io.micronaut.context.annotation.Value
import io.micronaut.context.exceptions.BeanInstantiationException
import io.micronaut.core.annotation.Introspected
import io.micronaut.core.reflect.ClassUtils
import io.micronaut.validation.validator.resolver.CompositeTraversableResolver
import jakarta.inject.Singleton
import spock.lang.AutoCleanup
Expand All @@ -21,7 +24,7 @@ class ValidatorSpec extends Specification {

@Shared
@AutoCleanup
ApplicationContext applicationContext = ApplicationContext.run()
ApplicationContext applicationContext = ApplicationContext.run(["a.number": 40])
@Shared
Validator validator = applicationContext.getBean(Validator)

Expand Down Expand Up @@ -792,7 +795,6 @@ class ValidatorSpec extends Specification {
violations[0].invalidValue == ""
}
@Ignore("https://github.com/micronaut-projects/micronaut-core/issues/8301")
void "test cascade to container with setter"() {
given:
def salad = new ValidatorSpecClasses.SaladWithSetter()
Expand All @@ -806,6 +808,81 @@ class ValidatorSpec extends Specification {
violations.size() == 1
violations[0].invalidValue == ""
}
void "test @Introspected is required to validate the bean"() {
when:
applicationContext.getBean(A)
then:
BeanInstantiationException e = thrown()
e.message.contains('''Cannot validate bean [io.micronaut.validation.validator.A]. No bean introspection present. Please add @Introspected.''')
and:
ClassUtils.forName('io.micronaut.validation.validator.$A$Definition', getClass().getClassLoader()).isPresent()
ClassUtils.forName('io.micronaut.validation.validator.$A$Definition$Intercepted', getClass().getClassLoader()).isEmpty()
}
void "test @Introspected is required to validate the bean and it's intercepted if one of the methods requires validation"() {
when:
def beanB = applicationContext.getBean(B)
then:
BeanInstantiationException e = thrown()
e.message.contains('''number - must be less than or equal to 20''')
and:
ClassUtils.forName('io.micronaut.validation.validator.$B$Definition', getClass().getClassLoader()).isPresent()
ClassUtils.forName('io.micronaut.validation.validator.$B$Definition$Intercepted', getClass().getClassLoader()).isPresent()
}
void "test @Introspected is required to validate the bean and it's intercepted if one of the methods requires validation 2"() {
when:
def beanC = applicationContext.getBean(C)
then:
beanC.number == 40
when:
beanC.updateNumber(100)
then:
Exception e = thrown()
e.message.contains('''updateNumber.number: must be less than or equal to 50''')
beanC.number == 40
and:
ClassUtils.forName('io.micronaut.validation.validator.$C$Definition', getClass().getClassLoader()).isPresent()
ClassUtils.forName('io.micronaut.validation.validator.$C$Definition$Intercepted', getClass().getClassLoader()).isPresent()
}
}
@Singleton
class A {
@Max(20l)
@NotNull
@Value('${a.number}')
Integer number
}
@Introspected
@Singleton
class B {
@Max(20l)
@NotNull
@Value('${a.number}')
Integer number
void updateNumber(@Max(20l)
@NotNull
Integer number) {
this.number = number
}
}
@Introspected
@Singleton
class C {
@Max(50l)
@NotNull
@Value('${a.number}')
Integer number
void updateNumber(@Max(50l)
@NotNull
Integer number) {
this.number = number
}
}
@Introspected
Expand Down

0 comments on commit b5f6bd7

Please sign in to comment.