major: v4 alpha (#505)

This commit is contained in:
Ryan Brink
2023-09-04 00:22:46 -04:00
committed by GitHub
parent 904cc6816d
commit f2786fec08
54 changed files with 1531 additions and 558 deletions

View File

@ -12,6 +12,26 @@
## Released ## Released
## [4.0.0-alpha] - September 3rd, 2023
### Added
- Support for `type` on sealed interfaces
- Ability to provide custom serializers
### Fixed
- Exception thrown when inheriting variable
- Notarized routes not discarded on test completion
- Data classes with property members breaks schema generation
- Security cannot be applied to individual path operations
- Serialization fails on generic response
- Parameter example descriptions not being applied
### Removed
- Out of the box support for Jackson and Gson (can still be implemented through custom schema configurators)
## [3.14.4] - June 5th, 2023 ## [3.14.4] - June 5th, 2023
### Changed ### Changed

View File

@ -48,8 +48,6 @@ dependencies {
testFixturesApi("io.ktor:ktor-server-core:$ktorVersion") testFixturesApi("io.ktor:ktor-server-core:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-test-host:$ktorVersion") testFixturesApi("io.ktor:ktor-server-test-host:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization:$ktorVersion") testFixturesApi("io.ktor:ktor-serialization:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-jackson:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-gson:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") testFixturesApi("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-content-negotiation:$ktorVersion") testFixturesApi("io.ktor:ktor-server-content-negotiation:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-auth:$ktorVersion") testFixturesApi("io.ktor:ktor-server-auth:$ktorVersion")

View File

@ -10,6 +10,7 @@ class DeleteInfo private constructor(
override val summary: String, override val summary: String,
override val description: String, override val description: String,
override val externalDocumentation: ExternalDocumentation?, override val externalDocumentation: ExternalDocumentation?,
override val security: Map<String, List<String>>?,
override val operationId: String?, override val operationId: String?,
override val deprecated: Boolean, override val deprecated: Boolean,
override val parameters: List<Parameter> override val parameters: List<Parameter>
@ -33,7 +34,8 @@ class DeleteInfo private constructor(
externalDocumentation = externalDocumentation, externalDocumentation = externalDocumentation,
operationId = operationId, operationId = operationId,
deprecated = deprecated, deprecated = deprecated,
parameters = parameters parameters = parameters,
security = security
) )
} }
} }

View File

@ -12,7 +12,8 @@ class GetInfo private constructor(
override val externalDocumentation: ExternalDocumentation?, override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?, override val operationId: String?,
override val deprecated: Boolean, override val deprecated: Boolean,
override val parameters: List<Parameter> override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?
) : MethodInfo { ) : MethodInfo {
companion object { companion object {
@ -33,7 +34,8 @@ class GetInfo private constructor(
externalDocumentation = externalDocumentation, externalDocumentation = externalDocumentation,
operationId = operationId, operationId = operationId,
deprecated = deprecated, deprecated = deprecated,
parameters = parameters parameters = parameters,
security = security
) )
} }
} }

View File

@ -12,7 +12,8 @@ class HeadInfo private constructor(
override val externalDocumentation: ExternalDocumentation?, override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?, override val operationId: String?,
override val deprecated: Boolean, override val deprecated: Boolean,
override val parameters: List<Parameter> override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?,
) : MethodInfo { ) : MethodInfo {
companion object { companion object {
@ -33,7 +34,8 @@ class HeadInfo private constructor(
externalDocumentation = externalDocumentation, externalDocumentation = externalDocumentation,
operationId = operationId, operationId = operationId,
deprecated = deprecated, deprecated = deprecated,
parameters = parameters parameters = parameters,
security = security,
) )
} }
} }

View File

@ -9,6 +9,9 @@ sealed interface MethodInfo {
val tags: Set<String> val tags: Set<String>
val summary: String val summary: String
val description: String val description: String
val security: Map<String, List<String>>?
get() = null
val externalDocumentation: ExternalDocumentation? val externalDocumentation: ExternalDocumentation?
get() = null get() = null
val operationId: String? val operationId: String?
@ -28,6 +31,7 @@ sealed interface MethodInfo {
internal var tags: Set<String> = emptySet() internal var tags: Set<String> = emptySet()
internal var parameters: List<Parameter> = emptyList() internal var parameters: List<Parameter> = emptyList()
internal var errors: MutableList<ResponseInfo> = mutableListOf() internal var errors: MutableList<ResponseInfo> = mutableListOf()
internal var security: Map<String, List<String>>? = null
fun response(init: ResponseInfo.Builder.() -> Unit) = apply { fun response(init: ResponseInfo.Builder.() -> Unit) = apply {
val builder = ResponseInfo.Builder() val builder = ResponseInfo.Builder()
@ -59,6 +63,8 @@ sealed interface MethodInfo {
fun parameters(vararg parameters: Parameter) = apply { this.parameters = parameters.toList() } fun parameters(vararg parameters: Parameter) = apply { this.parameters = parameters.toList() }
fun security(security: Map<String, List<String>>) = apply { this.security = security }
abstract fun build(): T abstract fun build(): T
} }
} }

View File

@ -12,7 +12,8 @@ class OptionsInfo private constructor(
override val externalDocumentation: ExternalDocumentation?, override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?, override val operationId: String?,
override val deprecated: Boolean, override val deprecated: Boolean,
override val parameters: List<Parameter> override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?,
) : MethodInfo { ) : MethodInfo {
companion object { companion object {
@ -33,7 +34,8 @@ class OptionsInfo private constructor(
externalDocumentation = externalDocumentation, externalDocumentation = externalDocumentation,
operationId = operationId, operationId = operationId,
deprecated = deprecated, deprecated = deprecated,
parameters = parameters parameters = parameters,
security = security,
) )
} }
} }

View File

@ -13,7 +13,8 @@ class PatchInfo private constructor(
override val externalDocumentation: ExternalDocumentation?, override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?, override val operationId: String?,
override val deprecated: Boolean, override val deprecated: Boolean,
override val parameters: List<Parameter> override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?,
) : MethodInfoWithRequest { ) : MethodInfoWithRequest {
companion object { companion object {
@ -35,7 +36,8 @@ class PatchInfo private constructor(
externalDocumentation = externalDocumentation, externalDocumentation = externalDocumentation,
operationId = operationId, operationId = operationId,
deprecated = deprecated, deprecated = deprecated,
parameters = parameters parameters = parameters,
security = security,
) )
} }
} }

View File

@ -13,7 +13,8 @@ class PostInfo private constructor(
override val externalDocumentation: ExternalDocumentation?, override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?, override val operationId: String?,
override val deprecated: Boolean, override val deprecated: Boolean,
override val parameters: List<Parameter> override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?,
) : MethodInfoWithRequest { ) : MethodInfoWithRequest {
companion object { companion object {
@ -35,7 +36,8 @@ class PostInfo private constructor(
externalDocumentation = externalDocumentation, externalDocumentation = externalDocumentation,
operationId = operationId, operationId = operationId,
deprecated = deprecated, deprecated = deprecated,
parameters = parameters parameters = parameters,
security = security,
) )
} }
} }

View File

@ -13,7 +13,8 @@ class PutInfo private constructor(
override val externalDocumentation: ExternalDocumentation?, override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?, override val operationId: String?,
override val deprecated: Boolean, override val deprecated: Boolean,
override val parameters: List<Parameter> override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?,
) : MethodInfoWithRequest { ) : MethodInfoWithRequest {
companion object { companion object {
@ -35,7 +36,8 @@ class PutInfo private constructor(
externalDocumentation = externalDocumentation, externalDocumentation = externalDocumentation,
operationId = operationId, operationId = operationId,
deprecated = deprecated, deprecated = deprecated,
parameters = parameters parameters = parameters,
security = security,
) )
} }
} }

