diff options
-rw-r--r-- | shared/build.gradle.kts | 8 | ||||
-rw-r--r-- | shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt | 202 |
2 files changed, 121 insertions, 89 deletions
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 72a5a1c..49f718d 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -17,7 +17,13 @@ kotlin { } sourceSets { - val commonMain by getting + val commonMain by getting { + dependencies { + implementation("io.ktor:ktor-client-core:1.6.6") + implementation("io.ktor:ktor-client-cio:1.6.6") + implementation("io.ktor:ktor-client-serialization:1.6.6") + } + } val commonTest by getting { dependencies { implementation(kotlin("test-common")) 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() ) } } |