Skip to content

Commit

Permalink
Merge pull request #292 from ctasada/ctasada/bytearray_as_inputstream
Browse files Browse the repository at this point in the history
Add support for InputStream
  • Loading branch information
averabaq authored Sep 13, 2024
2 parents 10c5a67 + 387c8e7 commit 696c2b0
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 2 deletions.
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,
)

0 comments on commit 696c2b0

Please sign in to comment.