View File

@ -49,8 +49,8 @@ class RequestInfo private constructor(
fun description(s: String) = apply { this.description = s } fun description(s: String) = apply { this.description = s }
fun examples(vararg e: Pair<String, Any>) = apply { fun examples(vararg e: Pair<String, MediaType.Example>) = apply {
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) } this.examples = e.toMap()
} }
fun mediaTypes(vararg m: String) = apply { fun mediaTypes(vararg m: String) = apply {

View File

@ -57,8 +57,8 @@ class ResponseInfo private constructor(
fun description(s: String) = apply { this.description = s } fun description(s: String) = apply { this.description = s }
fun examples(vararg e: Pair<String, Any>) = apply { fun examples(vararg e: Pair<String, MediaType.Example>) = apply {
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) } this.examples = e.toMap()
} }
fun mediaTypes(vararg m: String) = apply { fun mediaTypes(vararg m: String) = apply {

View File

@ -1,16 +1,15 @@
package io.bkbn.kompendium.core.plugin package io.bkbn.kompendium.core.plugin
import io.bkbn.kompendium.core.attribute.KompendiumAttributes import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.OpenApiSpec
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call import io.ktor.server.application.call
import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.response.respond import io.ktor.server.response.respond
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.routing.application
import io.ktor.server.routing.get import io.ktor.server.routing.get
import io.ktor.server.routing.route import io.ktor.server.routing.route
import io.ktor.server.routing.routing import io.ktor.server.routing.routing
@ -19,25 +18,26 @@ import kotlin.reflect.KType
object NotarizedApplication { object NotarizedApplication {
class Config { class Config {
lateinit var spec: OpenApiSpec lateinit var spec: () -> OpenApiSpec
var openApiJson: Routing.() -> Unit = { var specRoute: (OpenApiSpec, Routing) -> Unit = { spec, routing ->
route("/openapi.json") { routing.route("/openapi.json") {
get { get {
call.respond(HttpStatusCode.OK, this@route.application.attributes[KompendiumAttributes.openApiSpec]) call.respond(spec)
} }
} }
} }
var customTypes: Map<KType, JsonSchema> = emptyMap() var customTypes: Map<KType, JsonSchema> = emptyMap()
var schemaConfigurator: SchemaConfigurator = SchemaConfigurator.Default() var schemaConfigurator: SchemaConfigurator = KotlinXSchemaConfigurator()
} }
operator fun invoke() = createApplicationPlugin( operator fun invoke() = createApplicationPlugin(
name = "NotarizedApplication", name = "NotarizedApplication",
createConfiguration = ::Config createConfiguration = ::Config
) { ) {
val spec = pluginConfig.spec val spec = pluginConfig.spec()
val routing = application.routing { } val routing = application.routing {}
pluginConfig.openApiJson(routing) this@createApplicationPlugin.pluginConfig.specRoute(spec, routing)
// pluginConfig.openApiJson(routing)
pluginConfig.customTypes.forEach { (type, schema) -> pluginConfig.customTypes.forEach { (type, schema) ->
spec.components.schemas[type.getSimpleSlug()] = schema spec.components.schemas[type.getSimpleSlug()] = schema
} }

View File

@ -118,10 +118,7 @@ object Helpers {
operationId = this.operationId, operationId = this.operationId,
deprecated = this.deprecated, deprecated = this.deprecated,
parameters = this.parameters, parameters = this.parameters,
security = config.security security = this.createCombinedSecurityContext(config),
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toMutableList(),
requestBody = when (this) { requestBody = when (this) {
is MethodInfoWithRequest -> this.request?.let { reqInfo -> is MethodInfoWithRequest -> this.request?.let { reqInfo ->
Request( Request(
@ -150,6 +147,25 @@ object Helpers {
).plus(this.errors.toResponseMap()) ).plus(this.errors.toResponseMap())
) )
private fun MethodInfo.createCombinedSecurityContext(config: SpecConfig): MutableList<Map<String, List<String>>>? {
val configSecurity = config.security
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toMutableList()
val methodSecurity = this.security
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toMutableList()
return when {
configSecurity == null && methodSecurity == null -> null
configSecurity == null -> methodSecurity
methodSecurity == null -> configSecurity
else -> configSecurity.plus(methodSecurity).toMutableList()
}
}
private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error -> private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error ->
error.responseCode.value to Response( error.responseCode.value to Response(
description = error.description, description = error.description,

View File

@ -6,17 +6,20 @@ import io.bkbn.kompendium.core.util.arrayConstraints
import io.bkbn.kompendium.core.util.complexRequest import io.bkbn.kompendium.core.util.complexRequest
import io.bkbn.kompendium.core.util.customAuthConfig import io.bkbn.kompendium.core.util.customAuthConfig
import io.bkbn.kompendium.core.util.customFieldNameResponse import io.bkbn.kompendium.core.util.customFieldNameResponse
import io.bkbn.kompendium.core.util.customScopesOnSiblingPathOperations
import io.bkbn.kompendium.core.util.dateTimeString import io.bkbn.kompendium.core.util.dateTimeString
import io.bkbn.kompendium.core.util.defaultAuthConfig import io.bkbn.kompendium.core.util.defaultAuthConfig
import io.bkbn.kompendium.core.util.defaultField import io.bkbn.kompendium.core.util.defaultField
import io.bkbn.kompendium.core.util.defaultParameter import io.bkbn.kompendium.core.util.defaultParameter
import io.bkbn.kompendium.core.util.doubleConstraints import io.bkbn.kompendium.core.util.doubleConstraints
import io.bkbn.kompendium.core.util.enrichedGenericResponse
import io.bkbn.kompendium.core.util.enrichedComplexGenericType import io.bkbn.kompendium.core.util.enrichedComplexGenericType
import io.bkbn.kompendium.core.util.enrichedGenericResponse
import io.bkbn.kompendium.core.util.enrichedNestedCollection import io.bkbn.kompendium.core.util.enrichedNestedCollection
import io.bkbn.kompendium.core.util.enrichedSimpleRequest import io.bkbn.kompendium.core.util.enrichedSimpleRequest
import io.bkbn.kompendium.core.util.enrichedSimpleResponse import io.bkbn.kompendium.core.util.enrichedSimpleResponse
import io.bkbn.kompendium.core.util.exampleParams import io.bkbn.kompendium.core.util.exampleParams
import io.bkbn.kompendium.core.util.exampleSummaryAndDescription
import io.bkbn.kompendium.core.util.fieldOutsideConstructor
import io.bkbn.kompendium.core.util.genericException import io.bkbn.kompendium.core.util.genericException
import io.bkbn.kompendium.core.util.genericPolymorphicResponse import io.bkbn.kompendium.core.util.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls
@ -44,7 +47,10 @@ import io.bkbn.kompendium.core.util.nullableEnumField
import io.bkbn.kompendium.core.util.nullableField import io.bkbn.kompendium.core.util.nullableField
import io.bkbn.kompendium.core.util.nullableNestedObject import io.bkbn.kompendium.core.util.nullableNestedObject
import io.bkbn.kompendium.core.util.nullableReference import io.bkbn.kompendium.core.util.nullableReference
import io.bkbn.kompendium.core.util.optionalReqExample
import io.bkbn.kompendium.core.util.overrideMediaTypes import io.bkbn.kompendium.core.util.overrideMediaTypes
import io.bkbn.kompendium.core.util.overrideSealedTypeIdentifier
import io.bkbn.kompendium.core.util.paramWrapper
import io.bkbn.kompendium.core.util.polymorphicCollectionResponse import io.bkbn.kompendium.core.util.polymorphicCollectionResponse
import io.bkbn.kompendium.core.util.polymorphicException import io.bkbn.kompendium.core.util.polymorphicException
import io.bkbn.kompendium.core.util.polymorphicMapResponse import io.bkbn.kompendium.core.util.polymorphicMapResponse
@ -52,7 +58,6 @@ import io.bkbn.kompendium.core.util.polymorphicResponse
import io.bkbn.kompendium.core.util.postNoReqBody import io.bkbn.kompendium.core.util.postNoReqBody
import io.bkbn.kompendium.core.util.primitives import io.bkbn.kompendium.core.util.primitives
import io.bkbn.kompendium.core.util.reqRespExamples import io.bkbn.kompendium.core.util.reqRespExamples
import io.bkbn.kompendium.core.util.optionalReqExample
import io.bkbn.kompendium.core.util.requiredParams import io.bkbn.kompendium.core.util.requiredParams
import io.bkbn.kompendium.core.util.responseHeaders import io.bkbn.kompendium.core.util.responseHeaders
import io.bkbn.kompendium.core.util.returnsList import io.bkbn.kompendium.core.util.returnsList
@ -66,9 +71,9 @@ import io.bkbn.kompendium.core.util.singleException
import io.bkbn.kompendium.core.util.stringConstraints import io.bkbn.kompendium.core.util.stringConstraints
import io.bkbn.kompendium.core.util.stringContentEncodingConstraints import io.bkbn.kompendium.core.util.stringContentEncodingConstraints
import io.bkbn.kompendium.core.util.stringPatternConstraints import io.bkbn.kompendium.core.util.stringPatternConstraints
import io.bkbn.kompendium.core.util.subtypeNotCompleteSetOfParentProperties
import io.bkbn.kompendium.core.util.topLevelNullable import io.bkbn.kompendium.core.util.topLevelNullable
import io.bkbn.kompendium.core.util.trailingSlash import io.bkbn.kompendium.core.util.trailingSlash
import io.bkbn.kompendium.core.util.paramWrapper
import io.bkbn.kompendium.core.util.unbackedFieldsResponse import io.bkbn.kompendium.core.util.unbackedFieldsResponse
import io.bkbn.kompendium.core.util.withOperationId import io.bkbn.kompendium.core.util.withOperationId
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
@ -78,6 +83,7 @@ import io.bkbn.kompendium.oas.security.ApiKeyAuth
import io.bkbn.kompendium.oas.security.BasicAuth import io.bkbn.kompendium.oas.security.BasicAuth
import io.bkbn.kompendium.oas.security.BearerAuth import io.bkbn.kompendium.oas.security.BearerAuth
import io.bkbn.kompendium.oas.security.OAuth import io.bkbn.kompendium.oas.security.OAuth
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.should import io.kotest.matchers.should
@ -85,6 +91,8 @@ import io.kotest.matchers.string.startWith
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.call
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.auth.Authentication import io.ktor.server.auth.Authentication
import io.ktor.server.auth.OAuthServerSettings import io.ktor.server.auth.OAuthServerSettings
@ -92,6 +100,11 @@ import io.ktor.server.auth.UserIdPrincipal
import io.ktor.server.auth.basic import io.ktor.server.auth.basic
import io.ktor.server.auth.jwt.jwt import io.ktor.server.auth.jwt.jwt
import io.ktor.server.auth.oauth import io.ktor.server.auth.oauth
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.net.URI import java.net.URI
import java.time.Instant import java.time.Instant
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -182,6 +195,9 @@ class KompendiumTest : DescribeSpec({
it("Can generate example optional request body") { it("Can generate example optional request body") {
openApiTestAllSerializers("T0069__example_optional_req.json") { optionalReqExample() } openApiTestAllSerializers("T0069__example_optional_req.json") { optionalReqExample() }
} }
it("Can generate example summary and description") {
openApiTestAllSerializers("T0075__example_summary_and_description.json") { exampleSummaryAndDescription() }
}
} }
describe("Defaults") { describe("Defaults") {
it("Can generate a default parameter value") { it("Can generate a default parameter value") {
@ -235,6 +251,13 @@ class KompendiumTest : DescribeSpec({
it("Can handle a really gnarly generic example") { it("Can handle a really gnarly generic example") {
openApiTestAllSerializers("T0043__gnarly_generic_example.json") { gnarlyGenericResponse() } openApiTestAllSerializers("T0043__gnarly_generic_example.json") { gnarlyGenericResponse() }
} }
it("Can override the type name for a sealed interface implementation") {
openApiTestAllSerializers("T0070__sealed_interface_type_name_override.json") { overrideSealedTypeIdentifier() }
}
it("Can serialize an object where the subtype is not a complete set of parent properties") {
openApiTestAllSerializers("T0071__subtype_not_complete_set_of_parent_properties.json") {
subtypeNotCompleteSetOfParentProperties()
}
} }
describe("Custom Serializable Reader tests") { describe("Custom Serializable Reader tests") {
it("Can support ignoring fields") { it("Can support ignoring fields") {
@ -316,6 +339,39 @@ class KompendiumTest : DescribeSpec({
} }
) { notarizedGet() } ) { notarizedGet() }
} }
it("Can apply a custom serialization strategy to the openapi document") {
val customJsonEncoder = Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
}
openApiTestAllSerializers(
snapshotName = "T0072__custom_serialization_strategy.json",
notarizedApplicationConfigOverrides = {
specRoute = { spec, routing ->
routing {
route("/openapi.json") {
get {
call.response.headers.append("Content-Type", "application/json")
call.respondText { customJsonEncoder.encodeToString(spec) }
}
}
}
}
},
contentNegotiation = {
json(
Json {
encodeDefaults = true
explicitNulls = true
}
)
}
) { notarizedGet() }
}
it("Can serialize a data class with a field outside of the constructor") {
openApiTestAllSerializers("T0073__data_class_with_field_outside_constructor.json") { fieldOutsideConstructor() }
}
} }
describe("Error Handling") { describe("Error Handling") {
it("Throws a clear exception when an unidentified type is encountered") { it("Throws a clear exception when an unidentified type is encountered") {
@ -447,6 +503,50 @@ class KompendiumTest : DescribeSpec({
} }
) { multipleAuthStrategies() } ) { multipleAuthStrategies() }
} }
it("Can provide different scopes on path operations in the same route") {
openApiTestAllSerializers(
snapshotName = "T0074__auth_on_specific_path_operation.json",
applicationSetup = {
install(Authentication) {
oauth("auth-oauth-google") {
urlProvider = { "http://localhost:8080/callback" }
providerLookup = {
OAuthServerSettings.OAuth2ServerSettings(
name = "google",
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
accessTokenUrl = "https://accounts.google.com/o/oauth2/token",
requestMethod = HttpMethod.Post,
clientId = "DUMMY_VAL",
clientSecret = "DUMMY_VAL",
defaultScopes = listOf("https://www.googleapis.com/auth/userinfo.profile"),
extraTokenParameters = listOf("access_type" to "offline")
)
}
client = HttpClient(CIO)
}
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"auth-oauth-google" to OAuth(
flows = OAuth.Flows(
implicit = OAuth.Flows.Implicit(
authorizationUrl = "https://accounts.google.com/o/oauth2/auth",
scopes = mapOf(
"write:pets" to "modify pets in your account",
"read:pets" to "read your pets"
)
)
)
)
)
)
)
}
) { customScopesOnSiblingPathOperations() }
}
} }
describe("Enrichment") { describe("Enrichment") {
it("Can enrich a simple request") { it("Can enrich a simple request") {
@ -487,4 +587,5 @@ class KompendiumTest : DescribeSpec({
openApiTestAllSerializers("T0064__array_constraints.json") { arrayConstraints() } openApiTestAllSerializers("T0064__array_constraints.json") { arrayConstraints() }
} }
} }
}
}) })

View File

@ -2,6 +2,7 @@ package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.TestResponse import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription
import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
@ -11,6 +12,7 @@ import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.auth.authenticate import io.ktor.server.auth.authenticate
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.routing.post
import io.ktor.server.routing.route import io.ktor.server.routing.route
fun Routing.defaultAuthConfig() { fun Routing.defaultAuthConfig() {
@ -42,6 +44,43 @@ fun Routing.customAuthConfig() {
} }
} }
fun Routing.customScopesOnSiblingPathOperations() {
authenticate("auth-oauth-google") {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
security = mapOf(
"auth-oauth-google" to listOf("read:pets")
)
}
post = PostInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
request {
description(defaultResponseDescription)
requestType<TestResponse>()
}
security = mapOf(
"auth-oauth-google" to listOf("write:pets")
)
}
}
}
}
}
fun Routing.multipleAuthStrategies() { fun Routing.multipleAuthStrategies() {
authenticate("jwt", "api-key") { authenticate("jwt", "api-key") {
route(rootPath) { route(rootPath) {

View File

@ -11,6 +11,7 @@ import io.bkbn.kompendium.core.util.TestModules.defaultRequestDescription
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
import io.bkbn.kompendium.core.util.TestModules.rootPath import io.bkbn.kompendium.core.util.TestModules.rootPath
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.MediaType
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install import io.ktor.server.application.install
@ -27,7 +28,7 @@ fun Routing.reqRespExamples() {
description(defaultRequestDescription) description(defaultRequestDescription)
requestType<TestRequest>() requestType<TestRequest>()
examples( examples(
"Testerina" to TestRequest(TestNested("asdf"), 1.5, emptyList()) "Testerina" to MediaType.Example(TestRequest(TestNested("asdf"), 1.5, emptyList()))
) )
} }
response { response {
@ -35,7 +36,7 @@ fun Routing.reqRespExamples() {
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<TestResponse>() responseType<TestResponse>()
examples( examples(
"Testerino" to TestResponse("Heya") "Testerino" to MediaType.Example(TestResponse("Heya"))
) )
} }
} }
@ -50,7 +51,7 @@ fun Routing.exampleParams() = basicGetGenerator<TestResponse>(
`in` = Parameter.Location.path, `in` = Parameter.Location.path,
schema = TypeDefinition.STRING, schema = TypeDefinition.STRING,
examples = mapOf( examples = mapOf(
"foo" to Parameter.Example("testing") "foo" to MediaType.Example("testing")
) )
) )
) )
@ -66,7 +67,7 @@ fun Routing.optionalReqExample() {
description(defaultRequestDescription) description(defaultRequestDescription)
requestType<TestRequest>() requestType<TestRequest>()
examples( examples(
"Testerina" to TestRequest(TestNested("asdf"), 1.5, emptyList()) "Testerina" to MediaType.Example(TestRequest(TestNested("asdf"), 1.5, emptyList()))
) )
required(false) required(false)
} }
@ -75,7 +76,37 @@ fun Routing.optionalReqExample() {
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<TestResponse>() responseType<TestResponse>()
examples( examples(
"Testerino" to TestResponse("Heya") "Testerino" to MediaType.Example(TestResponse("Heya"))
)
}
}
}
}
}
fun Routing.exampleSummaryAndDescription() {
route(rootPath) {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary("This is a summary")
description("This is a description")
request {
description("This is a request description")
requestType<TestRequest>()
examples(
"Testerina" to MediaType.Example(
TestRequest(TestNested("asdf"), 1.5, emptyList()),
"summary",
"description"
)
)
}
response {
description("This is a response description")
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
examples(
"Testerino" to MediaType.Example(TestResponse("Heya"), "summary", "description")
) )
} }
} }

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.util package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.ComplexRequest import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.SomethingSimilar
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
import io.bkbn.kompendium.core.fixtures.TestRequest import io.bkbn.kompendium.core.fixtures.TestRequest
import io.bkbn.kompendium.core.fixtures.TestResponse import io.bkbn.kompendium.core.fixtures.TestResponse
@ -349,3 +350,23 @@ fun Routing.postNoReqBody() {
} }
} }
} }
fun Routing.fieldOutsideConstructor() {
route("/field_outside_constructor") {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
requestType<SomethingSimilar>()
description("A cool request")
}
response {
responseType<TestResponse>()
description("Cool response")
responseCode(HttpStatusCode.Created)
}
}
}
}
}

