aboutsummaryrefslogtreecommitdiff
path: root/shared/src/commonMain
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2021-12-04 00:10:11 -0600
committerIván Ávalos <avalos@disroot.org>2021-12-04 00:10:11 -0600
commit2333e4a95427cf26df831bc95065646faa014927 (patch)
tree6df24f6503545477554989b08ca8654df64168ae /shared/src/commonMain
parent33bab0553bceaa174b11b3fb7a9ba9d4de63526a (diff)
downloadetbsa-trackermap-mobile-2333e4a95427cf26df831bc95065646faa014927.tar.gz
etbsa-trackermap-mobile-2333e4a95427cf26df831bc95065646faa014927.tar.bz2
etbsa-trackermap-mobile-2333e4a95427cf26df831bc95065646faa014927.zip
Rewrote HTTP client code using Ktor
Diffstat (limited to 'shared/src/commonMain')
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt202
1 files changed, 114 insertions, 88 deletions
diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt
index aa4e45f..0f1d7da 100644
--- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt
+++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt
@@ -1,132 +1,158 @@
package mx.trackermap.TrackerMap.client.infrastructure
-import io.swagger.client.infrastructure.*
-import okhttp3.*
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import io.ktor.client.*
+import io.ktor.client.call.*
+import io.ktor.client.engine.cio.*
+import io.ktor.client.features.json.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.util.*
import java.io.File
open class ApiClient(val baseUrl: String) {
companion object {
- protected const val ContentType = "Content-Type"
- protected const val Accept = "Accept"
- protected const val JsonMediaType = "application/json"
- protected const val FormDataMediaType = "multipart/form-data"
- protected const val XmlMediaType = "application/xml"
-
- @JvmStatic
- val client: OkHttpClient = OkHttpClient()
+ protected const val ApiContentType = "Content-Type"
+ protected const val ApiAccept = "Accept"
+ protected const val ApiJsonMediaType = "application/json"
+ protected const val ApiFormDataMediaType = "multipart/form-data"
+ protected const val ApiXmlMediaType = "application/xml"
+
+ val client: HttpClient = HttpClient(CIO) {
+ install(JsonFeature)
+ }
- @JvmStatic
- var defaultHeaders: Map<String, String> by ApplicationDelegates.setOnce(
+ val defaultHeaders: Map<String, String> =
mapOf(
- ContentType to JsonMediaType,
- Accept to JsonMediaType
- )
- )
+ ApiContentType to ApiJsonMediaType,
+ ApiAccept to ApiJsonMediaType)
- @JvmStatic
- val jsonHeaders: Map<String, String> = mapOf(ContentType to JsonMediaType, Accept to JsonMediaType)
+ val jsonHeaders: Map<String, String> =
+ mapOf(
+ ApiContentType to ApiJsonMediaType,
+ ApiAccept to ApiJsonMediaType)
}
- protected inline fun <reified T> requestBody(content: T, mediaType: String = JsonMediaType): RequestBody =
- when {
- content is File -> RequestBody.create(mediaType.toMediaTypeOrNull(), content)
-
- mediaType == FormDataMediaType -> {
- var builder = FormBody.Builder()
- // content's type *must* be Map<String, Any>
- @Suppress("UNCHECKED_CAST")
- (content as Map<String, String>).forEach { key, value ->
- builder = builder.add(key, value)
+ protected inline fun <reified T> fillRequest(requestBuilder: HttpRequestBuilder, content: T, mediaType: String = ApiJsonMediaType) {
+ when {
+ content is File -> TODO("i don't know what to do here.")
+ mediaType == ApiFormDataMediaType && content is Map<*, *> -> {
+ val parametersBuilder = ParametersBuilder()
+ content.forEach { map ->
+ if (map.key is String && map.value is String) {
+ parametersBuilder[map.key as String] = map.value as String
}
- builder.build()
}
- mediaType == JsonMediaType -> RequestBody.create(
- mediaType.toMediaTypeOrNull(), Serializer.moshi.adapter(T::class.java).toJson(content)
- )
- mediaType == XmlMediaType -> TODO("xml not currently supported.")
-
- // TODO: this should be extended with other serializers
- else -> TODO("requestBody currently only supports JSON body and File body.")
+ parametersBuilder.build()
+ requestBuilder.contentType(ContentType.MultiPart.FormData)
+ requestBuilder.body = parametersBuilder
+ }
+ mediaType == ApiJsonMediaType -> {
+ requestBuilder.contentType(ContentType.Application.Json)
+ if (content != null) {
+ requestBuilder.body = content
+ }
}
+ mediaType == ApiXmlMediaType -> TODO("xml not currently supported.")
- protected inline fun <reified T : Any?> responseBody(body: ResponseBody?, mediaType: String = JsonMediaType): T? {
- if (body == null) return null
- return when (mediaType) {
- JsonMediaType -> Serializer.moshi.adapter(T::class.java).fromJson(body.source())
- else -> TODO()
+ // TODO: this should be extended with other serializers
+ else -> TODO("requestBody currently only supports JSON body and File body.")
}
}
- protected inline fun <reified T : Any?> request(requestConfig: RequestConfig, body: Any? = null): ApiInfrastructureResponse<T?> {
- val httpUrl = baseUrl.toHttpUrlOrNull() ?: throw IllegalStateException("baseUrl is invalid.")
+ protected suspend inline fun <reified T : Any?> request(requestConfig: RequestConfig, body: Any? = null): ApiInfrastructureResponse<T?> {
+ val httpUrl: Url
+ try {
+ httpUrl = Url(baseUrl)
+ } catch (e: URLDecodeException) {
+ throw IllegalStateException("baseUrl is invalid.")
+ }
- var urlBuilder = httpUrl.newBuilder()
- .addPathSegments(requestConfig.path.trimStart('/'))
+ val urlBuilder = URLBuilder(httpUrl)
+ .path(requestConfig.path.trimStart('/'))
requestConfig.query.forEach { query ->
query.value.forEach { queryValue ->
- urlBuilder = urlBuilder.addQueryParameter(query.key, queryValue)
+ urlBuilder.parameters.append(query.key, queryValue)
}
}
val url = urlBuilder.build()
val headers = requestConfig.headers + defaultHeaders
- if (headers[ContentType] ?: "" == "") {
- throw kotlin.IllegalStateException("Missing Content-Type header. This is required.")
+ if (headers[ApiContentType] ?: "" == "") {
+ throw IllegalStateException("Missing Content-Type header. This is required.")
}
- if (headers[Accept] ?: "" == "") {
- throw kotlin.IllegalStateException("Missing Accept header. This is required.")
+ if (headers[ApiAccept] ?: "" == "") {
+ throw IllegalStateException("Missing Accept header. This is required.")
}
// TODO: support multiple contentType,accept options here.
- val contentType = (headers[ContentType] as String).substringBefore(";").toLowerCase()
- val accept = (headers[Accept] as String).substringBefore(";").toLowerCase()
-
- var request: Request.Builder = when (requestConfig.method) {
- RequestMethod.DELETE -> Request.Builder().url(url).delete()
- RequestMethod.GET -> Request.Builder().url(url)
- RequestMethod.HEAD -> Request.Builder().url(url).head()
- RequestMethod.PATCH -> Request.Builder().url(url).patch(requestBody(body, contentType))
- RequestMethod.PUT -> Request.Builder().url(url).put(requestBody(body, contentType))
- RequestMethod.POST -> Request.Builder().url(url).post(requestBody(body, contentType))
- RequestMethod.OPTIONS -> Request.Builder().url(url).method("OPTIONS", null)
+ val contentType = (headers[ApiContentType] as String).substringBefore(";").lowercase()
+ val accept = (headers[ApiAccept] as String).substringBefore(";").lowercase()
+
+ val request = HttpRequestBuilder()
+ request.url(url)
+ request.accept(ContentType.parse(accept))
+
+ when (requestConfig.method) {
+ RequestMethod.DELETE -> {
+ request.method = HttpMethod.Delete
+ }
+ RequestMethod.GET -> {
+ request.method = HttpMethod.Get
+ }
+ RequestMethod.HEAD -> {
+ request.method = HttpMethod.Head
+ }
+ RequestMethod.PATCH -> {
+ request.method = HttpMethod.Patch
+ fillRequest(request, body, contentType)
+ }
+ RequestMethod.PUT -> {
+ request.method = HttpMethod.Put
+ fillRequest(request, body, contentType)
+ }
+ RequestMethod.POST -> {
+ request.method = HttpMethod.Post
+ fillRequest(request, body, contentType)
+ }
+ RequestMethod.OPTIONS -> {
+ request.method = HttpMethod.Options
+ }
}
- headers.forEach { header -> request = request.addHeader(header.key, header.value.toString()) }
+ headers.forEach { header ->
+ request.headers[header.key] = header.value
+ }
- val realRequest = request.build()
- val response = client.newCall(realRequest).execute()
+ val response: HttpResponse = client.request(request)
// TODO: handle specific mapping types. e.g. Map<int, Class<?>>
- when {
- response.isRedirect -> return Redirection(
- response.code,
- response.headers.toMultimap()
- )
- response.isInformational -> return Informational(
- response.message,
- response.code,
- response.headers.toMultimap()
- )
- response.isSuccessful -> return Success(
- responseBody(response.body, accept),
- response.code,
- response.headers.toMultimap()
+ when (response.status.value) {
+ in 300..399 -> return Redirection(
+ response.status.value,
+ response.headers.toMap()
)
- response.isClientError -> return ClientError(
- response.body?.string(),
- response.code,
- response.headers.toMultimap()
+ in 100..199 -> return Informational(
+ response.status.description,
+ response.status.value,
+ response.headers.toMap()
)
+ in 200..299 -> return Success(
+ response.receive(),
+ response.status.value,
+ response.headers.toMap())
+ in 400..499 -> return ClientError(
+ response.receive(),
+ response.status.value,
+ response.headers.toMap())
else -> return ServerError(
- null,
- response.body?.string(),
- response.code,
- response.headers.toMultimap()
+ null,
+ response.receive(),
+ response.status.value,
+ response.headers.toMap()
)
}
}