Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
9edd3a53ce | |||
91a6164663 | |||
5a7e052ac4 | |||
6ba3617e32 |
25
.github/workflows/pr_checks.yml
vendored
25
.github/workflows/pr_checks.yml
vendored
@ -37,3 +37,28 @@ jobs:
|
|||||||
run: ./gradlew assemble
|
run: ./gradlew assemble
|
||||||
- name: Run Unit Tests
|
- name: Run Unit Tests
|
||||||
run: ./gradlew test
|
run: ./gradlew test
|
||||||
|
- name: Cache Coverage Results
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ./**/build/reports/jacoco
|
||||||
|
key: ${{ runner.os }}-unit-${{ env.GITHUB_SHA }}
|
||||||
|
upload-code-coverage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- unit
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Cache Coverage Results
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ./**/build/reports/jacoco
|
||||||
|
key: ${{ runner.os }}-unit-${{ env.GITHUB_SHA }}
|
||||||
|
- name: Cache Coverage Results
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ./**/build/reports/jacoco
|
||||||
|
key: ${{ runner.os }}-integration-${{ env.GITHUB_SHA }}
|
||||||
|
- name: Publish code coverage to Codacy
|
||||||
|
uses: codacy/codacy-coverage-reporter-action@v1
|
||||||
|
with:
|
||||||
|
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||||
|
20
.github/workflows/publish.yml
vendored
20
.github/workflows/publish.yml
vendored
@ -24,3 +24,23 @@ jobs:
|
|||||||
run: ./gradlew publishAllPublicationsToGithubPackagesRepository
|
run: ./gradlew publishAllPublicationsToGithubPackagesRepository
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
code-coverage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
distribution: 'adopt'
|
||||||
|
java-version: '11'
|
||||||
|
- name: Cache Gradle packages
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.gradle/caches
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||||
|
restore-keys: ${{ runner.os }}-gradle
|
||||||
|
- name: Run Unit Tests
|
||||||
|
run: ./gradlew test
|
||||||
|
- name: Publish code coverage to Codacy
|
||||||
|
uses: codacy/codacy-coverage-reporter-action@v1
|
||||||
|
with:
|
||||||
|
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||||
|
22
CHANGELOG.md
22
CHANGELOG.md
@ -1,6 +1,26 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [1.6.0] - August 12, 2021
|
## [1.8.1] - October 4th, 2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Codacy integration
|
||||||
|
|
||||||
|
## [1.8.0] - October 4th, 2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Path calculation revamped to allow for simpler selector injection
|
||||||
|
- Kotlin version bumped to 1.5.31
|
||||||
|
- Ktor version bumped to 1.6.4
|
||||||
|
|
||||||
|
## [1.7.0] - August 14th, 2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added ability to inject an emergency `UndeclaredField` in the event of certain polymorphic serializers and such
|
||||||
|
|
||||||
|
## [1.6.0] - August 12th, 2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
43
README.md
43
README.md
@ -1,5 +1,7 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
|
|
||||||
|
[](https://www.codacy.com/gh/bkbnio/kompendium/dashboard?utm_source=github.com&utm_medium=referral&utm_content=bkbnio/kompendium&utm_campaign=Badge_Grade)
|
||||||
|
[](https://www.codacy.com/gh/bkbnio/kompendium/dashboard?utm_source=github.com&utm_medium=referral&utm_content=bkbnio/kompendium&utm_campaign=Badge_Coverage)
|
||||||
[](https://search.maven.org/search?q=io.bkbn%20kompendium)
|
[](https://search.maven.org/search?q=io.bkbn%20kompendium)
|
||||||
|
|
||||||
## What is Kompendium
|
## What is Kompendium
|
||||||
@ -96,9 +98,21 @@ The intended purpose of `KompendiumField` is to offer field level overrides such
|
|||||||
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Due to limitations in using repeated annotations, this can only be used once per class
|
||||||
|
|
||||||
|
This is a complete hack, and is included for odd scenarios like kotlinx serialization polymorphic adapters that expect a
|
||||||
|
`type` field in order to perform their analysis.
|
||||||
|
|
||||||
|
Use this _only_ when **all** else fails
|
||||||
|
|
||||||
### Polymorphism
|
### Polymorphism
|
||||||
|
|
||||||
Out of the box, Kompendium has support for sealed classes. 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 will build a mapping of all available sub-classes
|
||||||
and build a spec that takes `anyOf` the implementations. This is currently a weak point of the entire library, and
|
and build a spec that takes `anyOf` the implementations. This is currently a weak point of the entire library, and
|
||||||
suggestions on better implementations are welcome 🤠
|
suggestions on better implementations are welcome 🤠
|
||||||
|
|
||||||
@ -107,6 +121,33 @@ suggestions on better implementations are welcome 🤠
|
|||||||
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 leak to the actual API, meaning that users are free to choose the serialization library of their choice.
|
does not leak to the actual API, meaning that users are free to choose the serialization library of their choice.
|
||||||
|
|
||||||
|
### Route Handling
|
||||||
|
|
||||||
|
> ⚠️ Warning: Custom route handling is almost definitely an indication that a new Kompendium module needs to be added. If you have encountered a route type that is not 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
|
||||||
|
Ktor and Kompendium, this is not always possible.
|
||||||
|
|
||||||
|
Should you need to, custom route handlers can be registered via the
|
||||||
|
`Kompendium.addCustomRouteHandler` function.
|
||||||
|
|
||||||
|
The method declaration is a bit gross, so lets dig in to what is happening.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun <T : RouteSelector> addCustomRouteHandler(
|
||||||
|
selector: KClass<T>,
|
||||||
|
handler: PathCalculator.(Route, String) -> String
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
This function takes a selector, which _must_ be an implementation of the Ktor `RouteSelector`. The handler is a function
|
||||||
|
that extends the Kompendium `PathCalculator`. This is necessary because it gives you access to `PathCalculator.calculate`,
|
||||||
|
which you are going to want in order to calculate the rest of the route :)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## 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
|
||||||
|
@ -4,7 +4,7 @@ import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.jetbrains.kotlin.jvm") version "1.5.0" apply false
|
id("org.jetbrains.kotlin.jvm") version "1.5.31" apply false
|
||||||
id("io.gitlab.arturbosch.detekt") version "1.17.0-RC3" apply false
|
id("io.gitlab.arturbosch.detekt") version "1.17.0-RC3" apply false
|
||||||
id("com.adarshr.test-logger") version "3.0.0" apply false
|
id("com.adarshr.test-logger") version "3.0.0" apply false
|
||||||
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" apply true
|
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" apply true
|
||||||
@ -30,6 +30,7 @@ allprojects {
|
|||||||
apply(plugin = "io.gitlab.arturbosch.detekt")
|
apply(plugin = "io.gitlab.arturbosch.detekt")
|
||||||
apply(plugin = "com.adarshr.test-logger")
|
apply(plugin = "com.adarshr.test-logger")
|
||||||
apply(plugin = "idea")
|
apply(plugin = "idea")
|
||||||
|
apply(plugin = "jacoco")
|
||||||
|
|
||||||
tasks.withType<KotlinCompile>().configureEach {
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@ -37,6 +38,21 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test>() {
|
||||||
|
finalizedBy(tasks.withType(JacocoReport::class))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<JacocoReport>() {
|
||||||
|
reports {
|
||||||
|
html.required.set(true)
|
||||||
|
xml.required.set(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configure<JacocoPluginExtension> {
|
||||||
|
toolVersion = "0.8.7"
|
||||||
|
}
|
||||||
|
|
||||||
configure<TestLoggerExtension> {
|
configure<TestLoggerExtension> {
|
||||||
theme = ThemeType.MOCHA
|
theme = ThemeType.MOCHA
|
||||||
setLogLevel("lifecycle")
|
setLogLevel("lifecycle")
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=1.6.0
|
project.version=1.8.1
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Gradle
|
# Gradle
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "1.4.32"
|
kotlin = "1.4.32"
|
||||||
ktor = "1.5.3"
|
ktor = "1.6.4"
|
||||||
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"
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
package io.bkbn.kompendium.auth
|
|
||||||
|
|
||||||
import io.ktor.auth.AuthenticationRouteSelector
|
|
||||||
import io.ktor.routing.Route
|
|
||||||
import io.bkbn.kompendium.path.CorePathCalculator
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
class AuthPathCalculator : CorePathCalculator() {
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
|
||||||
|
|
||||||
override fun handleCustomSelectors(route: Route, tail: String): String = when (route.selector) {
|
|
||||||
is AuthenticationRouteSelector -> {
|
|
||||||
logger.debug("Found authentication route selector ${route.selector}")
|
|
||||||
super.calculate(route.parent, tail)
|
|
||||||
}
|
|
||||||
else -> super.handleCustomSelectors(route, tail)
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,11 +7,14 @@ import io.ktor.auth.jwt.jwt
|
|||||||
import io.ktor.auth.jwt.JWTAuthenticationProvider
|
import io.ktor.auth.jwt.JWTAuthenticationProvider
|
||||||
import io.bkbn.kompendium.Kompendium
|
import io.bkbn.kompendium.Kompendium
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecSchemaSecurity
|
import io.bkbn.kompendium.models.oas.OpenApiSpecSchemaSecurity
|
||||||
|
import io.ktor.auth.AuthenticationRouteSelector
|
||||||
|
|
||||||
object KompendiumAuth {
|
object KompendiumAuth {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Kompendium.pathCalculator = AuthPathCalculator()
|
Kompendium.addCustomRouteHandler(AuthenticationRouteSelector::class) { route, tail ->
|
||||||
|
calculate(route.parent, tail)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Authentication.Configuration.notarizedBasic(
|
fun Authentication.Configuration.notarizedBasic(
|
||||||
|
@ -5,8 +5,10 @@ import io.bkbn.kompendium.models.meta.SchemaMap
|
|||||||
import io.bkbn.kompendium.models.oas.OpenApiSpec
|
import io.bkbn.kompendium.models.oas.OpenApiSpec
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
|
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
|
||||||
import io.bkbn.kompendium.models.oas.TypedSchema
|
import io.bkbn.kompendium.models.oas.TypedSchema
|
||||||
import io.bkbn.kompendium.path.CorePathCalculator
|
import io.bkbn.kompendium.path.IPathCalculator
|
||||||
import io.bkbn.kompendium.path.PathCalculator
|
import io.bkbn.kompendium.path.PathCalculator
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.RouteSelector
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +25,7 @@ object Kompendium {
|
|||||||
paths = mutableMapOf()
|
paths = mutableMapOf()
|
||||||
)
|
)
|
||||||
|
|
||||||
var pathCalculator: PathCalculator = CorePathCalculator()
|
fun calculatePath(route: Route) = PathCalculator.calculate(route)
|
||||||
|
|
||||||
fun resetSchema() {
|
fun resetSchema() {
|
||||||
openApiSpec = OpenApiSpec(
|
openApiSpec = OpenApiSpec(
|
||||||
@ -37,4 +39,12 @@ object Kompendium {
|
|||||||
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
|
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
|
||||||
cache = cache.plus(clazz.simpleName!! to schema)
|
cache = cache.plus(clazz.simpleName!! to schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T : RouteSelector> addCustomRouteHandler(
|
||||||
|
selector: KClass<T>,
|
||||||
|
handler: IPathCalculator.(Route, String) -> String
|
||||||
|
) {
|
||||||
|
PathCalculator.addCustomRouteHandler(selector, handler)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package io.bkbn.kompendium
|
package io.bkbn.kompendium
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.annotations.UndeclaredField
|
||||||
import io.bkbn.kompendium.models.meta.SchemaMap
|
import io.bkbn.kompendium.models.meta.SchemaMap
|
||||||
import io.bkbn.kompendium.models.oas.AnyOfReferencedSchema
|
import io.bkbn.kompendium.models.oas.AnyOfReferencedSchema
|
||||||
import io.bkbn.kompendium.models.oas.ArraySchema
|
import io.bkbn.kompendium.models.oas.ArraySchema
|
||||||
@ -7,6 +8,7 @@ import io.bkbn.kompendium.models.oas.DictionarySchema
|
|||||||
import io.bkbn.kompendium.models.oas.EnumSchema
|
import io.bkbn.kompendium.models.oas.EnumSchema
|
||||||
import io.bkbn.kompendium.models.oas.FormatSchema
|
import io.bkbn.kompendium.models.oas.FormatSchema
|
||||||
import io.bkbn.kompendium.models.oas.ObjectSchema
|
import io.bkbn.kompendium.models.oas.ObjectSchema
|
||||||
|
import io.bkbn.kompendium.models.oas.OpenApiSpecComponentSchema
|
||||||
import io.bkbn.kompendium.models.oas.ReferencedSchema
|
import io.bkbn.kompendium.models.oas.ReferencedSchema
|
||||||
import io.bkbn.kompendium.models.oas.SimpleSchema
|
import io.bkbn.kompendium.models.oas.SimpleSchema
|
||||||
import io.bkbn.kompendium.util.Helpers.COMPONENT_SLUG
|
import io.bkbn.kompendium.util.Helpers.COMPONENT_SLUG
|
||||||
@ -204,8 +206,14 @@ object Kontent {
|
|||||||
}
|
}
|
||||||
Pair(prop.name, propSchema)
|
Pair(prop.name, propSchema)
|
||||||
}
|
}
|
||||||
|
logger.debug("Looking for undeclared fields")
|
||||||
|
val undeclaredFieldMap = clazz.annotations.filterIsInstance<UndeclaredField>().associate {
|
||||||
|
val undeclaredType = it.clazz.createType()
|
||||||
|
newCache = generateKontent(undeclaredType, newCache)
|
||||||
|
it.field to ReferencedSchema(undeclaredType.getReferenceSlug())
|
||||||
|
}
|
||||||
logger.debug("$slug contains $fieldMap")
|
logger.debug("$slug contains $fieldMap")
|
||||||
val schema = ObjectSchema(fieldMap)
|
val schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
|
||||||
logger.debug("$slug schema: $schema")
|
logger.debug("$slug schema: $schema")
|
||||||
newCache.plus(slug to schema)
|
newCache.plus(slug to schema)
|
||||||
}
|
}
|
||||||
@ -238,7 +246,7 @@ object Kontent {
|
|||||||
val valClass = valType?.classifier as KClass<*>
|
val valClass = valType?.classifier as KClass<*>
|
||||||
val valClassName = valClass.simpleName
|
val valClassName = valClass.simpleName
|
||||||
val referenceName = genericNameAdapter(type, clazz)
|
val referenceName = genericNameAdapter(type, clazz)
|
||||||
val valueReference = when(valClass.isSealed) {
|
val valueReference = when (valClass.isSealed) {
|
||||||
true -> {
|
true -> {
|
||||||
val subTypes = gatherSubTypes(valType)
|
val subTypes = gatherSubTypes(valType)
|
||||||
AnyOfReferencedSchema(subTypes.map {
|
AnyOfReferencedSchema(subTypes.map {
|
||||||
|
@ -36,7 +36,7 @@ object Notarized {
|
|||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||||
): Route =
|
): Route =
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
|
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
|
||||||
val path = Kompendium.pathCalculator.calculate(this)
|
val path = Kompendium.calculatePath(this)
|
||||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
|
Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
|
||||||
return method(HttpMethod.Get) { handle(body) }
|
return method(HttpMethod.Get) { handle(body) }
|
||||||
@ -55,7 +55,7 @@ object Notarized {
|
|||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||||
): Route =
|
): Route =
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
|
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
|
||||||
val path = Kompendium.pathCalculator.calculate(this)
|
val path = Kompendium.calculatePath(this)
|
||||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
|
Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
|
||||||
return method(HttpMethod.Post) { handle(body) }
|
return method(HttpMethod.Post) { handle(body) }
|
||||||
@ -74,7 +74,7 @@ object Notarized {
|
|||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
||||||
): Route =
|
): Route =
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
|
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
|
||||||
val path = Kompendium.pathCalculator.calculate(this)
|
val path = Kompendium.calculatePath(this)
|
||||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
Kompendium.openApiSpec.paths[path]?.put =
|
Kompendium.openApiSpec.paths[path]?.put =
|
||||||
parseMethodInfo(info, paramType, requestType, responseType)
|
parseMethodInfo(info, paramType, requestType, responseType)
|
||||||
@ -93,7 +93,7 @@ object Notarized {
|
|||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||||
): Route =
|
): Route =
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
|
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
|
||||||
val path = Kompendium.pathCalculator.calculate(this)
|
val path = Kompendium.calculatePath(this)
|
||||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
|
Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
|
||||||
return method(HttpMethod.Delete) { handle(body) }
|
return method(HttpMethod.Delete) { handle(body) }
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package io.bkbn.kompendium.annotations
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Repeatable
|
||||||
|
annotation class UndeclaredField(val field: String, val clazz: KClass<*>)
|
@ -1,51 +0,0 @@
|
|||||||
package io.bkbn.kompendium.path
|
|
||||||
|
|
||||||
import io.ktor.routing.PathSegmentConstantRouteSelector
|
|
||||||
import io.ktor.routing.PathSegmentParameterRouteSelector
|
|
||||||
import io.ktor.routing.RootRouteSelector
|
|
||||||
import io.ktor.routing.Route
|
|
||||||
import io.ktor.util.InternalAPI
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default [PathCalculator] meant to be overridden as necessary
|
|
||||||
*/
|
|
||||||
open class CorePathCalculator : PathCalculator {
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
|
||||||
|
|
||||||
@OptIn(InternalAPI::class)
|
|
||||||
override fun calculate(
|
|
||||||
route: Route?,
|
|
||||||
tail: String
|
|
||||||
): String = when (route) {
|
|
||||||
null -> tail
|
|
||||||
else -> when (route.selector) {
|
|
||||||
is RootRouteSelector -> {
|
|
||||||
logger.debug("Root route detected, returning path: $tail")
|
|
||||||
tail
|
|
||||||
}
|
|
||||||
is PathSegmentParameterRouteSelector -> {
|
|
||||||
logger.debug("Found segment parameter ${route.selector}, continuing to parent")
|
|
||||||
val newTail = "/${route.selector}$tail"
|
|
||||||
calculate(route.parent, newTail)
|
|
||||||
}
|
|
||||||
is PathSegmentConstantRouteSelector -> {
|
|
||||||
logger.debug("Found segment constant ${route.selector}, continuing to parent")
|
|
||||||
val newTail = "/${route.selector}$tail"
|
|
||||||
calculate(route.parent, newTail)
|
|
||||||
}
|
|
||||||
else -> when (route.selector.javaClass.simpleName) {
|
|
||||||
"TrailingSlashRouteSelector" -> {
|
|
||||||
logger.debug("Found trailing slash route selector")
|
|
||||||
val newTail = tail.ifBlank { "/" }
|
|
||||||
calculate(route.parent, newTail)
|
|
||||||
}
|
|
||||||
else -> handleCustomSelectors(route, tail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleCustomSelectors(route: Route, tail: String): String = error("Unknown selector ${route.selector}")
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,13 @@
|
|||||||
|
package io.bkbn.kompendium.path
|
||||||
|
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.RouteSelector
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
interface IPathCalculator {
|
||||||
|
fun calculate(route: Route?, tail: String = ""): String
|
||||||
|
fun <T : RouteSelector> addCustomRouteHandler(
|
||||||
|
selector: KClass<T>,
|
||||||
|
handler: IPathCalculator.(Route, String) -> String
|
||||||
|
)
|
||||||
|
}
|
@ -1,20 +1,54 @@
|
|||||||
package io.bkbn.kompendium.path
|
package io.bkbn.kompendium.path
|
||||||
|
|
||||||
|
import io.ktor.routing.PathSegmentConstantRouteSelector
|
||||||
|
import io.ktor.routing.PathSegmentParameterRouteSelector
|
||||||
|
import io.ktor.routing.RootRouteSelector
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.RouteSelector
|
||||||
|
import io.ktor.routing.TrailingSlashRouteSelector
|
||||||
|
import io.ktor.util.InternalAPI
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extensible interface for calculating Ktor paths
|
* Responsible for calculating a url path from a provided [Route]
|
||||||
*/
|
*/
|
||||||
interface PathCalculator {
|
@OptIn(InternalAPI::class)
|
||||||
|
internal object PathCalculator: IPathCalculator {
|
||||||
|
|
||||||
/**
|
private val pathHandler: RouteHandlerMap = mutableMapOf()
|
||||||
* Core route calculation method
|
|
||||||
*/
|
|
||||||
fun calculate(route: Route?, tail: String = ""): String
|
|
||||||
|
|
||||||
/**
|
init {
|
||||||
* Used to handle any custom selectors that may be missed by the base route calculation
|
addCustomRouteHandler(RootRouteSelector::class) { _, tail -> tail }
|
||||||
*/
|
addCustomRouteHandler(PathSegmentParameterRouteSelector::class) { route, tail ->
|
||||||
fun handleCustomSelectors(route: Route, tail: String): String
|
val newTail = "/${route.selector}$tail"
|
||||||
|
calculate(route.parent, newTail)
|
||||||
|
}
|
||||||
|
addCustomRouteHandler(PathSegmentConstantRouteSelector::class) { route, tail ->
|
||||||
|
val newTail = "/${route.selector}$tail"
|
||||||
|
calculate(route.parent, newTail)
|
||||||
|
}
|
||||||
|
addCustomRouteHandler(TrailingSlashRouteSelector::class) { route, tail ->
|
||||||
|
val newTail = tail.ifBlank { "/" }
|
||||||
|
calculate(route.parent, newTail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(InternalAPI::class)
|
||||||
|
override fun calculate(
|
||||||
|
route: Route?,
|
||||||
|
tail: String
|
||||||
|
): String = when (route) {
|
||||||
|
null -> tail
|
||||||
|
else -> when (pathHandler.containsKey(route.selector::class)) {
|
||||||
|
true -> pathHandler[route.selector::class]!!.invoke(this, route, tail)
|
||||||
|
else -> error("No handler has been registered for ${route.selector}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : RouteSelector> addCustomRouteHandler(
|
||||||
|
selector: KClass<T>,
|
||||||
|
handler: IPathCalculator.(Route, String) -> String
|
||||||
|
) {
|
||||||
|
pathHandler[selector] = handler
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package io.bkbn.kompendium.path
|
||||||
|
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.RouteSelector
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
typealias RouteHandlerMap = MutableMap<KClass<out RouteSelector>, IPathCalculator.(Route, String) -> String>
|
@ -43,6 +43,7 @@ import io.bkbn.kompendium.util.simpleGenericResponse
|
|||||||
import io.bkbn.kompendium.util.statusPageModule
|
import io.bkbn.kompendium.util.statusPageModule
|
||||||
import io.bkbn.kompendium.util.statusPageMultiExceptions
|
import io.bkbn.kompendium.util.statusPageMultiExceptions
|
||||||
import io.bkbn.kompendium.util.trailingSlash
|
import io.bkbn.kompendium.util.trailingSlash
|
||||||
|
import io.bkbn.kompendium.util.undeclaredType
|
||||||
import io.bkbn.kompendium.util.withDefaultParameter
|
import io.bkbn.kompendium.util.withDefaultParameter
|
||||||
import io.bkbn.kompendium.util.withExamples
|
import io.bkbn.kompendium.util.withExamples
|
||||||
|
|
||||||
@ -556,6 +557,22 @@ internal class KompendiumTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Can add an undeclared field`() {
|
||||||
|
withTestApplication({
|
||||||
|
kotlinxConfigModule()
|
||||||
|
docs()
|
||||||
|
undeclaredType()
|
||||||
|
}) {
|
||||||
|
// do
|
||||||
|
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||||
|
|
||||||
|
// expect
|
||||||
|
val expected = getFileSnapshot("undeclared_field.json").trim()
|
||||||
|
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val oas = Kompendium.openApiSpec.copy(
|
private val oas = Kompendium.openApiSpec.copy(
|
||||||
info = OpenApiSpecInfo(
|
info = OpenApiSpecInfo(
|
||||||
title = "Test API",
|
title = "Test API",
|
||||||
|
@ -4,6 +4,7 @@ import java.util.UUID
|
|||||||
import io.bkbn.kompendium.annotations.KompendiumField
|
import io.bkbn.kompendium.annotations.KompendiumField
|
||||||
import io.bkbn.kompendium.annotations.KompendiumParam
|
import io.bkbn.kompendium.annotations.KompendiumParam
|
||||||
import io.bkbn.kompendium.annotations.ParamType
|
import io.bkbn.kompendium.annotations.ParamType
|
||||||
|
import io.bkbn.kompendium.annotations.UndeclaredField
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
|
||||||
@ -94,3 +95,11 @@ sealed interface Flibbity<T>
|
|||||||
|
|
||||||
data class Gibbity<T>(val a: T): Flibbity<T>
|
data class Gibbity<T>(val a: T): Flibbity<T>
|
||||||
data class Bibbity<T>(val b: String, val f: T) : Flibbity<T>
|
data class Bibbity<T>(val b: String, val f: T) : Flibbity<T>
|
||||||
|
|
||||||
|
enum class Hehe {
|
||||||
|
HAHA,
|
||||||
|
HOHO
|
||||||
|
}
|
||||||
|
|
||||||
|
@UndeclaredField("nowYouDont", Hehe::class)
|
||||||
|
data class Mysterious(val nowYouSeeMe: String)
|
||||||
|
@ -340,6 +340,16 @@ fun Application.genericPolymorphicResponseMultipleImpls() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Application.undeclaredType() {
|
||||||
|
routing {
|
||||||
|
route("/test/polymorphic") {
|
||||||
|
notarizedGet(TestResponseInfo.undeclaredResponseType) {
|
||||||
|
call.respond(HttpStatusCode.OK, Mysterious("hi"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Application.simpleGenericResponse() {
|
fun Application.simpleGenericResponse() {
|
||||||
routing {
|
routing {
|
||||||
route("/test/polymorphic") {
|
route("/test/polymorphic") {
|
||||||
|
@ -103,6 +103,11 @@ object TestResponseInfo {
|
|||||||
description = "Polymorphic with generics but like... crazier",
|
description = "Polymorphic with generics but like... crazier",
|
||||||
responseInfo = simpleOkResponse()
|
responseInfo = simpleOkResponse()
|
||||||
)
|
)
|
||||||
|
val undeclaredResponseType = GetInfo<Unit, Mysterious>(
|
||||||
|
summary = "spooky class",
|
||||||
|
description = "break this glass in scenario of emergency",
|
||||||
|
responseInfo = simpleOkResponse()
|
||||||
|
)
|
||||||
val genericResponse = GetInfo<Unit, TestGeneric<Int>>(
|
val genericResponse = GetInfo<Unit, TestGeneric<Int>>(
|
||||||
summary = "Single Generic",
|
summary = "Single Generic",
|
||||||
description = "Simple generic data class",
|
description = "Simple generic data class",
|
||||||
|
73
kompendium-core/src/test/resources/undeclared_field.json
Normal file
73
kompendium-core/src/test/resources/undeclared_field.json
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"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/polymorphic" : {
|
||||||
|
"get" : {
|
||||||
|
"tags" : [ ],
|
||||||
|
"summary" : "spooky class",
|
||||||
|
"description" : "break this glass in scenario of emergency",
|
||||||
|
"parameters" : [ ],
|
||||||
|
"responses" : {
|
||||||
|
"200" : {
|
||||||
|
"description" : "A successful endeavor",
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/Mysterious"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated" : false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components" : {
|
||||||
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"Hehe" : {
|
||||||
|
"type" : "string",
|
||||||
|
"enum" : [ "HAHA", "HOHO" ]
|
||||||
|
},
|
||||||
|
"Mysterious" : {
|
||||||
|
"type" : "object",
|
||||||
|
"properties" : {
|
||||||
|
"nowYouSeeMe" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
},
|
||||||
|
"nowYouDont" : {
|
||||||
|
"$ref" : "#/components/schemas/Hehe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes" : { }
|
||||||
|
},
|
||||||
|
"security" : [ ],
|
||||||
|
"tags" : [ ]
|
||||||
|
}
|
@ -19,6 +19,7 @@ import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfo
|
|||||||
import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfoWithThrowable
|
import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfoWithThrowable
|
||||||
import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePostInfo
|
import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePostInfo
|
||||||
import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePutInfo
|
import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePutInfo
|
||||||
|
import io.bkbn.kompendium.playground.PlaygroundToC.testUndeclaredFields
|
||||||
import io.bkbn.kompendium.routes.openApi
|
import io.bkbn.kompendium.routes.openApi
|
||||||
import io.bkbn.kompendium.routes.redoc
|
import io.bkbn.kompendium.routes.redoc
|
||||||
import io.bkbn.kompendium.swagger.swaggerUI
|
import io.bkbn.kompendium.swagger.swaggerUI
|
||||||
@ -138,5 +139,10 @@ fun Application.mainModule() {
|
|||||||
error("bad things just happened")
|
error("bad things just happened")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
route("/undeclared") {
|
||||||
|
notarizedGet(testUndeclaredFields) {
|
||||||
|
call.respondText { "hi" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package io.bkbn.kompendium.playground
|
|||||||
import io.bkbn.kompendium.annotations.KompendiumField
|
import io.bkbn.kompendium.annotations.KompendiumField
|
||||||
import io.bkbn.kompendium.annotations.KompendiumParam
|
import io.bkbn.kompendium.annotations.KompendiumParam
|
||||||
import io.bkbn.kompendium.annotations.ParamType
|
import io.bkbn.kompendium.annotations.ParamType
|
||||||
|
import io.bkbn.kompendium.annotations.UndeclaredField
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
data class ExampleParams(
|
data class ExampleParams(
|
||||||
@ -33,3 +34,11 @@ data class ExceptionResponse(val message: String)
|
|||||||
data class ExampleCreatedResponse(val id: Int, val c: String)
|
data class ExampleCreatedResponse(val id: Int, val c: String)
|
||||||
|
|
||||||
data class DateTimeWrapper(val dt: DateTime)
|
data class DateTimeWrapper(val dt: DateTime)
|
||||||
|
|
||||||
|
enum class Testerino {
|
||||||
|
First,
|
||||||
|
Second
|
||||||
|
}
|
||||||
|
|
||||||
|
@UndeclaredField("type", Testerino::class)
|
||||||
|
data class SimpleYetMysterious(val exists: Boolean)
|
||||||
|
@ -109,4 +109,13 @@ object PlaygroundToC {
|
|||||||
),
|
),
|
||||||
securitySchemes = setOf("basic")
|
securitySchemes = setOf("basic")
|
||||||
)
|
)
|
||||||
|
val testUndeclaredFields = MethodInfo.GetInfo<Unit, SimpleYetMysterious>(
|
||||||
|
summary = "Tests adding undeclared fields",
|
||||||
|
description = "vvv mysterious",
|
||||||
|
tags = setOf("mysterious"),
|
||||||
|
responseInfo = ResponseInfo(
|
||||||
|
status = HttpStatusCode.OK,
|
||||||
|
description = "good tings"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
rootProject.name = "kompendium"
|
rootProject.name = "kompendium"
|
||||||
|
|
||||||
include("kompendium-core")
|
include("kompendium-core")
|
||||||
include("kompendium-auth")
|
include("kompendium-auth")
|
||||||
include("kompendium-swagger-ui")
|
include("kompendium-swagger-ui")
|
||||||
|
Reference in New Issue
Block a user