[Bug][Kotlin-client] Can now handle path param of type list (#12244)

* Bugfix Kotlin-client: Can now handle path param of type list for jvm-volley and multiplatform. Also cleaning up generated code

* Adding samples to github workflow. Deleting old workflow

* Tweaking setup of jvm-volley

* Updating samples

Co-authored-by: William Cheng <wing328hk@gmail.com>
This commit is contained in:
Johan Sjöblom
2022-05-04 17:04:20 +00:00
committed by GitHub
parent ee038e7e6c
commit 706791f43f
183 changed files with 3583 additions and 668 deletions

View File

@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.openapitools.client">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application />
</manifest>

View File

@@ -0,0 +1,96 @@
package org.openapitools.client.apis
import android.content.Context
import com.android.volley.DefaultRetryPolicy
import com.android.volley.Request
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.volley.toolbox.BaseHttpStack
import com.android.volley.toolbox.Volley
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import com.google.gson.reflect.TypeToken
import org.openapitools.client.request.IRequestFactory
import org.openapitools.client.request.RequestFactory
import org.openapitools.client.infrastructure.CollectionFormats.*
/*
* If you wish to use a custom http stack with your client you
* can pass that to the request queue like:
* Volley.newRequestQueue(context.applicationContext, myCustomHttpStack)
*/
class DefaultApi (
private val context: Context,
private val requestQueue: Lazy<RequestQueue> = lazy(initializer = {
Volley.newRequestQueue(context.applicationContext)
}),
private val requestFactory: IRequestFactory = RequestFactory(),
private val basePath: String = "http://localhost",
private val postProcessors :List <(Request<*>) -> Unit> = listOf()) {
/**
*
*
* @param ids
* @return void
*/
suspend fun idsGet(ids: kotlin.collections.List<kotlin.String>): Unit {
val body: Any? = null
val contentTypes : Array<String> = arrayOf()
val contentType: String = if (contentTypes.isNotEmpty()) { contentTypes.first() } else { "application/json" }
// Do some work or avoid some work based on what we know about the model,
// before we delegate to a pluggable request factory template
// The request factory template contains only pure code and no templates
// to make it easy to override with your own.
// create path and map variables
val path = "/{ids}".replace("{" + "ids" + "}", ids.joinToString(","))
val formParams = mapOf<String, String>()
// TODO: Cater for allowing empty values
// TODO, if its apikey auth, then add the header names here and the hardcoded auth key
// Only support hard coded apikey in query param auth for when we do this first path
val queryParams = mapOf<String, String>()
.filter { it.value.isNotEmpty() }
val headerParams: Map<String, String> = mapOf()
return suspendCoroutine { continuation ->
val responseListener = Response.Listener<Unit> { response ->
continuation.resume(response)
}
val errorListener = Response.ErrorListener { error ->
continuation.resumeWithException(error)
}
val responseType = object : TypeToken<Unit>() {}.type
// Call the correct request builder based on whether we have a return type or a body.
// All other switching on types must be done in code inside the builder
val request: Request<Unit> = requestFactory.build(
Request.Method.GET,
"$basePath$path",
body,
headerParams,
queryParams,
formParams,
contentType,
responseType,
responseListener,
errorListener)
postProcessors.forEach { it.invoke(request) }
requestQueue.value.add(request)
}
}
}

View File

@@ -0,0 +1,33 @@
package org.openapitools.client.infrastructure
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import com.google.gson.stream.JsonToken.NULL
import java.io.IOException
class ByteArrayAdapter : TypeAdapter<ByteArray>() {
@Throws(IOException::class)
override fun write(out: JsonWriter?, value: ByteArray?) {
if (value == null) {
out?.nullValue()
} else {
out?.value(String(value))
}
}
@Throws(IOException::class)
override fun read(out: JsonReader?): ByteArray? {
out ?: return null
when (out.peek()) {
NULL -> {
out.nextNull()
return null
}
else -> {
return out.nextString().toByteArray()
}
}
}
}

View File

@@ -0,0 +1,56 @@
package org.openapitools.client.infrastructure
class CollectionFormats {
open class CSVParams {
var params: List<String>
constructor(params: List<String>) {
this.params = params
}
constructor(vararg params: String) {
this.params = listOf(*params)
}
override fun toString(): String {
return params.joinToString(",")
}
}
open class SSVParams : CSVParams {
constructor(params: List<String>) : super(params)
constructor(vararg params: String) : super(*params)
override fun toString(): String {
return params.joinToString(" ")
}
}
class TSVParams : CSVParams {
constructor(params: List<String>) : super(params)
constructor(vararg params: String) : super(*params)
override fun toString(): String {
return params.joinToString("\t")
}
}
class PIPESParams : CSVParams {
constructor(params: List<String>) : super(params)
constructor(vararg params: String) : super(*params)
override fun toString(): String {
return params.joinToString("|")
}
}
class SPACEParams : SSVParams()
}

View File

@@ -0,0 +1,35 @@
package org.openapitools.client.infrastructure
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import com.google.gson.stream.JsonToken.NULL
import java.io.IOException
import java.time.LocalDate
import java.time.format.DateTimeFormatter
class LocalDateAdapter(private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE) : TypeAdapter<LocalDate>() {
@Throws(IOException::class)
override fun write(out: JsonWriter?, value: LocalDate?) {
if (value == null) {
out?.nullValue()
} else {
out?.value(formatter.format(value))
}
}
@Throws(IOException::class)
override fun read(out: JsonReader?): LocalDate? {
out ?: return null
when (out.peek()) {
NULL -> {
out.nextNull()
return null
}
else -> {
return LocalDate.parse(out.nextString(), formatter)
}
}
}
}

View File

@@ -0,0 +1,35 @@
package org.openapitools.client.infrastructure
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import com.google.gson.stream.JsonToken.NULL
import java.io.IOException
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class LocalDateTimeAdapter(private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME) : TypeAdapter<LocalDateTime>() {
@Throws(IOException::class)
override fun write(out: JsonWriter?, value: LocalDateTime?) {
if (value == null) {
out?.nullValue()
} else {
out?.value(formatter.format(value))
}
}
@Throws(IOException::class)
override fun read(out: JsonReader?): LocalDateTime? {
out ?: return null
when (out.peek()) {
NULL -> {
out.nextNull()
return null
}
else -> {
return LocalDateTime.parse(out.nextString(), formatter)
}
}
}
}

View File

@@ -0,0 +1,35 @@
package org.openapitools.client.infrastructure
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import com.google.gson.stream.JsonToken.NULL
import java.io.IOException
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
class OffsetDateTimeAdapter(private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME) : TypeAdapter<OffsetDateTime>() {
@Throws(IOException::class)
override fun write(out: JsonWriter?, value: OffsetDateTime?) {
if (value == null) {
out?.nullValue()
} else {
out?.value(formatter.format(value))
}
}
@Throws(IOException::class)
override fun read(out: JsonReader?): OffsetDateTime? {
out ?: return null
when (out.peek()) {
NULL -> {
out.nextNull()
return null
}
else -> {
return OffsetDateTime.parse(out.nextString(), formatter)
}
}
}
}

View File

@@ -0,0 +1,119 @@
package org.openapitools.client.request
import com.android.volley.NetworkResponse
import com.android.volley.ParseError
import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.toolbox.HttpHeaderParser
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonSyntaxException
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.net.HttpURLConnection
import java.lang.reflect.Type
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.OffsetDateTime
import org.openapitools.client.infrastructure.OffsetDateTimeAdapter
import org.openapitools.client.infrastructure.LocalDateTimeAdapter
import org.openapitools.client.infrastructure.LocalDateAdapter
import org.openapitools.client.infrastructure.ByteArrayAdapter
class GsonRequest<T>(
method: Int,
url: String,
private val body: Any?,
private val headers: Map<String, String>?,
private val params: MutableMap<String, String>?,
private val contentTypeForBody: String?,
private val encodingForParams: String?,
private val gsonAdapters: Map<Type, Any>?,
private val type: Type,
private val listener: Response.Listener<T>,
errorListener: Response.ErrorListener
) : Request<T>(method, url, errorListener) {
val gsonBuilder: GsonBuilder = GsonBuilder()
.registerTypeAdapter(OffsetDateTime::class.java, OffsetDateTimeAdapter())
.registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeAdapter())
.registerTypeAdapter(LocalDate::class.java, LocalDateAdapter())
.registerTypeAdapter(ByteArray::class.java, ByteArrayAdapter())
.apply {
gsonAdapters?.forEach {
this.registerTypeAdapter(it.key, it.value)
}
}
val gson: Gson by lazy {
gsonBuilder.create()
}
private var response: NetworkResponse? = null
override fun deliverResponse(response: T?) {
listener.onResponse(response)
}
override fun getParams(): MutableMap<String, String>? = params ?: super.getParams()
override fun getBodyContentType(): String = contentTypeForBody ?: super.getBodyContentType()
override fun getParamsEncoding(): String = encodingForParams ?: super.getParamsEncoding()
override fun getHeaders(): MutableMap<String, String> {
val combined = HashMap<String, String>()
combined.putAll(super.getHeaders())
if (headers != null) {
combined.putAll(headers)
}
return combined
}
override fun getBody(): ByteArray? {
if (body != null) {
return gson.toJson(body).toByteArray(Charsets.UTF_8)
}
return super.getBody()
}
override fun parseNetworkResponse(response: NetworkResponse?): Response<T> {
return try {
this.response = copyTo(response)
val json = String(
response?.data ?: ByteArray(0),
Charset.forName(HttpHeaderParser.parseCharset(response?.headers))
)
Response.success(
gson.fromJson<T>(json, type),
HttpHeaderParser.parseCacheHeaders(response)
)
} catch (e: UnsupportedEncodingException) {
Response.error(ParseError(e))
} catch (e: JsonSyntaxException) {
Response.error(ParseError(e))
}
}
private fun copyTo(response: NetworkResponse?): NetworkResponse {
return if (response != null) {
NetworkResponse(
response.statusCode,
response.data,
response.notModified,
response.networkTimeMs,
response.allHeaders
)
} else {
// Return an empty response.
NetworkResponse(
HttpURLConnection.HTTP_BAD_METHOD,
ByteArray(0),
false,
0,
emptyList()
)
}
}
}

