diff --git a/src/main/groovy/com/github/hauner/openapi/spring/converter/ApiConverter.groovy b/src/main/groovy/com/github/hauner/openapi/spring/converter/ApiConverter.groovy index 017c8e68..70af384b 100644 --- a/src/main/groovy/com/github/hauner/openapi/spring/converter/ApiConverter.groovy +++ b/src/main/groovy/com/github/hauner/openapi/spring/converter/ApiConverter.groovy @@ -18,6 +18,7 @@ package com.github.hauner.openapi.spring.converter import com.github.hauner.openapi.spring.model.Api import com.github.hauner.openapi.spring.model.Endpoint +import com.github.hauner.openapi.spring.model.RequestBody import com.github.hauner.openapi.spring.model.parameters.CookieParameter import com.github.hauner.openapi.spring.model.parameters.HeaderParameter import com.github.hauner.openapi.spring.model.parameters.Parameter as ModelParameter @@ -88,6 +89,26 @@ class ApiConverter { ep.parameters.addAll (createParameter(parameter, target, resolver)) } + if (httpOperation.requestBody != null) { + def required = httpOperation.requestBody.required != null ?: false + httpOperation.requestBody.content.each { Map.Entry requestBodyEntry -> + def contentType = requestBodyEntry.key + def requestBody = requestBodyEntry.value + + def info = new SchemaInfo (requestBody.schema, getInlineTypeName (path)) + info.resolver = resolver + + DataType dataType = dataTypeConverter.convert (info, target.models) + + def body = new RequestBody( + contentType: contentType, + requestBodyType: dataType, + required: required) + + ep.requestBodies.add (body) + } + } + httpOperation.responses.each { Map.Entry responseEntry -> def httpStatus = responseEntry.key def httpResponse = responseEntry.value @@ -136,6 +157,10 @@ class ApiConverter { } } + private String getInlineTypeName (String path) { + StringUtil.toCamelCase (path.substring (1)) + 'RequestBody' + } + private String getInlineResponseName (String path, String httpStatus) { StringUtil.toCamelCase (path.substring (1)) + 'Response' + httpStatus } @@ -189,4 +214,5 @@ class ApiConverter { private boolean hasTags (op) { op.tags && !op.tags.empty } + } diff --git a/src/main/groovy/com/github/hauner/openapi/spring/model/Endpoint.groovy b/src/main/groovy/com/github/hauner/openapi/spring/model/Endpoint.groovy index 7443fa6a..8a283e80 100644 --- a/src/main/groovy/com/github/hauner/openapi/spring/model/Endpoint.groovy +++ b/src/main/groovy/com/github/hauner/openapi/spring/model/Endpoint.groovy @@ -27,8 +27,13 @@ class Endpoint { String path HttpMethod method - List responses = [] List parameters = [] + List requestBodies = [] + List responses = [] + + RequestBody getRequestBody () { + requestBodies.first () + } Response getResponse () { responses.first () diff --git a/src/main/groovy/com/github/hauner/openapi/spring/model/RequestBody.groovy b/src/main/groovy/com/github/hauner/openapi/spring/model/RequestBody.groovy new file mode 100644 index 00000000..5d68e787 --- /dev/null +++ b/src/main/groovy/com/github/hauner/openapi/spring/model/RequestBody.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2019 the 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 + * + * http://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 com.github.hauner.openapi.spring.model + +import com.github.hauner.openapi.spring.model.datatypes.DataType + +/** + * Endpoint request body properties. + * + * @author Martin Hauner + */ +class RequestBody { + + String contentType + DataType requestBodyType + boolean required + + Set getImports () { + requestBodyType.imports + } + + String getAnnotationName () { + "RequestBody" + } + + String getAnnotationWithPackage () { + "org.springframework.web.bind.annotation.${annotationName}" + } + + String getAnnotation () { + "@${annotationName}" + } + +} diff --git a/src/main/groovy/com/github/hauner/openapi/spring/model/Response.groovy b/src/main/groovy/com/github/hauner/openapi/spring/model/Response.groovy index a0491472..22f42b25 100644 --- a/src/main/groovy/com/github/hauner/openapi/spring/model/Response.groovy +++ b/src/main/groovy/com/github/hauner/openapi/spring/model/Response.groovy @@ -20,7 +20,7 @@ import com.github.hauner.openapi.spring.model.datatypes.DataType import com.github.hauner.openapi.spring.model.datatypes.NoneDataType /** - * Endpoint response properties, + * Endpoint response properties. * * @author Martin Hauner */ diff --git a/src/main/groovy/com/github/hauner/openapi/spring/writer/MethodWriter.groovy b/src/main/groovy/com/github/hauner/openapi/spring/writer/MethodWriter.groovy index 73782e78..0f49b077 100644 --- a/src/main/groovy/com/github/hauner/openapi/spring/writer/MethodWriter.groovy +++ b/src/main/groovy/com/github/hauner/openapi/spring/writer/MethodWriter.groovy @@ -17,6 +17,7 @@ package com.github.hauner.openapi.spring.writer import com.github.hauner.openapi.spring.model.Endpoint +import com.github.hauner.openapi.spring.model.RequestBody import com.github.hauner.openapi.spring.model.parameters.Parameter import com.github.hauner.openapi.support.Identifier @@ -39,6 +40,11 @@ class MethodWriter { mapping += "(" mapping += 'path = ' + quote(endpoint.path) + if (!endpoint.requestBodies.empty) { + mapping += ", " + mapping += 'consumes = {' + quote(endpoint.requestBody.contentType) + '}' + } + if (!endpoint.response.empty) { mapping += ", " mapping += 'produces = {' + quote(endpoint.response.contentType) + '}' @@ -68,6 +74,17 @@ class MethodWriter { param } + private String createRequestBodyAnnotation (RequestBody requestBody) { + String param = "${requestBody.annotation}" + + // required is default, so add required only if the parameter is not required + if (!requestBody.required) { + param += '(required = false)' + } + + param + } + private String createMethodName (Endpoint endpoint) { def tokens = endpoint.path.tokenize ('/') tokens = tokens.collect { Identifier.fromJson (it).capitalize () } @@ -86,6 +103,12 @@ class MethodWriter { } + if (!endpoint.requestBodies.empty) { + def body = endpoint.requestBody + def param = "${createRequestBodyAnnotation(body)} ${body.requestBodyType.name} body" + ps.add (param) + } + ps.join (', ') } diff --git a/src/test/groovy/com/github/hauner/openapi/spring/converter/ApiConverterParameterSpec.groovy b/src/test/groovy/com/github/hauner/openapi/spring/converter/ApiConverterParameterSpec.groovy index f49b7248..f7636cd7 100644 --- a/src/test/groovy/com/github/hauner/openapi/spring/converter/ApiConverterParameterSpec.groovy +++ b/src/test/groovy/com/github/hauner/openapi/spring/converter/ApiConverterParameterSpec.groovy @@ -217,4 +217,5 @@ paths: e.name == 'foo' e.type == 'unknown' } + } diff --git a/src/test/groovy/com/github/hauner/openapi/spring/converter/ApiConverterRequestBodySpec.groovy b/src/test/groovy/com/github/hauner/openapi/spring/converter/ApiConverterRequestBodySpec.groovy new file mode 100644 index 00000000..f8fe9330 --- /dev/null +++ b/src/test/groovy/com/github/hauner/openapi/spring/converter/ApiConverterRequestBodySpec.groovy @@ -0,0 +1,66 @@ +/* + * Copyright 2019 the 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 + * + * http://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 com.github.hauner.openapi.spring.converter + +import spock.lang.Specification + +import static com.github.hauner.openapi.spring.support.OpenApiParser.parse + +class ApiConverterRequestBodySpec extends Specification { + + void "converts request body parameter"() { + def openApi = parse ( +"""\ +openapi: 3.0.2 +info: + title: test request body parameter + version: 1.0.0 + +paths: + /endpoint: + + get: + tags: + - endpoint + requestBody: + content: + application/json: + schema: + type: object + properties: + foo: + type: string + responses: + '204': + description: empty +""") + + when: + def api = new ApiConverter ().convert (openApi) + + then: + def itf = api.interfaces.first () + def ep = itf.endpoints.first () + def body = ep.requestBodies.first () + body.contentType == 'application/json' + body.requestBodyType.type == 'EndpointRequestBody' + !body.required + body.annotation == '@RequestBody' + body.annotationWithPackage == 'org.springframework.web.bind.annotation.RequestBody' + } + +} diff --git a/src/test/groovy/com/github/hauner/openapi/spring/writer/MethodWriterSpec.groovy b/src/test/groovy/com/github/hauner/openapi/spring/writer/MethodWriterSpec.groovy index 8d8599e7..1a65bb65 100644 --- a/src/test/groovy/com/github/hauner/openapi/spring/writer/MethodWriterSpec.groovy +++ b/src/test/groovy/com/github/hauner/openapi/spring/writer/MethodWriterSpec.groovy @@ -18,6 +18,7 @@ package com.github.hauner.openapi.spring.writer import com.github.hauner.openapi.spring.model.Endpoint import com.github.hauner.openapi.spring.model.HttpMethod +import com.github.hauner.openapi.spring.model.RequestBody import com.github.hauner.openapi.spring.model.Response import com.github.hauner.openapi.spring.model.datatypes.BooleanDataType import com.github.hauner.openapi.spring.model.datatypes.CollectionDataType @@ -322,4 +323,47 @@ class MethodWriterSpec extends Specification { """ } + void "writes required request body parameter" () { + def endpoint = new Endpoint (path: '/foo', method: HttpMethod.POST, responses: [ + new Response (contentType: 'application/json', responseType: new NoneDataType()) + ], requestBodies: [ + new RequestBody( + contentType: 'application/json', + requestBodyType: new ObjectDataType (type: 'FooRequestBody', + properties: ['foo': new StringDataType ()] as LinkedHashMap), + required: true) + ]) + + when: + writer.write (target, endpoint) + + then: + target.toString () == """\ + @PostMapping(path = "${endpoint.path}", consumes = {"application/json"}) + ResponseEntity postFoo(@RequestBody FooRequestBody body); +""" + } + + void "writes optional request body parameter" () { + def endpoint = new Endpoint (path: '/foo', method: HttpMethod.POST, responses: [ + new Response (contentType: 'application/json', responseType: new NoneDataType()) + ], requestBodies: [ + new RequestBody( + contentType: 'application/json', + requestBodyType: new ObjectDataType ( + type: 'FooRequestBody', + properties: ['foo': new StringDataType ()] as LinkedHashMap), + required: false) + ]) + + when: + writer.write (target, endpoint) + + then: + target.toString () == """\ + @PostMapping(path = "${endpoint.path}", consumes = {"application/json"}) + ResponseEntity postFoo(@RequestBody(required = false) FooRequestBody body); +""" + } + } diff --git a/src/testInt/groovy/com/github/hauner/openapi/generatr/GeneratrEndToEndTest.groovy b/src/testInt/groovy/com/github/hauner/openapi/generatr/GeneratrEndToEndTest.groovy index 2849832e..6f5b64f9 100644 --- a/src/testInt/groovy/com/github/hauner/openapi/generatr/GeneratrEndToEndTest.groovy +++ b/src/testInt/groovy/com/github/hauner/openapi/generatr/GeneratrEndToEndTest.groovy @@ -17,7 +17,7 @@ package com.github.hauner.openapi.generatr import org.junit.runner.RunWith -import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized /** @@ -30,12 +30,14 @@ class GeneratrEndToEndTest extends GeneratrTestBase { @Parameterized.Parameters(name = "{0}") static Collection sources () { return [ + new TestSet(name: 'ref-into-another-file'), new TestSet(name: 'no-response-content'), new TestSet(name: 'response-simple-data-types'), new TestSet(name: 'response-complex-data-types'), - new TestSet(name: 'ref-into-another-file'), + new TestSet(name: 'response-array-data-type-mapping'), new TestSet(name: 'params-simple-data-types'), - new TestSet(name: 'response-array-data-type-mapping') + new TestSet(name: 'params-path-simple-data-types'), + new TestSet(name: 'params-request-body') ] } diff --git a/src/testInt/groovy/com/github/hauner/openapi/generatr/GeneratrPendingTest.groovy b/src/testInt/groovy/com/github/hauner/openapi/generatr/GeneratrPendingTest.groovy index c406a223..9d15e406 100644 --- a/src/testInt/groovy/com/github/hauner/openapi/generatr/GeneratrPendingTest.groovy +++ b/src/testInt/groovy/com/github/hauner/openapi/generatr/GeneratrPendingTest.groovy @@ -27,7 +27,7 @@ class GeneratrPendingTest extends GeneratrTestBase { @Parameterized.Parameters(name = "{0}") static Collection sources () { return [ - new TestSet(name: 'path-params-simple-data-types') + new TestSet(name: 'params-request-body') ] } diff --git a/src/testInt/resources/path-params-simple-data-types/generated/api/EndpointApi.java b/src/testInt/resources/params-path-simple-data-types/generated/api/EndpointApi.java similarity index 100% rename from src/testInt/resources/path-params-simple-data-types/generated/api/EndpointApi.java rename to src/testInt/resources/params-path-simple-data-types/generated/api/EndpointApi.java diff --git a/src/testInt/resources/path-params-simple-data-types/openapi.yaml b/src/testInt/resources/params-path-simple-data-types/openapi.yaml similarity index 100% rename from src/testInt/resources/path-params-simple-data-types/openapi.yaml rename to src/testInt/resources/params-path-simple-data-types/openapi.yaml diff --git a/src/testInt/resources/params-request-body/generated/api/Api.java b/src/testInt/resources/params-request-body/generated/api/Api.java new file mode 100644 index 00000000..a697cf81 --- /dev/null +++ b/src/testInt/resources/params-request-body/generated/api/Api.java @@ -0,0 +1,17 @@ +/* + * This class is auto generated by https://github.com/hauner/openapi-generatr-spring. + * DO NOT EDIT. + */ + +package generated.api; + +import generated.model.Book; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; + +public interface Api { + + @PostMapping(path = "/book", consumes = {"application/json"}, produces = {"application/json"}) + ResponseEntity postBook(@RequestBody Book body); + +} diff --git a/src/testInt/resources/params-request-body/generated/model/Book.java b/src/testInt/resources/params-request-body/generated/model/Book.java new file mode 100644 index 00000000..82ea3638 --- /dev/null +++ b/src/testInt/resources/params-request-body/generated/model/Book.java @@ -0,0 +1,34 @@ +/* + * This class is auto generated by https://github.com/hauner/openapi-generatr-spring. + * DO NOT EDIT. + */ + +package generated.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Book { + + @JsonProperty("isbn") + private String isbn; + + @JsonProperty("title") + private String title; + + public String getIsbn () { + return isbn; + } + + public void setIsbn (String isbn) { + this.isbn = isbn; + } + + public String getTitle () { + return title; + } + + public void setTitle (String title) { + this.title = title; + } + +} diff --git a/src/testInt/resources/params-request-body/openapi.yaml b/src/testInt/resources/params-request-body/openapi.yaml new file mode 100644 index 00000000..10d94a02 --- /dev/null +++ b/src/testInt/resources/params-request-body/openapi.yaml @@ -0,0 +1,31 @@ +openapi: 3.0.2 +info: + title: test request body parameters + version: 1.0.0 + +paths: + /book: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + required: true + responses: + '201': + description: created book + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + +components: + schemas: + Book: + type: object + properties: + isbn: + type: string + title: + type: string