Skip to content

Commit

Permalink
Merge pull request #66 from Javuto2/master
Browse files Browse the repository at this point in the history
Some documentation and renamed template types
  • Loading branch information
Wicpar authored Jul 22, 2020
2 parents cb716fd + 8100b1e commit 1b1752a
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 164 deletions.
14 changes: 14 additions & 0 deletions src/main/kotlin/com/papsign/ktor/openapigen/APITag.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ import com.papsign.ktor.openapigen.model.info.ExternalDocumentationModel
import com.papsign.ktor.openapigen.model.info.TagModel


/**
* This interface is used to define tags to classify endpoints.
* It needs to be implemented using an enum so that the processor properly detects equality.
*
* This is assigned to a service using [com.papsign.ktor.openapigen.route.tag].
*
* Implementation example:
*
* enum class Tags(override val description: String) : APITag {
* EXAMPLE("Wow this is a tag?!")
* }
*
* @see [com.papsign.ktor.openapigen.route.tag]
*/
interface APITag {
val name: String
val description: String
Expand Down
48 changes: 42 additions & 6 deletions src/main/kotlin/com/papsign/ktor/openapigen/interop/StatusPages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,61 @@ import io.ktor.features.StatusPages
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond


/**
* Wraps [StatusPages.Configuration] to enable OpenAPI configuration for exception handling.
* ```
* val api = install(OpenAPIGen) { ... }
* ...
* // StatusPage interop, can also define exceptions per-route
* install(StatusPages) {
* withAPI(api) {
* exception<JsonMappingException, Error>(HttpStatusCode.BadRequest) {
* it.printStackTrace()
* Error("mapping.json", it.localizedMessage)
* }
* exception<ProperException, Error>(HttpStatusCode.BadRequest) {
* it.printStackTrace()
* Error(it.id, it.localizedMessage)
* }
* }
* }
* ```
*
* @param api the installed instance of [OpenAPIGen] in the [io.ktor.application.Application]
* @param cfg the block that loads the configuration, see [OpenAPIGenStatusPagesInterop]
*/
inline fun StatusPages.Configuration.withAPI(api: OpenAPIGen, crossinline cfg: OpenAPIGenStatusPagesInterop.() -> Unit = {}) {
OpenAPIGenStatusPagesInterop(api, this).cfg()
}

/**
* Wrapper for Status pages that handles exceptions and generates documentation in OpenAPI.
* This is useful for default error pages.
*/
class OpenAPIGenStatusPagesInterop(val api: OpenAPIGen, val statusCfg: StatusPages.Configuration) {

inline fun <reified EX : Throwable> exception(status: HttpStatusCode) {
val ex = apiException<EX>(status)
/**
* Registers a handler for exception type [TThrowable] and returns a [status] page
*/
inline fun <reified TThrowable : Throwable> exception(status: HttpStatusCode) {
val ex = apiException<TThrowable>(status)
api.globalModuleProvider.registerModule(ThrowsInfo(listOf(ex)))
statusCfg.exception<EX> {
statusCfg.exception<TThrowable> {
call.respond(status)
}
}

inline fun <reified EX : Throwable, reified B> exception(status: HttpStatusCode, example: B? = null, noinline gen: (EX) -> B) {
/**
* Registers a handler for exception type [TThrowable] and returns a [status] page that includes a response body
* of type [TResponse].
*
* @param example An example of the response body
* @param gen handler for [TThrowable] that should return an instance of [TResponse]
*/
inline fun <reified TThrowable : Throwable, reified TResponse> exception(status: HttpStatusCode, example: TResponse? = null, noinline gen: (TThrowable) -> TResponse) {
val ex = apiException(status, example, gen)
api.globalModuleProvider.registerModule(ThrowsInfo(listOf(ex)))
statusCfg.exception<EX> { t ->
statusCfg.exception<TThrowable> { t ->
val ret = gen(t)
if (ret != null) {
call.respond(status, ret)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ import com.papsign.ktor.openapigen.model.Described
import java.util.*
import kotlin.reflect.KProperty

class FlowsModel<T> : MutableMap<String, FlowsModel.FlowModel<T>> by HashMap<String, FlowModel<T>>()
where T : Enum<T>, T : Described {
class FlowsModel<TScope> : MutableMap<String, FlowsModel.FlowModel<TScope>> by HashMap<String, FlowModel<TScope>>()
where TScope : Enum<TScope>, TScope : Described {

private var implicit: FlowModel<T>? by this
private var password: FlowModel<T>? by this
private var clientCredentials: FlowModel<T>? by this
private var authorizationCode: FlowModel<T>? by this
private var implicit: FlowModel<TScope>? by this
private var password: FlowModel<TScope>? by this
private var clientCredentials: FlowModel<TScope>? by this
private var authorizationCode: FlowModel<TScope>? by this

fun implicit(
scopes: Iterable<T>,
scopes: Iterable<TScope>,
authorizationUrl: String,
refreshUrl: String? = null
): FlowsModel<T> {
): FlowsModel<TScope> {
implicit =
FlowModel.implicit(
scopes,
Expand All @@ -29,8 +29,8 @@ class FlowsModel<T> : MutableMap<String, FlowsModel.FlowModel<T>> by HashMap<Str
}

fun password(
scopes: Iterable<T>, tokenUrl: String, refreshUrl: String? = null
): FlowsModel<T> {
scopes: Iterable<TScope>, tokenUrl: String, refreshUrl: String? = null
): FlowsModel<TScope> {
password =
FlowModel.password(
scopes,
Expand All @@ -41,10 +41,10 @@ class FlowsModel<T> : MutableMap<String, FlowsModel.FlowModel<T>> by HashMap<Str
}

fun clientCredentials(
scopes: Iterable<T>,
scopes: Iterable<TScope>,
tokenUrl: String,
refreshUrl: String? = null
): FlowsModel<T> {
): FlowsModel<TScope> {
clientCredentials =
FlowModel.clientCredentials(
scopes,
Expand All @@ -55,11 +55,11 @@ class FlowsModel<T> : MutableMap<String, FlowsModel.FlowModel<T>> by HashMap<Str
}

fun authorizationCode(
scopes: Iterable<T>,
scopes: Iterable<TScope>,
authorizationUrl: String,
tokenUrl: String,
refreshUrl: String? = null
): FlowsModel<T> {
): FlowsModel<TScope> {
authorizationCode =
FlowModel.authorizationCode(
scopes,
Expand All @@ -70,14 +70,14 @@ class FlowsModel<T> : MutableMap<String, FlowsModel.FlowModel<T>> by HashMap<Str
return this
}

private operator fun setValue(any: Any, property: KProperty<*>, any1: FlowModel<T>?) {
private operator fun setValue(any: Any, property: KProperty<*>, any1: FlowModel<TScope>?) {
if (any1 == null)
this.remove(property.name)
else
this[property.name] = any1
}

private operator fun getValue(any: Any, property: KProperty<*>): FlowModel<T>? {
private operator fun getValue(any: Any, property: KProperty<*>): FlowModel<TScope>? {
return this[property.name]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package com.papsign.ktor.openapigen.model.security
import com.papsign.ktor.openapigen.model.DataModel
import com.papsign.ktor.openapigen.model.Described

data class SecuritySchemeModel<T> constructor(
data class SecuritySchemeModel<TScope> constructor(
val type: SecuritySchemeType,
val name: String,
val `in`: APIKeyLocation? = null,
val scheme: HttpSecurityScheme? = null,
val bearerFormat: String? = null,
val flows: FlowsModel<T>? = null,
val flows: FlowsModel<TScope>? = null,
val openIdConnectUrl: String? = null
): DataModel where T : Enum<T>, T : Described
): DataModel where TScope : Enum<TScope>, TScope : Described
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import com.papsign.ktor.openapigen.route.path.normal.NormalOpenAPIRoute
import io.ktor.application.ApplicationCall
import io.ktor.util.pipeline.PipelineContext

interface AuthProvider<A>: OpenAPIModule, DependentModule {
suspend fun getAuth(pipeline: PipelineContext<Unit, ApplicationCall>): A
fun apply(route: NormalOpenAPIRoute): OpenAPIAuthenticatedRoute<A>
interface AuthProvider<TAuth>: OpenAPIModule, DependentModule {
suspend fun getAuth(pipeline: PipelineContext<Unit, ApplicationCall>): TAuth
fun apply(route: NormalOpenAPIRoute): OpenAPIAuthenticatedRoute<TAuth>
val security: Iterable<Iterable<Security<*>>>
override val handlers: Collection<OpenAPIModule>
get() = listOf(AuthHandler)

data class Security<T>(val scheme: SecuritySchemeModel<T>, val requirements: List<T>) where T: Enum<T>, T: Described
data class Security<TScope>(val scheme: SecuritySchemeModel<TScope>, val requirements: List<TScope>) where TScope: Enum<TScope>, TScope: Described
}
84 changes: 64 additions & 20 deletions src/main/kotlin/com/papsign/ktor/openapigen/route/Functions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,57 +23,101 @@ fun <T : OpenAPIRoute<T>> T.route(path: String): T {
}
}

/**
* Creates a new route matching the specified [path]
*/
@ContextDsl
inline fun <T : OpenAPIRoute<T>> T.route(path: String, crossinline fn: T.() -> Unit) {
inline fun <TRoute : OpenAPIRoute<TRoute>> TRoute.route(path: String, crossinline fn: TRoute.() -> Unit) {
route(path).fn()
}

fun <T : OpenAPIRoute<T>> T.method(method: HttpMethod): T {
fun <TRoute : OpenAPIRoute<TRoute>> TRoute.method(method: HttpMethod): TRoute {
return child(ktorRoute.createChild(HttpMethodRouteSelector(method))).apply {
provider.registerModule(HttpMethodProviderModule(method))
}
}

/**
* Creates a new route matching the specified [method]
*/
@ContextDsl
inline fun <T : OpenAPIRoute<T>> T.method(method: HttpMethod, crossinline fn: T.() -> Unit) {
inline fun <TRoute : OpenAPIRoute<TRoute>> TRoute.method(method: HttpMethod, crossinline fn: TRoute.() -> Unit) {
method(method).fn()
}

fun <T : OpenAPIRoute<T>> T.provider(vararg content: ContentTypeProvider): T {
fun <TRoute : OpenAPIRoute<TRoute>> TRoute.provider(vararg content: ContentTypeProvider): TRoute {
return child().apply {
content.forEach {
provider.registerModule(it)
}
}
}

/**
* Creates a new route matching the specified [content]
*/
@ContextDsl
inline fun <T : OpenAPIRoute<T>> T.provider(vararg content: ContentTypeProvider, crossinline fn: T.() -> Unit) {
inline fun <TRoute : OpenAPIRoute<TRoute>> TRoute.provider(vararg content: ContentTypeProvider, crossinline fn: TRoute.() -> Unit) {
provider(*content).fn()
}


fun <T : OpenAPIRoute<T>> T.tag(tag: APITag): T {
/**
* Applies a tag to all children of this route.
* Parameter [tag] should be an enum that inherits from [APITag], check [APITag] description for
* an explanation.
*
* @param tag the tag to apply
* @return the same route that received the call to chain multiple calls
*/
fun <TRoute : OpenAPIRoute<TRoute>> TRoute.tag(tag: APITag): TRoute {
return child().apply {
provider.registerModule(TagModule(listOf(tag)))
}
}


/**
* This method assigns an OpenAPI [tag] too all child routes defined inside [fn].
* Parameter [tag] should be an enum that inherits from [APITag], check [APITag] description for an
* explanation.
*
* Usage example:
*
* // Defined tags
* enum class Tags(override val description: String) : APITag {
* EXAMPLE("Wow this is a tag?!")
* }
*
* ...
* apiRouting {
* route("examples") {
* tag(Tags.EXAMPLE) { // <-- Applies the tag here
* route("getTextData").get<StringParam, StringResponse> { params ->
* respond(StringResponse(params.a))
* }
* // Multiple routes can be specified here
* }
* }
* }
* ...
*
* @param tag the tag to apply
* @param fn the block where the sub routes are defined
*/
@ContextDsl
inline fun <T : OpenAPIRoute<T>> T.tag(tag: APITag, crossinline fn: T.() -> Unit) {
inline fun <TRoute : OpenAPIRoute<TRoute>> TRoute.tag(tag: APITag, crossinline fn: TRoute.() -> Unit) {
tag(tag).fn()
}

inline fun <reified P : Any, reified R : Any, reified B : Any, T : OpenAPIRoute<T>> T.preHandle(
exampleResponse: R? = null,
exampleRequest: B? = null,
noinline handle: T.() -> Unit
inline fun <reified TParams : Any, reified TResponse : Any, reified TRequest : Any, TRoute : OpenAPIRoute<TRoute>> TRoute.preHandle(
exampleResponse: TResponse? = null,
exampleRequest: TRequest? = null,
noinline handle: TRoute.() -> Unit
) {
preHandle<P, R, B, T>(
typeOf<P>(),
typeOf<R>(),
typeOf<B>(),
preHandle<TParams, TResponse, TRequest, TRoute>(
typeOf<TParams>(),
typeOf<TResponse>(),
typeOf<TRequest>(),
exampleResponse,
exampleRequest,
handle
Expand All @@ -82,13 +126,13 @@ inline fun <reified P : Any, reified R : Any, reified B : Any, T : OpenAPIRoute<

// hide this function from public api as it can be "misused" easily but make it accessible to inlined functions from this package
@PublishedApi
internal fun <P : Any, R : Any, B : Any, T : OpenAPIRoute<T>> T.preHandle(
internal fun <TParams : Any, TResponse : Any, TRequest : Any, TRoute : OpenAPIRoute<TRoute>> TRoute.preHandle(
pType: KType,
rType: KType,
bType: KType,
exampleResponse: R? = null,
exampleRequest: B? = null,
handle: T.() -> Unit
exampleResponse: TResponse? = null,
exampleRequest: TRequest? = null,
handle: TRoute.() -> Unit
) {
val path = pType.jvmErasure.findAnnotation<Path>()
val new = if (path != null) child(ktorRoute.createRouteFromPath(path.path)) else child()
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/com/papsign/ktor/openapigen/route/Info.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.RouteOpenAPIModule
import com.papsign.ktor.openapigen.modules.openapi.OperationModule

/**
* Adds a summary and description for the endpoint being configured
*/
fun info(summary: String? = null, description: String? = null) = EndpointInfo(summary, description)

data class EndpointInfo(
Expand Down
10 changes: 10 additions & 0 deletions src/main/kotlin/com/papsign/ktor/openapigen/route/RouteConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import io.ktor.routing.Routing
import io.ktor.routing.routing
import io.ktor.util.pipeline.ContextDsl

/**
* Wrapper for [io.ktor.routing.routing] to create the endpoints while configuring OpenAPI
* documentation at the same time.
*/
@ContextDsl
fun Application.apiRouting(config: NormalOpenAPIRoute.() -> Unit) {
routing {
Expand All @@ -18,6 +22,12 @@ fun Application.apiRouting(config: NormalOpenAPIRoute.() -> Unit) {
}
}

/**
* Wrapper for [io.ktor.routing.routing] to create the endpoints while configuring OpenAPI
* documentation at the same time.
*
* @param config
*/
@ContextDsl
fun Routing.apiRouting(config: NormalOpenAPIRoute.() -> Unit) {
NormalOpenAPIRoute(
Expand Down
Loading

0 comments on commit 1b1752a

Please sign in to comment.