View File

@@ -0,0 +1,64 @@
package org.openapitools.client.request
import com.android.volley.Request
import com.android.volley.Response
import java.io.UnsupportedEncodingException
import java.lang.reflect.Type
import java.net.URLEncoder
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.time.format.DateTimeFormatter
import java.time.OffsetDateTime
import java.time.LocalDate
interface IRequestFactory {
companion object {
/**
* ISO 8601 date time format.
* @see https://en.wikipedia.org/wiki/ISO_8601
*/
fun formatDateTime(datetime: OffsetDateTime) = DateTimeFormatter.ISO_INSTANT.format(datetime)
fun formatDate(date: LocalDate) = DateTimeFormatter.ISO_LOCAL_DATE.format(date)
fun escapeString(str: String): String {
return try {
URLEncoder.encode(str, "UTF-8")
} catch (e: UnsupportedEncodingException) {
str
}
}
fun parameterToString(param: Any?) =
when (param) {
null -> ""
is OffsetDateTime -> formatDateTime(param)
is Collection<*> -> {
val b = StringBuilder()
for (o in param) {
if (b.isNotEmpty()) {
b.append(",")
}
b.append(o.toString())
}
b.toString()
}
else -> param.toString()
}
}
fun <T> build(
method: Int,
url : String,
body: Any?,
headers: Map<String, String>?,
queryParams: Map<String, String>?,
formParams: Map<String, String>?,
contentTypeForBody: String?,
type: Type,
responseListener: Response.Listener<T>,
errorListener: Response.ErrorListener): Request<T>
}

