Compare commits

..

11 Commits

51 changed files with 1326 additions and 1301 deletions

View File

@ -5,6 +5,9 @@
### Added ### Added
### Changed ### Changed
- Can now put redoc route behind authentication
- Fixed issue where type erasure was breaking nested generics
- Fix bug with null references not displaying properly on properties
### Remove ### Remove
@ -12,6 +15,17 @@
## Released ## Released
## [3.0.0] - August 16th, 2022
### Added
- Ktor 2 Support 🎉
- OpenAPI 3.1 Standard
- JsonSchema Generator
- `NotarizedRoute` plugin
### Removed
- SwaggerUI module removed (due to lack of OpenAPI 3.1 support)
- Kompendium Annotations removed (field renames, undeclared fields, etc. will be follow-up work)
## [2.3.5] - June 7th, 2022 ## [2.3.5] - June 7th, 2022
### Added ### Added

View File

@ -1,6 +1,6 @@
# Kompendium # Kompendium
Welcome to Kompendium, the straight-forward, minimally-invasive OpenAPI generator for Ktor. Welcome to Kompendium, the straight-forward, non-invasive OpenAPI generator for Ktor.
## How to install ## How to install
@ -21,21 +21,74 @@ In addition to publishing releases to Maven Central, a snapshot version gets pub
to `main`. These can be consumed by adding the repository to your gradle build file. Instructions can be to `main`. These can be consumed by adding the repository to your gradle build file. Instructions can be
found [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package) found [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package)
## Setting up the Kompendium Plugin ## Setting up Kompendium
Kompendium is instantiated as a Ktor Feature/Plugin. It can be added to your API as follows Kompendium's core features are comprised of a singular application level plugin and a collection of route level plugins.
The former sets up your OpenApi spec along with various cross-route metadata and overrides such as custom types (useful
for things like datetime libraries)
### `NotarizedApplication` plugin
The notarized application plugin is installed at (surprise!) the app level
```kotlin ```kotlin
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs the Kompendium Plugin and sets up baseline server metadata install(NotarizedApplication()) {
install(Kompendium) { spec = OpenApiSpec(
spec = OpenApiSpec(/*..*/) // spec details go here ...
)
} }
// ...
} }
``` ```
## Notarization ### `NotarizedRoute` plugin
The concept of notarizing routes / exceptions / etc. is central to Kompendium. More details on _how_ to notarize your Notarized routes take advantage of Ktor 2's [route specific plugin](https://ktor.io/docs/plugins.html#install-route)
API can be found in the kompendium-core module. feature. This allows us to take individual routes, document them, and feed them back in to the application level plugin.
This also allows you to adopt Kompendium incrementally. Individual routes can be documented at your leisure, and is
purely
additive, meaning that you do not need to modify existing code to get documentation working, you just need new code!
Non-invasive FTW 🚀
Documenting a simple `GET` endpoint would look something like this
```kotlin
private fun Route.documentation() {
install(NotarizedRoute()) {
parameters = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
)
)
get = GetInfo.builder {
summary("Get user by id")
description("A very neat endpoint!")
response {
responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>()
description("Will return whether or not the user is real 😱")
}
}
}
}
route("/{id}") {
documentation()
get {
call.respond(HttpStatusCode.OK, ExampleResponse(true))
}
}
```
Full details on application and route notarization can be found in the `core` module
## The Playground
In addition to the documentation available here, Kompendium has a number of out-of-the-box examples available in the
playground module. Go ahead and fork the repo and run them directly on your machine to get a sense of what Kompendium
can do!

View File

@ -1,9 +1,9 @@
plugins { plugins {
kotlin("jvm") version "1.7.10" apply false kotlin("jvm") version "1.7.10" apply false
kotlin("plugin.serialization") version "1.7.10" apply false kotlin("plugin.serialization") version "1.7.10" apply false
id("io.bkbn.sourdough.library.jvm") version "0.9.0" apply false id("io.bkbn.sourdough.library.jvm") version "0.9.1" apply false
id("io.bkbn.sourdough.application.jvm") version "0.9.0" apply false id("io.bkbn.sourdough.application.jvm") version "0.9.1" apply false
id("io.bkbn.sourdough.root") version "0.9.0" id("io.bkbn.sourdough.root") version "0.9.1"
id("com.github.jakemarsden.git-hooks") version "0.0.2" id("com.github.jakemarsden.git-hooks") version "0.0.2"
id("org.jetbrains.dokka") version "1.7.10" id("org.jetbrains.dokka") version "1.7.10"
id("org.jetbrains.kotlinx.kover") version "0.5.1" id("org.jetbrains.kotlinx.kover") version "0.5.1"

View File

@ -6,12 +6,14 @@ It is also the only mandatory client-facing module for a basic setup.
# Package io.bkbn.kompendium.core # Package io.bkbn.kompendium.core
The root package contains several objects that power Kompendium, including the Kompendium Ktor Plugin, route ## Plugins
notarization methods, and the reflection engine that analyzes method info type parameters.
## Plugin As mentioned in the root documentation, there are two core Kompendium plugins.
The Kompendium plugin is an extremely light-weight plugin, with only a couple areas of customization. 1. The application level plugin that handles app level metadata, configuring up your OpenAPI spec, managing custom data
types, etc.
2. The route level plugin, which is how users declare the documentation for the given route. It _must_ be installed on
every route you wish to document
### Serialization ### Serialization
@ -29,76 +31,18 @@ serializer module will convert any `Any` serialization to be `Contextual`. This
only way to get Kotlinx to play nice with serializing `Any`. If you come up with a better solution, definitely go ahead only way to get Kotlinx to play nice with serializing `Any`. If you come up with a better solution, definitely go ahead
and open up a PR! and open up a PR!
## Notarization ## NotarizedApplication
Central to Kompendium is the concept of notarization. TODO
Notarizing a route is the mechanism by which Kompendium analyzes your route types, along with provided metadata, and ## NotarizedRoute
converts to the expected OpenAPI format.
Before jumping into notarization, lets first look at a standard Ktor route TODO
```kotlin
routing {
get {
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString()))
}
}
```
Now, let's compare this to the same functionality, but notarized using Kompendium
```kotlin
routing {
notarizedGet(simpleGetExample) {
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString()))
}
}
```
Pretty simple huh. But hold on... what is this `simpleGetExample`? How can I know that it is so "simple". Let's take a
look
```kotlin
val simpleGetExample = GetInfo<Unit, BasicResponse>(
summary = "Simple, Documented GET Request",
description = "This is to showcase just how easy it is to document your Ktor API!",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "This means everything went as expected!",
examples = mapOf("demo" to BasicResponse(c = "52c099d7-8642-46cc-b34e-22f39b923cf4"))
),
tags = setOf("Simple")
)
```
See, not so bad 😄 `GetInfo<*,*>` is an implementation of `MethodInfo<TParam, TResp>`, a sealed interface designed to
encapsulate all the metadata required for documenting an API route. Kompendium leverages this data, along with the
provided type parameters `TParam` and `TResp` to construct the full OpenAPI Specification for your route.
Additionally, just as a backup, each notarization method includes a "post-processing' hook that will allow you to have
final say in the generated route info prior to being attached to the spec. This can be accessed via the optional
parameter
```kotlin
routing {
notarizedGet(simpleGetExample, postProcess = { spec -> spec }) {
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString()))
}
}
```
This should only be used in _extremely_ rare scenarios, but it is nice to know it is there if you need it.
# Package io.bkbn.kompendium.core.metadata # Package io.bkbn.kompendium.core.metadata
Houses all interfaces and types related to describing route metadata. Houses all interfaces and types related to describing route metadata.
# Package io.bkbn.kompendium.core.parser
Responsible for the parse of method information. Base implementation is an interface to support extensibility as shown
in the `kompendium-locations` module.
# Package io.bkbn.kompendium.core.routes # Package io.bkbn.kompendium.core.routes
Houses any routes provided by the core module. At the moment the only supported route is to enable ReDoc support. Houses any routes provided by the core module. At the moment the only supported route is to enable ReDoc support.

View File

@ -4,44 +4,32 @@ import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.core.metadata.DeleteInfo import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.HeadInfo import io.bkbn.kompendium.core.metadata.HeadInfo
import io.bkbn.kompendium.core.metadata.MethodInfo
import io.bkbn.kompendium.core.metadata.MethodInfoWithRequest
import io.bkbn.kompendium.core.metadata.OptionsInfo import io.bkbn.kompendium.core.metadata.OptionsInfo
import io.bkbn.kompendium.core.metadata.PatchInfo import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.util.Helpers.addToSpec
import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug import io.bkbn.kompendium.core.util.SpecConfig
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.path.Path import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation
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.payload.Request
import io.bkbn.kompendium.oas.payload.Response
import io.ktor.server.application.ApplicationCallPipeline import io.ktor.server.application.ApplicationCallPipeline
import io.ktor.server.application.Hook import io.ktor.server.application.Hook
import io.ktor.server.application.createRouteScopedPlugin import io.ktor.server.application.createRouteScopedPlugin
import io.ktor.server.routing.Route import io.ktor.server.routing.Route
import kotlin.reflect.KClass
import kotlin.reflect.KType
object NotarizedRoute { object NotarizedRoute {
class Config { class Config : SpecConfig {
var tags: Set<String> = emptySet() override var tags: Set<String> = emptySet()
var parameters: List<Parameter> = emptyList() override var parameters: List<Parameter> = emptyList()
var get: GetInfo? = null override var get: GetInfo? = null
var post: PostInfo? = null override var post: PostInfo? = null
var put: PutInfo? = null override var put: PutInfo? = null
var delete: DeleteInfo? = null override var delete: DeleteInfo? = null
var patch: PatchInfo? = null override var patch: PatchInfo? = null
var head: HeadInfo? = null override var head: HeadInfo? = null
var options: OptionsInfo? = null override var options: OptionsInfo? = null
var security: Map<String, List<String>>? = null override var security: Map<String, List<String>>? = null
internal var path: Path? = null internal var path: Path? = null
} }
@ -86,86 +74,5 @@ object NotarizedRoute {
pluginConfig.path = path pluginConfig.path = path
} }
private fun MethodInfo.addToSpec(path: Path, spec: OpenApiSpec, config: Config) {
SchemaGenerator.fromTypeOrUnit(this.response.responseType, spec.components.schemas)?.let { schema ->
spec.components.schemas[this.response.responseType.getSimpleSlug()] = schema
}
errors.forEach { error ->
SchemaGenerator.fromTypeOrUnit(error.responseType, spec.components.schemas)?.let { schema ->
spec.components.schemas[error.responseType.getSimpleSlug()] = schema
}
}
when (this) {
is MethodInfoWithRequest -> {
SchemaGenerator.fromTypeOrUnit(this.request.requestType, spec.components.schemas)?.let { schema ->
spec.components.schemas[this.request.requestType.getSimpleSlug()] = schema
}
}
else -> {}
}
val operations = this.toPathOperation(config)
when (this) {
is DeleteInfo -> path.delete = operations
is GetInfo -> path.get = operations
is HeadInfo -> path.head = operations
is PatchInfo -> path.patch = operations
is PostInfo -> path.post = operations
is PutInfo -> path.put = operations
is OptionsInfo -> path.options = operations
}
}
private fun MethodInfo.toPathOperation(config: Config) = PathOperation(
tags = config.tags.plus(this.tags),
summary = this.summary,
description = this.description,
externalDocs = this.externalDocumentation,
operationId = this.operationId,
deprecated = this.deprecated,
parameters = this.parameters,
security = config.security
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toList(),
requestBody = when (this) {
is MethodInfoWithRequest -> Request(
description = this.request.description,
content = this.request.requestType.toReferenceContent(this.request.examples),
required = true
)
else -> null
},
responses = mapOf(
this.response.responseCode.value to Response(
description = this.response.description,
content = this.response.responseType.toReferenceContent(this.response.examples)
)
).plus(this.errors.toResponseMap())
)
private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error ->
error.responseCode.value to Response(
description = error.description,
content = error.responseType.toReferenceContent(error.examples)
)
}
private fun KType.toReferenceContent(examples: Map<String, MediaType.Example>?): Map<String, MediaType>? =
when (this.classifier as KClass<*>) {
Unit::class -> null
else -> mapOf(
"application/json" to MediaType(
schema = ReferenceDefinition(this.getReferenceSlug()),
examples = examples
)
)
}
private fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "") private fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
} }

