Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for InputStream #292

Merged
merged 2 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ This section documents the available CLI parameters for controlling what gets ge
| | CHOOSE ANY OF: |
| | `DATETIME_AS_INSTANT` - Use `Instant` as the datetime type. Defaults to `OffsetDateTime` |
| | `DATETIME_AS_LOCALDATETIME` - Use `LocalDateTime` as the datetime type. Defaults to `OffsetDateTime` |
| | `BYTEARRAY_AS_INPUTSTREAM` - Use `InputStream` as ByteArray type. Defaults to `ByteArray` |
| `--validation-library` | Specify which validation library to use for annotations in generated model classes. Default: JAVAX_VALIDATION |
| | CHOOSE ONE OF: |
| | `JAVAX_VALIDATION` - Use `javax.validation` annotations in generated model classes (default) |
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ enum class ControllerCodeGenTargetType(val description: String) {

enum class CodeGenTypeOverride(val description: String) {
DATETIME_AS_INSTANT("Use `Instant` as the datetime type. Defaults to `OffsetDateTime`"),
DATETIME_AS_LOCALDATETIME("Use `LocalDateTime` as the datetime type. Defaults to `OffsetDateTime`");
DATETIME_AS_LOCALDATETIME("Use `LocalDateTime` as the datetime type. Defaults to `OffsetDateTime`"),
BYTEARRAY_AS_INPUTSTREAM("Use `InputStream` as ByteArray type. Defaults to `ByteArray`");
override fun toString() = "`${super.toString()}` - $description"
}

Expand Down
12 changes: 11 additions & 1 deletion src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.isNotDefined
import com.cjbooms.fabrikt.util.KaizenParserExtensions.isOneOfSuperInterfaceWithDiscriminator
import com.cjbooms.fabrikt.util.ModelNameRegistry
import com.reprezen.kaizen.oasparser.model3.Schema
import java.io.ByteArrayInputStream
import java.math.BigDecimal
import java.net.URI
import java.time.LocalDate
Expand All @@ -31,6 +32,7 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
object Uuid : KotlinTypeInfo(UUID::class)
object Uri : KotlinTypeInfo(URI::class)
object ByteArray : KotlinTypeInfo(kotlin.ByteArray::class)
object InputStream : KotlinTypeInfo(java.io.InputStream::class)
object Boolean : KotlinTypeInfo(kotlin.Boolean::class)
object UntypedObject : KotlinTypeInfo(Any::class)
object AnyType : KotlinTypeInfo(Any::class)
Expand Down Expand Up @@ -70,7 +72,7 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
OasType.Uuid -> Uuid
OasType.Uri -> Uri
OasType.Base64String -> ByteArray
OasType.Binary -> ByteArray
OasType.Binary -> getOverridableByteArray()
OasType.Double -> Double
OasType.Float -> Float
OasType.Number -> Numeric
Expand Down Expand Up @@ -116,5 +118,13 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
else -> DateTime
}
}

