diff options
author | Iván Ávalos <avalos@disroot.org> | 2021-12-03 21:49:03 -0600 |
---|---|---|
committer | Iván Ávalos <avalos@disroot.org> | 2021-12-03 21:49:03 -0600 |
commit | 33bab0553bceaa174b11b3fb7a9ba9d4de63526a (patch) | |
tree | c308bb5b7ccb310cd57de815c93a7c200537d759 /shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure | |
parent | c7039b6b0f6ab0f99fefecac07196ada6da2221a (diff) | |
download | etbsa-trackermap-mobile-33bab0553bceaa174b11b3fb7a9ba9d4de63526a.tar.gz etbsa-trackermap-mobile-33bab0553bceaa174b11b3fb7a9ba9d4de63526a.tar.bz2 etbsa-trackermap-mobile-33bab0553bceaa174b11b3fb7a9ba9d4de63526a.zip |
Added Swagger auto-generated API client (no Ktor yet)
Diffstat (limited to 'shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure')
11 files changed, 366 insertions, 0 deletions
diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiAbstractions.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiAbstractions.kt new file mode 100644 index 0000000..f63f301 --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiAbstractions.kt @@ -0,0 +1,20 @@ +package mx.trackermap.TrackerMap.client.infrastructure + +typealias MultiValueMap = Map<String, List<String>> + +fun collectionDelimiter(collectionFormat: String) = when (collectionFormat) { + "csv" -> "," + "tsv" -> "\t" + "pipes" -> "|" + "ssv" -> " " + else -> "" +} + +val defaultMultiValueConverter: (item: Any?) -> String = { item -> "$item" } + +fun <T : Any?> toMultiValue(items: List<T>, collectionFormat: String, map: (item: Any?) -> String = defaultMultiValueConverter): List<String> { + return when (collectionFormat) { + "multi" -> items.map(map) + else -> listOf(items.map(map).joinToString(separator = collectionDelimiter(collectionFormat))) + } +}
\ No newline at end of file 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 new file mode 100644 index 0000000..aa4e45f --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt @@ -0,0 +1,133 @@ +package mx.trackermap.TrackerMap.client.infrastructure + +import io.swagger.client.infrastructure.* +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaTypeOrNull +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() + + @JvmStatic + var defaultHeaders: Map<String, String> by ApplicationDelegates.setOnce( + mapOf( + ContentType to JsonMediaType, + Accept to JsonMediaType + ) + ) + + @JvmStatic + val jsonHeaders: Map<String, String> = mapOf(ContentType to JsonMediaType, Accept to JsonMediaType) + } + + 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) + } + 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.") + } + + 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() + } + } + + protected inline fun <reified T : Any?> request(requestConfig: RequestConfig, body: Any? = null): ApiInfrastructureResponse<T?> { + val httpUrl = baseUrl.toHttpUrlOrNull() ?: throw IllegalStateException("baseUrl is invalid.") + + var urlBuilder = httpUrl.newBuilder() + .addPathSegments(requestConfig.path.trimStart('/')) + + requestConfig.query.forEach { query -> + query.value.forEach { queryValue -> + urlBuilder = urlBuilder.addQueryParameter(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[Accept] ?: "" == "") { + throw kotlin.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) + } + + headers.forEach { header -> request = request.addHeader(header.key, header.value.toString()) } + + val realRequest = request.build() + val response = client.newCall(realRequest).execute() + + // 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() + ) + response.isClientError -> return ClientError( + response.body?.string(), + response.code, + response.headers.toMultimap() + ) + else -> return ServerError( + null, + response.body?.string(), + response.code, + response.headers.toMultimap() + ) + } + } +}
\ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiInfrastructureResponse.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiInfrastructureResponse.kt new file mode 100644 index 0000000..bc0b4fb --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiInfrastructureResponse.kt @@ -0,0 +1,40 @@ +package mx.trackermap.TrackerMap.client.infrastructure + +enum class ResponseType { + Success, Informational, Redirection, ClientError, ServerError +} + +abstract class ApiInfrastructureResponse<T>(val responseType: ResponseType) { + abstract val statusCode: Int + abstract val headers: Map<String, List<String>> +} + +class Success<T>( + val data: T, + override val statusCode: Int = -1, + override val headers: Map<String, List<String>> = mapOf() +) : ApiInfrastructureResponse<T>(ResponseType.Success) + +class Informational<T>( + val statusText: String, + override val statusCode: Int = -1, + override val headers: Map<String, List<String>> = mapOf() +) : ApiInfrastructureResponse<T>(ResponseType.Informational) + +class Redirection<T>( + override val statusCode: Int = -1, + override val headers: Map<String, List<String>> = mapOf() +) : ApiInfrastructureResponse<T>(ResponseType.Redirection) + +class ClientError<T>( + val body: Any? = null, + override val statusCode: Int = -1, + override val headers: Map<String, List<String>> = mapOf() +) : ApiInfrastructureResponse<T>(ResponseType.ClientError) + +class ServerError<T>( + val message: String? = null, + val body: Any? = null, + override val statusCode: Int = -1, + override val headers: Map<String, List<String>> +) : ApiInfrastructureResponse<T>(ResponseType.ServerError)
\ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApplicationDelegates.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApplicationDelegates.kt new file mode 100644 index 0000000..7b91344 --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApplicationDelegates.kt @@ -0,0 +1,29 @@ +package mx.trackermap.TrackerMap.client.infrastructure + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +object ApplicationDelegates { + /** + * Provides a property delegate, allowing the property to be set once and only once. + * + * If unset (no default value), a get on the property will throw [IllegalStateException]. + */ + fun <T> setOnce(defaultValue: T? = null): ReadWriteProperty<Any?, T> = SetOnce(defaultValue) + + private class SetOnce<T>(defaultValue: T? = null) : ReadWriteProperty<Any?, T> { + private var isSet = false + private var value: T? = defaultValue + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value ?: throw IllegalStateException("${property.name} not initialized") + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = synchronized(this) { + if (!isSet) { + this.value = value + isSet = true + } + } + } +}
\ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/Errors.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/Errors.kt new file mode 100644 index 0000000..8932856 --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/Errors.kt @@ -0,0 +1,42 @@ +@file:Suppress("unused") +package mx.trackermap.TrackerMap.client.infrastructure + +import java.lang.RuntimeException + +open class ClientException : RuntimeException { + + /** + * Constructs an [ClientException] with no detail message. + */ + constructor() : super() + + /** + * Constructs an [ClientException] with the specified detail message. + + * @param message the detail message. + */ + constructor(message: kotlin.String) : super(message) + + companion object { + private const val serialVersionUID: Long = 123L + } +} + +open class ServerException : RuntimeException { + + /** + * Constructs an [ServerException] with no detail message. + */ + constructor() : super() + + /** + * Constructs an [ServerException] with the specified detail message. + + * @param message the detail message. + */ + constructor(message: kotlin.String) : super(message) + + companion object { + private const val serialVersionUID: Long = 456L + } +}
\ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/LocalDateAdapter.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/LocalDateAdapter.kt new file mode 100644 index 0000000..3aa236a --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/LocalDateAdapter.kt @@ -0,0 +1,18 @@ +package mx.trackermap.TrackerMap.client.infrastructure + +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class LocalDateAdapter { + @ToJson + fun toJson(value: LocalDate): String { + return DateTimeFormatter.ISO_LOCAL_DATE.format(value) + } + + @FromJson + fun fromJson(value: String): LocalDate { + return LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME) + } +}
\ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/LocalDateTimeAdapter.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/LocalDateTimeAdapter.kt new file mode 100644 index 0000000..3fbb237 --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/LocalDateTimeAdapter.kt @@ -0,0 +1,19 @@ +package mx.trackermap.TrackerMap.client.infrastructure + +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class LocalDateTimeAdapter { + @ToJson + fun toJson(value: LocalDateTime): String { + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(value) + } + + @FromJson + fun fromJson(value: String): LocalDateTime { + return LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME) + } + +}
\ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/RequestConfig.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/RequestConfig.kt new file mode 100644 index 0000000..73205ff --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/RequestConfig.kt @@ -0,0 +1,16 @@ +package mx.trackermap.TrackerMap.client.infrastructure + +/** + * Defines a config object for a given request. + * NOTE: This object doesn't include 'body' because it + * allows for caching of the constructed object + * for many request definitions. + * NOTE: Headers is a Map<String,String> because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class RequestConfig( + val method: RequestMethod, + val path: String, + val headers: Map<String, String> = mapOf(), + val query: Map<String, List<String>> = mapOf() +)
\ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/RequestMethod.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/RequestMethod.kt new file mode 100644 index 0000000..f118678 --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/RequestMethod.kt @@ -0,0 +1,8 @@ +package mx.trackermap.TrackerMap.client.infrastructure + +/** + * Provides enumerated HTTP verbs + */ +enum class RequestMethod { + GET, DELETE, HEAD, OPTIONS, PATCH, POST, PUT +}
\ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ResponseExtensions.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ResponseExtensions.kt new file mode 100644 index 0000000..61debbd --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ResponseExtensions.kt @@ -0,0 +1,23 @@ +package mx.trackermap.TrackerMap.client.infrastructure + +import okhttp3.Response + +/** + * Provides an extension to evaluation whether the response is a 1xx code + */ +val Response.isInformational: Boolean get() = this.code in 100..199 + +/** + * Provides an extension to evaluation whether the response is a 3xx code + */ +val Response.isRedirect: Boolean get() = this.code in 300..399 + +/** + * Provides an extension to evaluation whether the response is a 4xx code + */ +val Response.isClientError: Boolean get() = this.code in 400..499 + +/** + * Provides an extension to evaluation whether the response is a 5xx (Standard) through 999 (non-standard) code + */ +val Response.isServerError: Boolean get() = this.code in 500..999
\ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/Serializer.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/Serializer.kt new file mode 100644 index 0000000..9caf24f --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/Serializer.kt @@ -0,0 +1,18 @@ +package mx.trackermap.TrackerMap.client.infrastructure + +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import mx.trackermap.TrackerMap.client.infrastructure.LocalDateAdapter +import mx.trackermap.TrackerMap.client.infrastructure.LocalDateTimeAdapter +import java.util.Date + +object Serializer { + @JvmStatic + val moshi: Moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .add(Date::class.java, Rfc3339DateJsonAdapter().nullSafe()) + .add(LocalDateTimeAdapter()) + .add(LocalDateAdapter()) + .build() +}
\ No newline at end of file |