View File

@ -3,7 +3,7 @@ package io.bkbn.kompendium.core.routes
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call import io.ktor.server.application.call
import io.ktor.server.html.respondHtml import io.ktor.server.html.respondHtml
import io.ktor.server.routing.Routing 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 kotlinx.html.body import kotlinx.html.body
@ -20,7 +20,7 @@ import kotlinx.html.unsafe
* @param pageTitle Webpage title you wish to be displayed on your docs * @param pageTitle Webpage title you wish to be displayed on your docs
* @param specUrl url to point ReDoc to the OpenAPI json document * @param specUrl url to point ReDoc to the OpenAPI json document
*/ */
fun Routing.redoc(pageTitle: String = "Docs", specUrl: String = "/openapi.json") { fun Route.redoc(pageTitle: String = "Docs", specUrl: String = "/openapi.json") {
route("/docs") { route("/docs") {
get { get {
call.respondHtml(HttpStatusCode.OK) { call.respondHtml(HttpStatusCode.OK) {

View File

@ -1,35 +1,108 @@
package io.bkbn.kompendium.core.util package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.HeadInfo
import io.bkbn.kompendium.core.metadata.MethodInfo
import io.bkbn.kompendium.core.metadata.MethodInfoWithRequest
import io.bkbn.kompendium.core.metadata.OptionsInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation
import io.bkbn.kompendium.oas.payload.MediaType
import io.bkbn.kompendium.oas.payload.Request
import io.bkbn.kompendium.oas.payload.Response
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.jvm.javaField
import org.slf4j.LoggerFactory
import java.lang.reflect.ParameterizedType
import java.util.Locale
object Helpers { object Helpers {
private const val COMPONENT_SLUG = "#/components/schemas" fun MethodInfo.addToSpec(path: Path, spec: OpenApiSpec, config: SpecConfig) {
SchemaGenerator.fromTypeOrUnit(this.response.responseType, spec.components.schemas)?.let { schema ->
fun KType.getSimpleSlug(): String = when { spec.components.schemas[this.response.responseType.getSimpleSlug()] = schema
this.arguments.isNotEmpty() -> genericNameAdapter(this, classifier as KClass<*>)
else -> (classifier as KClass<*>).simpleName ?: error("Could not determine simple name for $this")
} }
fun KType.getReferenceSlug(): String = when { errors.forEach { error ->
arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}" SchemaGenerator.fromTypeOrUnit(error.responseType, spec.components.schemas)?.let { schema ->
else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).simpleName}" spec.components.schemas[error.responseType.getSimpleSlug()] = schema
}
} }
/** when (this) {
* Adapts a class with type parameters into a reference friendly string is MethodInfoWithRequest -> {
*/ SchemaGenerator.fromTypeOrUnit(this.request.requestType, spec.components.schemas)?.let { schema ->
private fun genericNameAdapter(type: KType, clazz: KClass<*>): String { spec.components.schemas[this.request.requestType.getSimpleSlug()] = schema
val classNames = type.arguments }
.map { it.type?.classifier as KClass<*> } }
.map { it.simpleName }
return classNames.joinToString(separator = "-", prefix = "${clazz.simpleName}-") else -> {}
}
val operations = this.toPathOperation(config)
when (this) {
is DeleteInfo -> path.delete = operations
is GetInfo -> path.get = operations
is HeadInfo -> path.head = operations
is PatchInfo -> path.patch = operations
is PostInfo -> path.post = operations
is PutInfo -> path.put = operations
is OptionsInfo -> path.options = operations
}
}
private fun MethodInfo.toPathOperation(config: SpecConfig) = PathOperation(
tags = config.tags.plus(this.tags),
summary = this.summary,
description = this.description,
externalDocs = this.externalDocumentation,
operationId = this.operationId,
deprecated = this.deprecated,
parameters = this.parameters,
security = config.security
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toList(),
requestBody = when (this) {
is MethodInfoWithRequest -> Request(
description = this.request.description,
content = this.request.requestType.toReferenceContent(this.request.examples),
required = true
)
else -> null
},
responses = mapOf(
this.response.responseCode.value to Response(
description = this.response.description,
content = this.response.responseType.toReferenceContent(this.response.examples)
)
).plus(this.errors.toResponseMap())
)
private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error ->
error.responseCode.value to Response(
description = error.description,
content = error.responseType.toReferenceContent(error.examples)
)
}
private fun KType.toReferenceContent(examples: Map<String, MediaType.Example>?): Map<String, MediaType>? =
when (this.classifier as KClass<*>) {
Unit::class -> null
else -> mapOf(
"application/json" to MediaType(
schema = ReferenceDefinition(this.getReferenceSlug()),
examples = examples
)
)
} }
} }

View File

@ -0,0 +1,23 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.HeadInfo
import io.bkbn.kompendium.core.metadata.OptionsInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.oas.payload.Parameter
interface SpecConfig {
var tags: Set<String>
var parameters: List<Parameter>
var get: GetInfo?
var post: PostInfo?
var put: PutInfo?
var delete: DeleteInfo?
var patch: PatchInfo?
var head: HeadInfo?
var options: OptionsInfo?
var security: Map<String, List<String>>?
}

View File

@ -14,9 +14,13 @@ import io.bkbn.kompendium.core.util.TestModules.singleException
import io.bkbn.kompendium.core.util.TestModules.genericException import io.bkbn.kompendium.core.util.TestModules.genericException
import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponse import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponseMultipleImpls import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponseMultipleImpls
import io.bkbn.kompendium.core.util.TestModules.gnarlyGenericResponse
import io.bkbn.kompendium.core.util.TestModules.headerParameter import io.bkbn.kompendium.core.util.TestModules.headerParameter
import io.bkbn.kompendium.core.util.TestModules.multipleExceptions import io.bkbn.kompendium.core.util.TestModules.multipleExceptions
import io.bkbn.kompendium.core.util.TestModules.nestedGenericCollection
import io.bkbn.kompendium.core.util.TestModules.nestedGenericMultipleParamsCollection
import io.bkbn.kompendium.core.util.TestModules.nestedGenericResponse import io.bkbn.kompendium.core.util.TestModules.nestedGenericResponse
import io.bkbn.kompendium.core.util.TestModules.nestedTypeName
import io.bkbn.kompendium.core.util.TestModules.nonRequiredParam import io.bkbn.kompendium.core.util.TestModules.nonRequiredParam
import io.bkbn.kompendium.core.util.TestModules.polymorphicException import io.bkbn.kompendium.core.util.TestModules.polymorphicException
import io.bkbn.kompendium.core.util.TestModules.notarizedHead import io.bkbn.kompendium.core.util.TestModules.notarizedHead
@ -27,6 +31,7 @@ import io.bkbn.kompendium.core.util.TestModules.notarizedPut
import io.bkbn.kompendium.core.util.TestModules.nullableEnumField import io.bkbn.kompendium.core.util.TestModules.nullableEnumField
import io.bkbn.kompendium.core.util.TestModules.nullableField import io.bkbn.kompendium.core.util.TestModules.nullableField
import io.bkbn.kompendium.core.util.TestModules.nullableNestedObject import io.bkbn.kompendium.core.util.TestModules.nullableNestedObject
import io.bkbn.kompendium.core.util.TestModules.nullableReference
import io.bkbn.kompendium.core.util.TestModules.polymorphicCollectionResponse import io.bkbn.kompendium.core.util.TestModules.polymorphicCollectionResponse
import io.bkbn.kompendium.core.util.TestModules.polymorphicMapResponse import io.bkbn.kompendium.core.util.TestModules.polymorphicMapResponse
import io.bkbn.kompendium.core.util.TestModules.polymorphicResponse import io.bkbn.kompendium.core.util.TestModules.polymorphicResponse
@ -37,6 +42,7 @@ import io.bkbn.kompendium.core.util.TestModules.returnsList
import io.bkbn.kompendium.core.util.TestModules.rootRoute import io.bkbn.kompendium.core.util.TestModules.rootRoute
import io.bkbn.kompendium.core.util.TestModules.simpleGenericResponse import io.bkbn.kompendium.core.util.TestModules.simpleGenericResponse
import io.bkbn.kompendium.core.util.TestModules.simplePathParsing import io.bkbn.kompendium.core.util.TestModules.simplePathParsing
import io.bkbn.kompendium.core.util.TestModules.simpleRecursive
import io.bkbn.kompendium.core.util.TestModules.trailingSlash import io.bkbn.kompendium.core.util.TestModules.trailingSlash
import io.bkbn.kompendium.core.util.TestModules.withOperationId import io.bkbn.kompendium.core.util.TestModules.withOperationId
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
@ -157,6 +163,15 @@ class KompendiumTest : DescribeSpec({
it("Can handle an absolutely psycho inheritance test") { it("Can handle an absolutely psycho inheritance test") {
openApiTestAllSerializers("T0033__crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() } openApiTestAllSerializers("T0033__crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() }
} }
it("Can support nested generic collections") {
openApiTestAllSerializers("T0039__nested_generic_collection.json") { nestedGenericCollection() }
}
it("Can support nested generics with multiple type parameters") {
openApiTestAllSerializers("T0040__nested_generic_multiple_type_params.json") { nestedGenericMultipleParamsCollection() }
}
it("Can handle a really gnarly generic example") {
openApiTestAllSerializers("T0043__gnarly_generic_example.json") { gnarlyGenericResponse() }
}
} }
describe("Miscellaneous") { describe("Miscellaneous") {
xit("Can generate the necessary ReDoc home page") { xit("Can generate the necessary ReDoc home page") {
@ -174,8 +189,8 @@ class KompendiumTest : DescribeSpec({
xit("Can override field name") { xit("Can override field name") {
// TODO Assess strategies here // TODO Assess strategies here
} }
xit("Can serialize a recursive type") { it("Can serialize a recursive type") {
// TODO openApiTestAllSerializers("simple_recursive.json") { simpleRecursive() } openApiTestAllSerializers("T0042__simple_recursive.json") { simpleRecursive() }
} }
it("Nullable fields do not lead to doom") { it("Nullable fields do not lead to doom") {
openApiTestAllSerializers("T0036__nullable_fields.json") { nullableNestedObject() } openApiTestAllSerializers("T0036__nullable_fields.json") { nullableNestedObject() }
@ -183,6 +198,12 @@ class KompendiumTest : DescribeSpec({
it("Can have a nullable enum as a member field") { it("Can have a nullable enum as a member field") {
openApiTestAllSerializers("T0037__nullable_enum_field.json") { nullableEnumField() } openApiTestAllSerializers("T0037__nullable_enum_field.json") { nullableEnumField() }
} }
it("Can have a nullable reference without impacting base type") {
openApiTestAllSerializers("T0041__nullable_reference.json") { nullableReference() }
}
it("Can handle nested type names") {
openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() }
}
} }
describe("Constraints") { describe("Constraints") {
// TODO Assess strategies here // TODO Assess strategies here

View File

@ -1,5 +1,6 @@
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.ColumnSchema import io.bkbn.kompendium.core.fixtures.ColumnSchema
import io.bkbn.kompendium.core.fixtures.ComplexRequest import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.DateTimeString import io.bkbn.kompendium.core.fixtures.DateTimeString
@ -7,9 +8,14 @@ import io.bkbn.kompendium.core.fixtures.DefaultField
import io.bkbn.kompendium.core.fixtures.ExceptionResponse import io.bkbn.kompendium.core.fixtures.ExceptionResponse
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.Gibbity import io.bkbn.kompendium.core.fixtures.Gibbity
import io.bkbn.kompendium.core.fixtures.ManyThings
import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics
import io.bkbn.kompendium.core.fixtures.Nested
import io.bkbn.kompendium.core.fixtures.NullableEnum import io.bkbn.kompendium.core.fixtures.NullableEnum
import io.bkbn.kompendium.core.fixtures.NullableField import io.bkbn.kompendium.core.fixtures.NullableField
import io.bkbn.kompendium.core.fixtures.Page
import io.bkbn.kompendium.core.fixtures.ProfileUpdateRequest import io.bkbn.kompendium.core.fixtures.ProfileUpdateRequest
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
import io.bkbn.kompendium.core.fixtures.TestNested import io.bkbn.kompendium.core.fixtures.TestNested
@ -561,28 +567,40 @@ object TestModules {
fun Routing.simpleGenericResponse() = basicGetGenerator<Gibbity<String>>() fun Routing.simpleGenericResponse() = basicGetGenerator<Gibbity<String>>()
fun Routing.gnarlyGenericResponse() = basicGetGenerator<Foosy<Barzo<Int>, String>>()
fun Routing.nestedGenericResponse() = basicGetGenerator<Gibbity<Map<String, String>>>() fun Routing.nestedGenericResponse() = basicGetGenerator<Gibbity<Map<String, String>>>()
fun Routing.genericPolymorphicResponse() = basicGetGenerator<Flibbity<Double>>() 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.nestedGenericMultipleParamsCollection() = basicGetGenerator<MultiNestedGenerics<String, ComplexRequest>>()
fun Routing.withOperationId() = basicGetGenerator<TestResponse>(operationId = "getThisDude") fun Routing.withOperationId() = basicGetGenerator<TestResponse>(operationId = "getThisDude")
fun Routing.nullableNestedObject() = basicGetGenerator<ProfileUpdateRequest>() fun Routing.nullableNestedObject() = basicGetGenerator<ProfileUpdateRequest>()
fun Routing.nullableEnumField() = basicGetGenerator<NullableEnum>() fun Routing.nullableEnumField() = basicGetGenerator<NullableEnum>()
fun Routing.nullableReference() = basicGetGenerator<ManyThings>()
fun Routing.dateTimeString() = basicGetGenerator<DateTimeString>() fun Routing.dateTimeString() = basicGetGenerator<DateTimeString>()
fun Routing.headerParameter() = basicGetGenerator<TestResponse>( params = listOf( fun Routing.headerParameter() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter( Parameter(
name = "X-User-Email", name = "X-User-Email",
`in` = Parameter.Location.header, `in` = Parameter.Location.header,
schema = TypeDefinition.STRING, schema = TypeDefinition.STRING,
required = true required = true
) )
)) )
)
fun Routing.nestedTypeName() = basicGetGenerator<Nested.Response>()
fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>() fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>()

View File

@ -62,30 +62,7 @@
"type": "null" "type": "null"
}, },
{ {
"type": "object", "$ref": "#/components/schemas/ProfileMetadataUpdateRequest"
"properties": {
"isPrivate": {
"oneOf": [
{
"type": "null"
},
{
"type": "boolean"
}
]
},
"otherThing": {
"oneOf": [
{
"type": "null"
},
{
"type": "string"
}
]
}
},
"required": []
} }
] ]
}, },
@ -112,6 +89,39 @@
} }
}, },
"required": [] "required": []
},
"ProfileMetadataUpdateRequest": {
"oneOf": [
{
"type": "null"
},
{
"type": "object",
"properties": {
"isPrivate": {
"oneOf": [
{
"type": "null"
},
{
"type": "boolean"
}
]
},
"otherThing": {
"oneOf": [
{
"type": "null"
},
{
"type": "string"
}
]
}
},
"required": []
}
]
} }
}, },
"securitySchemes": {} "securitySchemes": {}

