From 86c66fa37f852c685c10299a38c8b4c4e6d35fa8 Mon Sep 17 00:00:00 2001 From: Bhimsen Padalkar <54049838+bhimsenp@users.noreply.github.com> Date: Tue, 21 Jul 2020 15:45:00 +0530 Subject: [PATCH] [WIP] - Refactoring (#8) * Renamed response to responses * Using ParseResult to represent the model which contains both actual result & produced models * Remove files from outside of codegen Co-authored-by: Ashwini Desai --- .../main/kotlin/apifi/codegen/ApiBuilder.kt | 66 +++++++------------ .../kotlin/apifi/codegen/CodeGenerator.kt | 7 +- .../codegen/ControllerInterfaceBuilder.kt | 6 +- .../kotlin/apifi/codegen/HeaderBuilder.kt | 2 +- .../kotlin/apifi/codegen/ModelFileBuilder.kt | 2 +- .../apifi/codegen/OperationExtension.kt | 27 ++++++++ .../apifi/codegen/PathVariableBuilder.kt | 2 +- .../kotlin/apifi/codegen/QueryParamBuilder.kt | 2 +- .../apifi/codegen/RequestBodyBuilder.kt | 2 +- .../main/kotlin/apifi/helpers/Functions.kt | 8 +-- .../main/kotlin/apifi/parser/PathsParser.kt | 13 ++-- .../kotlin/apifi/parser/RequestBodyParser.kt | 10 +-- .../kotlin/apifi/parser/ResponseBodyParser.kt | 22 ++++--- .../kotlin/apifi/parser/SpecFileParser.kt | 10 +-- .../kotlin/apifi/parser/models/ParseResult.kt | 5 ++ .../kotlin/apifi/{ => parser}/models/Spec.kt | 5 +- .../kotlin/apifi/codegen/ApiBuilderTest.kt | 17 ++--- .../codegen/ControllerInterfaceBuilderTest.kt | 2 +- .../kotlin/apifi/codegen/HeaderBuilderTest.kt | 4 +- .../apifi/codegen/PathVariableBuilderTest.kt | 4 +- .../apifi/codegen/QueryParamBuilderTest.kt | 4 +- .../apifi/codegen/RequestBodyBuilderTest.kt | 2 +- .../kotlin/apifi/parser/PathsParserTest.kt | 12 ++-- .../apifi/parser/RequestBodyParserTest.kt | 14 ++-- .../apifi/parser/ResponseBodyParserTest.kt | 14 ++-- 25 files changed, 136 insertions(+), 126 deletions(-) create mode 100644 codegen/src/main/kotlin/apifi/codegen/OperationExtension.kt create mode 100644 codegen/src/main/kotlin/apifi/parser/models/ParseResult.kt rename codegen/src/main/kotlin/apifi/{ => parser}/models/Spec.kt (95%) diff --git a/codegen/src/main/kotlin/apifi/codegen/ApiBuilder.kt b/codegen/src/main/kotlin/apifi/codegen/ApiBuilder.kt index 4e68a0a..6eb6657 100644 --- a/codegen/src/main/kotlin/apifi/codegen/ApiBuilder.kt +++ b/codegen/src/main/kotlin/apifi/codegen/ApiBuilder.kt @@ -1,19 +1,17 @@ package apifi.codegen import apifi.codegen.exceptions.Non200ResponseHandler -import apifi.helpers.toKotlinPoetType import apifi.helpers.toTitleCase -import apifi.models.Operation -import apifi.models.ParamType -import apifi.models.Path +import apifi.parser.models.Operation +import apifi.parser.models.Path +import apifi.parser.models.Response import com.squareup.kotlinpoet.* -import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy object ApiBuilder { private const val micronautHttpAnnotation = "io.micronaut.http.annotation" - fun build(name: String, paths: List, basePackageName: String, modelMapping: List>): FileSpec { + fun build(name: String, paths: List, basePackageName: String, modelMapping: Map): FileSpec { val baseName = toTitleCase(name) val controllerClassName = "${baseName}Api" @@ -26,7 +24,6 @@ object ApiBuilder { val controllerProperty = PropertySpec.builder("controller", ClassName(basePackageName, controllerInterfaceClass.name!!)) .addModifiers(KModifier.PRIVATE).initializer("controller").build() - val classSpec = TypeSpec.classBuilder(ClassName(basePackageName, controllerClassName)) .addAnnotation(AnnotationSpec.builder(ClassName(micronautHttpAnnotation, "Controller")) .build()) @@ -38,37 +35,16 @@ object ApiBuilder { return FileSpec.builder(basePackageName, "$controllerClassName.kt").addType(classSpec.build()).addType(controllerInterfaceClass).build() } - private fun generateOperationFunctions(paths: List, basePackageName: String, modelMapping: List>): List { + private fun generateOperationFunctions(paths: List, basePackageName: String, modelMapping: Map): List { return paths.flatMap { path -> path.operations?.map { operation -> - val queryParams = operation.params?.filter { it.type == ParamType.Query }?.map(QueryParamBuilder::build) - ?: emptyList() - val pathParams = operation.params?.filter { it.type == ParamType.Path }?.map(PathVariableBuilder::build) - ?: emptyList() - val headerParams = operation.params?.filter { it.type == ParamType.Header }?.map(HeaderBuilder::build) - ?: emptyList() - val requestBodyParams = operation.request?.let { listOf(RequestBodyBuilder.build(it.type, modelMapping)) } - ?: emptyList() - - val serviceCallStatement = serviceCallStatement(operation, queryParams, pathParams, requestBodyParams) - - val responseType = operation.response?.firstOrNull { it.defaultOrStatus == "200" || it.defaultOrStatus == "201" }?.let { ClassName("io.micronaut.http", "HttpResponse").parameterizedBy(it.type.toKotlinPoetType(modelMapping)) } - - val non2xxStatusResponseFromOperation = operation.response?.filter { it.defaultOrStatus != "default" && it.defaultOrStatus != "200" && it.defaultOrStatus != "201" }?.map { it.defaultOrStatus.toInt() } - - val exceptionClassesForNon2xxResponses = non2xxStatusResponseFromOperation?.let { Non200ResponseHandler.getExceptionClassFor(it) } - - val exceptionAnnotations = exceptionClassesForNon2xxResponses?.map { exceptionClass -> - AnnotationSpec.builder(Throws::class) - .addMember("%T::class", ClassName("$basePackageName.exceptions", exceptionClass)) - .build()} - + val serviceCallStatement = controllerCallStatement(operation, modelMapping) FunSpec.builder(operation.name) - .also { b -> exceptionAnnotations?.let { b.addAnnotations(it) } } + .also { b -> operation.responses?.let { b.addAnnotations(operationExceptionAnnotations(it, basePackageName)) } } .addAnnotation(operationTypeAnnotation(operation, path)) .also { b -> operation.request?.consumes?.let { consumes -> b.addAnnotation(operationContentTypeAnnotation(consumes)) } } - .addParameters(queryParams + pathParams + headerParams + requestBodyParams) - .also { responseType?.let { res -> it.returns(res) } } + .addParameters(operation.queryParams() + operation.pathParams() + operation.headerParams() + operation.requestParams(modelMapping)) + .also { operation.returnType(modelMapping)?.let { rt -> it.returns(rt) } } .addStatement("return $serviceCallStatement") .build() } ?: emptyList() @@ -85,14 +61,22 @@ object ApiBuilder { .also { ab -> consumes.forEach { ab.addMember("%S", it) } } .build() - private fun serviceCallStatement(operation: Operation, queryParams: List, pathParams: List, requestBodyParams: List): String { - val queryParamNames = queryParams.map { it.name } - val pathParamNames = pathParams.map { it.name } - val requestParamNames = operation.request?.let { req -> - if (req.consumes?.contains("multipart/form-data") == true) "java.io.File.createTempFile(body.filename, \"\").also { it.writeBytes(body.bytes) }" else requestBodyParams.joinToString { it.name } - }?.let { listOf(it) } ?: emptyList() - return "HttpResponse.ok(controller.${operation.name}(${(queryParamNames + pathParamNames + requestParamNames).joinToString()}))" + private fun operationExceptionAnnotations(responses: List, basePackageName: String): List { + val non2xxStatusResponseFromOperation = responses.filter { it.defaultOrStatus != "default" && it.defaultOrStatus != "200" && it.defaultOrStatus != "201" }.map { it.defaultOrStatus.toInt() } + val exceptionClassesForNon2xxResponses = non2xxStatusResponseFromOperation.let { Non200ResponseHandler.getExceptionClassFor(it) } + return exceptionClassesForNon2xxResponses.map { exceptionClass -> + AnnotationSpec.builder(Throws::class) + .addMember("%T::class", ClassName("$basePackageName.exceptions", exceptionClass)) + .build() + } } + private fun controllerCallStatement(operation: Operation, modelMapping: Map): String { + val queryParamNames = operation.queryParams().map { it.name } + val pathParamNames = operation.pathParams().map { it.name } + val requestBodyParams = operation.requestParams(modelMapping) + val requestParamNames = requestBodyParams.map { it.name } + return "HttpResponse.ok(controller.${operation.name}(${(queryParamNames + pathParamNames + requestParamNames).joinToString()}))" + } -} +} \ No newline at end of file diff --git a/codegen/src/main/kotlin/apifi/codegen/CodeGenerator.kt b/codegen/src/main/kotlin/apifi/codegen/CodeGenerator.kt index ab88c1f..4e520d2 100644 --- a/codegen/src/main/kotlin/apifi/codegen/CodeGenerator.kt +++ b/codegen/src/main/kotlin/apifi/codegen/CodeGenerator.kt @@ -1,15 +1,14 @@ package apifi.codegen import apifi.codegen.exceptions.Non200ResponseHandler -import apifi.models.Spec +import apifi.parser.models.Spec import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.TypeSpec object CodeGenerator { fun generate(spec: Spec, basePackageName: String): List { - val modelFiles: List = if(spec.models.isNotEmpty()) listOf(ModelFileBuilder.build(spec.models, basePackageName)) else emptyList() - val modelMapping = modelFiles.flatMap { it.members.mapNotNull { m -> (m as TypeSpec).name }.map { name -> name to "${it.packageName}.$name" } } - + val modelFiles: List = if (spec.models.isNotEmpty()) listOf(ModelFileBuilder.build(spec.models, basePackageName)) else emptyList() + val modelMapping = modelFiles.flatMap { it.members.mapNotNull { m -> (m as TypeSpec).name }.map { name -> name to "${it.packageName}.$name" } }.toMap() val apiGroups = spec.paths.groupBy { it.operations?.firstOrNull { o -> o.tags != null }?.tags?.firstOrNull() }.filter { it.key != null } val apiClassFiles = apiGroups.map { ApiBuilder.build(it.key!!, it.value, basePackageName, modelMapping) } diff --git a/codegen/src/main/kotlin/apifi/codegen/ControllerInterfaceBuilder.kt b/codegen/src/main/kotlin/apifi/codegen/ControllerInterfaceBuilder.kt index fe281eb..402e5e4 100644 --- a/codegen/src/main/kotlin/apifi/codegen/ControllerInterfaceBuilder.kt +++ b/codegen/src/main/kotlin/apifi/codegen/ControllerInterfaceBuilder.kt @@ -1,8 +1,8 @@ package apifi.codegen import apifi.helpers.toKotlinPoetType -import apifi.models.ParamType -import apifi.models.Path +import apifi.parser.models.ParamType +import apifi.parser.models.Path import com.squareup.kotlinpoet.* object ControllerInterfaceBuilder { @@ -25,7 +25,7 @@ object ControllerInterfaceBuilder { .addModifiers(KModifier.ABSTRACT) .addParameters(params) .also { requestBodyParam?.let { req -> it.addParameter(req) } } - .also { (operation.response?.firstOrNull()?.let { res -> it.returns(res.type.toKotlinPoetType()) }) } + .also { (operation.responses?.firstOrNull()?.let { res -> it.returns(res.type.toKotlinPoetType()) }) } .build() } ?: emptyList() } diff --git a/codegen/src/main/kotlin/apifi/codegen/HeaderBuilder.kt b/codegen/src/main/kotlin/apifi/codegen/HeaderBuilder.kt index 5a20ba8..28c600b 100644 --- a/codegen/src/main/kotlin/apifi/codegen/HeaderBuilder.kt +++ b/codegen/src/main/kotlin/apifi/codegen/HeaderBuilder.kt @@ -1,6 +1,6 @@ package apifi.codegen -import apifi.models.Param +import apifi.parser.models.Param import apifi.helpers.toCamelCase import apifi.helpers.toKotlinPoetType import com.squareup.kotlinpoet.AnnotationSpec diff --git a/codegen/src/main/kotlin/apifi/codegen/ModelFileBuilder.kt b/codegen/src/main/kotlin/apifi/codegen/ModelFileBuilder.kt index ee097b0..d5cb50b 100644 --- a/codegen/src/main/kotlin/apifi/codegen/ModelFileBuilder.kt +++ b/codegen/src/main/kotlin/apifi/codegen/ModelFileBuilder.kt @@ -7,7 +7,7 @@ import com.squareup.kotlinpoet.* object ModelFileBuilder { fun build(models: List, basePackageName: String): FileSpec { val packageName = "$basePackageName.models" - val baseModelMapping = models.map { it.name to "$packageName.${it.name}" } + val baseModelMapping = models.map { it.name to "$packageName.${it.name}" }.toMap() val builder = FileSpec.builder(packageName, "Models.kt") models.forEach { model -> builder.addType( diff --git a/codegen/src/main/kotlin/apifi/codegen/OperationExtension.kt b/codegen/src/main/kotlin/apifi/codegen/OperationExtension.kt new file mode 100644 index 0000000..64f6dd7 --- /dev/null +++ b/codegen/src/main/kotlin/apifi/codegen/OperationExtension.kt @@ -0,0 +1,27 @@ +package apifi.codegen + +import apifi.helpers.toKotlinPoetType +import apifi.parser.models.Operation +import apifi.parser.models.ParamType +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy + +fun Operation.pathParams() = params?.filter { it.type == ParamType.Path }?.map(PathVariableBuilder::build) + ?: emptyList() + +fun Operation.queryParams() = params?.filter { it.type == ParamType.Query }?.map(QueryParamBuilder::build) + ?: emptyList() + +fun Operation.headerParams() = params?.filter { it.type == ParamType.Header }?.map(HeaderBuilder::build) + ?: emptyList() + +fun Operation.requestParams(modelMapping: Map) = request?.let { + listOf(ParameterSpec.builder("body", it.type.toKotlinPoetType(modelMapping)) + .addAnnotation(ClassName("io.micronaut.http.annotation", "Body")) + .build()) +} ?: emptyList() + +fun Operation.returnType(modelMapping: Map) = responses + ?.firstOrNull { it.defaultOrStatus == "200" || it.defaultOrStatus == "201" } + ?.let { ClassName("io.micronaut.http", "HttpResponse").parameterizedBy(it.type.toKotlinPoetType(modelMapping)) } \ No newline at end of file diff --git a/codegen/src/main/kotlin/apifi/codegen/PathVariableBuilder.kt b/codegen/src/main/kotlin/apifi/codegen/PathVariableBuilder.kt index abf42af..7b76301 100644 --- a/codegen/src/main/kotlin/apifi/codegen/PathVariableBuilder.kt +++ b/codegen/src/main/kotlin/apifi/codegen/PathVariableBuilder.kt @@ -1,7 +1,7 @@ package apifi.codegen import apifi.helpers.toKotlinPoetType -import apifi.models.Param +import apifi.parser.models.Param import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ParameterSpec diff --git a/codegen/src/main/kotlin/apifi/codegen/QueryParamBuilder.kt b/codegen/src/main/kotlin/apifi/codegen/QueryParamBuilder.kt index 4569fe1..11fbf57 100644 --- a/codegen/src/main/kotlin/apifi/codegen/QueryParamBuilder.kt +++ b/codegen/src/main/kotlin/apifi/codegen/QueryParamBuilder.kt @@ -1,7 +1,7 @@ package apifi.codegen import apifi.helpers.toKotlinPoetType -import apifi.models.Param +import apifi.parser.models.Param import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ParameterSpec diff --git a/codegen/src/main/kotlin/apifi/codegen/RequestBodyBuilder.kt b/codegen/src/main/kotlin/apifi/codegen/RequestBodyBuilder.kt index 496e77f..245e0f3 100644 --- a/codegen/src/main/kotlin/apifi/codegen/RequestBodyBuilder.kt +++ b/codegen/src/main/kotlin/apifi/codegen/RequestBodyBuilder.kt @@ -5,7 +5,7 @@ import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ParameterSpec object RequestBodyBuilder { - fun build(bodyType: String, modelMapping: List>): ParameterSpec = + fun build(bodyType: String, modelMapping: Map): ParameterSpec = ParameterSpec.builder("body", bodyType.toKotlinPoetType(modelMapping)) .addAnnotation(ClassName("io.micronaut.http.annotation", "Body")) .build() diff --git a/codegen/src/main/kotlin/apifi/helpers/Functions.kt b/codegen/src/main/kotlin/apifi/helpers/Functions.kt index 449bda2..579226b 100644 --- a/codegen/src/main/kotlin/apifi/helpers/Functions.kt +++ b/codegen/src/main/kotlin/apifi/helpers/Functions.kt @@ -15,10 +15,10 @@ fun toCamelCase(s: String): String = CaseUtils.toCamelCase(s, false, '_', '-', ' fun Schema.toCodeGenModel(name: String): CodegenModel = KotlinClientCodegen().fromModel(name, this, null) fun Schema.toCodeGenModel(): CodegenModel = this.toCodeGenModel("any") -fun String.toKotlinPoetType(): TypeName = toKotlinPoetType(emptyList()) +fun String.toKotlinPoetType(): TypeName = toKotlinPoetType(emptyMap()) -fun String.toKotlinPoetType(packageNameMapping: List>): TypeName { - val withPackage = { name: String -> ClassName.bestGuess(packageNameMapping.find { name == it.first }?.second ?: name) } +fun String.toKotlinPoetType(packageNameMapping: Map): TypeName { + val withPackage = { name: String -> ClassName.bestGuess(packageNameMapping[name] ?: name) } return if (this.contains("<")) { val parts = this.split('<') val primaryType = parts[0] @@ -27,4 +27,4 @@ fun String.toKotlinPoetType(packageNameMapping: List>): Typ } else { withPackage(this) } -} +} \ No newline at end of file diff --git a/codegen/src/main/kotlin/apifi/parser/PathsParser.kt b/codegen/src/main/kotlin/apifi/parser/PathsParser.kt index 6c61a71..2f9150a 100644 --- a/codegen/src/main/kotlin/apifi/parser/PathsParser.kt +++ b/codegen/src/main/kotlin/apifi/parser/PathsParser.kt @@ -4,13 +4,14 @@ import apifi.helpers.toCamelCase import apifi.helpers.toCodeGenModel import apifi.helpers.toTitleCase import apifi.models.* +import apifi.parser.models.* import io.swagger.v3.oas.models.PathItem import io.swagger.v3.oas.models.Paths object PathsParser { - fun parse(paths: Paths?): Pair, List> { + fun parse(paths: Paths?): ParseResult> { val models = mutableListOf() - return (paths?.map { (endpoint, config) -> + return ParseResult(paths?.map { (endpoint, config) -> val operations = config.readOperationsMap().map { (httpMethod, operation) -> val params = operation.parameters?.map { param -> Param(param.name, param.schema.toCodeGenModel().dataType, param.required, ParamType.fromString(param.`in`)) @@ -18,14 +19,14 @@ object PathsParser { val operationSpecifier = operationSpecifier(operation, httpMethod, endpoint) val request = RequestBodyParser.parse(operation.requestBody, operationSpecifier) val responses = ResponseBodyParser.parse(operation.responses, operationSpecifier) - models.addAll(request?.second ?: emptyList()) - models.addAll(responses?.second ?: emptyList()) + models.addAll(request?.models ?: emptyList()) + models.addAll(responses?.models ?: emptyList()) Operation(httpMethod, operation.operationId ?: toCamelCase(httpMethod.toString()), - operation.tags, params, request?.first, responses?.first) + operation.tags, params, request?.result, responses?.result) } Path(endpoint, operations) - } ?: emptyList()) to models + } ?: emptyList(), models) } private fun operationSpecifier(operation: io.swagger.v3.oas.models.Operation, httpMethod: PathItem.HttpMethod, endpoint: String) = diff --git a/codegen/src/main/kotlin/apifi/parser/RequestBodyParser.kt b/codegen/src/main/kotlin/apifi/parser/RequestBodyParser.kt index 7a421a6..5e7d190 100644 --- a/codegen/src/main/kotlin/apifi/parser/RequestBodyParser.kt +++ b/codegen/src/main/kotlin/apifi/parser/RequestBodyParser.kt @@ -2,21 +2,21 @@ package apifi.parser import apifi.parser.ModelParser.parseReference import apifi.parser.ModelParser.shouldCreateModel -import apifi.models.Model -import apifi.models.Request +import apifi.parser.models.ParseResult +import apifi.parser.models.Request import io.swagger.v3.oas.models.media.ArraySchema import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.oas.models.parameters.RequestBody object RequestBodyParser { - fun parse(requestBody: RequestBody?, operationSpecifier: String): Pair>? { + fun parse(requestBody: RequestBody?, operationSpecifier: String): ParseResult? { val consumes = requestBody?.content?.keys?.toList() ?: emptyList() return if (consumes.contains("multipart/form-data")) { - Request("io.micronaut.http.multipart.CompleteFileUpload", consumes) to emptyList() + ParseResult(Request("io.micronaut.http.multipart.CompleteFileUpload", consumes), emptyList()) } else requestBody?.content?.entries?.firstOrNull()?.value?.schema?.let { if (shouldCreateModel(it)) ModelParser.modelsFromSchema(requestModelName(operationSpecifier), it).let { m -> requestBodyType(m.first().name, it) to m } else parseReference(it) to emptyList() - }?.let { Request(it.first, consumes) to it.second } + }?.let { ParseResult(Request(it.first, consumes), it.second) } } private fun requestBodyType(modelName: String, schema: Schema) = if (schema is ArraySchema) "kotlin.Array<$modelName>" else modelName diff --git a/codegen/src/main/kotlin/apifi/parser/ResponseBodyParser.kt b/codegen/src/main/kotlin/apifi/parser/ResponseBodyParser.kt index d5abdbe..70098a9 100644 --- a/codegen/src/main/kotlin/apifi/parser/ResponseBodyParser.kt +++ b/codegen/src/main/kotlin/apifi/parser/ResponseBodyParser.kt @@ -1,22 +1,24 @@ package apifi.parser import apifi.parser.ModelParser.parseReference -import apifi.models.Model -import apifi.models.Response +import apifi.parser.models.ParseResult +import apifi.parser.models.Response import io.swagger.v3.oas.models.media.ArraySchema import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.oas.models.responses.ApiResponses object ResponseBodyParser { - fun parse(responses: ApiResponses?, operationSpecifier: String): Pair, List>? { - return responses?.mapValues { r -> r.value.content?.values!!.first().schema }?.map { - if (ModelParser.shouldCreateModel(it.value)) ModelParser.modelsFromSchema(responseModelName(operationSpecifier), it.value).let { m -> Response(it.key, responseBodyType(m.first().name, it.value)) to m } - else Response(it.key, parseReference(it.value)) to emptyList() - }?.let { it.map { p -> p.first } to it.flatMap { p -> p.second } } - } + fun parse(responses: ApiResponses?, operationSpecifier: String): ParseResult>? { + return responses?.mapValues { r -> r.value.content?.values!!.first().schema }?.map { + if (ModelParser.shouldCreateModel(it.value)) + ModelParser.modelsFromSchema(responseModelName(operationSpecifier), it.value) + .let { m -> Response(it.key, responseBodyType(m.first().name, it.value)) to m } + else Response(it.key, parseReference(it.value)) to emptyList() + }?.let { ParseResult(it.map { p -> p.first }, it.flatMap { p -> p.second }) } + } - private fun responseBodyType(modelName: String, schema: Schema) = if (schema is ArraySchema) "kotlin.Array<$modelName>" else modelName + private fun responseBodyType(modelName: String, schema: Schema) = if (schema is ArraySchema) "kotlin.Array<$modelName>" else modelName - private fun responseModelName(operationSpecifier: String) = "${operationSpecifier.capitalize()}Response" + private fun responseModelName(operationSpecifier: String) = "${operationSpecifier.capitalize()}Response" } \ No newline at end of file diff --git a/codegen/src/main/kotlin/apifi/parser/SpecFileParser.kt b/codegen/src/main/kotlin/apifi/parser/SpecFileParser.kt index 351adfb..73a7847 100644 --- a/codegen/src/main/kotlin/apifi/parser/SpecFileParser.kt +++ b/codegen/src/main/kotlin/apifi/parser/SpecFileParser.kt @@ -1,17 +1,17 @@ package apifi.parser -import apifi.models.SecurityDefinition -import apifi.models.SecurityDefinitionType -import apifi.models.Spec +import apifi.parser.models.SecurityDefinition +import apifi.parser.models.SecurityDefinitionType +import apifi.parser.models.Spec import io.swagger.v3.oas.models.OpenAPI object SpecFileParser { fun parse(openApiSpec: OpenAPI): Spec { val paths = PathsParser.parse(openApiSpec.paths) val models = (openApiSpec.components?.schemas?.flatMap { (name, schema) -> ModelParser.modelsFromSchema(name, schema) } - ?: emptyList()) + paths.second + ?: emptyList()) + paths.models val securitySchemes = openApiSpec.components?.securitySchemes?.entries?.map { scheme -> SecurityDefinition(scheme.key, SecurityDefinitionType.fromTypeAndScheme(scheme.value.type, scheme.value.scheme)) } ?: emptyList() val securityRequirements = openApiSpec.security?.flatMap { it.keys } ?: emptyList() - return Spec(paths.first, models, securityRequirements, securitySchemes) + return Spec(paths.result, models, securityRequirements, securitySchemes) } } \ No newline at end of file diff --git a/codegen/src/main/kotlin/apifi/parser/models/ParseResult.kt b/codegen/src/main/kotlin/apifi/parser/models/ParseResult.kt new file mode 100644 index 0000000..bc635ef --- /dev/null +++ b/codegen/src/main/kotlin/apifi/parser/models/ParseResult.kt @@ -0,0 +1,5 @@ +package apifi.parser.models + +import apifi.models.Model + +data class ParseResult(val result: T, val models: List) \ No newline at end of file diff --git a/codegen/src/main/kotlin/apifi/models/Spec.kt b/codegen/src/main/kotlin/apifi/parser/models/Spec.kt similarity index 95% rename from codegen/src/main/kotlin/apifi/models/Spec.kt rename to codegen/src/main/kotlin/apifi/parser/models/Spec.kt index 54ce6a7..4c9f70d 100644 --- a/codegen/src/main/kotlin/apifi/models/Spec.kt +++ b/codegen/src/main/kotlin/apifi/parser/models/Spec.kt @@ -1,5 +1,6 @@ -package apifi.models +package apifi.parser.models +import apifi.models.Model import io.swagger.v3.oas.models.PathItem import io.swagger.v3.oas.models.security.SecurityScheme @@ -13,7 +14,7 @@ data class Operation( val tags: List?, val params: List?, val request: Request?, - val response: List?, + val responses: List?, val securitySchemeType: SecurityDefinitionType = SecurityDefinitionType.BASIC_AUTH ) diff --git a/codegen/src/test/kotlin/apifi/codegen/ApiBuilderTest.kt b/codegen/src/test/kotlin/apifi/codegen/ApiBuilderTest.kt index 8866a99..21068e4 100644 --- a/codegen/src/test/kotlin/apifi/codegen/ApiBuilderTest.kt +++ b/codegen/src/test/kotlin/apifi/codegen/ApiBuilderTest.kt @@ -1,13 +1,13 @@ package apifi.codegen -import apifi.models.* +import apifi.parser.models.* import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.TypeSpec +import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.collections.shouldContain -import io.kotest.matchers.string.shouldContain import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.string.shouldContain import io.swagger.v3.oas.models.PathItem class ApiBuilderTest : DescribeSpec({ @@ -111,15 +111,6 @@ class ApiBuilderTest : DescribeSpec({ ")" } - it("should convert multipart file to java file") { - val operation = Operation(PathItem.HttpMethod.POST, "uploadDocument", emptyList(), emptyList(), Request("io.micronaut.http.multipart.CompleteFileUpload", listOf("multipart/form-data")), null) - - val api = ApiBuilder.build("pets", listOf(Path("/pets", listOf(operation))), "apifi.gen", modelMapping()) - - val apiClass = api.members[0] as TypeSpec - apiClass.funSpecs[0].body.toString().trimIndent() shouldBe "return HttpResponse.ok(controller.uploadDocument(java.io.File.createTempFile(body.filename, \"\").also { it.writeBytes(body.bytes) }))" - } - it("should add @throws annotation for all non 200 responses returned from an operation") { val request = Request("Pet", listOf("application/json", "text/plain")) val responses = listOf(Response("200", "PetResponse"), Response("400", "kotlin.String"), Response("403", "kotlin.String")) @@ -147,4 +138,4 @@ class ApiBuilderTest : DescribeSpec({ }) -fun modelMapping() = listOf("Pet" to "models.Pet", "PetResponse" to "models.PetResponse") +fun modelMapping() = mapOf("Pet" to "models.Pet", "PetResponse" to "models.PetResponse") \ No newline at end of file diff --git a/codegen/src/test/kotlin/apifi/codegen/ControllerInterfaceBuilderTest.kt b/codegen/src/test/kotlin/apifi/codegen/ControllerInterfaceBuilderTest.kt index c36576c..b418e8e 100644 --- a/codegen/src/test/kotlin/apifi/codegen/ControllerInterfaceBuilderTest.kt +++ b/codegen/src/test/kotlin/apifi/codegen/ControllerInterfaceBuilderTest.kt @@ -1,6 +1,6 @@ package apifi.codegen -import apifi.models.* +import apifi.parser.models.* import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.DescribeSpec import io.swagger.v3.oas.models.PathItem diff --git a/codegen/src/test/kotlin/apifi/codegen/HeaderBuilderTest.kt b/codegen/src/test/kotlin/apifi/codegen/HeaderBuilderTest.kt index 4873af4..e9de564 100644 --- a/codegen/src/test/kotlin/apifi/codegen/HeaderBuilderTest.kt +++ b/codegen/src/test/kotlin/apifi/codegen/HeaderBuilderTest.kt @@ -1,7 +1,7 @@ package apifi.codegen -import apifi.models.Param -import apifi.models.ParamType +import apifi.parser.models.Param +import apifi.parser.models.ParamType import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.DescribeSpec diff --git a/codegen/src/test/kotlin/apifi/codegen/PathVariableBuilderTest.kt b/codegen/src/test/kotlin/apifi/codegen/PathVariableBuilderTest.kt index 66617af..f15009a 100644 --- a/codegen/src/test/kotlin/apifi/codegen/PathVariableBuilderTest.kt +++ b/codegen/src/test/kotlin/apifi/codegen/PathVariableBuilderTest.kt @@ -1,7 +1,7 @@ package apifi.codegen -import apifi.models.Param -import apifi.models.ParamType +import apifi.parser.models.Param +import apifi.parser.models.ParamType import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.DescribeSpec diff --git a/codegen/src/test/kotlin/apifi/codegen/QueryParamBuilderTest.kt b/codegen/src/test/kotlin/apifi/codegen/QueryParamBuilderTest.kt index 33a8b7c..ff73839 100644 --- a/codegen/src/test/kotlin/apifi/codegen/QueryParamBuilderTest.kt +++ b/codegen/src/test/kotlin/apifi/codegen/QueryParamBuilderTest.kt @@ -1,7 +1,7 @@ package apifi.codegen -import apifi.models.Param -import apifi.models.ParamType +import apifi.parser.models.Param +import apifi.parser.models.ParamType import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.DescribeSpec diff --git a/codegen/src/test/kotlin/apifi/codegen/RequestBodyBuilderTest.kt b/codegen/src/test/kotlin/apifi/codegen/RequestBodyBuilderTest.kt index 9e571cb..39eb8c2 100644 --- a/codegen/src/test/kotlin/apifi/codegen/RequestBodyBuilderTest.kt +++ b/codegen/src/test/kotlin/apifi/codegen/RequestBodyBuilderTest.kt @@ -7,7 +7,7 @@ class RequestBodyBuilderTest : DescribeSpec( { describe("Request Body Builder") { it("should generate request body type") { - val requestBody = RequestBodyBuilder.build("Pet", listOf("Pet" to "models.Pet")) + val requestBody = RequestBodyBuilder.build("Pet", mapOf("Pet" to "models.Pet")) requestBody.toString() shouldBe "@io.micronaut.http.annotation.Body body: models.Pet" } } diff --git a/codegen/src/test/kotlin/apifi/parser/PathsParserTest.kt b/codegen/src/test/kotlin/apifi/parser/PathsParserTest.kt index 7e79dd0..343722e 100644 --- a/codegen/src/test/kotlin/apifi/parser/PathsParserTest.kt +++ b/codegen/src/test/kotlin/apifi/parser/PathsParserTest.kt @@ -1,7 +1,7 @@ package apifi.parser -import apifi.models.Param -import apifi.models.ParamType +import apifi.parser.models.Param +import apifi.parser.models.ParamType import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe import io.swagger.v3.oas.models.PathItem.HttpMethod @@ -14,7 +14,7 @@ class PathsParserTest : DescribeSpec({ it("with no params") { val file = FileUtils.getFile("src", "test-res", "parser", "params", "with-no-params.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI - val path = PathsParser.parse(openApi.paths).first[0] + val path = PathsParser.parse(openApi.paths).result[0] path.url shouldBe "/pets" path.operations!![0].params shouldBe null path.operations!![0].type shouldBe HttpMethod.GET @@ -24,7 +24,7 @@ class PathsParserTest : DescribeSpec({ it("with query params") { val file = FileUtils.getFile("src", "test-res", "parser", "params", "with-query-params.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI - val path = PathsParser.parse(openApi.paths).first[0] + val path = PathsParser.parse(openApi.paths).result[0] path.url shouldBe "/pets" path.operations!![0].type shouldBe HttpMethod.POST path.operations!![0].params!![0] shouldBe Param("limit", "kotlin.Int", false, ParamType.Query) @@ -33,7 +33,7 @@ class PathsParserTest : DescribeSpec({ it("with path params") { val file = FileUtils.getFile("src", "test-res", "parser", "params", "with-path-params.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI - val path = PathsParser.parse(openApi.paths).first[0] + val path = PathsParser.parse(openApi.paths).result[0] path.url shouldBe "/pets/{petId}" path.operations!![0].type shouldBe HttpMethod.GET path.operations!![0].params!![0] shouldBe Param("petId", "kotlin.String", true, ParamType.Path) @@ -42,7 +42,7 @@ class PathsParserTest : DescribeSpec({ it("with headers") { val file = FileUtils.getFile("src", "test-res", "parser", "params", "with-header-params.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI - val path = PathsParser.parse(openApi.paths).first[0] + val path = PathsParser.parse(openApi.paths).result[0] path.url shouldBe "/pets" path.operations!![0].type shouldBe HttpMethod.POST path.operations!![0].params!![0] shouldBe Param("x-header", "kotlin.String", true, ParamType.Header) diff --git a/codegen/src/test/kotlin/apifi/parser/RequestBodyParserTest.kt b/codegen/src/test/kotlin/apifi/parser/RequestBodyParserTest.kt index 3e16b11..40a7b96 100644 --- a/codegen/src/test/kotlin/apifi/parser/RequestBodyParserTest.kt +++ b/codegen/src/test/kotlin/apifi/parser/RequestBodyParserTest.kt @@ -2,7 +2,7 @@ package apifi.parser import apifi.models.Model import apifi.models.Property -import apifi.models.Request +import apifi.parser.models.Request import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.DescribeSpec import io.swagger.v3.parser.OpenAPIV3Parser @@ -15,16 +15,16 @@ class RequestBodyParserTest : DescribeSpec({ val file = FileUtils.getFile("src", "test-res", "parser", "models", "with-separate-schema.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI val request = RequestBodyParser.parse(openApi.paths["/pets"]?.post?.requestBody, "showById") - request?.first shouldBe Request("kotlin.Array", listOf("application/json")) - request?.second shouldBe emptyList() + request?.result shouldBe Request("kotlin.Array", listOf("application/json")) + request?.models shouldBe emptyList() } it("should parse request body with inline body") { val file = FileUtils.getFile("src", "test-res", "parser", "models", "with-inline-request-response-schema.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI val request = RequestBodyParser.parse(openApi.paths["/pets"]?.post?.requestBody, "showById") - request?.first shouldBe Request("kotlin.Array", listOf("application/json")) - request?.second shouldBe listOf( + request?.result shouldBe Request("kotlin.Array", listOf("application/json")) + request?.models shouldBe listOf( Model("ShowByIdRequest", listOf( Property("id", "kotlin.Long", false), Property("name", "kotlin.String", false), @@ -37,8 +37,8 @@ class RequestBodyParserTest : DescribeSpec({ val file = FileUtils.getFile("src", "test-res", "parser", "request", "with-multipart-content-type.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI val request = RequestBodyParser.parse(openApi.paths["/pet/{id}/uploadDoc"]?.post?.requestBody, "uploadDocument") - request?.first shouldBe Request("io.micronaut.http.multipart.CompleteFileUpload", listOf("multipart/form-data")) - request?.second shouldBe emptyList() + request?.result shouldBe Request("io.micronaut.http.multipart.CompleteFileUpload", listOf("multipart/form-data")) + request?.models shouldBe emptyList() } } }) \ No newline at end of file diff --git a/codegen/src/test/kotlin/apifi/parser/ResponseBodyParserTest.kt b/codegen/src/test/kotlin/apifi/parser/ResponseBodyParserTest.kt index 3e09773..9e307f6 100644 --- a/codegen/src/test/kotlin/apifi/parser/ResponseBodyParserTest.kt +++ b/codegen/src/test/kotlin/apifi/parser/ResponseBodyParserTest.kt @@ -2,7 +2,7 @@ package apifi.parser import apifi.models.Model import apifi.models.Property -import apifi.models.Response +import apifi.parser.models.Response import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.DescribeSpec import io.swagger.v3.parser.OpenAPIV3Parser @@ -15,16 +15,16 @@ class ResponseBodyParserTest : DescribeSpec({ val file = FileUtils.getFile("src", "test-res", "parser", "models", "with-separate-schema.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI val response = ResponseBodyParser.parse(openApi.paths["/pets/{petId}"]?.get?.responses, "showByPetId") - response?.first shouldBe listOf(Response("200", "Pet"), Response("default", "Error")) - response?.second shouldBe emptyList() + response?.result shouldBe listOf(Response("200", "Pet"), Response("default", "Error")) + response?.models shouldBe emptyList() } it("should parse response body with inline schema") { val file = FileUtils.getFile("src", "test-res", "parser", "models", "with-inline-request-response-schema.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI val response = ResponseBodyParser.parse(openApi.paths["/pets"]?.post?.responses, "showByPetId") - response?.first shouldBe listOf(Response("default", "ShowByPetIdResponse")) - response?.second shouldBe listOf( + response?.result shouldBe listOf(Response("default", "ShowByPetIdResponse")) + response?.models shouldBe listOf( Model("ShowByPetIdResponse", listOf( Property("code", "kotlin.Int", false), Property("message", "kotlin.String", false) @@ -36,8 +36,8 @@ class ResponseBodyParserTest : DescribeSpec({ val file = FileUtils.getFile("src", "test-res", "parser", "models", "with-non-200-responses.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI val response = ResponseBodyParser.parse(openApi.paths["/pets/{petId}"]?.get?.responses, "showByPetId") - response?.first shouldBe listOf(Response("200", "PetResponse"), Response("400", "kotlin.String"), Response("default", "Error")) - response?.second shouldBe emptyList() + response?.result shouldBe listOf(Response("200", "PetResponse"), Response("400", "kotlin.String"), Response("default", "Error")) + response?.models shouldBe emptyList() } } }) \ No newline at end of file