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 client streaming flag protection #314

Merged
merged 1 commit into from
Sep 16, 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
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,
)