Skip to content

Commit

Permalink
Issue 3 (#7)
Browse files Browse the repository at this point in the history
* Remove Response and ResponseType classes, won't be needed

* Remove Response and ResponseType classes, won't be needed

* Remove security dependencies from codegen, available in parsed object

* Remove default injection of httpRequest param in all controller routes which was done for basic auth header value

* Basic structure for generating exception classes and their default global exception handlers

* Basic structure for generating exception classes and their default global exception handlers

* Introduce status in Response parsed from spec

* Add @throws annotation to controller with exceptions mapping to non 200 responses of the operation

* Add test for Non200ResponseHandler

* Add missing annotations to exception handler class
  • Loading branch information
ashwini-desai authored Jul 3, 2020
1 parent bfdd88c commit 4a44ec9
Show file tree
Hide file tree
Showing 25 changed files with 339 additions and 245 deletions.
45 changes: 23 additions & 22 deletions src/main/kotlin/apifi/codegen/ApiBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package apifi.codegen

import apifi.helpers.toCamelCase
import apifi.helpers.toKotlinPoetType
import apifi.helpers.toTitleCase
import apifi.parser.models.Operation
Expand All @@ -11,7 +10,9 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy

object ApiBuilder {

fun build(name: String, paths: List<Path>, securityDependencies: List<SecurityDependency>, basePackageName: String, modelMapping: List<Pair<String, String>>): FileSpec {
private const val micronautHttpAnnotation = "io.micronaut.http.annotation"

fun build(name: String, paths: List<Path>, basePackageName: String, modelMapping: List<Pair<String, String>>): FileSpec {
val baseName = toTitleCase(name)
val controllerClassName = "${baseName}Api"

Expand All @@ -24,27 +25,19 @@ 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("io.micronaut.http.annotation", "Controller"))
.addAnnotation(AnnotationSpec.builder(ClassName(micronautHttpAnnotation, "Controller"))
.build())
.addProperty(controllerProperty)
.addFunctions(generateOperationFunctions(paths, modelMapping, securityDependencies))

securityDependencies.forEach { dependency ->
primaryConstructor.addParameter(
ParameterSpec.builder(toCamelCase(dependency.name), ClassName(dependency.packageName, dependency.name)).build()
)

classSpec.addProperty(PropertySpec.builder(toCamelCase(dependency.name), ClassName(dependency.packageName, dependency.name))
.addModifiers(KModifier.PRIVATE).initializer(toCamelCase(dependency.name)).build())
}
.addFunctions(generateOperationFunctions(paths, basePackageName, modelMapping))

classSpec.primaryConstructor(primaryConstructor.build())

return FileSpec.builder(basePackageName, "$controllerClassName.kt").addType(classSpec.build()).addType(controllerInterfaceClass).build()
}

private fun generateOperationFunctions(paths: List<Path>, modelMapping: List<Pair<String, String>>, securityDependencies: List<SecurityDependency>): List<FunSpec> {
private fun generateOperationFunctions(paths: List<Path>, basePackageName: String, modelMapping: List<Pair<String, String>>): List<FunSpec> {
return paths.flatMap { path ->
path.operations?.map { operation ->
val queryParams = operation.params?.filter { it.type == ParamType.Query }?.map(QueryParamBuilder::build)
Expand All @@ -58,29 +51,36 @@ object ApiBuilder {

val serviceCallStatement = serviceCallStatement(operation, queryParams, pathParams, requestBodyParams)

val httpRequestParam = ParameterSpec.builder("httpRequest",
ClassName("io.micronaut.http", "HttpRequest").parameterizedBy(Any::class.asClassName()))
.build()
val responseType = operation.response?.firstOrNull()?.let { ClassName("io.micronaut.http", "HttpResponse").parameterizedBy(it.toKotlinPoetType(modelMapping)) }
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()}

FunSpec.builder(operation.name)
.also { b -> exceptionAnnotations?.let { b.addAnnotations(it) } }
.addAnnotation(operationTypeAnnotation(operation, path))
.also { b -> operation.request?.consumes?.let { consumes -> b.addAnnotation(operationContentTypeAnnotation(consumes)) } }
.addParameters(queryParams + pathParams + headerParams + requestBodyParams)
.addParameter(httpRequestParam)
.also { responseType?.let { res -> it.returns(res) } }
.addStatement(if (securityDependencies.isNotEmpty()) "return basicauthorizer.authorize(httpRequest.headers.authorization){$serviceCallStatement}" else "return $serviceCallStatement")
.addStatement("return $serviceCallStatement")
.build()
} ?: emptyList()
}
}

private fun operationTypeAnnotation(operation: Operation, path: Path) =
AnnotationSpec.builder(ClassName("io.micronaut.http.annotation", toTitleCase(operation.type.toString())))
AnnotationSpec.builder(ClassName(micronautHttpAnnotation, toTitleCase(operation.type.toString())))
.addMember("value = %S", path.url)
.build()

private fun operationContentTypeAnnotation(consumes: List<String>) =
AnnotationSpec.builder(ClassName("io.micronaut.http.annotation", "Consumes"))
AnnotationSpec.builder(ClassName(micronautHttpAnnotation, "Consumes"))
.also { ab -> consumes.forEach { ab.addMember("%S", it) } }
.build()

Expand All @@ -93,4 +93,5 @@ object ApiBuilder {
return "HttpResponse.ok(controller.${operation.name}(${(queryParamNames + pathParamNames + requestParamNames).joinToString()}))"
}


}
25 changes: 5 additions & 20 deletions src/main/kotlin/apifi/codegen/CodeGenerator.kt
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
package apifi.codegen

import apifi.codegen.security.BasicAuthSecurityStubBuilder
import apifi.codegen.security.BearerAuthSecurityStubBuilder
import apifi.parser.models.SecurityDefinition
import apifi.parser.models.SecurityDefinitionType
import apifi.parser.models.Spec
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.TypeSpec

object CodeGenerator {
fun generate(spec: Spec, basePackageName: String): List<FileSpec> {
val modelFiles: List<FileSpec> = if(spec.models.isNotEmpty()) listOf(ModelFileBuilder.build(spec.models, basePackageName)) else emptyList()
val responseModelFile = ResponseModelBuilder.build(basePackageName)
val modelMapping = (modelFiles + responseModelFile).flatMap { it.members.mapNotNull { m -> (m as TypeSpec).name }.map { name -> name to "${it.packageName}.$name" } }
val securityFiles = spec.securityDefinitions.fold<SecurityDefinition, Map<SecurityDefinition, FileSpec>>(mapOf(), { acc, securityDefinition ->
when(securityDefinition.type) {
SecurityDefinitionType.BASIC_AUTH -> acc + mapOf(securityDefinition to BasicAuthSecurityStubBuilder.build(basePackageName))
SecurityDefinitionType.BEARER -> acc + mapOf(securityDefinition to BearerAuthSecurityStubBuilder.build(basePackageName))
}
})

val securityDependencies = securityFiles
.filter { spec.securityRequirements.contains(it.key.name) }
.map { (def, spec) -> SecurityDependency((spec.members.first() as TypeSpec).name!!, spec.packageName, def.type) }
val modelMapping = modelFiles.flatMap { it.members.mapNotNull { m -> (m as TypeSpec).name }.map { name -> name to "${it.packageName}.$name" } }

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, securityDependencies, basePackageName, modelMapping) }
val apiClassFiles = apiGroups.map { ApiBuilder.build(it.key!!, it.value, basePackageName, modelMapping) }

val exceptionClassesAndHandlerFiles = Non200ResponseHandler.generateExceptionClassesAndHandlers(basePackageName)

return (apiClassFiles + modelFiles + responseModelFile)
return (apiClassFiles + modelFiles + exceptionClassesAndHandlerFiles)
}

}