View File

@ -1,5 +1,6 @@
{ {
"openapi": "3.0.3", "openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": { "info": {
"title": "Test API", "title": "Test API",
"version": "1.33.7", "version": "1.33.7",
@ -26,72 +27,71 @@
} }
], ],
"paths": { "paths": {
"/test/test/{name}": { "/": {
"post": { "get": {
"tags": [], "tags": [],
"summary": "Location Test", "summary": "Great Summary!",
"description": "A cool test", "description": "testing more",
"parameters": [ "parameters": [],
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"requestBody": {
"description": "Cool stuff",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleRequest"
}
}
},
"required": true
},
"responses": { "responses": {
"200": { "200": {
"description": "A successful endeavor", "description": "A Successful Endeavor",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/SimpleResponse" "$ref": "#/components/schemas/Page-Int"
} }
} }
} }
} }
}, },
"deprecated": false "deprecated": false
} },
"parameters": []
} }
}, },
"webhooks": {},
"components": { "components": {
"schemas": { "schemas": {
"SimpleRequest": { "Page-Int": {
"type": "object",
"properties": { "properties": {
"input": { "content": {
"type": "string" "items": {
"type": "number",
"format": "int32"
},
"type": "array"
},
"number": {
"type": "number",
"format": "int32"
},
"numberOfElements": {
"type": "number",
"format": "int32"
},
"size": {
"type": "number",
"format": "int32"
},
"totalElements": {
"type": "number",
"format": "int64"
},
"totalPages": {
"type": "number",
"format": "int32"
} }
}, },
"required": [ "required": [
"input" "content",
], "number",
"type": "object" "numberOfElements",
}, "size",
"SimpleResponse": { "totalElements",
"properties": { "totalPages"
"result": { ]
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
} }
}, },
"securitySchemes": {} "securitySchemes": {}

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/MultiNestedGenerics-String-ComplexRequest"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"CrazyItem": {
"type": "object",
"properties": {
"enumeration": {
"enum": [
"ONE",
"TWO"
]
}
},
"required": [
"enumeration"
]
},
"NestedComplexItem": {
"type": "object",
"properties": {
"alias": {
"additionalProperties": {
"$ref": "#/components/schemas/CrazyItem"
},
"type": "object"
},
"name": {
"type": "string"
}
},
"required": [
"alias",
"name"
]
},
"ComplexRequest": {
"type": "object",
"properties": {
"amazingField": {
"type": "string"
},
"org": {
"type": "string"
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem"
},
"type": "array"
}
},
"required": [
"amazingField",
"org",
"tables"
]
},
"MultiNestedGenerics-String-ComplexRequest": {
"type": "object",
"properties": {
"content": {
"additionalProperties": {
"$ref": "#/components/schemas/ComplexRequest"
},
"type": "object"
}
},
"required": [
"content"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,5 +1,6 @@
{ {
"openapi": "3.0.3", "openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": { "info": {
"title": "Test API", "title": "Test API",
"version": "1.33.7", "version": "1.33.7",
@ -26,72 +27,68 @@
} }
], ],
"paths": { "paths": {
"/test/test/{name}": { "/": {
"put": { "get": {
"tags": [], "tags": [],
"summary": "Location Test", "summary": "Great Summary!",
"description": "A cool test", "description": "testing more",
"parameters": [ "parameters": [],
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"requestBody": {
"description": "Cool stuff",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleRequest"
}
}
},
"required": true
},
"responses": { "responses": {
"200": { "200": {
"description": "A successful endeavor", "description": "A Successful Endeavor",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/SimpleResponse" "$ref": "#/components/schemas/ManyThings"
} }
} }
} }
} }
}, },
"deprecated": false "deprecated": false
} },
"parameters": []
} }
}, },
"webhooks": {},
"components": { "components": {
"schemas": { "schemas": {
"SimpleRequest": { "Something": {
"type": "object",
"properties": { "properties": {
"input": { "a": {
"type": "string" "type": "string"
},
"b": {
"type": "number",
"format": "int32"
} }
}, },
"required": [ "required": [
"input" "a",
], "b"
"type": "object" ]
}, },
"SimpleResponse": { "ManyThings": {
"type": "object",
"properties": { "properties": {
"result": { "someA": {
"type": "boolean" "$ref": "#/components/schemas/Something"
},
"someB": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/Something"
}
]
} }
}, },
"required": [ "required": [
"result" "someA"
], ]
"type": "object"
} }
}, },
"securitySchemes": {} "securitySchemes": {}

