From 4e0ca515a827220f9275f28649d83c1b09e44fcb Mon Sep 17 00:00:00 2001 From: Isidro Henoch Date: Sun, 5 Dec 2021 14:47:54 -0600 Subject: Adds x-www-form-urlencoded support to http client - Installs a serialization library - Installs a ktor logging library - Updates ApiClient to support x-www-form-urlencoded requests - Serializes the User data model --- .../TrackerMap/client/apis/SessionApi.kt | 8 +-- .../TrackerMap/client/infrastructure/ApiClient.kt | 58 +++++++++++++++++----- .../mx/trackermap/TrackerMap/client/models/User.kt | 52 +++++++++---------- 3 files changed, 77 insertions(+), 41 deletions(-) (limited to 'shared/src') diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/apis/SessionApi.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/apis/SessionApi.kt index d740a58..f80135d 100644 --- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/apis/SessionApi.kt +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/apis/SessionApi.kt @@ -11,6 +11,8 @@ */ package mx.trackermap.TrackerMap.client.apis +import io.ktor.client.request.forms.FormDataContent +import io.ktor.http.Parameters import mx.trackermap.TrackerMap.client.models.User import mx.trackermap.TrackerMap.client.infrastructure.* @@ -73,10 +75,10 @@ class SessionApi(basePath: kotlin.String = "https://demo.traccar.org/api") : Api * @return User */ @Suppress("UNCHECKED_CAST") - suspend fun sessionPost(email: kotlin.String, password: kotlin.String): User { - val localVariableBody: kotlin.Any? = mapOf("email" to "$email", "password" to "$password") + suspend fun sessionPost(email: String, password: String): User { + val localVariableBody = mapOf("email" to email, "password" to password) - val localVariableHeaders: kotlin.collections.Map = mapOf("Content-Type" to "multipart/form-data") + val localVariableHeaders = mapOf("Content-Type" to "application/x-www-form-urlencoded") val localVariableConfig = RequestConfig( RequestMethod.POST, "/session", headers = localVariableHeaders 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 c44d473..91d0aa2 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 @@ -4,10 +4,17 @@ 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.features.json.serializer.KotlinxSerializer +import io.ktor.client.features.logging.DEFAULT +import io.ktor.client.features.logging.LogLevel +import io.ktor.client.features.logging.Logger +import io.ktor.client.features.logging.Logging import io.ktor.client.request.* +import io.ktor.client.request.forms.FormDataContent import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.util.* +import kotlinx.serialization.json.Json as KotlinJson open class ApiClient(val baseUrl: String) { companion object { @@ -15,24 +22,41 @@ open class ApiClient(val baseUrl: String) { protected const val ApiAccept = "Accept" protected const val ApiJsonMediaType = "application/json" protected const val ApiFormDataMediaType = "multipart/form-data" + protected const val ApiFormURLType = "application/x-www-form-urlencoded" protected const val ApiXmlMediaType = "application/xml" val client: HttpClient = HttpClient(CIO) { - install(JsonFeature) + install(JsonFeature) { + serializer = KotlinxSerializer( + KotlinJson { + ignoreUnknownKeys = true + } + ) + } + install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.HEADERS + } } val defaultHeaders: Map = mapOf( ApiContentType to ApiJsonMediaType, - ApiAccept to ApiJsonMediaType) + ApiAccept to ApiJsonMediaType + ) val jsonHeaders: Map = mapOf( ApiContentType to ApiJsonMediaType, - ApiAccept to ApiJsonMediaType) + ApiAccept to ApiJsonMediaType + ) } - protected inline fun fillRequest(requestBuilder: HttpRequestBuilder, content: T, mediaType: String = ApiJsonMediaType) { + protected inline fun fillRequest( + requestBuilder: HttpRequestBuilder, + content: T, + mediaType: String = ApiJsonMediaType + ) { when { mediaType == ApiFormDataMediaType && content is Map<*, *> -> { val parametersBuilder = ParametersBuilder() @@ -51,6 +75,13 @@ open class ApiClient(val baseUrl: String) { requestBuilder.body = content } } + mediaType == ApiFormURLType && content is Map<*, *> -> { + val parametersBuilder = ParametersBuilder() + content.forEach { item -> + parametersBuilder[item.key as String] = item.value as String + } + requestBuilder.body = FormDataContent(parametersBuilder.build()) + } mediaType == ApiXmlMediaType -> TODO("xml not currently supported.") // TODO: this should be extended with other serializers @@ -58,7 +89,10 @@ open class ApiClient(val baseUrl: String) { } } - protected suspend inline fun request(requestConfig: RequestConfig, body: Any? = null): ApiInfrastructureResponse { + protected suspend inline fun request( + requestConfig: RequestConfig, + body: Any? = null + ): ApiInfrastructureResponse { val httpUrl: Url try { httpUrl = Url(baseUrl) @@ -67,7 +101,7 @@ open class ApiClient(val baseUrl: String) { } val urlBuilder = URLBuilder(httpUrl) - .path(requestConfig.path.trimStart('/')) + .path("${httpUrl.encodedPath.trimStart('/')}${requestConfig.path}") requestConfig.query.forEach { query -> query.value.forEach { queryValue -> @@ -76,7 +110,7 @@ open class ApiClient(val baseUrl: String) { } val url = urlBuilder.build() - val headers = requestConfig.headers + defaultHeaders + val headers = defaultHeaders + requestConfig.headers if (headers[ApiContentType] ?: "" == "") { throw IllegalStateException("Missing Content-Type header. This is required.") @@ -121,10 +155,6 @@ open class ApiClient(val baseUrl: String) { } } - headers.forEach { header -> - request.headers[header.key] = header.value - } - val response: HttpResponse = client.request(request) // TODO: handle specific mapping types. e.g. Map> @@ -141,11 +171,13 @@ open class ApiClient(val baseUrl: String) { in 200..299 -> return Success( response.receive(), response.status.value, - response.headers.toMap()) + response.headers.toMap() + ) in 400..499 -> return ClientError( response.receive(), response.status.value, - response.headers.toMap()) + response.headers.toMap() + ) else -> return ServerError( null, response.receive(), diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/User.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/User.kt index 926ec9d..1bda398 100644 --- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/User.kt +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/User.kt @@ -11,6 +11,10 @@ */ package mx.trackermap.TrackerMap.client.models +import kotlinx.datetime.LocalDate +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable + /** * @@ -37,30 +41,28 @@ package mx.trackermap.TrackerMap.client.models * @param token * @param attributes */ +@Serializable data class User ( - - val id: kotlin.Int? = null, - val name: kotlin.String? = null, - val email: kotlin.String? = null, - val phone: kotlin.String? = null, - val readonly: kotlin.Boolean? = null, - val administrator: kotlin.Boolean? = null, - val map: kotlin.String? = null, - val latitude: java.math.BigDecimal? = null, - val longitude: java.math.BigDecimal? = null, - val zoom: kotlin.Int? = null, - val password: kotlin.String? = null, - val twelveHourFormat: kotlin.Boolean? = null, - val coordinateFormat: kotlin.String? = null, - val disabled: kotlin.Boolean? = null, + val id: Int? = null, + val name: String? = null, + val email: String? = null, + val phone: String? = null, + val readonly: Boolean? = null, + val administrator: Boolean? = null, + val map: String? = null, + val latitude: Double? = null, + val longitude: Double? = null, + val zoom: Int? = null, + val password: String? = null, + val twelveHourFormat: Boolean? = null, + val coordinateFormat: String? = null, + val disabled: Boolean? = null, /* in IS0 8601 format. eg. `1963-11-22T18:30:00Z` */ - val expirationTime: java.time.LocalDateTime? = null, - val deviceLimit: kotlin.Int? = null, - val userLimit: kotlin.Int? = null, - val deviceReadonly: kotlin.Boolean? = null, - val limitCommands: kotlin.Boolean? = null, - val poiLayer: kotlin.String? = null, - val token: kotlin.String? = null, - val attributes: kotlin.Any? = null -) { -} \ No newline at end of file + val expirationTime: LocalDate? = null, + val deviceLimit: Int? = null, + val userLimit: Int? = null, + val deviceReadonly: Boolean? = null, + val limitCommands: Boolean? = null, + val poiLayer: String? = null, + val token: String? = null +) \ No newline at end of file -- cgit v1.2.3