diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt index e24a0061..47f62552 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt @@ -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 @@ -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) { @@ -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 @@ -120,8 +125,19 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN } private fun getOverridableByteArray(): KotlinTypeInfo { + val types: Set = 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 } diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt index b0ce7bfe..56a5ccd5 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt @@ -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 @@ -36,7 +37,8 @@ class OkHttpClientGeneratorTest { "multiMediaType", "okHttpClientPostWithoutRequestBody", "pathLevelParameters", - "parameterNameClash" + "parameterNameClash", + "byteArrayStream", ) @BeforeEach @@ -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() } @@ -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( @@ -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( diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/OpenFeignClientGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/OpenFeignClientGeneratorTest.kt index 3b084a07..9fc63021 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/OpenFeignClientGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/OpenFeignClientGeneratorTest.kt @@ -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( diff --git a/src/test/resources/examples/byteArrayStream/client/ApiClient.kt b/src/test/resources/examples/byteArrayStream/client/ApiClient.kt new file mode 100644 index 00000000..4c94b5c6 --- /dev/null +++ b/src/test/resources/examples/byteArrayStream/client/ApiClient.kt @@ -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 = emptyMap(), + additionalQueryParameters: Map = emptyMap(), + ): ApiResponse { + 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()) + } +} diff --git a/src/test/resources/examples/byteArrayStream/client/ApiModels.kt b/src/test/resources/examples/byteArrayStream/client/ApiModels.kt new file mode 100644 index 00000000..d13a9c23 --- /dev/null +++ b/src/test/resources/examples/byteArrayStream/client/ApiModels.kt @@ -0,0 +1,32 @@ +package examples.byteArrayStream.client + +import okhttp3.Headers + +/** + * API 2xx success response returned by API call. + * + * @param The type of data that is deserialized from response body + */ +data class ApiResponse(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) diff --git a/src/test/resources/examples/byteArrayStream/client/ApiService.kt b/src/test/resources/examples/byteArrayStream/client/ApiService.kt new file mode 100644 index 00000000..cd8f01a6 --- /dev/null +++ b/src/test/resources/examples/byteArrayStream/client/ApiService.kt @@ -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 = emptyMap(), + ): ApiResponse = + withCircuitBreaker(circuitBreakerRegistry, circuitBreakerName) { + apiClient.postBinaryData(applicationOctetStream, additionalHeaders) + } +} diff --git a/src/test/resources/examples/byteArrayStream/client/HttpResilience4jUtil.kt b/src/test/resources/examples/byteArrayStream/client/HttpResilience4jUtil.kt new file mode 100644 index 00000000..74134649 --- /dev/null +++ b/src/test/resources/examples/byteArrayStream/client/HttpResilience4jUtil.kt @@ -0,0 +1,13 @@ +package examples.byteArrayStream.client + +import io.github.resilience4j.circuitbreaker.CircuitBreaker +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry + +fun withCircuitBreaker( + circuitBreakerRegistry: CircuitBreakerRegistry, + apiClientName: String, + apiCall: () -> ApiResponse +): ApiResponse { + val circuitBreaker = circuitBreakerRegistry.circuitBreaker(apiClientName) + return CircuitBreaker.decorateSupplier(circuitBreaker, apiCall).get() +} diff --git a/src/test/resources/examples/byteArrayStream/client/HttpUtil.kt b/src/test/resources/examples/byteArrayStream/client/HttpUtil.kt new file mode 100644 index 00000000..c5620608 --- /dev/null +++ b/src/test/resources/examples/byteArrayStream/client/HttpUtil.kt @@ -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 HttpUrl.Builder.queryParam(key: String, value: T?): HttpUrl.Builder = this.apply { + if (value != null) this.addQueryParameter(key, value.toString()) +} + +@Suppress("unused") +fun 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?, 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 Request.execute(client: OkHttpClient, objectMapper: ObjectMapper, typeRef: TypeReference): ApiResponse = + 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 = params.asSequence() + .joinToString { param -> + this.replace(param.first, param.second.toString()) + } + +fun ResponseBody.deserialize(objectMapper: ObjectMapper, typeRef: TypeReference): 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 diff --git a/src/test/resources/examples/byteArrayStream/client/OAuth.kt b/src/test/resources/examples/byteArrayStream/client/OAuth.kt new file mode 100644 index 00000000..0678474c --- /dev/null +++ b/src/test/resources/examples/byteArrayStream/client/OAuth.kt @@ -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) + } +} diff --git a/src/test/resources/examples/byteArrayStream/models/ClientModels.kt b/src/test/resources/examples/byteArrayStream/models/ClientModels.kt new file mode 100644 index 00000000..2384aa04 --- /dev/null +++ b/src/test/resources/examples/byteArrayStream/models/ClientModels.kt @@ -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, +) diff --git a/src/test/resources/examples/externalReferences/aggressive/models/Models.kt b/src/test/resources/examples/externalReferences/aggressive/models/ClientModels.kt similarity index 100% rename from src/test/resources/examples/externalReferences/aggressive/models/Models.kt rename to src/test/resources/examples/externalReferences/aggressive/models/ClientModels.kt diff --git a/src/test/resources/examples/multiMediaType/models/Models.kt b/src/test/resources/examples/multiMediaType/models/ClientModels.kt similarity index 100% rename from src/test/resources/examples/multiMediaType/models/Models.kt rename to src/test/resources/examples/multiMediaType/models/ClientModels.kt diff --git a/src/test/resources/examples/okHttpClient/models/Models.kt b/src/test/resources/examples/okHttpClient/models/ClientModels.kt similarity index 100% rename from src/test/resources/examples/okHttpClient/models/Models.kt rename to src/test/resources/examples/okHttpClient/models/ClientModels.kt diff --git a/src/test/resources/examples/okHttpClientPostWithoutRequestBody/models/Models.kt b/src/test/resources/examples/okHttpClientPostWithoutRequestBody/models/ClientModels.kt similarity index 100% rename from src/test/resources/examples/okHttpClientPostWithoutRequestBody/models/Models.kt rename to src/test/resources/examples/okHttpClientPostWithoutRequestBody/models/ClientModels.kt diff --git a/src/test/resources/examples/openFeignClient/models/Models.kt b/src/test/resources/examples/openFeignClient/models/ClientModels.kt similarity index 100% rename from src/test/resources/examples/openFeignClient/models/Models.kt rename to src/test/resources/examples/openFeignClient/models/ClientModels.kt diff --git a/src/test/resources/examples/parameterNameClash/models/Models.kt b/src/test/resources/examples/parameterNameClash/models/ClientModels.kt similarity index 100% rename from src/test/resources/examples/parameterNameClash/models/Models.kt rename to src/test/resources/examples/parameterNameClash/models/ClientModels.kt diff --git a/src/test/resources/examples/pathLevelParameters/models/Models.kt b/src/test/resources/examples/pathLevelParameters/models/ClientModels.kt similarity index 100% rename from src/test/resources/examples/pathLevelParameters/models/Models.kt rename to src/test/resources/examples/pathLevelParameters/models/ClientModels.kt