View File

@ -1,5 +1,6 @@
{ {
"openapi": "3.0.3", "openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": { "info": {
"title": "Test API", "title": "Test API",
"version": "1.33.7", "version": "1.33.7",
@ -26,59 +27,64 @@
} }
], ],
"paths": { "paths": {
"/test/test/{name}/nesty": { "/": {
"delete": { "get": {
"tags": [], "tags": [],
"summary": "Location Test", "summary": "Great Summary!",
"description": "A cool test", "description": "testing more",
"parameters": [ "parameters": [],
{
"name": "isCool",
"in": "query",
"schema": {
"type": "boolean"
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": { "responses": {
"200": { "200": {
"description": "A successful endeavor", "description": "A Successful Endeavor",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/SimpleResponse" "$ref": "#/components/schemas/ColumnSchema"
} }
} }
} }
} }
}, },
"deprecated": false "deprecated": false
} },
"parameters": []
} }
}, },
"webhooks": {},
"components": { "components": {
"schemas": { "schemas": {
"SimpleResponse": { "ColumnSchema": {
"type": "object",
"properties": { "properties": {
"result": { "description": {
"type": "boolean" "type": "string"
},
"mode": {
"enum": [
"NULLABLE",
"REQUIRED",
"REPEATED"
]
},
"name": {
"type": "string"
},
"subColumns": {
"items": {
"$ref": "#/components/schemas/ColumnSchema"
},
"type": "array"
},
"type": {
"type": "string"
} }
}, },
"required": [ "required": [
"result" "description",
], "mode",
"type": "object" "name",
"type"
]
} }
}, },
"securitySchemes": {} "securitySchemes": {}

View File

@ -1,5 +1,6 @@
{ {
"openapi": "3.0.3", "openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": { "info": {
"title": "Test API", "title": "Test API",
"version": "1.33.7", "version": "1.33.7",
@ -26,59 +27,61 @@
} }
], ],
"paths": { "paths": {
"/test/test/{name}/nesty": { "/": {
"get": { "get": {
"tags": [], "tags": [],
"summary": "Location Test", "summary": "Great Summary!",
"description": "A cool test", "description": "testing more",
"parameters": [ "parameters": [],
{
"name": "isCool",
"in": "query",
"schema": {
"type": "boolean"
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": { "responses": {
"200": { "200": {
"description": "A successful endeavor", "description": "A Successful Endeavor",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/SimpleResponse" "$ref": "#/components/schemas/Foosy-Barzo-String"
} }
} }
} }
} }
}, },
"deprecated": false "deprecated": false
} },
"parameters": []
} }
}, },
"webhooks": {},
"components": { "components": {
"schemas": { "schemas": {
"SimpleResponse": { "Foosy-Barzo-String": {
"type": "object",
"properties": {
"otherThing": {
"items": {
"type": "string"
},
"type": "array"
},
"test": {
"$ref": "#/components/schemas/Barzo-Int"
}
},
"required": [
"otherThing",
"test"
]
},
"Barzo-Int": {
"type": "object",
"properties": { "properties": {
"result": { "result": {
"type": "boolean" "type": "number",
"format": "int32"
} }
}, },
"required": [ "required": [
"result" "result"
], ]
"type": "object"
} }
}, },
"securitySchemes": {} "securitySchemes": {}

View File

@ -1,5 +1,6 @@
{ {
"openapi": "3.0.3", "openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": { "info": {
"title": "Test API", "title": "Test API",
"version": "1.33.7", "version": "1.33.7",
@ -26,50 +27,42 @@
} }
], ],
"paths": { "paths": {
"/test/test/{name}": { "/": {
"get": { "get": {
"tags": [], "tags": [],
"summary": "Location Test", "summary": "Great Summary!",
"description": "A cool test", "description": "testing more",
"parameters": [ "parameters": [],
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": { "responses": {
"200": { "200": {
"description": "A successful endeavor", "description": "A Successful Endeavor",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/SimpleResponse" "$ref": "#/components/schemas/NestedResponse"
} }
} }
} }
} }
}, },
"deprecated": false "deprecated": false
} },
"parameters": []
} }
}, },
"webhooks": {},
"components": { "components": {
"schemas": { "schemas": {
"SimpleResponse": { "NestedResponse": {
"type": "object",
"properties": { "properties": {
"result": { "idk": {
"type": "boolean" "type": "boolean"
} }
}, },
"required": [ "required": [
"result" "idk"
], ]
"type": "object"
} }
}, },
"securitySchemes": {} "securitySchemes": {}

View File

@ -23,6 +23,7 @@ import io.ktor.http.HttpStatusCode
import io.ktor.serialization.gson.gson import io.ktor.serialization.gson.gson
import io.ktor.serialization.jackson.jackson 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.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.server.testing.ApplicationTestBuilder
@ -63,17 +64,19 @@ object TestHelpers {
fun openApiTestAllSerializers( fun openApiTestAllSerializers(
snapshotName: String, snapshotName: String,
customTypes: Map<KType, JsonSchema> = emptyMap(), customTypes: Map<KType, JsonSchema> = emptyMap(),
applicationSetup: Application.() -> Unit = { },
routeUnderTest: Routing.() -> Unit routeUnderTest: Routing.() -> Unit
) { ) {
openApiTest(snapshotName, SupportedSerializer.KOTLINX, routeUnderTest, customTypes) openApiTest(snapshotName, SupportedSerializer.KOTLINX, routeUnderTest, applicationSetup, customTypes)
openApiTest(snapshotName, SupportedSerializer.JACKSON, routeUnderTest, customTypes) openApiTest(snapshotName, SupportedSerializer.JACKSON, routeUnderTest, applicationSetup, customTypes)
openApiTest(snapshotName, SupportedSerializer.GSON, routeUnderTest, customTypes) openApiTest(snapshotName, SupportedSerializer.GSON, routeUnderTest, applicationSetup, customTypes)
} }
private fun openApiTest( private fun openApiTest(
snapshotName: String, snapshotName: String,
serializer: SupportedSerializer, serializer: SupportedSerializer,
routeUnderTest: Routing.() -> Unit, routeUnderTest: Routing.() -> Unit,
applicationSetup: Application.() -> Unit,
typeOverrides: Map<KType, JsonSchema> = emptyMap() typeOverrides: Map<KType, JsonSchema> = emptyMap()
) = testApplication { ) = testApplication {
install(NotarizedApplication()) { install(NotarizedApplication()) {
@ -95,6 +98,7 @@ object TestHelpers {
} }
} }
} }
application(applicationSetup)
routing { routing {
redoc() redoc()
routeUnderTest() routeUnderTest()

View File

@ -118,3 +118,31 @@ public data class ProfileMetadataUpdateRequest(
public val isPrivate: Boolean?, public val isPrivate: Boolean?,
public val otherThing: String? public val otherThing: String?
) )
data class Page<T>(
val content: List<T>,
val totalElements: Long,
val totalPages: Int,
val numberOfElements: Int,
val number: Int,
val size: Int
)
data class MultiNestedGenerics<T, E>(
val content: Map<T, E>
)
data class Something(val a: String, val b: Int)
data class ManyThings(
val someA: Something,
val someB: Something?
)
data class Foosy<T, K>(val test: T, val otherThing: List<K>)
data class Barzo<G>(val result: G)
object Nested {
@Serializable
data class Response(val idk: Boolean)
}

View File

@ -1 +1 @@
<meta http-equiv="refresh" content="0; url=./2.3.4" /> <meta http-equiv="refresh" content="0; url=./3.0.0-beta-SNAPSHOT" />

View File

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

3
json-schema/Module.md Normal file
View File

@ -0,0 +1,3 @@
# Module kompendium-json-schema
This module handles converting Kotlin data classes to compliant [JsonSchema](https://json-schema.org)

View File

@ -14,6 +14,7 @@ import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
import java.util.UUID
object SchemaGenerator { object SchemaGenerator {
inline fun <reified T : Any?> fromTypeToSchema(cache: MutableMap<String, JsonSchema> = mutableMapOf()) = inline fun <reified T : Any?> fromTypeToSchema(cache: MutableMap<String, JsonSchema> = mutableMapOf()) =
@ -37,6 +38,7 @@ object SchemaGenerator {
Float::class -> checkForNull(type, TypeDefinition.FLOAT) Float::class -> checkForNull(type, TypeDefinition.FLOAT)
String::class -> checkForNull(type, TypeDefinition.STRING) String::class -> checkForNull(type, TypeDefinition.STRING)
Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN) Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN)
UUID::class -> checkForNull(type, TypeDefinition.UUID)
else -> when { else -> when {
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz) clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz)
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache) clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache)

View File

@ -40,6 +40,11 @@ data class TypeDefinition(
type = "string" type = "string"
) )
val UUID = TypeDefinition(
type = "string",
format = "uuid"
)
val BOOLEAN = TypeDefinition( val BOOLEAN = TypeDefinition(
type = "boolean" type = "boolean"
) )

View File

@ -13,21 +13,32 @@ import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.KTypeParameter import kotlin.reflect.KTypeParameter
import kotlin.reflect.KTypeProjection import kotlin.reflect.KTypeProjection
import kotlin.reflect.full.createType
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor import kotlin.reflect.full.primaryConstructor
object SimpleObjectHandler { object SimpleObjectHandler {
fun handle(type: KType, clazz: KClass<*>, cache: MutableMap<String, JsonSchema>): JsonSchema { fun handle(type: KType, clazz: KClass<*>, cache: MutableMap<String, JsonSchema>): JsonSchema {
// cache[type.getSimpleSlug()] = ReferenceDefinition("RECURSION_PLACEHOLDER")
cache[type.getSimpleSlug()] = ReferenceDefinition(type.getReferenceSlug())
val typeMap = clazz.typeParameters.zip(type.arguments).toMap() val typeMap = clazz.typeParameters.zip(type.arguments).toMap()
val props = clazz.memberProperties.associate { prop -> val props = clazz.memberProperties.associate { prop ->
val schema = when (typeMap.containsKey(prop.returnType.classifier)) { val schema = when (prop.needsToInjectGenerics(typeMap)) {
true -> handleNestedGenerics(typeMap, prop, cache)
false -> when (typeMap.containsKey(prop.returnType.classifier)) {
true -> handleGenericProperty(prop, typeMap, cache) true -> handleGenericProperty(prop, typeMap, cache)
false -> handleProperty(prop, cache) false -> handleProperty(prop, cache)
} }
}
prop.name to schema val nullCheckSchema = when (prop.returnType.isMarkedNullable && !schema.isNullable()) {
true -> OneOfDefinition(NullableDefinition(), schema)
false -> schema
}
prop.name to nullCheckSchema
} }
val required = clazz.memberProperties.filterNot { prop -> prop.returnType.isMarkedNullable } val required = clazz.memberProperties.filterNot { prop -> prop.returnType.isMarkedNullable }
@ -48,6 +59,34 @@ object SimpleObjectHandler {
} }
} }
private fun KProperty<*>.needsToInjectGenerics(
typeMap: Map<KTypeParameter, KTypeProjection>
): Boolean {
val typeSymbols = returnType.arguments.map { it.type.toString() }
return typeMap.any { (k, _) -> typeSymbols.contains(k.name) }
}
private fun handleNestedGenerics(
typeMap: Map<KTypeParameter, KTypeProjection>,
prop: KProperty<*>,
cache: MutableMap<String, JsonSchema>
): JsonSchema {
val propClass = prop.returnType.classifier as KClass<*>
val types = prop.returnType.arguments.map {
val typeSymbol = it.type.toString()
typeMap.filterKeys { k -> k.name == typeSymbol }.values.first()
}
val constructedType = propClass.createType(types)
return SchemaGenerator.fromTypeToSchema(constructedType, cache).let {
if (it.isOrContainsObjectDef()) {
cache[constructedType.getSimpleSlug()] = it
ReferenceDefinition(prop.returnType.getReferenceSlug())
} else {
it
}
}
}
private fun handleGenericProperty( private fun handleGenericProperty(
prop: KProperty<*>, prop: KProperty<*>,
typeMap: Map<KTypeParameter, KTypeProjection>, typeMap: Map<KTypeParameter, KTypeProjection>,
@ -55,9 +94,9 @@ object SimpleObjectHandler {
): JsonSchema { ): JsonSchema {
val type = typeMap[prop.returnType.classifier]?.type!! val type = typeMap[prop.returnType.classifier]?.type!!
return SchemaGenerator.fromTypeToSchema(type, cache).let { return SchemaGenerator.fromTypeToSchema(type, cache).let {
if (it is TypeDefinition && it.type == "object") { if (it.isOrContainsObjectDef()) {
cache[type.getSimpleSlug()] = it cache[type.getSimpleSlug()] = it
ReferenceDefinition(prop.returnType.getReferenceSlug()) ReferenceDefinition(type.getReferenceSlug())
} else { } else {
it it
} }
@ -66,11 +105,19 @@ object SimpleObjectHandler {
private fun handleProperty(prop: KProperty<*>, cache: MutableMap<String, JsonSchema>): JsonSchema = private fun handleProperty(prop: KProperty<*>, cache: MutableMap<String, JsonSchema>): JsonSchema =
SchemaGenerator.fromTypeToSchema(prop.returnType, cache).let { SchemaGenerator.fromTypeToSchema(prop.returnType, cache).let {
if (it is TypeDefinition && it.type == "object") { if (it.isOrContainsObjectDef()) {
cache[prop.returnType.getSimpleSlug()] = it cache[prop.returnType.getSimpleSlug()] = it
ReferenceDefinition(prop.returnType.getReferenceSlug()) ReferenceDefinition(prop.returnType.getReferenceSlug())
} else { } else {
it it
} }
} }
private fun JsonSchema.isOrContainsObjectDef(): Boolean {
val isTypeDef = this is TypeDefinition && type == "object"
val isTypeDefOneOf = this is OneOfDefinition && this.oneOf.any { js -> js is TypeDefinition && js.type == "object" }
return isTypeDef || isTypeDefOneOf
}
private fun JsonSchema.isNullable(): Boolean = this is OneOfDefinition && this.oneOf.any{ it is NullableDefinition }
} }

View File

@ -9,21 +9,26 @@ object Helpers {
fun KType.getSimpleSlug(): String = when { fun KType.getSimpleSlug(): String = when {
this.arguments.isNotEmpty() -> genericNameAdapter(this, classifier as KClass<*>) this.arguments.isNotEmpty() -> genericNameAdapter(this, classifier as KClass<*>)
else -> (classifier as KClass<*>).simpleName ?: error("Could not determine simple name for $this") else -> (classifier as KClass<*>).kompendiumSlug() ?: error("Could not determine simple name for $this")
} }
fun KType.getReferenceSlug(): String = when { fun KType.getReferenceSlug(): String = when {
arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}" arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}"
else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).simpleName}" else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).kompendiumSlug()}"
}
@Suppress("ReturnCount")
private fun KClass<*>.kompendiumSlug(): String? {
if (java.packageName == "java.lang") return simpleName
if (java.packageName == "java.util") return simpleName
val pkg = java.packageName
return qualifiedName?.replace(pkg, "")?.replace(".", "")
} }
/**
* Adapts a class with type parameters into a reference friendly string
*/
private fun genericNameAdapter(type: KType, clazz: KClass<*>): String { private fun genericNameAdapter(type: KType, clazz: KClass<*>): String {
val classNames = type.arguments val classNames = type.arguments
.map { it.type?.classifier as KClass<*> } .map { it.type?.classifier as KClass<*> }
.map { it.simpleName } .map { it.kompendiumSlug() }
return classNames.joinToString(separator = "-", prefix = "${clazz.simpleName}-") return classNames.joinToString(separator = "-", prefix = "${clazz.kompendiumSlug()}-")
} }
} }

View File

@ -12,6 +12,7 @@ import io.kotest.assertions.json.shouldEqualJson
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 kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.util.UUID
class SchemaGeneratorTest : DescribeSpec({ class SchemaGeneratorTest : DescribeSpec({
describe("Scalars") { describe("Scalars") {
@ -24,6 +25,9 @@ class SchemaGeneratorTest : DescribeSpec({
it("Can generate the schema for a String") { it("Can generate the schema for a String") {
jsonSchemaTest<String>("T0003__scalar_string.json") jsonSchemaTest<String>("T0003__scalar_string.json")
} }
it("Can generate the schema for a UUID") {
jsonSchemaTest<UUID>("T0017__scalar_uuid.json")
}
} }
describe("Objects") { describe("Objects") {
it("Can generate the schema for a simple object") { it("Can generate the schema for a simple object") {

View File

@ -0,0 +1,4 @@
{
"type": "string",
"format": "uuid"
}

View File

@ -1,4 +1,3 @@
# Module kompendium-locations # Module kompendium-locations
Adds support for Ktor [Locations](https://ktor.io/docs/locations.html) API. Any notarized location _must_ be provided Adds support for Ktor [Locations](https://ktor.io/docs/locations.html) API.
with a `TParam` annotated with `@Location`. Nested Locations are supported

View File

@ -1,84 +0,0 @@
package io.bkbn.kompendium.locations
//import io.bkbn.kompendium.annotations.Param
//import io.bkbn.kompendium.core.Kompendium
//import io.bkbn.kompendium.core.metadata.method.MethodInfo
//import io.bkbn.kompendium.core.parser.IMethodParser
//import io.bkbn.kompendium.oas.path.Path
//import io.bkbn.kompendium.oas.path.PathOperation
//import io.bkbn.kompendium.oas.payload.Parameter
//import io.ktor.application.feature
//import io.ktor.locations.KtorExperimentalLocationsAPI
//import io.ktor.locations.Location
//import io.ktor.routing.Route
//import io.ktor.routing.application
//import kotlin.reflect.KAnnotatedElement
//import kotlin.reflect.KClass
//import kotlin.reflect.KClassifier
//import kotlin.reflect.KType
//import kotlin.reflect.full.createType
//import kotlin.reflect.full.findAnnotation
//import kotlin.reflect.full.hasAnnotation
//import kotlin.reflect.full.memberProperties
//
//@OptIn(KtorExperimentalLocationsAPI::class)
//object LocationMethodParser : IMethodParser {
// override fun KType.toParameterSpec(info: MethodInfo<*, *>, feature: Kompendium): List<Parameter> {
// val clazzList = determineLocationParents(classifier!!)
// return clazzList.associateWith { it.memberProperties }
// .flatMap { (clazz, memberProperties) -> memberProperties.associateWith { clazz }.toList() }
// .filter { (prop, _) -> prop.hasAnnotation<Param>() }
// .map { (prop, clazz) -> prop.toParameter(info, clazz.createType(), clazz, feature) }
// }
//
// private fun determineLocationParents(classifier: KClassifier): List<KClass<*>> {
// var clazz: KClass<*>? = classifier as KClass<*>
// val clazzList = mutableListOf<KClass<*>>()
// while (clazz != null) {
// clazzList.add(clazz)
// clazz = getLocationParent(clazz)
// }
// return clazzList
// }
//
// private fun getLocationParent(clazz: KClass<*>): KClass<*>? {
// val parent = clazz.memberProperties
// .find { (it.returnType.classifier as KAnnotatedElement).hasAnnotation<Location>() }
// return parent?.returnType?.classifier as? KClass<*>
// }
//
// fun KClass<*>.calculateLocationPath(suffix: String = ""): String {
// val locationAnnotation = this.findAnnotation<Location>()
// require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
// val parent = this.java.declaringClass?.kotlin?.takeIf { it.hasAnnotation<Location>() }
// val newSuffix = locationAnnotation.path.plus(suffix)
// return when (parent) {
// null -> newSuffix
// else -> parent.calculateLocationPath(newSuffix)
// }
// }
//
// inline fun <reified TParam : Any> processBaseInfo(
// paramType: KType,
// requestType: KType,
// responseType: KType,
// info: MethodInfo<*, *>,
// route: Route
// ): LocationBaseInfo {
// val locationAnnotation = TParam::class.findAnnotation<Location>()
// require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
// val path = route.calculateRoutePath()
// val locationPath = TParam::class.calculateLocationPath()
// val pathWithLocation = path.plus(locationPath)
// val feature = route.application.feature(Kompendium)
// feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
// val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
// return LocationBaseInfo(baseInfo, feature, pathWithLocation)
// }
//
// data class LocationBaseInfo(
// val op: PathOperation,
// val feature: Kompendium,
// val path: String
// )
//}

View File

@ -1,110 +0,0 @@
package io.bkbn.kompendium.locations
//import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight
//import io.bkbn.kompendium.core.metadata.method.DeleteInfo
//import io.bkbn.kompendium.core.metadata.method.GetInfo
//import io.bkbn.kompendium.core.metadata.method.PostInfo
//import io.bkbn.kompendium.core.metadata.method.PutInfo
//import io.bkbn.kompendium.oas.path.PathOperation
//import io.ktor.application.ApplicationCall
//import io.ktor.http.HttpMethod
//import io.ktor.locations.KtorExperimentalLocationsAPI
//import io.ktor.locations.handle
//import io.ktor.locations.location
//import io.ktor.routing.Route
//import io.ktor.routing.method
//import io.ktor.util.pipeline.PipelineContext
//
///**
// * This version of notarized routes leverages the Ktor [io.ktor.locations.Locations] plugin to provide type safe access
// * to all path and query parameters.
// */
//@KtorExperimentalLocationsAPI
//object NotarizedLocation {
//
// /**
// * Notarization for an HTTP GET request leveraging the Ktor [io.ktor.locations.Locations] plugin
// * @param TParam The class containing all parameter fields.
// * Each field must be annotated with @[io.bkbn.kompendium.annotations.Param].
// * Additionally, the class must be annotated with @[io.ktor.locations.Location].
// * @param TResp Class detailing the expected API response
// * @param info Route metadata
// * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
// */
// inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
// info: GetInfo<TParam, TResp>,
// postProcess: (PathOperation) -> PathOperation = { p -> p },
// noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
// ): Route = methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
// val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
// lbi.feature.config.spec.paths[lbi.path]?.get = postProcess(lbi.op)
// return location(TParam::class) {
// method(HttpMethod.Get) { handle(body) }
// }
// }
//
// /**
// * Notarization for an HTTP POST request leveraging the Ktor [io.ktor.locations.Locations] plugin
// * @param TParam The class containing all parameter fields.
// * Each field must be annotated with @[io.bkbn.kompendium.annotations.Param]
// * Additionally, the class must be annotated with @[io.ktor.locations.Location].
// * @param TReq Class detailing the expected API request body
// * @param TResp Class detailing the expected API response
// * @param info Route metadata
// * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
// */
// inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
// info: PostInfo<TParam, TReq, TResp>,
// postProcess: (PathOperation) -> PathOperation = { p -> p },
// noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
// ): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
// val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
// lbi.feature.config.spec.paths[lbi.path]?.post = postProcess(lbi.op)
// return location(TParam::class) {
// method(HttpMethod.Post) { handle(body) }
// }
// }
//
// /**
// * Notarization for an HTTP Delete request leveraging the Ktor [io.ktor.locations.Locations] plugin
// * @param TParam The class containing all parameter fields.
// * Each field must be annotated with @[io.bkbn.kompendium.annotations.Param]
// * Additionally, the class must be annotated with @[io.ktor.locations.Location].
// * @param TReq Class detailing the expected API request body
// * @param TResp Class detailing the expected API response
// * @param info Route metadata
// * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
// */
// inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
// info: PutInfo<TParam, TReq, TResp>,
// postProcess: (PathOperation) -> PathOperation = { p -> p },
// noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
// ): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
// val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
// lbi.feature.config.spec.paths[lbi.path]?.put = postProcess(lbi.op)
// return location(TParam::class) {
// method(HttpMethod.Put) { handle(body) }
// }
// }
//
// /**
// * Notarization for an HTTP POST request leveraging the Ktor [io.ktor.locations.Locations] plugin
// * @param TParam The class containing all parameter fields.
// * Each field must be annotated with @[io.bkbn.kompendium.annotations.Param]
// * Additionally, the class must be annotated with @[io.ktor.locations.Location].
// * @param TResp Class detailing the expected API response
// * @param info Route metadata
// * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
// */
// inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
// info: DeleteInfo<TParam, TResp>,
// postProcess: (PathOperation) -> PathOperation = { p -> p },
// noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
// ): Route = methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
// val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
// lbi.feature.config.spec.paths[lbi.path]?.delete = postProcess(lbi.op)
// return location(TParam::class) {
// method(HttpMethod.Delete) { handle(body) }
// }
// }
//}

View File

@ -0,0 +1,79 @@
package io.bkbn.kompendium.locations
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.HeadInfo
import io.bkbn.kompendium.core.metadata.OptionsInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.util.Helpers.addToSpec
import io.bkbn.kompendium.core.util.SpecConfig
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.locations.KtorExperimentalLocationsAPI
import io.ktor.server.locations.Location
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
object NotarizedLocations {
data class LocationMetadata(
override var tags: Set<String> = emptySet(),
override var parameters: List<Parameter> = emptyList(),
override var get: GetInfo? = null,
override var post: PostInfo? = null,
override var put: PutInfo? = null,
override var delete: DeleteInfo? = null,
override var patch: PatchInfo? = null,
override var head: HeadInfo? = null,
override var options: OptionsInfo? = null,
override var security: Map<String, List<String>>? = null,
) : SpecConfig
class Config {
lateinit var locations: Map<KClass<*>, LocationMetadata>
}
operator fun invoke() = createApplicationPlugin(
name = "NotarizedLocations",
createConfiguration = ::Config
) {
val spec = application.attributes[KompendiumAttributes.openApiSpec]
pluginConfig.locations.forEach { (k, v) ->
val path = Path()
path.parameters = v.parameters
v.get?.addToSpec(path, spec, v)
v.delete?.addToSpec(path, spec, v)
v.head?.addToSpec(path, spec, v)
v.options?.addToSpec(path, spec, v)
v.post?.addToSpec(path, spec, v)
v.put?.addToSpec(path, spec, v)
v.patch?.addToSpec(path, spec, v)
val location = k.getLocationFromClass()
spec.paths[location] = path
}
}
@OptIn(KtorExperimentalLocationsAPI::class)
private fun KClass<*>.getLocationFromClass(): String {
// todo if parent
val location = findAnnotation<Location>()
?: error("Cannot notarize a location without annotating with @Location")
val path = location.path
val parent = memberProperties.map { it.returnType.classifier as KClass<*> }.find { it.hasAnnotation<Location>() }
return if (parent == null) {
path
} else {
parent.getLocationFromClass() + path
}
}
}

View File

@ -1,82 +1,119 @@
package io.bkbn.kompendium.locations package io.bkbn.kompendium.locations
//import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers import Listing
//import io.bkbn.kompendium.locations.util.locationsConfig import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers
//import io.bkbn.kompendium.locations.util.notarizedDeleteNestedLocation import io.bkbn.kompendium.core.fixtures.TestResponse
//import io.bkbn.kompendium.locations.util.notarizedDeleteSimpleLocation import io.bkbn.kompendium.core.metadata.GetInfo
//import io.bkbn.kompendium.locations.util.notarizedGetNestedLocation import io.bkbn.kompendium.json.schema.definition.TypeDefinition
//import io.bkbn.kompendium.locations.util.notarizedGetNestedLocationFromNonLocationClass import io.bkbn.kompendium.oas.payload.Parameter
//import io.bkbn.kompendium.locations.util.notarizedGetSimpleLocation import io.kotest.core.spec.style.DescribeSpec
//import io.bkbn.kompendium.locations.util.notarizedPostNestedLocation import io.ktor.http.HttpStatusCode
//import io.bkbn.kompendium.locations.util.notarizedPostSimpleLocation import io.ktor.server.application.call
//import io.bkbn.kompendium.locations.util.notarizedPutNestedLocation import io.ktor.server.application.install
//import io.bkbn.kompendium.locations.util.notarizedPutSimpleLocation import io.ktor.server.locations.Locations
//import io.kotest.core.spec.style.DescribeSpec import io.ktor.server.locations.get
// import io.ktor.server.response.respondText
//class KompendiumLocationsTest : DescribeSpec({
// describe("Locations") { class KompendiumLocationsTest : DescribeSpec({
// it("Can notarize a get request with a simple location") { describe("Location Tests") {
// // act it("Can notarize a simple location") {
// openApiTestAllSerializers("notarized_get_simple_location.json") { openApiTestAllSerializers(
// locationsConfig() snapshotName = "T0001__simple_location.json",
// notarizedGetSimpleLocation() applicationSetup = {
// } install(Locations)
// } install(NotarizedLocations()) {
// it("Can notarize a get request with a nested location") { locations = mapOf(
// // act Listing::class to NotarizedLocations.LocationMetadata(
// openApiTestAllSerializers("notarized_get_nested_location.json") { parameters = listOf(
// locationsConfig() Parameter(
// notarizedGetNestedLocation() name = "name",
// } `in` = Parameter.Location.path,
// } schema = TypeDefinition.STRING
// it("Can notarize a post with a simple location") { ),
// // act Parameter(
// openApiTestAllSerializers("notarized_post_simple_location.json") { name = "page",
// locationsConfig() `in` = Parameter.Location.path,
// notarizedPostSimpleLocation() schema = TypeDefinition.INT
// } )
// } ),
// it("Can notarize a post with a nested location") { get = GetInfo.builder {
// // act summary("Location")
// openApiTestAllSerializers("notarized_post_nested_location.json") { description("example location")
// locationsConfig() response {
// notarizedPostNestedLocation() responseCode(HttpStatusCode.OK)
// } responseType<TestResponse>()
// } description("does great things")
// it("Can notarize a put with a simple location") { }
// // act }
// openApiTestAllSerializers("notarized_put_simple_location.json") { ),
// locationsConfig() )
// notarizedPutSimpleLocation() }
// } }
// } ) {
// it("Can notarize a put with a nested location") { get<Listing> { listing ->
// // act call.respondText("Listing ${listing.name}, page ${listing.page}")
// openApiTestAllSerializers("notarized_put_nested_location.json") { }
// locationsConfig() }
// notarizedPutNestedLocation() }
// } it("Can notarize nested locations") {
// } openApiTestAllSerializers(
// it("Can notarize a delete with a simple location") { snapshotName = "T0002__nested_locations.json",
// // act applicationSetup = {
// openApiTestAllSerializers("notarized_delete_simple_location.json") { install(Locations)
// locationsConfig() install(NotarizedLocations()) {
// notarizedDeleteSimpleLocation() locations = mapOf(
// } Type.Edit::class to NotarizedLocations.LocationMetadata(
// } parameters = listOf(
// it("Can notarize a delete with a nested location") { Parameter(
// // act name = "name",
// openApiTestAllSerializers("notarized_delete_nested_location.json") { `in` = Parameter.Location.path,
// locationsConfig() schema = TypeDefinition.STRING
// notarizedDeleteNestedLocation() )
// } ),
// } get = GetInfo.builder {
// it("Can notarize a get with a nested location nested in a non-location class") { summary("Edit")
// // act description("example location")
// openApiTestAllSerializers("notarized_get_nested_location_from_non_location_class.json") { response {
// locationsConfig() responseCode(HttpStatusCode.OK)
// notarizedGetNestedLocationFromNonLocationClass() responseType<TestResponse>()
// } description("does great things")
// } }
// } }
//}) ),
Type.Other::class to NotarizedLocations.LocationMetadata(
parameters = listOf(
Parameter(
name = "name",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
),
Parameter(
name = "page",
`in` = Parameter.Location.path,
schema = TypeDefinition.INT
)
),
get = GetInfo.builder {
summary("Other")
description("example location")
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description("does great things")
}
}
),
)
}
}
) {
get<Type.Edit> { edit ->
call.respondText("Listing ${edit.parent.name}")
}
get<Type.Other> { other ->
call.respondText("Listing ${other.parent.name}, page ${other.page}")
}
}
}
}
})

View File

@ -1,22 +1,12 @@
package io.bkbn.kompendium.locations.util import io.ktor.server.locations.Location
//import io.bkbn.kompendium.annotations.Param @Location("/list/{name}/page/{page}")
//import io.bkbn.kompendium.annotations.ParamType data class Listing(val name: String, val page: Int)
//import io.ktor.locations.Location
// @Location("/type/{name}")
//@Location("/test/{name}") data class Type(val name: String) {
//data class SimpleLoc(@Param(ParamType.PATH) val name: String) { @Location("/edit")
// @Location("/nesty") data class Edit(val parent: Type)
// data class NestedLoc(@Param(ParamType.QUERY) val isCool: Boolean, val parent: SimpleLoc) @Location("/other/{page}")
//} data class Other(val parent: Type, val page: Int)
// }
//object NonLocationObject {
// @Location("/test/{name}")
// data class SimpleLoc(@Param(ParamType.PATH) val name: String) {
// @Location("/nesty")
// data class NestedLoc(@Param(ParamType.QUERY) val isCool: Boolean, val parent: SimpleLoc)
// }
//}
//
//data class SimpleResponse(val result: Boolean)
//data class SimpleRequest(val input: String)

View File

@ -1,107 +0,0 @@
package io.bkbn.kompendium.locations.util
//import io.bkbn.kompendium.locations.NotarizedLocation.notarizedDelete
//import io.bkbn.kompendium.locations.NotarizedLocation.notarizedGet
//import io.bkbn.kompendium.locations.NotarizedLocation.notarizedPost
//import io.bkbn.kompendium.locations.NotarizedLocation.notarizedPut
//import io.ktor.application.Application
//import io.ktor.application.call
//import io.ktor.application.install
//import io.ktor.locations.Locations
//import io.ktor.response.respondText
//import io.ktor.routing.route
//import io.ktor.routing.routing
//
//fun Application.locationsConfig() {
// install(Locations)
//}
//
//fun Application.notarizedGetSimpleLocation() {
// routing {
// route("/test") {
// notarizedGet(TestResponseInfo.testGetSimpleLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedGetNestedLocation() {
// routing {
// route("/test") {
// notarizedGet(TestResponseInfo.testGetNestedLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedPostSimpleLocation() {
// routing {
// route("/test") {
// notarizedPost(TestResponseInfo.testPostSimpleLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedPostNestedLocation() {
// routing {
// route("/test") {
// notarizedPost(TestResponseInfo.testPostNestedLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedPutSimpleLocation() {
// routing {
// route("/test") {
// notarizedPut(TestResponseInfo.testPutSimpleLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedPutNestedLocation() {
// routing {
// route("/test") {
// notarizedPut(TestResponseInfo.testPutNestedLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedDeleteSimpleLocation() {
// routing {
// route("/test") {
// notarizedDelete(TestResponseInfo.testDeleteSimpleLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedDeleteNestedLocation() {
// routing {
// route("/test") {
// notarizedDelete(TestResponseInfo.testDeleteNestedLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedGetNestedLocationFromNonLocationClass() {
// routing {
// route("/test") {
// notarizedGet(TestResponseInfo.testGetNestedLocationFromNonLocationClass) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}

View File

@ -1,97 +0,0 @@
package io.bkbn.kompendium.locations.util
//import io.bkbn.kompendium.core.legacy.metadata.RequestInfo
//import io.bkbn.kompendium.core.legacy.metadata.ResponseInfo
//import io.bkbn.kompendium.core.legacy.metadata.method.DeleteInfo
//import io.bkbn.kompendium.core.legacy.metadata.method.GetInfo
//import io.bkbn.kompendium.core.legacy.metadata.method.PostInfo
//import io.bkbn.kompendium.core.legacy.metadata.method.PutInfo
//import io.ktor.http.HttpStatusCode
//
//object TestResponseInfo {
// val testGetSimpleLocation = GetInfo<SimpleLoc, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testPostSimpleLocation = PostInfo<SimpleLoc, SimpleRequest, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// requestInfo = RequestInfo(
// description = "Cool stuff"
// ),
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testPutSimpleLocation = PutInfo<SimpleLoc, SimpleRequest, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// requestInfo = RequestInfo(
// description = "Cool stuff"
// ),
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testDeleteSimpleLocation = DeleteInfo<SimpleLoc, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testGetNestedLocation = GetInfo<SimpleLoc.NestedLoc, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testPostNestedLocation = PostInfo<SimpleLoc.NestedLoc, SimpleRequest, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// requestInfo = RequestInfo(
// description = "Cool stuff"
// ),
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testPutNestedLocation = PutInfo<SimpleLoc.NestedLoc, SimpleRequest, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// requestInfo = RequestInfo(
// description = "Cool stuff"
// ),
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testDeleteNestedLocation = DeleteInfo<SimpleLoc.NestedLoc, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
//
// val testGetNestedLocationFromNonLocationClass = GetInfo<NonLocationObject.SimpleLoc.NestedLoc, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
//}

View File

@ -1,5 +1,6 @@
{ {
"openapi": "3.0.3", "openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": { "info": {
"title": "Test API", "title": "Test API",
"version": "1.33.7", "version": "1.33.7",
@ -26,21 +27,27 @@
} }
], ],
"paths": { "paths": {
"/test/test/{name}/nesty": { "/list/{name}/page/{page}": {
"get": { "get": {
"tags": [], "tags": [],
"summary": "Location Test", "summary": "Location",
"description": "A cool test", "description": "example location",
"parameters": [ "parameters": [],
{ "responses": {
"name": "isCool", "200": {
"in": "query", "description": "does great things",
"content": {
"application/json": {
"schema": { "schema": {
"type": "boolean" "$ref": "#/components/schemas/TestResponse"
}
}
}
}
}, },
"required": true,
"deprecated": false "deprecated": false
}, },
"parameters": [
{ {
"name": "name", "name": "name",
"in": "path", "in": "path",
@ -49,36 +56,33 @@
}, },
"required": true, "required": true,
"deprecated": false "deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
}, },
{
"name": "page",
"in": "path",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false "deprecated": false
} }
]
} }
}, },
"webhooks": {},
"components": { "components": {
"schemas": { "schemas": {
"SimpleResponse": { "TestResponse": {
"type": "object",
"properties": { "properties": {
"result": { "c": {
"type": "boolean" "type": "string"
} }
}, },
"required": [ "required": [
"result" "c"
], ]
"type": "object"
} }
}, },
"securitySchemes": {} "securitySchemes": {}

View File

@ -0,0 +1,124 @@
{
"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": {
"/type/{name}/edit": {
"get": {
"tags": [],
"summary": "Edit",
"description": "example location",
"parameters": [],
"responses": {
"200": {
"description": "does great things",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
]
},
"/type/{name}/other/{page}": {
"get": {
"tags": [],
"summary": "Other",
"description": "example location",
"parameters": [],
"responses": {
"200": {
"description": "does great things",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "page",
"in": "path",
"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

@ -1,79 +0,0 @@
{
"openapi": "3.0.3",
"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/test/{name}": {
"delete": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,110 +0,0 @@
{
"openapi": "3.0.3",
"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/test/{name}/nesty": {
"post": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "isCool",
"in": "query",
"schema": {
"type": "boolean"
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"requestBody": {
"description": "Cool stuff",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleRequest": {
"properties": {
"input": {
"type": "string"
}
},
"required": [
"input"
],
"type": "object"
},
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,110 +0,0 @@
{
"openapi": "3.0.3",
"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/test/{name}/nesty": {
"put": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "isCool",
"in": "query",
"schema": {
"type": "boolean"
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"requestBody": {
"description": "Cool stuff",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleRequest": {
"properties": {
"input": {
"type": "string"
}
},
"required": [
"input"
],
"type": "object"
},
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -31,7 +31,7 @@ dependencies {
// Logging // Logging
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.17.2") implementation("org.apache.logging.log4j:log4j-api:2.18.0")
implementation("org.apache.logging.log4j:log4j-core:2.18.0") implementation("org.apache.logging.log4j:log4j-core:2.18.0")
implementation("org.slf4j:slf4j-api:1.7.36") implementation("org.slf4j:slf4j-api:1.7.36")
implementation("org.slf4j:slf4j-simple:1.7.36") implementation("org.slf4j:slf4j-simple:1.7.36")

View File

@ -30,10 +30,6 @@ import io.ktor.server.routing.route
import io.ktor.server.routing.routing import io.ktor.server.routing.routing
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
/**
* Application entrypoint. Run this and head on over to `localhost:8081/docs`
* to see a very simple yet beautifully documented API
*/
fun main() { fun main() {
embeddedServer( embeddedServer(
Netty, Netty,
@ -43,7 +39,6 @@ fun main() {
} }
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json(Json { json(Json {
serializersModule = KompendiumSerializersModule.module serializersModule = KompendiumSerializersModule.module
@ -75,12 +70,8 @@ private fun Application.mainModule() {
routing { routing {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
authenticate("basic") { authenticate("basic") {
// This adds ReDoc support at the `/docs` endpoint.
// By default, it will point at the `/openapi.json` created by Kompendium
// Route with a get handler
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
call.respond(HttpStatusCode.OK, ExampleResponse(true)) call.respond(HttpStatusCode.OK, ExampleResponse(true))
} }
@ -89,8 +80,7 @@ private fun Application.mainModule() {
} }
} }
// Documentation for our route private fun Route.locationDocumentation() {
private fun Route.idDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -47,7 +47,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() documentation()
get { get {
call.respond(HttpStatusCode.OK, ExampleResponse(true)) call.respond(HttpStatusCode.OK, ExampleResponse(true))
} }
@ -55,7 +55,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.documentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -54,7 +54,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
call.respond( call.respond(
HttpStatusCode.OK, HttpStatusCode.OK,
@ -68,7 +68,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -13,7 +13,6 @@ import io.bkbn.kompendium.playground.util.Util.baseSpec
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
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.application.call
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
@ -55,7 +54,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
throw RuntimeException("This wasn't your fault I promise <3") throw RuntimeException("This wasn't your fault I promise <3")
} }
@ -63,7 +62,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -43,7 +43,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
call.respond(HttpStatusCode.OK, ExampleResponse(true)) call.respond(HttpStatusCode.OK, ExampleResponse(true))
} }
@ -51,7 +51,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -0,0 +1,116 @@
package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.component.Components
import io.bkbn.kompendium.oas.payload.Parameter
import io.bkbn.kompendium.oas.security.BasicAuth
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.playground.util.ExampleResponse
import io.bkbn.kompendium.playground.util.Util.baseSpec
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.UserIdPrincipal
import io.ktor.server.auth.authenticate
import io.ktor.server.auth.basic
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.application
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import kotlinx.serialization.json.Json
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(ContentNegotiation) {
json(Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
})
}
install(Authentication) {
basic("basic") {
realm = "Ktor Server"
validate { credentials ->
if (credentials.name == credentials.password) {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}
install(NotarizedApplication()) {
spec = baseSpec.copy(
components = Components(
securitySchemes = mutableMapOf(
"basic" to BasicAuth()
)
)
)
openApiJson = {
authenticate("basic") {
route("/openapi.json") {
get {
call.respond(HttpStatusCode.OK, this@route.application.attributes[KompendiumAttributes.openApiSpec])
}
}
}
}
}
routing {
authenticate("basic") {
redoc(pageTitle = "Simple API Docs")
route("/{id}") {
locationDocumentation()
get {
call.respond(HttpStatusCode.OK, ExampleResponse(true))
}
}
}
}
}
private fun Route.locationDocumentation() {
install(NotarizedRoute()) {
parameters = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
)
)
get = GetInfo.builder {
summary("Get user by id")
description("A very neat endpoint!")
security = mapOf(
"basic" to emptyList()
)
response {
responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>()
description("Will return whether or not the user is real 😱")
}
}
}
}

View File

@ -46,7 +46,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
call.respond(HttpStatusCode.OK, ExampleResponse(true)) call.respond(HttpStatusCode.OK, ExampleResponse(true))
} }
@ -54,7 +54,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -0,0 +1,80 @@
package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.locations.NotarizedLocations
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.Listing
import io.bkbn.kompendium.playground.util.Util.baseSpec
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.locations.Locations
import io.ktor.server.locations.get
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respondText
import io.ktor.server.routing.routing
import kotlinx.serialization.json.Json
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(Locations)
install(ContentNegotiation) {
json(Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
})
}
install(NotarizedApplication()) {
spec = baseSpec
}
install(NotarizedLocations()) {
locations = mapOf(
Listing::class to NotarizedLocations.LocationMetadata(
parameters = listOf(
Parameter(
name = "name",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
),
Parameter(
name = "page",
`in` = Parameter.Location.path,
schema = TypeDefinition.INT
)
),
get = GetInfo.builder {
summary("Get user by id")
description("A very neat endpoint!")
response {
responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>()
description("Will return whether or not the user is real 😱")
}
}
),
)
}
routing {
redoc(pageTitle = "Simple API Docs")
get<Listing> { listing ->
call.respondText("Listing ${listing.name}, page ${listing.page}")
}
}
}

View File

@ -1,5 +1,7 @@
package io.bkbn.kompendium.playground.util package io.bkbn.kompendium.playground.util
import io.ktor.server.locations.KtorExperimentalLocationsAPI
import io.ktor.server.locations.Location
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -14,3 +16,12 @@ data class CustomTypeResponse(
@Serializable @Serializable
data class ExceptionResponse(val message: String) data class ExceptionResponse(val message: String)
@Location("/list/{name}/page/{page}")
data class Listing(val name: String, val page: Int)
@Location("/type/{name}") data class Type(val name: String) {
// In these classes we have to include the `name` property matching the parent.
@Location("/edit") data class Edit(val parent: Type)
@Location("/other/{page}") data class Other(val parent: Type, val page: Int)
}