Skip to content

Commit

Permalink
Add client streaming flag protection (#314)
Browse files Browse the repository at this point in the history
Co-authored-by: Alejandro Vera-baquero <[email protected]>
  • Loading branch information
averabaq and Alejandro Vera-baquero authored Sep 16, 2024
1 parent 696c2b0 commit 6528da2
Show file tree
Hide file tree
Showing 17 changed files with 261 additions and 4 deletions.
16 changes: 16 additions & 0 deletions src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.cjbooms.fabrikt.model

import com.cjbooms.fabrikt.cli.CodeGenTypeOverride
import com.cjbooms.fabrikt.cli.CodeGenerationType
import com.cjbooms.fabrikt.generators.MutableSettings
import com.cjbooms.fabrikt.model.OasType.Companion.toOasType
import com.cjbooms.fabrikt.util.KaizenParserExtensions.getEnumValues
Expand All @@ -15,6 +16,8 @@ import java.net.URI
import java.time.LocalDate
import java.time.OffsetDateTime
import java.util.UUID
import java.util.logging.Level
import java.util.logging.Logger
import kotlin.reflect.KClass

sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassName: String? = null) {
Expand Down Expand Up @@ -61,6 +64,8 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
}

companion object {
private val logger = Logger.getGlobal()

fun from(schema: Schema, oasKey: String = "", enclosingSchema: EnclosingSchemaInfo? = null): KotlinTypeInfo =
when (schema.toOasType(oasKey)) {
OasType.Date -> Date
Expand Down Expand Up @@ -120,8 +125,19 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
}

private fun getOverridableByteArray(): KotlinTypeInfo {
val types: Set<CodeGenerationType> = MutableSettings.generationTypes()
val typeOverrides = MutableSettings.typeOverrides()
return when {
CodeGenerationType.CLIENT in types && CodeGenTypeOverride.BYTEARRAY_AS_INPUTSTREAM in typeOverrides -> {
logger.log(
Level.WARNING,
"""
Client code generation does not support streaming, yet. The override flag
'BYTEARRAY_AS_INPUTSTREAM' is ignored. If generating server side code, please consider
splitting the client & server in different fabrikt executions. Defaulting to `ByteArray`...
""".trimIndent())
ByteArray
}
CodeGenTypeOverride.BYTEARRAY_AS_INPUTSTREAM in typeOverrides -> InputStream
else -> ByteArray
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.cjbooms.fabrikt.generators

import com.cjbooms.fabrikt.cli.ClientCodeGenOptionType
import com.cjbooms.fabrikt.cli.ClientCodeGenTargetType
import com.cjbooms.fabrikt.cli.CodeGenTypeOverride
import com.cjbooms.fabrikt.cli.CodeGenerationType
import com.cjbooms.fabrikt.cli.ExternalReferencesResolutionMode
import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
Expand Down Expand Up @@ -36,7 +37,8 @@ class OkHttpClientGeneratorTest {
"multiMediaType",
"okHttpClientPostWithoutRequestBody",
"pathLevelParameters",
"parameterNameClash"
"parameterNameClash",
"byteArrayStream",
)

@BeforeEach
Expand All @@ -45,6 +47,7 @@ class OkHttpClientGeneratorTest {
genTypes = setOf(CodeGenerationType.CLIENT),
clientTarget = ClientCodeGenTargetType.OK_HTTP,
modelOptions = setOf(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS),
typeOverrides = setOf(CodeGenTypeOverride.BYTEARRAY_AS_INPUTSTREAM)
)
ModelNameRegistry.clear()
}
Expand All @@ -56,7 +59,7 @@ class OkHttpClientGeneratorTest {
val apiLocation = javaClass.getResource("/examples/$testCaseName/api.yaml")!!
val sourceApi = SourceApi(apiLocation.readText(), baseDir = Paths.get(apiLocation.toURI()))

val expectedModel = readTextResource("/examples/$testCaseName/models/Models.kt")
val expectedModel = readTextResource("/examples/$testCaseName/models/ClientModels.kt")
val expectedClient = readTextResource("/examples/$testCaseName/client/ApiClient.kt")

val models = JacksonModelGenerator(
Expand Down Expand Up @@ -135,7 +138,7 @@ class OkHttpClientGeneratorTest {
val apiLocation = javaClass.getResource("/examples/externalReferences/aggressive/api.yaml")!!
val sourceApi = SourceApi(apiLocation.readText(), baseDir = Paths.get(apiLocation.toURI()))

val expectedModel = readTextResource("/examples/externalReferences/aggressive/models/Models.kt")
val expectedModel = readTextResource("/examples/externalReferences/aggressive/models/ClientModels.kt")
val expectedClient = readTextResource("/examples/externalReferences/aggressive/client/ApiClient.kt")
val expectedClientCode = readTextResource("/examples/externalReferences/aggressive/client/ApiService.kt")
MutableSettings.updateSettings(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class OpenFeignClientGeneratorTest {
val packages = Packages("examples.$testCaseName")
val sourceApi = SourceApi(readTextResource("/examples/$testCaseName/api.yaml"))

val expectedModel = readTextResource("/examples/$testCaseName/models/Models.kt")
val expectedModel = readTextResource("/examples/$testCaseName/models/ClientModels.kt")
val expectedClient = readTextResource("/examples/$testCaseName/client/$clientFileName")

val models = JacksonModelGenerator(
Expand Down
53 changes: 53 additions & 0 deletions src/test/resources/examples/byteArrayStream/client/ApiClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package examples.byteArrayStream.client

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import kotlin.ByteArray
import kotlin.String
import kotlin.Suppress
import kotlin.collections.Map
import kotlin.jvm.Throws

@Suppress("unused")
public class BinaryDataClient(
private val objectMapper: ObjectMapper,
private val baseUrl: String,
private val client: OkHttpClient,
) {
/**
*
*
* @param applicationOctetStream
*/
@Throws(ApiException::class)
public fun postBinaryData(
applicationOctetStream: ByteArray,
additionalHeaders: Map<String, String> = emptyMap(),
additionalQueryParameters: Map<String, String> = emptyMap(),
): ApiResponse<ByteArray> {
val httpUrl: HttpUrl = "$baseUrl/binary-data"
.toHttpUrl()
.newBuilder()
.also { builder -> additionalQueryParameters.forEach { builder.queryParam(it.key, it.value) } }
.build()

val headerBuilder = Headers.Builder()
additionalHeaders.forEach { headerBuilder.header(it.key, it.value) }
val httpHeaders: Headers = headerBuilder.build()

val request: Request = Request.Builder()
.url(httpUrl)
.headers(httpHeaders)
.post(objectMapper.writeValueAsString(applicationOctetStream).toRequestBody("application/octet-stream".toMediaType()))
.build()

return request.execute(client, objectMapper, jacksonTypeRef())
}
}
32 changes: 32 additions & 0 deletions src/test/resources/examples/byteArrayStream/client/ApiModels.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package examples.byteArrayStream.client

import okhttp3.Headers

/**
* API 2xx success response returned by API call.
*
* @param <T> The type of data that is deserialized from response body
*/
data class ApiResponse<T>(val statusCode: Int, val headers: Headers, val data: T? = null)

/**
* API non-2xx failure responses returned by API call.
*/
open class ApiException(override val message: String) : RuntimeException(message)

/**
* API 3xx redirect response returned by API call.
*/
open class ApiRedirectException(val statusCode: Int, val headers: Headers, override val message: String) : ApiException(message)

/**
* API 4xx failure responses returned by API call.
*/
data class ApiClientException(val statusCode: Int, val headers: Headers, override val message: String) :
ApiException(message)

/**
* API 5xx failure responses returned by API call.
*/
data class ApiServerException(val statusCode: Int, val headers: Headers, override val message: String) :
ApiException(message)
39 changes: 39 additions & 0 deletions src/test/resources/examples/byteArrayStream/client/ApiService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package examples.byteArrayStream.client

import com.fasterxml.jackson.databind.ObjectMapper
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
import okhttp3.OkHttpClient
import kotlin.ByteArray
import kotlin.String
import kotlin.Suppress
import kotlin.collections.Map
import kotlin.jvm.Throws

/**
* The circuit breaker registry should have the proper configuration to correctly action on circuit
* breaker transitions based on the client exceptions [ApiClientException], [ApiServerException] and
* [IOException].
*
* @see ApiClientException
* @see ApiServerException
*/
@Suppress("unused")
public class BinaryDataService(
private val circuitBreakerRegistry: CircuitBreakerRegistry,
objectMapper: ObjectMapper,
baseUrl: String,
client: OkHttpClient,
) {
public var circuitBreakerName: String = "binaryDataClient"

private val apiClient: BinaryDataClient = BinaryDataClient(objectMapper, baseUrl, client)

@Throws(ApiException::class)
public fun postBinaryData(
applicationOctetStream: ByteArray,
additionalHeaders: Map<String, String> = emptyMap(),
): ApiResponse<ByteArray> =
withCircuitBreaker(circuitBreakerRegistry, circuitBreakerName) {
apiClient.postBinaryData(applicationOctetStream, additionalHeaders)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package examples.byteArrayStream.client

import io.github.resilience4j.circuitbreaker.CircuitBreaker
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry

fun <T> withCircuitBreaker(
circuitBreakerRegistry: CircuitBreakerRegistry,
apiClientName: String,
apiCall: () -> ApiResponse<T>
): ApiResponse<T> {
val circuitBreaker = circuitBreakerRegistry.circuitBreaker(apiClientName)
return CircuitBreaker.decorateSupplier(circuitBreaker, apiCall).get()
}
69 changes: 69 additions & 0 deletions src/test/resources/examples/byteArrayStream/client/HttpUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package examples.byteArrayStream.client

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody

@Suppress("unused")
fun <T : Any> HttpUrl.Builder.queryParam(key: String, value: T?): HttpUrl.Builder = this.apply {
if (value != null) this.addQueryParameter(key, value.toString())
}

@Suppress("unused")
fun <T : Any> FormBody.Builder.formParam(key: String, value: T?): FormBody.Builder = this.apply {
if (value != null) this.add(key, value.toString())
}

@Suppress("unused")
fun HttpUrl.Builder.queryParam(key: String, values: List<String>?, explode: Boolean = true) = this.apply {
if (values != null) {
if (explode) values.forEach { addQueryParameter(key, it) }
else addQueryParameter(key, values.joinToString(","))
}
}

@Suppress("unused")
fun Headers.Builder.header(key: String, value: String?): Headers.Builder = this.apply {
if (value != null) this.add(key, value)
}

@Throws(ApiException::class)
fun <T> Request.execute(client: OkHttpClient, objectMapper: ObjectMapper, typeRef: TypeReference<T>): ApiResponse<T> =
client.newCall(this).execute().use { response ->
when {
response.isSuccessful ->
ApiResponse(response.code, response.headers, response.body?.deserialize(objectMapper, typeRef))
response.isRedirection() ->
throw ApiRedirectException(response.code, response.headers, response.errorMessage())
response.isBadRequest() ->
throw ApiClientException(response.code, response.headers, response.errorMessage())
response.isServerError() ->
throw ApiServerException(response.code, response.headers, response.errorMessage())
else -> throw ApiException("[${response.code}]: ${response.errorMessage()}")
}
}

@Suppress("unused")
fun String.pathParam(vararg params: Pair<String, Any>): String = params.asSequence()
.joinToString { param ->
this.replace(param.first, param.second.toString())
}

fun <T> ResponseBody.deserialize(objectMapper: ObjectMapper, typeRef: TypeReference<T>): T? =
this.string().isNotBlankOrNull()?.let { objectMapper.readValue(it, typeRef) }

fun String?.isNotBlankOrNull() = if (this.isNullOrBlank()) null else this

private fun Response.errorMessage(): String = this.body?.string() ?: this.message

private fun Response.isBadRequest(): Boolean = this.code in 400..499

private fun Response.isServerError(): Boolean = this.code in 500..599

private fun Response.isRedirection(): Boolean = this.code in 300..399
22 changes: 22 additions & 0 deletions src/test/resources/examples/byteArrayStream/client/OAuth.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package examples.byteArrayStream.client

import okhttp3.Authenticator
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route

class OAuth2(val accessToken: () -> String) : Authenticator, Interceptor {

override fun authenticate(route: Route?, response: Response): Request =
response.request.newBuilder()
.header("Authorization", "Bearer ${accessToken().trim()}")
.build()

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.header("Authorization", "Bearer ${accessToken().trim()}")
.build()
return chain.proceed(request)
}
}
10 changes: 10 additions & 0 deletions src/test/resources/examples/byteArrayStream/models/ClientModels.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 kotlin.ByteArray

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

0 comments on commit 6528da2

Please sign in to comment.