Kotlin multiplatform auth (#4284)

* Includes support for the Javascript platform

* Fixes enum serialization with non-string values

* Updates to expose api dependencies to consumers

* Uses explicit class name for Kotlin collection classes

* Maps unknown object type to Kotlin String

The Kotlinx serialization library uses reflectionless serialization and
requires compile-time serialization declarations. As a result, unknown
objects are mapped to Kotlin String instances to enable compilation
where object types are not explicitly defined.

* Improves support for binary objects

Previously, objects that contained binary data were assigned the
InputProvider data type. This was suitable for a binary input form
parameter, but unsuitable for Base64 or octet binary strings. These
binary strings are now assigned a more suitable object.

* Includes Kotlin Multiplatform auth classes

Includes support for:
- api key
- http basic
- http bearer
- oauth (partial support)

https://github.com/OpenAPITools/openapi-generator/issues/4283

* Updates Kotlin samples
This commit is contained in:
Andrew Emery
2019-10-28 19:03:49 +11:00
committed by William Cheng
parent 0f2272d9a4
commit d92d84bb12
154 changed files with 1858 additions and 533 deletions

View File

@@ -21,6 +21,7 @@ import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.SupportingFile;
@@ -192,29 +193,48 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen {
// multiplatform default includes
defaultIncludes.add("io.ktor.client.request.forms.InputProvider");
defaultIncludes.add(packageName + ".infrastructure.Base64ByteArray");
defaultIncludes.add(packageName + ".infrastructure.OctetByteArray");
// multiplatform type mapping
typeMapping.put("number", "kotlin.Double");
typeMapping.put("file", "InputProvider");
typeMapping.put("file", "OctetByteArray");
typeMapping.put("binary", "OctetByteArray");
typeMapping.put("ByteArray", "Base64ByteArray");
typeMapping.put("object", "kotlin.String"); // kotlin.Any not serializable
// multiplatform import mapping
importMapping.put("BigDecimal", "kotlin.Double");
importMapping.put("UUID", "kotlin.String");
importMapping.put("URI", "kotlin.String");
importMapping.put("InputProvider", "io.ktor.client.request.forms.InputProvider");
importMapping.put("File", "io.ktor.client.request.forms.InputProvider");
importMapping.put("File", packageName + ".infrastructure.OctetByteArray");
importMapping.put("Timestamp", "kotlin.String");
importMapping.put("LocalDateTime", "kotlin.String");
importMapping.put("LocalDate", "kotlin.String");
importMapping.put("LocalTime", "kotlin.String");
importMapping.put("Base64ByteArray", packageName + ".infrastructure.Base64ByteArray");
importMapping.put("OctetByteArray", packageName + ".infrastructure.OctetByteArray");
// multiplatform specific supporting files
supportingFiles.add(new SupportingFile("infrastructure/Base64ByteArray.kt.mustache", infrastructureFolder, "Base64ByteArray.kt"));
supportingFiles.add(new SupportingFile("infrastructure/Bytes.kt.mustache", infrastructureFolder, "Bytes.kt"));
supportingFiles.add(new SupportingFile("infrastructure/HttpResponse.kt.mustache", infrastructureFolder, "HttpResponse.kt"));
supportingFiles.add(new SupportingFile("infrastructure/OctetByteArray.kt.mustache", infrastructureFolder, "OctetByteArray.kt"));
// multiplatform specific auth
final String authFolder = (sourceFolder + File.separator + packageName + File.separator + "auth").replace(".", "/");
supportingFiles.add(new SupportingFile("auth/ApiKeyAuth.kt.mustache", authFolder, "ApiKeyAuth.kt"));
supportingFiles.add(new SupportingFile("auth/Authentication.kt.mustache", authFolder, "Authentication.kt"));
supportingFiles.add(new SupportingFile("auth/HttpBasicAuth.kt.mustache", authFolder, "HttpBasicAuth.kt"));
supportingFiles.add(new SupportingFile("auth/HttpBearerAuth.kt.mustache", authFolder, "HttpBearerAuth.kt"));
supportingFiles.add(new SupportingFile("auth/OAuth.kt.mustache", authFolder, "OAuth.kt"));
// multiplatform specific testing files
supportingFiles.add(new SupportingFile("commonTest/coroutine.mustache", "src/commonTest/kotlin/util", "Coroutine.kt"));
supportingFiles.add(new SupportingFile("iosTest/coroutine.mustache", "src/iosTest/kotlin/util", "Coroutine.kt"));
supportingFiles.add(new SupportingFile("jvmTest/coroutine.mustache", "src/jvmTest/kotlin/util", "Coroutine.kt"));
supportingFiles.add(new SupportingFile("commonTest/Coroutine.kt.mustache", "src/commonTest/kotlin/util", "Coroutine.kt"));
supportingFiles.add(new SupportingFile("iosTest/Coroutine.kt.mustache", "src/iosTest/kotlin/util", "Coroutine.kt"));
supportingFiles.add(new SupportingFile("jsTest/Coroutine.kt.mustache", "src/jsTest/kotlin/util", "Coroutine.kt"));
supportingFiles.add(new SupportingFile("jvmTest/Coroutine.kt.mustache", "src/jvmTest/kotlin/util", "Coroutine.kt"));
// gradle wrapper supporting files
supportingFiles.add(new SupportingFile("gradlew.mustache", "", "gradlew"));
@@ -290,11 +310,22 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen {
if (operations != null) {
List<CodegenOperation> ops = (List<CodegenOperation>) operations.get("operation");
for (CodegenOperation operation : ops) {
// set multipart against all relevant operations
if (operation.hasConsumes == Boolean.TRUE) {
if (isMultipartType(operation.consumes)) {
operation.isMultipart = Boolean.TRUE;
}
}
// modify the data type of binary form parameters to a more friendly type for multiplatform builds
if (MULTIPLATFORM.equals(getLibrary()) && operation.allParams != null) {
for (CodegenParameter param : operation.allParams) {
if (param.dataFormat != null && param.dataFormat.equals("binary")) {
param.baseType = param.dataType = "io.ktor.client.request.forms.InputProvider";
}
}
}
}
}
return operations;

View File

@@ -29,7 +29,7 @@ import {{packageName}}.infrastructure.toMultiValue
@Suppress("UNCHECKED_CAST"){{/returnType}}
fun {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) : {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}} {
val localVariableBody: kotlin.Any? = {{#hasBodyParam}}{{#bodyParams}}{{paramName}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}null{{/hasFormParams}}{{#hasFormParams}}mapOf({{#formParams}}"{{{baseName}}}" to "${{paramName}}"{{#hasMore}}, {{/hasMore}}{{/formParams}}){{/hasFormParams}}{{/hasBodyParam}}
val localVariableQuery: MultiValueMap = {{^hasQueryParams}}mapOf()
val localVariableQuery: MultiValueMap = {{^hasQueryParams}}mutableMapOf()
{{/hasQueryParams}}{{#hasQueryParams}}mutableMapOf<kotlin.String, List<kotlin.String>>()
.apply {
{{#queryParams}}

View File

@@ -71,7 +71,7 @@ import java.io.Serializable
{{/allowableValues}}
{{#multiplatform}}
{{#nonPublicApi}}internal {{/nonPublicApi}}object Serializer : CommonEnumSerializer<{{nameInCamelCase}}>("{{nameInCamelCase}}", values(), values().map { it.value }.toTypedArray())
{{#nonPublicApi}}internal {{/nonPublicApi}}object Serializer : CommonEnumSerializer<{{nameInCamelCase}}>("{{nameInCamelCase}}", values(), values().map { it.value.toString() }.toTypedArray())
{{/multiplatform}}
}
{{/isEnum}}

View File

@@ -42,6 +42,6 @@ import kotlinx.serialization.internal.CommonEnumSerializer
{{/enumVars}}{{/allowableValues}}
{{#multiplatform}}
{{#nonPublicApi}}internal {{/nonPublicApi}}object Serializer : CommonEnumSerializer<{{classname}}>("{{classname}}", values(), values().map { it.value }.toTypedArray())
{{#nonPublicApi}}internal {{/nonPublicApi}}object Serializer : CommonEnumSerializer<{{classname}}>("{{classname}}", values(), values().map { it.value.toString() }.toTypedArray())
{{/multiplatform}}
}

View File

@@ -1,6 +1,6 @@
package {{packageName}}.infrastructure
typealias MultiValueMap = Map<String,List<String>>
typealias MultiValueMap = MutableMap<String,List<String>>
{{#nonPublicApi}}internal {{/nonPublicApi}}fun collectionDelimiter(collectionFormat: String) = when(collectionFormat) {
"csv" -> ","

View File

@@ -12,5 +12,5 @@ package {{packageName}}.infrastructure
val method: RequestMethod,
val path: String,
val headers: MutableMap<String, String> = mutableMapOf(),
val query: Map<String, List<String>> = mapOf()
val query: MutableMap<String, List<String>> = mutableMapOf()
)

View File

@@ -41,6 +41,8 @@ import kotlinx.serialization.internal.StringDescriptor
{{/returnType}}
suspend fun {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) : HttpResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}> {
val localVariableAuthNames = listOf<String>({{#authMethods}}"{{name}}"{{#hasMore}}, {{/hasMore}}{{/authMethods}})
val localVariableBody = {{#hasBodyParam}}{{#bodyParam}}{{#isListContainer}}{{operationIdCamelCase}}Request({{paramName}}.asList()){{/isListContainer}}{{^isListContainer}}{{#isMapContainer}}{{operationIdCamelCase}}Request({{paramName}}){{/isMapContainer}}{{^isMapContainer}}{{paramName}}{{/isMapContainer}}{{/isListContainer}}{{/bodyParam}}{{/hasBodyParam}}
{{^hasBodyParam}}
{{#hasFormParams}}
@@ -83,7 +85,8 @@ import kotlinx.serialization.internal.StringDescriptor
return {{#hasBodyParam}}jsonRequest{{/hasBodyParam}}{{^hasBodyParam}}{{#hasFormParams}}{{#isMultipart}}multipartFormRequest{{/isMultipart}}{{^isMultipart}}urlEncodedFormRequest{{/isMultipart}}{{/hasFormParams}}{{^hasFormParams}}request{{/hasFormParams}}{{/hasBodyParam}}(
localVariableConfig,
localVariableBody
localVariableBody,
localVariableAuthNames
).{{#isListContainer}}wrap<{{operationIdCamelCase}}Response>().map { value.toTypedArray() }{{/isListContainer}}{{^isListContainer}}{{#isMapContainer}}wrap<{{operationIdCamelCase}}Response>().map { value }{{/isMapContainer}}{{^isMapContainer}}wrap(){{/isMapContainer}}{{/isListContainer}}
}

View File

@@ -0,0 +1,16 @@
package {{packageName}}.auth
class ApiKeyAuth(private val location: String, val paramName: String) : Authentication {
var apiKey: String? = null
var apiKeyPrefix: String? = null
override fun apply(query: MutableMap<String, List<String>>, headers: MutableMap<String, String>) {
val key: String = apiKey ?: return
val prefix: String? = apiKeyPrefix
val value: String = if (prefix != null) "$prefix $key" else key
when (location) {
"query" -> query[paramName] = listOf(value)
"header" -> headers[paramName] = value
}
}
}

View File

@@ -0,0 +1,13 @@
package {{packageName}}.auth
interface Authentication {
/**
* Apply authentication settings to header and query params.
*
* @param query Query parameters.
* @param headers Header parameters.
*/
fun apply(query: MutableMap<String, List<String>>, headers: MutableMap<String, String>)
}

View File

@@ -0,0 +1,17 @@
package {{packageName}}.auth
import io.ktor.util.InternalAPI
import io.ktor.util.encodeBase64
class HttpBasicAuth : Authentication {
var username: String? = null
var password: String? = null
@InternalAPI
override fun apply(query: MutableMap<String, List<String>>, headers: MutableMap<String, String>) {
if (username == null && password == null) return
val str = (username ?: "") + ":" + (password ?: "")
val auth = str.encodeBase64()
headers["Authorization"] = "Basic $auth"
}
}

View File

@@ -0,0 +1,14 @@
package {{packageName}}.auth
class HttpBearerAuth(private val scheme: String?) : Authentication {
var bearerToken: String? = null
override fun apply(query: MutableMap<String, List<String>>, headers: MutableMap<String, String>) {
val token: String = bearerToken ?: return
headers["Authorization"] = (if (scheme != null) upperCaseBearer(scheme)!! + " " else "") + token
}
private fun upperCaseBearer(scheme: String): String? {
return if ("bearer".equals(scheme, ignoreCase = true)) "Bearer" else scheme
}
}

View File

@@ -0,0 +1,10 @@
package {{packageName}}.auth
class OAuth : Authentication {
var accessToken: String? = null
override fun apply(query: MutableMap<String, List<String>>, headers: MutableMap<String, String>) {
val token: String = accessToken ?: return
headers["Authorization"] = "Bearer $token"
}
}

View File

@@ -30,6 +30,7 @@ kotlin {
jvm()
iosArm64() { binaries { framework { freeCompilerArgs.add("-Xobjc-generics") } } }
iosX64() { binaries { framework { freeCompilerArgs.add("-Xobjc-generics") } } }
js()
sourceSets {
commonMain {
@@ -37,9 +38,9 @@ kotlin {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-json:$ktor_version"
implementation "io.ktor:ktor-client-serialization:$ktor_version"
api "io.ktor:ktor-client-core:$ktor_version"
api "io.ktor:ktor-client-json:$ktor_version"
api "io.ktor:ktor-client-serialization:$ktor_version"
}
}
@@ -57,9 +58,9 @@ kotlin {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version"
implementation "io.ktor:ktor-client-core-jvm:$ktor_version"
implementation "io.ktor:ktor-client-json-jvm:$ktor_version"
implementation "io.ktor:ktor-client-serialization-jvm:$ktor_version"
api "io.ktor:ktor-client-core-jvm:$ktor_version"
api "io.ktor:ktor-client-json-jvm:$ktor_version"
api "io.ktor:ktor-client-serialization-jvm:$ktor_version"
}
}
@@ -77,7 +78,7 @@ kotlin {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version"
implementation "io.ktor:ktor-client-ios:$ktor_version"
api "io.ktor:ktor-client-ios:$ktor_version"
}
}
@@ -91,9 +92,9 @@ kotlin {
iosArm64().compilations.main.defaultSourceSet {
dependsOn iosMain
dependencies {
implementation "io.ktor:ktor-client-ios-iosarm64:$ktor_version"
implementation "io.ktor:ktor-client-json-iosarm64:$ktor_version"
implementation "io.ktor:ktor-client-serialization-iosarm64:$ktor_version"
api "io.ktor:ktor-client-ios-iosarm64:$ktor_version"
api "io.ktor:ktor-client-json-iosarm64:$ktor_version"
api "io.ktor:ktor-client-serialization-iosarm64:$ktor_version"
}
}
@@ -104,9 +105,31 @@ kotlin {
iosX64().compilations.main.defaultSourceSet {
dependsOn iosMain
dependencies {
implementation "io.ktor:ktor-client-ios-iosx64:$ktor_version"
implementation "io.ktor:ktor-client-json-iosx64:$ktor_version"
implementation "io.ktor:ktor-client-serialization-iosx64:$ktor_version"
api "io.ktor:ktor-client-ios-iosx64:$ktor_version"
api "io.ktor:ktor-client-json-iosx64:$ktor_version"
api "io.ktor:ktor-client-serialization-iosx64:$ktor_version"
}
}
jsMain {
dependsOn commonMain
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
api "io.ktor:ktor-client-js:$ktor_version"
api "io.ktor:ktor-client-json-js:$ktor_version"
api "io.ktor:ktor-client-serialization-js:$ktor_version"
}
}
jsTest {
dependsOn commonTest
dependencies {
implementation "io.ktor:ktor-client-mock-js:$ktor_version"
implementation "io.ktor:ktor-client-js:$ktor_version"
implementation "io.ktor:ktor-client-json:$ktor_version"
implementation "io.ktor:ktor-client-serialization-js:$ktor_version"
}
}

View File

@@ -23,6 +23,7 @@ import kotlinx.serialization.json.JsonConfiguration
import {{apiPackage}}.*
import {{modelPackage}}.*
import {{packageName}}.auth.*
{{#nonPublicApi}}internal {{/nonPublicApi}}open class ApiClient(
private val baseUrl: String,
@@ -46,6 +47,14 @@ import {{modelPackage}}.*
httpClientEngine?.let { HttpClient(it, clientConfig) } ?: HttpClient(clientConfig)
}
private val authentications: kotlin.collections.Map<String, Authentication> by lazy {
mapOf({{#authMethods}}{{#isBasic}}{{#isBasicBasic}}
"{{name}}" to HttpBasicAuth(){{/isBasicBasic}}{{^isBasicBasic}}
"{{name}}" to HttpBearerAuth("{{scheme}}"){{/isBasicBasic}}{{/isBasic}}{{#isApiKey}}
"{{name}}" to ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{keyParamName}}"){{/isApiKey}}{{#isOAuth}}
"{{name}}" to OAuth(){{/isOAuth}}{{#hasMore}}, {{/hasMore}}{{/authMethods}})
}
{{#nonPublicApi}}internal {{/nonPublicApi}}companion object {
protected val UNSAFE_HEADERS = listOf(HttpHeaders.ContentType)
@@ -54,27 +63,96 @@ import {{modelPackage}}.*
{{classname}}.setMappers(serializer)
{{/apis}}{{/apiInfo}}
{{#models}}
{{#model}}{{^isAlias}}serializer.setMapper({{classname}}::class, {{classname}}.serializer()){{/isAlias}}{{/model}}
{{#model}}{{^isAlias}}serializer.setMapper({{modelPackage}}.{{classname}}::class, {{modelPackage}}.{{classname}}.{{#isEnum}}Serializer{{/isEnum}}{{^isEnum}}serializer(){{/isEnum}}){{/isAlias}}{{/model}}
{{/models}}
}
}
protected suspend fun multipartFormRequest(requestConfig: RequestConfig, body: List<PartData>?): HttpResponse {
return request(requestConfig, MultiPartFormDataContent(body ?: listOf()))
/**
* Set the username for the first HTTP basic authentication.
*
* @param username Username
*/
fun setUsername(username: String) {
val auth = authentications.values.firstOrNull { it is HttpBasicAuth } as HttpBasicAuth?
?: throw Exception("No HTTP basic authentication configured")
auth.username = username
}
protected suspend fun urlEncodedFormRequest(requestConfig: RequestConfig, body: Parameters?): HttpResponse {
return request(requestConfig, FormDataContent(body ?: Parameters.Empty))
/**
* Set the password for the first HTTP basic authentication.
*
* @param password Password
*/
fun setPassword(password: String) {
val auth = authentications.values.firstOrNull { it is HttpBasicAuth } as HttpBasicAuth?
?: throw Exception("No HTTP basic authentication configured")
auth.password = password
}
protected suspend fun jsonRequest(requestConfig: RequestConfig, body: Any? = null): HttpResponse {
/**
* Set the API key value for the first API key authentication.
*
* @param apiKey API key
* @param paramName The name of the API key parameter, or null or set the first key.
*/
fun setApiKey(apiKey: String, paramName: String? = null) {
val auth = authentications.values.firstOrNull { it is ApiKeyAuth && (paramName == null || paramName == it.paramName)} as ApiKeyAuth?
?: throw Exception("No API key authentication configured")
auth.apiKey = apiKey
}
/**
* Set the API key prefix for the first API key authentication.
*
* @param apiKeyPrefix API key prefix
* @param paramName The name of the API key parameter, or null or set the first key.
*/
fun setApiKeyPrefix(apiKeyPrefix: String, paramName: String? = null) {
val auth = authentications.values.firstOrNull { it is ApiKeyAuth && (paramName == null || paramName == it.paramName) } as ApiKeyAuth?
?: throw Exception("No API key authentication configured")
auth.apiKeyPrefix = apiKeyPrefix
}
/**
* Set the access token for the first OAuth2 authentication.
*
* @param accessToken Access token
*/
fun setAccessToken(accessToken: String) {
val auth = authentications.values.firstOrNull { it is OAuth } as OAuth?
?: throw Exception("No OAuth2 authentication configured")
auth.accessToken = accessToken
}
/**
* Set the access token for the first Bearer authentication.
*
* @param bearerToken The bearer token.
*/
fun setBearerToken(bearerToken: String) {
val auth = authentications.values.firstOrNull { it is HttpBearerAuth } as HttpBearerAuth?
?: throw Exception("No Bearer authentication configured")
auth.bearerToken = bearerToken
}
protected suspend fun multipartFormRequest(requestConfig: RequestConfig, body: kotlin.collections.List<PartData>?, authNames: kotlin.collections.List<String>): HttpResponse {
return request(requestConfig, MultiPartFormDataContent(body ?: listOf()), authNames)
}
protected suspend fun urlEncodedFormRequest(requestConfig: RequestConfig, body: Parameters?, authNames: kotlin.collections.List<String>): HttpResponse {
return request(requestConfig, FormDataContent(body ?: Parameters.Empty), authNames)
}
protected suspend fun jsonRequest(requestConfig: RequestConfig, body: Any? = null, authNames: kotlin.collections.List<String>): HttpResponse {
val contentType = (requestConfig.headers[HttpHeaders.ContentType]?.let { ContentType.parse(it) }
?: ContentType.Application.Json)
return if (body != null) request(requestConfig, serializer.write(body, contentType))
else request(requestConfig)
return if (body != null) request(requestConfig, serializer.write(body, contentType), authNames)
else request(requestConfig, authNames = authNames)
}
protected suspend fun request(requestConfig: RequestConfig, body: OutgoingContent = EmptyContent): HttpResponse {
protected suspend fun request(requestConfig: RequestConfig, body: OutgoingContent = EmptyContent, authNames: kotlin.collections.List<String>): HttpResponse {
requestConfig.updateForAuth(authNames)
val headers = requestConfig.headers
return client.call {
@@ -95,7 +173,14 @@ import {{modelPackage}}.*
}.response
}
private fun URLBuilder.appendPath(components: List<String>): URLBuilder = apply {
private fun RequestConfig.updateForAuth(authNames: kotlin.collections.List<String>) {
for (authName in authNames) {
val auth = authentications[authName] ?: throw Exception("Authentication undefined: $authName")
auth.apply(query, headers)
}
}
private fun URLBuilder.appendPath(components: kotlin.collections.List<String>): URLBuilder = apply {
encodedPath = encodedPath.trimEnd('/') + components.joinToString("/", prefix = "/") { it.encodeURLQueryComponent() }
}

View File

@@ -0,0 +1,29 @@
package {{packageName}}.infrastructure
import kotlinx.serialization.*
import kotlinx.serialization.internal.StringDescriptor
@Serializable
class Base64ByteArray(val value: ByteArray) {
@Serializer(Base64ByteArray::class)
companion object : KSerializer<Base64ByteArray> {
override val descriptor = StringDescriptor.withName("Base64ByteArray")
override fun serialize(encoder: Encoder, obj: Base64ByteArray) = encoder.encodeString(obj.value.encodeBase64())
override fun deserialize(decoder: Decoder) = Base64ByteArray(decoder.decodeString().decodeBase64Bytes())
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as Base64ByteArray
return value.contentEquals(other.value)
}
override fun hashCode(): Int {
return value.contentHashCode()
}
override fun toString(): String {
return "Base64ByteArray(${hex(value)})"
}
}

View File

@@ -0,0 +1,102 @@
package {{packageName}}.infrastructure
import kotlinx.io.core.*
import kotlin.experimental.and
private val digits = "0123456789abcdef".toCharArray()
private const val BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
private const val BASE64_MASK: Byte = 0x3f
private const val BASE64_PAD = '='
private val BASE64_INVERSE_ALPHABET = IntArray(256) { BASE64_ALPHABET.indexOf(it.toChar()) }
private fun String.toCharArray(): CharArray = CharArray(length) { get(it) }
private fun ByteArray.clearFrom(from: Int) = (from until size).forEach { this[it] = 0 }
private fun Int.toBase64(): Char = BASE64_ALPHABET[this]
private fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK
internal fun ByteArray.encodeBase64(): String = buildPacket { writeFully(this@encodeBase64) }.encodeBase64()
internal fun String.decodeBase64Bytes(): ByteArray = buildPacket { writeStringUtf8(dropLastWhile { it == BASE64_PAD }) }.decodeBase64Bytes().readBytes()
/**
* Encode [bytes] as a HEX string with no spaces, newlines and `0x` prefixes.
*
* Taken from https://github.com/ktorio/ktor/blob/master/ktor-utils/common/src/io/ktor/util/Crypto.kt
*/
internal fun hex(bytes: ByteArray): String {
val result = CharArray(bytes.size * 2)
var resultIndex = 0
val digits = digits
for (element in bytes) {
val b = element.toInt() and 0xff
result[resultIndex++] = digits[b shr 4]
result[resultIndex++] = digits[b and 0x0f]
}
return String(result)
}
/**
* Decode bytes from HEX string. It should be no spaces and `0x` prefixes.
*
* Taken from https://github.com/ktorio/ktor/blob/master/ktor-utils/common/src/io/ktor/util/Crypto.kt
*/
internal fun hex(s: String): ByteArray {
val result = ByteArray(s.length / 2)
for (idx in result.indices) {
val srcIdx = idx * 2
val high = s[srcIdx].toString().toInt(16) shl 4
val low = s[srcIdx + 1].toString().toInt(16)
result[idx] = (high or low).toByte()
}
return result
}
/**
* Encode [ByteReadPacket] in base64 format.
*
* Taken from https://github.com/ktorio/ktor/blob/424d1d2cfaa3281302c60af9500f738c8c2fc846/ktor-utils/common/src/io/ktor/util/Base64.kt
*/
private fun ByteReadPacket.encodeBase64(): String = buildString {
val data = ByteArray(3)
while (remaining > 0) {
val read = readAvailable(data)
data.clearFrom(read)
val padSize = (data.size - read) * 8 / 6
val chunk = ((data[0].toInt() and 0xFF) shl 16) or
((data[1].toInt() and 0xFF) shl 8) or
(data[2].toInt() and 0xFF)
for (index in data.size downTo padSize) {
val char = (chunk shr (6 * index)) and BASE64_MASK.toInt()
append(char.toBase64())
}
repeat(padSize) { append(BASE64_PAD) }
}
}
/**
* Decode [ByteReadPacket] from base64 format
*
* Taken from https://github.com/ktorio/ktor/blob/424d1d2cfaa3281302c60af9500f738c8c2fc846/ktor-utils/common/src/io/ktor/util/Base64.kt
*/
private fun ByteReadPacket.decodeBase64Bytes(): Input = buildPacket {
val data = ByteArray(4)
while (remaining > 0) {
val read = readAvailable(data)
val chunk = data.foldIndexed(0) { index, result, current ->
result or (current.fromBase64().toInt() shl ((3 - index) * 6))
}
for (index in data.size - 2 downTo (data.size - read)) {
val origin = (chunk shr (8 * index)) and 0xff
writeByte(origin.toByte())
}
}
}

View File

@@ -0,0 +1,29 @@
package {{packageName}}.infrastructure
import kotlinx.serialization.*
import kotlinx.serialization.internal.StringDescriptor
@Serializable
class OctetByteArray(val value: ByteArray) {
@Serializer(OctetByteArray::class)
companion object : KSerializer<OctetByteArray> {
override val descriptor = StringDescriptor.withName("OctetByteArray")
override fun serialize(encoder: Encoder, obj: OctetByteArray) = encoder.encodeString(hex(obj.value))
override fun deserialize(decoder: Decoder) = OctetByteArray(hex(decoder.decodeString()))
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as OctetByteArray
return value.contentEquals(other.value)
}
override fun hashCode(): Int {
return value.contentHashCode()
}
override fun toString(): String {
return "OctetByteArray(${hex(value)})"
}
}

View File

@@ -0,0 +1,7 @@
package util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
actual fun <T> runTest(block: suspend (scope : CoroutineScope) -> T): dynamic = GlobalScope.promise { block(this) }