From 99cfd3b69bc79e854f79bdd1210a8e8bb1b96d4a Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 2 Mar 2023 08:59:04 -0500 Subject: [PATCH] Correct RxJava2 validation (#47) --- settings.gradle | 1 + validation/build.gradle | 1 + .../validator/DefaultValidator.java | 7 +- .../validator/reactive/BookService.java | 4 +- .../reactive/BookServiceRxJava2.java | 49 +++++++ .../ReactiveMethodValidationSpec.groovy | 4 + .../RxJava2MethodValidationSpec.groovy | 133 ++++++++++++++++++ 7 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/reactive/BookServiceRxJava2.java create mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/reactive/RxJava2MethodValidationSpec.groovy diff --git a/settings.gradle b/settings.gradle index 046a3a9a..6c9dfb84 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,5 +28,6 @@ include 'test-suite-kotlin' micronautBuild { importMicronautCatalog() importMicronautCatalog("micronaut-reactor") + importMicronautCatalog("micronaut-rxjava2") } diff --git a/validation/build.gradle b/validation/build.gradle index e14d33b0..a4b8b1ea 100644 --- a/validation/build.gradle +++ b/validation/build.gradle @@ -23,6 +23,7 @@ dependencies { testImplementation mn.micronaut.http.client testImplementation mn.micronaut.http.server.netty + testImplementation mnRxjava2.micronaut.rxjava2 testImplementation libs.groovy.json testImplementation mn.micronaut.inject.java.test testImplementation mn.micronaut.jackson.databind diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java index 2f1ecbc0..5730dad7 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -476,8 +476,11 @@ public Publisher validatePublisher(@NonNull ReturnType returnType, Flux.error(new ConstraintViolationException(violations)); }); } - - return Publishers.convertPublisher(conversionService, output, ((ReturnType) returnType).getType()); + Class returnClass = returnType.getType(); + if (!Publisher.class.isAssignableFrom(returnClass)) { + return (Publisher) output; + } + return Publishers.convertPublisher(conversionService, output, (Class) returnClass); } /** diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/reactive/BookService.java b/validation/src/test/groovy/io/micronaut/validation/validator/reactive/BookService.java index ff339540..91310efc 100644 --- a/validation/src/test/groovy/io/micronaut/validation/validator/reactive/BookService.java +++ b/validation/src/test/groovy/io/micronaut/validation/validator/reactive/BookService.java @@ -36,8 +36,8 @@ class BookService { } @Executable - void rxValidWithTypeParameter(Mono> books) { - books.block(); + Mono rxValidWithTypeParameter(Mono> books) { + return books.then(); } @Executable diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/reactive/BookServiceRxJava2.java b/validation/src/test/groovy/io/micronaut/validation/validator/reactive/BookServiceRxJava2.java new file mode 100644 index 00000000..4793f9c0 --- /dev/null +++ b/validation/src/test/groovy/io/micronaut/validation/validator/reactive/BookServiceRxJava2.java @@ -0,0 +1,49 @@ +package io.micronaut.validation.validator.reactive; + +import io.micronaut.context.annotation.Executable; +import io.reactivex.Completable; +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; +import jakarta.inject.Singleton; +import org.reactivestreams.Publisher; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.util.List; + +@Singleton +class BookServiceRxJava2 { + + @Executable + Publisher<@Valid Book> rxSimple(Publisher<@NotBlank String> title) { + return Single.fromPublisher(title).map(Book::new).toFlowable(); + } + + @Executable + Observable<@Valid Book> rxValid(Observable<@Valid Book> book) { + return book; + } + + @Executable + Completable rxValidWithTypeParameter(Single> books) { + return books.ignoreElement(); + } + + @Executable + Maybe<@Valid Book> rxValidMaybe(Maybe<@Valid Book> book) { return book; } + + @Executable + Publisher<@Valid Book> rxReturnInvalid(Publisher<@Valid Book> book) { + return Flowable.fromPublisher(book).map(b -> new Book("")); + } + + @Executable + Maybe rxReturnInvalidWithoutValidation(Flowable<@Valid Book> books) { + return books.firstElement().map(v -> new Book("")); + } + +} + + diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/reactive/ReactiveMethodValidationSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/reactive/ReactiveMethodValidationSpec.groovy index b88c026d..77a0cc16 100644 --- a/validation/src/test/groovy/io/micronaut/validation/validator/reactive/ReactiveMethodValidationSpec.groovy +++ b/validation/src/test/groovy/io/micronaut/validation/validator/reactive/ReactiveMethodValidationSpec.groovy @@ -57,6 +57,10 @@ class ReactiveMethodValidationSpec extends Specification { [Mono.just("")] as Object[] ) + then: "No errors because publisher is not executed" + violations.size() == 0 + + when: Mono.from(bookService.rxSimple(Mono.just(""))).block() then: diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/reactive/RxJava2MethodValidationSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/reactive/RxJava2MethodValidationSpec.groovy new file mode 100644 index 00000000..35642dcb --- /dev/null +++ b/validation/src/test/groovy/io/micronaut/validation/validator/reactive/RxJava2MethodValidationSpec.groovy @@ -0,0 +1,133 @@ +package io.micronaut.validation.validator.reactive + +import io.micronaut.context.ApplicationContext +import io.micronaut.validation.validator.Validator +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import javax.validation.ConstraintViolationException +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.regex.Pattern + +class RxJava2MethodValidationSpec extends Specification { + + @Shared + @AutoCleanup + ApplicationContext applicationContext = ApplicationContext.run() + + void "test reactive return type validation"() { + given: + BookServiceRxJava2 bookService = applicationContext.getBean(BookServiceRxJava2) + + when: + Single single = Single.just(new Book("It")) + Single.fromPublisher(bookService.rxReturnInvalid(single.toFlowable())).blockingGet() + + then: + ConstraintViolationException e = thrown() + e.message == 'publisher[].title: must not be blank' + e.getConstraintViolations().first().propertyPath.toString() == 'publisher[].title' + } + + void "test reactive return type no validation"() { + given: + BookServiceRxJava2 bookService = applicationContext.getBean(BookServiceRxJava2) + + when: + Single single = Single.just(new Book("It")) + bookService.rxReturnInvalidWithoutValidation(single.toFlowable()).blockingGet() + + then: + noExceptionThrown() + } + + void "test reactive validation with invalid simple argument"() { + given: + BookServiceRxJava2 bookService = applicationContext.getBean(BookServiceRxJava2) + + when: + var validator = applicationContext.getBean(Validator) + var violations = validator.forExecutables().validateParameters( + bookService, + BookService.class.getDeclaredMethod("rxSimple", Publisher), + [Flowable.just("")] as Object[] + ) + + then: "No errors because publisher is not executed" + violations.size() == 0 + + when: + Single.fromPublisher(bookService.rxSimple(Single.just("").toFlowable())).blockingGet() + + then: + def e = thrown(ConstraintViolationException) + Pattern.matches('rxSimple.title\\[]]*String>: must not be blank', e.message) + def path = e.getConstraintViolations().first().propertyPath.iterator() + path.next().getName() == 'rxSimple' + path.next().getName() == 'title' + path.next().isInIterable() + + } + + void "test reactive validation with valid argument"() { + given: + BookServiceRxJava2 bookService = applicationContext.getBean(BookServiceRxJava2) + + when: + def input = Observable.just(new Book("It")) + def book = bookService.rxValid(input).blockingFirst() + + then: + book.title == 'It' + } + + void "test reactive maybe validation with valid argument"() { + given: + BookServiceRxJava2 bookService = applicationContext.getBean(BookServiceRxJava2) + + when: + def input = Maybe.just(new Book("It")) + def book = bookService.rxValidMaybe(input).blockingGet() + + then: + book.title == 'It' + } + + void "test reactive validation with invalid argument"() { + given: + BookServiceRxJava2 bookService = applicationContext.getBean(BookServiceRxJava2) + + when: + def input = Observable.just(new Book("")) + bookService.rxValid(input).blockingFirst() + + then: + def e = thrown(ConstraintViolationException) + Pattern.matches('rxValid.book\\[].title: must not be blank', e.message) + e.getConstraintViolations().first().propertyPath.toString().startsWith('rxValid.book') + } + + void "test reactive validation with invalid argument type parameter"() { + given: + BookServiceRxJava2 bookService = applicationContext.getBean(BookServiceRxJava2) + + when: + def input = Single.just([new Book("It"), new Book("")]) + bookService.rxValidWithTypeParameter(input).blockingAwait() + + then: + def e = thrown(ConstraintViolationException) + Pattern.matches('rxValidWithTypeParameter.books\\[]\\[1].title: must not be blank', e.message) + e.getConstraintViolations().first().propertyPath.toString().startsWith('rxValidWithTypeParameter.books') + } + +}