diff --git a/CHANGELOG.md b/CHANGELOG.md index dc0bf2272..45ab7ecd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,26 @@ ## 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 ### Changed diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 233880c0f..a209e3746 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -48,8 +48,6 @@ dependencies { testFixturesApi("io.ktor:ktor-server-core:$ktorVersion") testFixturesApi("io.ktor:ktor-server-test-host:$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-server-content-negotiation:$ktorVersion") testFixturesApi("io.ktor:ktor-server-auth:$ktorVersion") diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/DeleteInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/DeleteInfo.kt index b1a2ba345..60f8c33bb 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/DeleteInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/DeleteInfo.kt @@ -10,6 +10,7 @@ class DeleteInfo private constructor( override val summary: String, override val description: String, override val externalDocumentation: ExternalDocumentation?, + override val security: Map>?, override val operationId: String?, override val deprecated: Boolean, override val parameters: List @@ -33,7 +34,8 @@ class DeleteInfo private constructor( externalDocumentation = externalDocumentation, operationId = operationId, deprecated = deprecated, - parameters = parameters + parameters = parameters, + security = security ) } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/GetInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/GetInfo.kt index 9f47184dd..d06ad577c 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/GetInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/GetInfo.kt @@ -12,7 +12,8 @@ class GetInfo private constructor( override val externalDocumentation: ExternalDocumentation?, override val operationId: String?, override val deprecated: Boolean, - override val parameters: List + override val parameters: List, + override val security: Map>? ) : MethodInfo { companion object { @@ -33,7 +34,8 @@ class GetInfo private constructor( externalDocumentation = externalDocumentation, operationId = operationId, deprecated = deprecated, - parameters = parameters + parameters = parameters, + security = security ) } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/HeadInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/HeadInfo.kt index f1d7bed54..671d60150 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/HeadInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/HeadInfo.kt @@ -12,7 +12,8 @@ class HeadInfo private constructor( override val externalDocumentation: ExternalDocumentation?, override val operationId: String?, override val deprecated: Boolean, - override val parameters: List + override val parameters: List, + override val security: Map>?, ) : MethodInfo { companion object { @@ -33,7 +34,8 @@ class HeadInfo private constructor( externalDocumentation = externalDocumentation, operationId = operationId, deprecated = deprecated, - parameters = parameters + parameters = parameters, + security = security, ) } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/MethodInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/MethodInfo.kt index ca2706ac2..7b98887b7 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/MethodInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/MethodInfo.kt @@ -9,6 +9,9 @@ sealed interface MethodInfo { val tags: Set val summary: String val description: String + + val security: Map>? + get() = null val externalDocumentation: ExternalDocumentation? get() = null val operationId: String? @@ -28,6 +31,7 @@ sealed interface MethodInfo { internal var tags: Set = emptySet() internal var parameters: List = emptyList() internal var errors: MutableList = mutableListOf() + internal var security: Map>? = null fun response(init: ResponseInfo.Builder.() -> Unit) = apply { val builder = ResponseInfo.Builder() @@ -59,6 +63,8 @@ sealed interface MethodInfo { fun parameters(vararg parameters: Parameter) = apply { this.parameters = parameters.toList() } + fun security(security: Map>) = apply { this.security = security } + abstract fun build(): T } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/OptionsInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/OptionsInfo.kt index f75268cec..c4a17dec4 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/OptionsInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/OptionsInfo.kt @@ -12,7 +12,8 @@ class OptionsInfo private constructor( override val externalDocumentation: ExternalDocumentation?, override val operationId: String?, override val deprecated: Boolean, - override val parameters: List + override val parameters: List, + override val security: Map>?, ) : MethodInfo { companion object { @@ -33,7 +34,8 @@ class OptionsInfo private constructor( externalDocumentation = externalDocumentation, operationId = operationId, deprecated = deprecated, - parameters = parameters + parameters = parameters, + security = security, ) } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PatchInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PatchInfo.kt index 9cefdb424..de2195e91 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PatchInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PatchInfo.kt @@ -13,7 +13,8 @@ class PatchInfo private constructor( override val externalDocumentation: ExternalDocumentation?, override val operationId: String?, override val deprecated: Boolean, - override val parameters: List + override val parameters: List, + override val security: Map>?, ) : MethodInfoWithRequest { companion object { @@ -35,7 +36,8 @@ class PatchInfo private constructor( externalDocumentation = externalDocumentation, operationId = operationId, deprecated = deprecated, - parameters = parameters + parameters = parameters, + security = security, ) } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PostInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PostInfo.kt index 2020fd176..5b5822a05 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PostInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PostInfo.kt @@ -13,7 +13,8 @@ class PostInfo private constructor( override val externalDocumentation: ExternalDocumentation?, override val operationId: String?, override val deprecated: Boolean, - override val parameters: List + override val parameters: List, + override val security: Map>?, ) : MethodInfoWithRequest { companion object { @@ -35,7 +36,8 @@ class PostInfo private constructor( externalDocumentation = externalDocumentation, operationId = operationId, deprecated = deprecated, - parameters = parameters + parameters = parameters, + security = security, ) } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PutInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PutInfo.kt index f4c508b71..ae795cfce 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PutInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/PutInfo.kt @@ -13,7 +13,8 @@ class PutInfo private constructor( override val externalDocumentation: ExternalDocumentation?, override val operationId: String?, override val deprecated: Boolean, - override val parameters: List + override val parameters: List, + override val security: Map>?, ) : MethodInfoWithRequest { companion object { @@ -35,7 +36,8 @@ class PutInfo private constructor( externalDocumentation = externalDocumentation, operationId = operationId, deprecated = deprecated, - parameters = parameters + parameters = parameters, + security = security, ) } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/RequestInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/RequestInfo.kt index 07233a8fd..d5f81f8f2 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/RequestInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/RequestInfo.kt @@ -49,8 +49,8 @@ class RequestInfo private constructor( fun description(s: String) = apply { this.description = s } - fun examples(vararg e: Pair) = apply { - this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) } + fun examples(vararg e: Pair) = apply { + this.examples = e.toMap() } fun mediaTypes(vararg m: String) = apply { diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/ResponseInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/ResponseInfo.kt index f4a970e43..a0b78f59e 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/ResponseInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/ResponseInfo.kt @@ -57,8 +57,8 @@ class ResponseInfo private constructor( fun description(s: String) = apply { this.description = s } - fun examples(vararg e: Pair) = apply { - this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) } + fun examples(vararg e: Pair) = apply { + this.examples = e.toMap() } fun mediaTypes(vararg m: String) = apply { diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedApplication.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedApplication.kt index 472587562..2b5275352 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedApplication.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedApplication.kt @@ -1,16 +1,15 @@ package io.bkbn.kompendium.core.plugin 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.definition.JsonSchema import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug import io.bkbn.kompendium.oas.OpenApiSpec -import io.ktor.http.HttpStatusCode import io.ktor.server.application.call import io.ktor.server.application.createApplicationPlugin import io.ktor.server.response.respond import io.ktor.server.routing.Routing -import io.ktor.server.routing.application import io.ktor.server.routing.get import io.ktor.server.routing.route import io.ktor.server.routing.routing @@ -19,25 +18,26 @@ import kotlin.reflect.KType object NotarizedApplication { class Config { - lateinit var spec: OpenApiSpec - var openApiJson: Routing.() -> Unit = { - route("/openapi.json") { + lateinit var spec: () -> OpenApiSpec + var specRoute: (OpenApiSpec, Routing) -> Unit = { spec, routing -> + routing.route("/openapi.json") { get { - call.respond(HttpStatusCode.OK, this@route.application.attributes[KompendiumAttributes.openApiSpec]) + call.respond(spec) } } } var customTypes: Map = emptyMap() - var schemaConfigurator: SchemaConfigurator = SchemaConfigurator.Default() + var schemaConfigurator: SchemaConfigurator = KotlinXSchemaConfigurator() } operator fun invoke() = createApplicationPlugin( name = "NotarizedApplication", createConfiguration = ::Config ) { - val spec = pluginConfig.spec - val routing = application.routing { } - pluginConfig.openApiJson(routing) + val spec = pluginConfig.spec() + val routing = application.routing {} + this@createApplicationPlugin.pluginConfig.specRoute(spec, routing) + // pluginConfig.openApiJson(routing) pluginConfig.customTypes.forEach { (type, schema) -> spec.components.schemas[type.getSimpleSlug()] = schema } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt index e9bb4a09d..721c67c02 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt @@ -118,10 +118,7 @@ object Helpers { operationId = this.operationId, deprecated = this.deprecated, parameters = this.parameters, - security = config.security - ?.map { (k, v) -> k to v } - ?.map { listOf(it).toMap() } - ?.toMutableList(), + security = this.createCombinedSecurityContext(config), requestBody = when (this) { is MethodInfoWithRequest -> this.request?.let { reqInfo -> Request( @@ -150,6 +147,25 @@ object Helpers { ).plus(this.errors.toResponseMap()) ) + private fun MethodInfo.createCombinedSecurityContext(config: SpecConfig): MutableList>>? { + 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.toResponseMap(): Map = associate { error -> error.responseCode.value to Response( description = error.description, diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt index d37abd22d..9f8d48cad 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt @@ -6,17 +6,20 @@ import io.bkbn.kompendium.core.util.arrayConstraints import io.bkbn.kompendium.core.util.complexRequest import io.bkbn.kompendium.core.util.customAuthConfig 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.defaultAuthConfig import io.bkbn.kompendium.core.util.defaultField import io.bkbn.kompendium.core.util.defaultParameter 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.enrichedGenericResponse import io.bkbn.kompendium.core.util.enrichedNestedCollection import io.bkbn.kompendium.core.util.enrichedSimpleRequest import io.bkbn.kompendium.core.util.enrichedSimpleResponse 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.genericPolymorphicResponse 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.nullableNestedObject 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.overrideSealedTypeIdentifier +import io.bkbn.kompendium.core.util.paramWrapper import io.bkbn.kompendium.core.util.polymorphicCollectionResponse import io.bkbn.kompendium.core.util.polymorphicException 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.primitives 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.responseHeaders 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.stringContentEncodingConstraints 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.trailingSlash -import io.bkbn.kompendium.core.util.paramWrapper import io.bkbn.kompendium.core.util.unbackedFieldsResponse import io.bkbn.kompendium.core.util.withOperationId 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.BearerAuth import io.bkbn.kompendium.oas.security.OAuth +import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.should @@ -85,6 +91,8 @@ import io.kotest.matchers.string.startWith import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO 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.auth.Authentication 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.jwt.jwt 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.time.Instant import kotlin.reflect.typeOf @@ -182,6 +195,9 @@ class KompendiumTest : DescribeSpec({ it("Can generate example optional request body") { openApiTestAllSerializers("T0069__example_optional_req.json") { optionalReqExample() } } + it("Can generate example summary and description") { + openApiTestAllSerializers("T0075__example_summary_and_description.json") { exampleSummaryAndDescription() } + } } describe("Defaults") { it("Can generate a default parameter value") { @@ -235,97 +251,62 @@ class KompendiumTest : DescribeSpec({ it("Can handle a really gnarly generic example") { openApiTestAllSerializers("T0043__gnarly_generic_example.json") { gnarlyGenericResponse() } } - } - describe("Custom Serializable Reader tests") { - it("Can support ignoring fields") { - openApiTestAllSerializers("T0048__ignored_property.json") { ignoredFieldsResponse() } + it("Can override the type name for a sealed interface implementation") { + openApiTestAllSerializers("T0070__sealed_interface_type_name_override.json") { overrideSealedTypeIdentifier() } } - it("Can support un-backed fields") { - openApiTestAllSerializers("T0049__unbacked_property.json") { unbackedFieldsResponse() } + 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() + } } - it("Can support custom named fields") { - openApiTestAllSerializers("T0050__custom_named_property.json") { customFieldNameResponse() } + describe("Custom Serializable Reader tests") { + it("Can support ignoring fields") { + openApiTestAllSerializers("T0048__ignored_property.json") { ignoredFieldsResponse() } + } + it("Can support un-backed fields") { + openApiTestAllSerializers("T0049__unbacked_property.json") { unbackedFieldsResponse() } + } + it("Can support custom named fields") { + openApiTestAllSerializers("T0050__custom_named_property.json") { customFieldNameResponse() } + } } - } - describe("Miscellaneous") { - xit("Can generate the necessary ReDoc home page") { - // TODO apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() } - } - it("Can add an operation id to a notarized route") { - openApiTestAllSerializers("T0034__notarized_get_with_operation_id.json") { withOperationId() } - } - xit("Can add an undeclared field") { - // TODO openApiTestAllSerializers("undeclared_field.json") { undeclaredType() } - } - it("Can add a custom header parameter with a name override") { - openApiTestAllSerializers("T0035__override_parameter_name.json") { headerParameter() } - } - xit("Can override field name") { - // TODO Assess strategies here - } - it("Can serialize a recursive type") { - openApiTestAllSerializers("T0042__simple_recursive.json") { simpleRecursive() } - } - it("Nullable fields do not lead to doom") { - openApiTestAllSerializers("T0036__nullable_fields.json") { nullableNestedObject() } - } - it("Can have a nullable enum as a member field") { - openApiTestAllSerializers("T0037__nullable_enum_field.json") { nullableEnumField() } - } - it("Can have a nullable reference without impacting base type") { - openApiTestAllSerializers("T0041__nullable_reference.json") { nullableReference() } - } - it("Can handle nested type names") { - openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() } - } - it("Can handle top level nullable types") { - openApiTestAllSerializers("T0051__top_level_nullable.json") { topLevelNullable() } - } - it("Can handle multiple registrations for different methods with the same path and different auth") { - openApiTestAllSerializers( - "T0053__same_path_different_methods_and_auth.json", - applicationSetup = { - install(Authentication) { - basic("basic") { - realm = "Ktor Server" - validate { UserIdPrincipal("Placeholder") } - } - } - }, - specOverrides = { - this.copy( - components = Components( - securitySchemes = mutableMapOf( - "basic" to BasicAuth() - ) - ) - ) - } - ) { samePathDifferentMethodsAndAuth() } - } - it("Can generate paths without application root-path") { - openApiTestAllSerializers( - "T0054__app_with_rootpath.json", - applicationEnvironmentBuilder = { - rootPath = "/example" - }, - specOverrides = { - copy( - servers = servers.map { it.copy(url = URI("${it.url}/example")) }.toMutableList() - ) - } - ) { notarizedGet() } - } - } - describe("Error Handling") { - it("Throws a clear exception when an unidentified type is encountered") { - val exception = shouldThrow { openApiTestAllSerializers("") { dateTimeString() } } - exception.message should startWith("An unknown type was encountered: class java.time.Instant") - } - it("Throws an exception when same method for same path has been previously registered") { - val exception = shouldThrow { + describe("Miscellaneous") { + xit("Can generate the necessary ReDoc home page") { + // TODO apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() } + } + it("Can add an operation id to a notarized route") { + openApiTestAllSerializers("T0034__notarized_get_with_operation_id.json") { withOperationId() } + } + xit("Can add an undeclared field") { + // TODO openApiTestAllSerializers("undeclared_field.json") { undeclaredType() } + } + it("Can add a custom header parameter with a name override") { + openApiTestAllSerializers("T0035__override_parameter_name.json") { headerParameter() } + } + xit("Can override field name") { + // TODO Assess strategies here + } + it("Can serialize a recursive type") { + openApiTestAllSerializers("T0042__simple_recursive.json") { simpleRecursive() } + } + it("Nullable fields do not lead to doom") { + openApiTestAllSerializers("T0036__nullable_fields.json") { nullableNestedObject() } + } + it("Can have a nullable enum as a member field") { + openApiTestAllSerializers("T0037__nullable_enum_field.json") { nullableEnumField() } + } + it("Can have a nullable reference without impacting base type") { + openApiTestAllSerializers("T0041__nullable_reference.json") { nullableReference() } + } + it("Can handle nested type names") { + openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() } + } + it("Can handle top level nullable types") { + openApiTestAllSerializers("T0051__top_level_nullable.json") { topLevelNullable() } + } + it("Can handle multiple registrations for different methods with the same path and different auth") { openApiTestAllSerializers( - snapshotName = "", + "T0053__same_path_different_methods_and_auth.json", applicationSetup = { install(Authentication) { basic("basic") { @@ -334,157 +315,277 @@ class KompendiumTest : DescribeSpec({ } } }, - ) { - samePathSameMethod() - } - } - exception.message should startWith("A route has already been registered for path: /test/{a} and method: GET") - } - } - describe("Formats") { - it("Can set a format for a simple type schema") { - openApiTestAllSerializers( - snapshotName = "T0038__formatted_date_time_string.json", - customTypes = mapOf(typeOf() to TypeDefinition(type = "string", format = "date")) - ) { dateTimeString() } - } - } - describe("Free Form") { - // todo Assess strategies here - } - describe("Authentication") { - it("Can add a default auth config by default") { - openApiTestAllSerializers( - snapshotName = "T0045__default_auth_config.json", - applicationSetup = { - install(Authentication) { - basic("basic") { - realm = "Ktor Server" - validate { UserIdPrincipal("Placeholder") } - } - } - }, - specOverrides = { - this.copy( - components = Components( - securitySchemes = mutableMapOf( - "basic" to BasicAuth() + specOverrides = { + this.copy( + components = Components( + securitySchemes = mutableMapOf( + "basic" to BasicAuth() + ) ) ) - ) - } - ) { defaultAuthConfig() } - } - it("Can provide custom auth config with proper scopes") { - openApiTestAllSerializers( - snapshotName = "T0046__custom_auth_config.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" + ) { samePathDifferentMethodsAndAuth() } + } + it("Can generate paths without application root-path") { + openApiTestAllSerializers( + "T0054__app_with_rootpath.json", + applicationEnvironmentBuilder = { + rootPath = "/example" + }, + specOverrides = { + copy( + servers = servers.map { it.copy(url = URI("${it.url}/example")) }.toMutableList() + ) + } + ) { 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") { + it("Throws a clear exception when an unidentified type is encountered") { + val exception = shouldThrow { openApiTestAllSerializers("") { dateTimeString() } } + exception.message should startWith("An unknown type was encountered: class java.time.Instant") + } + it("Throws an exception when same method for same path has been previously registered") { + val exception = shouldThrow { + openApiTestAllSerializers( + snapshotName = "", + applicationSetup = { + install(Authentication) { + basic("basic") { + realm = "Ktor Server" + validate { UserIdPrincipal("Placeholder") } + } + } + }, + ) { + samePathSameMethod() + } + } + exception.message should startWith("A route has already been registered for path: /test/{a} and method: GET") + } + } + describe("Formats") { + it("Can set a format for a simple type schema") { + openApiTestAllSerializers( + snapshotName = "T0038__formatted_date_time_string.json", + customTypes = mapOf(typeOf() to TypeDefinition(type = "string", format = "date")) + ) { dateTimeString() } + } + } + describe("Free Form") { + // todo Assess strategies here + } + describe("Authentication") { + it("Can add a default auth config by default") { + openApiTestAllSerializers( + snapshotName = "T0045__default_auth_config.json", + applicationSetup = { + install(Authentication) { + basic("basic") { + realm = "Ktor Server" + validate { UserIdPrincipal("Placeholder") } + } + } + }, + specOverrides = { + this.copy( + components = Components( + securitySchemes = mutableMapOf( + "basic" to BasicAuth() + ) + ) + ) + } + ) { defaultAuthConfig() } + } + it("Can provide custom auth config with proper scopes") { + openApiTestAllSerializers( + snapshotName = "T0046__custom_auth_config.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" + ) ) ) ) ) ) ) - ) - } - ) { customAuthConfig() } - } - it("Can provide multiple authentication strategies") { - openApiTestAllSerializers( - snapshotName = "T0047__multiple_auth_strategies.json", - applicationSetup = { - install(Authentication) { - apiKey("api-key") { - headerName = "X-API-KEY" - validate { - UserIdPrincipal("Placeholder") + } + ) { customAuthConfig() } + } + it("Can provide multiple authentication strategies") { + openApiTestAllSerializers( + snapshotName = "T0047__multiple_auth_strategies.json", + applicationSetup = { + install(Authentication) { + apiKey("api-key") { + headerName = "X-API-KEY" + validate { + UserIdPrincipal("Placeholder") + } + } + jwt("jwt") { + realm = "Server" } } - jwt("jwt") { - realm = "Server" - } - } - }, - specOverrides = { - this.copy( - components = Components( - securitySchemes = mutableMapOf( - "jwt" to BearerAuth("JWT"), - "api-key" to ApiKeyAuth(ApiKeyAuth.ApiKeyLocation.HEADER, "X-API-KEY") + }, + specOverrides = { + this.copy( + components = Components( + securitySchemes = mutableMapOf( + "jwt" to BearerAuth("JWT"), + "api-key" to ApiKeyAuth(ApiKeyAuth.ApiKeyLocation.HEADER, "X-API-KEY") + ) ) ) - ) - } - ) { multipleAuthStrategies() } - } - } - describe("Enrichment") { - it("Can enrich a simple request") { - openApiTestAllSerializers("T0055__enriched_simple_request.json") { enrichedSimpleRequest() } - } - it("Can enrich a simple response") { - openApiTestAllSerializers("T0058__enriched_simple_response.json") { enrichedSimpleResponse() } - } - it("Can enrich a nested collection") { - openApiTestAllSerializers("T0056__enriched_nested_collection.json") { enrichedNestedCollection() } - } - it("Can enrich a complex generic type") { - openApiTestAllSerializers("T0057__enriched_complex_generic_type.json") { enrichedComplexGenericType() } - } - it("Can enrich a generic object") { - openApiTestAllSerializers("T0067__enriched_generic_object.json") { enrichedGenericResponse() } - } - } - describe("Constraints") { - it("Can apply constraints to an int field") { - openApiTestAllSerializers("T0059__int_constraints.json") { intConstraints() } - } - it("Can apply constraints to a double field") { - openApiTestAllSerializers("T0060__double_constraints.json") { doubleConstraints() } - } - it("Can apply a min and max length to a string field") { - openApiTestAllSerializers("T0061__string_min_max_constraints.json") { stringConstraints() } - } - it("Can apply a pattern to a string field") { - openApiTestAllSerializers("T0062__string_pattern_constraints.json") { stringPatternConstraints() } - } - it("Can apply a content encoding and media type to a string field") { - openApiTestAllSerializers("T0063__string_content_encoding_constraints.json") { - stringContentEncodingConstraints() + } + ) { 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() } } } - it("Can apply constraints to an array field") { - openApiTestAllSerializers("T0064__array_constraints.json") { arrayConstraints() } + describe("Enrichment") { + it("Can enrich a simple request") { + openApiTestAllSerializers("T0055__enriched_simple_request.json") { enrichedSimpleRequest() } + } + it("Can enrich a simple response") { + openApiTestAllSerializers("T0058__enriched_simple_response.json") { enrichedSimpleResponse() } + } + it("Can enrich a nested collection") { + openApiTestAllSerializers("T0056__enriched_nested_collection.json") { enrichedNestedCollection() } + } + it("Can enrich a complex generic type") { + openApiTestAllSerializers("T0057__enriched_complex_generic_type.json") { enrichedComplexGenericType() } + } + it("Can enrich a generic object") { + openApiTestAllSerializers("T0067__enriched_generic_object.json") { enrichedGenericResponse() } + } + } + describe("Constraints") { + it("Can apply constraints to an int field") { + openApiTestAllSerializers("T0059__int_constraints.json") { intConstraints() } + } + it("Can apply constraints to a double field") { + openApiTestAllSerializers("T0060__double_constraints.json") { doubleConstraints() } + } + it("Can apply a min and max length to a string field") { + openApiTestAllSerializers("T0061__string_min_max_constraints.json") { stringConstraints() } + } + it("Can apply a pattern to a string field") { + openApiTestAllSerializers("T0062__string_pattern_constraints.json") { stringPatternConstraints() } + } + it("Can apply a content encoding and media type to a string field") { + openApiTestAllSerializers("T0063__string_content_encoding_constraints.json") { + stringContentEncodingConstraints() + } + } + it("Can apply constraints to an array field") { + openApiTestAllSerializers("T0064__array_constraints.json") { arrayConstraints() } + } } } }) diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Authentication.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Authentication.kt index fac73e21f..be79d7e67 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Authentication.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Authentication.kt @@ -2,6 +2,7 @@ package io.bkbn.kompendium.core.util import io.bkbn.kompendium.core.fixtures.TestResponse 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.util.TestModules.defaultPathDescription 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.auth.authenticate import io.ktor.server.routing.Routing +import io.ktor.server.routing.post import io.ktor.server.routing.route 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() + } + security = mapOf( + "auth-oauth-google" to listOf("read:pets") + ) + } + post = PostInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + request { + description(defaultResponseDescription) + requestType() + } + security = mapOf( + "auth-oauth-google" to listOf("write:pets") + ) + } + } + } + } +} + fun Routing.multipleAuthStrategies() { authenticate("jwt", "api-key") { route(rootPath) { diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Examples.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Examples.kt index 0a7da3406..132bd2965 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Examples.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Examples.kt @@ -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.rootPath import io.bkbn.kompendium.json.schema.definition.TypeDefinition +import io.bkbn.kompendium.oas.payload.MediaType import io.bkbn.kompendium.oas.payload.Parameter import io.ktor.http.HttpStatusCode import io.ktor.server.application.install @@ -27,7 +28,7 @@ fun Routing.reqRespExamples() { description(defaultRequestDescription) requestType() examples( - "Testerina" to TestRequest(TestNested("asdf"), 1.5, emptyList()) + "Testerina" to MediaType.Example(TestRequest(TestNested("asdf"), 1.5, emptyList())) ) } response { @@ -35,7 +36,7 @@ fun Routing.reqRespExamples() { responseCode(HttpStatusCode.OK) responseType() examples( - "Testerino" to TestResponse("Heya") + "Testerino" to MediaType.Example(TestResponse("Heya")) ) } } @@ -50,7 +51,7 @@ fun Routing.exampleParams() = basicGetGenerator( `in` = Parameter.Location.path, schema = TypeDefinition.STRING, examples = mapOf( - "foo" to Parameter.Example("testing") + "foo" to MediaType.Example("testing") ) ) ) @@ -66,7 +67,7 @@ fun Routing.optionalReqExample() { description(defaultRequestDescription) requestType() examples( - "Testerina" to TestRequest(TestNested("asdf"), 1.5, emptyList()) + "Testerina" to MediaType.Example(TestRequest(TestNested("asdf"), 1.5, emptyList())) ) required(false) } @@ -75,7 +76,37 @@ fun Routing.optionalReqExample() { responseCode(HttpStatusCode.OK) responseType() 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() + examples( + "Testerina" to MediaType.Example( + TestRequest(TestNested("asdf"), 1.5, emptyList()), + "summary", + "description" + ) + ) + } + response { + description("This is a response description") + responseCode(HttpStatusCode.OK) + responseType() + examples( + "Testerino" to MediaType.Example(TestResponse("Heya"), "summary", "description") ) } } diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/NotarizedOpenApi.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/NotarizedOpenApi.kt index 9d6b2dfe4..9c6947c86 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/util/NotarizedOpenApi.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/NotarizedOpenApi.kt @@ -1,6 +1,7 @@ package io.bkbn.kompendium.core.util 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.TestRequest 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() + description("A cool request") + } + response { + responseType() + description("Cool response") + responseCode(HttpStatusCode.Created) + } + } + } + } +} diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/PolymorphismAndGenerics.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/PolymorphismAndGenerics.kt index 2e07f1ff8..7c9b42733 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/util/PolymorphismAndGenerics.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/PolymorphismAndGenerics.kt @@ -1,11 +1,13 @@ package io.bkbn.kompendium.core.util 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.Flibbity import io.bkbn.kompendium.core.fixtures.FlibbityGibbit import io.bkbn.kompendium.core.fixtures.Foosy 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.Page import io.ktor.server.routing.Routing @@ -20,3 +22,5 @@ fun Routing.genericPolymorphicResponse() = basicGetGenerator>() fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator>() fun Routing.nestedGenericCollection() = basicGetGenerator>() fun Routing.nestedGenericMultipleParamsCollection() = basicGetGenerator>() +fun Routing.overrideSealedTypeIdentifier() = basicGetGenerator() +fun Routing.subtypeNotCompleteSetOfParentProperties() = basicGetGenerator() diff --git a/core/src/test/resources/T0018__polymorphic_error_status_codes.json b/core/src/test/resources/T0018__polymorphic_error_status_codes.json index 38136706f..3c83c524a 100644 --- a/core/src/test/resources/T0018__polymorphic_error_status_codes.json +++ b/core/src/test/resources/T0018__polymorphic_error_status_codes.json @@ -86,12 +86,19 @@ }, "z": { "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.ComplexGibbit" + ] } }, "required": [ "b", "c", - "z" + "z", + "type" ] }, "SimpleGibbit": { @@ -102,10 +109,17 @@ }, "z": { "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.SimpleGibbit" + ] } }, "required": [ - "a" + "a", + "type" ] }, "FlibbityGibbit": { diff --git a/core/src/test/resources/T0019__generic_exception.json b/core/src/test/resources/T0019__generic_exception.json index 0ea6e4368..f59ce0385 100644 --- a/core/src/test/resources/T0019__generic_exception.json +++ b/core/src/test/resources/T0019__generic_exception.json @@ -82,11 +82,18 @@ }, "f": { "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.Bibbity" + ] } }, "required": [ "b", - "f" + "f", + "type" ] }, "Gibbity-String": { @@ -94,10 +101,17 @@ "properties": { "a": { "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.Gibbity" + ] } }, "required": [ - "a" + "a", + "type" ] }, "Flibbity-String": { diff --git a/core/src/test/resources/T0027__polymorphic_response.json b/core/src/test/resources/T0027__polymorphic_response.json index df40ec0c9..2caf27336 100644 --- a/core/src/test/resources/T0027__polymorphic_response.json +++ b/core/src/test/resources/T0027__polymorphic_response.json @@ -65,12 +65,19 @@ }, "z": { "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.ComplexGibbit" + ] } }, "required": [ "b", "c", - "z" + "z", + "type" ] }, "SimpleGibbit": { @@ -81,10 +88,17 @@ }, "z": { "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.SimpleGibbit" + ] } }, "required": [ - "a" + "a", + "type" ] }, "FlibbityGibbit": { diff --git a/core/src/test/resources/T0028__polymorphic_list_response.json b/core/src/test/resources/T0028__polymorphic_list_response.json index 8cd11e1d2..2684a2abc 100644 --- a/core/src/test/resources/T0028__polymorphic_list_response.json +++ b/core/src/test/resources/T0028__polymorphic_list_response.json @@ -65,12 +65,19 @@ }, "z": { "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.ComplexGibbit" + ] } }, "required": [ "b", "c", - "z" + "z", + "type" ] }, "SimpleGibbit": { @@ -81,10 +88,17 @@ }, "z": { "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.SimpleGibbit" + ] } }, "required": [ - "a" + "a", + "type" ] }, "List-FlibbityGibbit": { diff --git a/core/src/test/resources/T0029__polymorphic_map_response.json b/core/src/test/resources/T0029__polymorphic_map_response.json index 245f13cfa..e36815640 100644 --- a/core/src/test/resources/T0029__polymorphic_map_response.json +++ b/core/src/test/resources/T0029__polymorphic_map_response.json @@ -65,12 +65,19 @@ }, "z": { "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.ComplexGibbit" + ] } }, "required": [ "b", "c", - "z" + "z", + "type" ] }, "SimpleGibbit": { @@ -81,10 +88,17 @@ }, "z": { "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.SimpleGibbit" + ] } }, "required": [ - "a" + "a", + "type" ] }, "Map-String-FlibbityGibbit": { diff --git a/core/src/test/resources/T0032__polymorphic_response_with_generics.json b/core/src/test/resources/T0032__polymorphic_response_with_generics.json index cb0785750..7f898585e 100644 --- a/core/src/test/resources/T0032__polymorphic_response_with_generics.json +++ b/core/src/test/resources/T0032__polymorphic_response_with_generics.json @@ -62,11 +62,18 @@ "f": { "type": "number", "format": "double" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.Bibbity" + ] } }, "required": [ "b", - "f" + "f", + "type" ] }, "Gibbity-Double": { @@ -75,10 +82,17 @@ "a": { "type": "number", "format": "double" + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.Gibbity" + ] } }, "required": [ - "a" + "a", + "type" ] }, "Flibbity-Double": { diff --git a/core/src/test/resources/T0033__crazy_polymorphic_example.json b/core/src/test/resources/T0033__crazy_polymorphic_example.json index 725ddc1aa..0f3e39a8b 100644 --- a/core/src/test/resources/T0033__crazy_polymorphic_example.json +++ b/core/src/test/resources/T0033__crazy_polymorphic_example.json @@ -53,40 +53,6 @@ "webhooks": {}, "components": { "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": { "type": "object", "properties": { @@ -102,11 +68,66 @@ "$ref": "#/components/schemas/SimpleGibbit" } ] + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.Bibbity" + ] } }, "required": [ "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": { @@ -121,10 +142,17 @@ "$ref": "#/components/schemas/SimpleGibbit" } ] + }, + "type": { + "type": "string", + "enum": [ + "io.bkbn.kompendium.core.fixtures.Gibbity" + ] } }, "required": [ - "a" + "a", + "type" ] }, "Flibbity-FlibbityGibbit": { diff --git a/core/src/test/resources/T0070__sealed_interface_type_name_override.json b/core/src/test/resources/T0070__sealed_interface_type_name_override.json new file mode 100644 index 000000000..2883a5a7f --- /dev/null +++ b/core/src/test/resources/T0070__sealed_interface_type_name_override.json @@ -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": [] +} diff --git a/core/src/test/resources/T0071__subtype_not_complete_set_of_parent_properties.json b/core/src/test/resources/T0071__subtype_not_complete_set_of_parent_properties.json new file mode 100644 index 000000000..f3c3db584 --- /dev/null +++ b/core/src/test/resources/T0071__subtype_not_complete_set_of_parent_properties.json @@ -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": [] +} diff --git a/core/src/test/resources/T0072__custom_serialization_strategy.json b/core/src/test/resources/T0072__custom_serialization_strategy.json new file mode 100644 index 000000000..7f6180fce --- /dev/null +++ b/core/src/test/resources/T0072__custom_serialization_strategy.json @@ -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": [] +} diff --git a/core/src/test/resources/T0073__data_class_with_field_outside_constructor.json b/core/src/test/resources/T0073__data_class_with_field_outside_constructor.json new file mode 100644 index 000000000..9c8246525 --- /dev/null +++ b/core/src/test/resources/T0073__data_class_with_field_outside_constructor.json @@ -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": [] +} diff --git a/core/src/test/resources/T0074__auth_on_specific_path_operation.json b/core/src/test/resources/T0074__auth_on_specific_path_operation.json new file mode 100644 index 000000000..d598e89a3 --- /dev/null +++ b/core/src/test/resources/T0074__auth_on_specific_path_operation.json @@ -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": [] +} diff --git a/core/src/test/resources/T0075__example_summary_and_description.json b/core/src/test/resources/T0075__example_summary_and_description.json new file mode 100644 index 000000000..71f9cb1b8 --- /dev/null +++ b/core/src/test/resources/T0075__example_summary_and_description.json @@ -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": [] +} diff --git a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SchemaConfigurators.kt b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SchemaConfigurators.kt deleted file mode 100644 index b6135de5a..000000000 --- a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SchemaConfigurators.kt +++ /dev/null @@ -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> { - // 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() } - return if(hasAnyExpose) { - clazz.memberProperties - .filter { it.hasJavaAnnotation() } - } else clazz.memberProperties - } - - override fun serializableName(property: KProperty1): String = - property.getJavaAnnotation()?.value?: property.name - -} - -class JacksonSchemaConfigurator: SchemaConfigurator { - - override fun serializableMemberProperties(clazz: KClass<*>): Collection> = - clazz.memberProperties - .filterNot { - it.hasJavaAnnotation() - } - - override fun serializableName(property: KProperty1): String = - property.getJavaAnnotation()?.value?: property.name - -} - -inline fun KProperty1<*, *>.hasJavaAnnotation(): Boolean { - return javaField?.isAnnotationPresent(T::class.java)?: false -} - -inline fun KProperty1<*, *>.getJavaAnnotation(): T? { - return javaField?.getDeclaredAnnotation(T::class.java) -} diff --git a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SupportedSerializers.kt b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SupportedSerializers.kt deleted file mode 100644 index 35db0c376..000000000 --- a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SupportedSerializers.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.bkbn.kompendium.core.fixtures - -enum class SupportedSerializer { - KOTLINX, - GSON, - JACKSON -} diff --git a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt index e59c1ee73..d5fae5f37 100644 --- a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt +++ b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt @@ -1,7 +1,5 @@ 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.plugin.NotarizedApplication import io.bkbn.kompendium.core.routes.redoc @@ -16,19 +14,17 @@ import io.kotest.matchers.shouldNot import io.kotest.matchers.string.beBlank import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType 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.server.application.Application import io.ktor.server.engine.ApplicationEngineEnvironmentBuilder import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.plugins.contentnegotiation.ContentNegotiationConfig import io.ktor.server.routing.Routing import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.server.testing.testApplication -import java.io.File import kotlinx.serialization.json.Json +import java.io.File import kotlin.reflect.KType object TestHelpers { @@ -55,7 +51,7 @@ object TestHelpers { /** * 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 - * 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 */ fun openApiTestAllSerializers( @@ -64,70 +60,47 @@ object TestHelpers { applicationSetup: Application.() -> Unit = { }, specOverrides: OpenApiSpec.() -> OpenApiSpec = { this }, applicationEnvironmentBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {}, + notarizedApplicationConfigOverrides: NotarizedApplication.Config.() -> Unit = {}, + contentNegotiation: ContentNegotiationConfig.() -> Unit = { + json(Json { + encodeDefaults = true + explicitNulls = false + serializersModule = KompendiumSerializersModule.module + }) + }, routeUnderTest: Routing.() -> Unit ) { openApiTest( snapshotName, - SupportedSerializer.KOTLINX, - routeUnderTest, - applicationSetup, - specOverrides, - customTypes, - applicationEnvironmentBuilder - ) - openApiTest( - snapshotName, - SupportedSerializer.JACKSON, - routeUnderTest, - applicationSetup, - specOverrides, - customTypes, - applicationEnvironmentBuilder - ) - openApiTest( - snapshotName, - SupportedSerializer.GSON, routeUnderTest, applicationSetup, specOverrides, customTypes, + notarizedApplicationConfigOverrides, + contentNegotiation, applicationEnvironmentBuilder ) } private fun openApiTest( snapshotName: String, - serializer: SupportedSerializer, routeUnderTest: Routing.() -> Unit, applicationSetup: Application.() -> Unit, specOverrides: OpenApiSpec.() -> OpenApiSpec, typeOverrides: Map = emptyMap(), - applicationBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {} + notarizedApplicationConfigOverrides: NotarizedApplication.Config.() -> Unit, + contentNegotiation: ContentNegotiationConfig.() -> Unit, + applicationBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit ) = testApplication { environment(applicationBuilder) install(NotarizedApplication()) { customTypes = typeOverrides - spec = defaultSpec().specOverrides() - schemaConfigurator = when (serializer) { - SupportedSerializer.KOTLINX -> KotlinXSchemaConfigurator() - SupportedSerializer.GSON -> GsonSchemaConfigurator() - SupportedSerializer.JACKSON -> JacksonSchemaConfigurator() - } + spec = { specOverrides(defaultSpec()) } + schemaConfigurator = KotlinXSchemaConfigurator() + notarizedApplicationConfigOverrides() } install(ContentNegotiation) { - when (serializer) { - 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) - } - } + contentNegotiation() } application(applicationSetup) routing { diff --git a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt index a84a47ca8..180e49e4f 100644 --- a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt +++ b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt @@ -1,12 +1,10 @@ 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.Serializable import kotlinx.serialization.Transient +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.time.Instant @Serializable @@ -91,6 +89,25 @@ data class AnothaJamma(val b: Float) : 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 data class Gibbity(val a: T) : Flibbity @@ -158,9 +175,7 @@ object Nested { @Serializable data class TransientObject( - @field:Expose val nonTransient: String, - @field:JsonIgnore @Transient val transient: String = "transient" ) @@ -174,8 +189,6 @@ data class UnbackedObject( @Serializable data class SerialNameObject( - @field:JsonProperty("snake_case_name") - @field:SerializedName("snake_case_name") @SerialName("snake_case_name") val camelCaseName: String ) @@ -194,3 +207,8 @@ enum class Color { data class ObjectWithEnum( val color: Color ) + +@Serializable +data class SomethingSimilar(val a: String) { + val b = "something else" +} diff --git a/gradle.properties b/gradle.properties index 2e2297048..7d735736c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=3.14.4 +project.version=4.0.0-alpha # Kotlin kotlin.code.style=official # Gradle diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/KotlinXSchemaConfigurator.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/KotlinXSchemaConfigurator.kt index 2bac18c38..57f4a02f2 100644 --- a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/KotlinXSchemaConfigurator.kt +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/KotlinXSchemaConfigurator.kt @@ -1,20 +1,51 @@ 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.Transient import kotlin.reflect.KClass import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.hasAnnotation import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.primaryConstructor class KotlinXSchemaConfigurator : SchemaConfigurator { - override fun serializableMemberProperties(clazz: KClass<*>): Collection> = - clazz.memberProperties + override fun serializableMemberProperties(clazz: KClass<*>): Collection> { + return clazz.memberProperties .filterNot { it.hasAnnotation() } + .filter { clazz.primaryConstructor?.parameters?.map { it.name }?.contains(it.name) ?: true } + } override fun serializableName(property: KProperty1): String = property.annotations .filterIsInstance() .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() + return nameOverrideAnnotation?.value ?: (type.classifier as KClass<*>).qualifiedName!! + } } diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaConfigurator.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaConfigurator.kt index 2d9213e3a..c6c72bc4f 100644 --- a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaConfigurator.kt +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaConfigurator.kt @@ -1,17 +1,16 @@ package io.bkbn.kompendium.json.schema +import io.bkbn.kompendium.json.schema.definition.JsonSchema import kotlin.reflect.KClass import kotlin.reflect.KProperty1 -import kotlin.reflect.full.memberProperties +import kotlin.reflect.KType interface SchemaConfigurator { fun serializableMemberProperties(clazz: KClass<*>): Collection> fun serializableName(property: KProperty1): String - open class Default : SchemaConfigurator { - override fun serializableMemberProperties(clazz: KClass<*>): Collection> = - clazz.memberProperties - - override fun serializableName(property: KProperty1): String = property.name - } + fun sealedTypeEnrichment( + implementationType: KType, + implementationSchema: JsonSchema + ): JsonSchema } diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SealedObjectHandler.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SealedObjectHandler.kt index 36c78c698..5bb9bd820 100644 --- a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SealedObjectHandler.kt +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SealedObjectHandler.kt @@ -25,15 +25,18 @@ object SealedObjectHandler { val subclasses = clazz.sealedSubclasses .map { it.createType(type.arguments) } .map { t -> - SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator, enrichment).let { js -> - if (js is TypeDefinition && js.type == "object") { - val slug = t.getSlug(enrichment) - cache[slug] = js - ReferenceDefinition(t.getReferenceSlug(enrichment)) - } else { - js + SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator, enrichment) + .let { + schemaConfigurator.sealedTypeEnrichment(t, it) + }.let { js -> + if (js is TypeDefinition && js.type == "object") { + val slug = t.getSlug(enrichment) + cache[slug] = js + ReferenceDefinition(t.getReferenceSlug(enrichment)) + } else { + js + } } - } } .toSet() return AnyOfDefinition(subclasses) diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/util/Helpers.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/util/Helpers.kt index 2bc61693e..cf041bdd2 100644 --- a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/util/Helpers.kt +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/util/Helpers.kt @@ -13,7 +13,7 @@ object Helpers { fun KType.getSlug(enrichment: Enrichment? = null) = when (enrichment) { is TypeEnrichment<*> -> getEnrichedSlug(enrichment) is PropertyEnrichment -> error("Slugs should not be generated for field enrichments") - null -> getSimpleSlug() + else -> getSimpleSlug() } fun KType.getSimpleSlug(): String = when { @@ -26,7 +26,7 @@ object Helpers { fun KType.getReferenceSlug(enrichment: Enrichment? = null): String = when (enrichment) { is TypeEnrichment<*> -> getSimpleReferenceSlug() + "-${enrichment.id}" is PropertyEnrichment -> error("Reference slugs should never be generated for field enrichments") - null -> getSimpleReferenceSlug() + else -> getSimpleReferenceSlug() } private fun KType.getSimpleReferenceSlug() = when { diff --git a/oas/src/main/kotlin/io/bkbn/kompendium/oas/payload/MediaType.kt b/oas/src/main/kotlin/io/bkbn/kompendium/oas/payload/MediaType.kt index b743d1287..f646e44c5 100644 --- a/oas/src/main/kotlin/io/bkbn/kompendium/oas/payload/MediaType.kt +++ b/oas/src/main/kotlin/io/bkbn/kompendium/oas/payload/MediaType.kt @@ -20,5 +20,5 @@ data class MediaType( val encoding: Map? = null, ) { @Serializable - data class Example(@Contextual val value: Any) + data class Example(@Contextual val value: Any, val summary: String? = null, val description: String? = null) } diff --git a/oas/src/main/kotlin/io/bkbn/kompendium/oas/payload/Parameter.kt b/oas/src/main/kotlin/io/bkbn/kompendium/oas/payload/Parameter.kt index 33b7c6ce1..c5c711c7a 100644 --- a/oas/src/main/kotlin/io/bkbn/kompendium/oas/payload/Parameter.kt +++ b/oas/src/main/kotlin/io/bkbn/kompendium/oas/payload/Parameter.kt @@ -1,7 +1,6 @@ package io.bkbn.kompendium.oas.payload import io.bkbn.kompendium.json.schema.definition.JsonSchema -import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable /** @@ -26,12 +25,9 @@ data class Parameter( val required: Boolean = true, val deprecated: Boolean = false, val allowEmptyValue: Boolean? = null, - val examples: Map? = null, + val examples: Map? = null, // todo support styling https://spec.openapis.org/oas/v3.1.0#style-values ) { - @Serializable - data class Example(@Contextual val value: Any) - @Suppress("EnumNaming") @Serializable enum class Location { diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt index 96fe6d72a..393082fbc 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt @@ -60,13 +60,15 @@ private fun Application.mainModule() { } } install(NotarizedApplication()) { - spec = baseSpec.copy( - components = Components( - securitySchemes = mutableMapOf( - "basic" to BasicAuth() + spec = { + baseSpec.copy( + components = Components( + securitySchemes = mutableMapOf( + "basic" to BasicAuth() + ) ) ) - ) + } } routing { swagger(pageTitle = "Simple API Docs") diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt index ed1d240d5..a84983e73 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt @@ -44,9 +44,7 @@ private fun Application.mainModule() { }) } install(NotarizedApplication()) { - spec = baseSpec - // Adds support for @Transient and @SerialName - // If you are not using them this is not required. + spec = { baseSpec } schemaConfigurator = KotlinXSchemaConfigurator() } routing { diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomRedocPathPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomRedocPathPlayground.kt index c4ad7ad02..e0f571ff3 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomRedocPathPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomRedocPathPlayground.kt @@ -43,9 +43,7 @@ private fun Application.mainModule() { }) } install(NotarizedApplication()) { - spec = baseSpec - // Adds support for @Transient and @SerialName - // If you are not using them this is not required. + spec = { baseSpec } schemaConfigurator = KotlinXSchemaConfigurator() } routing { diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomSerializationPlayground.kt similarity index 59% rename from playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt rename to playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomSerializationPlayground.kt index 54dfae6f3..62bfa9d5c 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomSerializationPlayground.kt @@ -1,19 +1,19 @@ 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.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.routes.redoc 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.oas.component.Components 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.Util.baseSpec 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.call 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.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.response.respond +import io.ktor.server.response.respondText import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.route import io.ktor.server.routing.routing -import kotlin.reflect.KClass -import kotlin.reflect.KProperty1 -import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.javaField +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json fun main() { embeddedServer( @@ -38,20 +37,44 @@ fun main() { ).start(wait = true) } +private val CustomJsonEncoder = Json { + serializersModule = KompendiumSerializersModule.module + encodeDefaults = true + explicitNulls = false +} + private fun Application.mainModule() { install(ContentNegotiation) { - gson { - setPrettyPrinting() - } + json(Json { + serializersModule = KompendiumSerializersModule.module + encodeDefaults = true + explicitNulls = true + }) } install(NotarizedApplication()) { - spec = baseSpec - schemaConfigurator = GsonSchemaConfigurator() + spec = { + 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 { swagger(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs") - route("/{id}") { locationDocumentation() get { @@ -73,6 +96,9 @@ private fun Route.locationDocumentation() { get = GetInfo.builder { summary("Get user by id") description("A very neat endpoint!") + security = mapOf( + "basic" to emptyList() + ) response { responseCode(HttpStatusCode.OK) responseType() @@ -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> { - return if(excludeFieldsWithoutExposeAnnotation) clazz.memberProperties - .filter { it.hasJavaAnnotation() } - else clazz.memberProperties - } - - override fun serializableName(property: KProperty1): String = - property.getJavaAnnotation()?.value?: property.name -} - -private inline fun KProperty1<*, *>.hasJavaAnnotation(): Boolean { - return javaField?.isAnnotationPresent(T::class.java)?: false -} - -private inline fun KProperty1<*, *>.getJavaAnnotation(): T? { - return javaField?.getDeclaredAnnotation(T::class.java) -} diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt index c122d6f23..6ad1c4aa7 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt @@ -46,7 +46,7 @@ private fun Application.mainModule() { }) } install(NotarizedApplication()) { - spec = baseSpec + spec = { baseSpec } customTypes = mapOf( typeOf() to TypeDefinition(type = "string", format = "date-time") ) diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/EnrichmentPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/EnrichmentPlayground.kt index c8aee894b..98b10cf02 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/EnrichmentPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/EnrichmentPlayground.kt @@ -45,7 +45,7 @@ private fun Application.mainModule() { }) } install(NotarizedApplication()) { - spec = baseSpec + spec = { baseSpec } // Adds support for @Transient and @SerialName // If you are not using them this is not required. schemaConfigurator = KotlinXSchemaConfigurator() diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExamplePlayground.kt similarity index 53% rename from playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt rename to playground/src/main/kotlin/io/bkbn/kompendium/playground/ExamplePlayground.kt index ffb847eb3..ec9c81b96 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExamplePlayground.kt @@ -1,21 +1,20 @@ 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.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.routes.redoc 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.oas.payload.MediaType 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.ExceptionResponse import io.bkbn.kompendium.playground.util.Util.baseSpec 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.call 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.route import io.ktor.server.routing.routing -import kotlin.reflect.KClass -import kotlin.reflect.KProperty1 -import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.javaField +import kotlinx.serialization.json.Json fun main() { embeddedServer( @@ -42,29 +38,35 @@ fun main() { private fun Application.mainModule() { install(ContentNegotiation) { - jackson { - enable(SerializationFeature.INDENT_OUTPUT) - setSerializationInclusion(JsonInclude.Include.NON_NULL) - } + json(Json { + serializersModule = KompendiumSerializersModule.module + encodeDefaults = true + explicitNulls = false + }) } install(NotarizedApplication()) { - spec = baseSpec - schemaConfigurator = JacksonSchemaConfigurator() + spec = { baseSpec } + schemaConfigurator = KotlinXSchemaConfigurator() } routing { swagger(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs") - route("/{id}") { - locationDocumentation() + idDocumentation() get { call.respond(HttpStatusCode.OK, ExampleResponse(true)) } + route("/profile") { + profileDocumentation() + get { + call.respond(HttpStatusCode.OK, ExampleResponse(true)) + } + } } } } -private fun Route.locationDocumentation() { +private fun Route.idDocumentation() { install(NotarizedRoute()) { parameters = listOf( Parameter( @@ -80,31 +82,42 @@ private fun Route.locationDocumentation() { responseCode(HttpStatusCode.OK) responseType() description("Will return whether or not the user is real 😱") + examples( + "example1" to MediaType.Example(ExampleResponse(true), "ahaha", "bhbh"), + ) + } + + canRespond { + responseType() + responseCode(HttpStatusCode.NotFound) + description("Indicates that a user with this id does not exist") } } } } -// Adds support for JsonIgnore and JsonProperty annotations, -// if you are not using them this is not required -// This also does not support class level configuration -private class JacksonSchemaConfigurator: SchemaConfigurator.Default() { - - override fun serializableMemberProperties(clazz: KClass<*>): Collection> = - clazz.memberProperties - .filterNot { - it.hasJavaAnnotation() +private fun Route.profileDocumentation() { + install(NotarizedRoute()) { + parameters = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING + ) + ) + get = GetInfo.builder { + summary("Get a users profile") + description("A cool endpoint!") + response { + responseCode(HttpStatusCode.OK) + responseType() + description("Returns user profile information") } - - override fun serializableName(property: KProperty1): String = - property.getJavaAnnotation()?.value?: property.name - -} - -private inline fun KProperty1<*, *>.hasJavaAnnotation(): Boolean { - return javaField?.isAnnotationPresent(T::class.java)?: false -} - -private inline fun KProperty1<*, *>.getJavaAnnotation(): T? { - return javaField?.getDeclaredAnnotation(T::class.java) + canRespond { + responseType() + responseCode(HttpStatusCode.NotFound) + description("Indicates that a user with this id does not exist") + } + } + } } diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt index 599133e2b..57eb452ae 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt @@ -43,7 +43,7 @@ private fun Application.mainModule() { }) } install(NotarizedApplication()) { - spec = baseSpec + spec = { baseSpec } } install(StatusPages) { exception { call, _ -> diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt index b2de57d23..1603d9925 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt @@ -62,18 +62,22 @@ private fun Application.mainModule() { } } install(NotarizedApplication()) { - spec = baseSpec.copy( - components = Components( - securitySchemes = mutableMapOf( - "basic" to BasicAuth() + spec = { + baseSpec.copy( + components = Components( + securitySchemes = mutableMapOf( + "basic" to BasicAuth() + ) ) ) - ) - openApiJson = { - authenticate("basic") { - route("/openapi.json") { - get { - call.respond(HttpStatusCode.OK, this@route.application.attributes[KompendiumAttributes.openApiSpec]) + } + specRoute = { spec, routing -> + routing { + authenticate("basic") { + route("/openapi.json") { + get { + call.respond(HttpStatusCode.OK, spec) + } } } } diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt index 8eb0838f8..42885a9fa 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt @@ -43,7 +43,7 @@ private fun Application.mainModule() { }) } install(NotarizedApplication()) { - spec = baseSpec + spec = { baseSpec } } install(NotarizedLocations()) { locations = mapOf( diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/ResourcesPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ResourcesPlayground.kt index 1119537d6..15eccb494 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/ResourcesPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ResourcesPlayground.kt @@ -44,7 +44,7 @@ private fun Application.mainModule() { }) } install(NotarizedApplication()) { - spec = baseSpec + spec = { baseSpec } } install(NotarizedResources()) { resources = mapOf(