data class SecurityDependency(val name: String, val packageName: String, val securityDefinitionType: SecurityDefinitionType)
Original file line number Diff line number Diff line change
Expand Up @@ -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.toKotlinPoetType()) }) }
.also { (operation.response?.firstOrNull()?.let { res -> it.returns(res.type.toKotlinPoetType()) }) }
.build()
} ?: emptyList()
}
Expand Down
49 changes: 49 additions & 0 deletions src/main/kotlin/apifi/codegen/ExceptionFileBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package apifi.codegen

import apifi.helpers.toKotlinPoetType
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy

object ExceptionFileBuilder {

fun build(exception: ExceptionDetailsHolder, basePackageName: String): FileSpec {
val packageName = "$basePackageName.exceptions"
val exceptionClassName = exception.exceptionClassName

val builder = FileSpec.builder(packageName, "$exceptionClassName.kt")
builder.addType(
TypeSpec.classBuilder(ClassName(packageName, exceptionClassName))
.superclass(Exception::class)
.addSuperclassConstructorParameter("%L", "message")
.primaryConstructor(
FunSpec.constructorBuilder()
.addParameter(ParameterSpec.builder("message", String::class).build()).build()
).build())

builder.addType(
TypeSpec.classBuilder(ClassName(packageName, "Global${exceptionClassName}Handler"))
.addAnnotation(ClassName("javax.inject", "Singleton"))
.addAnnotation(ClassName("io.micronaut.http.annotation", "Produces"))
.addAnnotation(
AnnotationSpec.builder(ClassName("io.micronaut.context.annotation", "Requires"))
.addMember("classes = [%T::class, %T::class]", ClassName(packageName, exceptionClassName), ClassName("io.micronaut.http.server.exceptions", "ExceptionHandler"))
.build())
.addSuperinterface(
ClassName("io.micronaut.http.server.exceptions", "ExceptionHandler")
.parameterizedBy(ClassName(packageName, exceptionClassName),
ClassName("io.micronaut.http", "HttpResponse").parameterizedBy("String".toKotlinPoetType()))
)
.addFunction(FunSpec.builder("handle")
.addParameter("request", ClassName("io.micronaut.http", "HttpRequest").parameterizedBy("Any".toKotlinPoetType()).copy(nullable = true))
.addParameter("exception", ClassName(packageName, exceptionClassName).copy(nullable = true))
.returns(ClassName("io.micronaut.http", "HttpResponse").parameterizedBy("String".toKotlinPoetType()))
.addStatement("val msg = exception?.conversionError?.cause?.localizedMessage ?: \"${exception.defaultExceptionMessage}\"")
.addStatement("HttpResponse.status<String>(HttpStatus.valueOf(${exception.status}), msg)")
.build()
)
.build()
)
return builder.build()
}

}
30 changes: 30 additions & 0 deletions src/main/kotlin/apifi/codegen/Non200ResponseHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package apifi.codegen

