Extract some WebFlux functional snippets

See gh-36089
This commit is contained in:
Sébastien Deleuze
2026-01-02 16:44:02 +01:00
parent 5ecece91dc
commit 6a28f06b2e
25 changed files with 1032 additions and 363 deletions
@@ -0,0 +1,19 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlerclasses;
public record Person(String name) { }
@@ -0,0 +1,67 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlerclasses;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
// tag::snippet[]
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
// listPeople is a handler function that returns all Person objects found
// in the repository as JSON
public Mono<ServerResponse> listPeople(ServerRequest request) {
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
// createPerson is a handler function that stores a new Person contained
// in the request body.
// Note that PersonRepository.savePerson(Person) returns Mono<Void>: an empty
// Mono that emits a completion signal when the person has been read from the
// request and stored. So we use the build(Publisher<Void>) method to send a
// response when that completion signal is received (that is, when the Person
// has been saved)
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
// getPerson is a handler function that returns a single person, identified by
// the id path variable. We retrieve that Person from the repository and create
// a JSON response, if it is found. If it is not found, we use switchIfEmpty(Mono<T>)
// to return a 404 Not Found response.
public Mono<ServerResponse> getPerson(ServerRequest request) {
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
// end::snippet[]
@@ -0,0 +1,29 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlerclasses;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface PersonRepository {
Flux<Person> allPeople();
Mono<Void> savePerson(Mono<Person> person);
Mono<Person> getPerson(int id);
}
@@ -0,0 +1,55 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlerfilterfunction;
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
public class RouterConfiguration {
public RouterFunction<ServerResponse> route(PersonHandler handler) {
// tag::snippet[]
SecurityManager securityManager = getSecurityManager();
RouterFunction<ServerResponse> route = RouterFunctions.route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
}).build();
// end::snippet[]
return route;
}
SecurityManager getSecurityManager() {
return path -> false;
}
}
@@ -0,0 +1,22 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlerfilterfunction;
public interface SecurityManager {
boolean allowAccessTo(String path);
}
@@ -0,0 +1,58 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlervalidation;
import org.springframework.docs.web.webfluxfnhandlerclasses.Person;
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
// tag::snippet[]
public class PersonHandler {
// Create Validator instance
private final Validator validator = new PersonValidator();
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// Apply validation
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate);
return ok().build(repository.savePerson(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
// Raise exception for a 400 response
throw new ServerWebInputException(errors.toString());
}
}
}
// end::snippet[]
@@ -0,0 +1,34 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlervalidation;
import org.springframework.docs.web.webfluxfnhandlerclasses.Person;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
public class PersonValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Person.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
// Validation logic
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnpredicates;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
public class RouterConfiguration {
public RouterFunction<ServerResponse> route() {
// tag::snippet[]
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
// end::snippet[]
return route;
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnrequest;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.multipart.FilePartEvent;
import org.springframework.http.codec.multipart.FormPartEvent;
import org.springframework.http.codec.multipart.PartEvent;
import org.springframework.web.reactive.function.server.ServerRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class PartEventHandler {
public void handle(ServerRequest request) {
// tag::snippet[]
request.bodyToFlux(PartEvent.class).windowUntil(PartEvent::isLast)
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) {
String value = formEvent.value();
// handle form field
}
else if (event instanceof FilePartEvent fileEvent) {
String filename = fileEvent.filename();
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
// handle file upload
}
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
}
}
else {
return partEvents; // either complete or error signal
}
return Mono.empty();
}));
// end::snippet[]
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnrequest;
import reactor.core.publisher.Mono;
import org.springframework.web.reactive.function.server.ServerRequest;
public class RequestHandler {
public void bind(ServerRequest request) {
// tag::snippet[]
Mono<Pet> pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));
// end::snippet[]
}
record Pet(String name) { }
}
@@ -0,0 +1,37 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnresponse;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
public class ResponseHandler {
public Mono<ServerResponse> createResponse() {
// tag::snippet[]
Mono<Person> person = getPerson();
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
// end::snippet[]
}
private Mono<Person> getPerson() {
return Mono.just(new Person("foo"));
}
record Person(String name) { }
}
@@ -0,0 +1,78 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnroutes;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.docs.web.webfluxfnhandlerclasses.Person;
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler;
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
public class RouterConfiguration {
public RouterFunction<ServerResponse> routes() {
// tag::snippet[]
PersonRepository repository = getPersonRepository();
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = getOtherRoute();
RouterFunction<ServerResponse> route = route()
// GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
// GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
// POST /person with no additional predicates is mapped to PersonHandler.createPerson
.POST("/person", handler::createPerson)
// otherRoute is a router function that is created elsewhere and added to the route built
.add(otherRoute)
.build();
// end::snippet[]
return route;
}
PersonRepository getPersonRepository() {
return new PersonRepository() {
@Override
public Flux<Person> allPeople() {
return null;
}
@Override
public Mono<Void> savePerson(Mono<Person> person) {
return null;
}
@Override
public Mono<Person> getPerson(int id) {
return null;
}
};
}
RouterFunction<ServerResponse> getOtherRoute() {
return RouterFunctions.route().build();
}
}
@@ -0,0 +1,19 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlerclasses
data class Person(val name: String)
@@ -0,0 +1,62 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlerclasses
import kotlinx.coroutines.flow.Flow
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.awaitBody
import org.springframework.web.reactive.function.server.bodyAndAwait
import org.springframework.web.reactive.function.server.bodyValueAndAwait
import org.springframework.web.reactive.function.server.buildAndAwait
// tag::snippet[]
class PersonHandler(private val repository: PersonRepository) {
// listPeople is a handler function that returns all Person objects found
// in the repository as JSON
suspend fun listPeople(request: ServerRequest): ServerResponse {
val people: Flow<Person> = repository.allPeople()
return ServerResponse.ok().contentType(APPLICATION_JSON).bodyAndAwait(people)
}
// createPerson is a handler function that stores a new Person contained
// in the request body.
// Note that PersonRepository.savePerson(Person) returns Mono<Void>: an empty
// Mono that emits a completion signal when the person has been read from the
// request and stored. So we use the build(Publisher<Void>) method to send a
// response when that completion signal is received (that is, when the Person
// has been saved)
suspend fun createPerson(request: ServerRequest): ServerResponse {
val person = request.awaitBody<Person>()
repository.savePerson(person)
return ServerResponse.ok().buildAndAwait()
}
// getPerson is a handler function that returns a single person, identified by
// the id path variable. We retrieve that Person from the repository and create
// a JSON response, if it is found. If it is not found, we use switchIfEmpty(Mono<T>)
// to return a 404 Not Found response.
suspend fun getPerson(request: ServerRequest): ServerResponse {
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ServerResponse.ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
?: ServerResponse.notFound().buildAndAwait()
}
}
// end::snippet[]
@@ -0,0 +1,28 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlerclasses
import kotlinx.coroutines.flow.Flow
interface PersonRepository {
fun allPeople(): Flow<Person>
suspend fun savePerson(person: Person)
suspend fun getPerson(id: Int): Person?
}
@@ -0,0 +1,58 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlerfilterfunction
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler
import org.springframework.http.HttpStatus.UNAUTHORIZED
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.buildAndAwait
import org.springframework.web.reactive.function.server.coRouter
class RouterConfiguration {
fun route(handler: PersonHandler): RouterFunction<ServerResponse> {
// tag::snippet[]
val securityManager: SecurityManager = getSecurityManager()
val route = coRouter {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("/", handler::listPeople)
POST("/", handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
ServerResponse.status(UNAUTHORIZED).buildAndAwait()
}
}
}
}
// end::snippet[]
return route
}
}
fun getSecurityManager() = object : SecurityManager {
override fun allowAccessTo(path: String): Boolean {
TODO("Not yet implemented")
}
}
@@ -0,0 +1,22 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlerfilterfunction
interface SecurityManager {
fun allowAccessTo(path: String): Boolean
}
@@ -0,0 +1,53 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlervalidation
import org.springframework.docs.web.webfluxfnhandlerclasses.Person
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository
import org.springframework.validation.BeanPropertyBindingResult
import org.springframework.validation.Errors
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.awaitBody
import org.springframework.web.reactive.function.server.buildAndAwait
import org.springframework.web.server.ServerWebInputException
// tag::snippet[]
class PersonHandler(private val repository: PersonRepository) {
// Create Validator instance
private val validator = PersonValidator()
suspend fun createPerson(request: ServerRequest): ServerResponse {
val person = request.awaitBody<Person>()
// Apply validation
validate(person)
repository.savePerson(person)
return ServerResponse.ok().buildAndAwait()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person")
validator.validate(person, errors)
if (errors.hasErrors()) {
// Raise exception for a 400 response
throw ServerWebInputException(errors.toString())
}
}
}
// end::snippet[]
@@ -0,0 +1,32 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnhandlervalidation
import org.springframework.docs.web.webfluxfnhandlerclasses.Person
import org.springframework.validation.Errors
import org.springframework.validation.Validator
class PersonValidator : Validator {
override fun supports(clazz: Class<*>): Boolean {
return Person::class.java.isAssignableFrom(clazz)
}
override fun validate(target: Any, errors: Errors) {
// Validation logic
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnpredicates
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.bodyValueAndAwait
import org.springframework.web.reactive.function.server.coRouter
class RouterConfiguration {
fun route(): RouterFunction<ServerResponse> {
// tag::snippet[]
val route = coRouter {
GET("/hello-world", accept(MediaType.TEXT_PLAIN)) {
ServerResponse.ok().bodyValueAndAwait("Hello World")
}
}
// end::snippet[]
return route
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnrequest
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.http.codec.multipart.FilePartEvent
import org.springframework.http.codec.multipart.FormPartEvent
import org.springframework.http.codec.multipart.PartEvent
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.bodyToFlux
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
class PartEventHandler {
fun handle(request: ServerRequest) {
// tag::snippet[]
request.bodyToFlux<PartEvent>().windowUntil(PartEvent::isLast)
.concatMap {
it.switchOnFirst { signal, partEvents ->
if (signal.hasValue()) {
val event = signal.get()
if (event is FormPartEvent) {
val value: String = event.value()
// handle form field
} else if (event is FilePartEvent) {
val filename: String = event.filename()
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content)
// handle file upload
} else {
return@switchOnFirst Mono.error(RuntimeException("Unexpected event: $event"))
}
} else {
return@switchOnFirst partEvents // either complete or error signal
}
Mono.empty()
}
}
// end::snippet[]
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnrequest
import org.springframework.web.reactive.function.server.ServerRequest
import reactor.core.publisher.Mono
class RequestHandler {
fun bind(request: ServerRequest) {
// tag::snippet[]
val pet: Mono<Pet> = request.bind(Pet::class.java) { dataBinder -> dataBinder.setAllowedFields("name") }
// end::snippet[]
}
data class Pet(val name: String)
}
@@ -0,0 +1,35 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnresponse
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.bodyValueWithTypeAndAwait
class ResponseHandler {
suspend fun createResponse(): ServerResponse {
// tag::snippet[]
val person: Person = getPerson()
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValueWithTypeAndAwait<Person>(person)
// end::snippet[]
}
fun getPerson() = Person("foo")
data class Person(val name: String)
}
@@ -0,0 +1,65 @@
/*
* Copyright 2002-present the original author or 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 org.springframework.docs.web.webfluxfnroutes
import kotlinx.coroutines.flow.Flow
import org.springframework.docs.web.webfluxfnhandlerclasses.Person
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.coRouter
class RouterConfiguration {
fun routes(): RouterFunction<ServerResponse> {
// tag::snippet[]
val repository: PersonRepository = getPersonRepository()
val handler = PersonHandler(repository)
val otherRoute: RouterFunction<ServerResponse> = getOtherRoute()
val route = coRouter {
// GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
// GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople
GET("/person", accept(APPLICATION_JSON), handler::listPeople)
// POST /person with no additional predicates is mapped to PersonHandler.createPerson
POST("/person", handler::createPerson)
// otherRoute is a router function that is created elsewhere and added to the route built
}.and(otherRoute)
// end::snippet[]
return route
}
}
fun getOtherRoute() = coRouter { }
fun getPersonRepository() = object: PersonRepository {
override fun allPeople(): Flow<Person> {
TODO("Not yet implemented")
}
override suspend fun savePerson(person: Person) {
TODO("Not yet implemented")
}
override suspend fun getPerson(id: Int): Person? {
TODO("Not yet implemented")
}
}