[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,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@@ -0,0 +1,20 @@
README.md
build.gradle
docs/DefaultApi.md
gradle.properties
gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.properties
gradlew
gradlew.bat
settings.gradle
src/main/AndroidManifest.xml
src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt
src/main/kotlin/org/openapitools/client/infrastructure/ByteArrayAdapter.kt
src/main/kotlin/org/openapitools/client/infrastructure/CollectionFormats.kt
src/main/kotlin/org/openapitools/client/infrastructure/CollectionFormats.kt
src/main/kotlin/org/openapitools/client/infrastructure/LocalDateAdapter.kt
src/main/kotlin/org/openapitools/client/infrastructure/LocalDateTimeAdapter.kt
src/main/kotlin/org/openapitools/client/infrastructure/OffsetDateTimeAdapter.kt
src/main/kotlin/org/openapitools/client/request/GsonRequest.kt
src/main/kotlin/org/openapitools/client/request/IRequestFactory.kt
src/main/kotlin/org/openapitools/client/request/RequestFactory.kt

View File

@@ -0,0 +1,189 @@
# org.openapitools.client - Kotlin client library for Demo
A kotlin client for Android using the currently recommended http client, Volley. See https://developer.android.com/training/volley
- Currently sends GsonRequests
- Currently only supports Gson as a serializer - will throw an exception if a different serializer is chosen
- Defaults the source location to src/main/java as per standard Android builds
## Design
Volley is a queue/request based layer on top of http url stack specific to Android. Android favours dependency injection and
a layered architecture, and IO performed off the main thread to maintain UI responsiveness, with a preferred technique of
kotlin co-routines. The code gen library reflects these factors.
- Api calls use co-routines, and execute them using volley callbacks to avoid tying up a thread.
- Facilitate dependency injection, with default implementations available.
- Generate a requestFactory that can be overridden
- Allow the passing of the RequestFactory per tag (api client) or per operation (an extra parameter is created on operations with non-global security), with per operation auth overriding global security.
- DI scoping of the Request Factory and pre-generated auth header factories allow for thread safe and secure setting of credentials.
- Lazy header factories allow for refreshing tokens etc
- Factoring of header factories to the Request Factory allow ambient provision of credentials. Code gen library is credential storage agnostic.
- Header factories allow the merging of generated headers from open api spec with dynamically added headers
- Injection of http url stack to allow custom http stacks. Default implementation is best practice singleton
- Data classes used for serialisation to reflect volley's preference - an immutable request that once queued can't be tampered with.
- Reuse model class and other jvm common infrastructure
- Optional generation of room database models, and transform methods to these from open api models
- Room and api models can be extended with additional extension properties.
## Future improvements
- Option to generate image requests on certain conditionals e.g content-type gif etc
- Support for kotlin serialization.
- Multi part form parameters and support for file inputs
## Usage
Hilt Dependency injection example - with default values for parameters overridden.
```
@Provides
internal fun provideSomeApi(
context: Context,
restService: IRestService,
configurationService: IConfigurationService,
sessionService: ISessionService
): SomeApi {
return SomeApi(
context = context,
requestQueue = restService.getRequestQueue(),
requestFactory = RequestFactory(listOf(createSessionHeaderFactory(sessionService), createTraceHeaderFactory()),
postProcessors = listOf(retryPolicySetter)),
basePath = configurationService.getBaseUrl()
)
}
```
Here is the constructor so you can see the defaults
```class SomeApi (
val context: Context,
val requestQueue: Lazy<RequestQueue> = lazy(initializer = {
Volley.newRequestQueue(context.applicationContext)
}),
val requestFactory: IRequestFactory = RequestFactory(),
val basePath: String = "https://yourbasepath.from_input_parameter.com/api",
private val postProcessors :List <(Request<*>) -> Unit> = listOf()) {
```
### Overriding defaults
The above constructor for each api allows the following to be customized
- A custom context, so either a singleton request queue or different scope can be created - see
https://developer.android.com/training/volley/requestqueue#singleton
- An overrideable request queue - which in turn can have a custom http url stack passed to it
- An overrideable request factory constructor call, or a request factory that can be overridden by a custom template, with
custom header factory, request post processors and custom gson adapters injected.
#### Overriding request generation
Request generation can be overridden by
- Overriding the entire request factory template
- Supplying custom header factories - methods that take any possible parameters but return a map of headers
- Supplying custom request post processors - methods that take and return the request object
Header factory examples can be found in the auth section, as these are implemented as header factories. eg
```
val basicAuthHeaderFactoryBuilder = { username: String?, password: String? ->
{ mapOf("Authorization" to "Basic " + Base64.encodeToString("${username ?: ""}:${password ?: ""}".toByteArray(), Base64.DEFAULT))}
}
```
In this case it's a lambda function (a factory method) that takes an username and password, and returns a map of headers. Other
generated code will supply the username and password. In this case it results in a map of just one key/value pair, but
it could be multiple. The important part is it's returning a map - and that the surrounding code
will can bind the inputs to it at some point.
Here is a different example that supplies tracing header values
```
/**
* Create a lambda of tracing headers to be injected into an API's [RequestFactory].
*/
private fun createTraceHeaderFactory(): () -> Map<String, String> = {
mapOf(
HttpHeaderType.b3_traceId.rawValue to UUIDExtensions.asTraceId(UUID.randomUUID()),
HttpHeaderType.b3_spanId.rawValue to UUIDExtensions.asSpanId(UUID.randomUUID()),
HttpHeaderType.b3_sampled.rawValue to "1"
)
}
```
Finally a post processor example
```
/**
* Configure a [DefaultRetryPolicy] to be injected into the [RequestFactory] with a maximum number of retries of zero.
*/
private val retryPolicySetter = { request: Request<*> ->
Unit.apply {
request.setRetryPolicy(
DefaultRetryPolicy(
RestService.DEFAULT_TIMEOUT_MS,
0,
DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
)
)
}
}
```
### Serialization
#### Gson and Polymorphic types
The GsonRequest object can be passed custom type adapters
```
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, Object>?,
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())
```
## Requires
* Kotlin 1.4.30
* Gradle 6.8.3
## Build
First, create the gradle wrapper script:
```
gradle wrapper
```
Then, run:
```
./gradlew check assemble
```
This runs all tests and packages the library.
<a name="documentation-for-api-endpoints"></a>
## Documentation for API Endpoints
All URIs are relative to *http://localhost*
Class | Method | HTTP request | Description
------------ | ------------- | ------------- | -------------
*DefaultApi* | [**idsGet**](docs/DefaultApi.md#idsget) | **GET** /{ids} |
<a name="documentation-for-models"></a>
## Documentation for Models
<a name="documentation-for-authorization"></a>
## Documentation for Authorization
All endpoints do not require authorization.

View File

@@ -0,0 +1,87 @@
buildscript {
ext.kotlin_version = '1.5.10'
ext.swagger_annotations_version = "1.6.2"
ext.gson_version = "2.8.6"
ext.volley_version = "1.2.0"
ext.junit_version = "4.13.2"
ext.robolectric_version = "4.5.1"
ext.concurrent_unit_version = "0.4.6"
repositories {
mavenLocal()
google()
maven {
url 'https://dl.google.com/dl/android/maven2'
}
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.android.tools.build:gradle:4.0.2'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
// Rename the aar correctly
libraryVariants.all { variant ->
variant.outputs.all { output ->
if (outputFile != null && outputFileName.endsWith('.aar')) {
outputFileName = "${archivesBaseName}-${version}.aar"
}
}
}
testOptions {
unitTests.returnDefaultValues = true
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "io.swagger:swagger-annotations:$swagger_annotations_version"
implementation "com.google.code.gson:gson:$gson_version"
implementation "com.android.volley:volley:${volley_version}"
testImplementation "junit:junit:$junit_version"
testImplementation "org.robolectric:robolectric:${robolectric_version}"
testImplementation "net.jodah:concurrentunit:${concurrent_unit_version}"
}
afterEvaluate {
android.libraryVariants.all { variant ->
def task = project.tasks.create "jar${variant.name.capitalize()}", Jar
task.description = "Create jar artifact for ${variant.name}"
task.dependsOn variant.javaCompile
task.from variant.javaCompile.destinationDir
task.destinationDirectory = project.file("${project.buildDir}/outputs/jar")
task.archiveFileName = "${project.name}-${variant.baseName}-${version}.jar"
artifacts.add('archives', task);
}
}

View File

@@ -0,0 +1,45 @@
# DefaultApi
All URIs are relative to *http://localhost*
Method | HTTP request | Description
------------- | ------------- | -------------
[**idsGet**](DefaultApi.md#idsGet) | **GET** /{ids} |
### Example
```kotlin
// Import classes:
//import org.openapitools.client.*
//import org.openapitools.client.infrastructure.*
//import org.openapitools.client.models.*
val apiClient = ApiClient()
val webService = apiClient.createWebservice(DefaultApi::class.java)
val ids : kotlin.collections.List<kotlin.String> = // kotlin.collections.List<kotlin.String> |
webService.idsGet(ids)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**ids** | [**kotlin.collections.List&lt;kotlin.String&gt;**](kotlin.String.md)| |
### Return type
null (empty response body)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined

View File

@@ -0,0 +1 @@
android.useAndroidX=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,2 @@
rootProject.name = 'kotlin-array-simple-string-jvm-volley'

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
}
}