View File

@@ -0,0 +1,62 @@
// Knowing the details of an operation it will produce a call to a Volley Request constructor
package org.openapitools.client.request
import com.android.volley.Request
import com.android.volley.Response
import org.openapitools.client.request.IRequestFactory.Companion.escapeString
import java.lang.reflect.Type
import java.util.Locale
import java.util.UUID
class RequestFactory(private val headerFactories : List<() -> Map<String, String>> = listOf(), private val postProcessors :List <(Request<*>) -> Unit> = listOf(), private val gsonAdapters: Map<Type, Any> = mapOf()): IRequestFactory {
/**
* {@inheritDoc}
*/
@Suppress("UNCHECKED_CAST")
override fun <T> build(
method: Int,
url: String,
body: Any?,
headers: Map<String, String>?,
queryParams: Map<String, String>?,
formParams: Map<String, String>?,
contentTypeForBody: String?,
type: Type,
responseListener: Response.Listener<T>,
errorListener: Response.ErrorListener
): Request<T> {
val afterMarketHeaders = (headers?.toMutableMap() ?: mutableMapOf())
// Factory built and aftermarket
// Merge the after market headers on top of the base ones in case we are overriding per call auth
val allHeaders = headerFactories.fold(afterMarketHeaders) { acc, factory -> (acc + factory.invoke()).toMutableMap() }
// If we decide to support auth parameters in the url, then you will reference them by supplying a url string
// with known variable name refernces in the string. We will then apply
val updatedUrl = if (!queryParams.isNullOrEmpty()) {
queryParams.asSequence().fold("$url?") {acc, param ->
"$acc${escapeString(param.key)}=${escapeString(param.value)}&"
}.trimEnd('&')
} else {
url
}
val request = GsonRequest(
method,
updatedUrl,
body,
allHeaders,
formParams?.toMutableMap(),
contentTypeForBody,
null,
gsonAdapters,
type,
responseListener,
errorListener)
postProcessors.forEach{ it.invoke(request)}
return request
}
}