private fun getOverridableByteArray(): KotlinTypeInfo {
val typeOverrides = MutableSettings.typeOverrides()
return when {
CodeGenTypeOverride.BYTEARRAY_AS_INPUTSTREAM in typeOverrides -> InputStream
else -> ByteArray
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.cjbooms.fabrikt.generators

import com.cjbooms.fabrikt.cli.CodeGenTypeOverride
import com.cjbooms.fabrikt.cli.CodeGenerationType
import com.cjbooms.fabrikt.cli.ControllerCodeGenOptionType
import com.cjbooms.fabrikt.cli.ControllerCodeGenTargetType
Expand Down Expand Up @@ -209,4 +210,21 @@ class KtorControllerInterfaceGeneratorTest {

assertThat(fileStr.trim()).isEqualTo(expectedControllers.trim())
}

@Test
fun `ensure generates ByteArrayStream body parameter and response for string with format binary`() {
MutableSettings.addOption(CodeGenTypeOverride.BYTEARRAY_AS_INPUTSTREAM)
val api = SourceApi(readTextResource("/examples/byteArrayStream/api.yaml"))
val generator = KtorControllerInterfaceGenerator(
Packages(basePackage),
api
)
val controllers = generator.generate()
val lib = generator.generateLibrary()

val fileStr = controllers.toSingleFile(lib)
val expectedControllers = readTextResource("/examples/byteArrayStream/controllers/ktor/Controllers.kt")

assertThat(fileStr.trim()).isEqualTo(expectedControllers.trim())
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.cjbooms.fabrikt.generators

import com.cjbooms.fabrikt.cli.CodeGenTypeOverride
import com.cjbooms.fabrikt.cli.CodeGenerationType
import com.cjbooms.fabrikt.cli.ControllerCodeGenOptionType
import com.cjbooms.fabrikt.cli.ControllerCodeGenTargetType
Expand Down Expand Up @@ -228,4 +229,14 @@ class MicronautControllerGeneratorTest {

assertThat(controllers.trim()).isEqualTo(expectedControllers.trim())
}

@Test
fun `ensure generates ByteArrayStream body parameter and response for string with format binary`() {
MutableSettings.addOption(CodeGenTypeOverride.BYTEARRAY_AS_INPUTSTREAM)
val api = SourceApi(readTextResource("/examples/byteArrayStream/api.yaml"))
val controllers = MicronautControllerInterfaceGenerator(Packages(basePackage), api, JavaxValidationAnnotations).generate().toSingleFile()
val expectedControllers = readTextResource("/examples/byteArrayStream/controllers/micronaut/Controllers.kt")

assertThat(controllers.trim()).isEqualTo(expectedControllers.trim())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class ModelGeneratorTest {
"openapi310",
"binary",
"oneOfMarkerInterface",
"byteArrayStream",
)

@BeforeEach
Expand Down Expand Up @@ -91,6 +92,9 @@ class ModelGeneratorTest {
if (testCaseName == "mapExamplesNonNullValues") {
MutableSettings.addOption(ModelCodeGenOptionType.NON_NULL_MAP_VALUES)
}
if (testCaseName == "byteArrayStream") {
MutableSettings.addOption(CodeGenTypeOverride.BYTEARRAY_AS_INPUTSTREAM)
}
val basePackage = "examples.${testCaseName.replace("/", ".")}"
val apiLocation = javaClass.getResource("/examples/$testCaseName/api.yaml")!!
val sourceApi = SourceApi(apiLocation.readText(), baseDir = Paths.get(apiLocation.toURI()))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.cjbooms.fabrikt.generators

import com.cjbooms.fabrikt.cli.CodeGenTypeOverride
import com.cjbooms.fabrikt.cli.CodeGenerationType
import com.cjbooms.fabrikt.cli.ControllerCodeGenOptionType
import com.cjbooms.fabrikt.cli.ValidationLibrary
Expand Down Expand Up @@ -246,6 +247,15 @@ class SpringControllerGeneratorTest {
}

@Test
fun `ensure generates ByteArrayStream body parameter and response for string with format binary`() {
MutableSettings.addOption(CodeGenTypeOverride.BYTEARRAY_AS_INPUTSTREAM)
val api = SourceApi(readTextResource("/examples/byteArrayStream/api.yaml"))
val controllers = SpringControllerInterfaceGenerator(Packages(basePackage), api, JavaxValidationAnnotations).generate().toSingleFile()
val expectedControllers = readTextResource("/examples/byteArrayStream/controllers/spring/Controllers.kt")

assertThat(controllers.trim()).isEqualTo(expectedControllers.trim())
}

fun `controller functions are wrapped by CompletionStage`() {
val basePackage = "examples.completionStage"
val api = SourceApi(readTextResource("/examples/githubApi/api.yaml"))
Expand Down
32 changes: 32 additions & 0 deletions src/test/resources/examples/byteArrayStream/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
openapi: 3.0.1
info:
description: Testing binary body and binary response
title: Test
version: '0.0'
paths:
/binary-data:
post:
operationId: postBinaryData
requestBody:
required: true
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
200:
description: Success
content:
application/octet-stream:
schema:
type: string
format: binary

components:
schemas:
BinaryData:
properties:
binaryValue:
type: string
format: binary
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package ie.zalando.controllers

import io.ktor.http.Headers
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.plugins.BadRequestException
import io.ktor.server.plugins.ParameterConversionException
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.post
import io.ktor.util.converters.DefaultConversionService
import io.ktor.util.reflect.typeInfo
import java.io.InputStream
import kotlin.Any
import kotlin.String
import kotlin.Suppress

public interface BinaryDataController {
/**
* Route is expected to respond with [java.io.InputStream].
* Use [ie.zalando.controllers.TypedApplicationCall.respondTyped] to send the response.
*
* @param applicationOctetStream
* @param call Decorated ApplicationCall with additional typed respond methods
*/
public suspend fun postBinaryData(
applicationOctetStream: InputStream,
call: TypedApplicationCall<InputStream>,
)

public companion object {
/**
* Mounts all routes for the BinaryData resource
*
* - POST /binary-data
*/
public fun Route.binaryDataRoutes(controller: BinaryDataController) {
post("/binary-data") {
val applicationOctetStream = call.receive<InputStream>()
controller.postBinaryData(applicationOctetStream, TypedApplicationCall(call))
}
}

/**
* Gets parameter value associated with this name or null if the name is not present.
* Converting to type R using DefaultConversionService.
*
* Throws:
* ParameterConversionException - when conversion from String to R fails
*/
private inline fun <reified R : Any> Parameters.getTyped(name: String): R? {
val values = getAll(name) ?: return null
val typeInfo = typeInfo<R>()
return try {
@Suppress("UNCHECKED_CAST")
DefaultConversionService.fromValues(values, typeInfo) as R
} catch (cause: Exception) {
throw ParameterConversionException(
name,
typeInfo.type.simpleName
?: typeInfo.type.toString(),
cause,
)
}
}

/**
* Gets first value from the list of values associated with a name.
*
* Throws:
* BadRequestException - when the name is not present
*/
private fun Headers.getOrFail(name: String): String = this[name] ?: throw
BadRequestException("Header " + name + " is required")
}
}

/**
* Decorator for Ktor's ApplicationCall that provides type safe variants of the [respond] functions.
*
* It can be used as a drop-in replacement for [io.ktor.server.application.ApplicationCall].
*
* @param R The type of the response body
*/
public class TypedApplicationCall<R : Any>(
private val applicationCall: ApplicationCall,
) : ApplicationCall by applicationCall {
@Suppress("unused")
public suspend inline fun <reified T : R> respondTyped(message: T) {
respond(message)
}

@Suppress("unused")
public suspend inline fun <reified T : R> respondTyped(status: HttpStatusCode, message: T) {
respond(status, message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ie.zalando.controllers

import io.micronaut.http.HttpResponse
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`.Produces
import java.io.InputStream
import javax.validation.Valid

@Controller
public interface BinaryDataController {
/**
*
*
* @param applicationOctetStream
*/
@Post(uri = "/binary-data")
@Consumes(value = ["application/octet-stream"])
@Produces(value = ["application/octet-stream"])
public fun postBinaryData(@Body @Valid applicationOctetStream: InputStream): HttpResponse<InputStream>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ie.zalando.controllers

import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.validation.`annotation`.Validated
import org.springframework.web.bind.`annotation`.RequestBody
import org.springframework.web.bind.`annotation`.RequestMapping
import org.springframework.web.bind.`annotation`.RequestMethod
import java.io.InputStream
import javax.validation.Valid

@Controller
@Validated
@RequestMapping("")
public interface BinaryDataController {
/**
*
*
* @param applicationOctetStream
*/
@RequestMapping(
value = ["/binary-data"],
produces = ["application/octet-stream"],
method = [RequestMethod.POST],
consumes = ["application/octet-stream"],
)
public fun postBinaryData(@RequestBody @Valid applicationOctetStream: InputStream): ResponseEntity<InputStream>
}
10 changes: 10 additions & 0 deletions src/test/resources/examples/byteArrayStream/models/BinaryData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package examples.byteArrayStream.models

import com.fasterxml.jackson.`annotation`.JsonProperty
import java.io.InputStream

public data class BinaryData(
@param:JsonProperty("binaryValue")
@get:JsonProperty("binaryValue")
public val binaryValue: InputStream? = null,
)