Skip to content

Commit

Permalink
Merge pull request #13 from papsign/experimental-parameters
Browse files Browse the repository at this point in the history
Experimental parameters
  • Loading branch information
Wicpar authored Mar 4, 2020
2 parents 5acd9fa + 486ba71 commit 37453e0
Show file tree
Hide file tree
Showing 81 changed files with 1,694 additions and 118 deletions.
12 changes: 3 additions & 9 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@ dependencies {
implementation "io.ktor:ktor-metrics:$ktor_version"
implementation "io.ktor:ktor-server-sessions:$ktor_version"

// needed for oauth interop
implementation "io.ktor:ktor-auth:$ktor_version"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-core-jvm:$ktor_version"
implementation "io.ktor:ktor-client-apache:$ktor_version"
// ------------------------

implementation "io.ktor:ktor-jackson:$ktor_version" // needed for parameter parsing and multipart parsing
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.8" // needed for multipart parsing
implementation 'org.webjars:swagger-ui:3.18.2'
Expand All @@ -37,10 +30,11 @@ dependencies {
}

compileKotlin {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.jvmTarget = "1.6"
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.ExperimentalStdlibApi"]
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.jvmTarget = "1.6"
}

wrapper {
Expand Down
5 changes: 3 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#Mon Mar 02 14:17:20 CET 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
24 changes: 13 additions & 11 deletions src/main/kotlin/com/papsign/kotlin/reflection/Reflection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import kotlin.reflect.*
import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.withNullability
import kotlin.reflect.jvm.jvmErasure

val unitKType = getKType<Unit>()
Expand All @@ -15,7 +14,8 @@ inline fun <reified T> isNullable(): Boolean {
}

inline fun <reified T> getKType(): KType {
return object : SuperTypeTokenHolder<T>() { }.getKTypeImpl().withNullability(isNullable<T>())
return typeOf<T>()
// return object : SuperTypeTokenHolder<T>() {}.getKTypeImpl().withNullability(isNullable<T>())
}

// TODO: Lobby for a real kotlin ::type for types because the status quo sucks
Expand All @@ -24,7 +24,7 @@ inline fun <reified T> getKType(): KType {
open class SuperTypeTokenHolder<T>

fun SuperTypeTokenHolder<*>.getKTypeImpl(): KType =
javaClass.genericSuperclass.toKType().arguments.single().type!!
javaClass.genericSuperclass.toKType().arguments.single().type!!

fun KClass<*>.toInvariantFlexibleProjection(arguments: List<KTypeProjection> = emptyList()): KTypeProjection {
// TODO: there should be an API in kotlin-reflect which creates KType instances corresponding to flexible types
Expand All @@ -41,13 +41,15 @@ fun Type.toKTypeProjection(): KTypeProjection = when (this) {
is Class<*> -> this.kotlin.toInvariantFlexibleProjection()
is ParameterizedType -> {
val erasure = (rawType as Class<*>).kotlin
erasure.toInvariantFlexibleProjection((erasure.typeParameters.zip(actualTypeArguments).map { (parameter, argument) ->
val projection = argument.toKTypeProjection()
projection.takeIf {
// Get rid of use-site projections on arguments, where the corresponding parameters already have a declaration-site projection
parameter.variance == KVariance.INVARIANT || parameter.variance != projection.variance
} ?: KTypeProjection.invariant(projection.type!!)
}))
erasure.toInvariantFlexibleProjection(
(erasure.typeParameters.zip(actualTypeArguments).map { (parameter, argument) ->
val projection = argument.toKTypeProjection()
projection.takeIf {
// Get rid of use-site projections on arguments, where the corresponding parameters already have a declaration-site projection
parameter.variance == KVariance.INVARIANT || parameter.variance != projection.variance
} ?: KTypeProjection.invariant(projection.type!!)
})
)
}
is WildcardType -> when {
lowerBounds.isNotEmpty() -> KTypeProjection.contravariant(lowerBounds.single().toKType())
Expand Down Expand Up @@ -122,4 +124,4 @@ fun KType.getObjectSubtypes(): Set<KType> {
// println(getKType<Array<Int?>?>())
// println(getKType<Array<Array<String>>>())
// println(getKType<Unit>())
//}
//}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.papsign.ktor.openapigen.annotations.parameters

import com.papsign.ktor.openapigen.openapi.ParameterLocation
import com.papsign.ktor.openapigen.parameters.PathParamStyle

@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@APIParam(ParameterLocation.path)
annotation class PathParam(val description: String, val required: Boolean = true, val deprecated: Boolean = false)
annotation class PathParam(
val description: String,
val style: PathParamStyle = PathParamStyle.simple,
val explode: Boolean = false,
val deprecated: Boolean = false
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package com.papsign.ktor.openapigen.annotations.parameters

import com.papsign.ktor.openapigen.openapi.ParameterLocation
import com.papsign.ktor.openapigen.parameters.QueryParamStyle

@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@APIParam(ParameterLocation.query)
annotation class QueryParam(val description: String, val allowEmptyValues: Boolean = false, val required: Boolean = true, val deprecated: Boolean = false)
annotation class QueryParam(
val description: String,
val style: QueryParamStyle = QueryParamStyle.form,
val explode: Boolean = true,
val allowEmptyValues: Boolean = false,
val deprecated: Boolean = false
)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.papsign.ktor.openapigen.classLogger
import com.papsign.ktor.openapigen.content.type.BodyParser
import com.papsign.ktor.openapigen.content.type.ContentTypeProvider
import com.papsign.ktor.openapigen.content.type.SelectedParser
import com.papsign.ktor.openapigen.generator.ParamBuilder
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.ofClass
import com.papsign.ktor.openapigen.modules.openapi.OperationModule
Expand Down Expand Up @@ -36,7 +35,7 @@ class RequestHandlerModule<T : Any>(

val requestMeta = requestClass.findAnnotation<Request>()

val parameters = provider.ofClass<ParameterProvider>().flatMap { it.getParameters(ParamBuilder(apiGen, provider)) }
val parameters = provider.ofClass<ParameterProvider>().flatMap { it.getParameters(apiGen, provider) }
operation.parameters = operation.parameters?.let { (it + parameters).distinct() } ?: parameters
operation.requestBody = operation.requestBody?.apply {
map.forEach { (key, value) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object RouteHandler: HandlerModule {
val methods = provider.ofClass<MethodProvider>()
if (methods.size > 1) error("API cannot have two methods simultaneously: ${methods.map { it.method.value }}")
val paths = provider.ofClass<PathProvider>()
val path = "/${paths.map { it.path.trim('/') }.filter { it.isNotBlank() }.joinToString("/")}"
val path = "/${paths.flatMap { it.path.split('/').filter(String::isNotEmpty) }.joinToString("/")}"
val operationModules = provider.ofClass<OperationModule>()
apiGen.api.paths.getOrPut(path) { PathItem() }.also {pathItem ->
methods.forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ import com.papsign.ktor.openapigen.modules.OpenAPIModule

interface HandlerModule: OpenAPIModule {
fun configure(apiGen: OpenAPIGen, provider: ModuleProvider<*>)
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.papsign.ktor.openapigen.modules.providers

import com.papsign.ktor.openapigen.generator.ParamBuilder
import com.papsign.ktor.openapigen.OpenAPIGen
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.OpenAPIModule
import com.papsign.ktor.openapigen.openapi.Parameter

interface ParameterProvider: OpenAPIModule {
fun getParameters(builder: ParamBuilder): List<Parameter<*>>
}
fun getParameters(apiGen: OpenAPIGen, provider: ModuleProvider<*>): List<Parameter<*>>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.papsign.ktor.openapigen.openapi

import com.papsign.ktor.openapigen.parameters.ParameterStyle

data class ExternalDocumentation(
var url: String,
var description: String? = null
Expand All @@ -14,7 +16,9 @@ data class Parameter<T>(
var allowEmptyValue: Boolean? = null,
var schema: Schema<T>? = null,
var example: T? = null,
var examples: MutableMap<String, T>? = null
var examples: MutableMap<String, T>? = null,
var style: ParameterStyle<*>? = null,
var explode: Boolean = false
// incomplete
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.papsign.ktor.openapigen.parameters

import com.papsign.ktor.openapigen.parameters.parsers.builders.Builder
import com.papsign.ktor.openapigen.parameters.parsers.builders.BuilderFactory

interface ParameterStyle<S> where S: ParameterStyle<S>, S: Enum<S> {
val name: String
val factory: BuilderFactory<Builder<S>, S>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.papsign.ktor.openapigen.parameters

import com.papsign.ktor.openapigen.parameters.parsers.builders.Builder
import com.papsign.ktor.openapigen.parameters.parsers.builders.BuilderFactory
import com.papsign.ktor.openapigen.parameters.parsers.builders.path.matrix.MatrixBuilderFactory
import com.papsign.ktor.openapigen.parameters.parsers.builders.path.label.LabelBuilderFactory
import com.papsign.ktor.openapigen.parameters.parsers.builders.path.simple.SimpleBuilderFactory


enum class PathParamStyle(override val factory: BuilderFactory<Builder<PathParamStyle>, PathParamStyle>): ParameterStyle<PathParamStyle> {
simple(SimpleBuilderFactory), label(LabelBuilderFactory), matrix(MatrixBuilderFactory)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.papsign.ktor.openapigen.parameters

import com.papsign.ktor.openapigen.parameters.parsers.builders.Builder
import com.papsign.ktor.openapigen.parameters.parsers.builders.BuilderFactory
import com.papsign.ktor.openapigen.parameters.parsers.builders.query.deepobject.DeepBuilderFactory
import com.papsign.ktor.openapigen.parameters.parsers.builders.query.delimited.PipeDelimitedBuilderFactory
import com.papsign.ktor.openapigen.parameters.parsers.builders.query.delimited.SpaceDelimitedBuilderFactory
import com.papsign.ktor.openapigen.parameters.parsers.builders.query.form.FormBuilderFactory

enum class QueryParamStyle(override val factory: BuilderFactory<Builder<QueryParamStyle>, QueryParamStyle>): ParameterStyle<QueryParamStyle> {
form(FormBuilderFactory), spaceDelimited(SpaceDelimitedBuilderFactory), pipeDelimited(PipeDelimitedBuilderFactory), deepObject(DeepBuilderFactory)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.papsign.ktor.openapigen.parameters.handlers

import com.papsign.ktor.openapigen.OpenAPIGen
import com.papsign.ktor.openapigen.annotations.parameters.PathParam
import com.papsign.ktor.openapigen.annotations.parameters.QueryParam
import com.papsign.ktor.openapigen.annotations.parameters.apiParam
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.openapi.Parameter
import com.papsign.ktor.openapigen.openapi.ParameterLocation
import com.papsign.ktor.openapigen.openapi.Schema
import com.papsign.ktor.openapigen.parameters.parsers.builders.Builder
import io.ktor.http.Parameters
import io.ktor.util.toMap
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.findAnnotation

class ModularParameterHander<T>(val parsers: Map<KParameter, Builder<*>>, val constructor: KFunction<T>) :
ParameterHandler<T> {

override fun parse(parameters: Parameters): T {
return constructor.callBy(parsers.mapValues { it.value.build(it.key.name!!, parameters.toMap()) })
}

override fun getParameters(apiGen: OpenAPIGen, provider: ModuleProvider<*>): List<Parameter<*>> {

fun createParam(param: KParameter, `in`: ParameterLocation, config: (Parameter<*>) -> Unit): Parameter<*> {
return Parameter<Any>(
param.name.toString(),
`in`,
!param.type.isMarkedNullable
).also {
it.schema = apiGen.schemaRegistrar[param.type].schema as Schema<Any>
config(it)
}
}

fun QueryParam.createParam(param: KParameter): Parameter<*> {
val parser = parsers[param]!!
return createParam(param, apiParam.`in`) {
it.description = description
it.allowEmptyValue = allowEmptyValues
it.deprecated = deprecated
it.style = parser.style
it.explode = parser.explode
}
}

fun PathParam.createParam(param: KParameter): Parameter<*> {
val parser = parsers[param]!!
return createParam(param, apiParam.`in`) {
it.description = description
it.deprecated = deprecated
it.style = parser.style
it.explode = parser.explode
}
}

return constructor.parameters.map {
it.findAnnotation<PathParam>()?.createParam(it) ?:
it.findAnnotation<QueryParam>()?.createParam(it) ?:
error("API routes with ${constructor.returnType} must have parameters annotated with one of ${paramAnnotationClasses.map { it.simpleName }}")
}
}

companion object {
private val paramAnnotationClasses = hashSetOf(PathParam::class, QueryParam::class)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.papsign.ktor.openapigen.parameters.handlers

import com.papsign.ktor.openapigen.modules.providers.ParameterProvider
import io.ktor.http.Parameters

interface ParameterHandler<T>: ParameterProvider {
fun parse(parameters: Parameters): T
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.papsign.ktor.openapigen.parameters.handlers

import com.papsign.ktor.openapigen.OpenAPIGen
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.openapi.Parameter
import com.papsign.ktor.openapigen.parameters.handlers.ParameterHandler
import io.ktor.http.Parameters

object UnitParameterHandler :
ParameterHandler<Unit> {
override fun parse(parameters: Parameters) = Unit
override fun getParameters(apiGen: OpenAPIGen, provider: ModuleProvider<*>): List<Parameter<*>> {
return listOf()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.papsign.ktor.openapigen.parameters.parsers.builders

import com.papsign.ktor.openapigen.parameters.ParameterStyle

interface Builder<S> where S: ParameterStyle<S>, S: Enum<S> {
val style: S
val explode: Boolean
fun build(key: String, parameters: Map<String, List<String>>): Any?
}
Loading

0 comments on commit 37453e0

Please sign in to comment.