View File

@ -1,11 +1,13 @@
package io.bkbn.kompendium.core.util package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.Barzo import io.bkbn.kompendium.core.fixtures.Barzo
import io.bkbn.kompendium.core.fixtures.ChillaxificationMaximization
import io.bkbn.kompendium.core.fixtures.ComplexRequest import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.Flibbity import io.bkbn.kompendium.core.fixtures.Flibbity
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
import io.bkbn.kompendium.core.fixtures.Foosy import io.bkbn.kompendium.core.fixtures.Foosy
import io.bkbn.kompendium.core.fixtures.Gibbity import io.bkbn.kompendium.core.fixtures.Gibbity
import io.bkbn.kompendium.core.fixtures.Gizmo
import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics
import io.bkbn.kompendium.core.fixtures.Page import io.bkbn.kompendium.core.fixtures.Page
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
@ -20,3 +22,5 @@ fun Routing.genericPolymorphicResponse() = basicGetGenerator<Flibbity<Double>>()
fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator<Flibbity<FlibbityGibbit>>() fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator<Flibbity<FlibbityGibbit>>()
fun Routing.nestedGenericCollection() = basicGetGenerator<Page<Int>>() fun Routing.nestedGenericCollection() = basicGetGenerator<Page<Int>>()
fun Routing.nestedGenericMultipleParamsCollection() = basicGetGenerator<MultiNestedGenerics<String, ComplexRequest>>() fun Routing.nestedGenericMultipleParamsCollection() = basicGetGenerator<MultiNestedGenerics<String, ComplexRequest>>()
fun Routing.overrideSealedTypeIdentifier() = basicGetGenerator<ChillaxificationMaximization>()
fun Routing.subtypeNotCompleteSetOfParentProperties() = basicGetGenerator<Gizmo>()