import org.apache.http.HttpStatus

object Non200ResponseHandler {

private val allExceptionDetailsHolder = listOf(
ExceptionDetailsHolder(HttpStatus.SC_BAD_REQUEST, "BadRequestException", "Bad Request"),
ExceptionDetailsHolder(HttpStatus.SC_UNAUTHORIZED, "UnauthorizedException", "Unauthorized Request"),
ExceptionDetailsHolder(HttpStatus.SC_FORBIDDEN, "ForbiddenException", "Forbidden Request"),
ExceptionDetailsHolder(HttpStatus.SC_NOT_FOUND, "NotFoundException", "Request Not Found"),
ExceptionDetailsHolder(HttpStatus.SC_INTERNAL_SERVER_ERROR, "InternalServerErrorException", "Internal server error occured")
)

fun getExceptionClassFor(statuses: List<Int>) = statuses.map { status -> allExceptionDetailsHolder.find { it.status == status }?.exceptionClassName ?: "InternalServerErrorException" }

fun generateExceptionClassesAndHandlers(basePackageName: String) =
allExceptionDetailsHolder.map { exception ->
ExceptionFileBuilder.build(exception, basePackageName)
}


}


data class ExceptionDetailsHolder(
val status: Int,
val exceptionClassName: String,
val defaultExceptionMessage: String
)
37 changes: 0 additions & 37 deletions src/main/kotlin/apifi/codegen/ResponseModelBuilder.kt

This file was deleted.

This file was deleted.

This file was deleted.

4 changes: 2 additions & 2 deletions src/main/kotlin/apifi/parser/PathsParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ object PathsParser {
val responses = ResponseBodyParser.parse(operation.responses, operationSpecifier)
models.addAll(request?.second ?: emptyList())
models.addAll(responses?.second ?: emptyList())
Operation(httpMethod, operation.operationId
?: toCamelCase(httpMethod.toString()), operation.tags, params, request?.first, responses?.first)
Operation(httpMethod, operation.operationId ?: toCamelCase(httpMethod.toString()),
operation.tags, params, request?.first, responses?.first)
}
Path(endpoint, operations)
} ?: emptyList()) to models
Expand Down
Loading

0 comments on commit 4a44ec9

Please sign in to comment.