Skip to content

Commit

Permalink
Method binding with no @Body TCK tests, updated docs and docs example…
Browse files Browse the repository at this point in the history
…s. (micronaut-projects#9479)

* Add http-tck tests for json and form data body and controller with no `@Body` annotation.
Update binding docs, and add docs examples for java, groovy, kotlin.

closes micronaut-projects#9388
  • Loading branch information
wetted committed Jun 23, 2023
1 parent a666245 commit f98a9f9
Show file tree
Hide file tree
Showing 14 changed files with 614 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 original authors
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -23,6 +23,7 @@
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Status;
Expand Down Expand Up @@ -116,6 +117,31 @@ void testCustomListBodyPOJOReactiveTypes() throws IOException {
.build()));
}

@Test
void testRequestBodyJsonNoBodyAnnotation() throws IOException {
String body = "{\"x\":10,\"y\":20}";
asserts(SPEC_NAME,
HttpRequest.POST("/response-body/args-no-body", body)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request,
HttpResponseAssertion.builder()
.status(HttpStatus.CREATED)
.body(BodyAssertion.builder().body(body).equals())
.build()));
}

@Test
void testRequestBodyFormDataNoBodyAnnotation() throws IOException {
asserts(SPEC_NAME,
HttpRequest.POST("/response-body/args-no-body-form", "x=10&y=20")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request,
HttpResponseAssertion.builder()
.status(HttpStatus.CREATED)
.body(BodyAssertion.builder().body("{\"x\":10,\"y\":20}").equals())
.build()));
}

@Controller("/response-body")
@Requires(property = "spec.name", value = SPEC_NAME)
static class BodyController {
Expand All @@ -126,6 +152,19 @@ Point post(@Body Point data) {
return data;
}

@Post(uri = "/args-no-body")
@Status(HttpStatus.CREATED)
Point postNoBody(Integer x, Integer y) {
return new Point(x, y);
}

@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Post("/args-no-body-form")
@Status(HttpStatus.CREATED)
Point postNoBodyFormData(Integer x, Integer y) {
return new Point(x, y);
}

@Post(uri = "/part-pojo")
@Status(HttpStatus.CREATED)
Point postPart(@Body("point") Point data) {
Expand Down Expand Up @@ -163,6 +202,11 @@ static class Point {
private Integer x;
private Integer y;

public Point(Integer x, Integer y) {
this.x = x;
this.y = y;
}

public Integer getX() {
return x;
}
Expand Down
7 changes: 6 additions & 1 deletion src/main/docs/guide/httpServer/binding.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,10 @@ The Micronaut framework tries to populate method arguments in the following orde
. URI variables like `/{id}`.
. From query parameters if the request is a `GET` request (e.g. `?foo=bar`).
. If there is a `@Body` and request allows the body, bind the body to it.
. If the request can have a body and no `@Body` is defined then try to parse the body (either JSON or form data) and bind the method arguments from the body.
. If the request can have a body and no `@Body` is defined then try to parse the body (either JSON or form data) and bind the method arguments from the body (see the example).
. Finally, if the method arguments cannot be populated return `400 BAD REQUEST`.

snippet::io.micronaut.docs.server.binding.PointController[tags="class", indent=0, title="Binding Method Arguments From Body with no `@Body`"]

<1> JSON request body binds to method controller arguments, e.g. '{"x":10,"y":20}' (with "application/json")
<2> Form data also works, e.g. 'x=10&y=20' (with "application/x-www-form-urlencoded")
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2017-2023 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.docs.server.binding

import io.micronaut.core.annotation.Introspected

@Introspected
class Point {

Integer x
Integer y

Point(Integer x, Integer y) {
this.x = x
this.y = y
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2017-2023 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.docs.server.binding

import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Consumes
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.Status

@Requires(property = 'spec.name', value = 'PointControllerSpec')
// tag::class[]
@Controller("/point")
class PointController {

@Post(uri = "/no-body-json")
@Status(HttpStatus.CREATED)
Point noBodyJson(Integer x, Integer y) { // (1)
new Point(x,y)
}

@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Post("/no-body-form")
@Status(HttpStatus.CREATED)
Point noBodyForm(Integer x, Integer y) { // (2)
new Point(x,y)
}
}
// end::class[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2017-2023 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.docs.server.binding

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpHeaders
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class PointControllerSpec extends Specification {

@Shared
@AutoCleanup
EmbeddedServer embeddedServer = ApplicationContext.run(
EmbeddedServer, ['spec.name': 'PointControllerSpec'])
@Shared
@AutoCleanup
HttpClient client = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.getURL())

void "test JSON with no @Body endpoint"() {
given:
HttpRequest<String> httpRequest = HttpRequest
.POST("/point/no-body-json", '{"x":10,"y":20}')
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)

when:
HttpResponse<Point> response = client.toBlocking().exchange(httpRequest, Point)

then:
assertResult(response.body.orElse(null))
}

void "test Form data with no @Body endpoint"() {
given:
HttpRequest<String> httpRequest = HttpRequest
.POST("/point/no-body-form", 'x=10&y=20')
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED)

when:
HttpResponse<Point> response = client.toBlocking().exchange(httpRequest, Point)

then:
assertResult(response.body.orElse(null))
}

void assertResult(Point p) {
p
p.x == 10
p.y == 20
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2017-2023 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.docs.server.binding

import io.micronaut.core.annotation.Introspected

@Introspected
data class Point (val x: Int, val y: Int)

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2017-2023 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.docs.server.binding

import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Consumes
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.Status

@Requires(property = "spec.name", value = "PointControllerTest")
// tag::class[]
@Controller("/point")
class PointController {

@Post(uri = "/no-body-json")
@Status(HttpStatus.CREATED)
fun noBodyJson(x: Int, y: Int) = Point(x,y) // (1)

@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Post("/no-body-form")
@Status(HttpStatus.CREATED)
fun noBodyForm(x: Int, y: Int) = Point(x,y) // (2)
}
// end::class[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2017-2023 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.docs.server.binding

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpHeaders
import io.micronaut.http.HttpRequest
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
import io.micronaut.runtime.server.EmbeddedServer
import java.util.Map

class PointControllerTest : StringSpec() {

val embeddedServer = autoClose(
ApplicationContext.run(EmbeddedServer::class.java, Map.of<String, Any>("spec.name", "PointControllerTest"))
)

val client = autoClose(
embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL())
)

init {
"test JSON with no @Body endpoint"() {
val httpRequest: HttpRequest<String> = HttpRequest
.POST("/point/no-body-json", "{\"x\":10,\"y\":20}")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
val response = client!!.toBlocking().exchange(httpRequest, Point::class.java)

assertResult(response.body.orElse(null))
}

"test Form data with no @Body endpoint"() {
val httpRequest: HttpRequest<String> = HttpRequest
.POST("/point/no-body-form", "x=10&y=20")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED)
val response = client!!.toBlocking().exchange(httpRequest, Point::class.java)

assertResult(response.body.orElse(null))
}
}

private fun assertResult(p: Point) {
p shouldNotBe null
p.x shouldBe 10
p.y shouldBe 20
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2017-2023 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.docs.server.binding

import io.micronaut.core.annotation.Introspected

@Introspected
data class Point (val x: Int, val y: Int)

Loading

0 comments on commit f98a9f9

Please sign in to comment.