View File

@ -86,12 +86,19 @@
}, },
"z": { "z": {
"type": "string" "type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.ComplexGibbit"
]
} }
}, },
"required": [ "required": [
"b", "b",
"c", "c",
"z" "z",
"type"
] ]
}, },
"SimpleGibbit": { "SimpleGibbit": {
@ -102,10 +109,17 @@
}, },
"z": { "z": {
"type": "string" "type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.SimpleGibbit"
]
} }
}, },
"required": [ "required": [
"a" "a",
"type"
] ]
}, },
"FlibbityGibbit": { "FlibbityGibbit": {

View File

@ -82,11 +82,18 @@
}, },
"f": { "f": {
"type": "string" "type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Bibbity"
]
} }
}, },
"required": [ "required": [
"b", "b",
"f" "f",
"type"
] ]
}, },
"Gibbity-String": { "Gibbity-String": {
@ -94,10 +101,17 @@
"properties": { "properties": {
"a": { "a": {
"type": "string" "type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Gibbity"
]
} }
}, },
"required": [ "required": [
"a" "a",
"type"
] ]
}, },
"Flibbity-String": { "Flibbity-String": {

View File

@ -65,12 +65,19 @@
}, },
"z": { "z": {
"type": "string" "type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.ComplexGibbit"
]
} }
}, },
"required": [ "required": [
"b", "b",
"c", "c",
"z" "z",
"type"
] ]
}, },
"SimpleGibbit": { "SimpleGibbit": {
@ -81,10 +88,17 @@
}, },
"z": { "z": {
"type": "string" "type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.SimpleGibbit"
]
} }
}, },
"required": [ "required": [
"a" "a",
"type"
] ]
}, },
"FlibbityGibbit": { "FlibbityGibbit": {

View File

@ -65,12 +65,19 @@
}, },
"z": { "z": {
"type": "string" "type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.ComplexGibbit"
]
} }
}, },
"required": [ "required": [
"b", "b",
"c", "c",
"z" "z",
"type"
] ]
}, },
"SimpleGibbit": { "SimpleGibbit": {
@ -81,10 +88,17 @@
}, },
"z": { "z": {
"type": "string" "type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.SimpleGibbit"
]
} }
}, },
"required": [ "required": [
"a" "a",
"type"
] ]
}, },
"List-FlibbityGibbit": { "List-FlibbityGibbit": {

View File

@ -65,12 +65,19 @@
}, },
"z": { "z": {
"type": "string" "type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.ComplexGibbit"
]
} }
}, },
"required": [ "required": [
"b", "b",
"c", "c",
"z" "z",
"type"
] ]
}, },
"SimpleGibbit": { "SimpleGibbit": {
@ -81,10 +88,17 @@
}, },
"z": { "z": {
"type": "string" "type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.SimpleGibbit"
]
} }
}, },
"required": [ "required": [
"a" "a",
"type"
] ]
}, },
"Map-String-FlibbityGibbit": { "Map-String-FlibbityGibbit": {

View File

@ -62,11 +62,18 @@
"f": { "f": {
"type": "number", "type": "number",
"format": "double" "format": "double"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Bibbity"
]
} }
}, },
"required": [ "required": [
"b", "b",
"f" "f",
"type"
] ]
}, },
"Gibbity-Double": { "Gibbity-Double": {
@ -75,10 +82,17 @@
"a": { "a": {
"type": "number", "type": "number",
"format": "double" "format": "double"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Gibbity"
]
} }
}, },
"required": [ "required": [
"a" "a",
"type"
] ]
}, },
"Flibbity-Double": { "Flibbity-Double": {

View File

@ -53,40 +53,6 @@
"webhooks": {}, "webhooks": {},
"components": { "components": {
"schemas": { "schemas": {
"ComplexGibbit": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"c": {
"type": "number",
"format": "int32"
},
"z": {
"type": "string"
}
},
"required": [
"b",
"c",
"z"
]
},
"SimpleGibbit": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"z": {
"type": "string"
}
},
"required": [
"a"
]
},
"Bibbity-FlibbityGibbit": { "Bibbity-FlibbityGibbit": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -102,11 +68,66 @@
"$ref": "#/components/schemas/SimpleGibbit" "$ref": "#/components/schemas/SimpleGibbit"
} }
] ]
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Bibbity"
]
} }
}, },
"required": [ "required": [
"b", "b",
"f" "f",
"type"
]
},
"ComplexGibbit": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"c": {
"type": "number",
"format": "int32"
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.ComplexGibbit"
]
}
},
"required": [
"b",
"c",
"z",
"type"
]
},
"SimpleGibbit": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.SimpleGibbit"
]
}
},
"required": [
"a",
"type"
] ]
}, },
"Gibbity-FlibbityGibbit": { "Gibbity-FlibbityGibbit": {
@ -121,10 +142,17 @@
"$ref": "#/components/schemas/SimpleGibbit" "$ref": "#/components/schemas/SimpleGibbit"
} }
] ]
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Gibbity"
]
} }
}, },
"required": [ "required": [
"a" "a",
"type"
] ]
}, },
"Flibbity-FlibbityGibbit": { "Flibbity-FlibbityGibbit": {

View File

@ -0,0 +1,108 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChillaxificationMaximization"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"Chillax": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"chillax"
]
}
},
"required": [
"a",
"type"
]
},
"ToDaMax": {
"type": "object",
"properties": {
"b": {
"type": "number",
"format": "int32"
},
"type": {
"type": "string",
"enum": [
"maximize"
]
}
},
"required": [
"b",
"type"
]
},
"ChillaxificationMaximization": {
"anyOf": [
{
"$ref": "#/components/schemas/Chillax"
},
{
"$ref": "#/components/schemas/ToDaMax"
}
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,72 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Gizmo"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"Gizmo": {
"type": "object",
"properties": {
"title": {
"type": "string"
}
},
"required": [
"title"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,92 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,94 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/field_outside_constructor": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A cool request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SomethingSimilar"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "Cool response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
},
"SomethingSimilar": {
"type": "object",
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,129 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false,
"security": [
{
"auth-oauth-google": [
"read:pets"
]
}
]
},
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false,
"security": [
{
"auth-oauth-google": [
"write:pets"
]
}
]
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {
"auth-oauth-google": {
"flows": {
"implicit": {
"authorizationUrl": "https://accounts.google.com/o/oauth2/auth",
"scopes": {
"write:pets": "modify pets in your account",
"read:pets": "read your pets"
}
}
},
"type": "oauth2"
}
}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,140 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/": {
"post": {
"tags": [],
"summary": "This is a summary",
"description": "This is a description",
"parameters": [],
"requestBody": {
"description": "This is a request description",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
},
"examples": {
"Testerina": {
"value": {
"fieldName": {
"nesty": "asdf"
},
"b": 1.5,
"aaa": []
},
"summary": "summary",
"description": "description"
}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "This is a response description",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
},
"examples": {
"Testerino": {
"value": {
"c": "Heya"
},
"summary": "summary",
"description": "description"
}
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
},
"TestRequest": {
"type": "object",
"properties": {
"aaa": {
"items": {
"type": "number",
"format": "int64"
},
"type": "array"
},
"b": {
"type": "number",
"format": "double"
},
"fieldName": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"aaa",
"b",
"fieldName"
]
},
"TestNested": {
"type": "object",
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,54 +0,0 @@
package io.bkbn.kompendium.core.fixtures
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
/*
These are test implementation and may well be a good starting point for creating production ones.
Both Gson and Jackson are complex and can achieve this things is more than one way therefore
these will not always work hence why they are in the test package
*/
class GsonSchemaConfigurator: SchemaConfigurator {
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> {
// NOTE: This is test logic Expose is set at a global Gson level so configure to match your Gson set up
val hasAnyExpose = clazz.memberProperties.any { it.hasJavaAnnotation<Expose>() }
return if(hasAnyExpose) {
clazz.memberProperties
.filter { it.hasJavaAnnotation<Expose>() }
} else clazz.memberProperties
}
override fun serializableName(property: KProperty1<out Any, *>): String =
property.getJavaAnnotation<SerializedName>()?.value?: property.name
}
class JacksonSchemaConfigurator: SchemaConfigurator {
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> =
clazz.memberProperties
.filterNot {
it.hasJavaAnnotation<JsonIgnore>()
}
override fun serializableName(property: KProperty1<out Any, *>): String =
property.getJavaAnnotation<JsonProperty>()?.value?: property.name
}
inline fun <reified T: Annotation> KProperty1<*, *>.hasJavaAnnotation(): Boolean {
return javaField?.isAnnotationPresent(T::class.java)?: false
}
inline fun <reified T: Annotation> KProperty1<*, *>.getJavaAnnotation(): T? {
return javaField?.getDeclaredAnnotation(T::class.java)
}

View File

@ -1,7 +0,0 @@
package io.bkbn.kompendium.core.fixtures
enum class SupportedSerializer {
KOTLINX,
GSON,
JACKSON
}

View File

@ -1,7 +1,5 @@
package io.bkbn.kompendium.core.fixtures package io.bkbn.kompendium.core.fixtures
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.fixtures.TestSpecs.defaultSpec import io.bkbn.kompendium.core.fixtures.TestSpecs.defaultSpec
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
@ -16,19 +14,17 @@ import io.kotest.matchers.shouldNot
import io.kotest.matchers.string.beBlank import io.kotest.matchers.string.beBlank
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.serialization.gson.gson
import io.ktor.serialization.jackson.jackson
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.engine.ApplicationEngineEnvironmentBuilder import io.ktor.server.engine.ApplicationEngineEnvironmentBuilder
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.contentnegotiation.ContentNegotiationConfig
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.server.testing.testApplication import io.ktor.server.testing.testApplication
import java.io.File
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File
import kotlin.reflect.KType import kotlin.reflect.KType
object TestHelpers { object TestHelpers {
@ -55,7 +51,7 @@ object TestHelpers {
/** /**
* This will take a provided JSON snapshot file, retrieve it from the resource folder, * This will take a provided JSON snapshot file, retrieve it from the resource folder,
* and build a test ktor server to compare the expected output with the output found in the default * and build a test ktor server to compare the expected output with the output found in the default
* OpenAPI json endpoint. By default, this will run the same test with Gson, Kotlinx, and Jackson serializers * OpenAPI json endpoint.
* @param snapshotName The snapshot file to retrieve from the resources folder * @param snapshotName The snapshot file to retrieve from the resources folder
*/ */
fun openApiTestAllSerializers( fun openApiTestAllSerializers(
@ -64,70 +60,47 @@ object TestHelpers {
applicationSetup: Application.() -> Unit = { }, applicationSetup: Application.() -> Unit = { },
specOverrides: OpenApiSpec.() -> OpenApiSpec = { this }, specOverrides: OpenApiSpec.() -> OpenApiSpec = { this },
applicationEnvironmentBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {}, applicationEnvironmentBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
notarizedApplicationConfigOverrides: NotarizedApplication.Config.() -> Unit = {},
contentNegotiation: ContentNegotiationConfig.() -> Unit = {
json(Json {
encodeDefaults = true
explicitNulls = false
serializersModule = KompendiumSerializersModule.module
})
},
routeUnderTest: Routing.() -> Unit routeUnderTest: Routing.() -> Unit
) { ) {
openApiTest( openApiTest(
snapshotName, snapshotName,
SupportedSerializer.KOTLINX,
routeUnderTest,
applicationSetup,
specOverrides,
customTypes,
applicationEnvironmentBuilder
)
openApiTest(
snapshotName,
SupportedSerializer.JACKSON,
routeUnderTest,
applicationSetup,
specOverrides,
customTypes,
applicationEnvironmentBuilder
)
openApiTest(
snapshotName,
SupportedSerializer.GSON,
routeUnderTest, routeUnderTest,
applicationSetup, applicationSetup,
specOverrides, specOverrides,
customTypes, customTypes,
notarizedApplicationConfigOverrides,
contentNegotiation,
applicationEnvironmentBuilder applicationEnvironmentBuilder
) )
} }
private fun openApiTest( private fun openApiTest(
snapshotName: String, snapshotName: String,
serializer: SupportedSerializer,
routeUnderTest: Routing.() -> Unit, routeUnderTest: Routing.() -> Unit,
applicationSetup: Application.() -> Unit, applicationSetup: Application.() -> Unit,
specOverrides: OpenApiSpec.() -> OpenApiSpec, specOverrides: OpenApiSpec.() -> OpenApiSpec,
typeOverrides: Map<KType, JsonSchema> = emptyMap(), typeOverrides: Map<KType, JsonSchema> = emptyMap(),
applicationBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {} notarizedApplicationConfigOverrides: NotarizedApplication.Config.() -> Unit,
contentNegotiation: ContentNegotiationConfig.() -> Unit,
applicationBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit
) = testApplication { ) = testApplication {
environment(applicationBuilder) environment(applicationBuilder)
install(NotarizedApplication()) { install(NotarizedApplication()) {
customTypes = typeOverrides customTypes = typeOverrides
spec = defaultSpec().specOverrides() spec = { specOverrides(defaultSpec()) }
schemaConfigurator = when (serializer) { schemaConfigurator = KotlinXSchemaConfigurator()
SupportedSerializer.KOTLINX -> KotlinXSchemaConfigurator() notarizedApplicationConfigOverrides()
SupportedSerializer.GSON -> GsonSchemaConfigurator()
SupportedSerializer.JACKSON -> JacksonSchemaConfigurator()
}
} }
install(ContentNegotiation) { install(ContentNegotiation) {
when (serializer) { contentNegotiation()
SupportedSerializer.KOTLINX -> json(Json {
encodeDefaults = true
explicitNulls = false
serializersModule = KompendiumSerializersModule.module
})
SupportedSerializer.GSON -> gson()
SupportedSerializer.JACKSON -> jackson(ContentType.Application.Json) {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
} }
application(applicationSetup) application(applicationSetup)
routing { routing {

View File

@ -1,12 +1,10 @@
package io.bkbn.kompendium.core.fixtures package io.bkbn.kompendium.core.fixtures
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant import java.time.Instant
@Serializable @Serializable
@ -91,6 +89,25 @@ data class AnothaJamma(val b: Float) : SlammaJamma
data class InsaneJamma(val c: SlammaJamma) : SlammaJamma data class InsaneJamma(val c: SlammaJamma) : SlammaJamma
sealed interface ChillaxificationMaximization
@Serializable
@SerialName("chillax")
data class Chillax(val a: String) : ChillaxificationMaximization
@Serializable
@SerialName("maximize")
data class ToDaMax(val b: Int) : ChillaxificationMaximization
sealed class Gadget(
open val title: String,
open val description: String
)
class Gizmo(
override val title: String,
) : Gadget(title, "Just a gizmo")
sealed interface Flibbity<T> sealed interface Flibbity<T>
data class Gibbity<T>(val a: T) : Flibbity<T> data class Gibbity<T>(val a: T) : Flibbity<T>
@ -158,9 +175,7 @@ object Nested {
@Serializable @Serializable
data class TransientObject( data class TransientObject(
@field:Expose
val nonTransient: String, val nonTransient: String,
@field:JsonIgnore
@Transient @Transient
val transient: String = "transient" val transient: String = "transient"
) )
@ -174,8 +189,6 @@ data class UnbackedObject(
@Serializable @Serializable
data class SerialNameObject( data class SerialNameObject(
@field:JsonProperty("snake_case_name")
@field:SerializedName("snake_case_name")
@SerialName("snake_case_name") @SerialName("snake_case_name")
val camelCaseName: String val camelCaseName: String
) )
@ -194,3 +207,8 @@ enum class Color {
data class ObjectWithEnum( data class ObjectWithEnum(
val color: Color val color: Color
) )
@Serializable
data class SomethingSimilar(val a: String) {
val b = "something else"
}

View File

@ -1,5 +1,5 @@
# Kompendium # Kompendium
project.version=3.14.4 project.version=4.0.0-alpha
# Kotlin # Kotlin
kotlin.code.style=official kotlin.code.style=official
# Gradle # Gradle

View File

@ -1,20 +1,51 @@
package io.bkbn.kompendium.json.schema package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KProperty1 import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
class KotlinXSchemaConfigurator : SchemaConfigurator { class KotlinXSchemaConfigurator : SchemaConfigurator {
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> = override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> {
clazz.memberProperties return clazz.memberProperties
.filterNot { it.hasAnnotation<Transient>() } .filterNot { it.hasAnnotation<Transient>() }
.filter { clazz.primaryConstructor?.parameters?.map { it.name }?.contains(it.name) ?: true }
}
override fun serializableName(property: KProperty1<out Any, *>): String = override fun serializableName(property: KProperty1<out Any, *>): String =
property.annotations property.annotations
.filterIsInstance<SerialName>() .filterIsInstance<SerialName>()
.firstOrNull()?.value ?: property.name .firstOrNull()?.value ?: property.name
override fun sealedTypeEnrichment(
implementationType: KType,
implementationSchema: JsonSchema,
): JsonSchema {
return if (implementationSchema is TypeDefinition && implementationSchema.type == "object") {
implementationSchema.copy(
required = implementationSchema.required?.plus("type"),
properties = implementationSchema.properties?.plus(
mapOf(
"type" to EnumDefinition("string", enum = setOf(determineTypeQualifier(implementationType)))
)
)
)
} else {
implementationSchema
}
}
private fun determineTypeQualifier(type: KType): String {
val nameOverrideAnnotation = (type.classifier as KClass<*>).findAnnotation<SerialName>()
return nameOverrideAnnotation?.value ?: (type.classifier as KClass<*>).qualifiedName!!
}
} }

View File

@ -1,17 +1,16 @@
package io.bkbn.kompendium.json.schema package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KProperty1 import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties import kotlin.reflect.KType
interface SchemaConfigurator { interface SchemaConfigurator {
fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>>
fun serializableName(property: KProperty1<out Any, *>): String fun serializableName(property: KProperty1<out Any, *>): String
open class Default : SchemaConfigurator { fun sealedTypeEnrichment(
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> = implementationType: KType,
clazz.memberProperties implementationSchema: JsonSchema
): JsonSchema
override fun serializableName(property: KProperty1<out Any, *>): String = property.name
}
} }

View File

@ -25,7 +25,10 @@ object SealedObjectHandler {
val subclasses = clazz.sealedSubclasses val subclasses = clazz.sealedSubclasses
.map { it.createType(type.arguments) } .map { it.createType(type.arguments) }
.map { t -> .map { t ->
SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator, enrichment).let { js -> SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator, enrichment)
.let {
schemaConfigurator.sealedTypeEnrichment(t, it)
}.let { js ->
if (js is TypeDefinition && js.type == "object") { if (js is TypeDefinition && js.type == "object") {
val slug = t.getSlug(enrichment) val slug = t.getSlug(enrichment)
cache[slug] = js cache[slug] = js

View File

@ -13,7 +13,7 @@ object Helpers {
fun KType.getSlug(enrichment: Enrichment? = null) = when (enrichment) { fun KType.getSlug(enrichment: Enrichment? = null) = when (enrichment) {
is TypeEnrichment<*> -> getEnrichedSlug(enrichment) is TypeEnrichment<*> -> getEnrichedSlug(enrichment)
is PropertyEnrichment -> error("Slugs should not be generated for field enrichments") is PropertyEnrichment -> error("Slugs should not be generated for field enrichments")
null -> getSimpleSlug() else -> getSimpleSlug()
} }
fun KType.getSimpleSlug(): String = when { fun KType.getSimpleSlug(): String = when {
@ -26,7 +26,7 @@ object Helpers {
fun KType.getReferenceSlug(enrichment: Enrichment? = null): String = when (enrichment) { fun KType.getReferenceSlug(enrichment: Enrichment? = null): String = when (enrichment) {
is TypeEnrichment<*> -> getSimpleReferenceSlug() + "-${enrichment.id}" is TypeEnrichment<*> -> getSimpleReferenceSlug() + "-${enrichment.id}"
is PropertyEnrichment -> error("Reference slugs should never be generated for field enrichments") is PropertyEnrichment -> error("Reference slugs should never be generated for field enrichments")
null -> getSimpleReferenceSlug() else -> getSimpleReferenceSlug()
} }
private fun KType.getSimpleReferenceSlug() = when { private fun KType.getSimpleReferenceSlug() = when {

View File

@ -20,5 +20,5 @@ data class MediaType(
val encoding: Map<String, Encoding>? = null, val encoding: Map<String, Encoding>? = null,
) { ) {
@Serializable @Serializable
data class Example(@Contextual val value: Any) data class Example(@Contextual val value: Any, val summary: String? = null, val description: String? = null)
} }

View File

@ -1,7 +1,6 @@
package io.bkbn.kompendium.oas.payload package io.bkbn.kompendium.oas.payload
import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.JsonSchema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/** /**
@ -26,12 +25,9 @@ data class Parameter(
val required: Boolean = true, val required: Boolean = true,
val deprecated: Boolean = false, val deprecated: Boolean = false,
val allowEmptyValue: Boolean? = null, val allowEmptyValue: Boolean? = null,
val examples: Map<String, Example>? = null, val examples: Map<String, MediaType.Example>? = null,
// todo support styling https://spec.openapis.org/oas/v3.1.0#style-values // todo support styling https://spec.openapis.org/oas/v3.1.0#style-values
) { ) {
@Serializable
data class Example(@Contextual val value: Any)
@Suppress("EnumNaming") @Suppress("EnumNaming")
@Serializable @Serializable
enum class Location { enum class Location {

View File

@ -60,7 +60,8 @@ private fun Application.mainModule() {
} }
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec.copy( spec = {
baseSpec.copy(
components = Components( components = Components(
securitySchemes = mutableMapOf( securitySchemes = mutableMapOf(
"basic" to BasicAuth() "basic" to BasicAuth()
@ -68,6 +69,7 @@ private fun Application.mainModule() {
) )
) )
} }
}
routing { routing {
swagger(pageTitle = "Simple API Docs") swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")

View File

@ -44,9 +44,7 @@ private fun Application.mainModule() {
}) })
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec spec = { baseSpec }
// Adds support for @Transient and @SerialName
// If you are not using them this is not required.
schemaConfigurator = KotlinXSchemaConfigurator() schemaConfigurator = KotlinXSchemaConfigurator()
} }
routing { routing {

View File

@ -43,9 +43,7 @@ private fun Application.mainModule() {
}) })
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec spec = { baseSpec }
// Adds support for @Transient and @SerialName
// If you are not using them this is not required.
schemaConfigurator = KotlinXSchemaConfigurator() schemaConfigurator = KotlinXSchemaConfigurator()
} }
routing { routing {

View File

@ -1,19 +1,19 @@
package io.bkbn.kompendium.playground package io.bkbn.kompendium.playground
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.component.Components
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.bkbn.kompendium.oas.security.BasicAuth
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.playground.util.ExampleResponse import io.bkbn.kompendium.playground.util.ExampleResponse
import io.bkbn.kompendium.playground.util.Util.baseSpec import io.bkbn.kompendium.playground.util.Util.baseSpec
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.serialization.gson.gson import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.call import io.ktor.server.application.call
import io.ktor.server.application.install import io.ktor.server.application.install
@ -21,14 +21,13 @@ import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respond import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route import io.ktor.server.routing.Route
import io.ktor.server.routing.get import io.ktor.server.routing.get
import io.ktor.server.routing.route import io.ktor.server.routing.route
import io.ktor.server.routing.routing import io.ktor.server.routing.routing
import kotlin.reflect.KClass import kotlinx.serialization.encodeToString
import kotlin.reflect.KProperty1 import kotlinx.serialization.json.Json
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
fun main() { fun main() {
embeddedServer( embeddedServer(
@ -38,20 +37,44 @@ fun main() {
).start(wait = true) ).start(wait = true)
} }
private val CustomJsonEncoder = Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
}
private fun Application.mainModule() { private fun Application.mainModule() {
install(ContentNegotiation) { install(ContentNegotiation) {
gson { json(Json {
setPrettyPrinting() serializersModule = KompendiumSerializersModule.module
} encodeDefaults = true
explicitNulls = true
})
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec spec = {
schemaConfigurator = GsonSchemaConfigurator() baseSpec.copy(
components = Components(
securitySchemes = mutableMapOf(
"basic" to BasicAuth()
)
)
)
}
specRoute = { spec, routing ->
routing {
route("/openapi.json") {
get {
call.response.headers.append("Content-Type", "application/json")
call.respondText { CustomJsonEncoder.encodeToString(spec) }
}
}
}
}
} }
routing { routing {
swagger(pageTitle = "Simple API Docs") swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
locationDocumentation() locationDocumentation()
get { get {
@ -73,6 +96,9 @@ private fun Route.locationDocumentation() {
get = GetInfo.builder { get = GetInfo.builder {
summary("Get user by id") summary("Get user by id")
description("A very neat endpoint!") description("A very neat endpoint!")
security = mapOf(
"basic" to emptyList()
)
response { response {
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>() responseType<ExampleResponse>()
@ -81,27 +107,3 @@ private fun Route.locationDocumentation() {
} }
} }
} }
// Adds support for Expose and SerializedName annotations,
// if you are not using them this is not required
class GsonSchemaConfigurator(
private val excludeFieldsWithoutExposeAnnotation: Boolean = false
): SchemaConfigurator.Default() {
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> {
return if(excludeFieldsWithoutExposeAnnotation) clazz.memberProperties
.filter { it.hasJavaAnnotation<Expose>() }
else clazz.memberProperties
}
override fun serializableName(property: KProperty1<out Any, *>): String =
property.getJavaAnnotation<SerializedName>()?.value?: property.name
}
private inline fun <reified T: Annotation> KProperty1<*, *>.hasJavaAnnotation(): Boolean {
return javaField?.isAnnotationPresent(T::class.java)?: false
}
private inline fun <reified T: Annotation> KProperty1<*, *>.getJavaAnnotation(): T? {
return javaField?.getDeclaredAnnotation(T::class.java)
}

View File

@ -46,7 +46,7 @@ private fun Application.mainModule() {
}) })
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec spec = { baseSpec }
customTypes = mapOf( customTypes = mapOf(
typeOf<Instant>() to TypeDefinition(type = "string", format = "date-time") typeOf<Instant>() to TypeDefinition(type = "string", format = "date-time")
) )

View File

@ -45,7 +45,7 @@ private fun Application.mainModule() {
}) })
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec spec = { baseSpec }
// Adds support for @Transient and @SerialName // Adds support for @Transient and @SerialName
// If you are not using them this is not required. // If you are not using them this is not required.
schemaConfigurator = KotlinXSchemaConfigurator() schemaConfigurator = KotlinXSchemaConfigurator()

View File

@ -1,21 +1,20 @@
package io.bkbn.kompendium.playground package io.bkbn.kompendium.playground
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.MediaType
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.playground.util.ExampleResponse import io.bkbn.kompendium.playground.util.ExampleResponse
import io.bkbn.kompendium.playground.util.ExceptionResponse
import io.bkbn.kompendium.playground.util.Util.baseSpec import io.bkbn.kompendium.playground.util.Util.baseSpec
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.serialization.jackson.jackson import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.call import io.ktor.server.application.call
import io.ktor.server.application.install import io.ktor.server.application.install
@ -27,10 +26,7 @@ import io.ktor.server.routing.Route
import io.ktor.server.routing.get import io.ktor.server.routing.get
import io.ktor.server.routing.route import io.ktor.server.routing.route
import io.ktor.server.routing.routing import io.ktor.server.routing.routing
import kotlin.reflect.KClass import kotlinx.serialization.json.Json
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
fun main() { fun main() {
embeddedServer( embeddedServer(
@ -42,29 +38,35 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
install(ContentNegotiation) { install(ContentNegotiation) {
jackson { json(Json {
enable(SerializationFeature.INDENT_OUTPUT) serializersModule = KompendiumSerializersModule.module
setSerializationInclusion(JsonInclude.Include.NON_NULL) encodeDefaults = true
} explicitNulls = false
})
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec spec = { baseSpec }
schemaConfigurator = JacksonSchemaConfigurator() schemaConfigurator = KotlinXSchemaConfigurator()
} }
routing { routing {
swagger(pageTitle = "Simple API Docs") swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
locationDocumentation() idDocumentation()
get {
call.respond(HttpStatusCode.OK, ExampleResponse(true))
}
route("/profile") {
profileDocumentation()
get { get {
call.respond(HttpStatusCode.OK, ExampleResponse(true)) call.respond(HttpStatusCode.OK, ExampleResponse(true))
} }
} }
} }
}
} }
private fun Route.locationDocumentation() { private fun Route.idDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(
@ -80,31 +82,42 @@ private fun Route.locationDocumentation() {
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>() responseType<ExampleResponse>()
description("Will return whether or not the user is real 😱") description("Will return whether or not the user is real 😱")
examples(
"example1" to MediaType.Example(ExampleResponse(true), "ahaha", "bhbh"),
)
}
canRespond {
responseType<ExceptionResponse>()
responseCode(HttpStatusCode.NotFound)
description("Indicates that a user with this id does not exist")
} }
} }
} }
} }
// Adds support for JsonIgnore and JsonProperty annotations, private fun Route.profileDocumentation() {
// if you are not using them this is not required install(NotarizedRoute()) {
// This also does not support class level configuration parameters = listOf(
private class JacksonSchemaConfigurator: SchemaConfigurator.Default() { Parameter(
name = "id",
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> = `in` = Parameter.Location.path,
clazz.memberProperties schema = TypeDefinition.STRING
.filterNot { )
it.hasJavaAnnotation<JsonIgnore>() )
get = GetInfo.builder {
summary("Get a users profile")
description("A cool endpoint!")
response {
responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>()
description("Returns user profile information")
}
canRespond {
responseType<ExceptionResponse>()
responseCode(HttpStatusCode.NotFound)
description("Indicates that a user with this id does not exist")
}
}
} }
override fun serializableName(property: KProperty1<out Any, *>): String =
property.getJavaAnnotation<JsonProperty>()?.value?: property.name
}
private inline fun <reified T: Annotation> KProperty1<*, *>.hasJavaAnnotation(): Boolean {
return javaField?.isAnnotationPresent(T::class.java)?: false
}
private inline fun <reified T: Annotation> KProperty1<*, *>.getJavaAnnotation(): T? {
return javaField?.getDeclaredAnnotation(T::class.java)
} }

View File

@ -43,7 +43,7 @@ private fun Application.mainModule() {
}) })
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec spec = { baseSpec }
} }
install(StatusPages) { install(StatusPages) {
exception<Throwable> { call, _ -> exception<Throwable> { call, _ ->

View File

@ -62,18 +62,22 @@ private fun Application.mainModule() {
} }
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec.copy( spec = {
baseSpec.copy(
components = Components( components = Components(
securitySchemes = mutableMapOf( securitySchemes = mutableMapOf(
"basic" to BasicAuth() "basic" to BasicAuth()
) )
) )
) )
openApiJson = { }
specRoute = { spec, routing ->
routing {
authenticate("basic") { authenticate("basic") {
route("/openapi.json") { route("/openapi.json") {
get { get {
call.respond(HttpStatusCode.OK, this@route.application.attributes[KompendiumAttributes.openApiSpec]) call.respond(HttpStatusCode.OK, spec)
}
} }
} }
} }

View File

@ -43,7 +43,7 @@ private fun Application.mainModule() {
}) })
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec spec = { baseSpec }
} }
install(NotarizedLocations()) { install(NotarizedLocations()) {
locations = mapOf( locations = mapOf(

View File

@ -44,7 +44,7 @@ private fun Application.mainModule() {
}) })
} }
install(NotarizedApplication()) { install(NotarizedApplication()) {
spec = baseSpec spec = { baseSpec }
} }
install(NotarizedResources()) { install(NotarizedResources()) {
resources = mapOf( resources = mapOf(