feat: Add opt-in locations support via ancillary module (#107)

This commit is contained in:
Ryan Brink
2021-11-25 13:09:18 -05:00
committed by GitHub
parent dd780ad29d
commit 5e070e1875
22 changed files with 1369 additions and 70 deletions

View File

@ -1,5 +1,9 @@
# Changelog # Changelog
## [1.11.0] - November 25th, 2021
### Added
- Support for Ktor Location Plugin
## [1.10.0] - November 25th, 2021 ## [1.10.0] - November 25th, 2021
### Changed ### Changed

167
README.md
View File

@ -8,14 +8,14 @@
### ⚠️ For info on V2 please see [here](#V2) ### ⚠️ For info on V2 please see [here](#V2)
Kompendium is intended to be a minimally invasive OpenApi Specification generator for [Ktor](https://ktor.io). Kompendium is intended to be a minimally invasive OpenApi Specification generator for [Ktor](https://ktor.io). Minimally
Minimally invasive meaning that users will use only Ktor native functions when implementing their API, and will invasive meaning that users will use only Ktor native functions when implementing their API, and will supplement with
supplement with Kompendium code in order to generate the appropriate spec. Kompendium code in order to generate the appropriate spec.
## How to install ## How to install
Kompendium publishes all releases to Maven Central. As such, using the stable version of `Kompendium` is as simple Kompendium publishes all releases to Maven Central. As such, using the stable version of `Kompendium` is as simple as
as declaring it as an implementation dependency in your `build.gradle.kts` declaring it as an implementation dependency in your `build.gradle.kts`
```kotlin ```kotlin
repositories { repositories {
@ -26,51 +26,53 @@ dependencies {
implementation("io.bkbn:kompendium-core:1.8.1") implementation("io.bkbn:kompendium-core:1.8.1")
implementation("io.bkbn:kompendium-auth:1.8.1") implementation("io.bkbn:kompendium-auth:1.8.1")
implementation("io.bkbn:kompendium-swagger-ui:1.8.1") implementation("io.bkbn:kompendium-swagger-ui:1.8.1")
// Other dependencies... // Other dependencies...
} }
``` ```
The last two dependencies are optional. The last two dependencies are optional.
If you want to get a little spicy 🤠 every merge of Kompendium is published to the GitHub package registry. Pulling If you want to get a little spicy 🤠 every merge of Kompendium is published to the GitHub package registry. Pulling from
from GitHub is slightly more involved, but such is the price you pay for bleeding edge fake data generation. GitHub is slightly more involved, but such is the price you pay for bleeding edge fake data generation.
```kotlin ```kotlin
// 1 Setup a helper function to import any Github Repository Package // 1 Setup a helper function to import any Github Repository Package
// This step is optional but I have a bunch of stuff stored on github so I find it useful 😄 // This step is optional but I have a bunch of stuff stored on github so I find it useful 😄
fun RepositoryHandler.github(packageUrl: String) = maven { fun RepositoryHandler.github(packageUrl: String) = maven {
name = "GithubPackages" name = "GithubPackages"
url = uri(packageUrl) url = uri(packageUrl)
credentials { credentials {
username = java.lang.System.getenv("GITHUB_USER") username = java.lang.System.getenv("GITHUB_USER")
password = java.lang.System.getenv("GITHUB_TOKEN") password = java.lang.System.getenv("GITHUB_TOKEN")
} }
} }
// 2 Add the repo in question (in this case Kompendium) // 2 Add the repo in question (in this case Kompendium)
repositories { repositories {
github("https://maven.pkg.github.com/bkbnio/kompendium") github("https://maven.pkg.github.com/bkbnio/kompendium")
} }
// 3 Add the package like any normal dependency // 3 Add the package like any normal dependency
dependencies { dependencies {
implementation("io.bkbn:kompendium-core:1.8.1") implementation("io.bkbn:kompendium-core:1.8.1")
} }
``` ```
## Local Development ## Local Development
Kompendium should run locally right out of the box, no configuration necessary (assuming you have JDK 1.8+ installed). New features can be built locally and published to your local maven repository with the `./gradlew publishToMavenLocal` command! Kompendium should run locally right out of the box, no configuration necessary (assuming you have JDK 1.8+ installed).
New features can be built locally and published to your local maven repository with the `./gradlew publishToMavenLocal`
command!
## In depth ## In depth
### Notarized Routes ### Notarized Routes
Kompendium introduces the concept of `notarized` HTTP methods. That is, for all your `GET`, `POST`, `PUT`, and `DELETE` Kompendium introduces the concept of `notarized` HTTP methods. That is, for all your `GET`, `POST`, `PUT`, and `DELETE`
operations, there is a corresponding `notarized` method. These operations are strongly typed, and use reification for operations, there is a corresponding `notarized` method. These operations are strongly typed, and use reification for a
a lot of the class based reflection that powers Kompendium. Generally speaking the three types that a `notarized` method lot of the class based reflection that powers Kompendium. Generally speaking the three types that a `notarized` method
will consume are will consume are
- `TParam`: Used to notarize expected request parameters - `TParam`: Used to notarize expected request parameters
@ -79,16 +81,17 @@ will consume are
`GET` and `DELETE` take `TParam` and `TResp` while `PUT` and `POST` take all three. `GET` and `DELETE` take `TParam` and `TResp` while `PUT` and `POST` take all three.
In addition to standard HTTP Methods, Kompendium also introduced the concept of `notarizedExceptions`. Using
In addition to standard HTTP Methods, Kompendium also introduced the concept of `notarizedExceptions`. Using the `StatusPage` the `StatusPage`
extension, users can notarize all handled exceptions, along with their respective HTTP codes and response types. extension, users can notarize all handled exceptions, along with their respective HTTP codes and response types.
Exceptions that have been `notarized` require two types as supplemental information Exceptions that have been `notarized` require two types as supplemental information
- `TErr`: Used to notarize the exception being handled by this use case. Used for matching responses at the route level. - `TErr`: Used to notarize the exception being handled by this use case. Used for matching responses at the route level.
- `TResp`: Same as above, this dictates the expected return type of the error response. - `TResp`: Same as above, this dictates the expected return type of the error response.
In keeping with minimal invasion, these extension methods all consume the same code block as a standard Ktor route method, In keeping with minimal invasion, these extension methods all consume the same code block as a standard Ktor route
meaning that swapping in a default Ktor route and a Kompendium `notarized` route is as simple as a single method change. method, meaning that swapping in a default Ktor route and a Kompendium `notarized` route is as simple as a single method
change.
### Supplemental Annotations ### Supplemental Annotations
@ -100,14 +103,15 @@ Currently, the annotations used by Kompendium are as follows
- `KompendiumField` - `KompendiumField`
- `KompendiumParam` - `KompendiumParam`
The intended purpose of `KompendiumField` is to offer field level overrides such as naming conventions (ie snake instead of camel). The intended purpose of `KompendiumField` is to offer field level overrides such as naming conventions (ie snake instead
of camel).
The purpose of `KompendiumParam` is to provide supplemental information needed to properly assign the type of parameter The purpose of `KompendiumParam` is to provide supplemental information needed to properly assign the type of parameter
(cookie, header, query, path) as well as other parameter-level metadata. (cookie, header, query, path) as well as other parameter-level metadata.
### Undeclared Field ### Undeclared Field
There is also a final `UndeclaredField` annotation. This should be used only in an absolutely emergency. This annotation There is also a final `UndeclaredField` annotation. This should be used only in an absolutely emergency. This annotation
will allow you to inject a _single_ undeclared field that will be included as part of the schema. will allow you to inject a _single_ undeclared field that will be included as part of the schema.
Due to limitations in using repeated annotations, this can only be used once per class Due to limitations in using repeated annotations, this can only be used once per class
@ -119,14 +123,14 @@ Use this _only_ when **all** else fails
### Polymorphism ### Polymorphism
Speaking of polymorphism... out of the box, Kompendium has support for sealed classes and interfaces. At runtime, it will build a mapping of all available sub-classes Speaking of polymorphism... out of the box, Kompendium has support for sealed classes and interfaces. At runtime, it
and build a spec that takes `anyOf` the implementations. This is currently a weak point of the entire library, and will build a mapping of all available sub-classes and build a spec that takes `anyOf` the implementations. This is
suggestions on better implementations are welcome 🤠 currently a weak point of the entire library, and suggestions on better implementations are welcome 🤠
### Serialization ### Serialization
Under the hood, Kompendium uses Jackson to serialize the final api spec. However, this implementation detail Under the hood, Kompendium uses Jackson to serialize the final api spec. However, this implementation detail does not
does not leak to the actual API, meaning that users are free to choose the serialization library of their choice. leak to the actual API, meaning that users are free to choose the serialization library of their choice.
Added the possibility to add your own ObjectMapper for Jackson. Added the possibility to add your own ObjectMapper for Jackson.
@ -138,7 +142,8 @@ ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT) .enable(SerializationFeature.INDENT_OUTPUT)
``` ```
If you want to change this default configuration and use your own ObjectMapper you only need to pass it as a second argument to the openApi module: If you want to change this default configuration and use your own ObjectMapper you only need to pass it as a second
argument to the openApi module:
```kotlin ```kotlin
routing { routing {
@ -153,9 +158,9 @@ routing {
### Route Handling ### Route Handling
> ⚠️ Warning: Custom route handling is almost definitely an indication that either a new selector should be added to kompendium-core or that kompendium is in need of another module to handle a new ktor companion module. If you have encountered a route selector that is not already handled, please consider opening an [issue](https://github.com/bkbnio/kompendium/issues/new) > ⚠️ Warning: Custom route handling is almost definitely an indication that either a new selector should be added to kompendium-core or that kompendium is in need of another module to handle a new ktor companion module. If you have encountered a route selector that is not already handled, please consider opening an [issue](https://github.com/bkbnio/kompendium/issues/new)
Kompendium does its best to handle all Ktor routes out of the gate. However, in keeping with the modular approach of Kompendium does its best to handle all Ktor routes out of the gate. However, in keeping with the modular approach of
Ktor and Kompendium, this is not always possible. Ktor and Kompendium, this is not always possible.
Should you need to, custom route handlers can be registered via the Should you need to, custom route handlers can be registered via the
@ -170,14 +175,44 @@ fun <T : RouteSelector> addCustomRouteHandler(
) )
``` ```
This function takes a selector, which _must_ be a KClass of the Ktor `RouteSelector` type. The handler is a function This function takes a selector, which _must_ be a KClass of the Ktor `RouteSelector` type. The handler is a function
that extends the Kompendium `PathCalculator`. This is necessary because it gives you access to `PathCalculator.calculate`, that extends the Kompendium `PathCalculator`. This is necessary because it gives you access
which you are going to want in order to recursively calculate the remainder of the route :) to `PathCalculator.calculate`, which you are going to want in order to recursively calculate the remainder of the
route :)
Its parameters are the `Route` itself, along with the "tail" of the Path (the path that has been calculated thus far). Its parameters are the `Route` itself, along with the "tail" of the Path (the path that has been calculated thus far).
Working examples `init` blocks of the `PathCalculator` and `KompendiumAuth` object. Working examples `init` blocks of the `PathCalculator` and `KompendiumAuth` object.
### Ktor Locations Plugin
Kompendium offers integration with the Ktor [Locations](https://ktor.io/docs/locations.html) plugin via an optional
module `kompendium-locations`.
This provides a wrapper around the Locations api, provide full path analysis, along with typesafe access to request
parameters. However, due to the way that Kompendium core parses class metadata, you will still need to annotate all
parameters with `@KompendiumParam`. A simple example
```kotlin
@Location("/test/{name}")
data class TestLocations(
@KompendiumParam(ParamType.PATH)
val name: String,
)
```
When passed to a notarized route (make sure you are using the notarized methods found in
the `io.bkbn.kompendium.locations` package ⚠️), this will provide you with a notarized route that also passes through
the type safe instance containing all of your parameters!
```kotlin
notarizedGet(testLocation) { tl ->
call.respondText { tl.name }
}
```
A working example can be found in the `kompendium-playground` in the `LocationsSpike` file :)
## Examples ## Examples
The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example
@ -222,12 +257,13 @@ val simpleGetInfo = GetInfo<Unit, ExampleResponse>(
### Kompendium Auth and security schemes ### Kompendium Auth and security schemes
There is a separate library to handle security schemes: `kompendium-auth`. There is a separate library to handle security schemes: `kompendium-auth`. This needs to be added to your project as
This needs to be added to your project as dependency. dependency.
At the moment, the basic and jwt authentication is only supported. At the moment, the basic and jwt authentication is only supported.
A minimal example would be: A minimal example would be:
```kotlin ```kotlin
install(Authentication) { install(Authentication) {
notarizedBasic("basic") { notarizedBasic("basic") {
@ -257,28 +293,32 @@ routing {
} }
val basicAuthGetInfo = MethodInfo<Unit, ExampleResponse>( val basicAuthGetInfo = MethodInfo<Unit, ExampleResponse>(
summary = "Another get test", summary = "Another get test",
description = "testing more", description = "testing more",
responseInfo = testGetResponse, responseInfo = testGetResponse,
securitySchemes = setOf("basic") securitySchemes = setOf("basic")
) )
val jwtAuthGetInfo = basicAuthGetInfo.copy(securitySchemes = setOf("jwt")) val jwtAuthGetInfo = basicAuthGetInfo.copy(securitySchemes = setOf("jwt"))
``` ```
### Enabling Swagger ui ### Enabling Swagger ui
To enable Swagger UI, `kompendium-swagger-ui` needs to be added.
This will also add the [ktor webjars feature](https://ktor.io/docs/webjars.html) to your classpath as it is required for swagger ui. To enable Swagger UI, `kompendium-swagger-ui` needs to be added. This will also add
the [ktor webjars feature](https://ktor.io/docs/webjars.html) to your classpath as it is required for swagger ui.
Minimal Example: Minimal Example:
```kotlin ```kotlin
install(Webjars) install(Webjars)
routing { routing {
openApi(oas) openApi(oas)
swaggerUI() swaggerUI()
} }
``` ```
### Enabling ReDoc ### Enabling ReDoc
Unlike swagger, redoc is provided (perhaps confusingly, in the `core` module). This means out of the box with `kompendium-core`, you can add
Unlike swagger, redoc is provided (perhaps confusingly, in the `core` module). This means out of the box
with `kompendium-core`, you can add
[ReDoc](https://github.com/Redocly/redoc) as follows [ReDoc](https://github.com/Redocly/redoc) as follows
```kotlin ```kotlin
@ -291,19 +331,19 @@ routing {
## Custom Type Overrides ## Custom Type Overrides
Kompendium does its best to analyze types and to generate an OpenAPI format accordingly. However, there are certain Kompendium does its best to analyze types and to generate an OpenAPI format accordingly. However, there are certain
classes that just don't play nice with the standard reflection analysis that Kompendium performs. classes that just don't play nice with the standard reflection analysis that Kompendium performs. Should you encounter a
Should you encounter a data type that Kompendium cannot comprehend, you will need to data type that Kompendium cannot comprehend, you will need to add it explicitly. For example, adding the Joda
add it explicitly. For example, adding the Joda Time `DateTime` object would be as simple as the following Time `DateTime` object would be as simple as the following
```kotlin ```kotlin
Kompendium.addCustomTypeSchema(DateTime::class, FormatSchema("date-time", "string")) Kompendium.addCustomTypeSchema(DateTime::class, FormatSchema("date-time", "string"))
``` ```
Since `Kompendium` is an object, this needs to be declared once, ahead of the actual API instantiation. This way, this Since `Kompendium` is an object, this needs to be declared once, ahead of the actual API instantiation. This way, this
type override can be cached ahead of reflection. Kompendium will then match all instances of this type and return the type override can be cached ahead of reflection. Kompendium will then match all instances of this type and return the
specified schema. specified schema.
So how do you know a type can and cannot be inferred? The safe bet is that it can be. So go ahead and give it a shot. So how do you know a type can and cannot be inferred? The safe bet is that it can be. So go ahead and give it a shot.
However, in the very odd scenario (almost always having to do with date/time libraries 😤) where it can't, you can rest However, in the very odd scenario (almost always having to do with date/time libraries 😤) where it can't, you can rest
safely knowing that you have the option to inject a custom override should you need to. safely knowing that you have the option to inject a custom override should you need to.
@ -311,26 +351,27 @@ safely knowing that you have the option to inject a custom override should you n
### Kompendium as a singleton ### Kompendium as a singleton
Currently, Kompendium exists as a Kotlin object. This comes with a couple perks, but a couple downsides. Primarily, Currently, Kompendium exists as a Kotlin object. This comes with a couple perks, but a couple downsides. Primarily, it
it offers a seriously clean UX where the implementer doesn't need to worry about what instance to send data to. The main offers a seriously clean UX where the implementer doesn't need to worry about what instance to send data to. The main
drawback, however, is that you are limited to a single API per classpath. drawback, however, is that you are limited to a single API per classpath.
If this is a blocker, please open a GitHub issue, and we can start to think out solutions! If this is a blocker, please open a GitHub issue, and we can start to think out solutions!
## Future Work ## Future Work
Work on V1 of Kompendium has come to a close. This, however, does not mean it has achieved complete
parity with the OpenAPI feature spec, nor does it have all-of-the nice to have features that a truly next-gen API spec Work on V1 of Kompendium has come to a close. This, however, does not mean it has achieved complete parity with the
should have. There are several outstanding features that have been added to the OpenAPI feature spec, nor does it have all-of-the nice to have features that a truly next-gen API spec should have.
[V2 Milestone](https://github.com/bkbnio/kompendium/milestone/2). Among others, this includes There are several outstanding features that have been added to the
[V2 Milestone](https://github.com/bkbnio/kompendium/milestone/2). Among others, this includes
- AsyncAPI Integration - AsyncAPI Integration
- Field Validation - Field Validation
If you have a feature that you would like to see implemented that is not on this list, or discover a 🐞, please open If you have a feature that you would like to see implemented that is not on this list, or discover a 🐞, please open an
an issue [here](https://github.com/bkbnio/kompendium/issues/new) issue [here](https://github.com/bkbnio/kompendium/issues/new)
### V2 ### V2
Due to the large number of breaking changes that will be made in version 2, development is currently being done on the Due to the large number of breaking changes that will be made in version 2, development is currently being done on the
long-lived `v2` feature branch. If you are working on any feature in the `V2` milestone, please target that branch! long-lived `v2` feature branch. If you are working on any feature in the `V2` milestone, please target that branch!
If you are unsure where your changes should be, please open an issue first :) If you are unsure where your changes should be, please open an issue first :)

View File

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

View File

@ -1,6 +1,6 @@
[versions] [versions]
kotlin = "1.4.32" kotlin = "1.4.32"
ktor = "1.6.4" ktor = "1.6.5"
kotlinx-serialization = "1.2.1" kotlinx-serialization = "1.2.1"
jackson-kotlin = "2.12.0" jackson-kotlin = "2.12.0"
slf4j = "1.7.30" slf4j = "1.7.30"
@ -17,6 +17,7 @@ ktor-html-builder = { group = "io.ktor", name = "ktor-html-builder", version.ref
ktor-auth-lib = { group = "io.ktor", name = "ktor-auth", version.ref = "ktor" } ktor-auth-lib = { group = "io.ktor", name = "ktor-auth", version.ref = "ktor" }
ktor-auth-jwt = { group = "io.ktor", name = "ktor-auth-jwt", version.ref = "ktor" } ktor-auth-jwt = { group = "io.ktor", name = "ktor-auth-jwt", version.ref = "ktor" }
ktor-webjars = { group = "io.ktor", name = "ktor-webjars", version.ref = "ktor" } ktor-webjars = { group = "io.ktor", name = "ktor-webjars", version.ref = "ktor" }
ktor-locations = { group = "io.ktor", name = "ktor-locations", version.ref = "ktor" }
# Serialization # Serialization
jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jackson-kotlin" } jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jackson-kotlin" }

View File

@ -174,7 +174,9 @@ object MethodParser {
*/ */
private fun KType.toParameterSpec(): List<OpenApiSpecParameter> { private fun KType.toParameterSpec(): List<OpenApiSpecParameter> {
val clazz = classifier as KClass<*> val clazz = classifier as KClass<*>
return clazz.memberProperties.map { prop -> return clazz.memberProperties.filter { prop ->
prop.findAnnotation<KompendiumParam>() != null
}.map { prop ->
val field = prop.javaField?.type?.kotlin val field = prop.javaField?.type?.kotlin
?: error("Unable to parse field type from $prop") ?: error("Unable to parse field type from $prop")
val anny = prop.findAnnotation<KompendiumParam>() val anny = prop.findAnnotation<KompendiumParam>()

View File

@ -0,0 +1,78 @@
plugins {
`java-library`
`maven-publish`
signing
}
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation(libs.bundles.ktor)
implementation(libs.ktor.locations)
implementation(projects.kompendiumCore)
testImplementation(libs.ktor.jackson)
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
testImplementation(libs.jackson.module.kotlin)
testImplementation(libs.ktor.server.test.host)
}
java {
withSourcesJar()
withJavadocJar()
}
publishing {
repositories {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/bkbnio/kompendium")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("kompendium") {
from(components["kotlin"])
artifact(tasks.sourcesJar)
artifact(tasks.javadocJar)
groupId = project.group.toString()
artifactId = project.name.toLowerCase()
version = project.version.toString()
pom {
name.set("Kompendium")
description.set("A minimally invasive OpenAPI spec generator for Ktor")
url.set("https://github.com/bkbnio/Kompendium")
licenses {
license {
name.set("MIT License")
url.set("https://mit-license.org/")
}
}
developers {
developer {
id.set("bkbnio")
name.set("Ryan Brink")
email.set("admin@bkbn.io")
}
}
scm {
connection.set("scm:git:git://github.com/bkbnio/Kompendium.git")
developerConnection.set("scm:git:ssh://github.com/bkbnio/Kompendium.git")
url.set("https://github.com/bkbnio/Kompendium.git")
}
}
}
}
}
signing {
val signingKey: String? by project
val signingPassword: String? by project
useInMemoryPgpKeys(signingKey, signingPassword)
sign(publishing.publications)
}

View File

@ -0,0 +1,144 @@
package io.bkbn.kompendium.locations
import io.bkbn.kompendium.Kompendium
import io.bkbn.kompendium.KompendiumPreFlight
import io.bkbn.kompendium.MethodParser.parseMethodInfo
import io.bkbn.kompendium.models.meta.MethodInfo.DeleteInfo
import io.bkbn.kompendium.models.meta.MethodInfo.GetInfo
import io.bkbn.kompendium.models.meta.MethodInfo.PostInfo
import io.bkbn.kompendium.models.meta.MethodInfo.PutInfo
import io.bkbn.kompendium.models.oas.OpenApiSpecPathItem
import io.ktor.application.ApplicationCall
import io.ktor.http.HttpMethod
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
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
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
/**
* 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.KompendiumField].
* Additionally, the class must be annotated with @[io.ktor.locations.Location].
* @param TResp Class detailing the expected API response
* @param info Route metadata
*/
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
info: GetInfo<TParam, TResp>,
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val path = Kompendium.calculatePath(this)
val locationPath = TParam::class.calculatePath()
val pathWithLocation = path.plus(locationPath)
Kompendium.openApiSpec.paths.getOrPut(pathWithLocation) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[pathWithLocation]?.get = parseMethodInfo(info, paramType, requestType, responseType)
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.KompendiumField]
* 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
*/
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
info: PostInfo<TParam, TReq, TResp>,
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val path = Kompendium.calculatePath(this)
val locationPath = TParam::class.calculatePath()
val pathWithLocation = path.plus(locationPath)
Kompendium.openApiSpec.paths.getOrPut(pathWithLocation) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[pathWithLocation]?.post = parseMethodInfo(info, paramType, requestType, responseType)
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.KompendiumField]
* 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
*/
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
info: PutInfo<TParam, TReq, TResp>,
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val path = Kompendium.calculatePath(this)
val locationPath = TParam::class.calculatePath()
val pathWithLocation = path.plus(locationPath)
Kompendium.openApiSpec.paths.getOrPut(pathWithLocation) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[pathWithLocation]?.put =
parseMethodInfo(info, paramType, requestType, responseType)
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.KompendiumField]
* Additionally, the class must be annotated with @[io.ktor.locations.Location].
* @param TResp Class detailing the expected API response
* @param info Route metadata
*/
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
info: DeleteInfo<TParam, TResp>,
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val path = Kompendium.calculatePath(this)
val locationPath = TParam::class.calculatePath()
val pathWithLocation = path.plus(locationPath)
Kompendium.openApiSpec.paths.getOrPut(pathWithLocation) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[pathWithLocation]?.delete =
parseMethodInfo(info, paramType, requestType, responseType)
return location(TParam::class) {
method(HttpMethod.Delete) { handle(body) }
}
}
fun KClass<*>.calculatePath(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
val newSuffix = locationAnnotation.path.plus(suffix)
return when (parent) {
null -> newSuffix
else -> parent.calculatePath(newSuffix)
}
}
}

View File

@ -0,0 +1,358 @@
package io.bkbn.kompendium.locations
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.Kompendium
import io.bkbn.kompendium.annotations.KompendiumParam
import io.bkbn.kompendium.annotations.ParamType
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.bkbn.kompendium.locations.util.TestData
import io.bkbn.kompendium.models.meta.MethodInfo
import io.bkbn.kompendium.models.meta.RequestInfo
import io.bkbn.kompendium.models.meta.ResponseInfo
import io.bkbn.kompendium.routes.openApi
import io.bkbn.kompendium.routes.redoc
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.locations.Location
import io.ktor.locations.Locations
import io.ktor.response.respondText
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.testing.handleRequest
import io.ktor.server.testing.withTestApplication
import org.junit.Test
import kotlin.test.AfterTest
import kotlin.test.assertEquals
class KompendiumLocationsTest {
@AfterTest
fun `reset Kompendium`() {
Kompendium.resetSchema()
}
@Test
fun `Notarized Get with simple location`() {
withTestApplication({
configModule()
docs()
notarizedGetSimpleLocation()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_get_simple_location.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Get with nested location`() {
withTestApplication({
configModule()
docs()
notarizedGetNestedLocation()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_get_nested_location.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Post with simple location`() {
withTestApplication({
configModule()
docs()
notarizedPostSimpleLocation()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_post_simple_location.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Post with nested location`() {
withTestApplication({
configModule()
docs()
notarizedPostNestedLocation()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_post_nested_location.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Put with simple location`() {
withTestApplication({
configModule()
docs()
notarizedPutSimpleLocation()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_put_simple_location.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Put with nested location`() {
withTestApplication({
configModule()
docs()
notarizedPutNestedLocation()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_put_nested_location.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Delete with simple location`() {
withTestApplication({
configModule()
docs()
notarizedDeleteSimpleLocation()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_delete_simple_location.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Delete with nested location`() {
withTestApplication({
configModule()
docs()
notarizedDeleteNestedLocation()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_delete_nested_location.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
private fun Application.configModule() {
install(ContentNegotiation) {
jackson(ContentType.Application.Json) {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
install(Locations)
}
private val oas = Kompendium.openApiSpec.copy()
private fun Application.docs() {
routing {
openApi(oas)
redoc(oas)
}
}
private fun Application.notarizedGetSimpleLocation() {
routing {
route("/test") {
notarizedGet(TestResponseInfo.testGetSimpleLocation) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
private fun Application.notarizedGetNestedLocation() {
routing {
route("/test") {
notarizedGet(TestResponseInfo.testGetNestedLocation) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
private fun Application.notarizedPostSimpleLocation() {
routing {
route("/test") {
notarizedPost(TestResponseInfo.testPostSimpleLocation) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
private fun Application.notarizedPostNestedLocation() {
routing {
route("/test") {
notarizedPost(TestResponseInfo.testPostNestedLocation) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
private fun Application.notarizedPutSimpleLocation() {
routing {
route("/test") {
notarizedPut(TestResponseInfo.testPutSimpleLocation) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
private fun Application.notarizedPutNestedLocation() {
routing {
route("/test") {
notarizedPut(TestResponseInfo.testPutNestedLocation) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
private fun Application.notarizedDeleteSimpleLocation() {
routing {
route("/test") {
notarizedDelete(TestResponseInfo.testDeleteSimpleLocation) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
private fun Application.notarizedDeleteNestedLocation() {
routing {
route("/test") {
notarizedDelete(TestResponseInfo.testDeleteNestedLocation) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
object TestResponseInfo {
val testGetSimpleLocation = MethodInfo.GetInfo<SimpleLoc, SimpleResponse>(
summary = "Location Test",
description = "A cool test",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "A successful endeavor"
)
)
val testPostSimpleLocation = MethodInfo.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 = MethodInfo.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 = MethodInfo.DeleteInfo<SimpleLoc, SimpleResponse>(
summary = "Location Test",
description = "A cool test",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "A successful endeavor"
)
)
val testGetNestedLocation = MethodInfo.GetInfo<SimpleLoc.NestedLoc, SimpleResponse>(
summary = "Location Test",
description = "A cool test",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "A successful endeavor"
)
)
val testPostNestedLocation = MethodInfo.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 = MethodInfo.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 = MethodInfo.DeleteInfo<SimpleLoc.NestedLoc, SimpleResponse>(
summary = "Location Test",
description = "A cool test",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "A successful endeavor"
)
)
}
}
@Location("/test/{name}")
data class SimpleLoc(@KompendiumParam(ParamType.PATH) val name: String) {
@Location("/nesty")
data class NestedLoc(@KompendiumParam(ParamType.QUERY) val isCool: Boolean, val parent: SimpleLoc)
}
data class SimpleResponse(val result: Boolean)
data class SimpleRequest(val input: String)

View File

@ -0,0 +1,11 @@
package io.bkbn.kompendium.locations.util
import java.io.File
object TestData {
fun getFileSnapshot(fileName: String): String {
val snapshotPath = "src/test/resources"
val file = File("$snapshotPath/$fileName")
return file.readText()
}
}

View File

@ -0,0 +1,2 @@
package io.bkbn.kompendium.locations.util

View File

@ -0,0 +1,65 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"paths" : {
"/test/test/{name}/nesty" : {
"delete" : {
"tags" : [ ],
"summary" : "Location Test",
"description" : "A cool test",
"parameters" : [ {
"name" : "isCool",
"in" : "query",
"schema" : {
"type" : "boolean"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"Boolean" : {
"type" : "boolean"
},
"SimpleResponse" : {
"type" : "object",
"properties" : {
"result" : {
"$ref" : "#/components/schemas/Boolean"
}
}
},
"String" : {
"type" : "string"
},
"SimpleLoc" : {
"type" : "object",
"properties" : {
"name" : {
"$ref" : "#/components/schemas/String"
}
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,57 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"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" : {
"Boolean" : {
"type" : "boolean"
},
"SimpleResponse" : {
"type" : "object",
"properties" : {
"result" : {
"$ref" : "#/components/schemas/Boolean"
}
}
},
"String" : {
"type" : "string"
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,65 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"paths" : {
"/test/test/{name}/nesty" : {
"get" : {
"tags" : [ ],
"summary" : "Location Test",
"description" : "A cool test",
"parameters" : [ {
"name" : "isCool",
"in" : "query",
"schema" : {
"type" : "boolean"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"Boolean" : {
"type" : "boolean"
},
"SimpleResponse" : {
"type" : "object",
"properties" : {
"result" : {
"$ref" : "#/components/schemas/Boolean"
}
}
},
"String" : {
"type" : "string"
},
"SimpleLoc" : {
"type" : "object",
"properties" : {
"name" : {
"$ref" : "#/components/schemas/String"
}
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,57 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"paths" : {
"/test/test/{name}" : {
"get" : {
"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" : {
"Boolean" : {
"type" : "boolean"
},
"SimpleResponse" : {
"type" : "object",
"properties" : {
"result" : {
"$ref" : "#/components/schemas/Boolean"
}
}
},
"String" : {
"type" : "string"
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,84 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"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
} ],
"requestBody" : {
"description" : "Cool stuff",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/SimpleRequest"
}
}
},
"required" : false
},
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"SimpleRequest" : {
"type" : "object",
"properties" : {
"input" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Boolean" : {
"type" : "boolean"
},
"SimpleResponse" : {
"type" : "object",
"properties" : {
"result" : {
"$ref" : "#/components/schemas/Boolean"
}
}
},
"SimpleLoc" : {
"type" : "object",
"properties" : {
"name" : {
"$ref" : "#/components/schemas/String"
}
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,76 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"paths" : {
"/test/test/{name}" : {
"post" : {
"tags" : [ ],
"summary" : "Location Test",
"description" : "A cool test",
"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" : false
},
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"SimpleRequest" : {
"type" : "object",
"properties" : {
"input" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Boolean" : {
"type" : "boolean"
},
"SimpleResponse" : {
"type" : "object",
"properties" : {
"result" : {
"$ref" : "#/components/schemas/Boolean"
}
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,84 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"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
} ],
"requestBody" : {
"description" : "Cool stuff",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/SimpleRequest"
}
}
},
"required" : false
},
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"SimpleRequest" : {
"type" : "object",
"properties" : {
"input" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Boolean" : {
"type" : "boolean"
},
"SimpleResponse" : {
"type" : "object",
"properties" : {
"result" : {
"$ref" : "#/components/schemas/Boolean"
}
}
},
"SimpleLoc" : {
"type" : "object",
"properties" : {
"name" : {
"$ref" : "#/components/schemas/String"
}
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,76 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"paths" : {
"/test/test/{name}" : {
"put" : {
"tags" : [ ],
"summary" : "Location Test",
"description" : "A cool test",
"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" : false
},
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"SimpleRequest" : {
"type" : "object",
"properties" : {
"input" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Boolean" : {
"type" : "boolean"
},
"SimpleResponse" : {
"type" : "object",
"properties" : {
"result" : {
"$ref" : "#/components/schemas/Boolean"
}
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -9,6 +9,7 @@ dependencies {
implementation(projects.kompendiumCore) implementation(projects.kompendiumCore)
implementation(projects.kompendiumAuth) implementation(projects.kompendiumAuth)
implementation(projects.kompendiumLocations)
implementation(projects.kompendiumSwaggerUi) implementation(projects.kompendiumSwaggerUi)
implementation(libs.bundles.ktor) implementation(libs.bundles.ktor)
@ -16,9 +17,10 @@ dependencies {
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.serialization) implementation(libs.ktor.serialization)
implementation(libs.bundles.ktorAuth) implementation(libs.bundles.ktorAuth)
implementation(libs.ktor.locations)
implementation(libs.bundles.logging) implementation(libs.bundles.logging)
implementation("joda-time:joda-time:2.10.10") implementation("joda-time:joda-time:2.10.13")
testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation("org.jetbrains.kotlin:kotlin-test-junit")

View File

@ -0,0 +1,91 @@
package io.bkbn.kompendium.playground
import io.bkbn.kompendium.Kompendium
import io.bkbn.kompendium.annotations.KompendiumParam
import io.bkbn.kompendium.annotations.ParamType
import io.bkbn.kompendium.locations.NotarizedLocation.notarizedGet
import io.bkbn.kompendium.models.meta.MethodInfo
import io.bkbn.kompendium.models.meta.ResponseInfo
import io.bkbn.kompendium.models.oas.FormatSchema
import io.bkbn.kompendium.playground.LocationsToC.testLocation
import io.bkbn.kompendium.routes.openApi
import io.bkbn.kompendium.routes.redoc
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.HttpStatusCode
import io.ktor.locations.Location
import io.ktor.locations.Locations
import io.ktor.response.respondText
import io.ktor.routing.routing
import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import org.joda.time.DateTime
fun main() {
Kompendium.addCustomTypeSchema(DateTime::class, FormatSchema("date-time", "string"))
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private var featuresInstalled = false
private fun Application.configModule() {
if (!featuresInstalled) {
install(ContentNegotiation) {
json()
}
install(Locations)
featuresInstalled = true
}
}
private fun Application.mainModule() {
configModule()
routing {
openApi(oas)
redoc(oas)
notarizedGet(testLocation) { tl ->
call.respondText { tl.parent.parent.name }
}
}
}
private object LocationsToC {
val testLocation = MethodInfo.GetInfo<TestLocations.NestedTestLocations.OhBoiUCrazy, ExampleResponse>(
summary = "Example Parameters",
description = "A test for setting parameter examples",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "nice",
examples = mapOf("test" to ExampleResponse(c = "spud"))
),
canThrow = setOf(Exception::class)
)
}
@Location("/test/{name}")
data class TestLocations(
@KompendiumParam(ParamType.PATH)
val name: String,
) {
@Location("/spaghetti")
data class NestedTestLocations(
@KompendiumParam(ParamType.QUERY)
val idk: Int,
val parent: TestLocations
) {
@Location("/hehe/{madness}")
data class OhBoiUCrazy(
@KompendiumParam(ParamType.PATH)
val madness: Boolean,
val parent: NestedTestLocations
)
}
}

View File

@ -52,9 +52,9 @@ fun main() {
).start(wait = true) ).start(wait = true)
} }
var featuresInstalled = false private var featuresInstalled = false
fun Application.configModule() { private fun Application.configModule() {
if (!featuresInstalled) { if (!featuresInstalled) {
install(ContentNegotiation) { install(ContentNegotiation) {
json() json()
@ -87,7 +87,7 @@ fun Application.configModule() {
} }
} }
fun Application.mainModule() { private fun Application.mainModule() {
configModule() configModule()
routing { routing {
openApi(oas) openApi(oas)

View File

@ -4,6 +4,7 @@ include("kompendium-core")
include("kompendium-auth") include("kompendium-auth")
include("kompendium-swagger-ui") include("kompendium-swagger-ui")
include("kompendium-playground") include("kompendium-playground")
include("kompendium-locations")
// Feature Previews // Feature Previews
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")