1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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()
)
}
}
}
|