
kotlin-client.libraries.jvm-okhttp.infrastructure.ApiClient.kt.mustache Maven / Gradle / Ivy
package {{packageName}}.infrastructure
{{#supportAndroidApiLevel25AndBelow}}
import android.os.Build
{{/supportAndroidApiLevel25AndBelow}}
import okhttp3.OkHttpClient
import okhttp3.RequestBody
{{#jvm-okhttp3}}
import okhttp3.MediaType
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
{{/jvm-okhttp4}}
import okhttp3.FormBody
{{#jvm-okhttp3}}
import okhttp3.HttpUrl
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
{{/jvm-okhttp4}}
import okhttp3.ResponseBody
{{#jvm-okhttp4}}
import okhttp3.MediaType.Companion.toMediaTypeOrNull
{{/jvm-okhttp4}}
import okhttp3.Request
import okhttp3.Headers
import okhttp3.MultipartBody
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
{{#jvm-okhttp3}}
import okhttp3.internal.Util.EMPTY_REQUEST
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
import okhttp3.internal.EMPTY_REQUEST
{{/jvm-okhttp4}}
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.net.URLConnection
{{^threetenbp}}
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.OffsetTime
{{/threetenbp}}
import java.util.Locale
{{#useCoroutines}}
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine
{{/useCoroutines}}
{{#kotlinx_serialization}}
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
{{/kotlinx_serialization}}
{{#threetenbp}}
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalTime
import org.threeten.bp.OffsetDateTime
import org.threeten.bp.OffsetTime
{{/threetenbp}}
{{#gson}}
import com.google.gson.reflect.TypeToken
{{/gson}}
{{#jackson}}
import com.fasterxml.jackson.core.type.TypeReference
{{/jackson}}
{{#moshi}}
import com.squareup.moshi.adapter
{{/moshi}}
{{#nonPublicApi}}internal {{/nonPublicApi}}open class ApiClient(val baseUrl: String) {
{{#nonPublicApi}}internal {{/nonPublicApi}}companion object {
protected const val ContentType = "Content-Type"
protected const val Accept = "Accept"
protected const val Authorization = "Authorization"
protected const val JsonMediaType = "application/json"
protected const val FormDataMediaType = "multipart/form-data"
protected const val FormUrlEncMediaType = "application/x-www-form-urlencoded"
protected const val XmlMediaType = "application/xml"
val apiKey: MutableMap = mutableMapOf()
val apiKeyPrefix: MutableMap = mutableMapOf()
var username: String? = null
var password: String? = null
var accessToken: String? = null
const val baseUrlKey = "{{packageName}}.baseUrl"
@JvmStatic
val client: OkHttpClient by lazy {
builder.build()
}
@JvmStatic
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
}
/**
* Guess Content-Type header from the given file (defaults to "application/octet-stream").
*
* @param file The given file
* @return The guessed Content-Type
*/
protected fun guessContentTypeFromFile(file: File): String {
val contentType = URLConnection.guessContentTypeFromName(file.name)
return contentType ?: "application/octet-stream"
}
protected inline fun requestBody(content: T, mediaType: String = JsonMediaType): RequestBody =
when {
{{#jvm-okhttp3}}
content is File -> RequestBody.create(MediaType.parse(mediaType), content)
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
content is File -> content.asRequestBody(mediaType.toMediaTypeOrNull())
{{/jvm-okhttp4}}
mediaType == FormDataMediaType -> {
MultipartBody.Builder()
.setType(MultipartBody.FORM)
.apply {
// content's type *must* be Map
@Suppress("UNCHECKED_CAST")
(content as Map).forEach { (key, value) ->
if (value is File) {
val partHeaders = Headers.{{#jvm-okhttp3}}of{{/jvm-okhttp3}}{{#jvm-okhttp4}}headersOf{{/jvm-okhttp4}}(
"Content-Disposition",
"form-data; name=\"$key\"; filename=\"${value.name}\""
)
{{#jvm-okhttp3}}
val fileMediaType = MediaType.parse(guessContentTypeFromFile(value))
addPart(partHeaders, RequestBody.create(fileMediaType, value))
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
val fileMediaType = guessContentTypeFromFile(value).toMediaTypeOrNull()
addPart(partHeaders, value.asRequestBody(fileMediaType))
{{/jvm-okhttp4}}
} else {
val partHeaders = Headers.{{#jvm-okhttp3}}of{{/jvm-okhttp3}}{{#jvm-okhttp4}}headersOf{{/jvm-okhttp4}}(
"Content-Disposition",
"form-data; name=\"$key\""
)
addPart(
partHeaders,
{{#jvm-okhttp3}}
RequestBody.create(null, parameterToString(value))
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
parameterToString(value).toRequestBody(null)
{{/jvm-okhttp4}}
)
}
}
}.build()
}
mediaType == FormUrlEncMediaType -> {
FormBody.Builder().apply {
// content's type *must* be Map
@Suppress("UNCHECKED_CAST")
(content as Map).forEach { (key, value) ->
add(key, parameterToString(value))
}
}.build()
}
mediaType.startsWith("application/") && mediaType.endsWith("json") ->
{{#jvm-okhttp3}}
if (content == null) {
EMPTY_REQUEST
} else {
RequestBody.create(
{{#moshi}}
MediaType.parse(mediaType), Serializer.moshi.adapter(T::class.java).toJson(content)
{{/moshi}}
{{#gson}}
MediaType.parse(mediaType), Serializer.gson.toJson(content, T::class.java)
{{/gson}}
{{#jackson}}
MediaType.parse(mediaType), Serializer.jacksonObjectMapper.writeValueAsString(content)
{{/jackson}}
{{#kotlinx_serialization}}
MediaType.parse(mediaType), Serializer.jvmJson.encodeToString(content)
{{/kotlinx_serialization}}
)
}
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
if (content == null) {
EMPTY_REQUEST
} else {
{{#moshi}}
Serializer.moshi.adapter(T::class.java).toJson(content)
{{/moshi}}
{{#gson}}
Serializer.gson.toJson(content, T::class.java)
{{/gson}}
{{#jackson}}
Serializer.jacksonObjectMapper.writeValueAsString(content)
{{/jackson}}
{{#kotlinx_serialization}}
Serializer.jvmJson.encodeToString(content)
{{/kotlinx_serialization}}
.toRequestBody(
mediaType.toMediaTypeOrNull()
)
}
{{/jvm-okhttp4}}
mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.")
// TODO: this should be extended with other serializers
else -> throw UnsupportedOperationException("requestBody currently only supports JSON body and File body.")
}
{{#moshi}}
@OptIn(ExperimentalStdlibApi::class)
{{/moshi}}
protected inline fun responseBody(body: ResponseBody?, mediaType: String? = JsonMediaType): T? {
if(body == null) {
return null
}
val bodyContent = body.string()
if (bodyContent.isEmpty()) {
return null
}
if (T::class.java == File::class.java) {
// return tempfile
{{^supportAndroidApiLevel25AndBelow}}
val f = java.nio.file.Files.createTempFile("tmp.{{packageName}}", null).toFile()
{{/supportAndroidApiLevel25AndBelow}}
{{#supportAndroidApiLevel25AndBelow}}
val f = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
java.nio.file.Files.createTempFile("tmp.net.medicineone.teleconsultationandroid.openapi.openapicommon", null).toFile()
} else {
@Suppress("DEPRECATION")
createTempFile("tmp.net.medicineone.teleconsultationandroid.openapi.openapicommon", null)
}
{{/supportAndroidApiLevel25AndBelow}}
f.deleteOnExit()
val out = BufferedWriter(FileWriter(f))
out.write(bodyContent)
out.close()
return f as T
}
return when {
mediaType==null || (mediaType.startsWith("application/") && mediaType.endsWith("json")) ->
{{#moshi}}Serializer.moshi.adapter().fromJson(bodyContent){{/moshi}}{{#gson}}Serializer.gson.fromJson(bodyContent, (object: TypeToken(){}).getType()){{/gson}}{{#jackson}}Serializer.jacksonObjectMapper.readValue(bodyContent, object: TypeReference() {}){{/jackson}}{{#kotlinx_serialization}}Serializer.jvmJson.decodeFromString(bodyContent){{/kotlinx_serialization}}
else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.")
}
}
{{#hasAuthMethods}}
protected fun updateAuthParams(requestConfig: RequestConfig) {
{{#authMethods}}
{{#isApiKey}}
{{#isKeyInHeader}}
if (requestConfig.headers["{{keyParamName}}"].isNullOrEmpty()) {
{{/isKeyInHeader}}
{{#isKeyInQuery}}
if (requestConfig.query["{{keyParamName}}"].isNullOrEmpty()) {
{{/isKeyInQuery}}
if (apiKey["{{keyParamName}}"] != null) {
if (apiKeyPrefix["{{keyParamName}}"] != null) {
{{#isKeyInHeader}}
requestConfig.headers["{{keyParamName}}"] = apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!
{{/isKeyInHeader}}
{{#isKeyInQuery}}
requestConfig.query["{{keyParamName}}"] = apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!
{{/isKeyInQuery}}
} else {
{{#isKeyInHeader}}
requestConfig.headers["{{keyParamName}}"] = apiKey["{{keyParamName}}"]!!
{{/isKeyInHeader}}
{{#isKeyInQuery}}
requestConfig.query["{{keyParamName}}"] = apiKey["{{keyParamName}}"]!!
{{/isKeyInQuery}}
}
}
}
{{/isApiKey}}
{{#isBasic}}
{{#isBasicBasic}}
if (requestConfig.headers[Authorization].isNullOrEmpty()) {
username?.let { username ->
password?.let { password ->
requestConfig.headers[Authorization] = okhttp3.Credentials.basic(username, password)
}
}
}
{{/isBasicBasic}}
{{#isBasicBearer}}
if (requestConfig.headers[Authorization].isNullOrEmpty()) {
accessToken?.let { accessToken ->
requestConfig.headers[Authorization] = "Bearer $accessToken"
}
}
{{/isBasicBearer}}
{{/isBasic}}
{{#isOAuth}}
if (requestConfig.headers[Authorization].isNullOrEmpty()) {
accessToken?.let { accessToken ->
requestConfig.headers[Authorization] = "Bearer $accessToken "
}
}
{{/isOAuth}}
{{/authMethods}}
}
{{/hasAuthMethods}}
protected {{#useCoroutines}}suspend {{/useCoroutines}}inline fun request(requestConfig: RequestConfig): ApiResponse {
{{#jvm-okhttp3}}
val httpUrl = HttpUrl.parse(baseUrl) ?: throw IllegalStateException("baseUrl is invalid.")
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
val httpUrl = baseUrl.toHttpUrlOrNull() ?: throw IllegalStateException("baseUrl is invalid.")
{{/jvm-okhttp4}}
{{#hasAuthMethods}}
// take authMethod from operation
updateAuthParams(requestConfig)
{{/hasAuthMethods}}
val url = httpUrl.newBuilder()
.addPathSegments(requestConfig.path.trimStart('/'))
.apply {
requestConfig.query.forEach { query ->
query.value.forEach { queryValue ->
addQueryParameter(query.key, queryValue)
}
}
}.build()
// take content-type/accept from spec or set to default (application/json) if not defined
if (requestConfig.headers[ContentType].isNullOrEmpty()) {
requestConfig.headers[ContentType] = JsonMediaType
}
if (requestConfig.headers[Accept].isNullOrEmpty()) {
requestConfig.headers[Accept] = JsonMediaType
}
val headers = requestConfig.headers
if(headers[ContentType].isNullOrEmpty()) {
throw kotlin.IllegalStateException("Missing Content-Type header. This is required.")
}
if(headers[Accept].isNullOrEmpty()) {
throw kotlin.IllegalStateException("Missing Accept header. This is required.")
}
// TODO: support multiple contentType options here.
val contentType = (headers[ContentType] as String).substringBefore(";").lowercase(Locale.getDefault())
val request = when (requestConfig.method) {
RequestMethod.DELETE -> Request.Builder().url(url).delete(requestBody(requestConfig.body, contentType))
RequestMethod.GET -> Request.Builder().url(url)
RequestMethod.HEAD -> Request.Builder().url(url).head()
RequestMethod.PATCH -> Request.Builder().url(url).patch(requestBody(requestConfig.body, contentType))
RequestMethod.PUT -> Request.Builder().url(url).put(requestBody(requestConfig.body, contentType))
RequestMethod.POST -> Request.Builder().url(url).post(requestBody(requestConfig.body, contentType))
RequestMethod.OPTIONS -> Request.Builder().url(url).method("OPTIONS", null)
}.apply {
headers.forEach { header -> addHeader(header.key, header.value) }
}.build()
{{#useCoroutines}}
val response: Response = suspendCancellableCoroutine { continuation ->
val call = client.newCall(request)
continuation.invokeOnCancellation { call.cancel() }
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
continuation.resume(response)
}
})
}
{{/useCoroutines}}
{{^useCoroutines}}
val response = client.newCall(request).execute()
{{/useCoroutines}}
val accept = response.header(ContentType)?.substringBefore(";")?.lowercase(Locale.getDefault())
// TODO: handle specific mapping types. e.g. Map>
return when {
response.isRedirect -> Redirection(
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
)
response.isInformational -> Informational(
response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
)
response.isSuccessful -> Success(
responseBody(response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}, accept),
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
)
response.isClientError -> ClientError(
response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}?.string(),
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
)
else -> ServerError(
response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}?.string(),
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
)
}
}
protected fun parameterToString(value: Any?): String = when (value) {
null -> ""
is Array<*> -> toMultiValue(value, "csv").toString()
is Iterable<*> -> toMultiValue(value, "csv").toString()
is OffsetDateTime, is OffsetTime, is LocalDateTime, is LocalDate, is LocalTime ->
parseDateToQueryString(value)
else -> value.toString()
}
protected inline fun parseDateToQueryString(value : T): String {
{{#toJson}}
/*
.replace("\"", "") converts the json object string to an actual string for the query parameter.
The moshi or gson adapter allows a more generic solution instead of trying to use a native
formatter. It also easily allows to provide a simple way to define a custom date format pattern
inside a gson/moshi adapter.
*/
{{#moshi}}
return Serializer.moshi.adapter(T::class.java).toJson(value).replace("\"", "")
{{/moshi}}
{{#gson}}
return Serializer.gson.toJson(value, T::class.java).replace("\"", "")
{{/gson}}
{{#jackson}}
return Serializer.jacksonObjectMapper.writeValueAsString(value).replace("\"", "")
{{/jackson}}
{{#kotlinx_serialization}}
return Serializer.jvmJson.encodeToString(value).replace("\"", "")
{{/kotlinx_serialization}}
{{/toJson}}
{{^toJson}}
return value.toString()
{{/toJson}}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy