Compare commits

..

44 Commits

Author SHA1 Message Date
f2786fec08 major: v4 alpha (#505) 2023-09-04 04:22:46 +00:00
904cc6816d fix(deps): update kotestversion to v5.7.1 (#501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-03 21:54:20 +00:00
a06821a910 fix(deps): update dependency org.slf4j:slf4j-api to v2.0.9 (#499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-03 19:34:31 +00:00
a9dffcf8a5 fix(deps): update dependency org.slf4j:slf4j-simple to v2.0.9 (#500)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-03 17:08:11 +00:00
548b1ff4bc fix(deps): update kotestversion to v5.7.0 (#498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-02 01:20:46 +00:00
6f85ee476a fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-datetime to v0.4.1 (#497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-01 08:02:24 +00:00
326bbb4419 fix(deps): update ktor to v2.3.4 (#496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-31 19:30:07 +00:00
438eb52dc8 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.24.2 (#495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-26 01:50:52 +00:00
1fd59db4e1 fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.9.10 (#494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 19:09:49 +00:00
238e372460 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.9.10 (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 17:12:05 +00:00
473e2c22af chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.9.10 (#493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 13:30:22 +00:00
7fb7e688ac fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.6.0 (#491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-22 19:10:33 +00:00
9f2c27aa0f fix(deps): update dependency com.google.protobuf:protobuf-java to v3.24.1 (#490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-18 21:11:22 +00:00
dc762d1d3e chore(deps): update dependency gradle to v8.3 (#489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 08:22:47 +00:00
b039803246 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.24.0 (#488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-09 06:42:32 +00:00
6ec240d7cd fix(deps): update ktor to v2.3.3 (#487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-01 17:30:05 +00:00
dd57347b90 chore(deps): update dependency gradle to v8.2.1 (#486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-10 16:31:44 +00:00
43c8fd1338 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.23.4 (#485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-07 05:14:32 +00:00
6c2725680e fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.9.0 (#484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-06 22:00:21 +00:00
84dfe4e6fe chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.9.0 (#483)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-06 18:02:48 +00:00
a0c1abf011 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.9.0 (#482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-06 13:19:39 +00:00
25820d0920 chore(deps): update dependency gradle to v8.2 (#481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-30 23:11:04 +00:00
7c18283ac8 fix(deps): update ktor to v2.3.2 (#480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-28 19:10:33 +00:00
c5f3b7fa8b fix(deps): update dependency com.google.protobuf:protobuf-java to v3.23.3 (#478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-15 00:06:06 +00:00
8b81010d72 fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.8.22 (#477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-08 10:28:12 +00:00
c07ffa6a14 chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.8.22 (#476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-08 08:04:51 +00:00
7793908ab6 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.8.22 (#475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-08 03:19:07 +00:00
bf6d08c2bd chore: prep for 3.14.4 release 2023-06-05 16:13:54 -04:00
68bae4918e fix: References for for protobuf objects (#466) 2023-06-05 16:12:31 -04:00
592c116c3b fix(deps): update dependency io.ktor:ktor-server-core to v2.3.1 (#474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-01 10:46:44 +00:00
c7fe5c288f fix(deps): update ktor to v2.3.1 (#473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-31 08:54:22 -04:00
e059633055 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.23.2 (#472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-26 20:27:33 +00:00
884a50fc83 chore: prep for 3.14.3 release 2023-05-22 09:39:54 -04:00
588e52c9df fix: allow for request bodies to be marked as required=false (#470) 2023-05-22 09:39:04 -04:00
f49fcb2a22 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.23.1 (#469)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-17 23:39:18 +00:00
0a9475a7ab fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.5.1 (#463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-11 21:54:50 +00:00
f8fbb7ad25 fix(deps): update kotestversion to v5.6.2 (#462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-11 02:53:10 +00:00
f792fb5d1f fix(deps): update dependency com.google.protobuf:protobuf-java to v3.23.0 (#461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-10 11:08:22 +00:00
845d1a971d fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.8.21 (#460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-10 06:19:56 +00:00
9396b2ecfe chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.8.21 (#459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-10 03:36:01 +00:00
d74b7b3f28 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.8.21 (#458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-10 03:31:13 +00:00
e783630845 chore(deps): update dependency gradle to v8.1.1 (#457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-09 23:17:06 +00:00
d907ba004a chore: prep for 3.14.2 release 2023-05-08 11:50:45 -04:00
5c7ec4fdb4 fix: route with parameter declared via ktor function (#455) 2023-05-08 11:49:29 -04:00
75 changed files with 2579 additions and 594 deletions

View File

@ -12,6 +12,44 @@
## Released ## Released
## [4.0.0-alpha] - September 3rd, 2023
### Added
- Support for `type` on sealed interfaces
- Ability to provide custom serializers
### Fixed
- Exception thrown when inheriting variable
- Notarized routes not discarded on test completion
- Data classes with property members breaks schema generation
- Security cannot be applied to individual path operations
- Serialization fails on generic response
- Parameter example descriptions not being applied
### Removed
- Out of the box support for Jackson and Gson (can still be implemented through custom schema configurators)
## [3.14.4] - June 5th, 2023
### Changed
- Components definitions were not in the proper schema section. Prefixed the path with component slug in `protobuf java converter`.
## [3.14.3] - May 22nd, 2023
### Added
- Added `required` parameter in request info builder.
## [3.14.2] - May 8rd, 2023
### Changed
- Fixed bug where routes were not resolving when a parameter was declared via the ktor SDK
## [3.14.1] - April 28th, 2023 ## [3.14.1] - April 28th, 2023
### Changed ### Changed

View File

@ -1,6 +1,6 @@
plugins { plugins {
kotlin("jvm") version "1.8.20" apply false kotlin("jvm") version "1.9.10" apply false
kotlin("plugin.serialization") version "1.8.20" apply false kotlin("plugin.serialization") version "1.9.10" apply false
id("io.bkbn.sourdough.library.jvm") version "0.12.0" apply false id("io.bkbn.sourdough.library.jvm") version "0.12.0" apply false
id("io.bkbn.sourdough.application.jvm") version "0.12.0" apply false id("io.bkbn.sourdough.application.jvm") version "0.12.0" apply false
id("io.bkbn.sourdough.root") version "0.12.0" id("io.bkbn.sourdough.root") version "0.12.0"

View File

@ -48,8 +48,6 @@ dependencies {
testFixturesApi("io.ktor:ktor-server-core:$ktorVersion") testFixturesApi("io.ktor:ktor-server-core:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-test-host:$ktorVersion") testFixturesApi("io.ktor:ktor-server-test-host:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization:$ktorVersion") testFixturesApi("io.ktor:ktor-serialization:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-jackson:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-gson:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") testFixturesApi("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-content-negotiation:$ktorVersion") testFixturesApi("io.ktor:ktor-server-content-negotiation:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-auth:$ktorVersion") testFixturesApi("io.ktor:ktor-server-auth:$ktorVersion")
@ -59,7 +57,7 @@ dependencies {
testFixturesApi("dev.forst:ktor-api-key:2.2.4") testFixturesApi("dev.forst:ktor-api-key:2.2.4")
testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
} }
testing { testing {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,8 @@ class RequestInfo private constructor(
val typeEnrichment: TypeEnrichment<*>?, val typeEnrichment: TypeEnrichment<*>?,
val description: String, val description: String,
val examples: Map<String, MediaType.Example>?, val examples: Map<String, MediaType.Example>?,
val mediaTypes: Set<String> val mediaTypes: Set<String>,
val required: Boolean
) { ) {
companion object { companion object {
@ -27,6 +28,11 @@ class RequestInfo private constructor(
private var description: String? = null private var description: String? = null
private var examples: Map<String, MediaType.Example>? = null private var examples: Map<String, MediaType.Example>? = null
private var mediaTypes: Set<String>? = null private var mediaTypes: Set<String>? = null
private var required: Boolean? = null
fun required(r: Boolean) = apply {
this.required = r
}
fun requestType(t: KType) = apply { fun requestType(t: KType) = apply {
this.requestType = t this.requestType = t
@ -43,8 +49,8 @@ class RequestInfo private constructor(
fun description(s: String) = apply { this.description = s } fun description(s: String) = apply { this.description = s }
fun examples(vararg e: Pair<String, Any>) = apply { fun examples(vararg e: Pair<String, MediaType.Example>) = apply {
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) } this.examples = e.toMap()
} }
fun mediaTypes(vararg m: String) = apply { fun mediaTypes(vararg m: String) = apply {
@ -56,7 +62,8 @@ class RequestInfo private constructor(
description = description ?: error("Description must be present"), description = description ?: error("Description must be present"),
typeEnrichment = typeEnrichment, typeEnrichment = typeEnrichment,
examples = examples, examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json") mediaTypes = mediaTypes ?: setOf("application/json"),
required = required ?: true
) )
} }
} }

View File

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

View File

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

View File

@ -86,6 +86,7 @@ object NotarizedRoute {
?: it ?: it
} }
.replace(Regex("/\\(.+\\)"), "") .replace(Regex("/\\(.+\\)"), "")
.replace(Regex("/\\[.+\\]"), "")
fun Route.collectAuthMethods() = toString() fun Route.collectAuthMethods() = toString()
.split("/") .split("/")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.routing.route import io.ktor.server.routing.route
import io.ktor.server.routing.param
fun Routing.simplePathParsing() { fun Routing.simplePathParsing() {
route("/this") { route("/this") {
@ -100,3 +101,32 @@ fun Routing.trailingSlash() {
} }
} }
} }
fun Routing.paramWrapper() {
route("/test") {
param("a") {
param("b") {
param("c") {
install(NotarizedRoute()) {
parameters = listOf(
Parameter(
name = "test",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING
)
)
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,82 @@
{
"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": {
"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": "test",
"in": "query",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,136 @@
{
"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": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "You gotta send it",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
},
"examples": {
"Testerina": {
"value": {
"fieldName": {
"nesty": "asdf"
},
"b": 1.5,
"aaa": []
}
}
}
}
},
"required": false
},
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
},
"examples": {
"Testerino": {
"value": {
"c": "Heya"
}
}
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
},
"TestRequest": {
"type": "object",
"properties": {
"aaa": {
"items": {
"type": "number",
"format": "int64"
},
"type": "array"
},
"b": {
"type": "number",
"format": "double"
},
"fieldName": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"aaa",
"b",
"fieldName"
]
},
"TestNested": {
"type": "object",
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
# Kompendium # Kompendium
project.version=3.14.1 project.version=4.0.0-alpha
# Kotlin # Kotlin
kotlin.code.style=official kotlin.code.style=official
# Gradle # Gradle
@ -9,6 +9,6 @@ org.gradle.jvmargs=-Xmx2000m
org.gradle.parallel=true org.gradle.parallel=true
# Dependencies # Dependencies
ktorVersion=2.3.0 ktorVersion=2.3.4
kotestVersion=5.6.1 kotestVersion=5.7.1
detektVersion=1.22.0 detektVersion=1.22.0

Binary file not shown.

View File

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

15
gradlew vendored
View File

@ -83,10 +83,8 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -133,10 +131,13 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
@ -197,6 +198,10 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in # shell script including quotes and variable substitutions, so put them in

View File

@ -23,8 +23,8 @@ dependencies {
// Kompendium // Kompendium
api(projects.kompendiumEnrichment) api(projects.kompendiumEnrichment)
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20") implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.10")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
// Formatting // Formatting
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")

View File

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

View File

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

View File

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

View File

@ -8,12 +8,12 @@ import kotlin.reflect.KType
object Helpers { object Helpers {
private const val COMPONENT_SLUG = "#/components/schemas" const val COMPONENT_SLUG = "#/components/schemas"
fun KType.getSlug(enrichment: Enrichment? = null) = when (enrichment) { fun KType.getSlug(enrichment: Enrichment? = null) = when (enrichment) {
is TypeEnrichment<*> -> getEnrichedSlug(enrichment) is TypeEnrichment<*> -> getEnrichedSlug(enrichment)
is PropertyEnrichment -> error("Slugs should not be generated for field enrichments") is PropertyEnrichment -> error("Slugs should not be generated for field enrichments")
null -> getSimpleSlug() else -> getSimpleSlug()
} }
fun KType.getSimpleSlug(): String = when { fun KType.getSimpleSlug(): String = when {
@ -26,7 +26,7 @@ object Helpers {
fun KType.getReferenceSlug(enrichment: Enrichment? = null): String = when (enrichment) { fun KType.getReferenceSlug(enrichment: Enrichment? = null): String = when (enrichment) {
is TypeEnrichment<*> -> getSimpleReferenceSlug() + "-${enrichment.id}" is TypeEnrichment<*> -> getSimpleReferenceSlug() + "-${enrichment.id}"
is PropertyEnrichment -> error("Reference slugs should never be generated for field enrichments") is PropertyEnrichment -> error("Reference slugs should never be generated for field enrichments")
null -> getSimpleReferenceSlug() else -> getSimpleReferenceSlug()
} }
private fun KType.getSimpleReferenceSlug() = when { private fun KType.getSimpleReferenceSlug() = when {

View File

@ -22,8 +22,8 @@ dependencies {
// IMPLEMENTATION // IMPLEMENTATION
implementation(projects.kompendiumCore) implementation(projects.kompendiumCore)
implementation("io.ktor:ktor-server-core:2.3.0") implementation("io.ktor:ktor-server-core:2.3.4")
implementation("io.ktor:ktor-server-locations:2.3.0") implementation("io.ktor:ktor-server-locations:2.3.4")
// TESTING // TESTING

View File

@ -22,7 +22,7 @@ dependencies {
api(projects.kompendiumJsonSchema) api(projects.kompendiumJsonSchema)
api(projects.kompendiumEnrichment) api(projects.kompendiumEnrichment)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
// Formatting // Formatting
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")

View File

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

View File

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

View File

@ -37,12 +37,12 @@ dependencies {
implementation("org.apache.logging.log4j:log4j-api-kotlin:1.2.0") implementation("org.apache.logging.log4j:log4j-api-kotlin:1.2.0")
implementation("org.apache.logging.log4j:log4j-api:2.20.0") implementation("org.apache.logging.log4j:log4j-api:2.20.0")
implementation("org.apache.logging.log4j:log4j-core:2.20.0") implementation("org.apache.logging.log4j:log4j-core:2.20.0")
implementation("org.slf4j:slf4j-api:2.0.7") implementation("org.slf4j:slf4j-api:2.0.9")
implementation("org.slf4j:slf4j-simple:2.0.7") implementation("org.slf4j:slf4j-simple:2.0.9")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1")
implementation("joda-time:joda-time:2.12.5") implementation("joda-time:joda-time:2.12.5")
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,9 +22,9 @@ dependencies {
implementation(projects.kompendiumJsonSchema) implementation(projects.kompendiumJsonSchema)
implementation("com.google.protobuf:protobuf-java:3.22.3") implementation("com.google.protobuf:protobuf-java:3.24.2")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20") implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.10")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
// Formatting // Formatting
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")

View File

@ -9,6 +9,7 @@ import io.bkbn.kompendium.json.schema.definition.MapDefinition
import io.bkbn.kompendium.json.schema.definition.NullableDefinition import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType import kotlin.reflect.full.createType
@ -150,7 +151,7 @@ fun fromTypeToSchema(
type = "string", type = "string",
enum = javaProtoField.enumType.values.map { it.name }.toSet() enum = javaProtoField.enumType.values.map { it.name }.toSet()
) )
ReferenceDefinition(javaProtoField.enumType.name) ReferenceDefinition("${Helpers.COMPONENT_SLUG}/${javaProtoField.enumType.name}")
} }
Descriptors.FieldDescriptor.JavaType.MESSAGE -> { Descriptors.FieldDescriptor.JavaType.MESSAGE -> {
// Traverse through possible nested messages // Traverse through possible nested messages
@ -160,7 +161,7 @@ fun fromTypeToSchema(
it.jsonName to fromNestedTypeToSchema(it, cache) it.jsonName to fromNestedTypeToSchema(it, cache)
}.toMap() }.toMap()
) )
ReferenceDefinition(javaProtoField.messageType.name) ReferenceDefinition("${Helpers.COMPONENT_SLUG}/${javaProtoField.messageType.name}")
} }
null -> NullableDefinition() null -> NullableDefinition()
} }

View File

@ -2,17 +2,21 @@ package io.bkbn.kompendium.protobufjavaconverter.converters
import com.google.protobuf.Descriptors import com.google.protobuf.Descriptors
import com.google.protobuf.GeneratedMessageV3 import com.google.protobuf.GeneratedMessageV3
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.json.schema.definition.ArrayDefinition import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
import io.bkbn.kompendium.json.schema.definition.EnumDefinition import io.bkbn.kompendium.json.schema.definition.EnumDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.MapDefinition import io.bkbn.kompendium.json.schema.definition.MapDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers
import io.bkbn.kompendium.protobufjavaconverter.Corpus import io.bkbn.kompendium.protobufjavaconverter.Corpus
import io.bkbn.kompendium.protobufjavaconverter.DoubleNestedMessage import io.bkbn.kompendium.protobufjavaconverter.DoubleNestedMessage
import io.bkbn.kompendium.protobufjavaconverter.NestedMapMessage
import io.bkbn.kompendium.protobufjavaconverter.EnumMessage import io.bkbn.kompendium.protobufjavaconverter.EnumMessage
import io.bkbn.kompendium.protobufjavaconverter.GoogleTypes import io.bkbn.kompendium.protobufjavaconverter.GoogleTypes
import io.bkbn.kompendium.protobufjavaconverter.NestedMapMessage
import io.bkbn.kompendium.protobufjavaconverter.NestedMessage import io.bkbn.kompendium.protobufjavaconverter.NestedMessage
import io.bkbn.kompendium.protobufjavaconverter.RepeatedEnumMessage import io.bkbn.kompendium.protobufjavaconverter.RepeatedEnumMessage
import io.bkbn.kompendium.protobufjavaconverter.RepeatedMessage import io.bkbn.kompendium.protobufjavaconverter.RepeatedMessage
@ -23,10 +27,15 @@ import io.kotest.matchers.maps.shouldContainExactly
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeTypeOf import io.kotest.matchers.types.shouldBeTypeOf
import io.kotest.matchers.types.shouldNotBeTypeOf import io.kotest.matchers.types.shouldNotBeTypeOf
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
import io.ktor.server.routing.Route
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType import kotlin.reflect.full.createType
class FieldDescriptiorConvertersKtTest : DescribeSpec({ class FieldDescriptiorConvertersKtTest : DescribeSpec({
val componentSlug = Helpers.COMPONENT_SLUG
describe("fromTypeToSchemaTests") { describe("fromTypeToSchemaTests") {
val simpleMessageDescriptor = SimpleTestMessage.getDescriptor() val simpleMessageDescriptor = SimpleTestMessage.getDescriptor()
it("java int field should return TypeDefinition INT") { it("java int field should return TypeDefinition INT") {
@ -77,7 +86,7 @@ class FieldDescriptiorConvertersKtTest : DescribeSpec({
val message = NestedMessage.getDescriptor() val message = NestedMessage.getDescriptor()
val result = fromNestedTypeToSchema(message.findFieldByName("nested_field")) val result = fromNestedTypeToSchema(message.findFieldByName("nested_field"))
result.shouldBeTypeOf<ReferenceDefinition>() result.shouldBeTypeOf<ReferenceDefinition>()
result.`$ref`.shouldBe(message.findFieldByName("nested_field").messageType.name) result.`$ref`.shouldBe("${Helpers.COMPONENT_SLUG}/${message.findFieldByName("nested_field").messageType.name}")
} }
it("Repeated message should return ArrayDefinition") { it("Repeated message should return ArrayDefinition") {
@ -85,7 +94,7 @@ class FieldDescriptiorConvertersKtTest : DescribeSpec({
val result = fromNestedTypeToSchema(message.findFieldByName("repeated_field")) val result = fromNestedTypeToSchema(message.findFieldByName("repeated_field"))
result.shouldBeTypeOf<ArrayDefinition>() result.shouldBeTypeOf<ArrayDefinition>()
result.items.shouldBeTypeOf<ReferenceDefinition>() result.items.shouldBeTypeOf<ReferenceDefinition>()
(result.items as ReferenceDefinition).`$ref`.shouldBe(SimpleTestMessage.getDescriptor().name) (result.items as ReferenceDefinition).`$ref`.shouldBe("$componentSlug/${SimpleTestMessage.getDescriptor().name}")
} }
it("Repeated enum message should return ArrayDefinition") { it("Repeated enum message should return ArrayDefinition") {
@ -93,7 +102,7 @@ class FieldDescriptiorConvertersKtTest : DescribeSpec({
val result: JsonSchema = fromNestedTypeToSchema(message.findFieldByName("repeated_field")) val result: JsonSchema = fromNestedTypeToSchema(message.findFieldByName("repeated_field"))
result.shouldBeTypeOf<ArrayDefinition>() result.shouldBeTypeOf<ArrayDefinition>()
result.items.shouldBeTypeOf<ReferenceDefinition>() result.items.shouldBeTypeOf<ReferenceDefinition>()
(result.items as ReferenceDefinition).`$ref`.shouldBe(Corpus.getDescriptor().name) (result.items as ReferenceDefinition).`$ref`.shouldBe("$componentSlug/${Corpus.getDescriptor().name}")
} }
it("SimpleMapMessage message should return MapDefinition") { it("SimpleMapMessage message should return MapDefinition") {
@ -169,7 +178,7 @@ class FieldDescriptiorConvertersKtTest : DescribeSpec({
// Our nested field should be a reference // Our nested field should be a reference
result.shouldBeTypeOf<ReferenceDefinition>() result.shouldBeTypeOf<ReferenceDefinition>()
// Our nested field should be a reference to simplemessage // Our nested field should be a reference to simplemessage
result.`$ref`.shouldBe(SimpleTestMessage.getDescriptor().name) result.`$ref`.shouldBe("$componentSlug/${SimpleTestMessage.getDescriptor().name}")
} }
it("Double nested message to schema") { it("Double nested message to schema") {
@ -201,11 +210,11 @@ class FieldDescriptiorConvertersKtTest : DescribeSpec({
// Our nested field should be a reference // Our nested field should be a reference
result.shouldBeTypeOf<ReferenceDefinition>() result.shouldBeTypeOf<ReferenceDefinition>()
// it should be a reference to our nested message // it should be a reference to our nested message
result.`$ref`.shouldBe(NestedMessage.getDescriptor().name) result.`$ref`.shouldBe("$componentSlug/${NestedMessage.getDescriptor().name}")
val nestedResult = (resultSchema[NestedMessage::class.createType()] as TypeDefinition).properties!!["nestedField"] val nestedResult = (resultSchema[NestedMessage::class.createType()] as TypeDefinition).properties!!["nestedField"]
nestedResult.shouldBeTypeOf<ReferenceDefinition>() nestedResult.shouldBeTypeOf<ReferenceDefinition>()
// Our nested message reference should be pointing to simpleTest message // Our nested message reference should be pointing to simpleTest message
nestedResult.`$ref`.shouldBe(SimpleTestMessage.getDescriptor().name) nestedResult.`$ref`.shouldBe("$componentSlug/${SimpleTestMessage.getDescriptor().name}")
// last but not least we should have definition for our SimpleTest message which is not a reference // last but not least we should have definition for our SimpleTest message which is not a reference
(resultSchema[SimpleTestMessage::class.createType()] as TypeDefinition).shouldNotBeTypeOf<ReferenceDefinition>() (resultSchema[SimpleTestMessage::class.createType()] as TypeDefinition).shouldNotBeTypeOf<ReferenceDefinition>()
} }
@ -235,6 +244,39 @@ class FieldDescriptiorConvertersKtTest : DescribeSpec({
testMessageBasics(message) testMessageBasics(message)
} }
} }
describe("Test spec generation") {
it("Generates simple message references") {
openApiTestAllSerializers(
"T0001__simpletestmessage_post.json",
testMessageBasics(SimpleTestMessage.getDefaultInstance())
) { testRoute<SimpleTestMessage>() }
}
it("Generates enum references") {
openApiTestAllSerializers(
"T0002__enummessage_post.json",
testMessageBasics(EnumMessage.getDefaultInstance())
) { testRoute<EnumMessage>() }
}
it("Generates repeated type references") {
openApiTestAllSerializers(
"T0003__repeatedmessage_post.json",
testMessageBasics(RepeatedMessage.getDefaultInstance())
) { testRoute<RepeatedMessage>() }
}
it("Generates nested type references") {
openApiTestAllSerializers(
"T0004__nestedmessage_post.json",
testMessageBasics(NestedMessage.getDefaultInstance())
) { testRoute<NestedMessage>() }
}
it("Generates nested map type references") {
openApiTestAllSerializers(
"T0005__nestedmapmessage_post.json",
testMessageBasics(NestedMapMessage.getDefaultInstance())
) { testRoute<NestedMapMessage>() }
}
}
}) })
/** /**
@ -256,3 +298,25 @@ fun testMessageBasics(message: GeneratedMessageV3): Map<KType, JsonSchema> {
} }
return resultSchema return resultSchema
} }
private const val DEFAULT_RESPONSE_DESCRIPTION = "A Successful Endeavor"
private const val DEFAULT_REQUEST_DESCRIPTION = "You gotta send it"
private const val DEFAULT_PATH_SUMMARY = "Great Summary!"
private const val DEFAULT_PATH_DESCRIPTION = "testing more"
private inline fun <reified T> Route.testRoute() {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary(DEFAULT_PATH_SUMMARY)
description(DEFAULT_PATH_DESCRIPTION)
request {
requestType<Unit>()
description(DEFAULT_REQUEST_DESCRIPTION)
}
response {
responseCode(HttpStatusCode.OK)
responseType<T>()
description(DEFAULT_RESPONSE_DESCRIPTION)
}
}
}
}

View File

@ -0,0 +1,127 @@
{
"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": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "You gotta send it",
"required": true
},
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleTestMessage"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"SimpleTestMessage": {
"type": "object",
"properties": {
"myTestDouble": {
"type": "number",
"format": "double"
},
"myTestFloat": {
"type": "number",
"format": "float"
},
"myTestInt32": {
"type": "number",
"format": "int32"
},
"myTestInt64": {
"type": "number",
"format": "int64"
},
"myTestUint32": {
"type": "number",
"format": "int32"
},
"myTestUint64": {
"type": "number",
"format": "int64"
},
"myTestSint32": {
"type": "number",
"format": "int32"
},
"myTestSint64": {
"type": "number",
"format": "int64"
},
"myTestFixed32": {
"type": "number",
"format": "int32"
},
"myTestFixed64": {
"type": "number",
"format": "int64"
},
"myTestSfixed32": {
"type": "number",
"format": "int32"
},
"myTestSfixed64": {
"type": "number",
"format": "int64"
},
"myTestBool": {
"type": "boolean"
},
"myTestBytes": {
"type": "string"
},
"myTestString": {
"type": "string"
}
}
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,86 @@
{
"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": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "You gotta send it",
"required": true
},
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EnumMessage"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"EnumMessage": {
"type": "object",
"properties": {
"corpus": {
"$ref": "#/components/schemas/Corpus"
}
}
},
"Corpus": {
"type": "string",
"enum": [
"CORPUS_UNSPECIFIED",
"CORPUS_UNIVERSAL",
"CORPUS_WEB",
"CORPUS_IMAGES",
"CORPUS_LOCAL",
"CORPUS_NEWS",
"CORPUS_PRODUCTS",
"CORPUS_VIDEO"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,138 @@
{
"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": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "You gotta send it",
"required": true
},
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepeatedMessage"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"RepeatedMessage": {
"type": "object",
"properties": {
"repeatedField": {
"items": {
"$ref": "#/components/schemas/SimpleTestMessage"
},
"type": "array"
}
}
},
"SimpleTestMessage": {
"type": "object",
"properties": {
"myTestDouble": {
"type": "number",
"format": "double"
},
"myTestFloat": {
"type": "number",
"format": "float"
},
"myTestInt32": {
"type": "number",
"format": "int32"
},
"myTestInt64": {
"type": "number",
"format": "int64"
},
"myTestUint32": {
"type": "number",
"format": "int32"
},
"myTestUint64": {
"type": "number",
"format": "int64"
},
"myTestSint32": {
"type": "number",
"format": "int32"
},
"myTestSint64": {
"type": "number",
"format": "int64"
},
"myTestFixed32": {
"type": "number",
"format": "int32"
},
"myTestFixed64": {
"type": "number",
"format": "int64"
},
"myTestSfixed32": {
"type": "number",
"format": "int32"
},
"myTestSfixed64": {
"type": "number",
"format": "int64"
},
"myTestBool": {
"type": "boolean"
},
"myTestBytes": {
"type": "string"
},
"myTestString": {
"type": "string"
}
}
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,135 @@
{
"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": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "You gotta send it",
"required": true
},
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NestedMessage"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"NestedMessage": {
"type": "object",
"properties": {
"nestedField": {
"$ref": "#/components/schemas/SimpleTestMessage"
}
}
},
"SimpleTestMessage": {
"type": "object",
"properties": {
"myTestDouble": {
"type": "number",
"format": "double"
},
"myTestFloat": {
"type": "number",
"format": "float"
},
"myTestInt32": {
"type": "number",
"format": "int32"
},
"myTestInt64": {
"type": "number",
"format": "int64"
},
"myTestUint32": {
"type": "number",
"format": "int32"
},
"myTestUint64": {
"type": "number",
"format": "int64"
},
"myTestSint32": {
"type": "number",
"format": "int32"
},
"myTestSint64": {
"type": "number",
"format": "int64"
},
"myTestFixed32": {
"type": "number",
"format": "int32"
},
"myTestFixed64": {
"type": "number",
"format": "int64"
},
"myTestSfixed32": {
"type": "number",
"format": "int32"
},
"myTestSfixed64": {
"type": "number",
"format": "int64"
},
"myTestBool": {
"type": "boolean"
},
"myTestBytes": {
"type": "string"
},
"myTestString": {
"type": "string"
}
}
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,146 @@
{
"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": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "You gotta send it",
"required": true
},
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NestedMapMessage"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"NestedMapMessage": {
"type": "object",
"properties": {
"mapField": {
"additionalProperties": {
"type": "object",
"properties": {
"myVariable0": {
"$ref": "#/components/schemas/SimpleTestMessage"
},
"myVariable1": {
"$ref": "#/components/schemas/SimpleTestMessage"
}
}
},
"type": "object"
}
}
},
"SimpleTestMessage": {
"type": "object",
"properties": {
"myTestDouble": {
"type": "number",
"format": "double"
},
"myTestFloat": {
"type": "number",
"format": "float"
},
"myTestInt32": {
"type": "number",
"format": "int32"
},
"myTestInt64": {
"type": "number",
"format": "int64"
},
"myTestUint32": {
"type": "number",
"format": "int32"
},
"myTestUint64": {
"type": "number",
"format": "int64"
},
"myTestSint32": {
"type": "number",
"format": "int32"
},
"myTestSint64": {
"type": "number",
"format": "int64"
},
"myTestFixed32": {
"type": "number",
"format": "int32"
},
"myTestFixed64": {
"type": "number",
"format": "int64"
},
"myTestSfixed32": {
"type": "number",
"format": "int32"
},
"myTestSfixed64": {
"type": "number",
"format": "int64"
},
"myTestBool": {
"type": "boolean"
},
"myTestBytes": {
"type": "string"
},
"myTestString": {
"type": "string"
}
}
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -22,8 +22,8 @@ dependencies {
// IMPLEMENTATION // IMPLEMENTATION
implementation(projects.kompendiumCore) implementation(projects.kompendiumCore)
implementation("io.ktor:ktor-server-core:2.3.0") implementation("io.ktor:ktor-server-core:2.3.4")
implementation("io.ktor:ktor-server-resources:2.3.0") implementation("io.ktor:ktor-server-resources:2.3.4")
// TESTING // TESTING