aboutsummaryrefslogtreecommitdiff
path: root/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2021-12-03 21:49:03 -0600
committerIván Ávalos <avalos@disroot.org>2021-12-03 21:49:03 -0600
commit33bab0553bceaa174b11b3fb7a9ba9d4de63526a (patch)
treec308bb5b7ccb310cd57de815c93a7c200537d759 /shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure
parentc7039b6b0f6ab0f99fefecac07196ada6da2221a (diff)
downloadetbsa-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')
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiAbstractions.kt20
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt133
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiInfrastructureResponse.kt40
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApplicationDelegates.kt29
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/Errors.kt42
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/LocalDateAdapter.kt18
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/LocalDateTimeAdapter.kt19
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/RequestConfig.kt16
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/RequestMethod.kt8
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ResponseExtensions.kt23
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/Serializer.kt18
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