Skip to content

Commit

Permalink
Merge branch 'develop' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
SMILEY4 committed Sep 17, 2022
2 parents 5c4d708 + 4a0f9b2 commit 74edd65
Show file tree
Hide file tree
Showing 30 changed files with 562 additions and 476 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ This library provides a Ktor plugin to document routes, generate an OpenApi Spec
- minimally invasive (no immediate change to existing code required)
- provides swagger-ui with no initial configuration required
- supports most of the [OpenAPI 3.0.3 Specification](https://swagger.io/specification/)
- Authentication (Basic, JWT, ...)
- automatic json-schema generation from arbitrary types/classes for bodies and parameters
- no annotations
- provide custom schemas or a custom schema-builder
- external/custom json-schemas for bodies
- protect Swagger-UI and OpenApi-Spec with custom authentication


## Documentation
Expand Down Expand Up @@ -68,7 +69,7 @@ get("hello", {
response {
HttpStatusCode.OK to {
description = "Successful Request"
body(String::class) { description = "the response" }
body<String> { description = "the response" }
}
HttpStatusCode.InternalServerError to {
description = "Something unexpected happened"
Expand All @@ -84,15 +85,15 @@ post("math/{operation}", {
tags = listOf("test")
description = "Performs the given operation on the given values and returns the result"
request {
pathParameter("operation", String::class) {
pathParameter<String>("operation") {
description = "the math operation to perform. Either 'add' or 'sub'"
}
body(MathRequest::class)
body<MathRequest>()
}
response {
HttpStatusCode.OK to {
description = "The operation was successful"
body(MathResult::class) {
body<MathResult> {
description = "The result of the operation"
}
}
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
}

group = "io.github.smiley4"
version = "0.5.2"
version = "0.6.0"

repositories {
mavenCentral()
Expand Down
63 changes: 1 addition & 62 deletions src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
package io.github.smiley4.ktorswaggerui

import io.github.smiley4.ktorswaggerui.specbuilder.ApiSpecBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiComponentsBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiContentBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiExampleBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiInfoBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiJsonSchemaBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiOAuthFlowsBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiParametersBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiPathBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiPathsBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiRequestBodyBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiResponsesBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiSchemaBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiSecuritySchemesBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiServersBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.OApiTagsBuilder
import io.github.smiley4.ktorswaggerui.specbuilder.RouteCollector
import io.ktor.server.application.ApplicationStarted
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.application.hooks.MonitoringEvent
Expand All @@ -37,7 +21,7 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration
if (application.pluginOrNull(Webjars) == null) {
application.install(Webjars)
}
apiSpecJson = getBuilder().build(application, pluginConfig)
apiSpecJson = ApiSpecBuilder().build(application, pluginConfig)
}

SwaggerRouting(
Expand All @@ -49,48 +33,3 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration
) { apiSpecJson }.setup(application)

}

private fun getBuilder(): ApiSpecBuilder {
return ApiSpecBuilder(
OApiInfoBuilder(),
OApiServersBuilder(),
OApiTagsBuilder(),
OApiPathsBuilder(
RouteCollector(),
OApiPathBuilder(
OApiParametersBuilder(
OApiSchemaBuilder(
OApiJsonSchemaBuilder()
)
),
OApiRequestBodyBuilder(
OApiContentBuilder(
OApiSchemaBuilder(
OApiJsonSchemaBuilder()
),
OApiExampleBuilder()
)
),
OApiResponsesBuilder(
OApiContentBuilder(
OApiSchemaBuilder(
OApiJsonSchemaBuilder()
),
OApiExampleBuilder()
),
OApiSchemaBuilder(
OApiJsonSchemaBuilder()
)
),
),
),
OApiComponentsBuilder(
OApiExampleBuilder(),
OApiSecuritySchemesBuilder(
OApiOAuthFlowsBuilder()
)
),
)
}


Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.smiley4.ktorswaggerui

import io.github.smiley4.ktorswaggerui.dsl.CustomSchemas
import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker
import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo
import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse
Expand Down Expand Up @@ -132,4 +133,13 @@ class SwaggerUIPluginConfig {

fun getTags(): List<OpenApiTag> = tags


private var customSchemas = CustomSchemas()

fun schemas(block: CustomSchemas.() -> Unit) {
this.customSchemas = CustomSchemas().apply(block)
}

fun getCustomSchemas() = customSchemas

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.github.smiley4.ktorswaggerui.dsl

import io.swagger.v3.oas.models.media.Schema
import java.lang.reflect.Type

@OpenApiDslMarker
class CustomSchemas {

private var jsonSchemaBuilder: ((type: Type) -> String?)? = null


/**
* Custom builder for building json-schemas from a given type. Return null to not use this builder for the given type.
*/
fun jsonSchemaBuilder(builder: (type: Type) -> String?) {
jsonSchemaBuilder = builder
}

fun getJsonSchemaBuilder() = jsonSchemaBuilder


private val customSchemas = mutableMapOf<String, BaseCustomSchema>()


/**
* Define the json-schema for an object/body with the given id
*/
fun json(id: String, provider: () -> String) {
customSchemas[id] = CustomJsonSchema(provider)
}


/**
* Define the [Schema] for an object/body with the given id
*/
fun openApi(id: String, provider: () -> Schema<Any>) {
customSchemas[id] = CustomOpenApiSchema(provider)
}


/**
* Define the external url for an object/body with the given id
*/
fun remote(id: String, url: String) {
customSchemas[id] = RemoteSchema(url)
}

fun getSchema(id: String): BaseCustomSchema? = customSchemas[id]


}


sealed class BaseCustomSchema

class CustomJsonSchema(val provider: () -> String) : BaseCustomSchema()

class CustomOpenApiSchema(val provider: () -> Schema<Any>) : BaseCustomSchema()

class RemoteSchema(val url: String) : BaseCustomSchema()
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ class OpenApiBody(
) {

/**
* url to a json-spec to use as schema for this body (alternative to 'type')
* id of a custom schema (alternative to 'type')
*/
var externalSchemaUrl: String? = null
var customSchemaId: String? = null


/**
* A brief description of the request body
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,17 @@ class OpenApiRequest {
/**
* The body returned with this response
*/
fun body(schemaUrl: String, block: OpenApiBody.() -> Unit) {
fun body(customSchemaId: String, block: OpenApiBody.() -> Unit) {
body = OpenApiBody(null).apply(block).apply {
externalSchemaUrl = schemaUrl
this.customSchemaId = customSchemaId
}
}


/**
* The body returned with this response
*/
fun body(schemaUrl: String) = body(schemaUrl) {}
fun body(customSchemaId: String) = body(customSchemaId) {}


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class OpenApiResponse(val statusCode: String) {
*/
fun body(schemaUrl: String, block: OpenApiBody.() -> Unit) {
body = OpenApiBody(null).apply(block).apply {
externalSchemaUrl = schemaUrl
customSchemaId = schemaUrl
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ package io.github.smiley4.ktorswaggerui.specbuilder
import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig
import io.ktor.server.application.Application
import io.swagger.v3.core.util.Json
import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI

/**
* Build the OpenApi-json for the given application
*/
class ApiSpecBuilder(
private val infoBuilder: OApiInfoBuilder,
private val serversBuilder: OApiServersBuilder,
private val tagsBuilder: OApiTagsBuilder,
private val pathsBuilder: OApiPathsBuilder,
private val componentsBuilder: OApiComponentsBuilder
) {
class ApiSpecBuilder {

private val infoBuilder = OApiInfoBuilder()
private val serversBuilder = OApiServersBuilder()
private val tagsBuilder = OApiTagsBuilder()
private val pathsBuilder = OApiPathsBuilder(RouteCollector())
private val componentsBuilder = OApiComponentsBuilder()


fun build(application: Application, config: SwaggerUIPluginConfig): String {
val componentCtx = ComponentsContext(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,64 @@ data class ComponentsContext(
/**
* Add the given schema for the given type to the components-section.
* The schema is an array, only the element type is added to the components-section
* @return the ref-string for the schema
* @return a schema referencing the complete schema (or the original schema if 'schemasInComponents' = false)
*/
fun addArraySchema(type: Type, schema: Schema<*>): String {
return when (type) {
is Class<*> -> addSchema(type.componentType, schema.items)
is ParameterizedType -> {
when (val actualTypeArgument = type.actualTypeArguments.firstOrNull()) {
is Class<*> -> addSchema(actualTypeArgument, schema.items)
is WildcardType -> {
addSchema(actualTypeArgument.upperBounds.first(), schema.items)
fun addArraySchema(type: Type, schema: Schema<*>): Schema<Any> {
if (this.schemasInComponents) {
val innerSchema: Schema<Any> = when (type) {
is Class<*> -> addSchema(type.componentType, schema.items)
is ParameterizedType -> {
when (val actualTypeArgument = type.actualTypeArguments.firstOrNull()) {
is Class<*> -> addSchema(actualTypeArgument, schema.items)
is WildcardType -> {
addSchema(actualTypeArgument.upperBounds.first(), schema.items)
}
else -> throw Exception("Could not add array-schema to components ($type)")
}
else -> throw Exception("Could not add array-schema to components ($type)")
}
else -> throw Exception("Could not add array-schema to components ($type)")
}
return Schema<Any>().apply {
this.type = "array"
this.items = Schema<Any>().apply {
`$ref` = innerSchema.`$ref`
}
}
else -> throw Exception("Could not add array-schema to components ($type)")
} else {
@Suppress("UNCHECKED_CAST")
return schema as Schema<Any>
}
}


/**
* Add the given schema for the given type to the components-section
* @return the ref-string for the schema
* @return a schema referencing the complete schema (or the original schema if 'schemasInComponents' = false)
*/
fun addSchema(type: Type, schema: Schema<*>): Schema<Any> {
return addSchema(getIdentifyingName(type), schema)
}


/**
* Add the given schema for the given type to the components-section
* @return a schema referencing the complete schema (or the original schema if 'schemasInComponents' = false)
*/
fun addSchema(type: Type, schema: Schema<*>): String {
val key = getIdentifyingName(type)
if (!schemas.containsKey(key)) {
schemas[key] = schema
fun addSchema(id: String, schema: Schema<*>): Schema<Any> {
if (schemasInComponents) {
if (!schemas.containsKey(id)) {
schemas[id] = schema
}
return Schema<Any>().apply {
`$ref` = asSchemaRef(id)
}
} else {
@Suppress("UNCHECKED_CAST")
return schema as Schema<Any>
}
return asSchemaRef(key)
}


private fun getIdentifyingName(type: Type): String {
return when (type) {
is Class<*> -> type.canonicalName
Expand Down
Loading

0 comments on commit 74edd65

Please sign in to comment.