Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some documentation and renamed template types #66

Merged
merged 2 commits into from
Jul 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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